api.test.js 34 KB

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