From c1bf12de4fe878cf1d6eb045f11ddeb8874a5f23 Mon Sep 17 00:00:00 2001 From: Rushil Perera Date: Sat, 21 Sep 2024 13:16:56 -0400 Subject: [PATCH] feat: associate device id with username when logging in --- src/controllers/auth/anilist/index.ts | 8 +- .../__snapshots__/index.spec.ts.snap | 238 +++++ .../search/__snapshots__/index.spec.ts.snap | 882 ++++++++++++++++++ .../title/__snapshots__/index.spec.ts.snap | 156 ++++ src/controllers/token/index.spec.ts | 132 +-- src/controllers/token/index.ts | 11 +- src/libs/errors/TokenAlreadyExists.ts | 5 - src/models/token.ts | 59 +- 8 files changed, 1302 insertions(+), 189 deletions(-) delete mode 100644 src/libs/errors/TokenAlreadyExists.ts diff --git a/src/controllers/auth/anilist/index.ts b/src/controllers/auth/anilist/index.ts index 0d4d35c..e380838 100644 --- a/src/controllers/auth/anilist/index.ts +++ b/src/controllers/auth/anilist/index.ts @@ -5,6 +5,7 @@ import { streamSSE } from "hono/streaming"; import { fetchEpisodes } from "~/controllers/episodes/getByAniListId"; import { maybeScheduleNextAiringEpisode } from "~/libs/maybeScheduleNextAiringEpisode"; import { readEnvVariable } from "~/libs/readEnvVariable"; +import { associateDeviceIdWithUsername } from "~/models/token"; import { setWatchStatus } from "~/models/watchStatus"; import type { Env } from "~/types/env"; import { EpisodesResponseSchema } from "~/types/episode"; @@ -60,9 +61,8 @@ const route = createRoute({ const app = new OpenAPIHono(); app.openapi(route, async (c) => { - // const deviceId = await c.req.header("X-Aniplay-Device-Id"); - // const aniListToken = await c.req.header("X-AniList-Token"); - const { deviceId, token: aniListToken } = await c.req.query(); + const deviceId = await c.req.header("X-Aniplay-Device-Id"); + const aniListToken = await c.req.header("X-AniList-Token"); if (!aniListToken) { return c.json(ErrorResponse, { status: 401 }); @@ -74,6 +74,8 @@ app.openapi(route, async (c) => { return c.json(ErrorResponse, { status: 401 }); } + await associateDeviceIdWithUsername(env(c, "workerd"), deviceId, username); + return streamSSE( c, async (stream) => { diff --git a/src/controllers/episodes/getByAniListId/__snapshots__/index.spec.ts.snap b/src/controllers/episodes/getByAniListId/__snapshots__/index.spec.ts.snap index 3ca5e4d..fa506f6 100644 --- a/src/controllers/episodes/getByAniListId/__snapshots__/index.spec.ts.snap +++ b/src/controllers/episodes/getByAniListId/__snapshots__/index.spec.ts.snap @@ -118,3 +118,241 @@ exports[`requests the "/episodes" route with list of episodes from Anify 1`] = ` "success": true, } `; + +exports[`requests the "/episodes" route with list of episodes from Anify 1`] = ` +{ + "result": { + "episodes": [ + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=103233", + "img": null, + "number": 1, + "rating": null, + "title": "Mission: Forgetter I", + "updatedAt": 0, + }, + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=103632", + "img": null, + "number": 2, + "rating": null, + "title": "Mission: Forgetter II", + "updatedAt": 0, + }, + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=104244", + "img": null, + "number": 3, + "rating": null, + "title": "Mission: Forgetter III", + "updatedAt": 0, + }, + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=104620", + "img": null, + "number": 4, + "rating": null, + "title": "Mission: Forgetter IV", + "updatedAt": 0, + }, + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=104844", + "img": null, + "number": 5, + "rating": null, + "title": "File: Glint", + "updatedAt": 0, + }, + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=105761", + "img": null, + "number": 6, + "rating": null, + "title": "File: Dreamspeaker Thea", + "updatedAt": 0, + }, + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=106135", + "img": null, + "number": 7, + "rating": null, + "title": "File: Forgetter Annette", + "updatedAt": 0, + }, + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=106518", + "img": null, + "number": 8, + "rating": null, + "title": "Mission: Dreamspeaker I", + "updatedAt": 0, + }, + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=106606", + "img": null, + "number": 9, + "rating": null, + "title": "Mission: Dreamspeaker II", + "updatedAt": 0, + }, + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=106981", + "img": null, + "number": 10, + "rating": null, + "title": "Mission: Dreamspeaker III", + "updatedAt": 0, + }, + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=107176", + "img": null, + "number": 11, + "rating": null, + "title": "Mission: Dreamspeaker IV", + "updatedAt": 0, + }, + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=107247", + "img": null, + "number": 12, + "rating": null, + "title": "File: Flower Garden Lily", + "updatedAt": 0, + }, + ], + "providerId": "zoro", + }, + "success": true, +} +`; + +exports[`requests the "/episodes" route with list of episodes from Anify 1`] = ` +{ + "result": { + "episodes": [ + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=103233", + "img": null, + "number": 1, + "rating": null, + "title": "Mission: Forgetter I", + "updatedAt": 0, + }, + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=103632", + "img": null, + "number": 2, + "rating": null, + "title": "Mission: Forgetter II", + "updatedAt": 0, + }, + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=104244", + "img": null, + "number": 3, + "rating": null, + "title": "Mission: Forgetter III", + "updatedAt": 0, + }, + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=104620", + "img": null, + "number": 4, + "rating": null, + "title": "Mission: Forgetter IV", + "updatedAt": 0, + }, + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=104844", + "img": null, + "number": 5, + "rating": null, + "title": "File: Glint", + "updatedAt": 0, + }, + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=105761", + "img": null, + "number": 6, + "rating": null, + "title": "File: Dreamspeaker Thea", + "updatedAt": 0, + }, + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=106135", + "img": null, + "number": 7, + "rating": null, + "title": "File: Forgetter Annette", + "updatedAt": 0, + }, + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=106518", + "img": null, + "number": 8, + "rating": null, + "title": "Mission: Dreamspeaker I", + "updatedAt": 0, + }, + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=106606", + "img": null, + "number": 9, + "rating": null, + "title": "Mission: Dreamspeaker II", + "updatedAt": 0, + }, + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=106981", + "img": null, + "number": 10, + "rating": null, + "title": "Mission: Dreamspeaker III", + "updatedAt": 0, + }, + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=107176", + "img": null, + "number": 11, + "rating": null, + "title": "Mission: Dreamspeaker IV", + "updatedAt": 0, + }, + { + "description": null, + "id": "/watch/spy-classroom-season-2-18468?ep=107247", + "img": null, + "number": 12, + "rating": null, + "title": "File: Flower Garden Lily", + "updatedAt": 0, + }, + ], + "providerId": "zoro", + }, + "success": true, +} +`; diff --git a/src/controllers/search/__snapshots__/index.spec.ts.snap b/src/controllers/search/__snapshots__/index.spec.ts.snap index 05d061c..bb82af3 100644 --- a/src/controllers/search/__snapshots__/index.spec.ts.snap +++ b/src/controllers/search/__snapshots__/index.spec.ts.snap @@ -440,3 +440,885 @@ exports[`requests the "/search" route valid query that returns anilist results 1 "success": true, } `; + +exports[`requests the "/search" route valid query that returns anilist results 1`] = ` +{ + "hasNextPage": false, + "results": [ + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx151807-yxY3olrjZH4k.png", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx151807-yxY3olrjZH4k.png", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx151807-yxY3olrjZH4k.png", + }, + "id": 151807, + "title": { + "english": "Solo Leveling", + "userPreferred": "Ore dake Level Up na Ken", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2759-z07kq8Pnw5B1.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2759-z07kq8Pnw5B1.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2759-z07kq8Pnw5B1.jpg", + }, + "id": 2759, + "title": { + "english": "Evangelion: 1.0 You Are (Not) Alone", + "userPreferred": "Evangelion Shin Movie: Jo", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx139589-oFz7JwpwRkQV.png", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx139589-oFz7JwpwRkQV.png", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx139589-oFz7JwpwRkQV.png", + }, + "id": 139589, + "title": { + "english": "Kotaro Lives Alone", + "userPreferred": "Kotarou wa Hitorigurashi", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx145815-XsgcXy7WzgtK.png", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx145815-XsgcXy7WzgtK.png", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx145815-XsgcXy7WzgtK.png", + }, + "id": 145815, + "title": { + "english": "I've Somehow Gotten Stronger When I Improved My Farm-Related Skills", + "userPreferred": "Noumin Kanren no Skill Bakka Agetetara Naze ka Tsuyoku Natta.", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx176496-r6oXxEqdZL0n.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx176496-r6oXxEqdZL0n.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx176496-r6oXxEqdZL0n.jpg", + }, + "id": 176496, + "title": { + "english": "Solo Leveling Season 2 -Arise from the Shadow-", + "userPreferred": "Ore dake Level Up na Ken: Season 2 - Arise from the Shadow", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1965-lWBpcTni9PS9.png", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1965-lWBpcTni9PS9.png", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1965-lWBpcTni9PS9.png", + }, + "id": 1965, + "title": { + "english": null, + "userPreferred": "sola", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx118123-xqn5fYsjKXJU.png", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx118123-xqn5fYsjKXJU.png", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx118123-xqn5fYsjKXJU.png", + }, + "id": 118123, + "title": { + "english": null, + "userPreferred": "Holo no Graffiti", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2582-aB1Vh1jDobQ3.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2582-aB1Vh1jDobQ3.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2582-aB1Vh1jDobQ3.jpg", + }, + "id": 2582, + "title": { + "english": "Armored Trooper Votoms", + "userPreferred": "Soukou Kihei Votoms", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx116384-xn0nQAKGFSd7.png", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx116384-xn0nQAKGFSd7.png", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx116384-xn0nQAKGFSd7.png", + }, + "id": 116384, + "title": { + "english": "Sol Levante", + "userPreferred": "Sol Levante", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx104073-OQ8YBTy7zmKf.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx104073-OQ8YBTy7zmKf.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx104073-OQ8YBTy7zmKf.jpg", + }, + "id": 104073, + "title": { + "english": null, + "userPreferred": "Sono Toki, Kanojo wa.", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15313.jpg", + }, + "id": 15313, + "title": { + "english": "Wooser's Hand-to-Mouth Life", + "userPreferred": "Wooser no Sono Higurashi", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/8068.jpg", + }, + "id": 8068, + "title": { + "english": null, + "userPreferred": "Kuroshitsuji Picture Drama", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/3174.jpg", + }, + "id": 3174, + "title": { + "english": null, + "userPreferred": "sola Specials", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/1443.jpg", + }, + "id": 1443, + "title": { + "english": null, + "userPreferred": "SOL BIANCA", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx153431-DMBYQxagH3Uu.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx153431-DMBYQxagH3Uu.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx153431-DMBYQxagH3Uu.jpg", + }, + "id": 153431, + "title": { + "english": null, + "userPreferred": "Onna no Sono no Hoshi", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1444-7Yn6hmQ2bk9D.png", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1444-7Yn6hmQ2bk9D.png", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1444-7Yn6hmQ2bk9D.png", + }, + "id": 1444, + "title": { + "english": "Sol Bianca: The Legacy", + "userPreferred": "Sol Bianca: Taiyou no Fune", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/4138.jpg", + }, + "id": 4138, + "title": { + "english": "The Adventures of Scamper the Penguin", + "userPreferred": "Chiisana Pengin: Lolo no Bouken", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx164192-KQ8sYXbaAl6i.png", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx164192-KQ8sYXbaAl6i.png", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx164192-KQ8sYXbaAl6i.png", + }, + "id": 164192, + "title": { + "english": "Planetarium", + "userPreferred": "Planetarium", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b5838-QTe07RRZylUm.jpg", + }, + "id": 5838, + "title": { + "english": null, + "userPreferred": "Furudera no Obake-soudou", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162882-OQENM5pXn7QQ.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162882-OQENM5pXn7QQ.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162882-OQENM5pXn7QQ.jpg", + }, + "id": 162882, + "title": { + "english": "P.E.T.", + "userPreferred": "P.E.T.", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/102710-dVayaOkzATwa.png", + }, + "id": 102710, + "title": { + "english": "The Garden of Pleasure", + "userPreferred": "Kairaku no Sono", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162881-c7xmNA6DlHFZ.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162881-c7xmNA6DlHFZ.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162881-c7xmNA6DlHFZ.jpg", + }, + "id": 162881, + "title": { + "english": "Mosh Race", + "userPreferred": "Mosh Race", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/5935.jpg", + }, + "id": 5935, + "title": { + "english": "Marco Polo's Adventures", + "userPreferred": "Marco Polo no Boken", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103449-FxDK8eJuMAKg.jpg", + }, + "id": 103449, + "title": { + "english": null, + "userPreferred": "SOL", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/12993.jpg", + }, + "id": 12993, + "title": { + "english": null, + "userPreferred": "Sono Mukou no Mukougawa", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20459.jpg", + }, + "id": 20459, + "title": { + "english": null, + "userPreferred": "Ganbare! Lulu Lolo", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b137760-CleNdfmuKRy7.png", + }, + "id": 137760, + "title": { + "english": null, + "userPreferred": "Soko ni wa Mata Meikyuu", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/7473.jpg", + }, + "id": 7473, + "title": { + "english": "Rennyo and His Mother", + "userPreferred": "Rennyo to Sono Haha", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/21418-TZYwdItidowx.jpg", + }, + "id": 21418, + "title": { + "english": null, + "userPreferred": "Ganbare! Lulu Lolo 3rd Season", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103517-XgOUryeFaPDW.jpg", + }, + "id": 103517, + "title": { + "english": null, + "userPreferred": "Toute wa Sono Kotae", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b113572-hP9x1SkRJXvA.jpg", + }, + "id": 113572, + "title": { + "english": "Journey to the beyond", + "userPreferred": "Sono Saki no Taniji", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20864.jpg", + }, + "id": 20864, + "title": { + "english": null, + "userPreferred": "Ganbare! Lulu Lolo 2nd Season", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15129.jpg", + }, + "id": 15129, + "title": { + "english": "Short Animations of Junpei Fujita", + "userPreferred": "Tanpen Animation Junpei Fujita", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx106557-TPLmwa2EccB9.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx106557-TPLmwa2EccB9.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx106557-TPLmwa2EccB9.jpg", + }, + "id": 106557, + "title": { + "english": "A Place to Name", + "userPreferred": "Sono Ie no Namae", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b118133-y7RvDFmr30hZ.jpg", + }, + "id": 118133, + "title": { + "english": "In Inertia", + "userPreferred": "Guzu no Soko", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx169686-exScHzB5UX2D.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx169686-exScHzB5UX2D.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx169686-exScHzB5UX2D.jpg", + }, + "id": 169686, + "title": { + "english": "Indoor Days", + "userPreferred": "Soto ni Denai hi", + }, + }, + ], + "success": true, +} +`; + +exports[`requests the "/search" route valid query that returns anilist results 1`] = ` +{ + "hasNextPage": false, + "results": [ + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx151807-yxY3olrjZH4k.png", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx151807-yxY3olrjZH4k.png", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx151807-yxY3olrjZH4k.png", + }, + "id": 151807, + "title": { + "english": "Solo Leveling", + "userPreferred": "Ore dake Level Up na Ken", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2759-z07kq8Pnw5B1.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2759-z07kq8Pnw5B1.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2759-z07kq8Pnw5B1.jpg", + }, + "id": 2759, + "title": { + "english": "Evangelion: 1.0 You Are (Not) Alone", + "userPreferred": "Evangelion Shin Movie: Jo", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx139589-oFz7JwpwRkQV.png", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx139589-oFz7JwpwRkQV.png", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx139589-oFz7JwpwRkQV.png", + }, + "id": 139589, + "title": { + "english": "Kotaro Lives Alone", + "userPreferred": "Kotarou wa Hitorigurashi", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx145815-XsgcXy7WzgtK.png", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx145815-XsgcXy7WzgtK.png", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx145815-XsgcXy7WzgtK.png", + }, + "id": 145815, + "title": { + "english": "I've Somehow Gotten Stronger When I Improved My Farm-Related Skills", + "userPreferred": "Noumin Kanren no Skill Bakka Agetetara Naze ka Tsuyoku Natta.", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx176496-r6oXxEqdZL0n.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx176496-r6oXxEqdZL0n.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx176496-r6oXxEqdZL0n.jpg", + }, + "id": 176496, + "title": { + "english": "Solo Leveling Season 2 -Arise from the Shadow-", + "userPreferred": "Ore dake Level Up na Ken: Season 2 - Arise from the Shadow", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1965-lWBpcTni9PS9.png", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1965-lWBpcTni9PS9.png", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1965-lWBpcTni9PS9.png", + }, + "id": 1965, + "title": { + "english": null, + "userPreferred": "sola", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx118123-xqn5fYsjKXJU.png", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx118123-xqn5fYsjKXJU.png", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx118123-xqn5fYsjKXJU.png", + }, + "id": 118123, + "title": { + "english": null, + "userPreferred": "Holo no Graffiti", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2582-aB1Vh1jDobQ3.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2582-aB1Vh1jDobQ3.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2582-aB1Vh1jDobQ3.jpg", + }, + "id": 2582, + "title": { + "english": "Armored Trooper Votoms", + "userPreferred": "Soukou Kihei Votoms", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx116384-xn0nQAKGFSd7.png", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx116384-xn0nQAKGFSd7.png", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx116384-xn0nQAKGFSd7.png", + }, + "id": 116384, + "title": { + "english": "Sol Levante", + "userPreferred": "Sol Levante", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx104073-OQ8YBTy7zmKf.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx104073-OQ8YBTy7zmKf.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx104073-OQ8YBTy7zmKf.jpg", + }, + "id": 104073, + "title": { + "english": null, + "userPreferred": "Sono Toki, Kanojo wa.", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15313.jpg", + }, + "id": 15313, + "title": { + "english": "Wooser's Hand-to-Mouth Life", + "userPreferred": "Wooser no Sono Higurashi", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/8068.jpg", + }, + "id": 8068, + "title": { + "english": null, + "userPreferred": "Kuroshitsuji Picture Drama", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/3174.jpg", + }, + "id": 3174, + "title": { + "english": null, + "userPreferred": "sola Specials", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/1443.jpg", + }, + "id": 1443, + "title": { + "english": null, + "userPreferred": "SOL BIANCA", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx153431-DMBYQxagH3Uu.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx153431-DMBYQxagH3Uu.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx153431-DMBYQxagH3Uu.jpg", + }, + "id": 153431, + "title": { + "english": null, + "userPreferred": "Onna no Sono no Hoshi", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1444-7Yn6hmQ2bk9D.png", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1444-7Yn6hmQ2bk9D.png", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1444-7Yn6hmQ2bk9D.png", + }, + "id": 1444, + "title": { + "english": "Sol Bianca: The Legacy", + "userPreferred": "Sol Bianca: Taiyou no Fune", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/4138.jpg", + }, + "id": 4138, + "title": { + "english": "The Adventures of Scamper the Penguin", + "userPreferred": "Chiisana Pengin: Lolo no Bouken", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx164192-KQ8sYXbaAl6i.png", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx164192-KQ8sYXbaAl6i.png", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx164192-KQ8sYXbaAl6i.png", + }, + "id": 164192, + "title": { + "english": "Planetarium", + "userPreferred": "Planetarium", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b5838-QTe07RRZylUm.jpg", + }, + "id": 5838, + "title": { + "english": null, + "userPreferred": "Furudera no Obake-soudou", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162882-OQENM5pXn7QQ.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162882-OQENM5pXn7QQ.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162882-OQENM5pXn7QQ.jpg", + }, + "id": 162882, + "title": { + "english": "P.E.T.", + "userPreferred": "P.E.T.", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/102710-dVayaOkzATwa.png", + }, + "id": 102710, + "title": { + "english": "The Garden of Pleasure", + "userPreferred": "Kairaku no Sono", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162881-c7xmNA6DlHFZ.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162881-c7xmNA6DlHFZ.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162881-c7xmNA6DlHFZ.jpg", + }, + "id": 162881, + "title": { + "english": "Mosh Race", + "userPreferred": "Mosh Race", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/5935.jpg", + }, + "id": 5935, + "title": { + "english": "Marco Polo's Adventures", + "userPreferred": "Marco Polo no Boken", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103449-FxDK8eJuMAKg.jpg", + }, + "id": 103449, + "title": { + "english": null, + "userPreferred": "SOL", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/12993.jpg", + }, + "id": 12993, + "title": { + "english": null, + "userPreferred": "Sono Mukou no Mukougawa", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20459.jpg", + }, + "id": 20459, + "title": { + "english": null, + "userPreferred": "Ganbare! Lulu Lolo", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b137760-CleNdfmuKRy7.png", + }, + "id": 137760, + "title": { + "english": null, + "userPreferred": "Soko ni wa Mata Meikyuu", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/7473.jpg", + }, + "id": 7473, + "title": { + "english": "Rennyo and His Mother", + "userPreferred": "Rennyo to Sono Haha", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/21418-TZYwdItidowx.jpg", + }, + "id": 21418, + "title": { + "english": null, + "userPreferred": "Ganbare! Lulu Lolo 3rd Season", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103517-XgOUryeFaPDW.jpg", + }, + "id": 103517, + "title": { + "english": null, + "userPreferred": "Toute wa Sono Kotae", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b113572-hP9x1SkRJXvA.jpg", + }, + "id": 113572, + "title": { + "english": "Journey to the beyond", + "userPreferred": "Sono Saki no Taniji", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20864.jpg", + }, + "id": 20864, + "title": { + "english": null, + "userPreferred": "Ganbare! Lulu Lolo 2nd Season", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15129.jpg", + }, + "id": 15129, + "title": { + "english": "Short Animations of Junpei Fujita", + "userPreferred": "Tanpen Animation Junpei Fujita", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx106557-TPLmwa2EccB9.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx106557-TPLmwa2EccB9.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx106557-TPLmwa2EccB9.jpg", + }, + "id": 106557, + "title": { + "english": "A Place to Name", + "userPreferred": "Sono Ie no Namae", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b118133-y7RvDFmr30hZ.jpg", + }, + "id": 118133, + "title": { + "english": "In Inertia", + "userPreferred": "Guzu no Soko", + }, + }, + { + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx169686-exScHzB5UX2D.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx169686-exScHzB5UX2D.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx169686-exScHzB5UX2D.jpg", + }, + "id": 169686, + "title": { + "english": "Indoor Days", + "userPreferred": "Soto ni Denai hi", + }, + }, + ], + "success": true, +} +`; diff --git a/src/controllers/title/__snapshots__/index.spec.ts.snap b/src/controllers/title/__snapshots__/index.spec.ts.snap index 01a5365..a999af3 100644 --- a/src/controllers/title/__snapshots__/index.spec.ts.snap +++ b/src/controllers/title/__snapshots__/index.spec.ts.snap @@ -77,3 +77,159 @@ The pages of Grimms' Fairy Tales, written by Jacob and Wilhelm, are now presente "success": true, } `; + +exports[`requests the "/title" route with a valid id & token 1`] = ` +{ + "result": { + "averageScore": 66, + "bannerImage": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", + "countryOfOrigin": "JP", + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx135643-2kJt86K9Db9P.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx135643-2kJt86K9Db9P.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx135643-2kJt86K9Db9P.jpg", + }, + "description": +"Once upon a time, brothers Jacob and Wilhelm collected fairy tales from across the land and made them into a book. They also had a much younger sister, the innocent and curious Charlotte, who they loved very much. One day, while the brothers were telling Charlotte a fairy tale like usual, they saw that she had a somewhat melancholy look on her face. She asked them, "Do you suppose they really lived happily ever after?" +

+The pages of Grimms' Fairy Tales, written by Jacob and Wilhelm, are now presented from the unique perspective of Charlotte, who sees the stories quite differently from her brothers. +

+(Source: Netflix Anime)" +, + "episodes": 6, + "genres": [ + "Fantasy", + "Thriller", + ], + "id": 135643, + "idMal": 49210, + "mediaListEntry": { + "id": 402665918, + "progress": 1, + "status": "CURRENT", + }, + "nextAiringEpisode": null, + "status": "FINISHED", + "title": { + "english": "The Grimm Variations", + "userPreferred": "The Grimm Variations", + }, + }, + "success": true, +} +`; + +exports[`requests the "/title" route with a valid id but no token 1`] = ` +{ + "result": { + "averageScore": 66, + "bannerImage": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", + "countryOfOrigin": "JP", + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx135643-2kJt86K9Db9P.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx135643-2kJt86K9Db9P.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx135643-2kJt86K9Db9P.jpg", + }, + "description": +"Once upon a time, brothers Jacob and Wilhelm collected fairy tales from across the land and made them into a book. They also had a much younger sister, the innocent and curious Charlotte, who they loved very much. One day, while the brothers were telling Charlotte a fairy tale like usual, they saw that she had a somewhat melancholy look on her face. She asked them, "Do you suppose they really lived happily ever after?" +

+The pages of Grimms' Fairy Tales, written by Jacob and Wilhelm, are now presented from the unique perspective of Charlotte, who sees the stories quite differently from her brothers. +

+(Source: Netflix Anime)" +, + "episodes": 6, + "genres": [ + "Fantasy", + "Thriller", + ], + "id": 135643, + "idMal": 49210, + "mediaListEntry": null, + "nextAiringEpisode": null, + "status": "FINISHED", + "title": { + "english": "The Grimm Variations", + "userPreferred": "The Grimm Variations", + }, + }, + "success": true, +} +`; + +exports[`requests the "/title" route with a valid id & token 1`] = ` +{ + "result": { + "averageScore": 66, + "bannerImage": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", + "countryOfOrigin": "JP", + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx135643-2kJt86K9Db9P.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx135643-2kJt86K9Db9P.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx135643-2kJt86K9Db9P.jpg", + }, + "description": +"Once upon a time, brothers Jacob and Wilhelm collected fairy tales from across the land and made them into a book. They also had a much younger sister, the innocent and curious Charlotte, who they loved very much. One day, while the brothers were telling Charlotte a fairy tale like usual, they saw that she had a somewhat melancholy look on her face. She asked them, "Do you suppose they really lived happily ever after?" +

+The pages of Grimms' Fairy Tales, written by Jacob and Wilhelm, are now presented from the unique perspective of Charlotte, who sees the stories quite differently from her brothers. +

+(Source: Netflix Anime)" +, + "episodes": 6, + "genres": [ + "Fantasy", + "Thriller", + ], + "id": 135643, + "idMal": 49210, + "mediaListEntry": { + "id": 402665918, + "progress": 1, + "status": "CURRENT", + }, + "nextAiringEpisode": null, + "status": "FINISHED", + "title": { + "english": "The Grimm Variations", + "userPreferred": "The Grimm Variations", + }, + }, + "success": true, +} +`; + +exports[`requests the "/title" route with a valid id but no token 1`] = ` +{ + "result": { + "averageScore": 66, + "bannerImage": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", + "countryOfOrigin": "JP", + "coverImage": { + "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx135643-2kJt86K9Db9P.jpg", + "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx135643-2kJt86K9Db9P.jpg", + "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx135643-2kJt86K9Db9P.jpg", + }, + "description": +"Once upon a time, brothers Jacob and Wilhelm collected fairy tales from across the land and made them into a book. They also had a much younger sister, the innocent and curious Charlotte, who they loved very much. One day, while the brothers were telling Charlotte a fairy tale like usual, they saw that she had a somewhat melancholy look on her face. She asked them, "Do you suppose they really lived happily ever after?" +

+The pages of Grimms' Fairy Tales, written by Jacob and Wilhelm, are now presented from the unique perspective of Charlotte, who sees the stories quite differently from her brothers. +

+(Source: Netflix Anime)" +, + "episodes": 6, + "genres": [ + "Fantasy", + "Thriller", + ], + "id": 135643, + "idMal": 49210, + "mediaListEntry": null, + "nextAiringEpisode": null, + "status": "FINISHED", + "title": { + "english": "The Grimm Variations", + "userPreferred": "The Grimm Variations", + }, + }, + "success": true, +} +`; diff --git a/src/controllers/token/index.spec.ts b/src/controllers/token/index.spec.ts index 17352f0..d6c11f3 100644 --- a/src/controllers/token/index.spec.ts +++ b/src/controllers/token/index.spec.ts @@ -27,7 +27,7 @@ describe("requests the /token route", () => { headers: new Headers({ "Content-Type": "application/json", }), - body: JSON.stringify({ token: "123", deviceId: "123", username: "test" }), + body: JSON.stringify({ token: "123", deviceId: "123" }), }); expect(res.json()).resolves.toEqual({ success: true }); @@ -41,50 +41,7 @@ describe("requests the /token route", () => { headers: new Headers({ "Content-Type": "application/json", }), - body: JSON.stringify({ token: "123", deviceId: "123", username: "test" }), - }); - - const row = await db - .select() - .from(deviceTokensTable) - .where(eq(deviceTokensTable.deviceId, "123")) - .get(); - - expect(row).toEqual({ - deviceId: "123", - token: "123", - username: "test", - lastConnectedAt: expect.any(String), - }); - // since SQL timestamp doesn't support milliseconds, compare to nearest second - expect( - +DateTime.fromSQL(row!.lastConnectedAt!, { zone: "utc" }).startOf( - "second", - ), - ).toBeGreaterThanOrEqual(+minimumTimestamp.startOf("second")); - }); - - it("with username as null, should succeed", async () => { - const res = await app.request("/token", { - method: "POST", - headers: new Headers({ - "Content-Type": "application/json", - }), - body: JSON.stringify({ token: "123", deviceId: "123", username: null }), - }); - - expect(res.json()).resolves.toEqual({ success: true }); - expect(res.status).toBe(200); - }); - - it("with username as null, db should contain entry", async () => { - const minimumTimestamp = DateTime.now(); - await app.request("/token", { - method: "POST", - headers: new Headers({ - "Content-Type": "application/json", - }), - body: JSON.stringify({ token: "123", deviceId: "123", username: null }), + body: JSON.stringify({ token: "123", deviceId: "123" }), }); const row = await db @@ -117,7 +74,7 @@ describe("requests the /token route", () => { headers: new Headers({ "Content-Type": "application/json", }), - body: JSON.stringify({ token: "124", deviceId: "123", username: null }), + body: JSON.stringify({ token: "124", deviceId: "123" }), }); expect(res.json()).resolves.toEqual({ success: true }); @@ -134,7 +91,7 @@ describe("requests the /token route", () => { headers: new Headers({ "Content-Type": "application/json", }), - body: JSON.stringify({ token: "124", deviceId: "123", username: null }), + body: JSON.stringify({ token: "124", deviceId: "123" }), }); const row = await db @@ -157,23 +114,6 @@ describe("requests the /token route", () => { ).toBeGreaterThanOrEqual(+minimumTimestamp.startOf("second")); }); - it("token already exists in db, should fail", async () => { - await db - .insert(deviceTokensTable) - .values({ deviceId: "123", token: "123" }); - - const res = await app.request("/token", { - method: "POST", - headers: new Headers({ - "Content-Type": "application/json", - }), - body: JSON.stringify({ token: "123", deviceId: "124", username: null }), - }); - - expect(res.json()).resolves.toEqual({ success: false }); - expect(res.status).toBe(412); - }); - it("token already exists in db, should not insert new entry", async () => { await db .insert(deviceTokensTable) @@ -183,7 +123,7 @@ describe("requests the /token route", () => { headers: new Headers({ "Content-Type": "application/json", }), - body: JSON.stringify({ token: "123", deviceId: "124", username: null }), + body: JSON.stringify({ token: "123", deviceId: "124" }), }); const row = await db @@ -195,64 +135,6 @@ describe("requests the /token route", () => { expect(row).toBeUndefined(); }); - it("associating a username with a token, should succeed", async () => { - await db - .insert(deviceTokensTable) - .values({ deviceId: "123", token: "123" }); - - const res = await app.request("/token", { - method: "POST", - headers: new Headers({ - "Content-Type": "application/json", - }), - body: JSON.stringify({ - token: "123", - deviceId: "123", - username: "aniplay", - }), - }); - - expect(res.json()).resolves.toEqual({ success: true }); - expect(res.status).toBe(200); - }); - - it("associating a username with a token should update existing entry", async () => { - const minimumTimestamp = DateTime.now(); - await db - .insert(deviceTokensTable) - .values({ deviceId: "123", token: "123" }); - await app.request("/token", { - method: "POST", - headers: new Headers({ - "Content-Type": "application/json", - }), - body: JSON.stringify({ - token: "123", - deviceId: "123", - username: "aniplay", - }), - }); - - const row = await db - .select() - .from(deviceTokensTable) - .where(eq(deviceTokensTable.deviceId, "123")) - .get(); - - expect(row).toEqual({ - deviceId: "123", - token: "123", - username: "aniplay", - lastConnectedAt: expect.any(String), - }); - // since SQL timestamp doesn't support milliseconds, compare to nearest second - expect( - +DateTime.fromSQL(row!.lastConnectedAt!, { zone: "utc" }).startOf( - "second", - ), - ).toBeGreaterThanOrEqual(+minimumTimestamp.startOf("second")); - }); - it("token is invalid, should fail", async () => { mock.module("src/libs/fcm/verifyFcmToken", () => ({ verifyFcmToken: () => false, @@ -263,7 +145,7 @@ describe("requests the /token route", () => { headers: new Headers({ "Content-Type": "application/json", }), - body: JSON.stringify({ token: "123", deviceId: "124", username: null }), + body: JSON.stringify({ token: "123", deviceId: "124" }), }); expect(res.json()).resolves.toEqual({ success: false }); @@ -279,7 +161,7 @@ describe("requests the /token route", () => { headers: new Headers({ "Content-Type": "application/json", }), - body: JSON.stringify({ token: "123", deviceId: "124", username: null }), + body: JSON.stringify({ token: "123", deviceId: "124" }), }); const row = await db diff --git a/src/controllers/token/index.ts b/src/controllers/token/index.ts index dc43684..1e4a9a2 100644 --- a/src/controllers/token/index.ts +++ b/src/controllers/token/index.ts @@ -3,7 +3,6 @@ import { env } from "hono/adapter"; import mapKeys from "lodash.mapkeys"; import { Case, changeStringCase } from "~/libs/changeStringCase"; -import { TokenAlreadyExistsError } from "~/libs/errors/TokenAlreadyExists"; import type { AdminSdkCredentials } from "~/libs/fcm/getGoogleAuthToken"; import { verifyFcmToken } from "~/libs/fcm/verifyFcmToken"; import { readEnvVariable } from "~/libs/readEnvVariable"; @@ -21,7 +20,6 @@ const app = new OpenAPIHono(); const SaveTokenRequest = z.object({ token: z.string(), deviceId: z.string(), - username: z.string().nullable(), }); const SaveTokenResponse = SuccessResponseSchema(); @@ -70,8 +68,7 @@ const route = createRoute({ }); app.openapi(route, async (c) => { - const { token, deviceId, username } = - await c.req.json(); + const { token, deviceId } = await c.req.json(); try { const isValidToken = await verifyFcmToken( @@ -85,12 +82,8 @@ app.openapi(route, async (c) => { return c.json(ErrorResponse, 401); } - await saveToken(env(c, "workerd"), deviceId, token, username); + await saveToken(env(c, "workerd"), deviceId, token); } catch (error) { - if (error instanceof TokenAlreadyExistsError) { - return c.json(ErrorResponse, 412); - } - console.error(new Error("Failed to save token", { cause: error })); return c.json(ErrorResponse, 500); } diff --git a/src/libs/errors/TokenAlreadyExists.ts b/src/libs/errors/TokenAlreadyExists.ts deleted file mode 100644 index abdaa41..0000000 --- a/src/libs/errors/TokenAlreadyExists.ts +++ /dev/null @@ -1,5 +0,0 @@ -export class TokenAlreadyExistsError extends Error { - constructor() { - super("Token already exists in the database"); - } -} diff --git a/src/models/token.ts b/src/models/token.ts index c220720..707110e 100644 --- a/src/models/token.ts +++ b/src/models/token.ts @@ -1,68 +1,33 @@ -import { and, eq, gt, or, sql } from "drizzle-orm"; +import { and, eq, gt, sql } from "drizzle-orm"; -import { TokenAlreadyExistsError } from "~/libs/errors/TokenAlreadyExists"; import type { Env } from "~/types/env"; import { getDb } from "./db"; import { deviceTokensTable, watchStatusTable } from "./schema"; -export function saveToken( - env: Env, - deviceId: string, - token: string, - username: string | null, -) { - return getDb(env) - .select() - .from(deviceTokensTable) - .where( - or( - eq(deviceTokensTable.deviceId, deviceId), - eq(deviceTokensTable.token, token), - ), - ) - .then((existingTokens) => { - const existingToken = existingTokens.find( - ({ token: existingToken, deviceId: existingDeviceId }) => - existingToken === token || existingDeviceId === deviceId, - ); - if (!existingToken) { - return insertToken(env, deviceId, token, username); - } - - if ( - existingToken.token === token && - !existingToken.username && - !username - ) { - throw new TokenAlreadyExistsError(); - } - - return updateToken(env, deviceId, token, username); - }); +export function saveToken(env: Env, deviceId: string, token: string) { + return insertToken(env, deviceId, token); } -function insertToken( - env: Env, - deviceId: string, - token: string, - username: string | null, -) { +function insertToken(env: Env, deviceId: string, token: string) { return getDb(env) .insert(deviceTokensTable) - .values({ deviceId, token, username }) + .values({ deviceId, token }) + .onConflictDoUpdate({ + set: { token }, + target: [deviceTokensTable.deviceId], + }) .run(); } -function updateToken( +export function associateDeviceIdWithUsername( env: Env, deviceId: string, - token: string, - username: string | null, + username: string, ) { return getDb(env) .update(deviceTokensTable) - .set({ token, username }) + .set({ username }) .where(eq(deviceTokensTable.deviceId, deviceId)) .run(); }