smoke-app-auth-learning.mjs 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. #!/usr/bin/env node
  2. import { randomBytes } from 'node:crypto';
  3. const APP_ID = process.env.XIAOSHU_PARSE_APP_ID || '7pIbDBJmKx_main';
  4. const MASTER_KEY = process.env.XIAOSHU_MASTER_KEY || '';
  5. const PARSE_URL = (process.env.XIAOSHU_PARSE_URL || 'https://server.xiaoshu.pro/parse').replace(/\/$/, '');
  6. const FUNCTION_URL = (process.env.XIAOSHU_FUNCTION_URL || `${new URL(PARSE_URL).origin}/api/functions`).replace(/\/$/, '');
  7. const COMPANY_ID = '7pIbDBJmKx';
  8. if (!MASTER_KEY) throw new Error('缺少 XIAOSHU_MASTER_KEY');
  9. const appHeaders = { 'X-Parse-Application-Id': APP_ID, 'Content-Type': 'application/json' };
  10. const masterHeaders = { ...appHeaders, 'X-Parse-Master-Key': MASTER_KEY };
  11. const company = { __type: 'Pointer', className: 'Company', objectId: COMPANY_ID };
  12. const userIds = [];
  13. async function request(url, init = {}, expected = 200) {
  14. const response = await fetch(url, init);
  15. const payload = await response.json().catch(() => ({}));
  16. if (response.status !== expected) throw new Error(`${url} 期望 ${expected},实际 ${response.status}: ${payload.retmsg || payload.error || ''}`);
  17. return payload;
  18. }
  19. async function master(path, init = {}, expected = 200) {
  20. return request(`${PARSE_URL}${path}`, { ...init, headers: { ...masterHeaders, ...(init.headers || {}) } }, expected);
  21. }
  22. async function cloud(token, action, params = {}, expected = 200) {
  23. return request(`${FUNCTION_URL}/xiaoshu/app/gateway`, {
  24. method: 'POST', headers: appHeaders, body: JSON.stringify({ ...(token ? { token } : {}), params: { action, ...params } }),
  25. }, expected);
  26. }
  27. async function first(className, where, keys) {
  28. const query = new URLSearchParams({ where: JSON.stringify(where), limit: '1', keys });
  29. return (await master(`/classes/${className}?${query}`)).results?.[0] || null;
  30. }
  31. try {
  32. const memory = await first('MemoryPracticeRecord', { company, yhid: { $gt: 0 }, plid: { $gt: 0 } }, 'yhid,plid');
  33. const study = await first('DailyStudyRecord', { company, userId: { $gt: 0 } }, 'userId');
  34. const studyCommon = await first('CommonModel', { company, modelId: 56 }, 'generalId');
  35. const orderCommon = await first('CommonModel', { company, modelId: 54 }, 'generalId');
  36. const practice = await first('PracticeRecord', { company, yhid: { $gt: 0 } }, 'yhid');
  37. if (!memory?.yhid || !study?.userId || !studyCommon?.generalId || !orderCommon?.generalId || !practice?.yhid) throw new Error('生产学习数据样本不完整');
  38. const username = `auth_learning_smoke_${Date.now()}`;
  39. const mobile = `139${String(Date.now()).slice(-8)}`;
  40. const password = `${randomBytes(24).toString('base64url')}Aa9!`;
  41. const created = await master('/users', {
  42. method: 'POST',
  43. body: JSON.stringify({ username, password, mobile, company, isAdmin: true, role: 'admin', roles: ['admin'], legacyUserId: 990000001, legacyGroupId: 1, testCreatedBy: 'smoke-app-auth-learning' }),
  44. }, 201);
  45. userIds.push(created.objectId);
  46. const login = await cloud('', 'user_login_passwd', { name: mobile, passwd: password });
  47. const sessionToken = String(login.result?.sessionToken || '');
  48. if (!sessionToken.startsWith('r:')) throw new Error('手机号映射登录没有返回有效 Parse 会话');
  49. await cloud('', 'e_get_21list', { uid: memory.yhid, cpage: 1, psize: 1 }, 401);
  50. const memoryList = await cloud(sessionToken, 'e_get_21list', { uid: memory.yhid, cpage: 1, psize: 20 });
  51. if (!Array.isArray(memoryList.result) || memoryList.result.length < 1 || Number(memoryList.page?.itemCount) < 1) throw new Error('抗遗忘记录读取为空');
  52. const studyList = await cloud(sessionToken, 'content_list', { modelId: 56, myfield2: `UserId=${study.userId}`, cpage: 1, psize: 20 });
  53. if (!Array.isArray(studyList.result) || studyList.result.length < 1 || Number(studyList.page?.itemCount) < 1) throw new Error('学习记录列表读取为空');
  54. const studyDetail = await cloud(sessionToken, 'e_record_detail', { id: studyCommon.generalId });
  55. if (!Array.isArray(studyDetail.result)) throw new Error('学习记录详情响应格式错误');
  56. const orderDetail = await cloud(sessionToken, 'e_order_detail', { id: orderCommon.generalId });
  57. if (!Array.isArray(orderDetail.result) || !orderDetail.result.length) throw new Error('预约详情读取为空');
  58. const newWords = await cloud(sessionToken, 'e_words_list', { uid: practice.yhid, cpage: 1, psize: 20 });
  59. if (!Array.isArray(newWords.result)) throw new Error('生词本响应格式错误');
  60. const overviewStartedAt = performance.now();
  61. const learningOverview = await cloud(sessionToken, 'app_learning_overview', { uid: study.userId, studentId: study.userId, date: '' });
  62. const learningOverviewMs = Math.round(performance.now() - overviewStartedAt);
  63. if (!Array.isArray(learningOverview.result?.records) || !learningOverview.result.records.length) throw new Error('学习记录轻量总览为空');
  64. if (!learningOverview.result?.recordDateCounts || !Array.isArray(learningOverview.result?.courses) || !Array.isArray(learningOverview.result?.schedules)) throw new Error('学习记录轻量总览结构错误');
  65. const companionStartedAt = performance.now();
  66. const companionOverview = await cloud(sessionToken, 'app_companion_overview', { uid: study.userId, mode: 'student', history: false, limit: 500 });
  67. const companionOverviewMs = Math.round(performance.now() - companionStartedAt);
  68. if (!Array.isArray(companionOverview.result?.courses) || !Array.isArray(companionOverview.result?.schedules)) throw new Error('伴学轻量总览结构错误');
  69. const accountStartedAt = performance.now();
  70. const accountOverview = await cloud(sessionToken, 'app_account_overview', { uid: memory.yhid });
  71. const accountOverviewMs = Math.round(performance.now() - accountStartedAt);
  72. if (!accountOverview.result?.profile || !accountOverview.result?.antiForgetSummary || !accountOverview.result?.companionSummary) throw new Error('我的页面轻量总览结构错误');
  73. const coachUsername = `coach_learning_smoke_${Date.now()}`;
  74. const coachPassword = `${randomBytes(24).toString('base64url')}Bb8!`;
  75. const coach = await master('/users', {
  76. method: 'POST',
  77. body: JSON.stringify({ username: coachUsername, password: coachPassword, company, legacyUserId: Number(memory.plid), legacyGroupId: 3, testCreatedBy: 'smoke-app-auth-learning' }),
  78. }, 201);
  79. userIds.push(coach.objectId);
  80. const coachLogin = await cloud('', 'user_login_passwd', { name: coachUsername, passwd: coachPassword });
  81. const coachToken = String(coachLogin.result?.sessionToken || '');
  82. const coachMemory = await cloud(coachToken, 'content_list', { modelId: 60, myfield2: `plid=${memory.plid}`, cpage: 1, psize: 20 });
  83. if (!Array.isArray(coachMemory.result) || coachMemory.result.length < 1) throw new Error('陪练本人抗遗忘任务读取为空');
  84. const coachCompanionStartedAt = performance.now();
  85. const coachCompanionOverview = await cloud(coachToken, 'app_companion_overview', { uid: memory.plid, mode: 'coach', history: false, limit: 500 });
  86. const coachCompanionOverviewMs = Math.round(performance.now() - coachCompanionStartedAt);
  87. if (!Array.isArray(coachCompanionOverview.result?.schedules)) throw new Error('陪练排课轻量总览结构错误');
  88. const coachAccountStartedAt = performance.now();
  89. const coachAccountOverview = await cloud(coachToken, 'app_account_overview', { uid: memory.plid });
  90. const coachAccountOverviewMs = Math.round(performance.now() - coachAccountStartedAt);
  91. if (!coachAccountOverview.result?.profile || !coachAccountOverview.result?.coachPanel) throw new Error('陪练我的页面轻量总览结构错误');
  92. console.log(JSON.stringify({ passed: true, parseSession: true, privateSessionRequired: true, coachOwnership: true, memoryRecords: memoryList.page?.itemCount, studyList: studyList.page?.itemCount, studyDetail: true, orderDetail: true, newWords: newWords.page?.itemCount, overviewLatencyMs: { learning: learningOverviewMs, companion: companionOverviewMs, account: accountOverviewMs, coachCompanion: coachCompanionOverviewMs, coachAccount: coachAccountOverviewMs } }, null, 2));
  93. } finally {
  94. for (const userId of userIds) {
  95. const pointer = { __type: 'Pointer', className: '_User', objectId: userId };
  96. const sessions = await master(`/classes/_Session?where=${encodeURIComponent(JSON.stringify({ user: pointer }))}&limit=1000&keys=objectId`).catch(() => ({ results: [] }));
  97. for (const session of sessions.results || []) await master(`/classes/_Session/${session.objectId}`, { method: 'DELETE' }).catch(() => undefined);
  98. await master(`/users/${userId}`, { method: 'DELETE' }).catch(() => undefined);
  99. }
  100. }