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:
+84
-21
@@ -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
@@ -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" };
|
||||
|
||||
Reference in New Issue
Block a user