@sansenjian/qq-music-api 2.2.1 → 2.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/CHANGELOG.md +120 -42
  2. package/dist/app.js +58 -0
  3. package/dist/config/user-info.js +59 -0
  4. package/dist/index.js +3 -0
  5. package/dist/jest.config.js +40 -0
  6. package/dist/middlewares/koa-cors.js +63 -0
  7. package/dist/module/apis/UCommon/UCommon.js +10 -0
  8. package/dist/module/apis/album/getAlbumInfo.js +23 -0
  9. package/dist/module/apis/comments/getComments.js +24 -0
  10. package/dist/module/apis/digitalAlbum/getDigitalAlbumLists.js +22 -0
  11. package/dist/module/apis/downloadQQMusic.js +48 -0
  12. package/dist/module/apis/extend/getPlaylistTags.js +158 -0
  13. package/dist/module/apis/music/getLyric.js +32 -0
  14. package/dist/module/apis/mv/getMvByTag.js +23 -0
  15. package/dist/module/apis/radio/getRadioLists.js +26 -0
  16. package/dist/module/apis/rank/getTopLists.js +35 -0
  17. package/dist/module/apis/recommend/getDailyRecommend.js +124 -0
  18. package/dist/module/apis/recommend/getPersonalRecommend.js +114 -0
  19. package/dist/module/apis/search/getHotKey.js +25 -0
  20. package/dist/module/apis/search/getSearchByKey.js +32 -0
  21. package/dist/module/apis/search/getSmartbox.js +40 -0
  22. package/dist/module/apis/singers/getSimilarSinger.js +24 -0
  23. package/dist/module/apis/singers/getSingerDesc.js +24 -0
  24. package/dist/module/apis/singers/getSingerMv.js +23 -0
  25. package/dist/module/apis/singers/getSingerStarNum.js +23 -0
  26. package/dist/module/apis/songLists/songListCategories.js +23 -0
  27. package/dist/module/apis/songLists/songListDetail.js +28 -0
  28. package/dist/module/apis/songLists/songLists.js +35 -0
  29. package/dist/module/apis/u_common.js +56 -0
  30. package/dist/module/apis/user/checkQQLoginQr.js +189 -0
  31. package/dist/module/apis/user/getQQLoginQr.js +23 -0
  32. package/dist/module/apis/user/getUserAvatar.js +25 -0
  33. package/dist/module/apis/user/getUserLikedSongs.js +129 -0
  34. package/dist/module/apis/user/getUserPlaylists.js +138 -0
  35. package/dist/module/apis/y_common.js +69 -0
  36. package/dist/module/config.js +24 -0
  37. package/dist/module/index.js +70 -0
  38. package/dist/package.json +117 -0
  39. package/dist/routers/context/batchGetSongInfo.js +53 -0
  40. package/dist/routers/context/batchGetSongLists.js +37 -0
  41. package/dist/routers/context/checkQQLoginQr.js +16 -0
  42. package/dist/routers/context/cookies.js +36 -0
  43. package/dist/routers/context/getAlbumInfo.js +29 -0
  44. package/dist/routers/context/getComments.js +38 -0
  45. package/dist/routers/context/getDailyRecommend.js +45 -0
  46. package/dist/routers/context/getDigitalAlbumLists.js +16 -0
  47. package/dist/routers/context/getDownloadQQMusic.js +16 -0
  48. package/dist/routers/context/getHotkey.js +21 -0
  49. package/dist/routers/context/getImageUrl.js +25 -0
  50. package/dist/routers/context/getLyric.js +29 -0
  51. package/dist/routers/context/getMusicPlay.js +91 -0
  52. package/dist/routers/context/getMv.js +57 -0
  53. package/dist/routers/context/getMvByTag.js +16 -0
  54. package/dist/routers/context/getMvPlay.js +105 -0
  55. package/dist/routers/context/getNewDisks.js +52 -0
  56. package/dist/routers/context/getPersonalRecommend.js +54 -0
  57. package/dist/routers/context/getPlaylistTags.js +60 -0
  58. package/dist/routers/context/getQQLoginQr.js +14 -0
  59. package/dist/routers/context/getRadioLists.js +16 -0
  60. package/dist/routers/context/getRanks.js +60 -0
  61. package/dist/routers/context/getRecommend.js +88 -0
  62. package/dist/routers/context/getSearchByKey.js +32 -0
  63. package/dist/routers/context/getSimilarSinger.js +27 -0
  64. package/dist/routers/context/getSingerAlbum.js +54 -0
  65. package/dist/routers/context/getSingerDesc.js +28 -0
  66. package/dist/routers/context/getSingerHotsong.js +54 -0
  67. package/dist/routers/context/getSingerList.js +51 -0
  68. package/dist/routers/context/getSingerMv.js +36 -0
  69. package/dist/routers/context/getSingerStarNum.js +27 -0
  70. package/dist/routers/context/getSmartbox.js +27 -0
  71. package/dist/routers/context/getSongInfo.js +47 -0
  72. package/dist/routers/context/getSongListCategories.js +23 -0
  73. package/dist/routers/context/getSongListDetail.js +19 -0
  74. package/dist/routers/context/getSongLists.js +25 -0
  75. package/dist/routers/context/getTicketInfo.js +47 -0
  76. package/dist/routers/context/getTopLists.js +16 -0
  77. package/dist/routers/context/getUserAvatar.js +48 -0
  78. package/dist/routers/context/getUserLikedSongs.js +24 -0
  79. package/dist/routers/context/getUserPlaylists.js +24 -0
  80. package/dist/routers/context/index.js +107 -0
  81. package/dist/routers/router.js +69 -0
  82. package/dist/routers/types.js +2 -0
  83. package/dist/routers/util.js +188 -0
  84. package/dist/types/api.js +55 -0
  85. package/dist/util/apiResponse.js +88 -0
  86. package/dist/util/colors.js +19 -0
  87. package/dist/util/cookie.js +26 -0
  88. package/dist/util/loginUtils.js +30 -0
  89. package/dist/util/lyricParse.js +72 -0
  90. package/dist/util/request.js +109 -0
  91. package/docs-dist/404.html +24 -0
  92. package/docs-dist/CHANGELOG-ARCHITECTURE.html +131 -0
  93. package/docs-dist/COOKIE_CONFIG_GUIDE.html +39 -0
  94. package/docs-dist/README.html +447 -0
  95. package/docs-dist/TEST_USER_PLAYLISTS.html +42 -0
  96. package/docs-dist/USER_AVATAR_GUIDE.html +100 -0
  97. package/docs-dist/api/comments.html +48 -0
  98. package/docs-dist/api/index.html +27 -0
  99. package/docs-dist/api/music.html +51 -0
  100. package/docs-dist/api/other.html +33 -0
  101. package/docs-dist/api/playlist.html +77 -0
  102. package/docs-dist/api/rank.html +48 -0
  103. package/docs-dist/api/search.html +62 -0
  104. package/docs-dist/api/singer.html +47 -0
  105. package/docs-dist/api/user.html +64 -0
  106. package/docs-dist/assets/CHANGELOG-ARCHITECTURE.md.BOe0ZtyR.js +105 -0
  107. package/docs-dist/assets/CHANGELOG-ARCHITECTURE.md.BOe0ZtyR.lean.js +1 -0
  108. package/docs-dist/assets/COOKIE_CONFIG_GUIDE.md.D68AwXR2.js +13 -0
  109. package/docs-dist/assets/COOKIE_CONFIG_GUIDE.md.D68AwXR2.lean.js +1 -0
  110. package/docs-dist/assets/README.md.ZJQGJ1Gb.js +421 -0
  111. package/docs-dist/assets/README.md.ZJQGJ1Gb.lean.js +1 -0
  112. package/docs-dist/assets/TEST_USER_PLAYLISTS.md.C02575X2.js +16 -0
  113. package/docs-dist/assets/TEST_USER_PLAYLISTS.md.C02575X2.lean.js +1 -0
  114. package/docs-dist/assets/USER_AVATAR_GUIDE.md.BOqjn5Cm.js +74 -0
  115. package/docs-dist/assets/USER_AVATAR_GUIDE.md.BOqjn5Cm.lean.js +1 -0
  116. package/docs-dist/assets/api_comments.md.DADvndEA.js +22 -0
  117. package/docs-dist/assets/api_comments.md.DADvndEA.lean.js +1 -0
  118. package/docs-dist/assets/api_index.md.D5IASxxG.js +1 -0
  119. package/docs-dist/assets/api_index.md.D5IASxxG.lean.js +1 -0
  120. package/docs-dist/assets/api_music.md.BgB8NmZq.js +25 -0
  121. package/docs-dist/assets/api_music.md.BgB8NmZq.lean.js +1 -0
  122. package/docs-dist/assets/api_other.md.BkRWXX2z.js +7 -0
  123. package/docs-dist/assets/api_other.md.BkRWXX2z.lean.js +1 -0
  124. package/docs-dist/assets/api_playlist.md.Dc0hTrZ4.js +51 -0
  125. package/docs-dist/assets/api_playlist.md.Dc0hTrZ4.lean.js +1 -0
  126. package/docs-dist/assets/api_rank.md.DRisCFyT.js +22 -0
  127. package/docs-dist/assets/api_rank.md.DRisCFyT.lean.js +1 -0
  128. package/docs-dist/assets/api_search.md.DNnMUZK0.js +36 -0
  129. package/docs-dist/assets/api_search.md.DNnMUZK0.lean.js +1 -0
  130. package/docs-dist/assets/api_singer.md.DCmuxQkk.js +21 -0
  131. package/docs-dist/assets/api_singer.md.DCmuxQkk.lean.js +1 -0
  132. package/docs-dist/assets/api_user.md.Cjm9GG3z.js +38 -0
  133. package/docs-dist/assets/api_user.md.Cjm9GG3z.lean.js +1 -0
  134. package/docs-dist/assets/app.Dx_1wB58.js +1 -0
  135. package/docs-dist/assets/chunks/@localSearchIndexroot.CMY5EIwU.js +1 -0
  136. package/docs-dist/assets/chunks/VPLocalSearchBox.DwKWtsdX.js +9 -0
  137. package/docs-dist/assets/chunks/framework.o40iizuP.js +19 -0
  138. package/docs-dist/assets/chunks/theme.pGVgJ9Cx.js +2 -0
  139. package/docs-dist/assets/guide_architecture.md.DGtNyuMH.js +258 -0
  140. package/docs-dist/assets/guide_architecture.md.DGtNyuMH.lean.js +1 -0
  141. package/docs-dist/assets/guide_authentication.md.mtI5LfCw.js +4 -0
  142. package/docs-dist/assets/guide_authentication.md.mtI5LfCw.lean.js +1 -0
  143. package/docs-dist/assets/guide_index.md.B-0SG46T.js +1 -0
  144. package/docs-dist/assets/guide_index.md.B-0SG46T.lean.js +1 -0
  145. package/docs-dist/assets/guide_installation.md.k-KpAfxv.js +7 -0
  146. package/docs-dist/assets/guide_installation.md.k-KpAfxv.lean.js +1 -0
  147. package/docs-dist/assets/guide_quickstart.md.Bff_KFOD.js +13 -0
  148. package/docs-dist/assets/guide_quickstart.md.Bff_KFOD.lean.js +1 -0
  149. package/docs-dist/assets/index.md.xrs-uIyo.js +1 -0
  150. package/docs-dist/assets/index.md.xrs-uIyo.lean.js +1 -0
  151. package/docs-dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
  152. package/docs-dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
  153. package/docs-dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
  154. package/docs-dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
  155. package/docs-dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
  156. package/docs-dist/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
  157. package/docs-dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
  158. package/docs-dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
  159. package/docs-dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
  160. package/docs-dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
  161. package/docs-dist/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
  162. package/docs-dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
  163. package/docs-dist/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
  164. package/docs-dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
  165. package/docs-dist/assets/reference_response-format.md.DKYTK6uJ.js +12 -0
  166. package/docs-dist/assets/reference_response-format.md.DKYTK6uJ.lean.js +1 -0
  167. package/docs-dist/assets/style.DM4qKDd4.css +1 -0
  168. package/docs-dist/guide/architecture.html +284 -0
  169. package/docs-dist/guide/authentication.html +30 -0
  170. package/docs-dist/guide/index.html +27 -0
  171. package/docs-dist/guide/installation.html +33 -0
  172. package/docs-dist/guide/quickstart.html +39 -0
  173. package/docs-dist/hashmap.json +1 -0
  174. package/docs-dist/index.html +27 -0
  175. package/docs-dist/logo.svg +4 -0
  176. package/docs-dist/reference/response-format.html +38 -0
  177. package/docs-dist/version.json +7 -0
  178. package/docs-dist/vp-icons.css +1 -0
  179. package/package.json +28 -17
  180. package/.babelrc +0 -7
  181. package/.dockerignore +0 -5
  182. package/.editorconfig +0 -31
  183. package/.eslintrc.json +0 -22
  184. package/.github/workflows/deploy-docs.yml +0 -54
  185. package/.github/workflows/release.yml +0 -37
  186. package/.github/workflows/test.yml +0 -70
  187. package/.husky/commit-msg +0 -1
  188. package/.husky/pre-commit +0 -1
  189. package/.prettierignore +0 -1
  190. package/.prettierrc +0 -9
  191. package/AGENTS.md +0 -153
  192. package/Dockerfile +0 -18
  193. package/QQ/351/237/263/344/271/220-v0.xmind +0 -0
  194. package/QQ/351/237/263/344/271/220-v1.xmind +0 -0
  195. package/app.ts +0 -68
  196. package/commitlint.config.js +0 -20
  197. package/config/user-info.ts +0 -71
  198. package/index.ts +0 -1
  199. package/jest.config.ts +0 -41
  200. package/middlewares/koa-cors.ts +0 -81
  201. package/module/apis/UCommon/UCommon.ts +0 -13
  202. package/module/apis/album/getAlbumInfo.ts +0 -22
  203. package/module/apis/comments/getComments.ts +0 -23
  204. package/module/apis/digitalAlbum/getDigitalAlbumLists.ts +0 -23
  205. package/module/apis/downloadQQMusic.ts +0 -51
  206. package/module/apis/music/getLyric.ts +0 -34
  207. package/module/apis/mv/getMvByTag.ts +0 -24
  208. package/module/apis/radio/getRadioLists.ts +0 -27
  209. package/module/apis/rank/getTopLists.ts +0 -37
  210. package/module/apis/search/getHotKey.ts +0 -24
  211. package/module/apis/search/getSearchByKey.ts +0 -31
  212. package/module/apis/search/getSmartbox.ts +0 -43
  213. package/module/apis/singers/getSimilarSinger.ts +0 -25
  214. package/module/apis/singers/getSingerDesc.ts +0 -25
  215. package/module/apis/singers/getSingerMv.ts +0 -24
  216. package/module/apis/singers/getSingerStarNum.ts +0 -24
  217. package/module/apis/songLists/songListCategories.ts +0 -22
  218. package/module/apis/songLists/songListDetail.ts +0 -27
  219. package/module/apis/songLists/songLists.ts +0 -35
  220. package/module/apis/u_common.ts +0 -29
  221. package/module/apis/user/checkQQLoginQr.ts +0 -230
  222. package/module/apis/user/getQQLoginQr.ts +0 -28
  223. package/module/apis/user/getUserAvatar.ts +0 -32
  224. package/module/apis/user/getUserLikedSongs.ts +0 -145
  225. package/module/apis/user/getUserPlaylists.ts +0 -163
  226. package/module/apis/y_common.ts +0 -44
  227. package/module/config.ts +0 -24
  228. package/module/index.ts +0 -95
  229. package/music.png +0 -0
  230. package/pnpm-workspace.yaml +0 -2
  231. package/public/index.html +0 -430
  232. package/routers/context/batchGetSongInfo.ts +0 -60
  233. package/routers/context/batchGetSongLists.ts +0 -46
  234. package/routers/context/checkQQLoginQr.ts +0 -19
  235. package/routers/context/cookies.ts +0 -38
  236. package/routers/context/getAlbumInfo.ts +0 -31
  237. package/routers/context/getComments.ts +0 -51
  238. package/routers/context/getDigitalAlbumLists.ts +0 -18
  239. package/routers/context/getDownloadQQMusic.ts +0 -17
  240. package/routers/context/getHotkey.ts +0 -25
  241. package/routers/context/getImageUrl.ts +0 -29
  242. package/routers/context/getLyric.ts +0 -32
  243. package/routers/context/getMusicPlay.ts +0 -102
  244. package/routers/context/getMv.ts +0 -61
  245. package/routers/context/getMvByTag.ts +0 -18
  246. package/routers/context/getMvPlay.ts +0 -114
  247. package/routers/context/getNewDisks.ts +0 -58
  248. package/routers/context/getQQLoginQr.ts +0 -16
  249. package/routers/context/getRadioLists.ts +0 -18
  250. package/routers/context/getRanks.ts +0 -67
  251. package/routers/context/getRecommend.ts +0 -92
  252. package/routers/context/getSearchByKey.ts +0 -34
  253. package/routers/context/getSimilarSinger.ts +0 -29
  254. package/routers/context/getSingerAlbum.ts +0 -58
  255. package/routers/context/getSingerDesc.ts +0 -30
  256. package/routers/context/getSingerHotsong.ts +0 -58
  257. package/routers/context/getSingerList.ts +0 -56
  258. package/routers/context/getSingerMv.ts +0 -41
  259. package/routers/context/getSingerStarNum.ts +0 -29
  260. package/routers/context/getSmartbox.ts +0 -27
  261. package/routers/context/getSongInfo.ts +0 -51
  262. package/routers/context/getSongListCategories.ts +0 -23
  263. package/routers/context/getSongListDetail.ts +0 -22
  264. package/routers/context/getSongLists.ts +0 -30
  265. package/routers/context/getTicketInfo.ts +0 -51
  266. package/routers/context/getTopLists.ts +0 -18
  267. package/routers/context/getUserAvatar.ts +0 -53
  268. package/routers/context/getUserLikedSongs.ts +0 -28
  269. package/routers/context/getUserPlaylists.ts +0 -29
  270. package/routers/context/index.ts +0 -87
  271. package/routers/router.ts +0 -88
  272. package/routers/types.ts +0 -18
  273. package/routers/util.ts +0 -231
  274. package/screenshot/album-image.png +0 -0
  275. package/screenshot/batchGetSongInfo.png +0 -0
  276. package/screenshot/batchGetSongLists.png +0 -0
  277. package/screenshot/downloadQQMusic.png +0 -0
  278. package/screenshot/get-album-image.png +0 -0
  279. package/screenshot/get-play-all-data.png +0 -0
  280. package/screenshot/get-song-album-id.png +0 -0
  281. package/screenshot/get-song-id.png +0 -0
  282. package/screenshot/get-song-image.png +0 -0
  283. package/screenshot/getAlbumInfo.png +0 -0
  284. package/screenshot/getComments-id.png +0 -0
  285. package/screenshot/getComments-param.png +0 -0
  286. package/screenshot/getComments.png +0 -0
  287. package/screenshot/getDigitalAlbumLists.png +0 -0
  288. package/screenshot/getLyric-parse.png +0 -0
  289. package/screenshot/getLyric.png +0 -0
  290. package/screenshot/getMusicPlay.png +0 -0
  291. package/screenshot/getMv.png +0 -0
  292. package/screenshot/getMvByTag.png +0 -0
  293. package/screenshot/getMvPlay.png +0 -0
  294. package/screenshot/getNewDisks.png +0 -0
  295. package/screenshot/getRadioLists.png +0 -0
  296. package/screenshot/getRanks.png +0 -0
  297. package/screenshot/getRecommend.png +0 -0
  298. package/screenshot/getSearchByKey.png +0 -0
  299. package/screenshot/getSimilarSinger.png +0 -0
  300. package/screenshot/getSingerAlbum.png +0 -0
  301. package/screenshot/getSingerDesc.png +0 -0
  302. package/screenshot/getSingerHotsong.png +0 -0
  303. package/screenshot/getSingerList.png +0 -0
  304. package/screenshot/getSingerMv-default.png +0 -0
  305. package/screenshot/getSingerMv-listen.png +0 -0
  306. package/screenshot/getSingerMv-time.png +0 -0
  307. package/screenshot/getSingerStarNum.png +0 -0
  308. package/screenshot/getSmartbox.png +0 -0
  309. package/screenshot/getSongInfo.png +0 -0
  310. package/screenshot/getSongListCategories.png +0 -0
  311. package/screenshot/getSongListDetail.png +0 -0
  312. package/screenshot/getSongLists-params.png +0 -0
  313. package/screenshot/getSongLists.png +0 -0
  314. package/screenshot/getTicketInfo.png +0 -0
  315. package/screenshot/getTopLists.png +0 -0
  316. package/screenshot/gethotkey.png +0 -0
  317. package/screenshot/just-get-play-url.png +0 -0
  318. package/screenshot/musicPlay.png +0 -0
  319. package/screenshot/new-feature-error-tips.png +0 -0
  320. package/screenshot/normalize-cookie.png +0 -0
  321. package/screenshot/qq-music-v0.png +0 -0
  322. package/screenshot/qq-music.png +0 -0
  323. package/screenshot/song-image.png +0 -0
  324. package/screenshot/song-quality-128.png +0 -0
  325. package/screenshot/song-quality-m4a.png +0 -0
  326. package/scripts/build-images.js +0 -36
  327. package/scripts/commit-push.sh +0 -103
  328. package/tests/integration/api/api.test.ts +0 -852
  329. package/tests/integration/middleware/cors.test.ts +0 -41
  330. package/tests/setup/jest.setup.ts +0 -15
  331. package/tests/setup/testUtils.ts +0 -35
  332. package/tests/unit/util/request.test.ts +0 -177
  333. package/tsconfig.json +0 -20
  334. package/tsconfig.test.json +0 -8
  335. package/types/api.ts +0 -105
  336. package/types/global.d.ts +0 -26
  337. package/types/index.d.ts +0 -97
  338. package/util/apiResponse.ts +0 -97
  339. package/util/colors.ts +0 -31
  340. package/util/cookie.ts +0 -40
  341. package/util/loginUtils.ts +0 -26
  342. package/util/lyricParse.ts +0 -86
  343. package/util/request.ts +0 -141
  344. package/vercel.json +0 -15
