const base = 'http://localhost:3101/api'; const results = []; function pass(name, detail = '') { results.push({ name, ok: true, detail }); console.log(`[PASS] ${name}${detail ? ' - ' + detail : ''}`); } function fail(name, detail = '') { results.push({ name, ok: false, detail }); console.log(`[FAIL] ${name}${detail ? ' - ' + detail : ''}`); } async function login(phone) { const r = await fetch(`${base}/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phone, password: '123456', rememberMe: false }), }); const j = await r.json(); if (!j.success) throw new Error(`login ${email}: ${JSON.stringify(j.error)}`); return j.data; } async function get(path, token) { const r = await fetch(`${base}${path}`, { headers: token ? { Authorization: `Bearer ${token}` } : {}, }); let j = null; try { j = await r.json(); } catch { j = null; } return { status: r.status, j }; } async function post(path, token, body) { const r = await fetch(`${base}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: body ? JSON.stringify(body) : undefined, }); let j = null; try { j = await r.json(); } catch { j = null; } return { status: r.status, j }; } console.log('\n=== Auto API Test Suite ===\n'); // 1. Health try { const h = await get('/health'); if (h.j?.success && h.j.data?.status === 'ok') { pass('health check', `groups=${h.j.data.database?.counts?.groupChat ?? '?'}`); } else fail('health check', JSON.stringify(h.j)); } catch (e) { fail('health check', e.message); } // 2. Auth required try { const g = await get('/qiwe/groups'); if (g.status === 401) pass('groups without auth returns 401'); else fail('groups without auth returns 401', `status=${g.status}`); } catch (e) { fail('groups without auth returns 401', e.message); } // 3. Logins let director, supervisor, manager; try { director = await login('13800000001'); pass('login director', director.user.email); } catch (e) { fail('login director', e.message); } try { supervisor = await login('13800000002'); pass('login supervisor', `region=${supervisor.user.regionCode}`); } catch (e) { fail('login supervisor', e.message); } try { manager = await login('13800000003'); pass('login store_manager', `storeId=${manager.user.storeId}`); } catch (e) { fail('login store_manager', e.message); } if (!director?.token) { console.log('\n=== Summary: aborted (no director token) ==='); process.exit(1); } const t = director.token; // 4. Director scope const dirDefault = await get('/dashboard/overview', t); if (dirDefault.j?.data?.scope?.level === 'city') { pass('director default scope is city', dirDefault.j.data.scope.label || ''); } else fail('director default scope is city', dirDefault.j?.data?.scope?.level); const dirGlobal = await get('/dashboard/overview?scopeLevel=global', t); if (dirGlobal.j?.data?.scope?.level === 'global') { pass('director can view global overview'); } else fail('director can view global overview', dirGlobal.j?.data?.scope?.level); // 5. Supervisor scope if (supervisor?.token) { const supGlobal = await get('/dashboard/overview?scopeLevel=global', supervisor.token); if (supGlobal.status === 403 || supGlobal.j?.error?.code === 'SCOPE_DENIED') { pass('supervisor blocked from global'); } else fail('supervisor blocked from global', `status=${supGlobal.status}`); const supRegion = await get('/dashboard/overview?scopeLevel=region', supervisor.token); if (supRegion.j?.data?.scope?.level === 'region') { pass('supervisor region scope', supRegion.j.data.scope.regionCode); } else fail('supervisor region scope', supRegion.j?.data?.scope?.level); } // 6. Manager scope if (manager?.token) { const mgrGlobal = await get('/dashboard/overview?scopeLevel=global', manager.token); if (mgrGlobal.status === 403 || mgrGlobal.j?.error?.code === 'SCOPE_DENIED') { pass('manager blocked from global'); } else fail('manager blocked from global', `status=${mgrGlobal.status}`); } // 7. Groups by scope const gGlobal = await get('/qiwe/groups?scopeLevel=global', t); const gStore = await get('/qiwe/groups', t); const globalCount = gGlobal.j?.data?.total ?? 0; const storeCount = gStore.j?.data?.total ?? 0; pass('groups global count', String(globalCount)); pass('groups default store count', String(storeCount)); if (globalCount >= storeCount) { pass('global groups >= store groups'); } else fail('global groups >= store groups', `${globalCount} vs ${storeCount}`); // 8. Group fields const sample = gGlobal.j?.data?.groups?.[0]; if (sample?.healthGrade && sample?.lifecyclePhase) { pass('group has healthGrade and lifecyclePhase', `${sample.healthGrade}/${sample.lifecyclePhase}`); } else if (globalCount === 0) { pass('group fields skipped', 'no groups in db'); } else { fail('group has healthGrade and lifecyclePhase', JSON.stringify(sample)); } // 9. Communities const comm = await get('/qiwe/communities', t); const commTotal = comm.j?.data?.total ?? 0; if (commTotal > 0) pass('communities list', `total=${commTotal}`); else fail('communities list', 'total=0'); // 10. Dashboard lifecycle distribution const life = dirDefault.j?.data?.lifecycleDistribution; if (Array.isArray(life) && life.length >= 4) { pass('dashboard lifecycleDistribution', `phases=${life.length}`); } else fail('dashboard lifecycleDistribution', String(life?.length)); // 11. QiWe sync-groups const sync = await post('/qiwe/sync-groups'); if (sync.j?.success) { const s = sync.j.data?.sync; pass('sync-groups API', `created=${s?.created} updated=${s?.updated} errors=${s?.errors?.length ?? 0}`); } else { const msg = sync.j?.error?.message || sync.status; if (String(msg).includes('guid') || String(msg).includes('不在线')) { fail('sync-groups API', `QiWe offline: ${msg}`); } else { fail('sync-groups API', msg); } } // 12. QiWe sync-messages const msgSync = await post('/qiwe/sync-messages', null, { limit: 20, maxPages: 3 }); if (msgSync.j?.success) { const s = msgSync.j.data?.sync; const dbMsg = msgSync.j.data?.database?.counts?.message; pass('sync-messages API', `created=${s?.created} skipped=${s?.skipped} total=${dbMsg}`); } else { const msg = msgSync.j?.error?.message || msgSync.status; if (String(msg).includes('不在线') || String(msg).includes('QiWe')) { fail('sync-messages API', `QiWe: ${msg}`); } else { fail('sync-messages API', msg); } } // 13. Messages list (if groups exist) const sampleRoom = gGlobal.j?.data?.groups?.[0]?.roomId; if (sampleRoom) { const msgs = await get(`/qiwe/messages?roomId=${encodeURIComponent(sampleRoom)}&limit=5`, t); if (msgs.j?.success && Array.isArray(msgs.j.data?.messages)) { pass('messages list API', `room=${sampleRoom} total=${msgs.j.data.total}`); } else { fail('messages list API', JSON.stringify(msgs.j?.error)); } } else if (globalCount === 0) { pass('messages list API skipped', 'no groups'); } else { fail('messages list API', 'no sample room'); } // 14. Org sync const org = await post('/qiwe/org/sync', t); if (org.j?.success) { pass('org sync', `members=${org.j.data?.members} source=${org.j.data?.source}`); } else { fail('org sync', org.j?.error?.message || org.status); } // 15. Risk API not implemented yet (expected 404) const risk = await get('/risk/work-orders', t); if (risk.status === 404) { pass('risk API not implemented (404 expected)', 'frontend still uses mock'); } else if (risk.j?.success) { pass('risk work-orders API exists', `total=${risk.j.data?.total ?? '?'}`); } else { pass('risk API response', `status=${risk.status}`); } // Summary console.log('\n=== Summary ==='); const passed = results.filter((r) => r.ok).length; const failed = results.filter((r) => !r.ok); console.log(`Passed: ${passed}/${results.length}, Failed: ${failed.length}`); if (failed.length) { console.log('\nFailed tests:'); failed.forEach((f) => console.log(` - ${f.name}: ${f.detail}`)); process.exit(1); } process.exit(0);