From 6a759366aa91b785d507cdcfe48220587e7cc6db Mon Sep 17 00:00:00 2001 From: Amund Eggen Svandal Date: Sat, 11 Jul 2026 15:20:40 +0200 Subject: [PATCH] refactor(mocks): type handler responses against API types Annotate every JSON response in the MSW handlers with the API type it represents (const response: API... = ...; return HttpResponse.json(response)). This makes the mocks fail to compile if they drift from the expected flashlight API response shapes. Adds exported response types for the account endpoints (APIUsernameResponse, APIUUIDResponse) and exports the existing APIWrappedData so the mocks can reference it. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/mocks/handlers.ts | 56 +++++++++++++++++++++-------------------- src/queries/username.ts | 5 ++++ src/queries/uuid.ts | 5 ++++ src/queries/wrapped.ts | 2 +- 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index 6ffc5cdc..c1f5e192 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -3,6 +3,9 @@ import { http, HttpResponse } from "msw"; import { isNormalizedUUID } from "#helpers/uuid.ts"; import type { APIPlayerDataPIT } from "#queries/playerdata.ts"; import type { APISession } from "#queries/sessions.ts"; +import type { APIUsernameResponse } from "#queries/username.ts"; +import type { APIUUIDResponse } from "#queries/uuid.ts"; +import type { APIWrappedData } from "#queries/wrapped.ts"; import { findUserByUsername, @@ -37,21 +40,20 @@ export const handlers = [ const user = findUserByUUID(uuid); if (user === null) { - return HttpResponse.json( - { - success: false, - uuid, - cause: "not found", - }, - { status: 404 }, - ); + const response: APIUsernameResponse = { + success: false, + uuid, + cause: "not found", + }; + return HttpResponse.json(response, { status: 404 }); } - return HttpResponse.json({ + const response: APIUsernameResponse = { success: true, username: user.username, uuid: user.uuid, - }); + }; + return HttpResponse.json(response); }), http.get(flashlightEndpoint("v1/account/username/:username"), (req) => { const { username } = req.params; @@ -61,21 +63,20 @@ export const handlers = [ const user = findUserByUsername(username); if (user === null) { - return HttpResponse.json( - { - success: false, - username, - cause: "not found", - }, - { status: 404 }, - ); + const response: APIUUIDResponse = { + success: false, + username, + cause: "not found", + }; + return HttpResponse.json(response, { status: 404 }); } - return HttpResponse.json({ + const response: APIUUIDResponse = { success: true, username: user.username, uuid: user.uuid, - }); + }; + return HttpResponse.json(response); }), http.post(flashlightEndpoint("v1/history"), async ({ request }) => { const body = (await request.json()) as { @@ -91,22 +92,22 @@ export const handlers = [ if (body.limit <= 2) { // For limit=2: return start and end only - const history: APIPlayerDataPIT[] = [ + const response: APIPlayerDataPIT[] = [ makePlayerDataPIT(body.uuid, startDate.toISOString(), 1), makePlayerDataPIT(body.uuid, endDate.toISOString(), 3), ].slice(0, body.limit); - return HttpResponse.json(history); + return HttpResponse.json(response); } const midDate = new Date((startDate.getTime() + endDate.getTime()) / 2); - const history: APIPlayerDataPIT[] = [ + const response: APIPlayerDataPIT[] = [ makePlayerDataPIT(body.uuid, startDate.toISOString(), 1), makePlayerDataPIT(body.uuid, midDate.toISOString(), 2), makePlayerDataPIT(body.uuid, endDate.toISOString(), 3), ].slice(0, body.limit); - return HttpResponse.json(history); + return HttpResponse.json(response); }), http.post(flashlightEndpoint("v1/sessions"), async ({ request }) => { const body = (await request.json()) as { @@ -120,12 +121,12 @@ export const handlers = [ const endDate = new Date(body.end); const midDate = new Date((startDate.getTime() + endDate.getTime()) / 2); - const sessions: APISession[] = [ + const response: APISession[] = [ makeSession(body.uuid, startDate.toISOString(), midDate.toISOString()), makeSession(body.uuid, midDate.toISOString(), endDate.toISOString()), ]; - return HttpResponse.json(sessions); + return HttpResponse.json(response); }), http.get(flashlightEndpoint("v1/wrapped/:uuid/:year"), (req) => { const uuid = validateUUID(req.params.uuid); @@ -138,7 +139,8 @@ export const handlers = [ throw new Error("Invalid year parameter"); } - return HttpResponse.json(makeWrappedResponse(uuid, year)); + const response: APIWrappedData = makeWrappedResponse(uuid, year); + return HttpResponse.json(response); }), http.get("https://api.mineatar.io/:variant/:uuid", (req) => { validateUUID(req.params.uuid); diff --git a/src/queries/username.ts b/src/queries/username.ts index 7965a321..984f6a47 100644 --- a/src/queries/username.ts +++ b/src/queries/username.ts @@ -7,6 +7,11 @@ import { getOrSetUserId } from "#helpers/userId.ts"; import { isNormalizedUUID } from "#helpers/uuid.ts"; import { MS_PER_HOUR } from "#time.ts"; +// Response of the flashlight `/v1/account/uuid/:uuid` endpoint. +export type APIUsernameResponse = + | { readonly success: true; readonly username: string; readonly uuid: string } + | { readonly success: false; readonly uuid: string; readonly cause: string }; + export const getUsernameQueryOptions = (uuid: string) => queryOptions({ staleTime: MS_PER_HOUR, diff --git a/src/queries/uuid.ts b/src/queries/uuid.ts index 71429723..2cad1e99 100644 --- a/src/queries/uuid.ts +++ b/src/queries/uuid.ts @@ -7,6 +7,11 @@ import { getOrSetUserId } from "#helpers/userId.ts"; import { normalizeUUID } from "#helpers/uuid.ts"; import { MS_PER_DAY } from "#time.ts"; +// Response of the flashlight `/v1/account/username/:username` endpoint. +export type APIUUIDResponse = + | { readonly success: true; readonly username: string; readonly uuid: string } + | { readonly success: false; readonly username: string; readonly cause: string }; + export const getUUIDQueryOptions = (username: string) => queryOptions({ staleTime: MS_PER_DAY * 21, diff --git a/src/queries/wrapped.ts b/src/queries/wrapped.ts index 01fecaf9..c466298b 100644 --- a/src/queries/wrapped.ts +++ b/src/queries/wrapped.ts @@ -124,7 +124,7 @@ interface SessionStats { } // API response structure (before conversion) -interface APIWrappedData { +export interface APIWrappedData { readonly success: boolean; readonly uuid: string; readonly year: number;