templates2.Admin.Next/tests/e2e/smoke.mjs
w4c-templates c64f7ffa9e
Some checks failed
CI / quality (push) Has been cancelled
chore(template): publish Admin.Next
2026-10-11 23:25:34 +03:00

114 lines
6.4 KiB
JavaScript

#!/usr/bin/env node
/**
* End-to-end smoke test — drives the running server over HTTP (no browser needed).
* Start the app first (`npm start`), then: QA_BASE_URL=http://127.0.0.1:3000 node tests/e2e/smoke.mjs
* The QA harness runs this automatically while the server is up.
*
* Covers the CRM core: staff guard, customers CRUD, CSV preview + commit, export, audit trail
* and the read-only viewer role.
*/
const BASE = (process.env.QA_BASE_URL || 'http://127.0.0.1:3000').replace(/\/$/, '');
const jar = new Map();
const failures = [];
function store(res) {
const list = typeof res.headers.getSetCookie === 'function' ? res.headers.getSetCookie() : [];
for (const raw of list) {
const pair = raw.split(';')[0];
const eq = pair.indexOf('=');
if (eq > 0) jar.set(pair.slice(0, eq), pair.slice(eq + 1));
}
}
async function raw(path, { method = 'GET', body, headers = {} } = {}) {
const h = { ...headers };
if (body !== undefined) h['Content-Type'] = 'application/json';
if (jar.size) h.cookie = [...jar.entries()].map(([k, v]) => `${k}=${v}`).join('; ');
const res = await fetch(BASE + path, { method, headers: h, body: body === undefined ? undefined : JSON.stringify(body), redirect: 'manual' });
store(res);
return res;
}
async function req(path, opts) {
const res = await raw(path, opts);
const json = await res.clone().json().catch(() => null);
return { status: res.status, json };
}
function check(name, cond, detail = '') {
if (cond) console.log(` ok ${name}`);
else { console.log(` FAIL ${name}${detail ? ` — ${detail}` : ''}`); failures.push(name); }
}
async function run() {
const stamp = Date.now().toString(36);
const newUser = `smoke_${stamp}@example.com`;
const customer = { name: `Smoke ${stamp}`, email: `smoke_${stamp}@example.com`, company: 'Acme', status: 'lead' };
const health = await req('/api/health');
check('health 200', health.status === 200 && health.json?.status === 'ok', `status=${health.status}`);
const reg = await req('/api/auth/register', { method: 'POST', body: { email: newUser, password: 'password123', name: 'Smoke' } });
check('register 201', reg.status === 201, `status=${reg.status}`);
const blocked = await req('/api/admin/customers');
check('non-staff blocked from CRM API', blocked.status === 403, `status=${blocked.status}`);
await req('/api/auth/logout', { method: 'POST' });
const adminLogin = await req('/api/auth/login', { method: 'POST', body: { email: 'admin@example.com', password: 'password' } });
check('seeded admin can sign in', adminLogin.status === 200 && adminLogin.json?.user?.role === 'admin', `status=${adminLogin.status}`);
const seeded = await req('/api/admin/customers?pageSize=50');
check('admin lists seeded customers', seeded.status === 200 && (seeded.json?.total ?? 0) >= 20, `status=${seeded.status} total=${seeded.json?.total}`);
const created = await req('/api/admin/customers', { method: 'POST', body: customer });
check('create customer 201', created.status === 201, `status=${created.status}`);
const id = created.json?.customer?.id;
const found = await req(`/api/admin/customers?q=${encodeURIComponent(customer.email)}`);
check('created customer appears in the list', found.status === 200 && found.json?.items?.some((c) => c.email === customer.email), `status=${found.status}`);
const detail = await req(`/api/admin/customers/${id}`);
check('customer detail 200', detail.status === 200 && detail.json?.customer?.email === customer.email, `status=${detail.status}`);
const patched = await req(`/api/admin/customers/${id}`, { method: 'PATCH', body: { ...customer, status: 'churned' } });
check('update customer 200', patched.status === 200 && patched.json?.customer?.status === 'churned', `status=${patched.status}`);
const importCsv = `name,email,company,status\nImported ${stamp},imported_${stamp}@example.com,Globex,lead\nBroken,bad-email,Globex,active\n`;
const preview = await req('/api/admin/import/customers', { method: 'POST', body: { csv: importCsv, commit: false } });
check('csv preview reports errors and valid rows', preview.status === 200 && preview.json?.preview === true && preview.json?.valid === 1 && preview.json?.errors?.length === 1, `status=${preview.status} valid=${preview.json?.valid} errors=${preview.json?.errors?.length}`);
const committed = await req('/api/admin/import/customers', { method: 'POST', body: { csv: importCsv, commit: true } });
check('csv commit imports the valid row', committed.status === 200 && (committed.json?.imported ?? 0) >= 1, `status=${committed.status} imported=${committed.json?.imported}`);
const exported = await raw('/api/admin/export/customers');
const csvBody = await exported.text();
check('csv export is a text/csv attachment', exported.status === 200 && /text\/csv/.test(exported.headers.get('content-type') || '') && csvBody.includes('email') && csvBody.includes(customer.email), `status=${exported.status}`);
const audit = await req('/api/admin/audit?action=customer.create');
check('audit trail records the create', audit.status === 200 && (audit.json?.entries?.length ?? 0) >= 1, `status=${audit.status} entries=${audit.json?.entries?.length}`);
const removed = await req(`/api/admin/customers/${id}`, { method: 'DELETE' });
check('admin can delete a customer', removed.status === 200, `status=${removed.status}`);
await req('/api/auth/logout', { method: 'POST' });
const viewerLogin = await req('/api/auth/login', { method: 'POST', body: { email: 'viewer@example.com', password: 'password' } });
check('seeded viewer can sign in', viewerLogin.status === 200 && viewerLogin.json?.user?.role === 'viewer', `status=${viewerLogin.status}`);
const viewerRead = await req('/api/admin/customers');
check('viewer can read the registry', viewerRead.status === 200, `status=${viewerRead.status}`);
const viewerWrite = await req('/api/admin/customers', { method: 'POST', body: { name: 'Nope', email: `viewer_${stamp}@example.com` } });
check('viewer cannot create (read-only)', viewerWrite.status === 403, `status=${viewerWrite.status}`);
const viewerDelete = await req(`/api/admin/customers/${seeded.json?.items?.[0]?.id}`, { method: 'DELETE' });
check('viewer cannot delete (read-only)', viewerDelete.status === 403, `status=${viewerDelete.status}`);
console.log(failures.length ? `\n${failures.length} check(s) failed` : '\nAll e2e checks passed');
process.exit(failures.length ? 1 : 0);
}
run().catch((e) => { console.error(e); process.exit(1); });