run_auto_test.mjs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. const base = 'http://localhost:3101/api';
  2. const results = [];
  3. function pass(name, detail = '') {
  4. results.push({ name, ok: true, detail });
  5. console.log(`[PASS] ${name}${detail ? ' - ' + detail : ''}`);
  6. }
  7. function fail(name, detail = '') {
  8. results.push({ name, ok: false, detail });
  9. console.log(`[FAIL] ${name}${detail ? ' - ' + detail : ''}`);
  10. }
  11. async function login(phone) {
  12. const r = await fetch(`${base}/auth/login`, {
  13. method: 'POST',
  14. headers: { 'Content-Type': 'application/json' },
  15. body: JSON.stringify({ phone, password: '123456', rememberMe: false }),
  16. });
  17. const j = await r.json();
  18. if (!j.success) throw new Error(`login ${email}: ${JSON.stringify(j.error)}`);
  19. return j.data;
  20. }
  21. async function get(path, token) {
  22. const r = await fetch(`${base}${path}`, {
  23. headers: token ? { Authorization: `Bearer ${token}` } : {},
  24. });
  25. let j = null;
  26. try { j = await r.json(); } catch { j = null; }
  27. return { status: r.status, j };
  28. }
  29. async function post(path, token, body) {
  30. const r = await fetch(`${base}${path}`, {
  31. method: 'POST',
  32. headers: {
  33. 'Content-Type': 'application/json',
  34. ...(token ? { Authorization: `Bearer ${token}` } : {}),
  35. },
  36. body: body ? JSON.stringify(body) : undefined,
  37. });
  38. let j = null;
  39. try { j = await r.json(); } catch { j = null; }
  40. return { status: r.status, j };
  41. }
  42. console.log('\n=== Auto API Test Suite ===\n');
  43. // 1. Health
  44. try {
  45. const h = await get('/health');
  46. if (h.j?.success && h.j.data?.status === 'ok') {
  47. pass('health check', `groups=${h.j.data.database?.counts?.groupChat ?? '?'}`);
  48. } else fail('health check', JSON.stringify(h.j));
  49. } catch (e) {
  50. fail('health check', e.message);
  51. }
  52. // 2. Auth required
  53. try {
  54. const g = await get('/qiwe/groups');
  55. if (g.status === 401) pass('groups without auth returns 401');
  56. else fail('groups without auth returns 401', `status=${g.status}`);
  57. } catch (e) {
  58. fail('groups without auth returns 401', e.message);
  59. }
  60. // 3. Logins
  61. let director, supervisor, manager;
  62. try {
  63. director = await login('13800000001');
  64. pass('login director', director.user.email);
  65. } catch (e) { fail('login director', e.message); }
  66. try {
  67. supervisor = await login('13800000002');
  68. pass('login supervisor', `region=${supervisor.user.regionCode}`);
  69. } catch (e) { fail('login supervisor', e.message); }
  70. try {
  71. manager = await login('13800000003');
  72. pass('login store_manager', `storeId=${manager.user.storeId}`);
  73. } catch (e) { fail('login store_manager', e.message); }
  74. if (!director?.token) {
  75. console.log('\n=== Summary: aborted (no director token) ===');
  76. process.exit(1);
  77. }
  78. const t = director.token;
  79. // 4. Director scope
  80. const dirDefault = await get('/dashboard/overview', t);
  81. if (dirDefault.j?.data?.scope?.level === 'city') {
  82. pass('director default scope is city', dirDefault.j.data.scope.label || '');
  83. } else fail('director default scope is city', dirDefault.j?.data?.scope?.level);
  84. const dirGlobal = await get('/dashboard/overview?scopeLevel=global', t);
  85. if (dirGlobal.j?.data?.scope?.level === 'global') {
  86. pass('director can view global overview');
  87. } else fail('director can view global overview', dirGlobal.j?.data?.scope?.level);
  88. // 5. Supervisor scope
  89. if (supervisor?.token) {
  90. const supGlobal = await get('/dashboard/overview?scopeLevel=global', supervisor.token);
  91. if (supGlobal.status === 403 || supGlobal.j?.error?.code === 'SCOPE_DENIED') {
  92. pass('supervisor blocked from global');
  93. } else fail('supervisor blocked from global', `status=${supGlobal.status}`);
  94. const supRegion = await get('/dashboard/overview?scopeLevel=region', supervisor.token);
  95. if (supRegion.j?.data?.scope?.level === 'region') {
  96. pass('supervisor region scope', supRegion.j.data.scope.regionCode);
  97. } else fail('supervisor region scope', supRegion.j?.data?.scope?.level);
  98. }
  99. // 6. Manager scope
  100. if (manager?.token) {
  101. const mgrGlobal = await get('/dashboard/overview?scopeLevel=global', manager.token);
  102. if (mgrGlobal.status === 403 || mgrGlobal.j?.error?.code === 'SCOPE_DENIED') {
  103. pass('manager blocked from global');
  104. } else fail('manager blocked from global', `status=${mgrGlobal.status}`);
  105. }
  106. // 7. Groups by scope
  107. const gGlobal = await get('/qiwe/groups?scopeLevel=global', t);
  108. const gStore = await get('/qiwe/groups', t);
  109. const globalCount = gGlobal.j?.data?.total ?? 0;
  110. const storeCount = gStore.j?.data?.total ?? 0;
  111. pass('groups global count', String(globalCount));
  112. pass('groups default store count', String(storeCount));
  113. if (globalCount >= storeCount) {
  114. pass('global groups >= store groups');
  115. } else fail('global groups >= store groups', `${globalCount} vs ${storeCount}`);
  116. // 8. Group fields
  117. const sample = gGlobal.j?.data?.groups?.[0];
  118. if (sample?.healthGrade && sample?.lifecyclePhase) {
  119. pass('group has healthGrade and lifecyclePhase', `${sample.healthGrade}/${sample.lifecyclePhase}`);
  120. } else if (globalCount === 0) {
  121. pass('group fields skipped', 'no groups in db');
  122. } else {
  123. fail('group has healthGrade and lifecyclePhase', JSON.stringify(sample));
  124. }
  125. // 9. Communities
  126. const comm = await get('/qiwe/communities', t);
  127. const commTotal = comm.j?.data?.total ?? 0;
  128. if (commTotal > 0) pass('communities list', `total=${commTotal}`);
  129. else fail('communities list', 'total=0');
  130. // 10. Dashboard lifecycle distribution
  131. const life = dirDefault.j?.data?.lifecycleDistribution;
  132. if (Array.isArray(life) && life.length >= 4) {
  133. pass('dashboard lifecycleDistribution', `phases=${life.length}`);
  134. } else fail('dashboard lifecycleDistribution', String(life?.length));
  135. // 11. QiWe sync-groups
  136. const sync = await post('/qiwe/sync-groups');
  137. if (sync.j?.success) {
  138. const s = sync.j.data?.sync;
  139. pass('sync-groups API', `created=${s?.created} updated=${s?.updated} errors=${s?.errors?.length ?? 0}`);
  140. } else {
  141. const msg = sync.j?.error?.message || sync.status;
  142. if (String(msg).includes('guid') || String(msg).includes('不在线')) {
  143. fail('sync-groups API', `QiWe offline: ${msg}`);
  144. } else {
  145. fail('sync-groups API', msg);
  146. }
  147. }
  148. // 12. QiWe sync-messages
  149. const msgSync = await post('/qiwe/sync-messages', null, { limit: 20, maxPages: 3 });
  150. if (msgSync.j?.success) {
  151. const s = msgSync.j.data?.sync;
  152. const dbMsg = msgSync.j.data?.database?.counts?.message;
  153. pass('sync-messages API', `created=${s?.created} skipped=${s?.skipped} total=${dbMsg}`);
  154. } else {
  155. const msg = msgSync.j?.error?.message || msgSync.status;
  156. if (String(msg).includes('不在线') || String(msg).includes('QiWe')) {
  157. fail('sync-messages API', `QiWe: ${msg}`);
  158. } else {
  159. fail('sync-messages API', msg);
  160. }
  161. }
  162. // 13. Messages list (if groups exist)
  163. const sampleRoom = gGlobal.j?.data?.groups?.[0]?.roomId;
  164. if (sampleRoom) {
  165. const msgs = await get(`/qiwe/messages?roomId=${encodeURIComponent(sampleRoom)}&limit=5`, t);
  166. if (msgs.j?.success && Array.isArray(msgs.j.data?.messages)) {
  167. pass('messages list API', `room=${sampleRoom} total=${msgs.j.data.total}`);
  168. } else {
  169. fail('messages list API', JSON.stringify(msgs.j?.error));
  170. }
  171. } else if (globalCount === 0) {
  172. pass('messages list API skipped', 'no groups');
  173. } else {
  174. fail('messages list API', 'no sample room');
  175. }
  176. // 14. Org sync
  177. const org = await post('/qiwe/org/sync', t);
  178. if (org.j?.success) {
  179. pass('org sync', `members=${org.j.data?.members} source=${org.j.data?.source}`);
  180. } else {
  181. fail('org sync', org.j?.error?.message || org.status);
  182. }
  183. // 15. Risk API not implemented yet (expected 404)
  184. const risk = await get('/risk/work-orders', t);
  185. if (risk.status === 404) {
  186. pass('risk API not implemented (404 expected)', 'frontend still uses mock');
  187. } else if (risk.j?.success) {
  188. pass('risk work-orders API exists', `total=${risk.j.data?.total ?? '?'}`);
  189. } else {
  190. pass('risk API response', `status=${risk.status}`);
  191. }
  192. // Summary
  193. console.log('\n=== Summary ===');
  194. const passed = results.filter((r) => r.ok).length;
  195. const failed = results.filter((r) => !r.ok);
  196. console.log(`Passed: ${passed}/${results.length}, Failed: ${failed.length}`);
  197. if (failed.length) {
  198. console.log('\nFailed tests:');
  199. failed.forEach((f) => console.log(` - ${f.name}: ${f.detail}`));
  200. process.exit(1);
  201. }
  202. process.exit(0);