api.test.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008
  1. /**
  2. * Tests for cfc-frontend utils/api.js
  3. *
  4. * Mocks uni.request to intercept and verify API calls.
  5. */
  6. import * as api from '@/utils/api'
  7. // --------------- helpers ---------------
  8. /** Helper: make uni.request call success(data) on next tick */
  9. function mockSuccess(data) {
  10. global.uni.request.mockImplementation((opts) => {
  11. process.nextTick(() => {
  12. if (opts.success) opts.success({ data })
  13. })
  14. })
  15. }
  16. /** Helper: make uni.request call fail(err) on next tick */
  17. function mockFail(err) {
  18. global.uni.request.mockImplementation((opts) => {
  19. process.nextTick(() => {
  20. if (opts.fail) opts.fail(err)
  21. })
  22. })
  23. }
  24. /** Helper: make uni.request return a sequence of responses (one per call) */
  25. function mockSequence(responses) {
  26. var i = 0
  27. global.uni.request.mockImplementation((opts) => {
  28. var resp = responses[Math.min(i, responses.length - 1)]
  29. i++
  30. process.nextTick(() => {
  31. if (resp.err) {
  32. if (opts.fail) opts.fail(resp.err)
  33. } else {
  34. if (opts.success) opts.success({ data: resp.data })
  35. }
  36. })
  37. })
  38. }
  39. // --------------- global beforeEach ---------------
  40. beforeEach(() => {
  41. global.uni._storage = {}
  42. global.uni.request.mockReset()
  43. global.uni.showToast.mockReset()
  44. global.uni.getAccountInfoSync.mockReset()
  45. global.uni.getAccountInfoSync.mockReturnValue({
  46. miniProgram: { envVersion: 'develop' }
  47. })
  48. })
  49. /** Capture the last uni.request call options */
  50. function lastRequest() {
  51. const calls = global.uni.request.mock.calls
  52. return calls[calls.length - 1][0]
  53. }
  54. // --------------- auth module ---------------
  55. describe('auth API', () => {
  56. beforeEach(() => {
  57. global.uni._storage.token = 'test-token'
  58. global.uni._storage.userId = '42'
  59. })
  60. test('wechatLogin sends POST /api/auth/wechat-login', async () => {
  61. mockSuccess({ code: 200, data: { token: 'jwt' } })
  62. const res = await api.wechatLogin('code123', { nickname: 'test' })
  63. const opts = lastRequest()
  64. expect(opts.method).toBe('POST')
  65. expect(opts.url).toMatch(/\/api\/auth\/wechat-login$/)
  66. expect(opts.data.code).toBe('code123')
  67. expect(res.data.token).toBe('jwt')
  68. })
  69. test('phoneLogin sends POST with phone and code', async () => {
  70. mockSuccess({ code: 200, data: { token: 'jwt' } })
  71. await api.phoneLogin({ phone: '13800138000', code: '1234' })
  72. const opts = lastRequest()
  73. expect(opts.method).toBe('POST')
  74. expect(opts.url).toMatch(/\/api\/auth\/phone-login$/)
  75. expect(opts.data.phone).toBe('13800138000')
  76. })
  77. test('sendCode sends POST with data', async () => {
  78. mockSuccess({ code: 200 })
  79. await api.sendCode({ phone: '13800138000' })
  80. const opts = lastRequest()
  81. expect(opts.method).toBe('POST')
  82. expect(opts.url).toMatch(/\/api\/auth\/send-code$/)
  83. })
  84. test('silentLogin sends POST with code', async () => {
  85. mockSuccess({ code: 200 })
  86. await api.silentLogin('wxcode')
  87. expect(lastRequest().data.code).toBe('wxcode')
  88. })
  89. test('autoLogin sends POST with openid', async () => {
  90. mockSuccess({ code: 200 })
  91. await api.autoLogin('openid_xxx')
  92. expect(lastRequest().data.openid).toBe('openid_xxx')
  93. })
  94. test('setPassword and verifyPassword send correct endpoints', async () => {
  95. mockSuccess({ code: 200 })
  96. await api.setPassword('secret')
  97. expect(lastRequest().url).toMatch(/\/api\/auth\/set-password$/)
  98. expect(lastRequest().data.password).toBe('secret')
  99. await api.verifyPassword('secret')
  100. expect(lastRequest().url).toMatch(/\/api\/auth\/verify-password$/)
  101. })
  102. test('directRegister sends POST', async () => {
  103. mockSuccess({ code: 200 })
  104. await api.directRegister({ phone: '13800138000', nickname: 'tester' })
  105. expect(lastRequest().url).toMatch(/\/api\/auth\/direct-register$/)
  106. })
  107. test('checkPhone sends POST with phone', async () => {
  108. mockSuccess({ code: 200 })
  109. await api.checkPhone('13800138000')
  110. expect(lastRequest().data.phone).toBe('13800138000')
  111. })
  112. test('wechatPhoneLogin sends POST', async () => {
  113. mockSuccess({ code: 200 })
  114. await api.wechatPhoneLogin({ code: 'wxcode', iv: 'iv', encryptedData: 'enc' })
  115. expect(lastRequest().url).toMatch(/\/api\/auth\/wechat-phone-login$/)
  116. })
  117. test('verifyToken sends POST /api/auth/verify with bearer token', async () => {
  118. mockSuccess({ code: 200, data: { userId: 42, role: 'parent' } })
  119. const res = await api.verifyToken()
  120. const opts = lastRequest()
  121. expect(opts.method).toBe('POST')
  122. expect(opts.url).toMatch(/\/api\/auth\/verify$/)
  123. expect(opts.header.Authorization).toBe('Bearer test-token')
  124. expect(res.data.userId).toBe(42)
  125. })
  126. test('verifyToken with 401 (token expired) clears auth and redirects to login', async () => {
  127. jest.useFakeTimers()
  128. mockSuccess({ code: 401, message: 'Token无效或已过期', data: null })
  129. await expect(api.verifyToken()).rejects.toEqual({ code: 401, message: 'Token无效或已过期', data: null })
  130. expect(global.uni._storage.token).toBeUndefined()
  131. // 触发 _clearAuthAndRedirect 内 1.5s setTimeout(reLaunch 在此定时器回调中执行)
  132. jest.runAllTimers()
  133. expect(global.uni.reLaunch).toHaveBeenCalledWith({ url: '/pages/login/login' })
  134. expect(global.uni.showToast).toHaveBeenCalled()
  135. jest.useRealTimers()
  136. })
  137. test('verifyToken with 401 (user deleted) clears auth and redirects to login', async () => {
  138. jest.useFakeTimers()
  139. mockSuccess({ code: 401, message: '用户不存在', data: null })
  140. await expect(api.verifyToken()).rejects.toEqual({ code: 401, message: '用户不存在', data: null })
  141. expect(global.uni._storage.token).toBeUndefined()
  142. jest.runAllTimers()
  143. expect(global.uni.reLaunch).toHaveBeenCalledWith({ url: '/pages/login/login' })
  144. expect(global.uni.showToast).toHaveBeenCalled()
  145. jest.useRealTimers()
  146. })
  147. test('Authorization header includes Bearer token', async () => {
  148. mockSuccess({ code: 200 })
  149. global.uni._storage.token = 'my-jwt-token'
  150. await api.getUserInfo()
  151. const opts = lastRequest()
  152. expect(opts.header.Authorization).toBe('Bearer my-jwt-token')
  153. })
  154. test('X-User-Id header is NOT sent (removed for security)', async () => {
  155. mockSuccess({ code: 200 })
  156. global.uni._storage.userId = '99'
  157. await api.getUserInfo()
  158. expect(lastRequest().header['X-User-Id']).toBeUndefined()
  159. })
  160. test('rejects on non-200 code', async () => {
  161. mockSuccess({ code: 400, message: 'Bad Request' })
  162. await expect(api.getUserInfo()).rejects.toEqual({ code: 400, message: 'Bad Request' })
  163. })
  164. test('rejects on network failure', async () => {
  165. mockFail(new Error('Network error'))
  166. await expect(api.getUserInfo()).rejects.toThrow('Network error')
  167. })
  168. })
  169. // --------------- streets module ---------------
  170. describe('street address API', () => {
  171. test('getProvinces sends POST to /api/streets/provinces', async () => {
  172. mockSuccess({ code: 200, data: [] })
  173. await api.getProvinces()
  174. expect(lastRequest().url).toMatch(/\/api\/streets\/provinces$/)
  175. })
  176. test('getStreetChildren sends POST with parentId', async () => {
  177. mockSuccess({ code: 200 })
  178. await api.getStreetChildren(110000)
  179. expect(lastRequest().url).toMatch(/\/api\/streets\/children\/110000$/)
  180. })
  181. test('getStreetById sends POST to correct URL', async () => {
  182. mockSuccess({ code: 200 })
  183. await api.getStreetById(110101)
  184. expect(lastRequest().url).toMatch(/\/api\/streets\/110101$/)
  185. })
  186. test('getStreetPath sends POST', async () => {
  187. mockSuccess({ code: 200 })
  188. await api.getStreetPath(110101)
  189. expect(lastRequest().url).toMatch(/\/api\/streets\/110101\/path$/)
  190. })
  191. test('searchStreet sends POST with keyword', async () => {
  192. mockSuccess({ code: 200 })
  193. await api.searchStreet('北京')
  194. expect(lastRequest().data.keyword).toBe('北京')
  195. })
  196. test('matchByStreet sends POST with streetId', async () => {
  197. mockSuccess({ code: 200 })
  198. await api.matchByStreet(110101)
  199. expect(lastRequest().url).toMatch(/\/110101\/match$/)
  200. })
  201. })
  202. // --------------- user / family module ---------------
  203. describe('user & family API', () => {
  204. test('getUserInfo sends POST', async () => {
  205. mockSuccess({ code: 200 })
  206. await api.getUserInfo()
  207. expect(lastRequest().url).toMatch(/\/api\/user\/info$/)
  208. })
  209. test('updateUserInfo sends POST with data', async () => {
  210. mockSuccess({ code: 200 })
  211. await api.updateUserInfo({ nickname: 'new-name' })
  212. expect(lastRequest().data.nickname).toBe('new-name')
  213. })
  214. test('joinFamily sends POST with inviteCode', async () => {
  215. mockSuccess({ code: 200 })
  216. await api.joinFamily('ABC123')
  217. expect(lastRequest().data.inviteCode).toBe('ABC123')
  218. })
  219. test('createFamily sends POST with name', async () => {
  220. mockSuccess({ code: 200 })
  221. await api.createFamily('幸福之家')
  222. expect(lastRequest().data.name).toBe('幸福之家')
  223. })
  224. test('getFamilyMemberList sends POST to unified endpoint', async () => {
  225. mockSuccess({ code: 200 })
  226. await api.getFamilyMemberList()
  227. expect(lastRequest().url).toMatch(/\/api\/family\/member\/list$/)
  228. })
  229. test('getFamilyMemberList sends visibleOnly param', async () => {
  230. mockSuccess({ code: 200 })
  231. await api.getFamilyMemberList({ visibleOnly: true })
  232. expect(lastRequest().url).toMatch(/\/api\/family\/member\/list$/)
  233. expect(lastRequest().data.visibleOnly).toBe(true)
  234. })
  235. test('getFamilyMemberList sends includeFamily param', async () => {
  236. mockSuccess({ code: 200 })
  237. await api.getFamilyMemberList({ includeFamily: true })
  238. expect(lastRequest().url).toMatch(/\/api\/family\/member\/list$/)
  239. expect(lastRequest().data.includeFamily).toBe(true)
  240. })
  241. test('updateMemberVisibility sends POST with params', async () => {
  242. mockSuccess({ code: 200 })
  243. await api.updateMemberVisibility(1, 'child', true)
  244. expect(lastRequest().data.memberId).toBe(1)
  245. expect(lastRequest().data.memberType).toBe('child')
  246. expect(lastRequest().data.showToFamily).toBe(true)
  247. })
  248. test('switchMode sends POST with mode', async () => {
  249. mockSuccess({ code: 200 })
  250. await api.switchMode('child')
  251. expect(lastRequest().data.mode).toBe('child')
  252. })
  253. test('switchToChild sends POST with memberId', async () => {
  254. mockSuccess({ code: 200 })
  255. await api.switchToChild(5)
  256. expect(lastRequest().data.memberId).toBe(5)
  257. })
  258. test('switchBackToParent sends POST', async () => {
  259. mockSuccess({ code: 200 })
  260. await api.switchBackToParent()
  261. expect(lastRequest().url).toMatch(/\/api\/user\/switch-back-to-parent$/)
  262. })
  263. test('getChildren sends POST', async () => {
  264. mockSuccess({ code: 200 })
  265. await api.getChildren()
  266. expect(lastRequest().url).toMatch(/\/api\/family\/user\/children\/list$/)
  267. })
  268. test('createChild sends POST with data', async () => {
  269. mockSuccess({ code: 200 })
  270. await api.createChild({ name: '小宝', gender: 'male' })
  271. expect(lastRequest().url).toMatch(/\/api\/family\/user\/children$/)
  272. expect(lastRequest().data.name).toBe('小宝')
  273. })
  274. test('updateChild sends POST merged with memberId', async () => {
  275. mockSuccess({ code: 200 })
  276. await api.updateChild(5, { nickname: '大宝' })
  277. expect(lastRequest().data.memberId).toBe(5)
  278. expect(lastRequest().data.nickname).toBe('大宝')
  279. })
  280. test('kickFamilyMember sends POST with targetUserId', async () => {
  281. mockSuccess({ code: 200 })
  282. await api.kickFamilyMember(10)
  283. expect(lastRequest().data.targetUserId).toBe(10)
  284. })
  285. })
  286. // --------------- membership module ---------------
  287. describe('membership API', () => {
  288. test('getMembershipLevels sends POST', async () => {
  289. mockSuccess({ code: 200 })
  290. await api.getMembershipLevels()
  291. expect(lastRequest().url).toMatch(/\/api\/membership\/levels$/)
  292. })
  293. test('getMyMembership sends POST', async () => {
  294. mockSuccess({ code: 200 })
  295. await api.getMyMembership()
  296. expect(lastRequest().url).toMatch(/\/api\/membership\/my$/)
  297. })
  298. test('createOrder sends POST with levelCode and paymentType', async () => {
  299. mockSuccess({ code: 200 })
  300. await api.createOrder('vip', 'wechat')
  301. expect(lastRequest().data.levelCode).toBe('vip')
  302. expect(lastRequest().data.paymentType).toBe('wechat')
  303. })
  304. })
  305. // --------------- tasks module ---------------
  306. describe('tasks API', () => {
  307. test('createTask sends POST with data', async () => {
  308. mockSuccess({ code: 200 })
  309. await api.createTask({ title: '晨读', memberId: 5 })
  310. expect(lastRequest().url).toMatch(/\/api\/tasks$/)
  311. expect(lastRequest().data.title).toBe('晨读')
  312. })
  313. test('getTodayTasks sends POST with memberId', async () => {
  314. mockSuccess({ code: 200 })
  315. await api.getTodayTasks(5)
  316. expect(lastRequest().data.memberId).toBe(5)
  317. })
  318. test('getPendingReviewTasks sends POST', async () => {
  319. mockSuccess({ code: 200 })
  320. await api.getPendingReviewTasks()
  321. expect(lastRequest().url).toMatch(/\/api\/tasks\/pending-review$/)
  322. })
  323. test('approveTask sends POST with approved:true', async () => {
  324. mockSuccess({ code: 200 })
  325. await api.approveTask(10)
  326. expect(lastRequest().url).toMatch(/\/api\/tasks\/10\/review$/)
  327. expect(lastRequest().data.approved).toBe(true)
  328. })
  329. test('rejectTask sends POST with approved:false', async () => {
  330. mockSuccess({ code: 200 })
  331. await api.rejectTask(10)
  332. expect(lastRequest().data.approved).toBe(false)
  333. })
  334. test('completeTask sends POST with memberId and photoUrl', async () => {
  335. mockSuccess({ code: 200 })
  336. await api.completeTask(10, 5, 'http://photo.url')
  337. expect(lastRequest().data.memberId).toBe(5)
  338. expect(lastRequest().data.photoUrl).toBe('http://photo.url')
  339. })
  340. test('reviewTask sends POST with data', async () => {
  341. mockSuccess({ code: 200 })
  342. await api.reviewTask(10, { approved: true, comment: '很好' })
  343. expect(lastRequest().url).toMatch(/\/api\/tasks\/10\/review$/)
  344. expect(lastRequest().data.comment).toBe('很好')
  345. })
  346. test('getTaskHistory sends POST with pagination', async () => {
  347. mockSuccess({ code: 200 })
  348. await api.getTaskHistory(5, 1, 20)
  349. expect(lastRequest().data.memberId).toBe(5)
  350. expect(lastRequest().data.page).toBe(1)
  351. expect(lastRequest().data.size).toBe(20)
  352. })
  353. test('deleteTask sends DELETE', async () => {
  354. mockSuccess({ code: 200 })
  355. await api.deleteTask(10)
  356. expect(lastRequest().method).toBe('DELETE')
  357. expect(lastRequest().url).toMatch(/\/api\/tasks\/10$/)
  358. })
  359. test('batchCompleteTasks sends POST with array', async () => {
  360. mockSuccess({ code: 200 })
  361. await api.batchCompleteTasks([1, 2, 3])
  362. expect(lastRequest().data.taskIds).toEqual([1, 2, 3])
  363. })
  364. test('getTodayParentTasks sends POST', async () => {
  365. mockSuccess({ code: 200 })
  366. await api.getTodayParentTasks()
  367. expect(lastRequest().url).toMatch(/\/api\/tasks\/today-parent$/)
  368. })
  369. })
  370. // --------------- points module ---------------
  371. describe('points API', () => {
  372. test('getPointsBalance sends POST with memberId', async () => {
  373. mockSuccess({ code: 200 })
  374. await api.getPointsBalance(5)
  375. expect(lastRequest().data.memberId).toBe(5)
  376. })
  377. test('getPointsLogs sends POST with pagination', async () => {
  378. mockSuccess({ code: 200 })
  379. await api.getPointsLogs(5, 2, 20)
  380. expect(lastRequest().data.memberId).toBe(5)
  381. expect(lastRequest().data.page).toBe(2)
  382. expect(lastRequest().data.size).toBe(20)
  383. })
  384. })
  385. // --------------- rewards / wishes module ---------------
  386. describe('rewards & wishes API', () => {
  387. test('createReward sends POST', async () => {
  388. mockSuccess({ code: 200 })
  389. await api.createReward({ name: '玩具', points: 100 })
  390. expect(lastRequest().data.name).toBe('玩具')
  391. })
  392. test('getWishlist sends POST with memberId', async () => {
  393. mockSuccess({ code: 200 })
  394. await api.getWishlist(5)
  395. expect(lastRequest().data.memberId).toBe(5)
  396. })
  397. test('exchangeReward sends POST', async () => {
  398. mockSuccess({ code: 200 })
  399. await api.exchangeReward(1, 5)
  400. expect(lastRequest().data.memberId).toBe(5)
  401. })
  402. test('getPendingWishes sends POST with empty body', async () => {
  403. mockSuccess({ code: 200 })
  404. await api.getPendingWishes()
  405. expect(lastRequest().data).toEqual({})
  406. })
  407. test('getWishes sends POST with params', async () => {
  408. mockSuccess({ code: 200 })
  409. await api.getWishes({ memberId: 5, status: 'pending' })
  410. expect(lastRequest().data.memberId).toBe(5)
  411. })
  412. test('setWishPrice sends POST with pointsRequired', async () => {
  413. mockSuccess({ code: 200 })
  414. await api.setWishPrice(1, 200)
  415. expect(lastRequest().data.pointsRequired).toBe(200)
  416. })
  417. test('approveWishExchange sends POST', async () => {
  418. mockSuccess({ code: 200 })
  419. await api.approveWishExchange(1, true)
  420. expect(lastRequest().data.approved).toBe(true)
  421. })
  422. test('createWish sends POST with data', async () => {
  423. mockSuccess({ code: 200 })
  424. await api.createWish({ title: '新书包', memberId: 5 })
  425. expect(lastRequest().data.title).toBe('新书包')
  426. })
  427. })
  428. // --------------- identity parameter hardening ---------------
  429. // 前端不再向后端发送可伪造的 userId / familyId / childId(仅 memberId),
  430. // 身份一律由后端从 JWT 推导。以下断言锁定这一不变量。
  431. describe('identity parameter hardening', () => {
  432. beforeEach(() => {
  433. global.uni._storage.token = 'test-token'
  434. })
  435. test('request() does NOT send X-User-Id header', async () => {
  436. mockSuccess({ code: 200 })
  437. await api.getPendingWishes()
  438. const opts = lastRequest()
  439. expect(opts.header['X-User-Id']).toBeUndefined()
  440. })
  441. test('getMyAssessmentResults sends empty body, no userId', async () => {
  442. mockSuccess({ code: 200 })
  443. await api.getMyAssessmentResults()
  444. const opts = lastRequest()
  445. expect(opts.data).toEqual({})
  446. expect(opts.data.userId).toBeUndefined()
  447. })
  448. test('getChildAssessmentResults sends empty body, no userId', async () => {
  449. mockSuccess({ code: 200 })
  450. await api.getChildAssessmentResults()
  451. const opts = lastRequest()
  452. expect(opts.data).toEqual({})
  453. expect(opts.data.userId).toBeUndefined()
  454. })
  455. test('getTodayTasksByCategory sends only memberId and category', async () => {
  456. mockSuccess({ code: 200 })
  457. await api.getTodayTasksByCategory(7, 'wisdom')
  458. const opts = lastRequest()
  459. expect(opts.data.memberId).toBe(7)
  460. expect(opts.data.category).toBe('wisdom')
  461. expect(opts.data.childId).toBeUndefined()
  462. expect(opts.data.userId).toBeUndefined()
  463. })
  464. })
  465. // --------------- guide / packages module ---------------
  466. describe('guide & packages API', () => {
  467. test('getGuidePackages sends POST to list', async () => {
  468. mockSuccess({ code: 200 })
  469. await api.getGuidePackages(1)
  470. expect(lastRequest().url).toMatch(/\/api\/guide\/packages$/)
  471. })
  472. test('registerGuide sends POST', async () => {
  473. mockSuccess({ code: 200 })
  474. await api.registerGuide({ name: '张老师', level: 'senior' })
  475. expect(lastRequest().url).toMatch(/\/api\/guide\/register$/)
  476. })
  477. test('generateBindInvite sends POST', async () => {
  478. mockSuccess({ code: 200 })
  479. await api.generateBindInvite()
  480. expect(lastRequest().url).toMatch(/\/api\/guide\/bind\/invite$/)
  481. })
  482. test('getBoundFamilies sends POST', async () => {
  483. mockSuccess({ code: 200 })
  484. await api.getBoundFamilies()
  485. expect(lastRequest().url).toMatch(/\/api\/guide\/families$/)
  486. })
  487. test('getTeamMembers sends POST', async () => {
  488. mockSuccess({ code: 200 })
  489. await api.getTeamMembers()
  490. expect(lastRequest().url).toMatch(/\/api\/guide\/team\/members$/)
  491. })
  492. test('getGuideOrders sends POST', async () => {
  493. mockSuccess({ code: 200 })
  494. await api.getGuideOrders()
  495. expect(lastRequest().url).toMatch(/\/api\/guide\/orders\/list$/)
  496. })
  497. test('getGuideMessages sends POST', async () => {
  498. mockSuccess({ code: 200 })
  499. await api.getGuideMessages()
  500. expect(lastRequest().url).toMatch(/\/api\/guide\/messages\/list$/)
  501. })
  502. })
  503. // --------------- payment module ---------------
  504. describe('payment API', () => {
  505. test('createPackageOrder sends POST', async () => {
  506. mockSuccess({ code: 200 })
  507. await api.createPackageOrder({ packageId: 1, userId: 5 })
  508. expect(lastRequest().url).toMatch(/\/api\/package\/order\/create$/)
  509. })
  510. test('createWechatPayOrder sends POST', async () => {
  511. mockSuccess({ code: 200 })
  512. await api.createWechatPayOrder('NO001', '测试订单', 100)
  513. expect(lastRequest().data.orderNo).toBe('NO001')
  514. expect(lastRequest().data.amount).toBe(100)
  515. })
  516. })
  517. // --------------- assessment module ---------------
  518. describe('assessment API', () => {
  519. test('createAssessmentAppointment sends POST', async () => {
  520. mockSuccess({ code: 200 })
  521. await api.createAssessmentAppointment({ memberId: 5, date: '2026-06-20' })
  522. expect(lastRequest().url).toMatch(/\/api\/dan-assessment\/appointment\/create$/)
  523. })
  524. test('payAssessmentOrder sends POST', async () => {
  525. mockSuccess({ code: 200 })
  526. await api.payAssessmentOrder('ORDER001', 'wechat')
  527. expect(lastRequest().data.orderNo).toBe('ORDER001')
  528. })
  529. test('recordAssessmentResult sends POST', async () => {
  530. mockSuccess({ code: 200 })
  531. await api.recordAssessmentResult({ orderId: 1, score: 85 })
  532. expect(lastRequest().url).toMatch(/\/api\/dan-assessment\/result\/record$/)
  533. })
  534. })
  535. // --------------- mini-games module ---------------
  536. describe('mini-games API', () => {
  537. test('getMiniGameList sends POST', async () => {
  538. mockSuccess({ code: 200 })
  539. await api.getMiniGameList()
  540. expect(lastRequest().url).toMatch(/\/api\/mini-game\/list$/)
  541. })
  542. test('getMiniGameByCode sends POST', async () => {
  543. mockSuccess({ code: 200 })
  544. await api.getMiniGameByCode('sudoku')
  545. expect(lastRequest().url).toMatch(/\/api\/mini-game\/sudoku$/)
  546. })
  547. test('completeMiniGame sends POST with game data', async () => {
  548. mockSuccess({ code: 200 })
  549. await api.completeMiniGame(5, 'sudoku', 120, 85)
  550. expect(lastRequest().data.memberId).toBe(5)
  551. expect(lastRequest().data.gameCode).toBe('sudoku')
  552. expect(lastRequest().data.completionTime).toBe(120)
  553. expect(lastRequest().data.score).toBe(85)
  554. })
  555. test('getGameHistory sends POST', async () => {
  556. mockSuccess({ code: 200 })
  557. await api.getGameHistory({ memberId: 5 })
  558. expect(lastRequest().url).toMatch(/\/api\/game\/history$/)
  559. })
  560. test('getGameLeaderboard sends POST', async () => {
  561. mockSuccess({ code: 200 })
  562. await api.getGameLeaderboard({ gameCode: 'sudoku' })
  563. expect(lastRequest().url).toMatch(/\/api\/game\/leaderboard$/)
  564. })
  565. })
  566. // --------------- growth module ---------------
  567. describe('growth records API', () => {
  568. test('createGrowthRecord sends POST', async () => {
  569. mockSuccess({ code: 200 })
  570. await api.createGrowthRecord({ memberId: 5, content: '长高了2cm' })
  571. expect(lastRequest().url).toMatch(/\/api\/growth\/record\/create$/)
  572. })
  573. test('getGrowthRecordList sends POST', async () => {
  574. mockSuccess({ code: 200 })
  575. await api.getGrowthRecordList()
  576. expect(lastRequest().url).toMatch(/\/api\/growth\/record\/list$/)
  577. })
  578. test('createGrowthRecord with alsoPurchaseAssessment uses merge endpoint', async () => {
  579. mockSuccess({ code: 200 })
  580. await api.createGrowthRecord({ memberId: 5, alsoPurchaseAssessment: true })
  581. expect(lastRequest().url).toMatch(/\/api\/growth\/record\/create-with-order$/)
  582. })
  583. })
  584. // --------------- energy module ---------------
  585. describe('energy API', () => {
  586. test('getEnergyOverview sends POST with memberId', async () => {
  587. mockSuccess({ code: 200 })
  588. await api.getEnergyOverview(5)
  589. expect(lastRequest().data.memberId).toBe(5)
  590. })
  591. test('getEnergyLogs sends POST', async () => {
  592. mockSuccess({ code: 200 })
  593. await api.getEnergyLogs(5, 'body', 1, 20)
  594. expect(lastRequest().data.dimensionCode).toBe('body')
  595. })
  596. test('getFamilyEnergySandbox sends POST', async () => {
  597. mockSuccess({ code: 200 })
  598. await api.getFamilyEnergySandbox()
  599. expect(lastRequest().url).toMatch(/\/api\/energy\/sandbox$/)
  600. })
  601. })
  602. // --------------- badges module ---------------
  603. describe('badges API', () => {
  604. test('getActiveBadges sends POST', async () => {
  605. mockSuccess({ code: 200 })
  606. await api.getActiveBadges()
  607. expect(lastRequest().url).toMatch(/\/api\/badges\/list$/)
  608. })
  609. test('getChildBadges sends POST with memberId', async () => {
  610. mockSuccess({ code: 200 })
  611. await api.getChildBadges(5)
  612. expect(lastRequest().data.memberId).toBe(5)
  613. })
  614. test('toggleBadgeFavorite sends POST', async () => {
  615. mockSuccess({ code: 200 })
  616. await api.toggleBadgeFavorite(5, 3)
  617. expect(lastRequest().data.memberId).toBe(5)
  618. expect(lastRequest().data.badgeId).toBe(3)
  619. })
  620. })
  621. // --------------- articles module ---------------
  622. describe('articles API', () => {
  623. test('getArticleCategories sends POST', async () => {
  624. mockSuccess({ code: 200 })
  625. await api.getArticleCategories()
  626. expect(lastRequest().url).toMatch(/\/api\/articles\/categories$/)
  627. })
  628. test('getArticleList sends POST with data', async () => {
  629. mockSuccess({ code: 200 })
  630. await api.getArticleList({ categoryId: 1, page: 1 })
  631. expect(lastRequest().url).toMatch(/\/api\/articles\/list$/)
  632. })
  633. test('getArticleDetail sends POST', async () => {
  634. mockSuccess({ code: 200 })
  635. await api.getArticleDetail({ id: 42 })
  636. expect(lastRequest().url).toMatch(/\/api\/articles\/detail$/)
  637. })
  638. test('getDailyTip sends POST', async () => {
  639. mockSuccess({ code: 200 })
  640. await api.getDailyTip()
  641. expect(lastRequest().url).toMatch(/\/api\/articles\/daily-tip$/)
  642. })
  643. })
  644. // --------------- contact module ---------------
  645. describe('contact API', () => {
  646. test('getContactList sends POST', async () => {
  647. mockSuccess({ code: 200 })
  648. await api.getContactList({ memberId: 5 })
  649. expect(lastRequest().url).toMatch(/\/api\/contact\/list$/)
  650. })
  651. test('createContact sends POST with data', async () => {
  652. mockSuccess({ code: 200 })
  653. await api.createContact({ name: '爷爷', relation: 'grandfather' })
  654. expect(lastRequest().url).toMatch(/\/api\/contact\/create$/)
  655. })
  656. test('deleteContact sends POST with id', async () => {
  657. mockSuccess({ code: 200 })
  658. await api.deleteContact(3)
  659. expect(lastRequest().url).toMatch(/\/api\/contact\/delete$/)
  660. expect(lastRequest().data.id).toBe(3)
  661. })
  662. test('importPhoneContact sends POST', async () => {
  663. mockSuccess({ code: 200 })
  664. await api.importPhoneContact({ contacts: [{ name: '张三', phone: '13800138000' }] })
  665. expect(lastRequest().url).toMatch(/\/api\/contact\/import-phone$/)
  666. })
  667. })
  668. // --------------- vendor module ---------------
  669. describe('vendor API', () => {
  670. test('vendorApply sends POST with data', async () => {
  671. mockSuccess({ code: 200 })
  672. await api.vendorApply({ companyName: '某某公司', type: 'planner' })
  673. expect(lastRequest().url).toMatch(/\/api\/vendor\/apply$/)
  674. })
  675. test('vendorStatus sends POST', async () => {
  676. mockSuccess({ code: 200 })
  677. await api.vendorStatus()
  678. expect(lastRequest().url).toMatch(/\/api\/vendor\/status$/)
  679. })
  680. test('vendorInfo sends POST', async () => {
  681. mockSuccess({ code: 200 })
  682. await api.vendorInfo()
  683. expect(lastRequest().url).toMatch(/\/api\/vendor\/info$/)
  684. })
  685. })
  686. // --------------- product module ---------------
  687. describe('product API', () => {
  688. test('productList sends POST', () => {
  689. // productList uses new Promise without success/fail callbacks,
  690. // so we check the request synchronously.
  691. api.productList({ page: 1, size: 10 })
  692. const opts = lastRequest()
  693. expect(opts.method).toBe('POST')
  694. expect(opts.url).toMatch(/\/api\/product\/list$/)
  695. })
  696. test('productDetail sends POST', async () => {
  697. mockSuccess({ code: 200 })
  698. await api.productDetail({ id: 1 })
  699. expect(lastRequest().url).toMatch(/\/api\/product\/detail$/)
  700. })
  701. test('productCreate sends POST', async () => {
  702. mockSuccess({ code: 200 })
  703. await api.productCreate({ name: '测试商品', price: 100 })
  704. expect(lastRequest().url).toMatch(/\/api\/product\/create$/)
  705. })
  706. test('productOrderCreate sends POST', async () => {
  707. mockSuccess({ code: 200 })
  708. await api.productOrderCreate({ productId: 1, quantity: 2 })
  709. expect(lastRequest().url).toMatch(/\/api\/product\/order\/create$/)
  710. })
  711. })
  712. // --------------- commission / invite ---------------
  713. describe('commission & invite API', () => {
  714. test('getReferralCode sends POST', async () => {
  715. mockSuccess({ code: 200 })
  716. await api.getReferralCode()
  717. expect(lastRequest().url).toMatch(/\/api\/invite\/code$/)
  718. })
  719. test('bindReferral sends POST with code', async () => {
  720. mockSuccess({ code: 200 })
  721. await api.bindReferral('ABC123')
  722. expect(lastRequest().data.referralCode).toBe('ABC123')
  723. })
  724. test('getCommissionSummary sends POST', async () => {
  725. mockSuccess({ code: 200 })
  726. await api.getCommissionSummary()
  727. expect(lastRequest().url).toMatch(/\/api\/commission\/summary$/)
  728. })
  729. test('applyWithdrawal sends POST', async () => {
  730. mockSuccess({ code: 200 })
  731. await api.applyWithdrawal(500, { bank: 'ICBC', account: '6222****' })
  732. expect(lastRequest().data.amount).toBe(500)
  733. })
  734. })
  735. // --------------- stats module ---------------
  736. describe('stats API', () => {
  737. test('getChildCompletionStats sends POST', async () => {
  738. mockSuccess({ code: 200 })
  739. await api.getChildCompletionStats({ memberId: 5 })
  740. expect(lastRequest().url).toMatch(/\/api\/stats\/child-completion$/)
  741. })
  742. test('getChildOverview sends POST with memberId', async () => {
  743. mockSuccess({ code: 200 })
  744. await api.getChildOverview(5)
  745. expect(lastRequest().data.memberId).toBe(5)
  746. })
  747. })
  748. // --------------- AI chat module ---------------
  749. describe('AI chat API', () => {
  750. test('aiSendMessage sends POST', async () => {
  751. mockSuccess({ code: 200 })
  752. await api.aiSendMessage({ conversationId: 1, content: '你好' })
  753. expect(lastRequest().url).toMatch(/\/api\/ai\/chat\/send$/)
  754. })
  755. test('aiGetConversations sends POST', async () => {
  756. mockSuccess({ code: 200 })
  757. await api.aiGetConversations()
  758. expect(lastRequest().url).toMatch(/\/api\/ai\/chat\/conversations$/)
  759. })
  760. })
  761. // --------------- health / wealth module ---------------
  762. // --------------- health report module (new) ---------------
  763. describe('health report API', () => {
  764. test('createHealthReport sends POST', async () => {
  765. mockSuccess({ code: 200 })
  766. await api.createHealthReport({ memberId: 5, reportData: { height: 120, weight: 22 } })
  767. expect(lastRequest().url).toMatch(/\/api\/health\/report\/create$/)
  768. expect(lastRequest().data.memberId).toBe(5)
  769. })
  770. test('getReportList sends POST with memberId', async () => {
  771. mockSuccess({ code: 200 })
  772. await api.getReportList(5)
  773. expect(lastRequest().url).toMatch(/\/api\/health\/report\/list$/)
  774. expect(lastRequest().data.memberId).toBe(5)
  775. })
  776. test('getReportDetail sends POST with reportId', async () => {
  777. mockSuccess({ code: 200 })
  778. await api.getReportDetail(42)
  779. expect(lastRequest().url).toMatch(/\/api\/health\/report\/detail$/)
  780. expect(lastRequest().data.reportId).toBe(42)
  781. })
  782. test('getLatestHealthReport sends POST with memberId', async () => {
  783. mockSuccess({ code: 200 })
  784. await api.getLatestHealthReport(5)
  785. expect(lastRequest().url).toMatch(/\/api\/health\/report\/latest$/)
  786. expect(lastRequest().data.memberId).toBe(5)
  787. })
  788. test('getHealthIndicatorList sends POST with memberId', async () => {
  789. mockSuccess({ code: 200 })
  790. await api.getHealthIndicatorList(5)
  791. expect(lastRequest().url).toMatch(/\/api\/health\/indicator\/list$/)
  792. expect(lastRequest().data.memberId).toBe(5)
  793. })
  794. test('createReportSurvey sends POST with reportId and memberId', async () => {
  795. mockSuccess({ code: 200 })
  796. await api.createReportSurvey(10, 5)
  797. expect(lastRequest().url).toMatch(/\/api\/health\/survey\/create$/)
  798. expect(lastRequest().data.reportId).toBe(10)
  799. expect(lastRequest().data.memberId).toBe(5)
  800. })
  801. test('submitReportSurvey sends POST with surveyId and data', async () => {
  802. mockSuccess({ code: 200 })
  803. await api.submitReportSurvey(1, { q1: 'yes', q2: 'no' })
  804. expect(lastRequest().url).toMatch(/\/api\/health\/survey\/submit$/)
  805. expect(lastRequest().data.surveyId).toBe(1)
  806. expect(lastRequest().data.surveyData.q1).toBe('yes')
  807. })
  808. test('getSurveyStatus sends POST with reportId', async () => {
  809. mockSuccess({ code: 200 })
  810. await api.getSurveyStatus(10)
  811. expect(lastRequest().url).toMatch(/\/api\/health\/survey\/status$/)
  812. expect(lastRequest().data.reportId).toBe(10)
  813. })
  814. })
  815. describe('health & wealth API', () => {
  816. test('healthCheckinList sends POST', async () => {
  817. mockSuccess({ code: 200 })
  818. await api.healthCheckinList({ memberId: 5 })
  819. expect(lastRequest().url).toMatch(/\/api\/health\/checkin\/list$/)
  820. })
  821. test('getCheckinList sends POST', async () => {
  822. mockSuccess({ code: 200 })
  823. await api.getCheckinList({ memberId: 5 })
  824. expect(lastRequest().url).toMatch(/\/api\/wealth\/checkin\/list$/)
  825. })
  826. test('getInsuranceList sends POST', async () => {
  827. mockSuccess({ code: 200 })
  828. await api.getInsuranceList({ memberId: 5 })
  829. expect(lastRequest().url).toMatch(/\/api\/wealth\/insurance\/list$/)
  830. })
  831. })
  832. // --------------- invite card module ---------------
  833. describe('invite card API', () => {
  834. test('generateInviteCard sends POST', async () => {
  835. mockSuccess({ code: 200 })
  836. await api.generateInviteCard('guide', 1)
  837. expect(lastRequest().data.type).toBe('guide')
  838. })
  839. test('verifyInviteCard sends POST with code', async () => {
  840. mockSuccess({ code: 200 })
  841. await api.verifyInviteCard('CARD123')
  842. expect(lastRequest().data.code).toBe('CARD123')
  843. })
  844. })