From 17c5b33a5ba70b715de4560f7ee53b2df732021f Mon Sep 17 00:00:00 2001 From: Djalim Simaila Date: Tue, 21 Apr 2026 11:31:45 +0200 Subject: [PATCH] refactor(test): improve fetch mock and update fixture types Add support for HTTP methods, status codes, body and headers in the fetch mock. Track calls and expose getFetchCalls for assertions. Update fixture interfaces to use string IDs, add ImportResult and ApiError types, and provide standard error constants. Adjust fixture data to match new types. --- tests/helpers/api_mock.ts | 105 ++++++++++++++++++++++++++++++-------- tests/helpers/fixtures.ts | 92 ++++++++++++++++++++++----------- 2 files changed, 145 insertions(+), 52 deletions(-) diff --git a/tests/helpers/api_mock.ts b/tests/helpers/api_mock.ts index 686f32d..e7fb68a 100644 --- a/tests/helpers/api_mock.ts +++ b/tests/helpers/api_mock.ts @@ -1,47 +1,94 @@ -// Mock de fetch() pour les tests +// Mock de fetch() pour les tests — supporte méthodes HTTP et status codes + +type HttpMethod = "GET" | "POST" | "PUT" | "DELETE"; + +export interface MockRoute { + method?: HttpMethod; + status?: number; + body?: unknown; + headers?: Record; +} // deno-lint-ignore no-explicit-any let _originalFetch: ((input: any, init?: any) => Promise) | null = null; +let _calls: { url: string; method: string; body?: unknown }[] = []; /** - * Remplace globalThis.fetch par un mock qui retourne des réponses - * pré-configurées selon l'URL. + * Remplace globalThis.fetch par un mock configurable. * - * @param routes - Map URL pattern → données de réponse (sera sérialisé en JSON) + * Usage simple (GET 200 par défaut) : + * mockFetch({ "/students": studentsData }) + * + * Usage avancé (méthode + status) : + * mockFetch({ "/students": { method: "POST", status: 201, body: newStudent } }) */ export function mockFetch( - routes: Record, + routes: Record, ): void { _originalFetch = globalThis.fetch; + _calls = []; - globalThis.fetch = ( + globalThis.fetch = async ( input: string | URL | Request, - _init?: RequestInit, + init?: RequestInit, ): Promise => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const method = (init?.method ?? "GET").toUpperCase(); - for (const [pattern, data] of Object.entries(routes)) { - if (url.includes(pattern)) { - return Promise.resolve( - new Response(JSON.stringify(data), { - status: 200, - headers: { "Content-Type": "application/json" }, - }), - ); + // Parse le body si présent + let reqBody: unknown = undefined; + if (init?.body) { + try { + reqBody = JSON.parse(init.body as string); + } catch { + reqBody = init.body; } } - return Promise.resolve( - new Response(JSON.stringify({ error: "Not Found" }), { - status: 404, - headers: { "Content-Type": "application/json" }, - }), - ); + _calls.push({ url, method, body: reqBody }); + + for (const [pattern, config] of Object.entries(routes)) { + if (!url.includes(pattern)) continue; + + // Config simple : la valeur est directement le body de réponse (GET 200) + if (!isRouteConfig(config)) { + return new Response(JSON.stringify(config), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + // Config avancée : vérifier la méthode si spécifiée + if (config.method && config.method !== method) continue; + + const status = config.status ?? 200; + + // 204 : pas de body + if (status === 204) { + return new Response(null, { status: 204 }); + } + + return new Response( + config.body !== undefined ? JSON.stringify(config.body) : null, + { + status, + headers: { + "Content-Type": "application/json", + ...config.headers, + }, + }, + ); + } + + return new Response(JSON.stringify({ error: "Not Found" }), { + status: 404, + headers: { "Content-Type": "application/json" }, + }); }; } @@ -53,4 +100,20 @@ export function restoreFetch(): void { globalThis.fetch = _originalFetch; _originalFetch = null; } + _calls = []; +} + +/** + * Retourne la liste des appels fetch interceptés. + */ +export function getFetchCalls(): { url: string; method: string; body?: unknown }[] { + return [..._calls]; +} + +function isRouteConfig(value: unknown): value is MockRoute { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + return false; + } + const v = value as Record; + return "status" in v || "method" in v || "body" in v; } diff --git a/tests/helpers/fixtures.ts b/tests/helpers/fixtures.ts index e63920e..67ece22 100644 --- a/tests/helpers/fixtures.ts +++ b/tests/helpers/fixtures.ts @@ -1,14 +1,16 @@ -// Types et données de test pour l'API PolyMPR +// Types et données de test alignés sur l'API REST PolyMPR + +// --- Types --- export interface Student { numEtud: number; nom: string; prenom: string; - idPromo: number; + idPromo: string; } export interface Promotion { - idPromo: number; + idPromo: string; annee: string; } @@ -19,14 +21,14 @@ export interface Prof { } export interface Module { - id: number; + id: string; nom: string; } export interface Note { note: number; numEtud: number; - idModule: number; + idModule: string; } export interface UE { @@ -35,16 +37,16 @@ export interface UE { } export interface UeModule { - idModule: number; + idModule: string; idUE: number; - idPromo: number; + idPromo: string; coeff: number; } export interface Enseignement { idProf: number; - idModule: number; - idPromo: number; + idModule: string; + idPromo: string; } export interface Ajustement { @@ -53,17 +55,37 @@ export interface Ajustement { valeur: number; } +export interface ImportResult { + imported: number; + errors: { line: number; message: string }[]; +} + +export interface ApiError { + error: string; +} + // --- Fixtures --- export const students: Student[] = [ - { numEtud: 1, nom: "Dupont", prenom: "Alice", idPromo: 1 }, - { numEtud: 2, nom: "Martin", prenom: "Bob", idPromo: 1 }, - { numEtud: 3, nom: "Durand", prenom: "Claire", idPromo: 2 }, + { numEtud: 21212006, nom: "Dupont", prenom: "Jean", idPromo: "4AFISE25/26" }, + { + numEtud: 21212007, + nom: "Martin", + prenom: "Alice", + idPromo: "4AFISE25/26", + }, + { + numEtud: 21212008, + nom: "Durand", + prenom: "Claire", + idPromo: "3AFISE25/26", + }, ]; export const promotions: Promotion[] = [ - { idPromo: 1, annee: "2025-2026" }, - { idPromo: 2, annee: "2024-2025" }, + { idPromo: "4AFISE25/26", annee: "2025" }, + { idPromo: "3AFISE25/26", annee: "2025" }, + { idPromo: "JIA4A2526", annee: "2025" }, ]; export const profs: Prof[] = [ @@ -72,36 +94,44 @@ export const profs: Prof[] = [ ]; export const modules: Module[] = [ - { id: 1, nom: "Mathématiques" }, - { id: 2, nom: "Informatique" }, - { id: 3, nom: "Physique" }, + { id: "JIN702C", nom: "Optimisation" }, + { id: "JIN703C", nom: "Informatique" }, + { id: "JIN704C", nom: "Physique" }, ]; export const notes: Note[] = [ - { note: 15, numEtud: 1, idModule: 1 }, - { note: 12, numEtud: 1, idModule: 2 }, - { note: 18, numEtud: 2, idModule: 1 }, - { note: 9, numEtud: 3, idModule: 3 }, + { note: 15.5, numEtud: 21212006, idModule: "JIN702C" }, + { note: 12.0, numEtud: 21212006, idModule: "JIN703C" }, + { note: 18.0, numEtud: 21212007, idModule: "JIN702C" }, + { note: 9.0, numEtud: 21212008, idModule: "JIN704C" }, ]; export const ues: UE[] = [ - { id: 1, nom: "Sciences fondamentales" }, - { id: 2, nom: "Sciences appliquées" }, + { id: 1, nom: "UE Informatique" }, + { id: 2, nom: "UE Mathématiques" }, ]; export const ueModules: UeModule[] = [ - { idModule: 1, idUE: 1, idPromo: 1, coeff: 3 }, - { idModule: 2, idUE: 2, idPromo: 1, coeff: 4 }, - { idModule: 3, idUE: 1, idPromo: 2, coeff: 2 }, + { idModule: "JIN702C", idUE: 1, idPromo: "4AFISE25/26", coeff: 3.0 }, + { idModule: "JIN703C", idUE: 2, idPromo: "4AFISE25/26", coeff: 4.0 }, + { idModule: "JIN704C", idUE: 1, idPromo: "3AFISE25/26", coeff: 2.0 }, ]; export const enseignements: Enseignement[] = [ - { idProf: 1, idModule: 1, idPromo: 1 }, - { idProf: 2, idModule: 2, idPromo: 1 }, - { idProf: 1, idModule: 3, idPromo: 2 }, + { idProf: 1, idModule: "JIN702C", idPromo: "4AFISE25/26" }, + { idProf: 2, idModule: "JIN703C", idPromo: "4AFISE25/26" }, + { idProf: 1, idModule: "JIN704C", idPromo: "3AFISE25/26" }, ]; export const ajustements: Ajustement[] = [ - { numEtud: 1, idUE: 1, valeur: 0.5 }, - { numEtud: 3, idUE: 1, valeur: -1 }, + { numEtud: 21212006, idUE: 1, valeur: 13.25 }, + { numEtud: 21212008, idUE: 1, valeur: 11.0 }, ]; + +// --- Réponses d'erreur standard --- + +export const ERROR_NOT_FOUND: ApiError = { error: "Ressource introuvable" }; +export const ERROR_CONFLICT: ApiError = { error: "Ressource déjà existante" }; +export const ERROR_BAD_REQUEST: ApiError = { error: "Requête invalide" }; +export const ERROR_UNAUTHORIZED: ApiError = { error: "Non authentifié" }; +export const ERROR_FORBIDDEN: ApiError = { error: "Accès interdit" };