Mise en place du framework de test #58
+81
-18
@@ -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
|
// deno-lint-ignore no-explicit-any
|
||||||
let _originalFetch: ((input: any, init?: any) => Promise<Response>) | null =
|
let _originalFetch: ((input: any, init?: any) => Promise<Response>) | null =
|
||||||
null;
|
null;
|
||||||
|
let _calls: { url: string; method: string; body?: unknown }[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remplace globalThis.fetch par un mock qui retourne des réponses
|
* Remplace globalThis.fetch par un mock configurable.
|
||||||
* pré-configurées selon l'URL.
|
|
||||||
*
|
*
|
||||||
* @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(
|
export function mockFetch(
|
||||||
routes: Record<string, unknown>,
|
routes: Record<string, unknown | MockRoute>,
|
||||||
): void {
|
): void {
|
||||||
_originalFetch = globalThis.fetch;
|
_originalFetch = globalThis.fetch;
|
||||||
|
_calls = [];
|
||||||
|
|
||||||
globalThis.fetch = (
|
globalThis.fetch = async (
|
||||||
input: string | URL | Request,
|
input: string | URL | Request,
|
||||||
_init?: RequestInit,
|
init?: RequestInit,
|
||||||
): Promise<Response> => {
|
): Promise<Response> => {
|
||||||
const url = typeof input === "string"
|
const url = typeof input === "string"
|
||||||
? input
|
? input
|
||||||
: input instanceof URL
|
: input instanceof URL
|
||||||
? input.toString()
|
? input.toString()
|
||||||
: input.url;
|
: input.url;
|
||||||
|
const method = (init?.method ?? "GET").toUpperCase();
|
||||||
|
|
||||||
for (const [pattern, data] of Object.entries(routes)) {
|
// Parse le body si présent
|
||||||
if (url.includes(pattern)) {
|
let reqBody: unknown = undefined;
|
||||||
return Promise.resolve(
|
if (init?.body) {
|
||||||
new Response(JSON.stringify(data), {
|
try {
|
||||||
|
reqBody = JSON.parse(init.body as string);
|
||||||
|
} catch {
|
||||||
|
reqBody = init.body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_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,
|
status: 200,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(
|
// Config avancée : vérifier la méthode si spécifiée
|
||||||
new Response(JSON.stringify({ error: "Not Found" }), {
|
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,
|
status: 404,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,4 +100,20 @@ export function restoreFetch(): void {
|
|||||||
globalThis.fetch = _originalFetch;
|
globalThis.fetch = _originalFetch;
|
||||||
_originalFetch = null;
|
_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 {
|
export interface Student {
|
||||||
numEtud: number;
|
numEtud: number;
|
||||||
nom: string;
|
nom: string;
|
||||||
prenom: string;
|
prenom: string;
|
||||||
idPromo: number;
|
idPromo: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Promotion {
|
export interface Promotion {
|
||||||
idPromo: number;
|
idPromo: string;
|
||||||
annee: string;
|
annee: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,14 +21,14 @@ export interface Prof {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Module {
|
export interface Module {
|
||||||
id: number;
|
id: string;
|
||||||
nom: string;
|
nom: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Note {
|
export interface Note {
|
||||||
note: number;
|
note: number;
|
||||||
numEtud: number;
|
numEtud: number;
|
||||||
idModule: number;
|
idModule: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UE {
|
export interface UE {
|
||||||
@@ -35,16 +37,16 @@ export interface UE {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UeModule {
|
export interface UeModule {
|
||||||
idModule: number;
|
idModule: string;
|
||||||
idUE: number;
|
idUE: number;
|
||||||
idPromo: number;
|
idPromo: string;
|
||||||
coeff: number;
|
coeff: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Enseignement {
|
export interface Enseignement {
|
||||||
idProf: number;
|
idProf: number;
|
||||||
idModule: number;
|
idModule: string;
|
||||||
idPromo: number;
|
idPromo: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Ajustement {
|
export interface Ajustement {
|
||||||
@@ -53,17 +55,37 @@ export interface Ajustement {
|
|||||||
valeur: number;
|
valeur: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ImportResult {
|
||||||
|
imported: number;
|
||||||
|
errors: { line: number; message: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiError {
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Fixtures ---
|
// --- Fixtures ---
|
||||||
|
|
||||||
export const students: Student[] = [
|
export const students: Student[] = [
|
||||||
{ numEtud: 1, nom: "Dupont", prenom: "Alice", idPromo: 1 },
|
{ numEtud: 21212006, nom: "Dupont", prenom: "Jean", idPromo: "4AFISE25/26" },
|
||||||
{ numEtud: 2, nom: "Martin", prenom: "Bob", idPromo: 1 },
|
{
|
||||||
{ numEtud: 3, nom: "Durand", prenom: "Claire", idPromo: 2 },
|
numEtud: 21212007,
|
||||||
|
nom: "Martin",
|
||||||
|
prenom: "Alice",
|
||||||
|
idPromo: "4AFISE25/26",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
numEtud: 21212008,
|
||||||
|
nom: "Durand",
|
||||||
|
prenom: "Claire",
|
||||||
|
idPromo: "3AFISE25/26",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const promotions: Promotion[] = [
|
export const promotions: Promotion[] = [
|
||||||
{ idPromo: 1, annee: "2025-2026" },
|
{ idPromo: "4AFISE25/26", annee: "2025" },
|
||||||
{ idPromo: 2, annee: "2024-2025" },
|
{ idPromo: "3AFISE25/26", annee: "2025" },
|
||||||
|
{ idPromo: "JIA4A2526", annee: "2025" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const profs: Prof[] = [
|
export const profs: Prof[] = [
|
||||||
@@ -72,36 +94,44 @@ export const profs: Prof[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const modules: Module[] = [
|
export const modules: Module[] = [
|
||||||
{ id: 1, nom: "Mathématiques" },
|
{ id: "JIN702C", nom: "Optimisation" },
|
||||||
{ id: 2, nom: "Informatique" },
|
{ id: "JIN703C", nom: "Informatique" },
|
||||||
{ id: 3, nom: "Physique" },
|
{ id: "JIN704C", nom: "Physique" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const notes: Note[] = [
|
export const notes: Note[] = [
|
||||||
{ note: 15, numEtud: 1, idModule: 1 },
|
{ note: 15.5, numEtud: 21212006, idModule: "JIN702C" },
|
||||||
{ note: 12, numEtud: 1, idModule: 2 },
|
{ note: 12.0, numEtud: 21212006, idModule: "JIN703C" },
|
||||||
{ note: 18, numEtud: 2, idModule: 1 },
|
{ note: 18.0, numEtud: 21212007, idModule: "JIN702C" },
|
||||||
{ note: 9, numEtud: 3, idModule: 3 },
|
{ note: 9.0, numEtud: 21212008, idModule: "JIN704C" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ues: UE[] = [
|
export const ues: UE[] = [
|
||||||
{ id: 1, nom: "Sciences fondamentales" },
|
{ id: 1, nom: "UE Informatique" },
|
||||||
{ id: 2, nom: "Sciences appliquées" },
|
{ id: 2, nom: "UE Mathématiques" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ueModules: UeModule[] = [
|
export const ueModules: UeModule[] = [
|
||||||
{ idModule: 1, idUE: 1, idPromo: 1, coeff: 3 },
|
{ idModule: "JIN702C", idUE: 1, idPromo: "4AFISE25/26", coeff: 3.0 },
|
||||||
{ idModule: 2, idUE: 2, idPromo: 1, coeff: 4 },
|
{ idModule: "JIN703C", idUE: 2, idPromo: "4AFISE25/26", coeff: 4.0 },
|
||||||
{ idModule: 3, idUE: 1, idPromo: 2, coeff: 2 },
|
{ idModule: "JIN704C", idUE: 1, idPromo: "3AFISE25/26", coeff: 2.0 },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const enseignements: Enseignement[] = [
|
export const enseignements: Enseignement[] = [
|
||||||
{ idProf: 1, idModule: 1, idPromo: 1 },
|
{ idProf: 1, idModule: "JIN702C", idPromo: "4AFISE25/26" },
|
||||||
{ idProf: 2, idModule: 2, idPromo: 1 },
|
{ idProf: 2, idModule: "JIN703C", idPromo: "4AFISE25/26" },
|
||||||
{ idProf: 1, idModule: 3, idPromo: 2 },
|
{ idProf: 1, idModule: "JIN704C", idPromo: "3AFISE25/26" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ajustements: Ajustement[] = [
|
export const ajustements: Ajustement[] = [
|
||||||
{ numEtud: 1, idUE: 1, valeur: 0.5 },
|
{ numEtud: 21212006, idUE: 1, valeur: 13.25 },
|
||||||
{ numEtud: 3, idUE: 1, valeur: -1 },
|
{ 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