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" };