114 lines
6.4 KiB
JavaScript
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); });
|