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.
This commit is contained in:
2026-04-21 11:31:45 +02:00
parent edb20db2ef
commit 204a590b37
2 changed files with 145 additions and 52 deletions
+84 -21
View File
@@ -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<string, string>;
}
// deno-lint-ignore no-explicit-any
let _originalFetch: ((input: any, init?: any) => Promise<Response>) | 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<string, unknown>,
routes: Record<string, unknown | MockRoute>,
): void {
_originalFetch = globalThis.fetch;
_calls = [];
globalThis.fetch = (
globalThis.fetch = async (
input: string | URL | Request,
_init?: RequestInit,
init?: RequestInit,
): Promise<Response> => {
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<string, unknown>;
return "status" in v || "method" in v || "body" in v;
}
+61 -31
View File
@@ -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" };