diff --git a/tests/helpers/db_mock.ts b/tests/helpers/db_mock.ts new file mode 100644 index 0000000..4d18bb9 --- /dev/null +++ b/tests/helpers/db_mock.ts @@ -0,0 +1,122 @@ +// Mock de la couche Drizzle pour les tests unitaires/intégration +// Permet de tester les handlers sans connexion PostgreSQL + +export interface MockQueryResult { + rows: T[]; +} + +export interface MockDbConfig { + // Table name → array of rows + // deno-lint-ignore no-explicit-any + tables: Record[]>; +} + +/** + * Crée un mock de la DB Drizzle. + * Simule select/insert/update/delete avec un store en mémoire. + * + * Usage : + * ```ts + * const db = createMockDb({ + * tables: { + * students: [{ numEtud: 21212006, nom: "Dupont", ... }], + * notes: [], + * } + * }); + * + * // Lire toutes les lignes d'une table + * const rows = db.getTable("students"); + * + * // Insérer + * db.insert("students", { numEtud: 21212009, nom: "Test", ... }); + * + * // Trouver par clé + * const student = db.findOne("students", (r) => r.numEtud === 21212006); + * + * // Supprimer + * db.deleteWhere("students", (r) => r.numEtud === 21212006); + * ``` + */ +export function createMockDb(config: MockDbConfig) { + // Deep clone pour éviter les mutations entre tests + // deno-lint-ignore no-explicit-any + const tables: Record[]> = {}; + for (const [name, rows] of Object.entries(config.tables)) { + tables[name] = rows.map((r) => ({ ...r })); + } + + return { + /** Retourne toutes les lignes d'une table */ + getTable>(name: string): T[] { + return (tables[name] ?? []) as T[]; + }, + + /** Retourne les lignes qui matchent le filtre */ + findMany>( + name: string, + predicate: (row: T) => boolean, + ): T[] { + return (this.getTable(name)).filter(predicate); + }, + + /** Retourne la première ligne qui matche, ou undefined */ + findOne>( + name: string, + predicate: (row: T) => boolean, + ): T | undefined { + return (this.getTable(name)).find(predicate); + }, + + /** Insère une ligne dans la table */ + insert>(name: string, row: T): T { + if (!tables[name]) tables[name] = []; + const copy = { ...row } as T; + // deno-lint-ignore no-explicit-any + tables[name].push(copy as any); + return copy; + }, + + /** Met à jour les lignes qui matchent le prédicat */ + updateWhere>( + name: string, + predicate: (row: T) => boolean, + updates: Partial, + ): number { + const rows = this.getTable(name); + let count = 0; + for (const row of rows) { + if (predicate(row)) { + Object.assign(row as Record, updates); + count++; + } + } + return count; + }, + + /** Supprime les lignes qui matchent le prédicat */ + deleteWhere>( + name: string, + predicate: (row: T) => boolean, + ): number { + const before = (tables[name] ?? []).length; + tables[name] = (tables[name] ?? []).filter( + (r) => !predicate(r as unknown as T), + ); + return before - tables[name].length; + }, + + /** Vide une table */ + clear(name: string): void { + tables[name] = []; + }, + + /** Vide toutes les tables */ + reset(): void { + for (const name of Object.keys(tables)) { + tables[name] = []; + } + }, + }; +} + +export type MockDb = ReturnType; diff --git a/tests/unit/example_test.ts b/tests/unit/example_test.ts index 1de45e0..b0dc313 100644 --- a/tests/unit/example_test.ts +++ b/tests/unit/example_test.ts @@ -1,24 +1,46 @@ import { assertEquals, assertExists } from "@std/assert"; -import { mockFetch, restoreFetch } from "../helpers/api_mock.ts"; -import { notes, students } from "../helpers/fixtures.ts"; +import { + getFetchCalls, + mockFetch, + restoreFetch, +} from "../helpers/api_mock.ts"; +import { createMockDb } from "../helpers/db_mock.ts"; +import { + type Student, + ERROR_CONFLICT, + ERROR_NOT_FOUND, + modules, + notes, + students, +} from "../helpers/fixtures.ts"; import { cleanupDOM, setupDOM } from "../helpers/render.ts"; -Deno.test("fixtures - students have expected shape", () => { +// --- Fixtures --- + +Deno.test("fixtures - students match API shape", () => { assertEquals(students.length, 3); - assertEquals(students[0].nom, "Dupont"); - assertExists(students[0].numEtud); + assertEquals(students[0].numEtud, 21212006); + assertEquals(students[0].idPromo, "4AFISE25/26"); + assertEquals(typeof students[0].idPromo, "string"); }); -Deno.test("mockFetch - returns mocked data for matching route", async () => { - mockFetch({ - "/students": students, - "/notes": notes, - }); +Deno.test("fixtures - modules have string ids", () => { + assertEquals(modules[0].id, "JIN702C"); + assertEquals(typeof modules[0].id, "string"); +}); +Deno.test("fixtures - notes use decimal values", () => { + assertEquals(notes[0].note, 15.5); + assertEquals(notes[0].idModule, "JIN702C"); +}); + +// --- Mock fetch simple (GET 200) --- + +Deno.test("mockFetch - GET returns mocked data", async () => { + mockFetch({ "/students": students }); try { const res = await fetch("http://localhost/api/students"); assertEquals(res.status, 200); - const data = await res.json(); assertEquals(data.length, 3); assertEquals(data[0].nom, "Dupont"); @@ -29,7 +51,6 @@ Deno.test("mockFetch - returns mocked data for matching route", async () => { Deno.test("mockFetch - returns 404 for unknown routes", async () => { mockFetch({}); - try { const res = await fetch("http://localhost/api/unknown"); assertEquals(res.status, 404); @@ -38,24 +59,208 @@ Deno.test("mockFetch - returns 404 for unknown routes", async () => { } }); +// --- Mock fetch avancé (méthodes + status codes) --- + +Deno.test("mockFetch - POST 201 created", async () => { + const newStudent = students[0]; + mockFetch({ + "/students": { method: "POST", status: 201, body: newStudent }, + }); + try { + const res = await fetch("http://localhost/api/students", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(newStudent), + }); + assertEquals(res.status, 201); + const data = await res.json(); + assertEquals(data.numEtud, 21212006); + } finally { + restoreFetch(); + } +}); + +Deno.test("mockFetch - DELETE 204 no content", async () => { + mockFetch({ + "/students/21212006": { method: "DELETE", status: 204 }, + }); + try { + const res = await fetch("http://localhost/api/students/21212006", { + method: "DELETE", + }); + assertEquals(res.status, 204); + assertEquals(res.body, null); + } finally { + restoreFetch(); + } +}); + +Deno.test("mockFetch - 404 error response", async () => { + mockFetch({ + "/students/99999": { status: 404, body: ERROR_NOT_FOUND }, + }); + try { + const res = await fetch("http://localhost/api/students/99999"); + assertEquals(res.status, 404); + const data = await res.json(); + assertEquals(data.error, "Ressource introuvable"); + } finally { + restoreFetch(); + } +}); + +Deno.test("mockFetch - 409 conflict", async () => { + mockFetch({ + "/enseignements": { method: "POST", status: 409, body: ERROR_CONFLICT }, + }); + try { + const res = await fetch("http://localhost/api/enseignements", { + method: "POST", + body: JSON.stringify({ idProf: 1, idModule: "JIN702C", idPromo: "4AFISE25/26" }), + }); + assertEquals(res.status, 409); + } finally { + restoreFetch(); + } +}); + +// --- getFetchCalls --- + +Deno.test("getFetchCalls - tracks all intercepted calls", async () => { + mockFetch({ "/notes": notes }); + try { + await fetch("http://localhost/api/notes"); + await fetch("http://localhost/api/notes?numEtud=21212006"); + const calls = getFetchCalls(); + assertEquals(calls.length, 2); + assertEquals(calls[0].method, "GET"); + assertEquals(calls[1].url, "http://localhost/api/notes?numEtud=21212006"); + } finally { + restoreFetch(); + } +}); + +Deno.test("getFetchCalls - captures POST body", async () => { + mockFetch({ "/notes": { method: "POST", status: 201, body: notes[0] } }); + try { + await fetch("http://localhost/api/notes", { + method: "POST", + body: JSON.stringify(notes[0]), + }); + const calls = getFetchCalls(); + assertEquals(calls.length, 1); + assertEquals(calls[0].method, "POST"); + assertEquals((calls[0].body as { note: number }).note, 15.5); + } finally { + restoreFetch(); + } +}); + +// --- Mock DB --- + +Deno.test("mockDb - getTable returns seeded rows", () => { + const db = createMockDb({ tables: { students: [...students] } }); + assertEquals(db.getTable("students").length, 3); +}); + +Deno.test("mockDb - findOne by key", () => { + const db = createMockDb({ tables: { students: [...students] } }); + const found = db.findOne("students", (r) => r.numEtud === 21212006); + assertExists(found); + assertEquals(found.nom, "Dupont"); +}); + +Deno.test("mockDb - findOne returns undefined for missing", () => { + const db = createMockDb({ tables: { students: [...students] } }); + const found = db.findOne("students", (r) => r.numEtud === 99999); + assertEquals(found, undefined); +}); + +Deno.test("mockDb - insert adds a row", () => { + const db = createMockDb({ tables: { students: [] } }); + const newStudent: Student = { + numEtud: 21212099, + nom: "Test", + prenom: "User", + idPromo: "4AFISE25/26", + }; + db.insert("students", newStudent); + assertEquals(db.getTable("students").length, 1); + assertEquals( + db.findOne("students", (r) => r.numEtud === 21212099)?.nom, + "Test", + ); +}); + +Deno.test("mockDb - updateWhere modifies matching rows", () => { + const db = createMockDb({ tables: { students: [...students] } }); + const updated = db.updateWhere( + "students", + (r) => r.numEtud === 21212006, + { prenom: "Marie" }, + ); + assertEquals(updated, 1); + assertEquals( + db.findOne("students", (r) => r.numEtud === 21212006)?.prenom, + "Marie", + ); +}); + +Deno.test("mockDb - deleteWhere removes matching rows", () => { + const db = createMockDb({ tables: { students: [...students] } }); + const deleted = db.deleteWhere( + "students", + (r) => r.numEtud === 21212006, + ); + assertEquals(deleted, 1); + assertEquals(db.getTable("students").length, 2); +}); + +Deno.test("mockDb - findMany with filter", () => { + const db = createMockDb({ tables: { students: [...students] } }); + const promo4 = db.findMany( + "students", + (r) => r.idPromo === "4AFISE25/26", + ); + assertEquals(promo4.length, 2); +}); + +Deno.test("mockDb - reset clears all tables", () => { + const db = createMockDb({ + tables: { students: [...students], notes: [...notes] }, + }); + db.reset(); + assertEquals(db.getTable("students").length, 0); + assertEquals(db.getTable("notes").length, 0); +}); + +Deno.test("mockDb - isolated between instances", () => { + const db1 = createMockDb({ tables: { students: [...students] } }); + const db2 = createMockDb({ tables: { students: [...students] } }); + db1.deleteWhere("students", () => true); + assertEquals(db1.getTable("students").length, 0); + assertEquals(db2.getTable("students").length, 3); +}); + +// --- happy-dom --- + Deno.test({ name: "happy-dom - document is available after setup", sanitizeResources: false, sanitizeOps: false, fn() { - setupDOM(); + setupDOM(); + try { + const doc = globalThis.document; + assertExists(doc); - try { - const doc = globalThis.document; - assertExists(doc); + const div = doc.createElement("div"); + div.textContent = "hello"; + doc.body.appendChild(div); - const div = doc.createElement("div"); - div.textContent = "hello"; - doc.body.appendChild(div); - - assertEquals(doc.body.textContent, "hello"); - } finally { - cleanupDOM(); - } + assertEquals(doc.body.textContent, "hello"); + } finally { + cleanupDOM(); + } }, });