diff --git a/package.json b/package.json index ed2cf82e0..d84a5b0fe 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "eslint-plugin-unused-imports": "^2.0.0", "eslint-plugin-vitest": "^0.0.54", "happy-dom": "^8.9.0", + "node-mocks-http": "^1.12.2", "prettier": "^2.7.1", "sass": "^1.56.1", "turbo": "^1.8.3", diff --git a/src/pages/api/modules/media-requests/index.spec.ts b/src/pages/api/modules/media-requests/index.spec.ts new file mode 100644 index 000000000..69a6460a1 --- /dev/null +++ b/src/pages/api/modules/media-requests/index.spec.ts @@ -0,0 +1,292 @@ +import Consola from 'consola'; +import { createMocks } from 'node-mocks-http'; +import { describe, expect, it, vi } from 'vitest'; +import 'vitest-fetch-mock'; +import { ConfigType } from '../../../../types/config'; +import MediaRequestsRoute from './index'; + +const mockedGetConfig = vi.fn(); + +describe('media-requests api', () => { + it('reduce when empty list of requests', async () => { + // Arrange + const { req, res } = createMocks(); + + vi.mock('./../../../../tools/config/getConfig.ts', () => ({ + get getConfig() { + return mockedGetConfig; + }, + })); + mockedGetConfig.mockReturnValue({ + apps: [], + }); + + // Act + await MediaRequestsRoute(req, res); + + // Assert + expect(res._getStatusCode()).toBe(200); + expect(res.finished).toBe(true); + expect(JSON.parse(res._getData())).toEqual([]); + }); + + it('log error when fetch was not successful', async () => { + // Arrange + const { req, res } = createMocks(); + + vi.mock('./../../../../tools/config/getConfig.ts', () => ({ + get getConfig() { + return mockedGetConfig; + }, + })); + mockedGetConfig.mockReturnValue({ + apps: [ + { + integration: { + type: 'overseerr', + properties: [ + { + field: 'apiKey', + type: 'private', + value: 'abc', + }, + ], + }, + }, + ], + } as ConfigType); + const logSpy = vi.spyOn(Consola, 'error'); + + // Act + await MediaRequestsRoute(req, res); + + // Assert + expect(res._getStatusCode()).toBe(200); + expect(res.finished).toBe(true); + expect(JSON.parse(res._getData())).toEqual([]); + + expect(logSpy).toHaveBeenCalledOnce(); + expect(logSpy.mock.lastCall).toEqual([ + 'Failed to request data from Overseerr: FetchError: invalid json response body at reason: Unexpected end of JSON input', + ]); + + logSpy.mockRestore(); + }); + + it('fetch and return requests in response', async () => { + // Arrange + const { req, res } = createMocks({ + method: 'GET', + }); + + vi.mock('./../../../../tools/config/getConfig.ts', () => ({ + get getConfig() { + return mockedGetConfig; + }, + })); + mockedGetConfig.mockReturnValue({ + apps: [ + { + url: 'http://my-overseerr.local', + integration: { + type: 'overseerr', + properties: [ + { + field: 'apiKey', + type: 'private', + value: 'abc', + }, + ], + }, + }, + ], + } as ConfigType); + const logSpy = vi.spyOn(Consola, 'error'); + + fetchMock.mockResponse((request) => { + if (request.url === 'http://my-overseerr.local/api/v1/request?take=25&skip=0&sort=added') { + return JSON.stringify({ + pageInfo: { pages: 3, pageSize: 20, results: 42, page: 1 }, + results: [ + { + id: 44, + status: 2, + createdAt: '2023-04-06T19:38:45.000Z', + updatedAt: '2023-04-06T19:38:45.000Z', + type: 'movie', + is4k: false, + serverId: 0, + profileId: 4, + tags: [], + isAutoRequest: false, + media: { + downloadStatus: [], + downloadStatus4k: [], + id: 999, + mediaType: 'movie', + tmdbId: 99999999, + tvdbId: null, + imdbId: null, + status: 5, + status4k: 1, + createdAt: '2023-02-06T19:38:45.000Z', + updatedAt: '2023-02-06T20:00:04.000Z', + lastSeasonChange: '2023-08-06T19:38:45.000Z', + mediaAddedAt: '2023-05-14T06:30:34.000Z', + serviceId: 0, + serviceId4k: null, + externalServiceId: 32, + externalServiceId4k: null, + externalServiceSlug: '000000000000', + externalServiceSlug4k: null, + ratingKey: null, + ratingKey4k: null, + jellyfinMediaId: '0000', + jellyfinMediaId4k: null, + mediaUrl: + 'http://your-jellyfin.local/web/index.html#!/details?id=mn8q2j4gq038g&context=home&serverId=jf83fj34gm340g', + serviceUrl: 'http://your-jellyfin.local/movie/0000', + }, + seasons: [], + modifiedBy: { + permissions: 2, + warnings: [], + id: 1, + email: 'example-user@homarr.dev', + plexUsername: null, + jellyfinUsername: 'example-user', + username: null, + recoveryLinkExpirationDate: null, + userType: 3, + plexId: null, + jellyfinUserId: '00000000000000000', + jellyfinDeviceId: '111111111111111111', + jellyfinAuthToken: '2222222222222222222', + plexToken: null, + avatar: '/os_logo_square.png', + movieQuotaLimit: null, + movieQuotaDays: null, + tvQuotaLimit: null, + tvQuotaDays: null, + createdAt: '2022-07-03T19:53:08.000Z', + updatedAt: '2022-07-03T19:53:08.000Z', + requestCount: 34, + displayName: 'Example User', + }, + requestedBy: { + permissions: 2, + warnings: [], + id: 1, + email: 'example-user@homarr.dev', + plexUsername: null, + jellyfinUsername: 'example-user', + username: null, + recoveryLinkExpirationDate: null, + userType: 3, + plexId: null, + jellyfinUserId: '00000000000000000', + jellyfinDeviceId: '111111111111111111', + jellyfinAuthToken: '2222222222222222222', + plexToken: null, + avatar: '/os_logo_square.png', + movieQuotaLimit: null, + movieQuotaDays: null, + tvQuotaLimit: null, + tvQuotaDays: null, + createdAt: '2022-07-03T19:53:08.000Z', + updatedAt: '2022-07-03T19:53:08.000Z', + requestCount: 34, + displayName: 'Example User', + }, + seasonCount: 0, + }, + ], + }); + } + + if (request.url === 'http://my-overseerr.local/api/v1/movie/99999999') { + return JSON.stringify({ + id: 0, + adult: false, + budget: 0, + genres: [ + { + id: 18, + name: 'Dashboards', + }, + ], + relatedVideos: [], + originalLanguage: 'jp', + originalTitle: 'Homarrrr Movie', + popularity: 9.352, + productionCompanies: [], + productionCountries: [], + releaseDate: '2023-12-08', + releases: { + results: [], + }, + revenue: 0, + spokenLanguages: [ + { + english_name: 'Japanese', + iso_639_1: 'jp', + name: '日本語', + }, + ], + status: 'Released', + title: 'Homarr Movie', + video: false, + voteAverage: 9.999, + voteCount: 0, + backdropPath: '/mhjq8jr0qgrjnghnh.jpg', + homepage: '', + imdbId: 'tt0000000', + overview: 'A very cool movie', + posterPath: '/hf4j0928gq543njgh8935nqh8.jpg', + runtime: 97, + tagline: '', + credits: {}, + collection: null, + externalIds: { + facebookId: null, + imdbId: null, + instagramId: null, + twitterId: null, + }, + watchProviders: [], + keywords: [], + }); + } + + return Promise.reject(new Error(`Bad url: ${request.url}`)); + }); + + // Act + await MediaRequestsRoute(req, res); + + // Assert + expect(res._getStatusCode()).toBe(200); + expect(res.finished).toBe(true); + expect(JSON.parse(res._getData())).toEqual([ + { + airDate: '2023-12-08', + backdropPath: 'https://image.tmdb.org/t/p/original//mhjq8jr0qgrjnghnh.jpg', + createdAt: '2023-04-06T19:38:45.000Z', + href: 'http://my-overseerr.local/movie/99999999', + id: 44, + name: 'Homarrrr Movie', + posterPath: + 'https://image.tmdb.org/t/p/w600_and_h900_bestv2//hf4j0928gq543njgh8935nqh8.jpg', + status: 2, + type: 'movie', + userLink: 'http://my-overseerr.local/users/1', + userName: 'Example User', + userProfilePicture: 'http://my-overseerr.local//os_logo_square.png', + }, + ]); + + expect(logSpy).not.toHaveBeenCalled(); + + logSpy.mockRestore(); + }); +}); diff --git a/src/pages/api/modules/media-requests/index.ts b/src/pages/api/modules/media-requests/index.ts index 0edf6e342..bbe9f0ef2 100644 --- a/src/pages/api/modules/media-requests/index.ts +++ b/src/pages/api/modules/media-requests/index.ts @@ -60,7 +60,7 @@ const Get = async (request: NextApiRequest, response: NextApiResponse) => { }); }); - const mediaRequests = (await Promise.all(promises)).reduce((prev, cur) => prev.concat(cur)); + const mediaRequests = (await Promise.all(promises)).reduce((prev, cur) => prev.concat(cur), []); return response.status(200).json(mediaRequests); }; diff --git a/yarn.lock b/yarn.lock index 41070bd38..d0ee2773a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2325,6 +2325,16 @@ __metadata: languageName: node linkType: hard +"accepts@npm:^1.3.7": + version: 1.3.8 + resolution: "accepts@npm:1.3.8" + dependencies: + mime-types: ~2.1.34 + negotiator: 0.6.3 + checksum: 50c43d32e7b50285ebe84b613ee4a3aa426715a7d131b65b786e2ead0fd76b6b60091b9916d3478a75f11f162628a2139991b6c03ab3f1d9ab7c86075dc8eab4 + languageName: node + linkType: hard + "acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" @@ -3116,6 +3126,15 @@ __metadata: languageName: node linkType: hard +"content-disposition@npm:^0.5.3": + version: 0.5.4 + resolution: "content-disposition@npm:0.5.4" + dependencies: + safe-buffer: 5.2.1 + checksum: afb9d545e296a5171d7574fcad634b2fdf698875f4006a9dd04a3e1333880c5c0c98d47b560d01216fb6505a54a2ba6a843ee3a02ec86d7e911e8315255f56c3 + languageName: node + linkType: hard + "convert-source-map@npm:^1.5.0, convert-source-map@npm:^1.6.0, convert-source-map@npm:^1.7.0": version: 1.9.0 resolution: "convert-source-map@npm:1.9.0" @@ -3471,6 +3490,13 @@ __metadata: languageName: node linkType: hard +"depd@npm:^1.1.0": + version: 1.1.2 + resolution: "depd@npm:1.1.2" + checksum: 6b406620d269619852885ce15965272b829df6f409724415e0002c8632ab6a8c0a08ec1f0bd2add05dc7bd7507606f7e2cc034fa24224ab829580040b835ecd9 + languageName: node + linkType: hard + "depd@npm:^2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" @@ -4454,6 +4480,13 @@ __metadata: languageName: node linkType: hard +"fresh@npm:^0.5.2": + version: 0.5.2 + resolution: "fresh@npm:0.5.2" + checksum: 13ea8b08f91e669a64e3ba3a20eb79d7ca5379a81f1ff7f4310d54e2320645503cc0c78daedc93dfb6191287295f6479544a649c64d8e41a1c0fb0c221552346 + languageName: node + linkType: hard + "fs-constants@npm:^1.0.0": version: 1.0.0 resolution: "fs-constants@npm:1.0.0" @@ -4950,6 +4983,7 @@ __metadata: js-file-download: ^0.4.12 next: ^13.2.1 next-i18next: ^11.3.0 + node-mocks-http: ^1.12.2 nzbget-api: ^0.0.3 prettier: ^2.7.1 prismjs: ^1.29.0 @@ -5935,6 +5969,20 @@ __metadata: languageName: node linkType: hard +"media-typer@npm:0.3.0": + version: 0.3.0 + resolution: "media-typer@npm:0.3.0" + checksum: af1b38516c28ec95d6b0826f6c8f276c58aec391f76be42aa07646b4e39d317723e869700933ca6995b056db4b09a78c92d5440dc23657e6764be5d28874bba1 + languageName: node + linkType: hard + +"merge-descriptors@npm:^1.0.1": + version: 1.0.1 + resolution: "merge-descriptors@npm:1.0.1" + checksum: 5abc259d2ae25bb06d19ce2b94a21632583c74e2a9109ee1ba7fd147aa7362b380d971e0251069f8b3eb7d48c21ac839e21fa177b335e82c76ec172e30c31a26 + languageName: node + linkType: hard + "merge-options@npm:^3.0.4": version: 3.0.4 resolution: "merge-options@npm:3.0.4" @@ -5951,6 +5999,13 @@ __metadata: languageName: node linkType: hard +"methods@npm:^1.1.2": + version: 1.1.2 + resolution: "methods@npm:1.1.2" + checksum: 0917ff4041fa8e2f2fda5425a955fe16ca411591fbd123c0d722fcf02b73971ed6f764d85f0a6f547ce49ee0221ce2c19a5fa692157931cecb422984f1dcd13a + languageName: node + linkType: hard + "micromatch@npm:^4.0.4": version: 4.0.5 resolution: "micromatch@npm:4.0.5" @@ -5968,7 +6023,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^2.1.12": +"mime-types@npm:^2.1.12, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -5977,6 +6032,15 @@ __metadata: languageName: node linkType: hard +"mime@npm:^1.3.4": + version: 1.6.0 + resolution: "mime@npm:1.6.0" + bin: + mime: cli.js + checksum: fef25e39263e6d207580bdc629f8872a3f9772c923c7f8c7e793175cee22777bbe8bba95e5d509a40aaa292d8974514ce634ae35769faa45f22d17edda5e8557 + languageName: node + linkType: hard + "mimic-response@npm:^1.0.0": version: 1.0.1 resolution: "mimic-response@npm:1.0.1" @@ -6233,7 +6297,7 @@ __metadata: languageName: node linkType: hard -"negotiator@npm:^0.6.3": +"negotiator@npm:0.6.3, negotiator@npm:^0.6.3": version: 0.6.3 resolution: "negotiator@npm:0.6.3" checksum: b8ffeb1e262eff7968fc90a2b6767b04cfd9842582a9d0ece0af7049537266e7b2506dfb1d107a32f06dd849ab2aea834d5830f7f4d0e5cb7d36e1ae55d021d9 @@ -6391,6 +6455,24 @@ __metadata: languageName: node linkType: hard +"node-mocks-http@npm:^1.12.2": + version: 1.12.2 + resolution: "node-mocks-http@npm:1.12.2" + dependencies: + accepts: ^1.3.7 + content-disposition: ^0.5.3 + depd: ^1.1.0 + fresh: ^0.5.2 + merge-descriptors: ^1.0.1 + methods: ^1.1.2 + mime: ^1.3.4 + parseurl: ^1.3.3 + range-parser: ^1.2.0 + type-is: ^1.6.18 + checksum: 39e50b7146bd37fd56a0588ee3df46fd310f76395d52e9b8889545910aca6cc8e8a41de3cdd3e103903d4bbfc556e67624fcbe934c0bd3b0cca6ee1358a0f440 + languageName: node + linkType: hard + "node-releases@npm:^2.0.8": version: 2.0.10 resolution: "node-releases@npm:2.0.10" @@ -6649,6 +6731,13 @@ __metadata: languageName: node linkType: hard +"parseurl@npm:^1.3.3": + version: 1.3.3 + resolution: "parseurl@npm:1.3.3" + checksum: 407cee8e0a3a4c5cd472559bca8b6a45b82c124e9a4703302326e9ab60fc1081442ada4e02628efef1eb16197ddc7f8822f5a91fd7d7c86b51f530aedb17dfa2 + languageName: node + linkType: hard + "path-exists@npm:^4.0.0": version: 4.0.0 resolution: "path-exists@npm:4.0.0" @@ -6895,6 +6984,13 @@ __metadata: languageName: node linkType: hard +"range-parser@npm:^1.2.0": + version: 1.2.1 + resolution: "range-parser@npm:1.2.1" + checksum: 0a268d4fea508661cf5743dfe3d5f47ce214fd6b7dec1de0da4d669dd4ef3d2144468ebe4179049eff253d9d27e719c88dae55be64f954e80135a0cada804ec9 + languageName: node + linkType: hard + "react-dom@npm:^18.2.0": version: 18.2.0 resolution: "react-dom@npm:18.2.0" @@ -7336,6 +7432,13 @@ __metadata: languageName: node linkType: hard +"safe-buffer@npm:5.2.1, safe-buffer@npm:~5.2.0": + version: 5.2.1 + resolution: "safe-buffer@npm:5.2.1" + checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 + languageName: node + linkType: hard + "safe-buffer@npm:~5.1.0, safe-buffer@npm:~5.1.1": version: 5.1.2 resolution: "safe-buffer@npm:5.1.2" @@ -7343,13 +7446,6 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:~5.2.0": - version: 5.2.1 - resolution: "safe-buffer@npm:5.2.1" - checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 - languageName: node - linkType: hard - "safe-json-parse@npm:4.0.0": version: 4.0.0 resolution: "safe-json-parse@npm:4.0.0" @@ -8137,6 +8233,16 @@ __metadata: languageName: node linkType: hard +"type-is@npm:^1.6.18": + version: 1.6.18 + resolution: "type-is@npm:1.6.18" + dependencies: + media-typer: 0.3.0 + mime-types: ~2.1.24 + checksum: 2c8e47675d55f8b4e404bcf529abdf5036c537a04c2b20177bcf78c9e3c1da69da3942b1346e6edb09e823228c0ee656ef0e033765ec39a70d496ef601a0c657 + languageName: node + linkType: hard + "typed-array-length@npm:^1.0.4": version: 1.0.4 resolution: "typed-array-length@npm:1.0.4"