@@ -1,852 +0,0 @@
1
- // API 集成测试
2
-
3
- import request from 'supertest';
4
- import Koa from 'koa';
5
- import bodyParser from 'koa-bodyparser';
6
- import router from '../../../routers/router';
7
- import cors from '../../../middlewares/koa-cors';
8
- import type { UserInfo } from '../../../types/global';
9
-
10
- // Mock axios 模块
11
- jest.mock('axios', () => {
12
- const mockFn = jest.fn().mockResolvedValue({ data: { code: 0, data: {} } });
13
- (mockFn as jest.Mock & { interceptors: object }).interceptors = {
14
- request: { use: jest.fn() },
15
- response: { use: jest.fn() }
16
- };
17
- return {
18
- get: mockFn,
19
- post: mockFn,
20
- create: jest.fn(() => mockFn),
21
- defaults: {
22
- withCredentials: true,
23
- timeout: 10000,
24
- headers: { post: {} },
25
- responseType: 'json'
26
- }
27
- };
28
- });
29
-
30
- // eslint-disable-next-line @typescript-eslint/no-require-imports
31
- const axios = require('axios');
32
-
33
- interface FetchResponseOptions {
34
- ok?: boolean;
35
- text?: string;
36
- arrayBuffer?: Buffer;
37
- headers?: Record<string, string>;
38
- status?: number;
39
- }
40
-
41
- type ResponseBody = Record<string, unknown> & {
42
- response?: {
43
- code?: number;
44
- data?: Record<string, unknown> & {
45
- playlists?: Array<Record<string, unknown>>;
46
- avatarUrl?: string;
47
- };
48
- error?: string;
49
- playLists?: Record<string, string[]>;
50
- } | string;
51
- error?: string;
52
- isOk?: boolean;
53
- refresh?: boolean;
54
- message?: string;
55
- img?: string;
56
- ptqrtoken?: string;
57
- qrsig?: string;
58
- session?: {
59
- loginUin: string;
60
- cookie: string;
61
- cookieList: string[];
62
- cookieObject: Record<string, string>;
63
- };
64
- data?: unknown[];
65
- status?: number;
66
- };
67
-
68
- interface MockMvUrlEntry {
69
- freeflow_url: string[];
70
- }
71
-
72
- const createFetchResponse = ({
73
- ok = true,
74
- text = '',
75
- arrayBuffer = Buffer.from(''),
76
- headers = {},
77
- status = 200
78
- }: FetchResponseOptions = {}) => ({
79
- ok,
80
- status,
81
- text: async () => text,
82
- arrayBuffer: async () => arrayBuffer,
83
- headers: {
84
- get: (name: string) => {
85
- const matchedKey = Object.keys(headers).find(key => key.toLowerCase() === String(name).toLowerCase());
86
- return matchedKey ? headers[matchedKey] : null;
87
- }
88
- }
89
- });
90
-
91
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
92
- type AnyMiddleware = any;
93
-
94
- function createTestApp(): Koa {
95
- const app = new Koa();
96
- // 使用类型断言绕过中间件类型不兼容问题
97
- app.use(cors() as AnyMiddleware);
98
- app.use(bodyParser() as AnyMiddleware);
99
- app.use(router.routes());
100
- app.use(router.allowedMethods());
101
- return app;
102
- }
103
-
104
- // 创建测试用的完整 UserInfo 对象
105
- function createTestUserInfo(): UserInfo {
106
- return {
107
- loginUin: '123456',
108
- cookie: 'test_cookie=123',
109
- cookieList: ['test_cookie=123'],
110
- cookieObject: { test_cookie: '123' },
111
- refreshData: () => {}
112
- };
113
- }
114
-
115
- const expectSuccessResponse = (responseBody: ResponseBody) => {
116
- expect(responseBody).toHaveProperty('response');
117
- expect(responseBody).not.toHaveProperty('error');
118
- expect(responseBody.response).toHaveProperty('code');
119
- expect(responseBody.response).toHaveProperty('data');
120
- };
121
-
122
- const expectErrorResponse = (responseBody: ResponseBody) => {
123
- expect(responseBody).toHaveProperty('error');
124
- expect(responseBody).not.toHaveProperty('response');
125
- };
126
-
127
- describe('API Integration Tests', () => {
128
- let app: Koa;
129
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
130
- let callback: any;
131
- let mockService: jest.Mock;
132
- let consoleErrorSpy: jest.SpyInstance;
133
- let consoleLogSpy: jest.SpyInstance;
134
-
135
- beforeAll(() => {
136
- app = createTestApp();
137
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
138
- callback = (app as any).callback();
139
- mockService = axios.create();
140
- });
141
-
142
- beforeEach(() => {
143
- jest.clearAllMocks();
144
- mockService.mockResolvedValue({ data: { code: 0, data: {} } });
145
- global.userInfo = createTestUserInfo();
146
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
147
- (global as any).fetch = jest.fn();
148
- consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
149
- consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined);
150
- });
151
-
152
- afterEach(() => {
153
- consoleErrorSpy.mockRestore();
154
- consoleLogSpy.mockRestore();
155
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
156
- delete (global as any).fetch;
157
- });
158
-
159
- describe('GET /getHotkey', () => {
160
- test('should return hot search keywords', async () => {
161
- const response = await request(callback).get('/getHotkey').expect(200);
162
- expectSuccessResponse(response.body);
163
- }, 10000);
164
- });
165
-
166
- describe('GET /getTopLists', () => {
167
- test('should return top lists', async () => {
168
- const response = await request(callback).get('/getTopLists').expect(200);
169
- expectSuccessResponse(response.body);
170
- }, 10000);
171
- });
172
-
173
- describe('GET /getSearchByKey', () => {
174
- test('should search with query param', async () => {
175
- const response = await request(callback)
176
- .get('/getSearchByKey')
177
- .query({ key: 'test' })
178
- .expect(200);
179
- expectSuccessResponse(response.body);
180
- }, 10000);
181
-
182
- test('should search with path param (backward compatibility)', async () => {
183
- const response = await request(callback).get('/getSearchByKey/test').expect(200);
184
- expectSuccessResponse(response.body);
185
- }, 10000);
186
-
187
- test('should return 400 for missing search key', async () => {
188
- const response = await request(callback).get('/getSearchByKey').expect(400);
189
- expect(response.body.response).toBe('search key is null');
190
- });
191
- });
192
-
193
- describe('GET /getLyric', () => {
194
- test('should return lyric with query param', async () => {
195
- const response = await request(callback)
196
- .get('/getLyric')
197
- .query({ songmid: 'test123' })
198
- .expect(200);
199
- expectSuccessResponse(response.body);
200
- }, 10000);
201
- });
202
-
203
- describe('POST /batchGetSongInfo', () => {
204
- test('should batch get song info', async () => {
205
- mockService
206
- .mockResolvedValueOnce({ data: { code: 0, data: [{ mid: 'test1' }] } })
207
- .mockResolvedValueOnce({ data: { code: 0, data: [{ mid: 'test2' }] } });
208
-
209
- const response = await request(callback)
210
- .post('/batchGetSongInfo')
211
- .send({ songs: [['test1', '1'], ['test2', '2']] })
212
- .expect(200);
213
-
214
- expectSuccessResponse(response.body);
215
- expect(Array.isArray(response.body.response.data)).toBe(true);
216
- expect(response.body.response.data).toHaveLength(2);
217
- expect(mockService).toHaveBeenCalledTimes(2);
218
- }, 10000);
219
-
220
- test('should return empty array when songs is missing', async () => {
221
- const response = await request(callback)
222
- .post('/batchGetSongInfo')
223
- .send({})
224
- .expect(200);
225
-
226
- expectSuccessResponse(response.body);
227
- expect(response.body.response.data).toEqual([]);
228
- expect(mockService).not.toHaveBeenCalled();
229
- });
230
-
231
- test('should use empty string as default song_id when it is omitted', async () => {
232
- mockService.mockResolvedValueOnce({ data: { code: 0, data: [{ mid: 'test1' }] } });
233
-
234
- await request(callback)
235
- .post('/batchGetSongInfo')
236
- .send({ songs: [['test1']] })
237
- .expect(200);
238
-
239
- const firstCallConfig = mockService.mock.calls[0][0] as { params: { data: { songinfo: { param: { song_id: string } } } } };
240
- expect(firstCallConfig.params.data.songinfo.param.song_id).toBe('');
241
- });
242
- });
243
-
244
- describe('GET /user/getUserPlaylists', () => {
245
- test('should return 400 when uin is missing', async () => {
246
- const response = await request(callback).get('/user/getUserPlaylists').expect(400);
247
-
248
- expectErrorResponse(response.body);
249
- expect(response.body.error).toBe('缺少 uin 参数');
250
- });
251
-
252
- test('should return playlists when upstream payload contains playlist field', async () => {
253
- mockService.mockResolvedValueOnce({
254
- data: {
255
- code: 0,
256
- data: {
257
- playlists: [{ dissid: '1', dissname: 'test playlist' }]
258
- }
259
- }
260
- });
261
-
262
- const response = await request(callback)
263
- .get('/user/getUserPlaylists')
264
- .query({ uin: '123456789' })
265
- .expect(200);
266
-
267
- expectSuccessResponse(response.body);
268
- expect(response.body.response.data.playlists).toEqual([{ dissid: '1', dissname: 'test playlist' }]);
269
- });
270
-
271
- test('should return playlists when upstream payload contains creator.playlist field', async () => {
272
- mockService.mockResolvedValueOnce({
273
- data: {
274
- code: 0,
275
- data: {
276
- creator: {
277
- playlist: [{ dissid: '2', dissname: 'creator playlist' }]
278
- }
279
- }
280
- }
281
- });
282
-
283
- const response = await request(callback)
284
- .get('/user/getUserPlaylists')
285
- .query({ uin: '123456789' })
286
- .expect(200);
287
-
288
- expectSuccessResponse(response.body);
289
- expect(response.body.response.data.playlists).toEqual([{ dissid: '2', dissname: 'creator playlist' }]);
290
- });
291
-
292
- test('should return 502 when upstream payload does not contain playlist field', async () => {
293
- mockService.mockResolvedValueOnce({
294
- data: {
295
- code: 0,
296
- data: {
297
- profile: { uin: '123456789' }
298
- }
299
- }
300
- });
301
-
302
- const response = await request(callback)
303
- .get('/user/getUserPlaylists')
304
- .query({ uin: '123456789' })
305
- .expect(502);
306
-
307
- expectErrorResponse(response.body);
308
- expect(response.body.error).toBe('用户歌单响应中未找到歌单列表字段');
309
- });
310
- });
311
-
312
- describe('GET /user/getUserAvatar', () => {
313
- test('should return 400 when k and uin are both missing', async () => {
314
- const response = await request(callback).get('/user/getUserAvatar').expect(400);
315
-
316
- expectErrorResponse(response.body);
317
- expect(response.body.error).toBe('缺少 k 或 uin 参数');
318
- });
319
-
320
- test.each([
321
- ['non-numeric size', 'abc'],
322
- ['zero size', '0'],
323
- ['negative size', '-1']
324
- ])('should return 400 for %s', async (_caseName, size) => {
325
- const response = await request(callback)
326
- .get('/user/getUserAvatar')
327
- .query({ uin: '123456789', size })
328
- .expect(400);
329
-
330
- expectErrorResponse(response.body);
331
- expect(response.body.error).toBe('size 参数无效');
332
- });
333
-
334
- test('should return avatar url when uin is provided', async () => {
335
- const response = await request(callback)
336
- .get('/user/getUserAvatar')
337
- .query({ uin: '123456789', size: 140 })
338
- .expect(200);
339
-
340
- expectSuccessResponse(response.body);
341
- expect(response.body.response.data.avatarUrl).toBe(
342
- 'https://q.qlogo.cn/headimg_dl?dst_uin=123456789&spec=140'
343
- );
344
- });
345
-
346
- test('should only use the first query value when duplicated params are provided', async () => {
347
- const response = await request(callback)
348
- .get('/user/getUserAvatar?uin=123456789&uin=987654321&size=140&size=640')
349
- .expect(200);
350
-
351
- expectSuccessResponse(response.body);
352
- expect(response.body.response.data.avatarUrl).toBe(
353
- 'https://q.qlogo.cn/headimg_dl?dst_uin=123456789&spec=140'
354
- );
355
- });
356
-
357
- test('should prefer k over uin when both are provided', async () => {
358
- const response = await request(callback)
359
- .get('/user/getUserAvatar')
360
- .query({ k: 'mock-k', uin: '123456789', size: 640 })
361
- .expect(200);
362
-
363
- expectSuccessResponse(response.body);
364
- expect(response.body.response.data.avatarUrl).toBe(
365
- 'https://thirdqq.qlogo.cn/g?b=sdk&k=mock-k&s=640'
366
- );
367
- });
368
- });
369
-
370
- describe('GET /getMvPlay', () => {
371
- test('should return 400 when vid is missing', async () => {
372
- const response = await request(callback).get('/getMvPlay').expect(400);
373
-
374
- expect(response.body.response).toBe('vid is null');
375
- });
376
-
377
- test('should return grouped playLists when upstream returns nested freeflow urls', async () => {
378
- const createMvUrlEntry = (...urls: string[]): MockMvUrlEntry => ({
379
- freeflow_url: urls
380
- });
381
-
382
- mockService.mockResolvedValueOnce({
383
- data: {
384
- getMVUrl: {
385
- data: {
386
- testVid: {
387
- mp4: [
388
- createMvUrlEntry('https://cdn.example.com/video.f10.mp4'),
389
- createMvUrlEntry('https://cdn.example.com/video.f20.mp4')
390
- ],
391
- hls: [createMvUrlEntry('https://cdn.example.com/video.f30.mp4')]
392
- }
393
- }
394
- }
395
- }
396
- });
397
-
398
- const response = await request(callback)
399
- .get('/getMvPlay')
400
- .query({ vid: 'testVid' })
401
- .expect(200);
402
-
403
- expect((response.body as ResponseBody).response).toMatchObject({
404
- playLists: {
405
- f10: ['https://cdn.example.com/video.f10.mp4'],
406
- f20: ['https://cdn.example.com/video.f20.mp4'],
407
- f30: ['https://cdn.example.com/video.f30.mp4'],
408
- f40: []
409
- }
410
- });
411
- });
412
-
413
- test('should handle payloads with only hls urls', async () => {
414
- const createMvUrlEntry = (...urls: string[]): MockMvUrlEntry => ({
415
- freeflow_url: urls
416
- });
417
-
418
- mockService.mockResolvedValueOnce({
419
- data: {
420
- getMVUrl: {
421
- data: {
422
- testVid: {
423
- hls: [
424
- createMvUrlEntry('https://cdn.example.com/video.f30.mp4'),
425
- createMvUrlEntry('https://cdn.example.com/video.f40.mp4')
426
- ]
427
- }
428
- }
429
- }
430
- }
431
- });
432
-
433
- const response = await request(callback)
434
- .get('/getMvPlay')
435
- .query({ vid: 'testVid' })
436
- .expect(200);
437
-
438
- expect((response.body as ResponseBody).response).toMatchObject({
439
- playLists: {
440
- f10: [],
441
- f20: [],
442
- f30: ['https://cdn.example.com/video.f30.mp4'],
443
- f40: ['https://cdn.example.com/video.f40.mp4']
444
- }
445
- });
446
- });
447
-
448
- test('should return empty grouped lists when all freeflow urls are empty', async () => {
449
- mockService.mockResolvedValueOnce({
450
- data: {
451
- getMVUrl: {
452
- data: {
453
- testVid: {
454
- mp4: [{ freeflow_url: [] }],
455
- hls: [{ freeflow_url: [] }]
456
- }
457
- }
458
- }
459
- }
460
- });
461
-
462
- const response = await request(callback)
463
- .get('/getMvPlay')
464
- .query({ vid: 'testVid' })
465
- .expect(200);
466
-
467
- expect((response.body as ResponseBody).response).toMatchObject({
468
- playLists: {
469
- f10: [],
470
- f20: [],
471
- f30: [],
472
- f40: []
473
- }
474
- });
475
- });
476
-
477
- test('should return 502 when upstream mv url payload is empty', async () => {
478
- mockService.mockResolvedValueOnce({
479
- data: {
480
- getMVUrl: {
481
- data: {}
482
- }
483
- }
484
- });
485
-
486
- const response = await request(callback)
487
- .get('/getMvPlay')
488
- .query({ vid: 'testVid' })
489
- .expect(502);
490
-
491
- expect((response.body as ResponseBody).response).toEqual({
492
- data: null,
493
- error: 'Failed to get MV URL data'
494
- });
495
- });
496
- });
497
-
498
- describe('POST /batchGetSongLists', () => {
499
- test('should use default categoryIds when request body omits them', async () => {
500
- mockService.mockResolvedValueOnce({
501
- data: {
502
- code: 0,
503
- data: {
504
- list: [{ disstid: 'default-category' }]
505
- }
506
- }
507
- });
508
-
509
- const response = await request(callback)
510
- .post('/batchGetSongLists')
511
- .send({})
512
- .expect(200);
513
-
514
- expect(response.body).toEqual({
515
- status: 200,
516
- data: [{ list: [{ disstid: 'default-category' }] }]
517
- });
518
- expect(mockService).toHaveBeenCalledTimes(1);
519
- const firstCallConfig = mockService.mock.calls[0][0] as { params: { categoryId: number } };
520
- expect(firstCallConfig.params.categoryId).toBe(10000000);
521
- });
522
-
523
- test('should request each categoryId and merge downstream success payloads', async () => {
524
- mockService
525
- .mockResolvedValueOnce({ data: { code: 0, data: { list: [{ disstid: 'cat-1' }] } } })
526
- .mockResolvedValueOnce({ data: { code: 0, data: { list: [{ disstid: 'cat-2' }] } } });
527
-
528
- const response = await request(callback)
529
- .post('/batchGetSongLists')
530
- .send({ categoryIds: [1, 2], limit: 10, page: 2, sortId: 5 })
531
- .expect(200);
532
-
533
- expect(response.body).toEqual({
534
- status: 200,
535
- data: [{ list: [{ disstid: 'cat-1' }] }, { list: [{ disstid: 'cat-2' }] }]
536
- });
537
- expect(mockService).toHaveBeenCalledTimes(2);
538
- });
539
-
540
- test('should preserve downstream business error payloads', async () => {
541
- mockService.mockResolvedValueOnce({
542
- data: {
543
- code: 1,
544
- message: 'mock song list error'
545
- }
546
- });
547
-
548
- const response = await request(callback)
549
- .post('/batchGetSongLists')
550
- .send({ categoryIds: [1] })
551
- .expect(200);
552
-
553
- expect(response.body).toEqual({
554
- status: 200,
555
- data: [{ code: 1, message: 'mock song list error' }]
556
- });
557
- });
558
- });
559
-
560
- describe('Error handling', () => {
561
- test('should return 404 for unknown route', async () => {
562
- await request(callback).get('/unknown-route').expect(404);
563
- });
564
-
565
- test('should return 500 when downstream request rejects', async () => {
566
- mockService.mockRejectedValueOnce(new Error('downstream failure'));
567
- const response = await request(callback).get('/getHotkey').expect(500);
568
- expect(response.body.error).toBeDefined();
569
- });
570
-
571
- test('should preserve non-success business response code from downstream', async () => {
572
- mockService.mockResolvedValueOnce({ data: { code: 1, message: 'mock business error' } });
573
- const response = await request(callback).get('/getHotkey').expect(200);
574
- expect(response.body.response).toEqual({ code: 1, message: 'mock business error' });
575
- });
576
- });
577
-
578
- describe('QQ QR login endpoints', () => {
579
- test('GET /getQQLoginQr should return base64 QR and ptqrtoken/qrsig', async () => {
580
- const qrBuffer = Buffer.from('fake-qr-image-bytes');
581
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
582
- (global as any).fetch = jest.fn().mockResolvedValueOnce(
583
- createFetchResponse({
584
- arrayBuffer: qrBuffer,
585
- headers: {
586
- 'Set-Cookie': 'qrsig=mockQrSig; Path=/; HttpOnly'
587
- }
588
- })
589
- );
590
-
591
- const response = await request(callback).get('/getQQLoginQr').expect(200);
592
-
593
- expect(response.body.img).toBe(`data:image/png;base64,${qrBuffer.toString('base64')}`);
594
- expect(response.body.ptqrtoken).toBeDefined();
595
- expect(response.body.qrsig).toBe('mockQrSig');
596
- });
597
-
598
- test('POST /checkQQLoginQr should return 400 when ptqrtoken/qrsig are missing', async () => {
599
- const response = await request(callback).post('/checkQQLoginQr').send({}).expect(400);
600
-
601
- expect(response.body.error).toBe('参数错误');
602
- });
603
-
604
- test('POST /checkQQLoginQr should return session on success', async () => {
605
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
606
- (global as any).fetch = jest
607
- .fn()
608
- .mockResolvedValueOnce(
609
- createFetchResponse({
610
- text: "ptuiCB('0','0','登录成功!','https://ssl.ptlogin2.qq.com/check_sig?uin=123456789','0','0');",
611
- headers: {
612
- 'Set-Cookie': 'pt_login_sig=abc123; Path=/; HttpOnly'
613
- }
614
- })
615
- )
616
- .mockResolvedValueOnce(
617
- createFetchResponse({
618
- headers: {
619
- 'Set-Cookie': 'p_skey=mockPSkey; Path=/; HttpOnly, uin=o123456789; Path=/; HttpOnly'
620
- }
621
- })
622
- )
623
- .mockResolvedValueOnce(
624
- createFetchResponse({
625
- status: 302,
626
- headers: {
627
- Location: 'https://y.qq.com/portal/wx_redirect.html?code=mockAuthCode',
628
- 'Set-Cookie': 'graph_key=graphValue; Path=/; HttpOnly'
629
- }
630
- })
631
- )
632
- .mockResolvedValueOnce(
633
- createFetchResponse({
634
- headers: {
635
- 'Set-Cookie': 'qm_keyst=finalValue; Path=/; HttpOnly'
636
- }
637
- })
638
- );
639
-
640
- const response = await request(callback)
641
- .post('/checkQQLoginQr')
642
- .send({ ptqrtoken: 'mockToken', qrsig: 'mockQrSig' })
643
- .expect(200);
644
-
645
- expect(response.body.isOk).toBe(true);
646
- expect(response.body.message).toBe('登录成功');
647
- expect(response.body.session).toBeDefined();
648
- expect(response.body.session.loginUin).toBe('o123456789');
649
- expect(response.body.session.cookie).toContain('uin=o123456789');
650
- expect(response.body.session.cookie).toContain('qm_keyst=finalValue');
651
- expect(Array.isArray(response.body.session.cookieList)).toBe(true);
652
- expect(response.body.session.cookieList.length).toBeGreaterThan(0);
653
- expect(response.body.session.cookieObject).toMatchObject({
654
- uin: 'o123456789',
655
- p_skey: 'mockPSkey',
656
- qm_keyst: 'finalValue'
657
- });
658
- });
659
-
660
- test('POST /checkQQLoginQr should return 502 when checkSigUrl cannot be extracted', async () => {
661
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
662
- (global as any).fetch = jest.fn().mockResolvedValueOnce(
663
- createFetchResponse({
664
- text: "ptuiCB('0','0','登录成功!','','0','0');",
665
- headers: {
666
- 'Set-Cookie': 'pt_login_sig=abc123; Path=/; HttpOnly'
667
- }
668
- })
669
- );
670
-
671
- const response = await request(callback)
672
- .post('/checkQQLoginQr')
673
- .send({ ptqrtoken: 'mockToken', qrsig: 'mockQrSig' })
674
- .expect(502);
675
-
676
- expect(response.body.error).toBe('Failed to extract checkSigUrl from response');
677
- });
678
-
679
- test('POST /checkQQLoginQr should return 502 when p_skey is missing', async () => {
680
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
681
- (global as any).fetch = jest
682
- .fn()
683
- .mockResolvedValueOnce(
684
- createFetchResponse({
685
- text: "ptuiCB('0','0','登录成功!','https://ssl.ptlogin2.qq.com/check_sig?uin=123456789','0','0');",
686
- headers: {
687
- 'Set-Cookie': 'pt_login_sig=abc123; Path=/; HttpOnly'
688
- }
689
- })
690
- )
691
- .mockResolvedValueOnce(
692
- createFetchResponse({
693
- headers: {
694
- 'Set-Cookie': 'uin=o123456789; Path=/; HttpOnly'
695
- }
696
- })
697
- );
698
-
699
- const response = await request(callback)
700
- .post('/checkQQLoginQr')
701
- .send({ ptqrtoken: 'mockToken', qrsig: 'mockQrSig' })
702
- .expect(502);
703
-
704
- expect(response.body.error).toBe('Failed to extract p_skey from response');
705
- });
706
-
707
- test('POST /checkQQLoginQr should return 502 when authorize response does not redirect', async () => {
708
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
709
- (global as any).fetch = jest
710
- .fn()
711
- .mockResolvedValueOnce(
712
- createFetchResponse({
713
- text: "ptuiCB('0','0','登录成功!','https://ssl.ptlogin2.qq.com/check_sig?uin=123456789','0','0');",
714
- headers: {
715
- 'Set-Cookie': 'pt_login_sig=abc123; Path=/; HttpOnly'
716
- }
717
- })
718
- )
719
- .mockResolvedValueOnce(
720
- createFetchResponse({
721
- headers: {
722
- 'Set-Cookie': 'p_skey=mockPSkey; Path=/; HttpOnly, uin=o123456789; Path=/; HttpOnly'
723
- }
724
- })
725
- )
726
- .mockResolvedValueOnce(
727
- createFetchResponse({
728
- status: 200,
729
- headers: {
730
- 'Content-Type': 'text/html; charset=utf-8'
731
- }
732
- })
733
- );
734
-
735
- const response = await request(callback)
736
- .post('/checkQQLoginQr')
737
- .send({ ptqrtoken: 'mockToken', qrsig: 'mockQrSig' })
738
- .expect(502);
739
-
740
- expect(response.body.error).toBe('授权响应异常,未返回跳转地址');
741
- });
742
-
743
- test('POST /checkQQLoginQr should return 502 when authorize redirect location misses code', async () => {
744
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
745
- (global as any).fetch = jest
746
- .fn()
747
- .mockResolvedValueOnce(
748
- createFetchResponse({
749
- text: "ptuiCB('0','0','登录成功!','https://ssl.ptlogin2.qq.com/check_sig?uin=123456789','0','0');",
750
- headers: {
751
- 'Set-Cookie': 'pt_login_sig=abc123; Path=/; HttpOnly'
752
- }
753
- })
754
- )
755
- .mockResolvedValueOnce(
756
- createFetchResponse({
757
- headers: {
758
- 'Set-Cookie': 'p_skey=mockPSkey; Path=/; HttpOnly, uin=o123456789; Path=/; HttpOnly'
759
- }
760
- })
761
- )
762
- .mockResolvedValueOnce(
763
- createFetchResponse({
764
- status: 302,
765
- headers: {
766
- Location: 'https://y.qq.com/portal/wx_redirect.html?state=mockState'
767
- }
768
- })
769
- );
770
-
771
- const response = await request(callback)
772
- .post('/checkQQLoginQr')
773
- .send({ ptqrtoken: 'mockToken', qrsig: 'mockQrSig' })
774
- .expect(502);
775
-
776
- expect(response.body.error).toBe('授权响应中未找到 code 参数');
777
- });
778
-
779
- test('POST /checkQQLoginQr should return 504 and 登录检查超时 on fetch timeout', async () => {
780
- const abortError = new Error('Aborted');
781
- abortError.name = 'AbortError';
782
-
783
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
784
- (global as any).fetch = jest.fn().mockRejectedValueOnce(abortError);
785
-
786
- const response = await request(callback)
787
- .post('/checkQQLoginQr')
788
- .send({ ptqrtoken: 'mockToken', qrsig: 'mockQrSig' })
789
- .expect(504);
790
-
791
- expect(response.body).toEqual({
792
- error: '登录检查超时'
793
- });
794
- });
795
-
796
- test('POST /checkQQLoginQr should map non-success polling result to 未扫描二维码', async () => {
797
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
798
- (global as any).fetch = jest.fn().mockResolvedValueOnce(
799
- createFetchResponse({
800
- text: 'ptuiCB("66","0","","0","二维码未失效","","");'
801
- })
802
- );
803
-
804
- const response = await request(callback)
805
- .post('/checkQQLoginQr')
806
- .send({ ptqrtoken: 'mockToken', qrsig: 'mockQrSig' })
807
- .expect(200);
808
-
809
- expect(response.body).toEqual({
810
- isOk: false,
811
- refresh: false,
812
- message: '未扫描二维码'
813
- });
814
- });
815
-
816
- test('POST /checkQQLoginQr should map expired polling result to 二维码已失效', async () => {
817
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
818
- (global as any).fetch = jest.fn().mockResolvedValueOnce(
819
- createFetchResponse({
820
- text: 'ptuiCB("65","0","二维码已失效","0","二维码已失效","","");'
821
- })
822
- );
823
-
824
- const response = await request(callback)
825
- .post('/checkQQLoginQr')
826
- .send({ ptqrtoken: 'mockToken', qrsig: 'mockQrSig' })
827
- .expect(200);
828
-
829
- expect(response.body).toEqual({
830
- isOk: false,
831
- refresh: true,
832
- message: '二维码已失效'
833
- });
834
- });
835
-
836
- test('GET /getQQLoginQr should map upstream failure to 502', async () => {
837
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
838
- (global as any).fetch = jest.fn().mockRejectedValueOnce(new Error('network timeout'));
839
-
840
- const response = await request(callback).get('/getQQLoginQr').expect(502);
841
-
842
- expect(response.body.error).toBe('Failed to fetch QQ login QR');
843
- });
844
- });
845
-
846
- describe('CORS middleware', () => {
847
- test('should set CORS headers', async () => {
848
- const response = await request(callback).get('/getHotkey').expect(200);
849
- expect(response.headers['access-control-allow-origin']).toBeDefined();
850
- });
851
- });
852
- });