@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.
- package/CHANGELOG.md +120 -42
- package/dist/app.js +58 -0
- package/dist/config/user-info.js +59 -0
- package/dist/index.js +3 -0
- package/dist/jest.config.js +40 -0
- package/dist/middlewares/koa-cors.js +63 -0
- package/dist/module/apis/UCommon/UCommon.js +10 -0
- package/dist/module/apis/album/getAlbumInfo.js +23 -0
- package/dist/module/apis/comments/getComments.js +24 -0
- package/dist/module/apis/digitalAlbum/getDigitalAlbumLists.js +22 -0
- package/dist/module/apis/downloadQQMusic.js +48 -0
- package/dist/module/apis/extend/getPlaylistTags.js +158 -0
- package/dist/module/apis/music/getLyric.js +32 -0
- package/dist/module/apis/mv/getMvByTag.js +23 -0
- package/dist/module/apis/radio/getRadioLists.js +26 -0
- package/dist/module/apis/rank/getTopLists.js +35 -0
- package/dist/module/apis/recommend/getDailyRecommend.js +124 -0
- package/dist/module/apis/recommend/getPersonalRecommend.js +114 -0
- package/dist/module/apis/search/getHotKey.js +25 -0
- package/dist/module/apis/search/getSearchByKey.js +32 -0
- package/dist/module/apis/search/getSmartbox.js +40 -0
- package/dist/module/apis/singers/getSimilarSinger.js +24 -0
- package/dist/module/apis/singers/getSingerDesc.js +24 -0
- package/dist/module/apis/singers/getSingerMv.js +23 -0
- package/dist/module/apis/singers/getSingerStarNum.js +23 -0
- package/dist/module/apis/songLists/songListCategories.js +23 -0
- package/dist/module/apis/songLists/songListDetail.js +28 -0
- package/dist/module/apis/songLists/songLists.js +35 -0
- package/dist/module/apis/u_common.js +56 -0
- package/dist/module/apis/user/checkQQLoginQr.js +189 -0
- package/dist/module/apis/user/getQQLoginQr.js +23 -0
- package/dist/module/apis/user/getUserAvatar.js +25 -0
- package/dist/module/apis/user/getUserLikedSongs.js +129 -0
- package/dist/module/apis/user/getUserPlaylists.js +138 -0
- package/dist/module/apis/y_common.js +69 -0
- package/dist/module/config.js +24 -0
- package/dist/module/index.js +70 -0
- package/dist/package.json +117 -0
- package/dist/routers/context/batchGetSongInfo.js +53 -0
- package/dist/routers/context/batchGetSongLists.js +37 -0
- package/dist/routers/context/checkQQLoginQr.js +16 -0
- package/dist/routers/context/cookies.js +36 -0
- package/dist/routers/context/getAlbumInfo.js +29 -0
- package/dist/routers/context/getComments.js +38 -0
- package/dist/routers/context/getDailyRecommend.js +45 -0
- package/dist/routers/context/getDigitalAlbumLists.js +16 -0
- package/dist/routers/context/getDownloadQQMusic.js +16 -0
- package/dist/routers/context/getHotkey.js +21 -0
- package/dist/routers/context/getImageUrl.js +25 -0
- package/dist/routers/context/getLyric.js +29 -0
- package/dist/routers/context/getMusicPlay.js +91 -0
- package/dist/routers/context/getMv.js +57 -0
- package/dist/routers/context/getMvByTag.js +16 -0
- package/dist/routers/context/getMvPlay.js +105 -0
- package/dist/routers/context/getNewDisks.js +52 -0
- package/dist/routers/context/getPersonalRecommend.js +54 -0
- package/dist/routers/context/getPlaylistTags.js +60 -0
- package/dist/routers/context/getQQLoginQr.js +14 -0
- package/dist/routers/context/getRadioLists.js +16 -0
- package/dist/routers/context/getRanks.js +60 -0
- package/dist/routers/context/getRecommend.js +88 -0
- package/dist/routers/context/getSearchByKey.js +32 -0
- package/dist/routers/context/getSimilarSinger.js +27 -0
- package/dist/routers/context/getSingerAlbum.js +54 -0
- package/dist/routers/context/getSingerDesc.js +28 -0
- package/dist/routers/context/getSingerHotsong.js +54 -0
- package/dist/routers/context/getSingerList.js +51 -0
- package/dist/routers/context/getSingerMv.js +36 -0
- package/dist/routers/context/getSingerStarNum.js +27 -0
- package/dist/routers/context/getSmartbox.js +27 -0
- package/dist/routers/context/getSongInfo.js +47 -0
- package/dist/routers/context/getSongListCategories.js +23 -0
- package/dist/routers/context/getSongListDetail.js +19 -0
- package/dist/routers/context/getSongLists.js +25 -0
- package/dist/routers/context/getTicketInfo.js +47 -0
- package/dist/routers/context/getTopLists.js +16 -0
- package/dist/routers/context/getUserAvatar.js +48 -0
- package/dist/routers/context/getUserLikedSongs.js +24 -0
- package/dist/routers/context/getUserPlaylists.js +24 -0
- package/dist/routers/context/index.js +107 -0
- package/dist/routers/router.js +69 -0
- package/dist/routers/types.js +2 -0
- package/dist/routers/util.js +188 -0
- package/dist/types/api.js +55 -0
- package/dist/util/apiResponse.js +88 -0
- package/dist/util/colors.js +19 -0
- package/dist/util/cookie.js +26 -0
- package/dist/util/loginUtils.js +30 -0
- package/dist/util/lyricParse.js +72 -0
- package/dist/util/request.js +109 -0
- package/docs-dist/404.html +24 -0
- package/docs-dist/CHANGELOG-ARCHITECTURE.html +131 -0
- package/docs-dist/COOKIE_CONFIG_GUIDE.html +39 -0
- package/docs-dist/README.html +447 -0
- package/docs-dist/TEST_USER_PLAYLISTS.html +42 -0
- package/docs-dist/USER_AVATAR_GUIDE.html +100 -0
- package/docs-dist/api/comments.html +48 -0
- package/docs-dist/api/index.html +27 -0
- package/docs-dist/api/music.html +51 -0
- package/docs-dist/api/other.html +33 -0
- package/docs-dist/api/playlist.html +77 -0
- package/docs-dist/api/rank.html +48 -0
- package/docs-dist/api/search.html +62 -0
- package/docs-dist/api/singer.html +47 -0
- package/docs-dist/api/user.html +64 -0
- package/docs-dist/assets/CHANGELOG-ARCHITECTURE.md.BOe0ZtyR.js +105 -0
- package/docs-dist/assets/CHANGELOG-ARCHITECTURE.md.BOe0ZtyR.lean.js +1 -0
- package/docs-dist/assets/COOKIE_CONFIG_GUIDE.md.D68AwXR2.js +13 -0
- package/docs-dist/assets/COOKIE_CONFIG_GUIDE.md.D68AwXR2.lean.js +1 -0
- package/docs-dist/assets/README.md.ZJQGJ1Gb.js +421 -0
- package/docs-dist/assets/README.md.ZJQGJ1Gb.lean.js +1 -0
- package/docs-dist/assets/TEST_USER_PLAYLISTS.md.C02575X2.js +16 -0
- package/docs-dist/assets/TEST_USER_PLAYLISTS.md.C02575X2.lean.js +1 -0
- package/docs-dist/assets/USER_AVATAR_GUIDE.md.BOqjn5Cm.js +74 -0
- package/docs-dist/assets/USER_AVATAR_GUIDE.md.BOqjn5Cm.lean.js +1 -0
- package/docs-dist/assets/api_comments.md.DADvndEA.js +22 -0
- package/docs-dist/assets/api_comments.md.DADvndEA.lean.js +1 -0
- package/docs-dist/assets/api_index.md.D5IASxxG.js +1 -0
- package/docs-dist/assets/api_index.md.D5IASxxG.lean.js +1 -0
- package/docs-dist/assets/api_music.md.BgB8NmZq.js +25 -0
- package/docs-dist/assets/api_music.md.BgB8NmZq.lean.js +1 -0
- package/docs-dist/assets/api_other.md.BkRWXX2z.js +7 -0
- package/docs-dist/assets/api_other.md.BkRWXX2z.lean.js +1 -0
- package/docs-dist/assets/api_playlist.md.Dc0hTrZ4.js +51 -0
- package/docs-dist/assets/api_playlist.md.Dc0hTrZ4.lean.js +1 -0
- package/docs-dist/assets/api_rank.md.DRisCFyT.js +22 -0
- package/docs-dist/assets/api_rank.md.DRisCFyT.lean.js +1 -0
- package/docs-dist/assets/api_search.md.DNnMUZK0.js +36 -0
- package/docs-dist/assets/api_search.md.DNnMUZK0.lean.js +1 -0
- package/docs-dist/assets/api_singer.md.DCmuxQkk.js +21 -0
- package/docs-dist/assets/api_singer.md.DCmuxQkk.lean.js +1 -0
- package/docs-dist/assets/api_user.md.Cjm9GG3z.js +38 -0
- package/docs-dist/assets/api_user.md.Cjm9GG3z.lean.js +1 -0
- package/docs-dist/assets/app.Dx_1wB58.js +1 -0
- package/docs-dist/assets/chunks/@localSearchIndexroot.CMY5EIwU.js +1 -0
- package/docs-dist/assets/chunks/VPLocalSearchBox.DwKWtsdX.js +9 -0
- package/docs-dist/assets/chunks/framework.o40iizuP.js +19 -0
- package/docs-dist/assets/chunks/theme.pGVgJ9Cx.js +2 -0
- package/docs-dist/assets/guide_architecture.md.DGtNyuMH.js +258 -0
- package/docs-dist/assets/guide_architecture.md.DGtNyuMH.lean.js +1 -0
- package/docs-dist/assets/guide_authentication.md.mtI5LfCw.js +4 -0
- package/docs-dist/assets/guide_authentication.md.mtI5LfCw.lean.js +1 -0
- package/docs-dist/assets/guide_index.md.B-0SG46T.js +1 -0
- package/docs-dist/assets/guide_index.md.B-0SG46T.lean.js +1 -0
- package/docs-dist/assets/guide_installation.md.k-KpAfxv.js +7 -0
- package/docs-dist/assets/guide_installation.md.k-KpAfxv.lean.js +1 -0
- package/docs-dist/assets/guide_quickstart.md.Bff_KFOD.js +13 -0
- package/docs-dist/assets/guide_quickstart.md.Bff_KFOD.lean.js +1 -0
- package/docs-dist/assets/index.md.xrs-uIyo.js +1 -0
- package/docs-dist/assets/index.md.xrs-uIyo.lean.js +1 -0
- package/docs-dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
- package/docs-dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
- package/docs-dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
- package/docs-dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
- package/docs-dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
- package/docs-dist/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
- package/docs-dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
- package/docs-dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
- package/docs-dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
- package/docs-dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
- package/docs-dist/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
- package/docs-dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
- package/docs-dist/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
- package/docs-dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
- package/docs-dist/assets/reference_response-format.md.DKYTK6uJ.js +12 -0
- package/docs-dist/assets/reference_response-format.md.DKYTK6uJ.lean.js +1 -0
- package/docs-dist/assets/style.DM4qKDd4.css +1 -0
- package/docs-dist/guide/architecture.html +284 -0
- package/docs-dist/guide/authentication.html +30 -0
- package/docs-dist/guide/index.html +27 -0
- package/docs-dist/guide/installation.html +33 -0
- package/docs-dist/guide/quickstart.html +39 -0
- package/docs-dist/hashmap.json +1 -0
- package/docs-dist/index.html +27 -0
- package/docs-dist/logo.svg +4 -0
- package/docs-dist/reference/response-format.html +38 -0
- package/docs-dist/version.json +7 -0
- package/docs-dist/vp-icons.css +1 -0
- package/package.json +28 -17
- package/.babelrc +0 -7
- package/.dockerignore +0 -5
- package/.editorconfig +0 -31
- package/.eslintrc.json +0 -22
- package/.github/workflows/deploy-docs.yml +0 -54
- package/.github/workflows/release.yml +0 -37
- package/.github/workflows/test.yml +0 -70
- package/.husky/commit-msg +0 -1
- package/.husky/pre-commit +0 -1
- package/.prettierignore +0 -1
- package/.prettierrc +0 -9
- package/AGENTS.md +0 -153
- package/Dockerfile +0 -18
- package/QQ/351/237/263/344/271/220-v0.xmind +0 -0
- package/QQ/351/237/263/344/271/220-v1.xmind +0 -0
- package/app.ts +0 -68
- package/commitlint.config.js +0 -20
- package/config/user-info.ts +0 -71
- package/index.ts +0 -1
- package/jest.config.ts +0 -41
- package/middlewares/koa-cors.ts +0 -81
- package/module/apis/UCommon/UCommon.ts +0 -13
- package/module/apis/album/getAlbumInfo.ts +0 -22
- package/module/apis/comments/getComments.ts +0 -23
- package/module/apis/digitalAlbum/getDigitalAlbumLists.ts +0 -23
- package/module/apis/downloadQQMusic.ts +0 -51
- package/module/apis/music/getLyric.ts +0 -34
- package/module/apis/mv/getMvByTag.ts +0 -24
- package/module/apis/radio/getRadioLists.ts +0 -27
- package/module/apis/rank/getTopLists.ts +0 -37
- package/module/apis/search/getHotKey.ts +0 -24
- package/module/apis/search/getSearchByKey.ts +0 -31
- package/module/apis/search/getSmartbox.ts +0 -43
- package/module/apis/singers/getSimilarSinger.ts +0 -25
- package/module/apis/singers/getSingerDesc.ts +0 -25
- package/module/apis/singers/getSingerMv.ts +0 -24
- package/module/apis/singers/getSingerStarNum.ts +0 -24
- package/module/apis/songLists/songListCategories.ts +0 -22
- package/module/apis/songLists/songListDetail.ts +0 -27
- package/module/apis/songLists/songLists.ts +0 -35
- package/module/apis/u_common.ts +0 -29
- package/module/apis/user/checkQQLoginQr.ts +0 -230
- package/module/apis/user/getQQLoginQr.ts +0 -28
- package/module/apis/user/getUserAvatar.ts +0 -32
- package/module/apis/user/getUserLikedSongs.ts +0 -145
- package/module/apis/user/getUserPlaylists.ts +0 -163
- package/module/apis/y_common.ts +0 -44
- package/module/config.ts +0 -24
- package/module/index.ts +0 -95
- package/music.png +0 -0
- package/pnpm-workspace.yaml +0 -2
- package/public/index.html +0 -430
- package/routers/context/batchGetSongInfo.ts +0 -60
- package/routers/context/batchGetSongLists.ts +0 -46
- package/routers/context/checkQQLoginQr.ts +0 -19
- package/routers/context/cookies.ts +0 -38
- package/routers/context/getAlbumInfo.ts +0 -31
- package/routers/context/getComments.ts +0 -51
- package/routers/context/getDigitalAlbumLists.ts +0 -18
- package/routers/context/getDownloadQQMusic.ts +0 -17
- package/routers/context/getHotkey.ts +0 -25
- package/routers/context/getImageUrl.ts +0 -29
- package/routers/context/getLyric.ts +0 -32
- package/routers/context/getMusicPlay.ts +0 -102
- package/routers/context/getMv.ts +0 -61
- package/routers/context/getMvByTag.ts +0 -18
- package/routers/context/getMvPlay.ts +0 -114
- package/routers/context/getNewDisks.ts +0 -58
- package/routers/context/getQQLoginQr.ts +0 -16
- package/routers/context/getRadioLists.ts +0 -18
- package/routers/context/getRanks.ts +0 -67
- package/routers/context/getRecommend.ts +0 -92
- package/routers/context/getSearchByKey.ts +0 -34
- package/routers/context/getSimilarSinger.ts +0 -29
- package/routers/context/getSingerAlbum.ts +0 -58
- package/routers/context/getSingerDesc.ts +0 -30
- package/routers/context/getSingerHotsong.ts +0 -58
- package/routers/context/getSingerList.ts +0 -56
- package/routers/context/getSingerMv.ts +0 -41
- package/routers/context/getSingerStarNum.ts +0 -29
- package/routers/context/getSmartbox.ts +0 -27
- package/routers/context/getSongInfo.ts +0 -51
- package/routers/context/getSongListCategories.ts +0 -23
- package/routers/context/getSongListDetail.ts +0 -22
- package/routers/context/getSongLists.ts +0 -30
- package/routers/context/getTicketInfo.ts +0 -51
- package/routers/context/getTopLists.ts +0 -18
- package/routers/context/getUserAvatar.ts +0 -53
- package/routers/context/getUserLikedSongs.ts +0 -28
- package/routers/context/getUserPlaylists.ts +0 -29
- package/routers/context/index.ts +0 -87
- package/routers/router.ts +0 -88
- package/routers/types.ts +0 -18
- package/routers/util.ts +0 -231
- package/screenshot/album-image.png +0 -0
- package/screenshot/batchGetSongInfo.png +0 -0
- package/screenshot/batchGetSongLists.png +0 -0
- package/screenshot/downloadQQMusic.png +0 -0
- package/screenshot/get-album-image.png +0 -0
- package/screenshot/get-play-all-data.png +0 -0
- package/screenshot/get-song-album-id.png +0 -0
- package/screenshot/get-song-id.png +0 -0
- package/screenshot/get-song-image.png +0 -0
- package/screenshot/getAlbumInfo.png +0 -0
- package/screenshot/getComments-id.png +0 -0
- package/screenshot/getComments-param.png +0 -0
- package/screenshot/getComments.png +0 -0
- package/screenshot/getDigitalAlbumLists.png +0 -0
- package/screenshot/getLyric-parse.png +0 -0
- package/screenshot/getLyric.png +0 -0
- package/screenshot/getMusicPlay.png +0 -0
- package/screenshot/getMv.png +0 -0
- package/screenshot/getMvByTag.png +0 -0
- package/screenshot/getMvPlay.png +0 -0
- package/screenshot/getNewDisks.png +0 -0
- package/screenshot/getRadioLists.png +0 -0
- package/screenshot/getRanks.png +0 -0
- package/screenshot/getRecommend.png +0 -0
- package/screenshot/getSearchByKey.png +0 -0
- package/screenshot/getSimilarSinger.png +0 -0
- package/screenshot/getSingerAlbum.png +0 -0
- package/screenshot/getSingerDesc.png +0 -0
- package/screenshot/getSingerHotsong.png +0 -0
- package/screenshot/getSingerList.png +0 -0
- package/screenshot/getSingerMv-default.png +0 -0
- package/screenshot/getSingerMv-listen.png +0 -0
- package/screenshot/getSingerMv-time.png +0 -0
- package/screenshot/getSingerStarNum.png +0 -0
- package/screenshot/getSmartbox.png +0 -0
- package/screenshot/getSongInfo.png +0 -0
- package/screenshot/getSongListCategories.png +0 -0
- package/screenshot/getSongListDetail.png +0 -0
- package/screenshot/getSongLists-params.png +0 -0
- package/screenshot/getSongLists.png +0 -0
- package/screenshot/getTicketInfo.png +0 -0
- package/screenshot/getTopLists.png +0 -0
- package/screenshot/gethotkey.png +0 -0
- package/screenshot/just-get-play-url.png +0 -0
- package/screenshot/musicPlay.png +0 -0
- package/screenshot/new-feature-error-tips.png +0 -0
- package/screenshot/normalize-cookie.png +0 -0
- package/screenshot/qq-music-v0.png +0 -0
- package/screenshot/qq-music.png +0 -0
- package/screenshot/song-image.png +0 -0
- package/screenshot/song-quality-128.png +0 -0
- package/screenshot/song-quality-m4a.png +0 -0
- package/scripts/build-images.js +0 -36
- package/scripts/commit-push.sh +0 -103
- package/tests/integration/api/api.test.ts +0 -852
- package/tests/integration/middleware/cors.test.ts +0 -41
- package/tests/setup/jest.setup.ts +0 -15
- package/tests/setup/testUtils.ts +0 -35
- package/tests/unit/util/request.test.ts +0 -177
- package/tsconfig.json +0 -20
- package/tsconfig.test.json +0 -8
- package/types/api.ts +0 -105
- package/types/global.d.ts +0 -26
- package/types/index.d.ts +0 -97
- package/util/apiResponse.ts +0 -97
- package/util/colors.ts +0 -31
- package/util/cookie.ts +0 -40
- package/util/loginUtils.ts +0 -26
- package/util/lyricParse.ts +0 -86
- package/util/request.ts +0 -141
- 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
|
-
});
|