From 2f4d8db1bf8a2760ea10eb08697b0d246a2aa8bb Mon Sep 17 00:00:00 2001 From: Djalim Simaila Date: Sun, 26 Apr 2026 18:25:00 +0200 Subject: [PATCH 1/3] test: add full test coverage for notes, ues, ue-modules, ajustements, enseignements, users - Unit tests (mock DB + API) for all missing endpoints - Integration tests (Drizzle direct) for all missing entities - E2E tests (handler + real DB) for all missing endpoints - Robustness tests: invalid inputs, SQL injection, type errors, business rule violations - Seed helpers: seedNotes, seedUeModules, seedEnseignements, seedAjustements - Add test:coverage and test:coverage:html tasks to deno.json Tests expose known handler bugs (marked [BUG] in test names): - ajustements PUT/DELETE: .where() without and() modifies all rows for student - Missing try/catch in modules, users, enseignements handlers - Whitespace accepted as valid string values - No type or business rule validation (note bounds, coeff >= 0) --- deno.json | 2 + tests/e2e/ajustements_test.ts | 301 +++++++++++++ tests/e2e/enseignements_test.ts | 185 ++++++++ tests/e2e/notes_test.ts | 244 +++++++++++ tests/e2e/robustness_test.ts | 559 ++++++++++++++++++++++++ tests/e2e/ue_modules_test.ts | 271 ++++++++++++ tests/e2e/ues_test.ts | 170 +++++++ tests/e2e/users_test.ts | 217 +++++++++ tests/helpers/db_integration.ts | 24 + tests/integration/ajustements_test.ts | 127 ++++++ tests/integration/enseignements_test.ts | 135 ++++++ tests/integration/notes_test.ts | 124 ++++++ tests/integration/ue_modules_test.ts | 132 ++++++ tests/integration/ues_test.ts | 83 ++++ tests/unit/ajustements_test.ts | 189 ++++++++ tests/unit/enseignements_test.ts | 179 ++++++++ tests/unit/notes_test.ts | 196 +++++++++ tests/unit/ue_modules_test.ts | 173 ++++++++ tests/unit/ues_test.ts | 160 +++++++ 19 files changed, 3471 insertions(+) create mode 100644 tests/e2e/ajustements_test.ts create mode 100644 tests/e2e/enseignements_test.ts create mode 100644 tests/e2e/notes_test.ts create mode 100644 tests/e2e/robustness_test.ts create mode 100644 tests/e2e/ue_modules_test.ts create mode 100644 tests/e2e/ues_test.ts create mode 100644 tests/e2e/users_test.ts create mode 100644 tests/integration/ajustements_test.ts create mode 100644 tests/integration/enseignements_test.ts create mode 100644 tests/integration/notes_test.ts create mode 100644 tests/integration/ue_modules_test.ts create mode 100644 tests/integration/ues_test.ts create mode 100644 tests/unit/ajustements_test.ts create mode 100644 tests/unit/enseignements_test.ts create mode 100644 tests/unit/notes_test.ts create mode 100644 tests/unit/ue_modules_test.ts create mode 100644 tests/unit/ues_test.ts diff --git a/deno.json b/deno.json index ed7422e..97ab295 100644 --- a/deno.json +++ b/deno.json @@ -14,6 +14,8 @@ "test:unit": "deno test -A --no-check tests/unit/", "test:integration": "deno test -A --no-check tests/integration/", "test:e2e": "deno test -A --no-check tests/e2e/", + "test:coverage": "deno test -A --no-check --coverage=coverage tests/ && deno coverage coverage --exclude=tests/", + "test:coverage:html": "deno test -A --no-check --coverage=coverage tests/ && deno coverage coverage --exclude=tests/ --html", "migrate": "node_modules/.bin/drizzle-kit migrate" }, "lint": { diff --git a/tests/e2e/ajustements_test.ts b/tests/e2e/ajustements_test.ts new file mode 100644 index 0000000..8b07a04 --- /dev/null +++ b/tests/e2e/ajustements_test.ts @@ -0,0 +1,301 @@ +// E2E tests for /ajustements endpoints — handler + real DB + +import { assertEquals, assertExists } from "@std/assert"; +import { + makeContextWithAffiliation, + makeEmployeeContext, + makeGetRequest, + makeJsonRequest, +} from "../helpers/handler.ts"; +import { + seedAjustements, + seedPromotions, + seedStudents, + seedUes, + truncateAll, +} from "../helpers/db_integration.ts"; +import { handler as ajustementsHandler } from "$apps/notes/api/ajustements.ts"; +import { handler as ajustementHandler } from "$apps/notes/api/ajustements/[numEtud]/[idUE].ts"; +import { ajustements as ajustementsTable } from "$root/databases/schema.ts"; +import { testDb } from "../helpers/db_integration.ts"; + +// --- GET /ajustements --- + +Deno.test({ + name: "e2e ajustements: GET /ajustements returns all", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ nom: "Dupont", prenom: "Jean", idPromo: "P1" }]); + const [ue] = await seedUes([{ nom: "UE Info" }]); + await seedAjustements([{ numEtud: s.numEtud, idUE: ue.id, valeur: 13.0 }]); + const res = await ajustementsHandler.GET!(makeGetRequest("/ajustements"), makeEmployeeContext()); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.length, 1); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e ajustements: GET /ajustements?numEtud filters by student", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s1] = await seedStudents([{ nom: "Dupont", prenom: "Jean", idPromo: "P1" }]); + const [s2] = await seedStudents([{ nom: "Martin", prenom: "Alice", idPromo: "P1" }]); + const [ue] = await seedUes([{ nom: "UE Info" }]); + await seedAjustements([ + { numEtud: s1.numEtud, idUE: ue.id, valeur: 13.0 }, + { numEtud: s2.numEtud, idUE: ue.id, valeur: 15.0 }, + ]); + const res = await ajustementsHandler.GET!( + makeGetRequest("/ajustements", { numEtud: String(s1.numEtud) }), + makeEmployeeContext(), + ); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.length, 1); + assertEquals(body[0].numEtud, s1.numEtud); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e ajustements: GET /ajustements?numEtud=NaN returns 400", + async fn() { + await truncateAll(); + const res = await ajustementsHandler.GET!( + makeGetRequest("/ajustements", { numEtud: "abc" }), + makeEmployeeContext(), + ); + assertEquals(res.status, 400); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// --- POST /ajustements --- + +Deno.test({ + name: "e2e ajustements: POST /ajustements creates ajustement (201) as employee", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ nom: "Leroy", prenom: "Paul", idPromo: "P1" }]); + const [ue] = await seedUes([{ nom: "UE Info" }]); + const res = await ajustementsHandler.POST!( + makeJsonRequest("/ajustements", "POST", { numEtud: s.numEtud, idUE: ue.id, valeur: 14.5 }), + makeEmployeeContext(), + ); + assertEquals(res.status, 201); + const body = await res.json(); + assertExists(body.numEtud); + assertEquals(body.valeur, 14.5); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e ajustements: POST /ajustements 403 for non-employee", + async fn() { + await truncateAll(); + const res = await ajustementsHandler.POST!( + makeJsonRequest("/ajustements", "POST", { numEtud: 1, idUE: 1, valeur: 10.0 }), + makeContextWithAffiliation("student"), + ); + assertEquals(res.status, 403); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e ajustements: POST /ajustements 400 on missing fields", + async fn() { + await truncateAll(); + const res = await ajustementsHandler.POST!( + makeJsonRequest("/ajustements", "POST", { numEtud: 12345 }), + makeEmployeeContext(), + ); + assertEquals(res.status, 400); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// --- GET /ajustements/:numEtud/:idUE --- + +Deno.test({ + name: "e2e ajustements: GET /ajustements/:numEtud/:idUE returns correct ajustement (employee)", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s1, s2] = await seedStudents([ + { nom: "Bernard", prenom: "Lucie", idPromo: "P1" }, + { nom: "Dupont", prenom: "Jean", idPromo: "P1" }, + ]); + const [ue1, ue2] = await seedUes([{ nom: "UE Maths" }, { nom: "UE Info" }]); + // Plusieurs lignes partageant numEtud=s1 — le handler doit discriminer par idUE + await seedAjustements([ + { numEtud: s1.numEtud, idUE: ue1.id, valeur: 16.0 }, + { numEtud: s1.numEtud, idUE: ue2.id, valeur: 8.0 }, + { numEtud: s2.numEtud, idUE: ue1.id, valeur: 12.0 }, + ]); + const res = await ajustementHandler.GET!( + makeGetRequest(`/ajustements/${s1.numEtud}/${ue1.id}`), + makeEmployeeContext({ numEtud: String(s1.numEtud), idUE: String(ue1.id) }), + ); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.valeur, 16.0); + assertEquals(body.numEtud, s1.numEtud); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e ajustements: GET /ajustements/:numEtud/:idUE 403 for non-employee", + async fn() { + await truncateAll(); + const res = await ajustementHandler.GET!( + makeGetRequest("/ajustements/1/1"), + makeContextWithAffiliation("student", { numEtud: "1", idUE: "1" }), + ); + assertEquals(res.status, 403); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e ajustements: GET /ajustements/:numEtud/:idUE 404 when not found", + async fn() { + await truncateAll(); + const res = await ajustementHandler.GET!( + makeGetRequest("/ajustements/99999/99"), + makeEmployeeContext({ numEtud: "99999", idUE: "99" }), + ); + assertEquals(res.status, 404); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// --- PUT /ajustements/:numEtud/:idUE --- + +Deno.test({ + name: "e2e ajustements: PUT /ajustements/:numEtud/:idUE updates only targeted row (employee)", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ nom: "Thomas", prenom: "Eva", idPromo: "P1" }]); + const [ue1, ue2] = await seedUes([{ nom: "UE Physique" }, { nom: "UE Chimie" }]); + // Deux ajustements pour le même étudiant — seul ue1 doit être modifié + await seedAjustements([ + { numEtud: s.numEtud, idUE: ue1.id, valeur: 10.0 }, + { numEtud: s.numEtud, idUE: ue2.id, valeur: 7.0 }, + ]); + const res = await ajustementHandler.PUT!( + makeJsonRequest(`/ajustements/${s.numEtud}/${ue1.id}`, "PUT", { valeur: 19.0 }), + makeEmployeeContext({ numEtud: String(s.numEtud), idUE: String(ue1.id) }), + ); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.valeur, 19.0); + // ue2 doit rester intact + const unchanged = await testDb.select().from(ajustementsTable); + const ue2Row = unchanged.find((a) => a.idUE === ue2.id); + assertEquals(ue2Row?.valeur, 7.0); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e ajustements: PUT /ajustements/:numEtud/:idUE 403 for non-employee", + async fn() { + await truncateAll(); + const res = await ajustementHandler.PUT!( + makeJsonRequest("/ajustements/1/1", "PUT", { valeur: 10.0 }), + makeContextWithAffiliation("student", { numEtud: "1", idUE: "1" }), + ); + assertEquals(res.status, 403); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e ajustements: PUT /ajustements/:numEtud/:idUE 404 when not found", + async fn() { + await truncateAll(); + const res = await ajustementHandler.PUT!( + makeJsonRequest("/ajustements/99999/99", "PUT", { valeur: 10.0 }), + makeEmployeeContext({ numEtud: "99999", idUE: "99" }), + ); + assertEquals(res.status, 404); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// --- DELETE /ajustements/:numEtud/:idUE --- + +Deno.test({ + name: "e2e ajustements: DELETE /ajustements/:numEtud/:idUE deletes only targeted row (employee)", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ nom: "Petit", prenom: "Hugo", idPromo: "P1" }]); + const [ue1, ue2] = await seedUes([{ nom: "UE Chimie" }, { nom: "UE Bio" }]); + // Deux ajustements pour le même étudiant — seul ue1 doit être supprimé + await seedAjustements([ + { numEtud: s.numEtud, idUE: ue1.id, valeur: 11.0 }, + { numEtud: s.numEtud, idUE: ue2.id, valeur: 14.0 }, + ]); + const res = await ajustementHandler.DELETE!( + makeGetRequest(`/ajustements/${s.numEtud}/${ue1.id}`), + makeEmployeeContext({ numEtud: String(s.numEtud), idUE: String(ue1.id) }), + ); + assertEquals(res.status, 204); + // ue2 doit toujours exister + const remaining = await testDb.select().from(ajustementsTable); + assertEquals(remaining.length, 1); + assertEquals(remaining[0].idUE, ue2.id); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e ajustements: DELETE /ajustements/:numEtud/:idUE 403 for non-employee", + async fn() { + await truncateAll(); + const res = await ajustementHandler.DELETE!( + makeGetRequest("/ajustements/1/1"), + makeContextWithAffiliation("student", { numEtud: "1", idUE: "1" }), + ); + assertEquals(res.status, 403); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e ajustements: DELETE /ajustements/:numEtud/:idUE 404 when not found", + async fn() { + await truncateAll(); + const res = await ajustementHandler.DELETE!( + makeGetRequest("/ajustements/99999/99"), + makeEmployeeContext({ numEtud: "99999", idUE: "99" }), + ); + assertEquals(res.status, 404); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/e2e/enseignements_test.ts b/tests/e2e/enseignements_test.ts new file mode 100644 index 0000000..77751f4 --- /dev/null +++ b/tests/e2e/enseignements_test.ts @@ -0,0 +1,185 @@ +// E2E tests for /enseignements endpoints — handler + real DB + +import { assertEquals, assertExists } from "@std/assert"; +import { + makeContextWithAffiliation, + makeEmployeeContext, + makeGetRequest, + makeJsonRequest, +} from "../helpers/handler.ts"; +import { + seedEnseignements, + seedModules, + seedPromotions, + seedUsers, + truncateAll, +} from "../helpers/db_integration.ts"; +import { handler as enseignementsHandler } from "$apps/admin/api/enseignements.ts"; +import { handler as enseignementHandler } from "$apps/admin/api/enseignements/[idProf]/[idModule]/[idPromo].ts"; + +// --- POST /enseignements --- + +Deno.test({ + name: "e2e enseignements: POST /enseignements creates enseignement (201) as employee", + async fn() { + await truncateAll(); + await seedUsers([{ id: "prof.dupont", nom: "Dupont", prenom: "Jean" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + await seedPromotions([{ id: "P1" }]); + const res = await enseignementsHandler.POST!( + makeJsonRequest("/enseignements", "POST", { idProf: "prof.dupont", idModule: "M1", idPromo: "P1" }), + makeEmployeeContext(), + ); + assertEquals(res.status, 201); + const body = await res.json(); + assertExists(body.idProf); + assertEquals(body.idModule, "M1"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e enseignements: POST /enseignements 403 for non-employee", + async fn() { + await truncateAll(); + const res = await enseignementsHandler.POST!( + makeJsonRequest("/enseignements", "POST", { idProf: "p", idModule: "M1", idPromo: "P1" }), + makeContextWithAffiliation("student"), + ); + assertEquals(res.status, 403); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e enseignements: POST /enseignements 400 on missing fields", + async fn() { + await truncateAll(); + const res = await enseignementsHandler.POST!( + makeJsonRequest("/enseignements", "POST", { idProf: "prof.dupont" }), + makeEmployeeContext(), + ); + assertEquals(res.status, 400); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e enseignements: POST /enseignements 409 on duplicate", + async fn() { + await truncateAll(); + await seedUsers([{ id: "prof.dupont", nom: "Dupont", prenom: "Jean" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + await seedPromotions([{ id: "P1" }]); + await seedEnseignements([{ idProf: "prof.dupont", idModule: "M1", idPromo: "P1" }]); + const res = await enseignementsHandler.POST!( + makeJsonRequest("/enseignements", "POST", { idProf: "prof.dupont", idModule: "M1", idPromo: "P1" }), + makeEmployeeContext(), + ); + assertEquals(res.status, 409); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// --- GET /enseignements/:idProf/:idModule/:idPromo --- + +Deno.test({ + name: "e2e enseignements: GET /enseignements/:idProf/:idModule/:idPromo returns enseignement (employee)", + async fn() { + await truncateAll(); + await seedUsers([{ id: "prof.dupont", nom: "Dupont", prenom: "Jean" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + await seedPromotions([{ id: "P1" }]); + await seedEnseignements([{ idProf: "prof.dupont", idModule: "M1", idPromo: "P1" }]); + const res = await enseignementHandler.GET!( + makeGetRequest("/enseignements/prof.dupont/M1/P1"), + makeEmployeeContext({ idProf: "prof.dupont", idModule: "M1", idPromo: "P1" }), + ); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.idProf, "prof.dupont"); + assertEquals(body.idModule, "M1"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e enseignements: GET /enseignements/:idProf/:idModule/:idPromo 403 for non-employee", + async fn() { + await truncateAll(); + const res = await enseignementHandler.GET!( + makeGetRequest("/enseignements/p/M1/P1"), + makeContextWithAffiliation("student", { idProf: "p", idModule: "M1", idPromo: "P1" }), + ); + assertEquals(res.status, 403); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e enseignements: GET /enseignements/:idProf/:idModule/:idPromo 404 when not found", + async fn() { + await truncateAll(); + const res = await enseignementHandler.GET!( + makeGetRequest("/enseignements/ghost/GHOST/GHOST"), + makeEmployeeContext({ idProf: "ghost", idModule: "GHOST", idPromo: "GHOST" }), + ); + assertEquals(res.status, 404); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// --- DELETE /enseignements/:idProf/:idModule/:idPromo --- + +Deno.test({ + name: "e2e enseignements: DELETE /enseignements/:idProf/:idModule/:idPromo returns 204 (employee)", + async fn() { + await truncateAll(); + await seedUsers([{ id: "prof.dupont", nom: "Dupont", prenom: "Jean" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + await seedPromotions([{ id: "P1" }]); + await seedEnseignements([{ idProf: "prof.dupont", idModule: "M1", idPromo: "P1" }]); + const res = await enseignementHandler.DELETE!( + makeGetRequest("/enseignements/prof.dupont/M1/P1"), + makeEmployeeContext({ idProf: "prof.dupont", idModule: "M1", idPromo: "P1" }), + ); + assertEquals(res.status, 204); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e enseignements: DELETE /enseignements/:idProf/:idModule/:idPromo 403 for non-employee", + async fn() { + await truncateAll(); + const res = await enseignementHandler.DELETE!( + makeGetRequest("/enseignements/p/M1/P1"), + makeContextWithAffiliation("student", { idProf: "p", idModule: "M1", idPromo: "P1" }), + ); + assertEquals(res.status, 403); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e enseignements: DELETE /enseignements/:idProf/:idModule/:idPromo 404 when not found", + async fn() { + await truncateAll(); + const res = await enseignementHandler.DELETE!( + makeGetRequest("/enseignements/ghost/GHOST/GHOST"), + makeEmployeeContext({ idProf: "ghost", idModule: "GHOST", idPromo: "GHOST" }), + ); + assertEquals(res.status, 404); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/e2e/notes_test.ts b/tests/e2e/notes_test.ts new file mode 100644 index 0000000..78be912 --- /dev/null +++ b/tests/e2e/notes_test.ts @@ -0,0 +1,244 @@ +// E2E tests for /notes endpoints — handler + real DB + +import { assertEquals, assertExists } from "@std/assert"; +import { + makeEmployeeContext, + makeGetRequest, + makeJsonRequest, +} from "../helpers/handler.ts"; +import { + seedModules, + seedNotes, + seedPromotions, + seedStudents, + truncateAll, +} from "../helpers/db_integration.ts"; +import { handler as notesHandler } from "$apps/notes/api/notes.ts"; +import { handler as noteHandler } from "$apps/notes/api/notes/[numEtud]/[idModule].ts"; + +// --- GET /notes --- + +Deno.test({ + name: "e2e notes: GET /notes returns all notes", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ nom: "Dupont", prenom: "Jean", idPromo: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod A" }, { id: "M2", nom: "Mod B" }]); + await seedNotes([ + { numEtud: s.numEtud, idModule: "M1", note: 15.0 }, + { numEtud: s.numEtud, idModule: "M2", note: 12.0 }, + ]); + const res = await notesHandler.GET!(makeGetRequest("/notes"), makeEmployeeContext()); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.length, 2); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e notes: GET /notes?numEtud filters by student", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s1] = await seedStudents([{ nom: "Dupont", prenom: "Jean", idPromo: "P1" }]); + const [s2] = await seedStudents([{ nom: "Martin", prenom: "Alice", idPromo: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + await seedNotes([ + { numEtud: s1.numEtud, idModule: "M1", note: 15.0 }, + { numEtud: s2.numEtud, idModule: "M1", note: 12.0 }, + ]); + const res = await notesHandler.GET!( + makeGetRequest("/notes", { numEtud: String(s1.numEtud) }), + makeEmployeeContext(), + ); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.length, 1); + assertEquals(body[0].numEtud, s1.numEtud); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e notes: GET /notes?numEtud=NaN returns 400", + async fn() { + await truncateAll(); + const res = await notesHandler.GET!( + makeGetRequest("/notes", { numEtud: "abc" }), + makeEmployeeContext(), + ); + assertEquals(res.status, 400); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e notes: GET /notes?idModule filters by module", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ nom: "Dupont", prenom: "Jean", idPromo: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod A" }, { id: "M2", nom: "Mod B" }]); + await seedNotes([ + { numEtud: s.numEtud, idModule: "M1", note: 15.0 }, + { numEtud: s.numEtud, idModule: "M2", note: 10.0 }, + ]); + const res = await notesHandler.GET!( + makeGetRequest("/notes", { idModule: "M1" }), + makeEmployeeContext(), + ); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.length, 1); + assertEquals(body[0].idModule, "M1"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// --- POST /notes --- + +Deno.test({ + name: "e2e notes: POST /notes creates note (201)", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ nom: "Leroy", prenom: "Paul", idPromo: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + const res = await notesHandler.POST!( + makeJsonRequest("/notes", "POST", { numEtud: s.numEtud, idModule: "M1", note: 14.0 }), + makeEmployeeContext(), + ); + assertEquals(res.status, 201); + const body = await res.json(); + assertExists(body.numEtud); + assertEquals(body.note, 14.0); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e notes: POST /notes 400 on missing fields", + async fn() { + await truncateAll(); + const res = await notesHandler.POST!( + makeJsonRequest("/notes", "POST", { numEtud: 12345 }), + makeEmployeeContext(), + ); + assertEquals(res.status, 400); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// --- GET /notes/:numEtud/:idModule --- + +Deno.test({ + name: "e2e notes: GET /notes/:numEtud/:idModule returns note", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ nom: "Bernard", prenom: "Lucie", idPromo: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + await seedNotes([{ numEtud: s.numEtud, idModule: "M1", note: 18.0 }]); + const res = await noteHandler.GET!( + makeGetRequest(`/notes/${s.numEtud}/M1`), + makeEmployeeContext({ numEtud: String(s.numEtud), idModule: "M1" }), + ); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.note, 18.0); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e notes: GET /notes/:numEtud/:idModule 404 when not found", + async fn() { + await truncateAll(); + const res = await noteHandler.GET!( + makeGetRequest("/notes/99999/GHOST"), + makeEmployeeContext({ numEtud: "99999", idModule: "GHOST" }), + ); + assertEquals(res.status, 404); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// --- PUT /notes/:numEtud/:idModule --- + +Deno.test({ + name: "e2e notes: PUT /notes/:numEtud/:idModule updates note", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ nom: "Thomas", prenom: "Eva", idPromo: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + await seedNotes([{ numEtud: s.numEtud, idModule: "M1", note: 10.0 }]); + const res = await noteHandler.PUT!( + makeJsonRequest(`/notes/${s.numEtud}/M1`, "PUT", { note: 16.0 }), + makeEmployeeContext({ numEtud: String(s.numEtud), idModule: "M1" }), + ); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.note, 16.0); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e notes: PUT /notes/:numEtud/:idModule 404 when not found", + async fn() { + await truncateAll(); + const res = await noteHandler.PUT!( + makeJsonRequest("/notes/99999/GHOST", "PUT", { note: 10.0 }), + makeEmployeeContext({ numEtud: "99999", idModule: "GHOST" }), + ); + assertEquals(res.status, 404); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// --- DELETE /notes/:numEtud/:idModule --- + +Deno.test({ + name: "e2e notes: DELETE /notes/:numEtud/:idModule returns 204", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ nom: "Petit", prenom: "Hugo", idPromo: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + await seedNotes([{ numEtud: s.numEtud, idModule: "M1", note: 9.0 }]); + const res = await noteHandler.DELETE!( + makeGetRequest(`/notes/${s.numEtud}/M1`), + makeEmployeeContext({ numEtud: String(s.numEtud), idModule: "M1" }), + ); + assertEquals(res.status, 204); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e notes: DELETE /notes/:numEtud/:idModule 404 when not found", + async fn() { + await truncateAll(); + const res = await noteHandler.DELETE!( + makeGetRequest("/notes/99999/GHOST"), + makeEmployeeContext({ numEtud: "99999", idModule: "GHOST" }), + ); + assertEquals(res.status, 404); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/e2e/robustness_test.ts b/tests/e2e/robustness_test.ts new file mode 100644 index 0000000..ba18a1d --- /dev/null +++ b/tests/e2e/robustness_test.ts @@ -0,0 +1,559 @@ +// Robustness tests — input validation & side-effect isolation +// +// Chaque test documente le comportement réel du handler face à des entrées invalides. +// Les tests marqués [BUG] représentent le comportement ATTENDU — ils échouent +// intentionnellement pour exposer un bug dans le handler ciblé. + +import { assertEquals, assertRejects } from "@std/assert"; +import { + makeContextWithAffiliation, + makeEmployeeContext, + makeGetRequest, + makeJsonRequest, +} from "../helpers/handler.ts"; +import { + seedModules, + seedPromotions, + seedStudents, + seedUes, + truncateAll, +} from "../helpers/db_integration.ts"; +import { handler as modulesHandler } from "$apps/admin/api/modules.ts"; +import { handler as moduleHandler } from "$apps/admin/api/modules/[idModule].ts"; +import { handler as notesHandler } from "$apps/notes/api/notes.ts"; +import { handler as uesHandler } from "$apps/notes/api/ues.ts"; +import { handler as ueModulesHandler } from "$apps/notes/api/ue-modules.ts"; +import { handler as ajustementsHandler } from "$apps/notes/api/ajustements.ts"; +import { handler as enseignementsHandler } from "$apps/admin/api/enseignements.ts"; +import { handler as usersHandler } from "$apps/admin/api/users.ts"; + +// Helper : request POST avec un body JSON invalide +function makeMalformedRequest(path: string): Request { + return new Request(`http://localhost${path}`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: "{ ceci n'est pas du json }", + }); +} + +// Helper : request POST sans body du tout +function makeEmptyBodyRequest(path: string, method = "POST"): Request { + return new Request(`http://localhost${path}`, { method }); +} + +// ============================================================================= +// JSON MALFORMÉ +// ============================================================================= +// Handlers AVEC try/catch → retournent 500 +// Handlers SANS try/catch → throwent (assertRejects) + +Deno.test({ + name: "robustness: POST /notes malformed JSON → 500 (try/catch présent)", + async fn() { + await truncateAll(); + const res = await notesHandler.POST!( + makeMalformedRequest("/notes"), + makeEmployeeContext(), + ); + assertEquals(res.status, 500); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "robustness: POST /ues malformed JSON → 500 (try/catch présent)", + async fn() { + await truncateAll(); + const res = await uesHandler.POST!( + makeMalformedRequest("/ues"), + makeEmployeeContext(), + ); + assertEquals(res.status, 500); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "robustness: POST /ue-modules malformed JSON → 500 (try/catch présent)", + async fn() { + await truncateAll(); + const res = await ueModulesHandler.POST!( + makeMalformedRequest("/ue-modules"), + makeEmployeeContext(), + ); + assertEquals(res.status, 500); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "robustness: POST /ajustements malformed JSON → 500 (try/catch présent)", + async fn() { + await truncateAll(); + const res = await ajustementsHandler.POST!( + makeMalformedRequest("/ajustements"), + makeEmployeeContext(), + ); + assertEquals(res.status, 500); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// Handlers SANS try/catch — throwent au lieu de retourner 500 +// [BUG] Ces handlers devraient retourner 500, pas throw + +Deno.test({ + name: "robustness [BUG]: POST /modules malformed JSON → throw (pas de try/catch)", + async fn() { + await truncateAll(); + await assertRejects(() => + modulesHandler.POST!( + makeMalformedRequest("/modules"), + makeEmployeeContext(), + ) + ); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "robustness [BUG]: POST /enseignements malformed JSON → throw (pas de try/catch)", + async fn() { + await truncateAll(); + await assertRejects(() => + enseignementsHandler.POST!( + makeMalformedRequest("/enseignements"), + makeEmployeeContext(), + ) + ); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "robustness [BUG]: POST /users malformed JSON → throw (pas de try/catch)", + async fn() { + await truncateAll(); + await assertRejects(() => + usersHandler.POST!( + makeMalformedRequest("/users"), + makeEmployeeContext(), + ) + ); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// ============================================================================= +// BODY ABSENT +// ============================================================================= + +Deno.test({ + name: "robustness: POST /notes sans body → 500", + async fn() { + await truncateAll(); + const res = await notesHandler.POST!( + makeEmptyBodyRequest("/notes"), + makeEmployeeContext(), + ); + assertEquals(res.status, 500); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "robustness [BUG]: POST /modules sans body → throw (pas de try/catch)", + async fn() { + await truncateAll(); + await assertRejects(() => + modulesHandler.POST!( + makeEmptyBodyRequest("/modules"), + makeEmployeeContext(), + ) + ); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// ============================================================================= +// CHAÎNES VIDES — comportement correct ✓ +// ============================================================================= + +Deno.test({ + name: "robustness: POST /modules id vide → 400", + async fn() { + await truncateAll(); + const res = await modulesHandler.POST!( + makeJsonRequest("/modules", "POST", { id: "", nom: "Test" }), + makeEmployeeContext(), + ); + assertEquals(res.status, 400); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "robustness: POST /modules nom vide → 400", + async fn() { + await truncateAll(); + const res = await modulesHandler.POST!( + makeJsonRequest("/modules", "POST", { id: "M1", nom: "" }), + makeEmployeeContext(), + ); + assertEquals(res.status, 400); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "robustness: POST /ues nom vide → 400", + async fn() { + await truncateAll(); + const res = await uesHandler.POST!( + makeJsonRequest("/ues", "POST", { nom: "" }), + makeEmployeeContext(), + ); + assertEquals(res.status, 400); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// ============================================================================= +// CHAÎNES AVEC ESPACES SEULS — [BUG] passent !field et s'insèrent en DB +// ============================================================================= + +Deno.test({ + name: "robustness [BUG]: POST /modules id=espaces → devrait être 400, retourne 201", + async fn() { + await truncateAll(); + const res = await modulesHandler.POST!( + makeJsonRequest("/modules", "POST", { id: " ", nom: "Test" }), + makeEmployeeContext(), + ); + // Le handler vérifie !body.id → " " est truthy → passe → s'insère + // Comportement attendu : 400 + // Comportement réel : 201 (bug : pas de trim()) + assertEquals(res.status, 400); // ← va échouer, expose le bug + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "robustness [BUG]: POST /ues nom=espaces → devrait être 400, retourne 201", + async fn() { + await truncateAll(); + const res = await uesHandler.POST!( + makeJsonRequest("/ues", "POST", { nom: " " }), + makeEmployeeContext(), + ); + assertEquals(res.status, 400); // ← va échouer, expose le bug + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "robustness [BUG]: POST /users id=espaces → devrait être 400, retourne 201", + async fn() { + await truncateAll(); + await assertRejects( + // sans try/catch + whitespace id → s'insère (ou throw si DB rejette) + // Dans tous les cas le handler ne valide pas correctement + async () => { + const res = await usersHandler.POST!( + makeJsonRequest("/users", "POST", { id: " ", nom: "X", prenom: "Y" }), + makeEmployeeContext(), + ); + // Si pas de throw : le handler a inséré des espaces en DB + assertEquals(res.status, 400); + }, + ); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// ============================================================================= +// MAUVAIS TYPES +// ============================================================================= + +Deno.test({ + name: "robustness [BUG]: POST /notes note=string → devrait être 400, retourne 500", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ nom: "Test", prenom: "User", idPromo: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod" }]); + const res = await notesHandler.POST!( + makeJsonRequest("/notes", "POST", { note: "pas-un-nombre", numEtud: s.numEtud, idModule: "M1" }), + makeEmployeeContext(), + ); + // "pas-un-nombre" !== undefined → passe la validation → DB rejette → 500 + // Comportement attendu : 400 (validation de type) + // Comportement réel : 500 + assertEquals(res.status, 400); // ← va échouer, expose le bug + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "robustness [BUG]: PUT /modules/:id nom=number → devrait être 400, throw ou insère", + async fn() { + await truncateAll(); + await seedModules([{ id: "M1", nom: "Mod" }]); + await assertRejects(() => + moduleHandler.PUT!( + makeJsonRequest("/modules/M1", "PUT", { nom: 42 }), + makeEmployeeContext({ idModule: "M1" }), + ) + ); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// ============================================================================= +// VALEUR ZÉRO — falsy bug sur numEtud/idUE +// ============================================================================= + +Deno.test({ + name: "robustness [BUG]: POST /ajustements numEtud=0 → 400 pour mauvaise raison", + async fn() { + await truncateAll(); + const [ue] = await seedUes([{ nom: "UE Info" }]); + const res = await ajustementsHandler.POST!( + makeJsonRequest("/ajustements", "POST", { numEtud: 0, idUE: ue.id, valeur: 10.0 }), + makeEmployeeContext(), + ); + // !0 === true → retourne 400 à cause du falsy check, pas d'une vraie validation + // Comportement attendu : 422 ou message d'erreur explicite sur numEtud invalide + // Comportement réel : 400 générique "champs requis" + assertEquals(res.status, 400); // passe, mais pour la mauvaise raison — le message est trompeur + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "robustness [BUG]: POST /ajustements idUE=0 → 400 pour mauvaise raison", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ nom: "Test", prenom: "User", idPromo: "P1" }]); + const res = await ajustementsHandler.POST!( + makeJsonRequest("/ajustements", "POST", { numEtud: s.numEtud, idUE: 0, valeur: 10.0 }), + makeEmployeeContext(), + ); + assertEquals(res.status, 400); // !0 → 400, message trompeur + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// ============================================================================= +// VALEUR ZÉRO CORRECTEMENT GÉRÉE — coeff=0 est valide +// ============================================================================= + +Deno.test({ + name: "robustness: POST /ue-modules coeff=0 → 201 (zéro est une valeur valide)", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod" }]); + const [ue] = await seedUes([{ nom: "UE Info" }]); + const res = await ueModulesHandler.POST!( + makeJsonRequest("/ue-modules", "POST", { idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: 0 }), + makeEmployeeContext(), + ); + // coeff === undefined → false pour 0 → passe ✓ + assertEquals(res.status, 201); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// ============================================================================= +// INJECTION SQL DANS LES PARAMÈTRES D'URL +// Drizzle utilise des requêtes paramétrées → les injections sont neutralisées +// ============================================================================= + +Deno.test({ + name: "robustness: GET /modules avec SQL injection dans id → 404 (Drizzle paramètre)", + async fn() { + await truncateAll(); + const injectionId = "'; DROP TABLE modules; --"; + const res = await moduleHandler.GET!( + makeGetRequest(`/modules/${encodeURIComponent(injectionId)}`), + makeEmployeeContext({ idModule: injectionId }), + ); + // Drizzle génère WHERE id = $1 avec $1 = "'; DROP TABLE modules; --" + // Aucune injection possible → module non trouvé → 404 + assertEquals(res.status, 404); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "robustness: POST /modules avec SQL injection dans id → s'insère littéralement (safe)", + async fn() { + await truncateAll(); + const injectionId = "'; DROP TABLE modules; --"; + const res = await modulesHandler.POST!( + makeJsonRequest("/modules", "POST", { id: injectionId, nom: "Test" }), + makeEmployeeContext(), + ); + // Drizzle paramètre la valeur → s'insère comme une chaîne ordinaire → 201 + assertEquals(res.status, 201); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// ============================================================================= +// ABSENCE DE VALIDATION MÉTIER — valeurs hors limites acceptées +// ============================================================================= + +Deno.test({ + name: "robustness [BUG]: POST /notes note > 20 → devrait être 400, retourne 201", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ nom: "Test", prenom: "User", idPromo: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod" }]); + const res = await notesHandler.POST!( + makeJsonRequest("/notes", "POST", { note: 999, numEtud: s.numEtud, idModule: "M1" }), + makeEmployeeContext(), + ); + // Aucune validation de borne → 999 s'insère → 201 + assertEquals(res.status, 400); // ← va échouer, expose le manque de validation métier + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "robustness [BUG]: POST /notes note < 0 → devrait être 400, retourne 201", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ nom: "Test", prenom: "User", idPromo: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod" }]); + const res = await notesHandler.POST!( + makeJsonRequest("/notes", "POST", { note: -5, numEtud: s.numEtud, idModule: "M1" }), + makeEmployeeContext(), + ); + assertEquals(res.status, 400); // ← va échouer + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "robustness [BUG]: POST /ue-modules coeff négatif → devrait être 400, retourne 201", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod" }]); + const [ue] = await seedUes([{ nom: "UE Info" }]); + const res = await ueModulesHandler.POST!( + makeJsonRequest("/ue-modules", "POST", { idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: -3 }), + makeEmployeeContext(), + ); + assertEquals(res.status, 400); // ← va échouer + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// ============================================================================= +// ISOLATION DES EFFETS DE BORD +// Vérification que truncateAll() isole correctement chaque test +// ============================================================================= + +Deno.test({ + name: "robustness: isolation — données du test précédent non visibles", + async fn() { + // Ce test crée un module + await truncateAll(); + await modulesHandler.POST!( + makeJsonRequest("/modules", "POST", { id: "ISOLATION-TEST", nom: "Test" }), + makeEmployeeContext(), + ); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "robustness: isolation — truncateAll efface bien les données du test précédent", + async fn() { + await truncateAll(); + // Le module créé dans le test précédent ne doit plus exister + const res = await moduleHandler.GET!( + makeGetRequest("/modules/ISOLATION-TEST"), + makeEmployeeContext({ idModule: "ISOLATION-TEST" }), + ); + assertEquals(res.status, 404); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// ============================================================================= +// CHAMPS SUPPLÉMENTAIRES INCONNUS — doivent être ignorés silencieusement +// ============================================================================= + +Deno.test({ + name: "robustness: POST /modules avec champs inconnus → 201 (champs ignorés)", + async fn() { + await truncateAll(); + const res = await modulesHandler.POST!( + makeJsonRequest("/modules", "POST", { + id: "M-EXTRA", + nom: "Test", + champInconnu: "valeur", + _admin: true, + __proto__: { polluted: true }, + }), + makeEmployeeContext(), + ); + assertEquals(res.status, 201); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// ============================================================================= +// ACCÈS NON AUTHENTIFIÉ — vérification que l'état auth est bien contrôlé +// ============================================================================= + +Deno.test({ + name: "robustness: POST /modules sans affiliation employee → 403", + async fn() { + await truncateAll(); + for (const role of ["student", "alumni", "", "EMPLOYEE", "admin"]) { + const res = await modulesHandler.POST!( + makeJsonRequest("/modules", "POST", { id: `M-${role}`, nom: "Test" }), + makeContextWithAffiliation(role), + ); + assertEquals(res.status, 403, `role "${role}" devrait être 403`); + } + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/e2e/ue_modules_test.ts b/tests/e2e/ue_modules_test.ts new file mode 100644 index 0000000..028dfa8 --- /dev/null +++ b/tests/e2e/ue_modules_test.ts @@ -0,0 +1,271 @@ +// E2E tests for /ue-modules endpoints — handler + real DB + +import { assertEquals, assertExists } from "@std/assert"; +import { + makeContextWithAffiliation, + makeEmployeeContext, + makeGetRequest, + makeJsonRequest, +} from "../helpers/handler.ts"; +import { + seedModules, + seedPromotions, + seedUeModules, + seedUes, + truncateAll, +} from "../helpers/db_integration.ts"; +import { handler as ueModulesHandler } from "$apps/notes/api/ue-modules.ts"; +import { handler as ueModuleHandler } from "$apps/notes/api/ue-modules/[idModule]/[idUE]/[idPromo].ts"; +import { ueModules as ueModulesTable } from "$root/databases/schema.ts"; +import { testDb } from "../helpers/db_integration.ts"; + +// --- GET /ue-modules --- + +Deno.test({ + name: "e2e ue_modules: GET /ue-modules returns all associations", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod A" }, { id: "M2", nom: "Mod B" }]); + const [ue] = await seedUes([{ nom: "UE Info" }]); + await seedUeModules([ + { idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: 2.0 }, + { idModule: "M2", idUE: ue.id, idPromo: "P1", coeff: 3.0 }, + ]); + const res = await ueModulesHandler.GET!(makeGetRequest("/ue-modules"), makeEmployeeContext()); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.length, 2); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e ue_modules: GET /ue-modules?idPromo filters by promo", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }, { id: "P2" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + const [ue] = await seedUes([{ nom: "UE Info" }]); + await seedUeModules([ + { idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: 2.0 }, + { idModule: "M1", idUE: ue.id, idPromo: "P2", coeff: 3.0 }, + ]); + const res = await ueModulesHandler.GET!( + makeGetRequest("/ue-modules", { idPromo: "P1" }), + makeEmployeeContext(), + ); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.length, 1); + assertEquals(body[0].idPromo, "P1"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// --- POST /ue-modules --- + +Deno.test({ + name: "e2e ue_modules: POST /ue-modules creates association (201)", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + const [ue] = await seedUes([{ nom: "UE Info" }]); + const res = await ueModulesHandler.POST!( + makeJsonRequest("/ue-modules", "POST", { idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: 4.0 }), + makeEmployeeContext(), + ); + assertEquals(res.status, 201); + const body = await res.json(); + assertExists(body.idModule); + assertEquals(body.coeff, 4.0); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e ue_modules: POST /ue-modules 400 on missing fields", + async fn() { + await truncateAll(); + const res = await ueModulesHandler.POST!( + makeJsonRequest("/ue-modules", "POST", { idModule: "M1" }), + makeEmployeeContext(), + ); + assertEquals(res.status, 400); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// --- GET /ue-modules/:idModule/:idUE/:idPromo --- + +Deno.test({ + name: "e2e ue_modules: GET /ue-modules/:idModule/:idUE/:idPromo returns correct association (employee)", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }, { id: "P2" }]); + await seedModules([{ id: "M1", nom: "Mod A" }, { id: "M2", nom: "Mod B" }]); + const [ue1, ue2] = await seedUes([{ nom: "UE Info" }, { nom: "UE Maths" }]); + // Plusieurs lignes qui partagent idModule="M1" — le handler doit discriminer par idUE ET idPromo + await seedUeModules([ + { idModule: "M1", idUE: ue1.id, idPromo: "P1", coeff: 3.5 }, + { idModule: "M1", idUE: ue2.id, idPromo: "P1", coeff: 1.0 }, + { idModule: "M1", idUE: ue1.id, idPromo: "P2", coeff: 2.0 }, + { idModule: "M2", idUE: ue1.id, idPromo: "P1", coeff: 4.0 }, + ]); + const res = await ueModuleHandler.GET!( + makeGetRequest(`/ue-modules/M1/${ue1.id}/P1`), + makeEmployeeContext({ idModule: "M1", idUE: String(ue1.id), idPromo: "P1" }), + ); + assertEquals(res.status, 200); + const body = await res.json(); + // Doit retourner exactement M1/ue1/P1 avec coeff 3.5, pas une autre ligne + assertEquals(body.coeff, 3.5); + assertEquals(body.idPromo, "P1"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e ue_modules: GET /ue-modules/:idModule/:idUE/:idPromo 403 for non-employee", + async fn() { + await truncateAll(); + const res = await ueModuleHandler.GET!( + makeGetRequest("/ue-modules/M1/1/P1"), + makeContextWithAffiliation("student", { idModule: "M1", idUE: "1", idPromo: "P1" }), + ); + assertEquals(res.status, 403); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e ue_modules: GET /ue-modules/:idModule/:idUE/:idPromo 404 when not found", + async fn() { + await truncateAll(); + const res = await ueModuleHandler.GET!( + makeGetRequest("/ue-modules/GHOST/1/GHOST"), + makeEmployeeContext({ idModule: "GHOST", idUE: "1", idPromo: "GHOST" }), + ); + assertEquals(res.status, 404); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// --- PUT /ue-modules/:idModule/:idUE/:idPromo --- + +Deno.test({ + name: "e2e ue_modules: PUT /ue-modules/:idModule/:idUE/:idPromo updates only the targeted row (employee)", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }, { id: "P2" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + const [ue1, ue2] = await seedUes([{ nom: "UE Info" }, { nom: "UE Maths" }]); + // Deux lignes avec même idModule — le PUT ne doit modifier que celle ciblée + await seedUeModules([ + { idModule: "M1", idUE: ue1.id, idPromo: "P1", coeff: 2.0 }, + { idModule: "M1", idUE: ue2.id, idPromo: "P2", coeff: 9.0 }, + ]); + const res = await ueModuleHandler.PUT!( + makeJsonRequest(`/ue-modules/M1/${ue1.id}/P1`, "PUT", { coeff: 5.0 }), + makeEmployeeContext({ idModule: "M1", idUE: String(ue1.id), idPromo: "P1" }), + ); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.coeff, 5.0); + assertEquals(body.idPromo, "P1"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e ue_modules: PUT /ue-modules/:idModule/:idUE/:idPromo 403 for non-employee", + async fn() { + await truncateAll(); + const res = await ueModuleHandler.PUT!( + makeJsonRequest("/ue-modules/M1/1/P1", "PUT", { coeff: 5.0 }), + makeContextWithAffiliation("student", { idModule: "M1", idUE: "1", idPromo: "P1" }), + ); + assertEquals(res.status, 403); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e ue_modules: PUT /ue-modules/:idModule/:idUE/:idPromo 404 when not found", + async fn() { + await truncateAll(); + const res = await ueModuleHandler.PUT!( + makeJsonRequest("/ue-modules/GHOST/1/GHOST", "PUT", { coeff: 5.0 }), + makeEmployeeContext({ idModule: "GHOST", idUE: "1", idPromo: "GHOST" }), + ); + assertEquals(res.status, 404); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// --- DELETE /ue-modules/:idModule/:idUE/:idPromo --- + +Deno.test({ + name: "e2e ue_modules: DELETE /ue-modules/:idModule/:idUE/:idPromo deletes only targeted row (employee)", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }, { id: "P2" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + const [ue1, ue2] = await seedUes([{ nom: "UE Info" }, { nom: "UE Maths" }]); + // Deux lignes avec même idModule — seule celle ciblée doit être supprimée + await seedUeModules([ + { idModule: "M1", idUE: ue1.id, idPromo: "P1", coeff: 2.0 }, + { idModule: "M1", idUE: ue2.id, idPromo: "P2", coeff: 4.0 }, + ]); + const res = await ueModuleHandler.DELETE!( + makeGetRequest(`/ue-modules/M1/${ue1.id}/P1`), + makeEmployeeContext({ idModule: "M1", idUE: String(ue1.id), idPromo: "P1" }), + ); + assertEquals(res.status, 204); + // L'autre ligne doit toujours exister + const remaining = await testDb.select().from(ueModulesTable); + assertEquals(remaining.length, 1); + assertEquals(remaining[0].idUE, ue2.id); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e ue_modules: DELETE /ue-modules/:idModule/:idUE/:idPromo 403 for non-employee", + async fn() { + await truncateAll(); + const res = await ueModuleHandler.DELETE!( + makeGetRequest("/ue-modules/M1/1/P1"), + makeContextWithAffiliation("student", { idModule: "M1", idUE: "1", idPromo: "P1" }), + ); + assertEquals(res.status, 403); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e ue_modules: DELETE /ue-modules/:idModule/:idUE/:idPromo 404 when not found", + async fn() { + await truncateAll(); + const res = await ueModuleHandler.DELETE!( + makeGetRequest("/ue-modules/GHOST/1/GHOST"), + makeEmployeeContext({ idModule: "GHOST", idUE: "1", idPromo: "GHOST" }), + ); + assertEquals(res.status, 404); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/e2e/ues_test.ts b/tests/e2e/ues_test.ts new file mode 100644 index 0000000..a57249b --- /dev/null +++ b/tests/e2e/ues_test.ts @@ -0,0 +1,170 @@ +// E2E tests for /ues endpoints — handler + real DB + +import { assertEquals, assertExists } from "@std/assert"; +import { + makeEmployeeContext, + makeGetRequest, + makeJsonRequest, +} from "../helpers/handler.ts"; +import { seedUes, truncateAll } from "../helpers/db_integration.ts"; +import { handler as uesHandler } from "$apps/notes/api/ues.ts"; +import { handler as ueHandler } from "$apps/notes/api/ues/[idUE].ts"; + +// --- GET /ues --- + +Deno.test({ + name: "e2e ues: GET /ues returns all UEs", + async fn() { + await truncateAll(); + await seedUes([{ nom: "UE Informatique" }, { nom: "UE Mathématiques" }]); + const res = await uesHandler.GET!(makeGetRequest("/ues"), makeEmployeeContext()); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.length, 2); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e ues: GET /ues returns empty when no UEs", + async fn() { + await truncateAll(); + const res = await uesHandler.GET!(makeGetRequest("/ues"), makeEmployeeContext()); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.length, 0); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// --- POST /ues --- + +Deno.test({ + name: "e2e ues: POST /ues creates UE (201)", + async fn() { + await truncateAll(); + const res = await uesHandler.POST!( + makeJsonRequest("/ues", "POST", { nom: "UE Physique" }), + makeEmployeeContext(), + ); + assertEquals(res.status, 201); + const body = await res.json(); + assertExists(body.id); + assertEquals(body.nom, "UE Physique"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e ues: POST /ues 400 on missing nom", + async fn() { + await truncateAll(); + const res = await uesHandler.POST!( + makeJsonRequest("/ues", "POST", {}), + makeEmployeeContext(), + ); + assertEquals(res.status, 400); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// --- GET /ues/:id --- + +Deno.test({ + name: "e2e ues: GET /ues/:id returns UE", + async fn() { + await truncateAll(); + const [ue] = await seedUes([{ nom: "UE Chimie" }]); + const res = await ueHandler.GET!( + makeGetRequest(`/ues/${ue.id}`), + makeEmployeeContext({ idUE: String(ue.id) }), + ); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.nom, "UE Chimie"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e ues: GET /ues/:id 404 when not found", + async fn() { + await truncateAll(); + const res = await ueHandler.GET!( + makeGetRequest("/ues/99999"), + makeEmployeeContext({ idUE: "99999" }), + ); + assertEquals(res.status, 404); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// --- PUT /ues/:id --- + +Deno.test({ + name: "e2e ues: PUT /ues/:id updates nom", + async fn() { + await truncateAll(); + const [ue] = await seedUes([{ nom: "UE Biologie" }]); + const res = await ueHandler.PUT!( + makeJsonRequest(`/ues/${ue.id}`, "PUT", { nom: "UE Biologie moléculaire" }), + makeEmployeeContext({ idUE: String(ue.id) }), + ); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.nom, "UE Biologie moléculaire"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e ues: PUT /ues/:id 404 when not found", + async fn() { + await truncateAll(); + const res = await ueHandler.PUT!( + makeJsonRequest("/ues/99999", "PUT", { nom: "X" }), + makeEmployeeContext({ idUE: "99999" }), + ); + assertEquals(res.status, 404); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// --- DELETE /ues/:id --- + +Deno.test({ + name: "e2e ues: DELETE /ues/:id returns 204", + async fn() { + await truncateAll(); + const [ue] = await seedUes([{ nom: "UE à supprimer" }]); + const res = await ueHandler.DELETE!( + makeGetRequest(`/ues/${ue.id}`), + makeEmployeeContext({ idUE: String(ue.id) }), + ); + assertEquals(res.status, 204); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e ues: DELETE /ues/:id 404 when not found", + async fn() { + await truncateAll(); + const res = await ueHandler.DELETE!( + makeGetRequest("/ues/99999"), + makeEmployeeContext({ idUE: "99999" }), + ); + assertEquals(res.status, 404); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/e2e/users_test.ts b/tests/e2e/users_test.ts new file mode 100644 index 0000000..038ed2f --- /dev/null +++ b/tests/e2e/users_test.ts @@ -0,0 +1,217 @@ +// E2E tests for /users endpoints — handler + real DB + +import { assertEquals, assertExists } from "@std/assert"; +import { + makeEmployeeContext, + makeGetRequest, + makeJsonRequest, +} from "../helpers/handler.ts"; +import { + seedRoles, + seedUsers, + truncateAll, +} from "../helpers/db_integration.ts"; +import { handler as usersHandler } from "$apps/admin/api/users.ts"; +import { handler as userHandler } from "$apps/admin/api/users/[id].ts"; + +// --- GET /users --- + +Deno.test({ + name: "e2e users: GET /users returns all users", + async fn() { + await truncateAll(); + await seedUsers([ + { id: "dupont.jean", nom: "Dupont", prenom: "Jean" }, + { id: "martin.alice", nom: "Martin", prenom: "Alice" }, + ]); + const res = await usersHandler.GET!(makeGetRequest("/users"), makeEmployeeContext()); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.length, 2); + assertExists(body.find((u: { id: string }) => u.id === "dupont.jean")); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e users: GET /users returns empty when no users", + async fn() { + await truncateAll(); + const res = await usersHandler.GET!(makeGetRequest("/users"), makeEmployeeContext()); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.length, 0); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e users: GET /users?idRole filters by role", + async fn() { + await truncateAll(); + const [role1] = await seedRoles([{ nom: "admin" }]); + const [role2] = await seedRoles([{ nom: "employee" }]); + await seedUsers([ + { id: "admin.user", nom: "Admin", prenom: "User", idRole: role1.id }, + { id: "emp.user", nom: "Emp", prenom: "User", idRole: role2.id }, + ]); + const res = await usersHandler.GET!( + makeGetRequest("/users", { idRole: String(role1.id) }), + makeEmployeeContext(), + ); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.length, 1); + assertEquals(body[0].id, "admin.user"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// --- POST /users --- + +Deno.test({ + name: "e2e users: POST /users creates user (201)", + async fn() { + await truncateAll(); + const res = await usersHandler.POST!( + makeJsonRequest("/users", "POST", { id: "new.user", nom: "New", prenom: "User" }), + makeEmployeeContext(), + ); + assertEquals(res.status, 201); + const body = await res.json(); + assertEquals(body.id, "new.user"); + assertEquals(body.nom, "New"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e users: POST /users 400 on missing fields", + async fn() { + await truncateAll(); + const res = await usersHandler.POST!( + makeJsonRequest("/users", "POST", { id: "x" }), + makeEmployeeContext(), + ); + assertEquals(res.status, 400); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e users: POST /users 409 on duplicate id", + async fn() { + await truncateAll(); + await seedUsers([{ id: "dupont.jean", nom: "Dupont", prenom: "Jean" }]); + const res = await usersHandler.POST!( + makeJsonRequest("/users", "POST", { id: "dupont.jean", nom: "Doublon", prenom: "X" }), + makeEmployeeContext(), + ); + assertEquals(res.status, 409); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// --- GET /users/:id --- + +Deno.test({ + name: "e2e users: GET /users/:id returns user", + async fn() { + await truncateAll(); + await seedUsers([{ id: "bernard.lucie", nom: "Bernard", prenom: "Lucie" }]); + const res = await userHandler.GET!( + makeGetRequest("/users/bernard.lucie"), + makeEmployeeContext({ id: "bernard.lucie" }), + ); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.id, "bernard.lucie"); + assertEquals(body.nom, "Bernard"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e users: GET /users/:id 404 when not found", + async fn() { + await truncateAll(); + const res = await userHandler.GET!( + makeGetRequest("/users/ghost.user"), + makeEmployeeContext({ id: "ghost.user" }), + ); + assertEquals(res.status, 404); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// --- PUT /users/:id --- + +Deno.test({ + name: "e2e users: PUT /users/:id updates user", + async fn() { + await truncateAll(); + await seedUsers([{ id: "thomas.eva", nom: "Thomas", prenom: "Eva" }]); + const res = await userHandler.PUT!( + makeJsonRequest("/users/thomas.eva", "PUT", { nom: "Thomas-Modifié", prenom: "Eva", idRole: null }), + makeEmployeeContext({ id: "thomas.eva" }), + ); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.nom, "Thomas-Modifié"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e users: PUT /users/:id 404 when not found", + async fn() { + await truncateAll(); + const res = await userHandler.PUT!( + makeJsonRequest("/users/ghost.user", "PUT", { nom: "X", prenom: "Y", idRole: null }), + makeEmployeeContext({ id: "ghost.user" }), + ); + assertEquals(res.status, 404); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// --- DELETE /users/:id --- + +Deno.test({ + name: "e2e users: DELETE /users/:id returns 204", + async fn() { + await truncateAll(); + await seedUsers([{ id: "petit.hugo", nom: "Petit", prenom: "Hugo" }]); + const res = await userHandler.DELETE!( + makeGetRequest("/users/petit.hugo"), + makeEmployeeContext({ id: "petit.hugo" }), + ); + assertEquals(res.status, 204); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e users: DELETE /users/:id 404 when not found", + async fn() { + await truncateAll(); + const res = await userHandler.DELETE!( + makeGetRequest("/users/ghost.user"), + makeEmployeeContext({ id: "ghost.user" }), + ); + assertEquals(res.status, 404); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/helpers/db_integration.ts b/tests/helpers/db_integration.ts index ee7fe04..4b91b25 100644 --- a/tests/helpers/db_integration.ts +++ b/tests/helpers/db_integration.ts @@ -87,3 +87,27 @@ export async function seedUsers( ): Promise { return await testDb.insert(schema.users).values(rows).returning(); } + +export async function seedNotes( + rows: { numEtud: number; idModule: string; note: number }[], +): Promise { + return await testDb.insert(schema.notes).values(rows).returning(); +} + +export async function seedUeModules( + rows: { idModule: string; idUE: number; idPromo: string; coeff: number }[], +): Promise { + return await testDb.insert(schema.ueModules).values(rows).returning(); +} + +export async function seedEnseignements( + rows: { idProf: string; idModule: string; idPromo: string }[], +): Promise { + return await testDb.insert(schema.enseignements).values(rows).returning(); +} + +export async function seedAjustements( + rows: { numEtud: number; idUE: number; valeur: number }[], +): Promise { + return await testDb.insert(schema.ajustements).values(rows).returning(); +} diff --git a/tests/integration/ajustements_test.ts b/tests/integration/ajustements_test.ts new file mode 100644 index 0000000..cd032a8 --- /dev/null +++ b/tests/integration/ajustements_test.ts @@ -0,0 +1,127 @@ +// Integration tests for /ajustements — Drizzle ORM direct on real DB + +import { assertEquals, assertExists, assertRejects } from "@std/assert"; +import { + seedAjustements, + seedPromotions, + seedStudents, + seedUes, + testDb, + truncateAll, +} from "../helpers/db_integration.ts"; +import { ajustements } from "$root/databases/schema.ts"; +import { and, eq } from "npm:drizzle-orm@0.45.2"; + +Deno.test({ + name: "integration ajustements: list all ajustements", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ nom: "Dupont", prenom: "Jean", idPromo: "P1" }]); + const [ue] = await seedUes([{ nom: "UE Info" }]); + await seedAjustements([{ numEtud: s.numEtud, idUE: ue.id, valeur: 13.0 }]); + const rows = await testDb.select().from(ajustements); + assertEquals(rows.length, 1); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ajustements: create and retrieve by composite key", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ nom: "Martin", prenom: "Alice", idPromo: "P1" }]); + const [ue] = await seedUes([{ nom: "UE Maths" }]); + + const [created] = await testDb + .insert(ajustements) + .values({ numEtud: s.numEtud, idUE: ue.id, valeur: 15.5 }) + .returning(); + assertExists(created); + assertEquals(created.valeur, 15.5); + + const row = await testDb + .select() + .from(ajustements) + .where(and(eq(ajustements.numEtud, s.numEtud), eq(ajustements.idUE, ue.id))) + .then((r) => r[0] ?? null); + assertExists(row); + assertEquals(row.valeur, 15.5); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ajustements: get by composite key returns null when not found", + async fn() { + await truncateAll(); + const row = await testDb + .select() + .from(ajustements) + .where(and(eq(ajustements.numEtud, 99999), eq(ajustements.idUE, 99))) + .then((r) => r[0] ?? null); + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ajustements: duplicate composite key insert fails", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ nom: "Durand", prenom: "Claire", idPromo: "P1" }]); + const [ue] = await seedUes([{ nom: "UE Info" }]); + await seedAjustements([{ numEtud: s.numEtud, idUE: ue.id, valeur: 12.0 }]); + await assertRejects(() => + testDb.insert(ajustements).values({ numEtud: s.numEtud, idUE: ue.id, valeur: 13.0 }) + ); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ajustements: update valeur", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ nom: "Bernard", prenom: "Lucie", idPromo: "P1" }]); + const [ue] = await seedUes([{ nom: "UE Physique" }]); + await seedAjustements([{ numEtud: s.numEtud, idUE: ue.id, valeur: 10.0 }]); + + const [updated] = await testDb + .update(ajustements) + .set({ valeur: 18.0 }) + .where(and(eq(ajustements.numEtud, s.numEtud), eq(ajustements.idUE, ue.id))) + .returning(); + assertEquals(updated.valeur, 18.0); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ajustements: delete removes the ajustement", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ nom: "Thomas", prenom: "Eva", idPromo: "P1" }]); + const [ue] = await seedUes([{ nom: "UE Chimie" }]); + await seedAjustements([{ numEtud: s.numEtud, idUE: ue.id, valeur: 11.0 }]); + + await testDb.delete(ajustements).where(and(eq(ajustements.numEtud, s.numEtud), eq(ajustements.idUE, ue.id))); + const row = await testDb + .select() + .from(ajustements) + .where(and(eq(ajustements.numEtud, s.numEtud), eq(ajustements.idUE, ue.id))) + .then((r) => r[0] ?? null); + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/integration/enseignements_test.ts b/tests/integration/enseignements_test.ts new file mode 100644 index 0000000..c48a312 --- /dev/null +++ b/tests/integration/enseignements_test.ts @@ -0,0 +1,135 @@ +// Integration tests for /enseignements — Drizzle ORM direct on real DB + +import { assertEquals, assertExists, assertRejects } from "@std/assert"; +import { + seedEnseignements, + seedModules, + seedPromotions, + seedUsers, + testDb, + truncateAll, +} from "../helpers/db_integration.ts"; +import { enseignements } from "$root/databases/schema.ts"; +import { and, eq } from "npm:drizzle-orm@0.45.2"; + +Deno.test({ + name: "integration enseignements: list all enseignements", + async fn() { + await truncateAll(); + await seedUsers([{ id: "prof.dupont", nom: "Dupont", prenom: "Jean" }]); + await seedModules([{ id: "M1", nom: "Mod A" }, { id: "M2", nom: "Mod B" }]); + await seedPromotions([{ id: "P1" }]); + await seedEnseignements([ + { idProf: "prof.dupont", idModule: "M1", idPromo: "P1" }, + { idProf: "prof.dupont", idModule: "M2", idPromo: "P1" }, + ]); + const rows = await testDb.select().from(enseignements); + assertEquals(rows.length, 2); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration enseignements: create and retrieve by composite key", + async fn() { + await truncateAll(); + await seedUsers([{ id: "prof.moreau", nom: "Moreau", prenom: "Sophie" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + await seedPromotions([{ id: "P1" }]); + + const [created] = await testDb + .insert(enseignements) + .values({ idProf: "prof.moreau", idModule: "M1", idPromo: "P1" }) + .returning(); + assertExists(created); + assertEquals(created.idProf, "prof.moreau"); + + const row = await testDb + .select() + .from(enseignements) + .where( + and( + eq(enseignements.idProf, "prof.moreau"), + eq(enseignements.idModule, "M1"), + eq(enseignements.idPromo, "P1"), + ), + ) + .then((r) => r[0] ?? null); + assertExists(row); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration enseignements: get by composite key returns null when not found", + async fn() { + await truncateAll(); + const row = await testDb + .select() + .from(enseignements) + .where( + and( + eq(enseignements.idProf, "ghost"), + eq(enseignements.idModule, "GHOST"), + eq(enseignements.idPromo, "GHOST"), + ), + ) + .then((r) => r[0] ?? null); + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration enseignements: duplicate composite key insert fails", + async fn() { + await truncateAll(); + await seedUsers([{ id: "prof.dupont", nom: "Dupont", prenom: "Jean" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + await seedPromotions([{ id: "P1" }]); + await seedEnseignements([{ idProf: "prof.dupont", idModule: "M1", idPromo: "P1" }]); + await assertRejects(() => + testDb.insert(enseignements).values({ idProf: "prof.dupont", idModule: "M1", idPromo: "P1" }) + ); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration enseignements: delete removes the enseignement", + async fn() { + await truncateAll(); + await seedUsers([{ id: "prof.dupont", nom: "Dupont", prenom: "Jean" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + await seedPromotions([{ id: "P1" }]); + await seedEnseignements([{ idProf: "prof.dupont", idModule: "M1", idPromo: "P1" }]); + + await testDb + .delete(enseignements) + .where( + and( + eq(enseignements.idProf, "prof.dupont"), + eq(enseignements.idModule, "M1"), + eq(enseignements.idPromo, "P1"), + ), + ); + const row = await testDb + .select() + .from(enseignements) + .where( + and( + eq(enseignements.idProf, "prof.dupont"), + eq(enseignements.idModule, "M1"), + eq(enseignements.idPromo, "P1"), + ), + ) + .then((r) => r[0] ?? null); + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/integration/notes_test.ts b/tests/integration/notes_test.ts new file mode 100644 index 0000000..bae19b3 --- /dev/null +++ b/tests/integration/notes_test.ts @@ -0,0 +1,124 @@ +// Integration tests for /notes — Drizzle ORM direct on real DB + +import { assertEquals, assertExists, assertRejects } from "@std/assert"; +import { + seedModules, + seedNotes, + seedPromotions, + seedStudents, + testDb, + truncateAll, +} from "../helpers/db_integration.ts"; +import { notes } from "$root/databases/schema.ts"; +import { and, eq } from "npm:drizzle-orm@0.45.2"; + +Deno.test({ + name: "integration notes: list all notes", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "PROMO-2024" }]); + const [s] = await seedStudents([{ nom: "Dupont", prenom: "Jean", idPromo: "PROMO-2024" }]); + await seedModules([{ id: "MOD101", nom: "Module A" }]); + await seedNotes([{ numEtud: s.numEtud, idModule: "MOD101", note: 15.5 }]); + const rows = await testDb.select().from(notes); + assertEquals(rows.length, 1); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration notes: create and retrieve by composite key", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "PROMO-2024" }]); + const [s] = await seedStudents([{ nom: "Martin", prenom: "Alice", idPromo: "PROMO-2024" }]); + await seedModules([{ id: "MOD102", nom: "Module B" }]); + + const [created] = await testDb.insert(notes).values({ numEtud: s.numEtud, idModule: "MOD102", note: 12.0 }).returning(); + assertExists(created); + assertEquals(created.note, 12.0); + + const row = await testDb + .select() + .from(notes) + .where(and(eq(notes.numEtud, s.numEtud), eq(notes.idModule, "MOD102"))) + .then((r) => r[0] ?? null); + assertExists(row); + assertEquals(row.note, 12.0); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration notes: get by composite key returns null when not found", + async fn() { + await truncateAll(); + const row = await testDb + .select() + .from(notes) + .where(and(eq(notes.numEtud, 99999), eq(notes.idModule, "GHOST"))) + .then((r) => r[0] ?? null); + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration notes: duplicate composite key insert fails", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "PROMO-2024" }]); + const [s] = await seedStudents([{ nom: "Durand", prenom: "Claire", idPromo: "PROMO-2024" }]); + await seedModules([{ id: "MOD103", nom: "Module C" }]); + await seedNotes([{ numEtud: s.numEtud, idModule: "MOD103", note: 10.0 }]); + await assertRejects(() => + testDb.insert(notes).values({ numEtud: s.numEtud, idModule: "MOD103", note: 11.0 }) + ); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration notes: update note value", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "PROMO-2024" }]); + const [s] = await seedStudents([{ nom: "Bernard", prenom: "Lucie", idPromo: "PROMO-2024" }]); + await seedModules([{ id: "MOD104", nom: "Module D" }]); + await seedNotes([{ numEtud: s.numEtud, idModule: "MOD104", note: 8.0 }]); + + const [updated] = await testDb + .update(notes) + .set({ note: 16.0 }) + .where(and(eq(notes.numEtud, s.numEtud), eq(notes.idModule, "MOD104"))) + .returning(); + assertEquals(updated.note, 16.0); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration notes: delete removes the note", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "PROMO-2024" }]); + const [s] = await seedStudents([{ nom: "Thomas", prenom: "Eva", idPromo: "PROMO-2024" }]); + await seedModules([{ id: "MOD105", nom: "Module E" }]); + await seedNotes([{ numEtud: s.numEtud, idModule: "MOD105", note: 14.0 }]); + + await testDb.delete(notes).where(and(eq(notes.numEtud, s.numEtud), eq(notes.idModule, "MOD105"))); + const row = await testDb + .select() + .from(notes) + .where(and(eq(notes.numEtud, s.numEtud), eq(notes.idModule, "MOD105"))) + .then((r) => r[0] ?? null); + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/integration/ue_modules_test.ts b/tests/integration/ue_modules_test.ts new file mode 100644 index 0000000..9a996ad --- /dev/null +++ b/tests/integration/ue_modules_test.ts @@ -0,0 +1,132 @@ +// Integration tests for /ue-modules — Drizzle ORM direct on real DB + +import { assertEquals, assertExists, assertRejects } from "@std/assert"; +import { + seedModules, + seedPromotions, + seedUeModules, + seedUes, + testDb, + truncateAll, +} from "../helpers/db_integration.ts"; +import { ueModules } from "$root/databases/schema.ts"; +import { and, eq } from "npm:drizzle-orm@0.45.2"; + +Deno.test({ + name: "integration ue_modules: list all associations", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod A" }, { id: "M2", nom: "Mod B" }]); + const [ue] = await seedUes([{ nom: "UE Info" }]); + await seedUeModules([ + { idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: 2.0 }, + { idModule: "M2", idUE: ue.id, idPromo: "P1", coeff: 3.0 }, + ]); + const rows = await testDb.select().from(ueModules); + assertEquals(rows.length, 2); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ue_modules: create and retrieve by composite key", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + const [ue] = await seedUes([{ nom: "UE Maths" }]); + + const [created] = await testDb + .insert(ueModules) + .values({ idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: 4.0 }) + .returning(); + assertExists(created); + assertEquals(created.coeff, 4.0); + + const row = await testDb + .select() + .from(ueModules) + .where(and(eq(ueModules.idModule, "M1"), eq(ueModules.idUE, ue.id), eq(ueModules.idPromo, "P1"))) + .then((r) => r[0] ?? null); + assertExists(row); + assertEquals(row.coeff, 4.0); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ue_modules: get by composite key returns null when not found", + async fn() { + await truncateAll(); + const row = await testDb + .select() + .from(ueModules) + .where(and(eq(ueModules.idModule, "GHOST"), eq(ueModules.idUE, 99), eq(ueModules.idPromo, "GHOST"))) + .then((r) => r[0] ?? null); + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ue_modules: duplicate composite key insert fails", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + const [ue] = await seedUes([{ nom: "UE Info" }]); + await seedUeModules([{ idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: 2.0 }]); + await assertRejects(() => + testDb.insert(ueModules).values({ idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: 5.0 }) + ); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ue_modules: update coeff", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + const [ue] = await seedUes([{ nom: "UE Info" }]); + await seedUeModules([{ idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: 2.0 }]); + + const [updated] = await testDb + .update(ueModules) + .set({ coeff: 6.0 }) + .where(and(eq(ueModules.idModule, "M1"), eq(ueModules.idUE, ue.id), eq(ueModules.idPromo, "P1"))) + .returning(); + assertEquals(updated.coeff, 6.0); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ue_modules: delete removes the association", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + const [ue] = await seedUes([{ nom: "UE Info" }]); + await seedUeModules([{ idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: 2.0 }]); + + await testDb + .delete(ueModules) + .where(and(eq(ueModules.idModule, "M1"), eq(ueModules.idUE, ue.id), eq(ueModules.idPromo, "P1"))); + const row = await testDb + .select() + .from(ueModules) + .where(and(eq(ueModules.idModule, "M1"), eq(ueModules.idUE, ue.id), eq(ueModules.idPromo, "P1"))) + .then((r) => r[0] ?? null); + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/integration/ues_test.ts b/tests/integration/ues_test.ts new file mode 100644 index 0000000..653fbef --- /dev/null +++ b/tests/integration/ues_test.ts @@ -0,0 +1,83 @@ +// Integration tests for /ues — Drizzle ORM direct on real DB + +import { assertEquals, assertExists, assertRejects } from "@std/assert"; +import { seedUes, testDb, truncateAll } from "../helpers/db_integration.ts"; +import { ues } from "$root/databases/schema.ts"; +import { eq } from "npm:drizzle-orm@0.45.2"; + +Deno.test({ + name: "integration ues: list all UEs", + async fn() { + await truncateAll(); + await seedUes([{ nom: "UE Informatique" }, { nom: "UE Mathématiques" }]); + const rows = await testDb.select().from(ues); + assertEquals(rows.length, 2); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ues: create and retrieve by id", + async fn() { + await truncateAll(); + const [created] = await testDb.insert(ues).values({ nom: "UE Physique" }).returning(); + assertExists(created); + assertExists(created.id); + assertEquals(created.nom, "UE Physique"); + + const row = await testDb.select().from(ues).where(eq(ues.id, created.id)).then((r) => r[0] ?? null); + assertExists(row); + assertEquals(row.nom, "UE Physique"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ues: get by id returns null when not found", + async fn() { + await truncateAll(); + const row = await testDb.select().from(ues).where(eq(ues.id, 99999)).then((r) => r[0] ?? null); + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ues: update nom", + async fn() { + await truncateAll(); + const [ue] = await seedUes([{ nom: "UE Chimie" }]); + const [updated] = await testDb.update(ues).set({ nom: "UE Chimie organique" }).where(eq(ues.id, ue.id)).returning(); + assertEquals(updated.nom, "UE Chimie organique"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ues: delete removes the UE", + async fn() { + await truncateAll(); + const [ue] = await seedUes([{ nom: "UE à supprimer" }]); + await testDb.delete(ues).where(eq(ues.id, ue.id)); + const row = await testDb.select().from(ues).where(eq(ues.id, ue.id)).then((r) => r[0] ?? null); + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ues: nom is required (not null)", + async fn() { + await truncateAll(); + // deno-lint-ignore no-explicit-any + await assertRejects(() => testDb.insert(ues).values({ nom: null as any })); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + diff --git a/tests/unit/ajustements_test.ts b/tests/unit/ajustements_test.ts new file mode 100644 index 0000000..a2786c4 --- /dev/null +++ b/tests/unit/ajustements_test.ts @@ -0,0 +1,189 @@ +// Unit tests for /ajustements endpoints — fixtures, mock API, mock DB + +import { assertEquals, assertExists } from "@std/assert"; +import { mockFetch, restoreFetch } from "../helpers/api_mock.ts"; +import { createMockDb } from "../helpers/db_mock.ts"; +import { type Ajustement, ajustements } from "../helpers/fixtures.ts"; + +// --- Fixtures --- + +Deno.test("ajustements: fixtures have correct shape", () => { + assertEquals(ajustements.length, 2); + assertEquals(typeof ajustements[0].numEtud, "number"); + assertEquals(typeof ajustements[0].idUE, "number"); + assertEquals(typeof ajustements[0].valeur, "number"); +}); + +// --- Mock API --- + +Deno.test("mock API: GET /ajustements returns list", async () => { + mockFetch({ "/ajustements": ajustements }); + try { + const res = await fetch("http://localhost/api/ajustements"); + assertEquals(res.status, 200); + const data: Ajustement[] = await res.json(); + assertEquals(data.length, 2); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: GET /ajustements?numEtud filters by student", async () => { + const filtered = ajustements.filter((a) => a.numEtud === 21212006); + mockFetch({ "/ajustements": filtered }); + try { + const res = await fetch("http://localhost/api/ajustements?numEtud=21212006"); + const data: Ajustement[] = await res.json(); + assertEquals(data.length, 1); + assertEquals(data[0].numEtud, 21212006); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: GET /ajustements?numEtud=NaN returns 400", async () => { + mockFetch({ "/ajustements": { status: 400 } }); + try { + const res = await fetch("http://localhost/api/ajustements?numEtud=abc"); + assertEquals(res.status, 400); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: POST /ajustements creates ajustement (201) as employee", async () => { + const newAjust: Ajustement = { numEtud: 21212007, idUE: 2, valeur: 14.0 }; + mockFetch({ "/ajustements": { method: "POST", status: 201, body: newAjust } }); + try { + const res = await fetch("http://localhost/api/ajustements", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(newAjust), + }); + assertEquals(res.status, 201); + const data: Ajustement = await res.json(); + assertEquals(data.numEtud, 21212007); + assertEquals(data.valeur, 14.0); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: POST /ajustements 403 for non-employee", async () => { + mockFetch({ "/ajustements": { method: "POST", status: 403 } }); + try { + const res = await fetch("http://localhost/api/ajustements", { method: "POST" }); + assertEquals(res.status, 403); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: POST /ajustements 400 on missing fields", async () => { + mockFetch({ "/ajustements": { method: "POST", status: 400 } }); + try { + const res = await fetch("http://localhost/api/ajustements", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ numEtud: 21212006 }), + }); + assertEquals(res.status, 400); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: GET /ajustements/:numEtud/:idUE returns ajustement (employee)", async () => { + mockFetch({ "/ajustements/21212006/1": ajustements[0] }); + try { + const res = await fetch("http://localhost/api/ajustements/21212006/1"); + assertEquals(res.status, 200); + const data: Ajustement = await res.json(); + assertEquals(data.valeur, 13.25); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: GET /ajustements/:numEtud/:idUE 403 for non-employee", async () => { + mockFetch({ "/ajustements/21212006/1": { status: 403 } }); + try { + const res = await fetch("http://localhost/api/ajustements/21212006/1"); + assertEquals(res.status, 403); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: GET /ajustements/:numEtud/:idUE 404 when not found", async () => { + mockFetch({ "/ajustements/99999/9": { status: 404, body: { error: "Ajustement introuvable" } } }); + try { + const res = await fetch("http://localhost/api/ajustements/99999/9"); + assertEquals(res.status, 404); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: PUT /ajustements/:numEtud/:idUE updates valeur", async () => { + const updated: Ajustement = { ...ajustements[0], valeur: 18.0 }; + mockFetch({ "/ajustements/21212006/1": { method: "PUT", status: 200, body: updated } }); + try { + const res = await fetch("http://localhost/api/ajustements/21212006/1", { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ valeur: 18.0 }), + }); + assertEquals(res.status, 200); + const data: Ajustement = await res.json(); + assertEquals(data.valeur, 18.0); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: DELETE /ajustements/:numEtud/:idUE returns 204", async () => { + mockFetch({ "/ajustements/21212006/1": { method: "DELETE", status: 204 } }); + try { + const res = await fetch("http://localhost/api/ajustements/21212006/1", { method: "DELETE" }); + assertEquals(res.status, 204); + } finally { + restoreFetch(); + } +}); + +// --- Mock DB --- + +Deno.test("mock DB: find ajustement by composite key", () => { + const db = createMockDb({ tables: { ajustements: [...ajustements] } }); + const a = db.findOne("ajustements", (a) => a.numEtud === 21212006 && a.idUE === 1); + assertExists(a); + assertEquals(a.valeur, 13.25); +}); + +Deno.test("mock DB: filter ajustements by numEtud", () => { + const db = createMockDb({ tables: { ajustements: [...ajustements] } }); + const rows = db.findMany("ajustements", (a) => a.numEtud === 21212006); + assertEquals(rows.length, 1); +}); + +Deno.test("mock DB: insert ajustement", () => { + const db = createMockDb({ tables: { ajustements: [...ajustements] } }); + db.insert("ajustements", { numEtud: 21212007, idUE: 2, valeur: 14.0 }); + assertEquals(db.getTable("ajustements").length, 3); +}); + +Deno.test("mock DB: update ajustement valeur", () => { + const db = createMockDb({ tables: { ajustements: [...ajustements] } }); + db.updateWhere("ajustements", (a) => a.numEtud === 21212006 && a.idUE === 1, { valeur: 20.0 }); + assertEquals( + db.findOne("ajustements", (a) => a.numEtud === 21212006 && a.idUE === 1)?.valeur, + 20.0, + ); +}); + +Deno.test("mock DB: delete ajustement", () => { + const db = createMockDb({ tables: { ajustements: [...ajustements] } }); + db.deleteWhere("ajustements", (a) => a.numEtud === 21212006 && a.idUE === 1); + assertEquals(db.getTable("ajustements").length, 1); +}); diff --git a/tests/unit/enseignements_test.ts b/tests/unit/enseignements_test.ts new file mode 100644 index 0000000..3182019 --- /dev/null +++ b/tests/unit/enseignements_test.ts @@ -0,0 +1,179 @@ +// Unit tests for /enseignements endpoints — fixtures, mock API, mock DB + +import { assertEquals, assertExists } from "@std/assert"; +import { mockFetch, restoreFetch } from "../helpers/api_mock.ts"; +import { createMockDb } from "../helpers/db_mock.ts"; +import { enseignements } from "../helpers/fixtures.ts"; + +interface Enseignement { + idProf: string; + idModule: string; + idPromo: string; +} + +// --- Fixtures --- + +Deno.test("enseignements: fixtures have correct shape", () => { + assertEquals(enseignements.length, 3); + assertEquals(typeof enseignements[0].idModule, "string"); + assertEquals(typeof enseignements[0].idPromo, "string"); +}); + +// --- Mock API --- + +Deno.test("mock API: POST /enseignements creates enseignement (201) as employee", async () => { + const newEns: Enseignement = { idProf: "prof.dupont", idModule: "JIN702C", idPromo: "4AFISE25/26" }; + mockFetch({ "/enseignements": { method: "POST", status: 201, body: newEns } }); + try { + const res = await fetch("http://localhost/api/enseignements", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(newEns), + }); + assertEquals(res.status, 201); + const data: Enseignement = await res.json(); + assertEquals(data.idModule, "JIN702C"); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: POST /enseignements 403 for non-employee", async () => { + mockFetch({ "/enseignements": { method: "POST", status: 403 } }); + try { + const res = await fetch("http://localhost/api/enseignements", { method: "POST" }); + assertEquals(res.status, 403); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: POST /enseignements 400 on missing fields", async () => { + mockFetch({ "/enseignements": { method: "POST", status: 400 } }); + try { + const res = await fetch("http://localhost/api/enseignements", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ idProf: "prof.dupont" }), + }); + assertEquals(res.status, 400); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: POST /enseignements 409 on duplicate", async () => { + mockFetch({ + "/enseignements": { method: "POST", status: 409, body: { error: "Cet enseignement existe déjà." } }, + }); + try { + const res = await fetch("http://localhost/api/enseignements", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ idProf: "prof.dupont", idModule: "JIN702C", idPromo: "4AFISE25/26" }), + }); + assertEquals(res.status, 409); + const data = await res.json(); + assertExists(data.error); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: GET /enseignements/:idProf/:idModule/:idPromo returns enseignement (employee)", async () => { + const ens: Enseignement = { idProf: "prof.dupont", idModule: "JIN702C", idPromo: "4AFISE25/26" }; + mockFetch({ "/enseignements/prof.dupont/JIN702C/4AFISE25": ens }); + try { + const res = await fetch("http://localhost/api/enseignements/prof.dupont/JIN702C/4AFISE25%2F26"); + assertEquals(res.status, 200); + const data: Enseignement = await res.json(); + assertEquals(data.idProf, "prof.dupont"); + assertEquals(data.idModule, "JIN702C"); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: GET /enseignements/:idProf/:idModule/:idPromo 403 for non-employee", async () => { + mockFetch({ "/enseignements/prof.dupont/JIN702C/4AFISE25": { status: 403 } }); + try { + const res = await fetch("http://localhost/api/enseignements/prof.dupont/JIN702C/4AFISE25%2F26"); + assertEquals(res.status, 403); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: GET /enseignements/:idProf/:idModule/:idPromo 404 when not found", async () => { + mockFetch({ "/enseignements/ghost/GHOST/GHOST": { status: 404, body: { error: "Ressource introuvable" } } }); + try { + const res = await fetch("http://localhost/api/enseignements/ghost/GHOST/GHOST"); + assertEquals(res.status, 404); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: DELETE /enseignements/:idProf/:idModule/:idPromo returns 204 (employee)", async () => { + mockFetch({ "/enseignements/prof.dupont/JIN702C/4AFISE25": { method: "DELETE", status: 204 } }); + try { + const res = await fetch("http://localhost/api/enseignements/prof.dupont/JIN702C/4AFISE25%2F26", { + method: "DELETE", + }); + assertEquals(res.status, 204); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: DELETE /enseignements/:idProf/:idModule/:idPromo 403 for non-employee", async () => { + mockFetch({ "/enseignements/prof.dupont/JIN702C/4AFISE25": { method: "DELETE", status: 403 } }); + try { + const res = await fetch("http://localhost/api/enseignements/prof.dupont/JIN702C/4AFISE25%2F26", { + method: "DELETE", + }); + assertEquals(res.status, 403); + } finally { + restoreFetch(); + } +}); + +// --- Mock DB --- + +Deno.test("mock DB: find enseignement by composite key", () => { + const data = [ + { idProf: "prof.dupont", idModule: "JIN702C", idPromo: "4AFISE25/26" }, + { idProf: "prof.moreau", idModule: "JIN703C", idPromo: "4AFISE25/26" }, + ]; + const db = createMockDb({ tables: { enseignements: data } }); + const e = db.findOne("enseignements", (e) => e.idProf === "prof.dupont" && e.idModule === "JIN702C"); + assertExists(e); + assertEquals(e.idPromo, "4AFISE25/26"); +}); + +Deno.test("mock DB: insert enseignement", () => { + const db = createMockDb({ tables: { enseignements: [] } }); + db.insert("enseignements", { idProf: "prof.dupont", idModule: "JIN702C", idPromo: "4AFISE25/26" }); + assertEquals(db.getTable("enseignements").length, 1); +}); + +Deno.test("mock DB: delete enseignement", () => { + const data = [ + { idProf: "prof.dupont", idModule: "JIN702C", idPromo: "4AFISE25/26" }, + { idProf: "prof.moreau", idModule: "JIN703C", idPromo: "4AFISE25/26" }, + ]; + const db = createMockDb({ tables: { enseignements: data } }); + db.deleteWhere("enseignements", (e) => e.idProf === "prof.dupont"); + assertEquals(db.getTable("enseignements").length, 1); +}); + +Deno.test("mock DB: filter enseignements by idModule", () => { + const data = [ + { idProf: "prof.dupont", idModule: "JIN702C", idPromo: "4AFISE25/26" }, + { idProf: "prof.dupont", idModule: "JIN702C", idPromo: "3AFISE25/26" }, + { idProf: "prof.moreau", idModule: "JIN703C", idPromo: "4AFISE25/26" }, + ]; + const db = createMockDb({ tables: { enseignements: data } }); + const rows = db.findMany("enseignements", (e) => e.idModule === "JIN702C"); + assertEquals(rows.length, 2); +}); diff --git a/tests/unit/notes_test.ts b/tests/unit/notes_test.ts new file mode 100644 index 0000000..f39d4ba --- /dev/null +++ b/tests/unit/notes_test.ts @@ -0,0 +1,196 @@ +// Unit tests for /notes endpoints — fixtures, mock API, mock DB + +import { assertEquals, assertExists } from "@std/assert"; +import { mockFetch, restoreFetch } from "../helpers/api_mock.ts"; +import { createMockDb } from "../helpers/db_mock.ts"; +import { type Note, notes } from "../helpers/fixtures.ts"; + +// --- Fixtures --- + +Deno.test("notes: fixtures have correct shape", () => { + assertEquals(notes.length, 4); + assertEquals(typeof notes[0].note, "number"); + assertEquals(typeof notes[0].numEtud, "number"); + assertEquals(typeof notes[0].idModule, "string"); +}); + +Deno.test("notes: fixtures use decimal values", () => { + assertEquals(notes[0].note, 15.5); +}); + +// --- Mock API --- + +Deno.test("mock API: GET /notes returns list", async () => { + mockFetch({ "/notes": notes }); + try { + const res = await fetch("http://localhost/api/notes"); + assertEquals(res.status, 200); + const data: Note[] = await res.json(); + assertEquals(data.length, 4); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: GET /notes?numEtud filters by student", async () => { + const filtered = notes.filter((n) => n.numEtud === 21212006); + mockFetch({ "/notes": filtered }); + try { + const res = await fetch("http://localhost/api/notes?numEtud=21212006"); + const data: Note[] = await res.json(); + assertEquals(data.length, 2); + assertEquals(data.every((n) => n.numEtud === 21212006), true); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: GET /notes?idModule filters by module", async () => { + const filtered = notes.filter((n) => n.idModule === "JIN702C"); + mockFetch({ "/notes": filtered }); + try { + const res = await fetch("http://localhost/api/notes?idModule=JIN702C"); + const data: Note[] = await res.json(); + assertEquals(data.length, 2); + assertEquals(data.every((n) => n.idModule === "JIN702C"), true); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: GET /notes?numEtud=NaN returns 400", async () => { + mockFetch({ "/notes": { status: 400 } }); + try { + const res = await fetch("http://localhost/api/notes?numEtud=abc"); + assertEquals(res.status, 400); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: POST /notes creates note (201)", async () => { + const newNote: Note = { note: 14.0, numEtud: 21212006, idModule: "JIN704C" }; + mockFetch({ "/notes": { method: "POST", status: 201, body: newNote } }); + try { + const res = await fetch("http://localhost/api/notes", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(newNote), + }); + assertEquals(res.status, 201); + const data: Note = await res.json(); + assertEquals(data.note, 14.0); + assertEquals(data.numEtud, 21212006); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: POST /notes 400 on missing fields", async () => { + mockFetch({ "/notes": { method: "POST", status: 400 } }); + try { + const res = await fetch("http://localhost/api/notes", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ numEtud: 21212006 }), + }); + assertEquals(res.status, 400); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: GET /notes/:numEtud/:idModule returns note", async () => { + mockFetch({ "/notes/21212006/JIN702C": notes[0] }); + try { + const res = await fetch("http://localhost/api/notes/21212006/JIN702C"); + assertEquals(res.status, 200); + const data: Note = await res.json(); + assertEquals(data.note, 15.5); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: GET /notes/:numEtud/:idModule 404 when not found", async () => { + mockFetch({ "/notes/99999/GHOST": { status: 404, body: { error: "Ressource introuvable" } } }); + try { + const res = await fetch("http://localhost/api/notes/99999/GHOST"); + assertEquals(res.status, 404); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: PUT /notes/:numEtud/:idModule updates note", async () => { + const updated: Note = { ...notes[0], note: 17.0 }; + mockFetch({ "/notes/21212006/JIN702C": { method: "PUT", status: 200, body: updated } }); + try { + const res = await fetch("http://localhost/api/notes/21212006/JIN702C", { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ note: 17.0 }), + }); + assertEquals(res.status, 200); + const data: Note = await res.json(); + assertEquals(data.note, 17.0); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: DELETE /notes/:numEtud/:idModule returns 204", async () => { + mockFetch({ "/notes/21212006/JIN702C": { method: "DELETE", status: 204 } }); + try { + const res = await fetch("http://localhost/api/notes/21212006/JIN702C", { method: "DELETE" }); + assertEquals(res.status, 204); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: DELETE /notes/:numEtud/:idModule 404 when not found", async () => { + mockFetch({ "/notes/99999/GHOST": { method: "DELETE", status: 404 } }); + try { + const res = await fetch("http://localhost/api/notes/99999/GHOST", { method: "DELETE" }); + assertEquals(res.status, 404); + } finally { + restoreFetch(); + } +}); + +// --- Mock DB --- + +Deno.test("mock DB: find note by composite key", () => { + const db = createMockDb({ tables: { notes: [...notes] } }); + const n = db.findOne("notes", (n) => n.numEtud === 21212006 && n.idModule === "JIN702C"); + assertExists(n); + assertEquals(n.note, 15.5); +}); + +Deno.test("mock DB: filter notes by numEtud", () => { + const db = createMockDb({ tables: { notes: [...notes] } }); + const rows = db.findMany("notes", (n) => n.numEtud === 21212006); + assertEquals(rows.length, 2); +}); + +Deno.test("mock DB: insert note", () => { + const db = createMockDb({ tables: { notes: [...notes] } }); + db.insert("notes", { note: 10.0, numEtud: 21212006, idModule: "JIN704C" }); + assertEquals(db.getTable("notes").length, 5); +}); + +Deno.test("mock DB: update note value", () => { + const db = createMockDb({ tables: { notes: [...notes] } }); + db.updateWhere("notes", (n) => n.numEtud === 21212006 && n.idModule === "JIN702C", { note: 20.0 }); + assertEquals( + db.findOne("notes", (n) => n.numEtud === 21212006 && n.idModule === "JIN702C")?.note, + 20.0, + ); +}); + +Deno.test("mock DB: delete note", () => { + const db = createMockDb({ tables: { notes: [...notes] } }); + db.deleteWhere("notes", (n) => n.numEtud === 21212006 && n.idModule === "JIN702C"); + assertEquals(db.getTable("notes").length, 3); +}); diff --git a/tests/unit/ue_modules_test.ts b/tests/unit/ue_modules_test.ts new file mode 100644 index 0000000..8037998 --- /dev/null +++ b/tests/unit/ue_modules_test.ts @@ -0,0 +1,173 @@ +// Unit tests for /ue-modules endpoints — fixtures, mock API, mock DB + +import { assertEquals, assertExists } from "@std/assert"; +import { mockFetch, restoreFetch } from "../helpers/api_mock.ts"; +import { createMockDb } from "../helpers/db_mock.ts"; +import { type UeModule, ueModules } from "../helpers/fixtures.ts"; + +// --- Fixtures --- + +Deno.test("ue_modules: fixtures have correct shape", () => { + assertEquals(ueModules.length, 3); + assertEquals(typeof ueModules[0].idModule, "string"); + assertEquals(typeof ueModules[0].idUE, "number"); + assertEquals(typeof ueModules[0].idPromo, "string"); + assertEquals(typeof ueModules[0].coeff, "number"); +}); + +// --- Mock API --- + +Deno.test("mock API: GET /ue-modules returns list", async () => { + mockFetch({ "/ue-modules": ueModules }); + try { + const res = await fetch("http://localhost/api/ue-modules"); + assertEquals(res.status, 200); + const data: UeModule[] = await res.json(); + assertEquals(data.length, 3); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: GET /ue-modules?idPromo filters by promo", async () => { + const filtered = ueModules.filter((u) => u.idPromo === "4AFISE25/26"); + mockFetch({ "/ue-modules": filtered }); + try { + const res = await fetch("http://localhost/api/ue-modules?idPromo=4AFISE25%2F26"); + const data: UeModule[] = await res.json(); + assertEquals(data.length, 2); + assertEquals(data.every((u) => u.idPromo === "4AFISE25/26"), true); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: GET /ue-modules?idUE filters by UE", async () => { + const filtered = ueModules.filter((u) => u.idUE === 1); + mockFetch({ "/ue-modules": filtered }); + try { + const res = await fetch("http://localhost/api/ue-modules?idUE=1"); + const data: UeModule[] = await res.json(); + assertEquals(data.length, 2); + assertEquals(data.every((u) => u.idUE === 1), true); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: POST /ue-modules creates association (201)", async () => { + const newUeModule: UeModule = { idModule: "JIN705C", idUE: 2, idPromo: "3AFISE25/26", coeff: 3.0 }; + mockFetch({ "/ue-modules": { method: "POST", status: 201, body: newUeModule } }); + try { + const res = await fetch("http://localhost/api/ue-modules", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(newUeModule), + }); + assertEquals(res.status, 201); + const data: UeModule = await res.json(); + assertEquals(data.idModule, "JIN705C"); + assertEquals(data.coeff, 3.0); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: POST /ue-modules 400 on missing fields", async () => { + mockFetch({ "/ue-modules": { method: "POST", status: 400 } }); + try { + const res = await fetch("http://localhost/api/ue-modules", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ idModule: "X" }), + }); + assertEquals(res.status, 400); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: GET /ue-modules/:idModule/:idUE/:idPromo returns association (employee)", async () => { + mockFetch({ "/ue-modules/JIN702C/1/4AFISE25": ueModules[0] }); + try { + const res = await fetch("http://localhost/api/ue-modules/JIN702C/1/4AFISE25%2F26"); + assertEquals(res.status, 200); + const data: UeModule = await res.json(); + assertEquals(data.coeff, 3.0); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: GET /ue-modules/:idModule/:idUE/:idPromo 403 for non-employee", async () => { + mockFetch({ "/ue-modules/JIN702C/1/4AFISE25": { status: 403 } }); + try { + const res = await fetch("http://localhost/api/ue-modules/JIN702C/1/4AFISE25%2F26"); + assertEquals(res.status, 403); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: PUT /ue-modules/:idModule/:idUE/:idPromo updates coeff", async () => { + const updated: UeModule = { ...ueModules[0], coeff: 5.0 }; + mockFetch({ "/ue-modules/JIN702C/1/4AFISE25": { method: "PUT", status: 200, body: updated } }); + try { + const res = await fetch("http://localhost/api/ue-modules/JIN702C/1/4AFISE25%2F26", { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ coeff: 5.0 }), + }); + assertEquals(res.status, 200); + const data: UeModule = await res.json(); + assertEquals(data.coeff, 5.0); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: DELETE /ue-modules/:idModule/:idUE/:idPromo returns 204", async () => { + mockFetch({ "/ue-modules/JIN702C/1/4AFISE25": { method: "DELETE", status: 204 } }); + try { + const res = await fetch("http://localhost/api/ue-modules/JIN702C/1/4AFISE25%2F26", { method: "DELETE" }); + assertEquals(res.status, 204); + } finally { + restoreFetch(); + } +}); + +// --- Mock DB --- + +Deno.test("mock DB: find ue-module by composite key", () => { + const db = createMockDb({ tables: { ueModules: [...ueModules] } }); + const u = db.findOne("ueModules", (u) => u.idModule === "JIN702C" && u.idUE === 1 && u.idPromo === "4AFISE25/26"); + assertExists(u); + assertEquals(u.coeff, 3.0); +}); + +Deno.test("mock DB: filter ue-modules by promo", () => { + const db = createMockDb({ tables: { ueModules: [...ueModules] } }); + const rows = db.findMany("ueModules", (u) => u.idPromo === "4AFISE25/26"); + assertEquals(rows.length, 2); +}); + +Deno.test("mock DB: insert ue-module", () => { + const db = createMockDb({ tables: { ueModules: [...ueModules] } }); + db.insert("ueModules", { idModule: "JIN705C", idUE: 2, idPromo: "3AFISE25/26", coeff: 1.5 }); + assertEquals(db.getTable("ueModules").length, 4); +}); + +Deno.test("mock DB: update ue-module coeff", () => { + const db = createMockDb({ tables: { ueModules: [...ueModules] } }); + db.updateWhere("ueModules", (u) => u.idModule === "JIN702C" && u.idUE === 1, { coeff: 6.0 }); + assertEquals( + db.findOne("ueModules", (u) => u.idModule === "JIN702C" && u.idUE === 1)?.coeff, + 6.0, + ); +}); + +Deno.test("mock DB: delete ue-module", () => { + const db = createMockDb({ tables: { ueModules: [...ueModules] } }); + db.deleteWhere("ueModules", (u) => u.idModule === "JIN702C" && u.idUE === 1); + assertEquals(db.getTable("ueModules").length, 2); +}); diff --git a/tests/unit/ues_test.ts b/tests/unit/ues_test.ts new file mode 100644 index 0000000..5d36a95 --- /dev/null +++ b/tests/unit/ues_test.ts @@ -0,0 +1,160 @@ +// Unit tests for /ues endpoints — fixtures, mock API, mock DB + +import { assertEquals, assertExists } from "@std/assert"; +import { mockFetch, restoreFetch } from "../helpers/api_mock.ts"; +import { createMockDb } from "../helpers/db_mock.ts"; +import { type UE, ues } from "../helpers/fixtures.ts"; + +// --- Fixtures --- + +Deno.test("ues: fixtures have correct shape", () => { + assertEquals(ues.length, 2); + assertEquals(typeof ues[0].id, "number"); + assertEquals(typeof ues[0].nom, "string"); +}); + +// --- Mock API --- + +Deno.test("mock API: GET /ues returns list", async () => { + mockFetch({ "/ues": ues }); + try { + const res = await fetch("http://localhost/api/ues"); + assertEquals(res.status, 200); + const data: UE[] = await res.json(); + assertEquals(data.length, 2); + assertExists(data.find((u) => u.nom === "UE Informatique")); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: GET /ues/:id returns one UE", async () => { + mockFetch({ "/ues/1": ues[0] }); + try { + const res = await fetch("http://localhost/api/ues/1"); + assertEquals(res.status, 200); + const data: UE = await res.json(); + assertEquals(data.id, 1); + assertEquals(data.nom, "UE Informatique"); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: GET /ues/:id 404 when not found", async () => { + mockFetch({ "/ues/99": { status: 404, body: { error: "Ressource introuvable" } } }); + try { + const res = await fetch("http://localhost/api/ues/99"); + assertEquals(res.status, 404); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: POST /ues creates UE (201)", async () => { + const newUE: UE = { id: 3, nom: "UE Physique" }; + mockFetch({ "/ues": { method: "POST", status: 201, body: newUE } }); + try { + const res = await fetch("http://localhost/api/ues", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ nom: "UE Physique" }), + }); + assertEquals(res.status, 201); + const data: UE = await res.json(); + assertEquals(data.nom, "UE Physique"); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: POST /ues 400 on missing nom", async () => { + mockFetch({ "/ues": { method: "POST", status: 400 } }); + try { + const res = await fetch("http://localhost/api/ues", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({}), + }); + assertEquals(res.status, 400); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: PUT /ues/:id updates nom", async () => { + const updated: UE = { id: 1, nom: "UE Informatique avancée" }; + mockFetch({ "/ues/1": { method: "PUT", status: 200, body: updated } }); + try { + const res = await fetch("http://localhost/api/ues/1", { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ nom: "UE Informatique avancée" }), + }); + assertEquals(res.status, 200); + const data: UE = await res.json(); + assertEquals(data.nom, "UE Informatique avancée"); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: PUT /ues/:id 404 when not found", async () => { + mockFetch({ "/ues/99": { method: "PUT", status: 404 } }); + try { + const res = await fetch("http://localhost/api/ues/99", { + method: "PUT", + body: JSON.stringify({ nom: "X" }), + }); + assertEquals(res.status, 404); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: DELETE /ues/:id returns 204", async () => { + mockFetch({ "/ues/1": { method: "DELETE", status: 204 } }); + try { + const res = await fetch("http://localhost/api/ues/1", { method: "DELETE" }); + assertEquals(res.status, 204); + } finally { + restoreFetch(); + } +}); + +Deno.test("mock API: DELETE /ues/:id 404 when not found", async () => { + mockFetch({ "/ues/99": { method: "DELETE", status: 404 } }); + try { + const res = await fetch("http://localhost/api/ues/99", { method: "DELETE" }); + assertEquals(res.status, 404); + } finally { + restoreFetch(); + } +}); + +// --- Mock DB --- + +Deno.test("mock DB: find UE by id", () => { + const db = createMockDb({ tables: { ues: [...ues] } }); + const u = db.findOne("ues", (u) => u.id === 1); + assertExists(u); + assertEquals(u.nom, "UE Informatique"); +}); + +Deno.test("mock DB: insert UE", () => { + const db = createMockDb({ tables: { ues: [...ues] } }); + db.insert("ues", { id: 3, nom: "UE Physique" }); + assertEquals(db.getTable("ues").length, 3); +}); + +Deno.test("mock DB: update UE nom", () => { + const db = createMockDb({ tables: { ues: [...ues] } }); + db.updateWhere("ues", (u) => u.id === 1, { nom: "Updated" }); + assertEquals(db.findOne("ues", (u) => u.id === 1)?.nom, "Updated"); +}); + +Deno.test("mock DB: delete UE", () => { + const db = createMockDb({ tables: { ues: [...ues] } }); + db.deleteWhere("ues", (u) => u.id === 1); + assertEquals(db.getTable("ues").length, 1); +}); -- 2.52.0 From b0930b8da257842564f07acdefbfcbe9cd8bd363 Mon Sep 17 00:00:00 2001 From: Djalim Simaila Date: Sun, 26 Apr 2026 19:01:53 +0200 Subject: [PATCH 2/3] fix: correct handler bugs exposed by test suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ajustements [numEtud]/[idUE]: fix .where() missing and() — PUT/DELETE were applying only numEtud condition, modifying all rows for a student - modules/users/enseignements POST: add try/catch, return 500 on invalid JSON - modules/[idModule] PUT: add try/catch + type check on nom (string required) - modules POST: add .trim() check to reject whitespace-only id/nom - users POST: add .trim() check to reject whitespace-only id/nom/prenom - ues POST: add .trim() check to reject whitespace-only nom - notes POST: add type check (typeof number) and bounds check (0 ≤ note ≤ 20) - ue-modules POST: add coeff >= 0 validation Update robustness tests to reflect fixed behavior (remove [BUG] labels, replace assertRejects with status code assertions). --- routes/(apps)/admin/api/enseignements.ts | 11 +- routes/(apps)/admin/api/modules.ts | 9 +- routes/(apps)/admin/api/modules/[idModule].ts | 11 +- routes/(apps)/admin/api/users.ts | 13 +- .../notes/api/ajustements/[numEtud]/[idUE].ts | 8 +- routes/(apps)/notes/api/notes.ts | 6 + routes/(apps)/notes/api/ue-modules.ts | 6 + routes/(apps)/notes/api/ues.ts | 2 +- tests/e2e/robustness_test.ts | 199 ++++++++++-------- 9 files changed, 166 insertions(+), 99 deletions(-) diff --git a/routes/(apps)/admin/api/enseignements.ts b/routes/(apps)/admin/api/enseignements.ts index 06408bc..cb2ab47 100644 --- a/routes/(apps)/admin/api/enseignements.ts +++ b/routes/(apps)/admin/api/enseignements.ts @@ -26,11 +26,12 @@ export const handler: Handlers = { return FORBIDDEN; } - const body: { - idProf: string; - idModule: string; - idPromo: string; - } = await request.json(); + let body: { idProf: string; idModule: string; idPromo: string }; + try { + body = await request.json(); + } catch { + return new Response(null, { status: 500 }); + } if (!body.idProf || !body.idModule || !body.idPromo) { return new Response(null, { status: 400 }); diff --git a/routes/(apps)/admin/api/modules.ts b/routes/(apps)/admin/api/modules.ts index 2cb2fe7..bdb37b9 100644 --- a/routes/(apps)/admin/api/modules.ts +++ b/routes/(apps)/admin/api/modules.ts @@ -31,9 +31,14 @@ export const handler: Handlers = { return new Response(null, { status: 403 }); } - const body: { id: string; nom: string } = await request.json(); + let body: { id: string; nom: string }; + try { + body = await request.json(); + } catch { + return new Response(null, { status: 500 }); + } - if (!body.id || !body.nom) { + if (!body.id || !body.id.trim() || !body.nom || !body.nom.trim()) { return new Response(null, { status: 400 }); } diff --git a/routes/(apps)/admin/api/modules/[idModule].ts b/routes/(apps)/admin/api/modules/[idModule].ts index 6f17dfe..d3d9467 100644 --- a/routes/(apps)/admin/api/modules/[idModule].ts +++ b/routes/(apps)/admin/api/modules/[idModule].ts @@ -33,7 +33,16 @@ export const handler: Handlers = { request: Request, context: FreshContext, ): Promise { - const body: { nom: string } = await request.json(); + let body: { nom: string }; + try { + body = await request.json(); + } catch { + return new Response(null, { status: 500 }); + } + + if (typeof body.nom !== "string") { + return new Response(null, { status: 400 }); + } const [updated] = await db .update(modules) diff --git a/routes/(apps)/admin/api/users.ts b/routes/(apps)/admin/api/users.ts index d2fbd56..61317d7 100644 --- a/routes/(apps)/admin/api/users.ts +++ b/routes/(apps)/admin/api/users.ts @@ -27,10 +27,17 @@ export const handler: Handlers = { request: Request, _context: FreshContext, ): Promise { - const body: { id: string; nom: string; prenom: string; idRole: number } = - await request.json(); + let body: { id: string; nom: string; prenom: string; idRole: number }; + try { + body = await request.json(); + } catch { + return new Response(null, { status: 500 }); + } - if (!body.id || !body.nom || !body.prenom) { + if ( + !body.id || !body.id.trim() || !body.nom || !body.nom.trim() || + !body.prenom || !body.prenom.trim() + ) { return new Response(null, { status: 400 }); } diff --git a/routes/(apps)/notes/api/ajustements/[numEtud]/[idUE].ts b/routes/(apps)/notes/api/ajustements/[numEtud]/[idUE].ts index c9b3ab0..a165f44 100644 --- a/routes/(apps)/notes/api/ajustements/[numEtud]/[idUE].ts +++ b/routes/(apps)/notes/api/ajustements/[numEtud]/[idUE].ts @@ -2,7 +2,7 @@ import { FreshContext, Handlers } from "$fresh/server.ts"; import { db } from "$root/databases/db.ts"; import { ajustements } from "$root/databases/schema.ts"; import { AuthenticatedState } from "$root/defaults/interfaces.ts"; -import { eq } from "npm:drizzle-orm@0.45.2"; +import { and, eq } from "npm:drizzle-orm@0.45.2"; const NOT_FOUND = new Response( JSON.stringify({ error: "Ajustement introuvable" }), @@ -31,7 +31,7 @@ export const handler: Handlers = { const ajustement = await db .select() .from(ajustements) - .where(eq(ajustements.numEtud, numEtud), eq(ajustements.idUE, idUE)) + .where(and(eq(ajustements.numEtud, numEtud), eq(ajustements.idUE, idUE))) .then((rows) => rows[0] ?? null); if (!ajustement) return NOT_FOUND; @@ -69,7 +69,7 @@ export const handler: Handlers = { const [updated] = await db .update(ajustements) .set({ valeur: body.valeur }) - .where(eq(ajustements.numEtud, numEtud), eq(ajustements.idUE, idUE)) + .where(and(eq(ajustements.numEtud, numEtud), eq(ajustements.idUE, idUE))) .returning(); if (!updated) return NOT_FOUND; @@ -97,7 +97,7 @@ export const handler: Handlers = { const [deleted] = await db .delete(ajustements) - .where(eq(ajustements.numEtud, numEtud), eq(ajustements.idUE, idUE)) + .where(and(eq(ajustements.numEtud, numEtud), eq(ajustements.idUE, idUE))) .returning(); if (!deleted) return NOT_FOUND; diff --git a/routes/(apps)/notes/api/notes.ts b/routes/(apps)/notes/api/notes.ts index 22d387e..b7fd580 100644 --- a/routes/(apps)/notes/api/notes.ts +++ b/routes/(apps)/notes/api/notes.ts @@ -49,6 +49,12 @@ export const handler: Handlers = { }); } + if (typeof note !== "number" || note < 0 || note > 20) { + return new Response("Champ 'note' doit être un nombre entre 0 et 20", { + status: 400, + }); + } + const result = await db.insert(notes).values({ note, numEtud, idModule }) .returning(); diff --git a/routes/(apps)/notes/api/ue-modules.ts b/routes/(apps)/notes/api/ue-modules.ts index 8cd48bc..1a825a6 100644 --- a/routes/(apps)/notes/api/ue-modules.ts +++ b/routes/(apps)/notes/api/ue-modules.ts @@ -47,6 +47,12 @@ export const handler: Handlers = { ); } + if (typeof coeff !== "number" || coeff < 0) { + return new Response("Champ 'coeff' doit être un nombre >= 0", { + status: 400, + }); + } + const result = await db.insert(ueModules).values({ idModule, idUE, diff --git a/routes/(apps)/notes/api/ues.ts b/routes/(apps)/notes/api/ues.ts index 757245c..92242da 100644 --- a/routes/(apps)/notes/api/ues.ts +++ b/routes/(apps)/notes/api/ues.ts @@ -24,7 +24,7 @@ export const handler: Handlers = { const body = await request.json(); const { nom } = body; - if (!nom) { + if (!nom || !nom.trim()) { return new Response("Champ 'nom' manquant", { status: 400 }); } diff --git a/tests/e2e/robustness_test.ts b/tests/e2e/robustness_test.ts index ba18a1d..fb5552b 100644 --- a/tests/e2e/robustness_test.ts +++ b/tests/e2e/robustness_test.ts @@ -4,7 +4,7 @@ // Les tests marqués [BUG] représentent le comportement ATTENDU — ils échouent // intentionnellement pour exposer un bug dans le handler ciblé. -import { assertEquals, assertRejects } from "@std/assert"; +import { assertEquals } from "@std/assert"; import { makeContextWithAffiliation, makeEmployeeContext, @@ -90,7 +90,8 @@ Deno.test({ }); Deno.test({ - name: "robustness: POST /ajustements malformed JSON → 500 (try/catch présent)", + name: + "robustness: POST /ajustements malformed JSON → 500 (try/catch présent)", async fn() { await truncateAll(); const res = await ajustementsHandler.POST!( @@ -103,49 +104,43 @@ Deno.test({ sanitizeOps: false, }); -// Handlers SANS try/catch — throwent au lieu de retourner 500 -// [BUG] Ces handlers devraient retourner 500, pas throw - Deno.test({ - name: "robustness [BUG]: POST /modules malformed JSON → throw (pas de try/catch)", + name: "robustness: POST /modules malformed JSON → 500", async fn() { await truncateAll(); - await assertRejects(() => - modulesHandler.POST!( - makeMalformedRequest("/modules"), - makeEmployeeContext(), - ) + const res = await modulesHandler.POST!( + makeMalformedRequest("/modules"), + makeEmployeeContext(), ); + assertEquals(res.status, 500); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ - name: "robustness [BUG]: POST /enseignements malformed JSON → throw (pas de try/catch)", + name: "robustness: POST /enseignements malformed JSON → 500", async fn() { await truncateAll(); - await assertRejects(() => - enseignementsHandler.POST!( - makeMalformedRequest("/enseignements"), - makeEmployeeContext(), - ) + const res = await enseignementsHandler.POST!( + makeMalformedRequest("/enseignements"), + makeEmployeeContext(), ); + assertEquals(res.status, 500); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ - name: "robustness [BUG]: POST /users malformed JSON → throw (pas de try/catch)", + name: "robustness: POST /users malformed JSON → 500", async fn() { await truncateAll(); - await assertRejects(() => - usersHandler.POST!( - makeMalformedRequest("/users"), - makeEmployeeContext(), - ) + const res = await usersHandler.POST!( + makeMalformedRequest("/users"), + makeEmployeeContext(), ); + assertEquals(res.status, 500); }, sanitizeResources: false, sanitizeOps: false, @@ -170,15 +165,14 @@ Deno.test({ }); Deno.test({ - name: "robustness [BUG]: POST /modules sans body → throw (pas de try/catch)", + name: "robustness: POST /modules sans body → 500", async fn() { await truncateAll(); - await assertRejects(() => - modulesHandler.POST!( - makeEmptyBodyRequest("/modules"), - makeEmployeeContext(), - ) + const res = await modulesHandler.POST!( + makeEmptyBodyRequest("/modules"), + makeEmployeeContext(), ); + assertEquals(res.status, 500); }, sanitizeResources: false, sanitizeOps: false, @@ -235,52 +229,42 @@ Deno.test({ // ============================================================================= Deno.test({ - name: "robustness [BUG]: POST /modules id=espaces → devrait être 400, retourne 201", + name: "robustness: POST /modules id=espaces → 400", async fn() { await truncateAll(); const res = await modulesHandler.POST!( makeJsonRequest("/modules", "POST", { id: " ", nom: "Test" }), makeEmployeeContext(), ); - // Le handler vérifie !body.id → " " est truthy → passe → s'insère - // Comportement attendu : 400 - // Comportement réel : 201 (bug : pas de trim()) - assertEquals(res.status, 400); // ← va échouer, expose le bug + assertEquals(res.status, 400); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ - name: "robustness [BUG]: POST /ues nom=espaces → devrait être 400, retourne 201", + name: "robustness: POST /ues nom=espaces → 400", async fn() { await truncateAll(); const res = await uesHandler.POST!( makeJsonRequest("/ues", "POST", { nom: " " }), makeEmployeeContext(), ); - assertEquals(res.status, 400); // ← va échouer, expose le bug + assertEquals(res.status, 400); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ - name: "robustness [BUG]: POST /users id=espaces → devrait être 400, retourne 201", + name: "robustness: POST /users id=espaces → 400", async fn() { await truncateAll(); - await assertRejects( - // sans try/catch + whitespace id → s'insère (ou throw si DB rejette) - // Dans tous les cas le handler ne valide pas correctement - async () => { - const res = await usersHandler.POST!( - makeJsonRequest("/users", "POST", { id: " ", nom: "X", prenom: "Y" }), - makeEmployeeContext(), - ); - // Si pas de throw : le handler a inséré des espaces en DB - assertEquals(res.status, 400); - }, + const res = await usersHandler.POST!( + makeJsonRequest("/users", "POST", { id: " ", nom: "X", prenom: "Y" }), + makeEmployeeContext(), ); + assertEquals(res.status, 400); }, sanitizeResources: false, sanitizeOps: false, @@ -291,36 +275,40 @@ Deno.test({ // ============================================================================= Deno.test({ - name: "robustness [BUG]: POST /notes note=string → devrait être 400, retourne 500", + name: "robustness: POST /notes note=string → 400", async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ nom: "Test", prenom: "User", idPromo: "P1" }]); + const [s] = await seedStudents([{ + nom: "Test", + prenom: "User", + idPromo: "P1", + }]); await seedModules([{ id: "M1", nom: "Mod" }]); const res = await notesHandler.POST!( - makeJsonRequest("/notes", "POST", { note: "pas-un-nombre", numEtud: s.numEtud, idModule: "M1" }), + makeJsonRequest("/notes", "POST", { + note: "pas-un-nombre", + numEtud: s.numEtud, + idModule: "M1", + }), makeEmployeeContext(), ); - // "pas-un-nombre" !== undefined → passe la validation → DB rejette → 500 - // Comportement attendu : 400 (validation de type) - // Comportement réel : 500 - assertEquals(res.status, 400); // ← va échouer, expose le bug + assertEquals(res.status, 400); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ - name: "robustness [BUG]: PUT /modules/:id nom=number → devrait être 400, throw ou insère", + name: "robustness: PUT /modules/:id nom=number → 400", async fn() { await truncateAll(); await seedModules([{ id: "M1", nom: "Mod" }]); - await assertRejects(() => - moduleHandler.PUT!( - makeJsonRequest("/modules/M1", "PUT", { nom: 42 }), - makeEmployeeContext({ idModule: "M1" }), - ) + const res = await moduleHandler.PUT!( + makeJsonRequest("/modules/M1", "PUT", { nom: 42 }), + makeEmployeeContext({ idModule: "M1" }), ); + assertEquals(res.status, 400); }, sanitizeResources: false, sanitizeOps: false, @@ -331,12 +319,17 @@ Deno.test({ // ============================================================================= Deno.test({ - name: "robustness [BUG]: POST /ajustements numEtud=0 → 400 pour mauvaise raison", + name: + "robustness [BUG]: POST /ajustements numEtud=0 → 400 pour mauvaise raison", async fn() { await truncateAll(); const [ue] = await seedUes([{ nom: "UE Info" }]); const res = await ajustementsHandler.POST!( - makeJsonRequest("/ajustements", "POST", { numEtud: 0, idUE: ue.id, valeur: 10.0 }), + makeJsonRequest("/ajustements", "POST", { + numEtud: 0, + idUE: ue.id, + valeur: 10.0, + }), makeEmployeeContext(), ); // !0 === true → retourne 400 à cause du falsy check, pas d'une vraie validation @@ -353,9 +346,17 @@ Deno.test({ async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ nom: "Test", prenom: "User", idPromo: "P1" }]); + const [s] = await seedStudents([{ + nom: "Test", + prenom: "User", + idPromo: "P1", + }]); const res = await ajustementsHandler.POST!( - makeJsonRequest("/ajustements", "POST", { numEtud: s.numEtud, idUE: 0, valeur: 10.0 }), + makeJsonRequest("/ajustements", "POST", { + numEtud: s.numEtud, + idUE: 0, + valeur: 10.0, + }), makeEmployeeContext(), ); assertEquals(res.status, 400); // !0 → 400, message trompeur @@ -369,14 +370,20 @@ Deno.test({ // ============================================================================= Deno.test({ - name: "robustness: POST /ue-modules coeff=0 → 201 (zéro est une valeur valide)", + name: + "robustness: POST /ue-modules coeff=0 → 201 (zéro est une valeur valide)", async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); await seedModules([{ id: "M1", nom: "Mod" }]); const [ue] = await seedUes([{ nom: "UE Info" }]); const res = await ueModulesHandler.POST!( - makeJsonRequest("/ue-modules", "POST", { idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: 0 }), + makeJsonRequest("/ue-modules", "POST", { + idModule: "M1", + idUE: ue.id, + idPromo: "P1", + coeff: 0, + }), makeEmployeeContext(), ); // coeff === undefined → false pour 0 → passe ✓ @@ -392,7 +399,8 @@ Deno.test({ // ============================================================================= Deno.test({ - name: "robustness: GET /modules avec SQL injection dans id → 404 (Drizzle paramètre)", + name: + "robustness: GET /modules avec SQL injection dans id → 404 (Drizzle paramètre)", async fn() { await truncateAll(); const injectionId = "'; DROP TABLE modules; --"; @@ -409,7 +417,8 @@ Deno.test({ }); Deno.test({ - name: "robustness: POST /modules avec SQL injection dans id → s'insère littéralement (safe)", + name: + "robustness: POST /modules avec SQL injection dans id → s'insère littéralement (safe)", async fn() { await truncateAll(); const injectionId = "'; DROP TABLE modules; --"; @@ -429,52 +438,72 @@ Deno.test({ // ============================================================================= Deno.test({ - name: "robustness [BUG]: POST /notes note > 20 → devrait être 400, retourne 201", + name: "robustness: POST /notes note > 20 → 400", async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ nom: "Test", prenom: "User", idPromo: "P1" }]); + const [s] = await seedStudents([{ + nom: "Test", + prenom: "User", + idPromo: "P1", + }]); await seedModules([{ id: "M1", nom: "Mod" }]); const res = await notesHandler.POST!( - makeJsonRequest("/notes", "POST", { note: 999, numEtud: s.numEtud, idModule: "M1" }), + makeJsonRequest("/notes", "POST", { + note: 999, + numEtud: s.numEtud, + idModule: "M1", + }), makeEmployeeContext(), ); - // Aucune validation de borne → 999 s'insère → 201 - assertEquals(res.status, 400); // ← va échouer, expose le manque de validation métier + assertEquals(res.status, 400); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ - name: "robustness [BUG]: POST /notes note < 0 → devrait être 400, retourne 201", + name: "robustness: POST /notes note < 0 → 400", async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ nom: "Test", prenom: "User", idPromo: "P1" }]); + const [s] = await seedStudents([{ + nom: "Test", + prenom: "User", + idPromo: "P1", + }]); await seedModules([{ id: "M1", nom: "Mod" }]); const res = await notesHandler.POST!( - makeJsonRequest("/notes", "POST", { note: -5, numEtud: s.numEtud, idModule: "M1" }), + makeJsonRequest("/notes", "POST", { + note: -5, + numEtud: s.numEtud, + idModule: "M1", + }), makeEmployeeContext(), ); - assertEquals(res.status, 400); // ← va échouer + assertEquals(res.status, 400); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ - name: "robustness [BUG]: POST /ue-modules coeff négatif → devrait être 400, retourne 201", + name: "robustness: POST /ue-modules coeff négatif → 400", async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); await seedModules([{ id: "M1", nom: "Mod" }]); const [ue] = await seedUes([{ nom: "UE Info" }]); const res = await ueModulesHandler.POST!( - makeJsonRequest("/ue-modules", "POST", { idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: -3 }), + makeJsonRequest("/ue-modules", "POST", { + idModule: "M1", + idUE: ue.id, + idPromo: "P1", + coeff: -3, + }), makeEmployeeContext(), ); - assertEquals(res.status, 400); // ← va échouer + assertEquals(res.status, 400); }, sanitizeResources: false, sanitizeOps: false, @@ -491,7 +520,10 @@ Deno.test({ // Ce test crée un module await truncateAll(); await modulesHandler.POST!( - makeJsonRequest("/modules", "POST", { id: "ISOLATION-TEST", nom: "Test" }), + makeJsonRequest("/modules", "POST", { + id: "ISOLATION-TEST", + nom: "Test", + }), makeEmployeeContext(), ); }, @@ -500,7 +532,8 @@ Deno.test({ }); Deno.test({ - name: "robustness: isolation — truncateAll efface bien les données du test précédent", + name: + "robustness: isolation — truncateAll efface bien les données du test précédent", async fn() { await truncateAll(); // Le module créé dans le test précédent ne doit plus exister -- 2.52.0 From 714486f43c1e72b8c63878cbee7f9177cba9654f Mon Sep 17 00:00:00 2001 From: Djalim Simaila Date: Sun, 26 Apr 2026 19:07:15 +0200 Subject: [PATCH 3/3] chore: formated tests --- tests/e2e/ajustements_test.ts | 84 +++++++++++++++---- tests/e2e/enseignements_test.ts | 93 ++++++++++++++++----- tests/e2e/notes_test.ts | 59 +++++++++++--- tests/e2e/ue_modules_test.ts | 75 +++++++++++++---- tests/e2e/ues_test.ts | 14 +++- tests/e2e/users_test.ts | 34 ++++++-- tests/integration/ajustements_test.ts | 55 ++++++++++--- tests/integration/enseignements_test.ts | 21 ++++- tests/integration/notes_test.ts | 46 +++++++++-- tests/integration/ue_modules_test.ts | 71 +++++++++++++--- tests/integration/ues_test.ts | 19 +++-- tests/unit/ajustements_test.ts | 59 +++++++++++--- tests/unit/enseignements_test.ts | 104 +++++++++++++++++++----- tests/unit/notes_test.ts | 46 +++++++++-- tests/unit/ue_modules_test.ts | 87 +++++++++++++++----- tests/unit/ues_test.ts | 8 +- 16 files changed, 699 insertions(+), 176 deletions(-) diff --git a/tests/e2e/ajustements_test.ts b/tests/e2e/ajustements_test.ts index 8b07a04..2ca2ef7 100644 --- a/tests/e2e/ajustements_test.ts +++ b/tests/e2e/ajustements_test.ts @@ -26,10 +26,17 @@ Deno.test({ async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ nom: "Dupont", prenom: "Jean", idPromo: "P1" }]); + const [s] = await seedStudents([{ + nom: "Dupont", + prenom: "Jean", + idPromo: "P1", + }]); const [ue] = await seedUes([{ nom: "UE Info" }]); await seedAjustements([{ numEtud: s.numEtud, idUE: ue.id, valeur: 13.0 }]); - const res = await ajustementsHandler.GET!(makeGetRequest("/ajustements"), makeEmployeeContext()); + const res = await ajustementsHandler.GET!( + makeGetRequest("/ajustements"), + makeEmployeeContext(), + ); assertEquals(res.status, 200); const body = await res.json(); assertEquals(body.length, 1); @@ -43,8 +50,16 @@ Deno.test({ async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); - const [s1] = await seedStudents([{ nom: "Dupont", prenom: "Jean", idPromo: "P1" }]); - const [s2] = await seedStudents([{ nom: "Martin", prenom: "Alice", idPromo: "P1" }]); + const [s1] = await seedStudents([{ + nom: "Dupont", + prenom: "Jean", + idPromo: "P1", + }]); + const [s2] = await seedStudents([{ + nom: "Martin", + prenom: "Alice", + idPromo: "P1", + }]); const [ue] = await seedUes([{ nom: "UE Info" }]); await seedAjustements([ { numEtud: s1.numEtud, idUE: ue.id, valeur: 13.0 }, @@ -80,14 +95,23 @@ Deno.test({ // --- POST /ajustements --- Deno.test({ - name: "e2e ajustements: POST /ajustements creates ajustement (201) as employee", + name: + "e2e ajustements: POST /ajustements creates ajustement (201) as employee", async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ nom: "Leroy", prenom: "Paul", idPromo: "P1" }]); + const [s] = await seedStudents([{ + nom: "Leroy", + prenom: "Paul", + idPromo: "P1", + }]); const [ue] = await seedUes([{ nom: "UE Info" }]); const res = await ajustementsHandler.POST!( - makeJsonRequest("/ajustements", "POST", { numEtud: s.numEtud, idUE: ue.id, valeur: 14.5 }), + makeJsonRequest("/ajustements", "POST", { + numEtud: s.numEtud, + idUE: ue.id, + valeur: 14.5, + }), makeEmployeeContext(), ); assertEquals(res.status, 201); @@ -104,7 +128,11 @@ Deno.test({ async fn() { await truncateAll(); const res = await ajustementsHandler.POST!( - makeJsonRequest("/ajustements", "POST", { numEtud: 1, idUE: 1, valeur: 10.0 }), + makeJsonRequest("/ajustements", "POST", { + numEtud: 1, + idUE: 1, + valeur: 10.0, + }), makeContextWithAffiliation("student"), ); assertEquals(res.status, 403); @@ -130,7 +158,8 @@ Deno.test({ // --- GET /ajustements/:numEtud/:idUE --- Deno.test({ - name: "e2e ajustements: GET /ajustements/:numEtud/:idUE returns correct ajustement (employee)", + name: + "e2e ajustements: GET /ajustements/:numEtud/:idUE returns correct ajustement (employee)", async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); @@ -147,7 +176,10 @@ Deno.test({ ]); const res = await ajustementHandler.GET!( makeGetRequest(`/ajustements/${s1.numEtud}/${ue1.id}`), - makeEmployeeContext({ numEtud: String(s1.numEtud), idUE: String(ue1.id) }), + makeEmployeeContext({ + numEtud: String(s1.numEtud), + idUE: String(ue1.id), + }), ); assertEquals(res.status, 200); const body = await res.json(); @@ -189,19 +221,28 @@ Deno.test({ // --- PUT /ajustements/:numEtud/:idUE --- Deno.test({ - name: "e2e ajustements: PUT /ajustements/:numEtud/:idUE updates only targeted row (employee)", + name: + "e2e ajustements: PUT /ajustements/:numEtud/:idUE updates only targeted row (employee)", async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ nom: "Thomas", prenom: "Eva", idPromo: "P1" }]); - const [ue1, ue2] = await seedUes([{ nom: "UE Physique" }, { nom: "UE Chimie" }]); + const [s] = await seedStudents([{ + nom: "Thomas", + prenom: "Eva", + idPromo: "P1", + }]); + const [ue1, ue2] = await seedUes([{ nom: "UE Physique" }, { + nom: "UE Chimie", + }]); // Deux ajustements pour le même étudiant — seul ue1 doit être modifié await seedAjustements([ { numEtud: s.numEtud, idUE: ue1.id, valeur: 10.0 }, { numEtud: s.numEtud, idUE: ue2.id, valeur: 7.0 }, ]); const res = await ajustementHandler.PUT!( - makeJsonRequest(`/ajustements/${s.numEtud}/${ue1.id}`, "PUT", { valeur: 19.0 }), + makeJsonRequest(`/ajustements/${s.numEtud}/${ue1.id}`, "PUT", { + valeur: 19.0, + }), makeEmployeeContext({ numEtud: String(s.numEtud), idUE: String(ue1.id) }), ); assertEquals(res.status, 200); @@ -247,11 +288,16 @@ Deno.test({ // --- DELETE /ajustements/:numEtud/:idUE --- Deno.test({ - name: "e2e ajustements: DELETE /ajustements/:numEtud/:idUE deletes only targeted row (employee)", + name: + "e2e ajustements: DELETE /ajustements/:numEtud/:idUE deletes only targeted row (employee)", async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ nom: "Petit", prenom: "Hugo", idPromo: "P1" }]); + const [s] = await seedStudents([{ + nom: "Petit", + prenom: "Hugo", + idPromo: "P1", + }]); const [ue1, ue2] = await seedUes([{ nom: "UE Chimie" }, { nom: "UE Bio" }]); // Deux ajustements pour le même étudiant — seul ue1 doit être supprimé await seedAjustements([ @@ -273,7 +319,8 @@ Deno.test({ }); Deno.test({ - name: "e2e ajustements: DELETE /ajustements/:numEtud/:idUE 403 for non-employee", + name: + "e2e ajustements: DELETE /ajustements/:numEtud/:idUE 403 for non-employee", async fn() { await truncateAll(); const res = await ajustementHandler.DELETE!( @@ -287,7 +334,8 @@ Deno.test({ }); Deno.test({ - name: "e2e ajustements: DELETE /ajustements/:numEtud/:idUE 404 when not found", + name: + "e2e ajustements: DELETE /ajustements/:numEtud/:idUE 404 when not found", async fn() { await truncateAll(); const res = await ajustementHandler.DELETE!( diff --git a/tests/e2e/enseignements_test.ts b/tests/e2e/enseignements_test.ts index 77751f4..32c9326 100644 --- a/tests/e2e/enseignements_test.ts +++ b/tests/e2e/enseignements_test.ts @@ -20,14 +20,19 @@ import { handler as enseignementHandler } from "$apps/admin/api/enseignements/[i // --- POST /enseignements --- Deno.test({ - name: "e2e enseignements: POST /enseignements creates enseignement (201) as employee", + name: + "e2e enseignements: POST /enseignements creates enseignement (201) as employee", async fn() { await truncateAll(); await seedUsers([{ id: "prof.dupont", nom: "Dupont", prenom: "Jean" }]); await seedModules([{ id: "M1", nom: "Mod A" }]); await seedPromotions([{ id: "P1" }]); const res = await enseignementsHandler.POST!( - makeJsonRequest("/enseignements", "POST", { idProf: "prof.dupont", idModule: "M1", idPromo: "P1" }), + makeJsonRequest("/enseignements", "POST", { + idProf: "prof.dupont", + idModule: "M1", + idPromo: "P1", + }), makeEmployeeContext(), ); assertEquals(res.status, 201); @@ -44,7 +49,11 @@ Deno.test({ async fn() { await truncateAll(); const res = await enseignementsHandler.POST!( - makeJsonRequest("/enseignements", "POST", { idProf: "p", idModule: "M1", idPromo: "P1" }), + makeJsonRequest("/enseignements", "POST", { + idProf: "p", + idModule: "M1", + idPromo: "P1", + }), makeContextWithAffiliation("student"), ); assertEquals(res.status, 403); @@ -74,9 +83,17 @@ Deno.test({ await seedUsers([{ id: "prof.dupont", nom: "Dupont", prenom: "Jean" }]); await seedModules([{ id: "M1", nom: "Mod A" }]); await seedPromotions([{ id: "P1" }]); - await seedEnseignements([{ idProf: "prof.dupont", idModule: "M1", idPromo: "P1" }]); + await seedEnseignements([{ + idProf: "prof.dupont", + idModule: "M1", + idPromo: "P1", + }]); const res = await enseignementsHandler.POST!( - makeJsonRequest("/enseignements", "POST", { idProf: "prof.dupont", idModule: "M1", idPromo: "P1" }), + makeJsonRequest("/enseignements", "POST", { + idProf: "prof.dupont", + idModule: "M1", + idPromo: "P1", + }), makeEmployeeContext(), ); assertEquals(res.status, 409); @@ -88,16 +105,25 @@ Deno.test({ // --- GET /enseignements/:idProf/:idModule/:idPromo --- Deno.test({ - name: "e2e enseignements: GET /enseignements/:idProf/:idModule/:idPromo returns enseignement (employee)", + name: + "e2e enseignements: GET /enseignements/:idProf/:idModule/:idPromo returns enseignement (employee)", async fn() { await truncateAll(); await seedUsers([{ id: "prof.dupont", nom: "Dupont", prenom: "Jean" }]); await seedModules([{ id: "M1", nom: "Mod A" }]); await seedPromotions([{ id: "P1" }]); - await seedEnseignements([{ idProf: "prof.dupont", idModule: "M1", idPromo: "P1" }]); + await seedEnseignements([{ + idProf: "prof.dupont", + idModule: "M1", + idPromo: "P1", + }]); const res = await enseignementHandler.GET!( makeGetRequest("/enseignements/prof.dupont/M1/P1"), - makeEmployeeContext({ idProf: "prof.dupont", idModule: "M1", idPromo: "P1" }), + makeEmployeeContext({ + idProf: "prof.dupont", + idModule: "M1", + idPromo: "P1", + }), ); assertEquals(res.status, 200); const body = await res.json(); @@ -109,12 +135,17 @@ Deno.test({ }); Deno.test({ - name: "e2e enseignements: GET /enseignements/:idProf/:idModule/:idPromo 403 for non-employee", + name: + "e2e enseignements: GET /enseignements/:idProf/:idModule/:idPromo 403 for non-employee", async fn() { await truncateAll(); const res = await enseignementHandler.GET!( makeGetRequest("/enseignements/p/M1/P1"), - makeContextWithAffiliation("student", { idProf: "p", idModule: "M1", idPromo: "P1" }), + makeContextWithAffiliation("student", { + idProf: "p", + idModule: "M1", + idPromo: "P1", + }), ); assertEquals(res.status, 403); }, @@ -123,12 +154,17 @@ Deno.test({ }); Deno.test({ - name: "e2e enseignements: GET /enseignements/:idProf/:idModule/:idPromo 404 when not found", + name: + "e2e enseignements: GET /enseignements/:idProf/:idModule/:idPromo 404 when not found", async fn() { await truncateAll(); const res = await enseignementHandler.GET!( makeGetRequest("/enseignements/ghost/GHOST/GHOST"), - makeEmployeeContext({ idProf: "ghost", idModule: "GHOST", idPromo: "GHOST" }), + makeEmployeeContext({ + idProf: "ghost", + idModule: "GHOST", + idPromo: "GHOST", + }), ); assertEquals(res.status, 404); }, @@ -139,16 +175,25 @@ Deno.test({ // --- DELETE /enseignements/:idProf/:idModule/:idPromo --- Deno.test({ - name: "e2e enseignements: DELETE /enseignements/:idProf/:idModule/:idPromo returns 204 (employee)", + name: + "e2e enseignements: DELETE /enseignements/:idProf/:idModule/:idPromo returns 204 (employee)", async fn() { await truncateAll(); await seedUsers([{ id: "prof.dupont", nom: "Dupont", prenom: "Jean" }]); await seedModules([{ id: "M1", nom: "Mod A" }]); await seedPromotions([{ id: "P1" }]); - await seedEnseignements([{ idProf: "prof.dupont", idModule: "M1", idPromo: "P1" }]); + await seedEnseignements([{ + idProf: "prof.dupont", + idModule: "M1", + idPromo: "P1", + }]); const res = await enseignementHandler.DELETE!( makeGetRequest("/enseignements/prof.dupont/M1/P1"), - makeEmployeeContext({ idProf: "prof.dupont", idModule: "M1", idPromo: "P1" }), + makeEmployeeContext({ + idProf: "prof.dupont", + idModule: "M1", + idPromo: "P1", + }), ); assertEquals(res.status, 204); }, @@ -157,12 +202,17 @@ Deno.test({ }); Deno.test({ - name: "e2e enseignements: DELETE /enseignements/:idProf/:idModule/:idPromo 403 for non-employee", + name: + "e2e enseignements: DELETE /enseignements/:idProf/:idModule/:idPromo 403 for non-employee", async fn() { await truncateAll(); const res = await enseignementHandler.DELETE!( makeGetRequest("/enseignements/p/M1/P1"), - makeContextWithAffiliation("student", { idProf: "p", idModule: "M1", idPromo: "P1" }), + makeContextWithAffiliation("student", { + idProf: "p", + idModule: "M1", + idPromo: "P1", + }), ); assertEquals(res.status, 403); }, @@ -171,12 +221,17 @@ Deno.test({ }); Deno.test({ - name: "e2e enseignements: DELETE /enseignements/:idProf/:idModule/:idPromo 404 when not found", + name: + "e2e enseignements: DELETE /enseignements/:idProf/:idModule/:idPromo 404 when not found", async fn() { await truncateAll(); const res = await enseignementHandler.DELETE!( makeGetRequest("/enseignements/ghost/GHOST/GHOST"), - makeEmployeeContext({ idProf: "ghost", idModule: "GHOST", idPromo: "GHOST" }), + makeEmployeeContext({ + idProf: "ghost", + idModule: "GHOST", + idPromo: "GHOST", + }), ); assertEquals(res.status, 404); }, diff --git a/tests/e2e/notes_test.ts b/tests/e2e/notes_test.ts index 78be912..ee1f491 100644 --- a/tests/e2e/notes_test.ts +++ b/tests/e2e/notes_test.ts @@ -23,13 +23,20 @@ Deno.test({ async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ nom: "Dupont", prenom: "Jean", idPromo: "P1" }]); + const [s] = await seedStudents([{ + nom: "Dupont", + prenom: "Jean", + idPromo: "P1", + }]); await seedModules([{ id: "M1", nom: "Mod A" }, { id: "M2", nom: "Mod B" }]); await seedNotes([ { numEtud: s.numEtud, idModule: "M1", note: 15.0 }, { numEtud: s.numEtud, idModule: "M2", note: 12.0 }, ]); - const res = await notesHandler.GET!(makeGetRequest("/notes"), makeEmployeeContext()); + const res = await notesHandler.GET!( + makeGetRequest("/notes"), + makeEmployeeContext(), + ); assertEquals(res.status, 200); const body = await res.json(); assertEquals(body.length, 2); @@ -43,8 +50,16 @@ Deno.test({ async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); - const [s1] = await seedStudents([{ nom: "Dupont", prenom: "Jean", idPromo: "P1" }]); - const [s2] = await seedStudents([{ nom: "Martin", prenom: "Alice", idPromo: "P1" }]); + const [s1] = await seedStudents([{ + nom: "Dupont", + prenom: "Jean", + idPromo: "P1", + }]); + const [s2] = await seedStudents([{ + nom: "Martin", + prenom: "Alice", + idPromo: "P1", + }]); await seedModules([{ id: "M1", nom: "Mod A" }]); await seedNotes([ { numEtud: s1.numEtud, idModule: "M1", note: 15.0 }, @@ -82,7 +97,11 @@ Deno.test({ async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ nom: "Dupont", prenom: "Jean", idPromo: "P1" }]); + const [s] = await seedStudents([{ + nom: "Dupont", + prenom: "Jean", + idPromo: "P1", + }]); await seedModules([{ id: "M1", nom: "Mod A" }, { id: "M2", nom: "Mod B" }]); await seedNotes([ { numEtud: s.numEtud, idModule: "M1", note: 15.0 }, @@ -108,10 +127,18 @@ Deno.test({ async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ nom: "Leroy", prenom: "Paul", idPromo: "P1" }]); + const [s] = await seedStudents([{ + nom: "Leroy", + prenom: "Paul", + idPromo: "P1", + }]); await seedModules([{ id: "M1", nom: "Mod A" }]); const res = await notesHandler.POST!( - makeJsonRequest("/notes", "POST", { numEtud: s.numEtud, idModule: "M1", note: 14.0 }), + makeJsonRequest("/notes", "POST", { + numEtud: s.numEtud, + idModule: "M1", + note: 14.0, + }), makeEmployeeContext(), ); assertEquals(res.status, 201); @@ -144,7 +171,11 @@ Deno.test({ async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ nom: "Bernard", prenom: "Lucie", idPromo: "P1" }]); + const [s] = await seedStudents([{ + nom: "Bernard", + prenom: "Lucie", + idPromo: "P1", + }]); await seedModules([{ id: "M1", nom: "Mod A" }]); await seedNotes([{ numEtud: s.numEtud, idModule: "M1", note: 18.0 }]); const res = await noteHandler.GET!( @@ -180,7 +211,11 @@ Deno.test({ async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ nom: "Thomas", prenom: "Eva", idPromo: "P1" }]); + const [s] = await seedStudents([{ + nom: "Thomas", + prenom: "Eva", + idPromo: "P1", + }]); await seedModules([{ id: "M1", nom: "Mod A" }]); await seedNotes([{ numEtud: s.numEtud, idModule: "M1", note: 10.0 }]); const res = await noteHandler.PUT!( @@ -216,7 +251,11 @@ Deno.test({ async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ nom: "Petit", prenom: "Hugo", idPromo: "P1" }]); + const [s] = await seedStudents([{ + nom: "Petit", + prenom: "Hugo", + idPromo: "P1", + }]); await seedModules([{ id: "M1", nom: "Mod A" }]); await seedNotes([{ numEtud: s.numEtud, idModule: "M1", note: 9.0 }]); const res = await noteHandler.DELETE!( diff --git a/tests/e2e/ue_modules_test.ts b/tests/e2e/ue_modules_test.ts index 028dfa8..3a921f8 100644 --- a/tests/e2e/ue_modules_test.ts +++ b/tests/e2e/ue_modules_test.ts @@ -32,7 +32,10 @@ Deno.test({ { idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: 2.0 }, { idModule: "M2", idUE: ue.id, idPromo: "P1", coeff: 3.0 }, ]); - const res = await ueModulesHandler.GET!(makeGetRequest("/ue-modules"), makeEmployeeContext()); + const res = await ueModulesHandler.GET!( + makeGetRequest("/ue-modules"), + makeEmployeeContext(), + ); assertEquals(res.status, 200); const body = await res.json(); assertEquals(body.length, 2); @@ -75,7 +78,12 @@ Deno.test({ await seedModules([{ id: "M1", nom: "Mod A" }]); const [ue] = await seedUes([{ nom: "UE Info" }]); const res = await ueModulesHandler.POST!( - makeJsonRequest("/ue-modules", "POST", { idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: 4.0 }), + makeJsonRequest("/ue-modules", "POST", { + idModule: "M1", + idUE: ue.id, + idPromo: "P1", + coeff: 4.0, + }), makeEmployeeContext(), ); assertEquals(res.status, 201); @@ -104,7 +112,8 @@ Deno.test({ // --- GET /ue-modules/:idModule/:idUE/:idPromo --- Deno.test({ - name: "e2e ue_modules: GET /ue-modules/:idModule/:idUE/:idPromo returns correct association (employee)", + name: + "e2e ue_modules: GET /ue-modules/:idModule/:idUE/:idPromo returns correct association (employee)", async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }, { id: "P2" }]); @@ -119,7 +128,11 @@ Deno.test({ ]); const res = await ueModuleHandler.GET!( makeGetRequest(`/ue-modules/M1/${ue1.id}/P1`), - makeEmployeeContext({ idModule: "M1", idUE: String(ue1.id), idPromo: "P1" }), + makeEmployeeContext({ + idModule: "M1", + idUE: String(ue1.id), + idPromo: "P1", + }), ); assertEquals(res.status, 200); const body = await res.json(); @@ -132,12 +145,17 @@ Deno.test({ }); Deno.test({ - name: "e2e ue_modules: GET /ue-modules/:idModule/:idUE/:idPromo 403 for non-employee", + name: + "e2e ue_modules: GET /ue-modules/:idModule/:idUE/:idPromo 403 for non-employee", async fn() { await truncateAll(); const res = await ueModuleHandler.GET!( makeGetRequest("/ue-modules/M1/1/P1"), - makeContextWithAffiliation("student", { idModule: "M1", idUE: "1", idPromo: "P1" }), + makeContextWithAffiliation("student", { + idModule: "M1", + idUE: "1", + idPromo: "P1", + }), ); assertEquals(res.status, 403); }, @@ -146,7 +164,8 @@ Deno.test({ }); Deno.test({ - name: "e2e ue_modules: GET /ue-modules/:idModule/:idUE/:idPromo 404 when not found", + name: + "e2e ue_modules: GET /ue-modules/:idModule/:idUE/:idPromo 404 when not found", async fn() { await truncateAll(); const res = await ueModuleHandler.GET!( @@ -162,7 +181,8 @@ Deno.test({ // --- PUT /ue-modules/:idModule/:idUE/:idPromo --- Deno.test({ - name: "e2e ue_modules: PUT /ue-modules/:idModule/:idUE/:idPromo updates only the targeted row (employee)", + name: + "e2e ue_modules: PUT /ue-modules/:idModule/:idUE/:idPromo updates only the targeted row (employee)", async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }, { id: "P2" }]); @@ -175,7 +195,11 @@ Deno.test({ ]); const res = await ueModuleHandler.PUT!( makeJsonRequest(`/ue-modules/M1/${ue1.id}/P1`, "PUT", { coeff: 5.0 }), - makeEmployeeContext({ idModule: "M1", idUE: String(ue1.id), idPromo: "P1" }), + makeEmployeeContext({ + idModule: "M1", + idUE: String(ue1.id), + idPromo: "P1", + }), ); assertEquals(res.status, 200); const body = await res.json(); @@ -187,12 +211,17 @@ Deno.test({ }); Deno.test({ - name: "e2e ue_modules: PUT /ue-modules/:idModule/:idUE/:idPromo 403 for non-employee", + name: + "e2e ue_modules: PUT /ue-modules/:idModule/:idUE/:idPromo 403 for non-employee", async fn() { await truncateAll(); const res = await ueModuleHandler.PUT!( makeJsonRequest("/ue-modules/M1/1/P1", "PUT", { coeff: 5.0 }), - makeContextWithAffiliation("student", { idModule: "M1", idUE: "1", idPromo: "P1" }), + makeContextWithAffiliation("student", { + idModule: "M1", + idUE: "1", + idPromo: "P1", + }), ); assertEquals(res.status, 403); }, @@ -201,7 +230,8 @@ Deno.test({ }); Deno.test({ - name: "e2e ue_modules: PUT /ue-modules/:idModule/:idUE/:idPromo 404 when not found", + name: + "e2e ue_modules: PUT /ue-modules/:idModule/:idUE/:idPromo 404 when not found", async fn() { await truncateAll(); const res = await ueModuleHandler.PUT!( @@ -217,7 +247,8 @@ Deno.test({ // --- DELETE /ue-modules/:idModule/:idUE/:idPromo --- Deno.test({ - name: "e2e ue_modules: DELETE /ue-modules/:idModule/:idUE/:idPromo deletes only targeted row (employee)", + name: + "e2e ue_modules: DELETE /ue-modules/:idModule/:idUE/:idPromo deletes only targeted row (employee)", async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }, { id: "P2" }]); @@ -230,7 +261,11 @@ Deno.test({ ]); const res = await ueModuleHandler.DELETE!( makeGetRequest(`/ue-modules/M1/${ue1.id}/P1`), - makeEmployeeContext({ idModule: "M1", idUE: String(ue1.id), idPromo: "P1" }), + makeEmployeeContext({ + idModule: "M1", + idUE: String(ue1.id), + idPromo: "P1", + }), ); assertEquals(res.status, 204); // L'autre ligne doit toujours exister @@ -243,12 +278,17 @@ Deno.test({ }); Deno.test({ - name: "e2e ue_modules: DELETE /ue-modules/:idModule/:idUE/:idPromo 403 for non-employee", + name: + "e2e ue_modules: DELETE /ue-modules/:idModule/:idUE/:idPromo 403 for non-employee", async fn() { await truncateAll(); const res = await ueModuleHandler.DELETE!( makeGetRequest("/ue-modules/M1/1/P1"), - makeContextWithAffiliation("student", { idModule: "M1", idUE: "1", idPromo: "P1" }), + makeContextWithAffiliation("student", { + idModule: "M1", + idUE: "1", + idPromo: "P1", + }), ); assertEquals(res.status, 403); }, @@ -257,7 +297,8 @@ Deno.test({ }); Deno.test({ - name: "e2e ue_modules: DELETE /ue-modules/:idModule/:idUE/:idPromo 404 when not found", + name: + "e2e ue_modules: DELETE /ue-modules/:idModule/:idUE/:idPromo 404 when not found", async fn() { await truncateAll(); const res = await ueModuleHandler.DELETE!( diff --git a/tests/e2e/ues_test.ts b/tests/e2e/ues_test.ts index a57249b..1797f8d 100644 --- a/tests/e2e/ues_test.ts +++ b/tests/e2e/ues_test.ts @@ -17,7 +17,10 @@ Deno.test({ async fn() { await truncateAll(); await seedUes([{ nom: "UE Informatique" }, { nom: "UE Mathématiques" }]); - const res = await uesHandler.GET!(makeGetRequest("/ues"), makeEmployeeContext()); + const res = await uesHandler.GET!( + makeGetRequest("/ues"), + makeEmployeeContext(), + ); assertEquals(res.status, 200); const body = await res.json(); assertEquals(body.length, 2); @@ -30,7 +33,10 @@ Deno.test({ name: "e2e ues: GET /ues returns empty when no UEs", async fn() { await truncateAll(); - const res = await uesHandler.GET!(makeGetRequest("/ues"), makeEmployeeContext()); + const res = await uesHandler.GET!( + makeGetRequest("/ues"), + makeEmployeeContext(), + ); assertEquals(res.status, 200); const body = await res.json(); assertEquals(body.length, 0); @@ -113,7 +119,9 @@ Deno.test({ await truncateAll(); const [ue] = await seedUes([{ nom: "UE Biologie" }]); const res = await ueHandler.PUT!( - makeJsonRequest(`/ues/${ue.id}`, "PUT", { nom: "UE Biologie moléculaire" }), + makeJsonRequest(`/ues/${ue.id}`, "PUT", { + nom: "UE Biologie moléculaire", + }), makeEmployeeContext({ idUE: String(ue.id) }), ); assertEquals(res.status, 200); diff --git a/tests/e2e/users_test.ts b/tests/e2e/users_test.ts index 038ed2f..830aefa 100644 --- a/tests/e2e/users_test.ts +++ b/tests/e2e/users_test.ts @@ -24,7 +24,10 @@ Deno.test({ { id: "dupont.jean", nom: "Dupont", prenom: "Jean" }, { id: "martin.alice", nom: "Martin", prenom: "Alice" }, ]); - const res = await usersHandler.GET!(makeGetRequest("/users"), makeEmployeeContext()); + const res = await usersHandler.GET!( + makeGetRequest("/users"), + makeEmployeeContext(), + ); assertEquals(res.status, 200); const body = await res.json(); assertEquals(body.length, 2); @@ -38,7 +41,10 @@ Deno.test({ name: "e2e users: GET /users returns empty when no users", async fn() { await truncateAll(); - const res = await usersHandler.GET!(makeGetRequest("/users"), makeEmployeeContext()); + const res = await usersHandler.GET!( + makeGetRequest("/users"), + makeEmployeeContext(), + ); assertEquals(res.status, 200); const body = await res.json(); assertEquals(body.length, 0); @@ -77,7 +83,11 @@ Deno.test({ async fn() { await truncateAll(); const res = await usersHandler.POST!( - makeJsonRequest("/users", "POST", { id: "new.user", nom: "New", prenom: "User" }), + makeJsonRequest("/users", "POST", { + id: "new.user", + nom: "New", + prenom: "User", + }), makeEmployeeContext(), ); assertEquals(res.status, 201); @@ -109,7 +119,11 @@ Deno.test({ await truncateAll(); await seedUsers([{ id: "dupont.jean", nom: "Dupont", prenom: "Jean" }]); const res = await usersHandler.POST!( - makeJsonRequest("/users", "POST", { id: "dupont.jean", nom: "Doublon", prenom: "X" }), + makeJsonRequest("/users", "POST", { + id: "dupont.jean", + nom: "Doublon", + prenom: "X", + }), makeEmployeeContext(), ); assertEquals(res.status, 409); @@ -160,7 +174,11 @@ Deno.test({ await truncateAll(); await seedUsers([{ id: "thomas.eva", nom: "Thomas", prenom: "Eva" }]); const res = await userHandler.PUT!( - makeJsonRequest("/users/thomas.eva", "PUT", { nom: "Thomas-Modifié", prenom: "Eva", idRole: null }), + makeJsonRequest("/users/thomas.eva", "PUT", { + nom: "Thomas-Modifié", + prenom: "Eva", + idRole: null, + }), makeEmployeeContext({ id: "thomas.eva" }), ); assertEquals(res.status, 200); @@ -176,7 +194,11 @@ Deno.test({ async fn() { await truncateAll(); const res = await userHandler.PUT!( - makeJsonRequest("/users/ghost.user", "PUT", { nom: "X", prenom: "Y", idRole: null }), + makeJsonRequest("/users/ghost.user", "PUT", { + nom: "X", + prenom: "Y", + idRole: null, + }), makeEmployeeContext({ id: "ghost.user" }), ); assertEquals(res.status, 404); diff --git a/tests/integration/ajustements_test.ts b/tests/integration/ajustements_test.ts index cd032a8..49e6fcd 100644 --- a/tests/integration/ajustements_test.ts +++ b/tests/integration/ajustements_test.ts @@ -17,7 +17,11 @@ Deno.test({ async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ nom: "Dupont", prenom: "Jean", idPromo: "P1" }]); + const [s] = await seedStudents([{ + nom: "Dupont", + prenom: "Jean", + idPromo: "P1", + }]); const [ue] = await seedUes([{ nom: "UE Info" }]); await seedAjustements([{ numEtud: s.numEtud, idUE: ue.id, valeur: 13.0 }]); const rows = await testDb.select().from(ajustements); @@ -32,7 +36,11 @@ Deno.test({ async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ nom: "Martin", prenom: "Alice", idPromo: "P1" }]); + const [s] = await seedStudents([{ + nom: "Martin", + prenom: "Alice", + idPromo: "P1", + }]); const [ue] = await seedUes([{ nom: "UE Maths" }]); const [created] = await testDb @@ -45,7 +53,9 @@ Deno.test({ const row = await testDb .select() .from(ajustements) - .where(and(eq(ajustements.numEtud, s.numEtud), eq(ajustements.idUE, ue.id))) + .where( + and(eq(ajustements.numEtud, s.numEtud), eq(ajustements.idUE, ue.id)), + ) .then((r) => r[0] ?? null); assertExists(row); assertEquals(row.valeur, 15.5); @@ -55,7 +65,8 @@ Deno.test({ }); Deno.test({ - name: "integration ajustements: get by composite key returns null when not found", + name: + "integration ajustements: get by composite key returns null when not found", async fn() { await truncateAll(); const row = await testDb @@ -74,11 +85,19 @@ Deno.test({ async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ nom: "Durand", prenom: "Claire", idPromo: "P1" }]); + const [s] = await seedStudents([{ + nom: "Durand", + prenom: "Claire", + idPromo: "P1", + }]); const [ue] = await seedUes([{ nom: "UE Info" }]); await seedAjustements([{ numEtud: s.numEtud, idUE: ue.id, valeur: 12.0 }]); await assertRejects(() => - testDb.insert(ajustements).values({ numEtud: s.numEtud, idUE: ue.id, valeur: 13.0 }) + testDb.insert(ajustements).values({ + numEtud: s.numEtud, + idUE: ue.id, + valeur: 13.0, + }) ); }, sanitizeResources: false, @@ -90,14 +109,20 @@ Deno.test({ async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ nom: "Bernard", prenom: "Lucie", idPromo: "P1" }]); + const [s] = await seedStudents([{ + nom: "Bernard", + prenom: "Lucie", + idPromo: "P1", + }]); const [ue] = await seedUes([{ nom: "UE Physique" }]); await seedAjustements([{ numEtud: s.numEtud, idUE: ue.id, valeur: 10.0 }]); const [updated] = await testDb .update(ajustements) .set({ valeur: 18.0 }) - .where(and(eq(ajustements.numEtud, s.numEtud), eq(ajustements.idUE, ue.id))) + .where( + and(eq(ajustements.numEtud, s.numEtud), eq(ajustements.idUE, ue.id)), + ) .returning(); assertEquals(updated.valeur, 18.0); }, @@ -110,15 +135,23 @@ Deno.test({ async fn() { await truncateAll(); await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ nom: "Thomas", prenom: "Eva", idPromo: "P1" }]); + const [s] = await seedStudents([{ + nom: "Thomas", + prenom: "Eva", + idPromo: "P1", + }]); const [ue] = await seedUes([{ nom: "UE Chimie" }]); await seedAjustements([{ numEtud: s.numEtud, idUE: ue.id, valeur: 11.0 }]); - await testDb.delete(ajustements).where(and(eq(ajustements.numEtud, s.numEtud), eq(ajustements.idUE, ue.id))); + await testDb.delete(ajustements).where( + and(eq(ajustements.numEtud, s.numEtud), eq(ajustements.idUE, ue.id)), + ); const row = await testDb .select() .from(ajustements) - .where(and(eq(ajustements.numEtud, s.numEtud), eq(ajustements.idUE, ue.id))) + .where( + and(eq(ajustements.numEtud, s.numEtud), eq(ajustements.idUE, ue.id)), + ) .then((r) => r[0] ?? null); assertEquals(row, null); }, diff --git a/tests/integration/enseignements_test.ts b/tests/integration/enseignements_test.ts index c48a312..40086a9 100644 --- a/tests/integration/enseignements_test.ts +++ b/tests/integration/enseignements_test.ts @@ -63,7 +63,8 @@ Deno.test({ }); Deno.test({ - name: "integration enseignements: get by composite key returns null when not found", + name: + "integration enseignements: get by composite key returns null when not found", async fn() { await truncateAll(); const row = await testDb @@ -90,9 +91,17 @@ Deno.test({ await seedUsers([{ id: "prof.dupont", nom: "Dupont", prenom: "Jean" }]); await seedModules([{ id: "M1", nom: "Mod A" }]); await seedPromotions([{ id: "P1" }]); - await seedEnseignements([{ idProf: "prof.dupont", idModule: "M1", idPromo: "P1" }]); + await seedEnseignements([{ + idProf: "prof.dupont", + idModule: "M1", + idPromo: "P1", + }]); await assertRejects(() => - testDb.insert(enseignements).values({ idProf: "prof.dupont", idModule: "M1", idPromo: "P1" }) + testDb.insert(enseignements).values({ + idProf: "prof.dupont", + idModule: "M1", + idPromo: "P1", + }) ); }, sanitizeResources: false, @@ -106,7 +115,11 @@ Deno.test({ await seedUsers([{ id: "prof.dupont", nom: "Dupont", prenom: "Jean" }]); await seedModules([{ id: "M1", nom: "Mod A" }]); await seedPromotions([{ id: "P1" }]); - await seedEnseignements([{ idProf: "prof.dupont", idModule: "M1", idPromo: "P1" }]); + await seedEnseignements([{ + idProf: "prof.dupont", + idModule: "M1", + idPromo: "P1", + }]); await testDb .delete(enseignements) diff --git a/tests/integration/notes_test.ts b/tests/integration/notes_test.ts index bae19b3..b9018b9 100644 --- a/tests/integration/notes_test.ts +++ b/tests/integration/notes_test.ts @@ -17,7 +17,11 @@ Deno.test({ async fn() { await truncateAll(); await seedPromotions([{ id: "PROMO-2024" }]); - const [s] = await seedStudents([{ nom: "Dupont", prenom: "Jean", idPromo: "PROMO-2024" }]); + const [s] = await seedStudents([{ + nom: "Dupont", + prenom: "Jean", + idPromo: "PROMO-2024", + }]); await seedModules([{ id: "MOD101", nom: "Module A" }]); await seedNotes([{ numEtud: s.numEtud, idModule: "MOD101", note: 15.5 }]); const rows = await testDb.select().from(notes); @@ -32,10 +36,18 @@ Deno.test({ async fn() { await truncateAll(); await seedPromotions([{ id: "PROMO-2024" }]); - const [s] = await seedStudents([{ nom: "Martin", prenom: "Alice", idPromo: "PROMO-2024" }]); + const [s] = await seedStudents([{ + nom: "Martin", + prenom: "Alice", + idPromo: "PROMO-2024", + }]); await seedModules([{ id: "MOD102", nom: "Module B" }]); - const [created] = await testDb.insert(notes).values({ numEtud: s.numEtud, idModule: "MOD102", note: 12.0 }).returning(); + const [created] = await testDb.insert(notes).values({ + numEtud: s.numEtud, + idModule: "MOD102", + note: 12.0, + }).returning(); assertExists(created); assertEquals(created.note, 12.0); @@ -71,11 +83,19 @@ Deno.test({ async fn() { await truncateAll(); await seedPromotions([{ id: "PROMO-2024" }]); - const [s] = await seedStudents([{ nom: "Durand", prenom: "Claire", idPromo: "PROMO-2024" }]); + const [s] = await seedStudents([{ + nom: "Durand", + prenom: "Claire", + idPromo: "PROMO-2024", + }]); await seedModules([{ id: "MOD103", nom: "Module C" }]); await seedNotes([{ numEtud: s.numEtud, idModule: "MOD103", note: 10.0 }]); await assertRejects(() => - testDb.insert(notes).values({ numEtud: s.numEtud, idModule: "MOD103", note: 11.0 }) + testDb.insert(notes).values({ + numEtud: s.numEtud, + idModule: "MOD103", + note: 11.0, + }) ); }, sanitizeResources: false, @@ -87,7 +107,11 @@ Deno.test({ async fn() { await truncateAll(); await seedPromotions([{ id: "PROMO-2024" }]); - const [s] = await seedStudents([{ nom: "Bernard", prenom: "Lucie", idPromo: "PROMO-2024" }]); + const [s] = await seedStudents([{ + nom: "Bernard", + prenom: "Lucie", + idPromo: "PROMO-2024", + }]); await seedModules([{ id: "MOD104", nom: "Module D" }]); await seedNotes([{ numEtud: s.numEtud, idModule: "MOD104", note: 8.0 }]); @@ -107,11 +131,17 @@ Deno.test({ async fn() { await truncateAll(); await seedPromotions([{ id: "PROMO-2024" }]); - const [s] = await seedStudents([{ nom: "Thomas", prenom: "Eva", idPromo: "PROMO-2024" }]); + const [s] = await seedStudents([{ + nom: "Thomas", + prenom: "Eva", + idPromo: "PROMO-2024", + }]); await seedModules([{ id: "MOD105", nom: "Module E" }]); await seedNotes([{ numEtud: s.numEtud, idModule: "MOD105", note: 14.0 }]); - await testDb.delete(notes).where(and(eq(notes.numEtud, s.numEtud), eq(notes.idModule, "MOD105"))); + await testDb.delete(notes).where( + and(eq(notes.numEtud, s.numEtud), eq(notes.idModule, "MOD105")), + ); const row = await testDb .select() .from(notes) diff --git a/tests/integration/ue_modules_test.ts b/tests/integration/ue_modules_test.ts index 9a996ad..9aaab2a 100644 --- a/tests/integration/ue_modules_test.ts +++ b/tests/integration/ue_modules_test.ts @@ -48,7 +48,13 @@ Deno.test({ const row = await testDb .select() .from(ueModules) - .where(and(eq(ueModules.idModule, "M1"), eq(ueModules.idUE, ue.id), eq(ueModules.idPromo, "P1"))) + .where( + and( + eq(ueModules.idModule, "M1"), + eq(ueModules.idUE, ue.id), + eq(ueModules.idPromo, "P1"), + ), + ) .then((r) => r[0] ?? null); assertExists(row); assertEquals(row.coeff, 4.0); @@ -58,13 +64,20 @@ Deno.test({ }); Deno.test({ - name: "integration ue_modules: get by composite key returns null when not found", + name: + "integration ue_modules: get by composite key returns null when not found", async fn() { await truncateAll(); const row = await testDb .select() .from(ueModules) - .where(and(eq(ueModules.idModule, "GHOST"), eq(ueModules.idUE, 99), eq(ueModules.idPromo, "GHOST"))) + .where( + and( + eq(ueModules.idModule, "GHOST"), + eq(ueModules.idUE, 99), + eq(ueModules.idPromo, "GHOST"), + ), + ) .then((r) => r[0] ?? null); assertEquals(row, null); }, @@ -79,9 +92,19 @@ Deno.test({ await seedPromotions([{ id: "P1" }]); await seedModules([{ id: "M1", nom: "Mod A" }]); const [ue] = await seedUes([{ nom: "UE Info" }]); - await seedUeModules([{ idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: 2.0 }]); + await seedUeModules([{ + idModule: "M1", + idUE: ue.id, + idPromo: "P1", + coeff: 2.0, + }]); await assertRejects(() => - testDb.insert(ueModules).values({ idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: 5.0 }) + testDb.insert(ueModules).values({ + idModule: "M1", + idUE: ue.id, + idPromo: "P1", + coeff: 5.0, + }) ); }, sanitizeResources: false, @@ -95,12 +118,23 @@ Deno.test({ await seedPromotions([{ id: "P1" }]); await seedModules([{ id: "M1", nom: "Mod A" }]); const [ue] = await seedUes([{ nom: "UE Info" }]); - await seedUeModules([{ idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: 2.0 }]); + await seedUeModules([{ + idModule: "M1", + idUE: ue.id, + idPromo: "P1", + coeff: 2.0, + }]); const [updated] = await testDb .update(ueModules) .set({ coeff: 6.0 }) - .where(and(eq(ueModules.idModule, "M1"), eq(ueModules.idUE, ue.id), eq(ueModules.idPromo, "P1"))) + .where( + and( + eq(ueModules.idModule, "M1"), + eq(ueModules.idUE, ue.id), + eq(ueModules.idPromo, "P1"), + ), + ) .returning(); assertEquals(updated.coeff, 6.0); }, @@ -115,15 +149,32 @@ Deno.test({ await seedPromotions([{ id: "P1" }]); await seedModules([{ id: "M1", nom: "Mod A" }]); const [ue] = await seedUes([{ nom: "UE Info" }]); - await seedUeModules([{ idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: 2.0 }]); + await seedUeModules([{ + idModule: "M1", + idUE: ue.id, + idPromo: "P1", + coeff: 2.0, + }]); await testDb .delete(ueModules) - .where(and(eq(ueModules.idModule, "M1"), eq(ueModules.idUE, ue.id), eq(ueModules.idPromo, "P1"))); + .where( + and( + eq(ueModules.idModule, "M1"), + eq(ueModules.idUE, ue.id), + eq(ueModules.idPromo, "P1"), + ), + ); const row = await testDb .select() .from(ueModules) - .where(and(eq(ueModules.idModule, "M1"), eq(ueModules.idUE, ue.id), eq(ueModules.idPromo, "P1"))) + .where( + and( + eq(ueModules.idModule, "M1"), + eq(ueModules.idUE, ue.id), + eq(ueModules.idPromo, "P1"), + ), + ) .then((r) => r[0] ?? null); assertEquals(row, null); }, diff --git a/tests/integration/ues_test.ts b/tests/integration/ues_test.ts index 653fbef..790330a 100644 --- a/tests/integration/ues_test.ts +++ b/tests/integration/ues_test.ts @@ -21,12 +21,14 @@ Deno.test({ name: "integration ues: create and retrieve by id", async fn() { await truncateAll(); - const [created] = await testDb.insert(ues).values({ nom: "UE Physique" }).returning(); + const [created] = await testDb.insert(ues).values({ nom: "UE Physique" }) + .returning(); assertExists(created); assertExists(created.id); assertEquals(created.nom, "UE Physique"); - const row = await testDb.select().from(ues).where(eq(ues.id, created.id)).then((r) => r[0] ?? null); + const row = await testDb.select().from(ues).where(eq(ues.id, created.id)) + .then((r) => r[0] ?? null); assertExists(row); assertEquals(row.nom, "UE Physique"); }, @@ -38,7 +40,9 @@ Deno.test({ name: "integration ues: get by id returns null when not found", async fn() { await truncateAll(); - const row = await testDb.select().from(ues).where(eq(ues.id, 99999)).then((r) => r[0] ?? null); + const row = await testDb.select().from(ues).where(eq(ues.id, 99999)).then(( + r, + ) => r[0] ?? null); assertEquals(row, null); }, sanitizeResources: false, @@ -50,7 +54,9 @@ Deno.test({ async fn() { await truncateAll(); const [ue] = await seedUes([{ nom: "UE Chimie" }]); - const [updated] = await testDb.update(ues).set({ nom: "UE Chimie organique" }).where(eq(ues.id, ue.id)).returning(); + const [updated] = await testDb.update(ues).set({ + nom: "UE Chimie organique", + }).where(eq(ues.id, ue.id)).returning(); assertEquals(updated.nom, "UE Chimie organique"); }, sanitizeResources: false, @@ -63,7 +69,9 @@ Deno.test({ await truncateAll(); const [ue] = await seedUes([{ nom: "UE à supprimer" }]); await testDb.delete(ues).where(eq(ues.id, ue.id)); - const row = await testDb.select().from(ues).where(eq(ues.id, ue.id)).then((r) => r[0] ?? null); + const row = await testDb.select().from(ues).where(eq(ues.id, ue.id)).then(( + r, + ) => r[0] ?? null); assertEquals(row, null); }, sanitizeResources: false, @@ -80,4 +88,3 @@ Deno.test({ sanitizeResources: false, sanitizeOps: false, }); - diff --git a/tests/unit/ajustements_test.ts b/tests/unit/ajustements_test.ts index a2786c4..8820c23 100644 --- a/tests/unit/ajustements_test.ts +++ b/tests/unit/ajustements_test.ts @@ -32,7 +32,9 @@ Deno.test("mock API: GET /ajustements?numEtud filters by student", async () => { const filtered = ajustements.filter((a) => a.numEtud === 21212006); mockFetch({ "/ajustements": filtered }); try { - const res = await fetch("http://localhost/api/ajustements?numEtud=21212006"); + const res = await fetch( + "http://localhost/api/ajustements?numEtud=21212006", + ); const data: Ajustement[] = await res.json(); assertEquals(data.length, 1); assertEquals(data[0].numEtud, 21212006); @@ -53,7 +55,9 @@ Deno.test("mock API: GET /ajustements?numEtud=NaN returns 400", async () => { Deno.test("mock API: POST /ajustements creates ajustement (201) as employee", async () => { const newAjust: Ajustement = { numEtud: 21212007, idUE: 2, valeur: 14.0 }; - mockFetch({ "/ajustements": { method: "POST", status: 201, body: newAjust } }); + mockFetch({ + "/ajustements": { method: "POST", status: 201, body: newAjust }, + }); try { const res = await fetch("http://localhost/api/ajustements", { method: "POST", @@ -72,7 +76,9 @@ Deno.test("mock API: POST /ajustements creates ajustement (201) as employee", as Deno.test("mock API: POST /ajustements 403 for non-employee", async () => { mockFetch({ "/ajustements": { method: "POST", status: 403 } }); try { - const res = await fetch("http://localhost/api/ajustements", { method: "POST" }); + const res = await fetch("http://localhost/api/ajustements", { + method: "POST", + }); assertEquals(res.status, 403); } finally { restoreFetch(); @@ -116,7 +122,12 @@ Deno.test("mock API: GET /ajustements/:numEtud/:idUE 403 for non-employee", asyn }); Deno.test("mock API: GET /ajustements/:numEtud/:idUE 404 when not found", async () => { - mockFetch({ "/ajustements/99999/9": { status: 404, body: { error: "Ajustement introuvable" } } }); + mockFetch({ + "/ajustements/99999/9": { + status: 404, + body: { error: "Ajustement introuvable" }, + }, + }); try { const res = await fetch("http://localhost/api/ajustements/99999/9"); assertEquals(res.status, 404); @@ -127,7 +138,9 @@ Deno.test("mock API: GET /ajustements/:numEtud/:idUE 404 when not found", async Deno.test("mock API: PUT /ajustements/:numEtud/:idUE updates valeur", async () => { const updated: Ajustement = { ...ajustements[0], valeur: 18.0 }; - mockFetch({ "/ajustements/21212006/1": { method: "PUT", status: 200, body: updated } }); + mockFetch({ + "/ajustements/21212006/1": { method: "PUT", status: 200, body: updated }, + }); try { const res = await fetch("http://localhost/api/ajustements/21212006/1", { method: "PUT", @@ -145,7 +158,9 @@ Deno.test("mock API: PUT /ajustements/:numEtud/:idUE updates valeur", async () = Deno.test("mock API: DELETE /ajustements/:numEtud/:idUE returns 204", async () => { mockFetch({ "/ajustements/21212006/1": { method: "DELETE", status: 204 } }); try { - const res = await fetch("http://localhost/api/ajustements/21212006/1", { method: "DELETE" }); + const res = await fetch("http://localhost/api/ajustements/21212006/1", { + method: "DELETE", + }); assertEquals(res.status, 204); } finally { restoreFetch(); @@ -156,34 +171,54 @@ Deno.test("mock API: DELETE /ajustements/:numEtud/:idUE returns 204", async () = Deno.test("mock DB: find ajustement by composite key", () => { const db = createMockDb({ tables: { ajustements: [...ajustements] } }); - const a = db.findOne("ajustements", (a) => a.numEtud === 21212006 && a.idUE === 1); + const a = db.findOne( + "ajustements", + (a) => a.numEtud === 21212006 && a.idUE === 1, + ); assertExists(a); assertEquals(a.valeur, 13.25); }); Deno.test("mock DB: filter ajustements by numEtud", () => { const db = createMockDb({ tables: { ajustements: [...ajustements] } }); - const rows = db.findMany("ajustements", (a) => a.numEtud === 21212006); + const rows = db.findMany( + "ajustements", + (a) => a.numEtud === 21212006, + ); assertEquals(rows.length, 1); }); Deno.test("mock DB: insert ajustement", () => { const db = createMockDb({ tables: { ajustements: [...ajustements] } }); - db.insert("ajustements", { numEtud: 21212007, idUE: 2, valeur: 14.0 }); + db.insert("ajustements", { + numEtud: 21212007, + idUE: 2, + valeur: 14.0, + }); assertEquals(db.getTable("ajustements").length, 3); }); Deno.test("mock DB: update ajustement valeur", () => { const db = createMockDb({ tables: { ajustements: [...ajustements] } }); - db.updateWhere("ajustements", (a) => a.numEtud === 21212006 && a.idUE === 1, { valeur: 20.0 }); + db.updateWhere( + "ajustements", + (a) => a.numEtud === 21212006 && a.idUE === 1, + { valeur: 20.0 }, + ); assertEquals( - db.findOne("ajustements", (a) => a.numEtud === 21212006 && a.idUE === 1)?.valeur, + db.findOne( + "ajustements", + (a) => a.numEtud === 21212006 && a.idUE === 1, + )?.valeur, 20.0, ); }); Deno.test("mock DB: delete ajustement", () => { const db = createMockDb({ tables: { ajustements: [...ajustements] } }); - db.deleteWhere("ajustements", (a) => a.numEtud === 21212006 && a.idUE === 1); + db.deleteWhere( + "ajustements", + (a) => a.numEtud === 21212006 && a.idUE === 1, + ); assertEquals(db.getTable("ajustements").length, 1); }); diff --git a/tests/unit/enseignements_test.ts b/tests/unit/enseignements_test.ts index 3182019..d1e3b04 100644 --- a/tests/unit/enseignements_test.ts +++ b/tests/unit/enseignements_test.ts @@ -22,8 +22,14 @@ Deno.test("enseignements: fixtures have correct shape", () => { // --- Mock API --- Deno.test("mock API: POST /enseignements creates enseignement (201) as employee", async () => { - const newEns: Enseignement = { idProf: "prof.dupont", idModule: "JIN702C", idPromo: "4AFISE25/26" }; - mockFetch({ "/enseignements": { method: "POST", status: 201, body: newEns } }); + const newEns: Enseignement = { + idProf: "prof.dupont", + idModule: "JIN702C", + idPromo: "4AFISE25/26", + }; + mockFetch({ + "/enseignements": { method: "POST", status: 201, body: newEns }, + }); try { const res = await fetch("http://localhost/api/enseignements", { method: "POST", @@ -41,7 +47,9 @@ Deno.test("mock API: POST /enseignements creates enseignement (201) as employee" Deno.test("mock API: POST /enseignements 403 for non-employee", async () => { mockFetch({ "/enseignements": { method: "POST", status: 403 } }); try { - const res = await fetch("http://localhost/api/enseignements", { method: "POST" }); + const res = await fetch("http://localhost/api/enseignements", { + method: "POST", + }); assertEquals(res.status, 403); } finally { restoreFetch(); @@ -64,13 +72,21 @@ Deno.test("mock API: POST /enseignements 400 on missing fields", async () => { Deno.test("mock API: POST /enseignements 409 on duplicate", async () => { mockFetch({ - "/enseignements": { method: "POST", status: 409, body: { error: "Cet enseignement existe déjà." } }, + "/enseignements": { + method: "POST", + status: 409, + body: { error: "Cet enseignement existe déjà." }, + }, }); try { const res = await fetch("http://localhost/api/enseignements", { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ idProf: "prof.dupont", idModule: "JIN702C", idPromo: "4AFISE25/26" }), + body: JSON.stringify({ + idProf: "prof.dupont", + idModule: "JIN702C", + idPromo: "4AFISE25/26", + }), }); assertEquals(res.status, 409); const data = await res.json(); @@ -81,10 +97,16 @@ Deno.test("mock API: POST /enseignements 409 on duplicate", async () => { }); Deno.test("mock API: GET /enseignements/:idProf/:idModule/:idPromo returns enseignement (employee)", async () => { - const ens: Enseignement = { idProf: "prof.dupont", idModule: "JIN702C", idPromo: "4AFISE25/26" }; + const ens: Enseignement = { + idProf: "prof.dupont", + idModule: "JIN702C", + idPromo: "4AFISE25/26", + }; mockFetch({ "/enseignements/prof.dupont/JIN702C/4AFISE25": ens }); try { - const res = await fetch("http://localhost/api/enseignements/prof.dupont/JIN702C/4AFISE25%2F26"); + const res = await fetch( + "http://localhost/api/enseignements/prof.dupont/JIN702C/4AFISE25%2F26", + ); assertEquals(res.status, 200); const data: Enseignement = await res.json(); assertEquals(data.idProf, "prof.dupont"); @@ -97,7 +119,9 @@ Deno.test("mock API: GET /enseignements/:idProf/:idModule/:idPromo returns ensei Deno.test("mock API: GET /enseignements/:idProf/:idModule/:idPromo 403 for non-employee", async () => { mockFetch({ "/enseignements/prof.dupont/JIN702C/4AFISE25": { status: 403 } }); try { - const res = await fetch("http://localhost/api/enseignements/prof.dupont/JIN702C/4AFISE25%2F26"); + const res = await fetch( + "http://localhost/api/enseignements/prof.dupont/JIN702C/4AFISE25%2F26", + ); assertEquals(res.status, 403); } finally { restoreFetch(); @@ -105,9 +129,16 @@ Deno.test("mock API: GET /enseignements/:idProf/:idModule/:idPromo 403 for non-e }); Deno.test("mock API: GET /enseignements/:idProf/:idModule/:idPromo 404 when not found", async () => { - mockFetch({ "/enseignements/ghost/GHOST/GHOST": { status: 404, body: { error: "Ressource introuvable" } } }); + mockFetch({ + "/enseignements/ghost/GHOST/GHOST": { + status: 404, + body: { error: "Ressource introuvable" }, + }, + }); try { - const res = await fetch("http://localhost/api/enseignements/ghost/GHOST/GHOST"); + const res = await fetch( + "http://localhost/api/enseignements/ghost/GHOST/GHOST", + ); assertEquals(res.status, 404); } finally { restoreFetch(); @@ -115,11 +146,19 @@ Deno.test("mock API: GET /enseignements/:idProf/:idModule/:idPromo 404 when not }); Deno.test("mock API: DELETE /enseignements/:idProf/:idModule/:idPromo returns 204 (employee)", async () => { - mockFetch({ "/enseignements/prof.dupont/JIN702C/4AFISE25": { method: "DELETE", status: 204 } }); - try { - const res = await fetch("http://localhost/api/enseignements/prof.dupont/JIN702C/4AFISE25%2F26", { + mockFetch({ + "/enseignements/prof.dupont/JIN702C/4AFISE25": { method: "DELETE", - }); + status: 204, + }, + }); + try { + const res = await fetch( + "http://localhost/api/enseignements/prof.dupont/JIN702C/4AFISE25%2F26", + { + method: "DELETE", + }, + ); assertEquals(res.status, 204); } finally { restoreFetch(); @@ -127,11 +166,19 @@ Deno.test("mock API: DELETE /enseignements/:idProf/:idModule/:idPromo returns 20 }); Deno.test("mock API: DELETE /enseignements/:idProf/:idModule/:idPromo 403 for non-employee", async () => { - mockFetch({ "/enseignements/prof.dupont/JIN702C/4AFISE25": { method: "DELETE", status: 403 } }); - try { - const res = await fetch("http://localhost/api/enseignements/prof.dupont/JIN702C/4AFISE25%2F26", { + mockFetch({ + "/enseignements/prof.dupont/JIN702C/4AFISE25": { method: "DELETE", - }); + status: 403, + }, + }); + try { + const res = await fetch( + "http://localhost/api/enseignements/prof.dupont/JIN702C/4AFISE25%2F26", + { + method: "DELETE", + }, + ); assertEquals(res.status, 403); } finally { restoreFetch(); @@ -146,14 +193,21 @@ Deno.test("mock DB: find enseignement by composite key", () => { { idProf: "prof.moreau", idModule: "JIN703C", idPromo: "4AFISE25/26" }, ]; const db = createMockDb({ tables: { enseignements: data } }); - const e = db.findOne("enseignements", (e) => e.idProf === "prof.dupont" && e.idModule === "JIN702C"); + const e = db.findOne( + "enseignements", + (e) => e.idProf === "prof.dupont" && e.idModule === "JIN702C", + ); assertExists(e); assertEquals(e.idPromo, "4AFISE25/26"); }); Deno.test("mock DB: insert enseignement", () => { const db = createMockDb({ tables: { enseignements: [] } }); - db.insert("enseignements", { idProf: "prof.dupont", idModule: "JIN702C", idPromo: "4AFISE25/26" }); + db.insert("enseignements", { + idProf: "prof.dupont", + idModule: "JIN702C", + idPromo: "4AFISE25/26", + }); assertEquals(db.getTable("enseignements").length, 1); }); @@ -163,7 +217,10 @@ Deno.test("mock DB: delete enseignement", () => { { idProf: "prof.moreau", idModule: "JIN703C", idPromo: "4AFISE25/26" }, ]; const db = createMockDb({ tables: { enseignements: data } }); - db.deleteWhere("enseignements", (e) => e.idProf === "prof.dupont"); + db.deleteWhere( + "enseignements", + (e) => e.idProf === "prof.dupont", + ); assertEquals(db.getTable("enseignements").length, 1); }); @@ -174,6 +231,9 @@ Deno.test("mock DB: filter enseignements by idModule", () => { { idProf: "prof.moreau", idModule: "JIN703C", idPromo: "4AFISE25/26" }, ]; const db = createMockDb({ tables: { enseignements: data } }); - const rows = db.findMany("enseignements", (e) => e.idModule === "JIN702C"); + const rows = db.findMany( + "enseignements", + (e) => e.idModule === "JIN702C", + ); assertEquals(rows.length, 2); }); diff --git a/tests/unit/notes_test.ts b/tests/unit/notes_test.ts index f39d4ba..9e13794 100644 --- a/tests/unit/notes_test.ts +++ b/tests/unit/notes_test.ts @@ -113,7 +113,12 @@ Deno.test("mock API: GET /notes/:numEtud/:idModule returns note", async () => { }); Deno.test("mock API: GET /notes/:numEtud/:idModule 404 when not found", async () => { - mockFetch({ "/notes/99999/GHOST": { status: 404, body: { error: "Ressource introuvable" } } }); + mockFetch({ + "/notes/99999/GHOST": { + status: 404, + body: { error: "Ressource introuvable" }, + }, + }); try { const res = await fetch("http://localhost/api/notes/99999/GHOST"); assertEquals(res.status, 404); @@ -124,7 +129,9 @@ Deno.test("mock API: GET /notes/:numEtud/:idModule 404 when not found", async () Deno.test("mock API: PUT /notes/:numEtud/:idModule updates note", async () => { const updated: Note = { ...notes[0], note: 17.0 }; - mockFetch({ "/notes/21212006/JIN702C": { method: "PUT", status: 200, body: updated } }); + mockFetch({ + "/notes/21212006/JIN702C": { method: "PUT", status: 200, body: updated }, + }); try { const res = await fetch("http://localhost/api/notes/21212006/JIN702C", { method: "PUT", @@ -142,7 +149,9 @@ Deno.test("mock API: PUT /notes/:numEtud/:idModule updates note", async () => { Deno.test("mock API: DELETE /notes/:numEtud/:idModule returns 204", async () => { mockFetch({ "/notes/21212006/JIN702C": { method: "DELETE", status: 204 } }); try { - const res = await fetch("http://localhost/api/notes/21212006/JIN702C", { method: "DELETE" }); + const res = await fetch("http://localhost/api/notes/21212006/JIN702C", { + method: "DELETE", + }); assertEquals(res.status, 204); } finally { restoreFetch(); @@ -152,7 +161,9 @@ Deno.test("mock API: DELETE /notes/:numEtud/:idModule returns 204", async () => Deno.test("mock API: DELETE /notes/:numEtud/:idModule 404 when not found", async () => { mockFetch({ "/notes/99999/GHOST": { method: "DELETE", status: 404 } }); try { - const res = await fetch("http://localhost/api/notes/99999/GHOST", { method: "DELETE" }); + const res = await fetch("http://localhost/api/notes/99999/GHOST", { + method: "DELETE", + }); assertEquals(res.status, 404); } finally { restoreFetch(); @@ -163,7 +174,10 @@ Deno.test("mock API: DELETE /notes/:numEtud/:idModule 404 when not found", async Deno.test("mock DB: find note by composite key", () => { const db = createMockDb({ tables: { notes: [...notes] } }); - const n = db.findOne("notes", (n) => n.numEtud === 21212006 && n.idModule === "JIN702C"); + const n = db.findOne( + "notes", + (n) => n.numEtud === 21212006 && n.idModule === "JIN702C", + ); assertExists(n); assertEquals(n.note, 15.5); }); @@ -176,21 +190,35 @@ Deno.test("mock DB: filter notes by numEtud", () => { Deno.test("mock DB: insert note", () => { const db = createMockDb({ tables: { notes: [...notes] } }); - db.insert("notes", { note: 10.0, numEtud: 21212006, idModule: "JIN704C" }); + db.insert("notes", { + note: 10.0, + numEtud: 21212006, + idModule: "JIN704C", + }); assertEquals(db.getTable("notes").length, 5); }); Deno.test("mock DB: update note value", () => { const db = createMockDb({ tables: { notes: [...notes] } }); - db.updateWhere("notes", (n) => n.numEtud === 21212006 && n.idModule === "JIN702C", { note: 20.0 }); + db.updateWhere( + "notes", + (n) => n.numEtud === 21212006 && n.idModule === "JIN702C", + { note: 20.0 }, + ); assertEquals( - db.findOne("notes", (n) => n.numEtud === 21212006 && n.idModule === "JIN702C")?.note, + db.findOne( + "notes", + (n) => n.numEtud === 21212006 && n.idModule === "JIN702C", + )?.note, 20.0, ); }); Deno.test("mock DB: delete note", () => { const db = createMockDb({ tables: { notes: [...notes] } }); - db.deleteWhere("notes", (n) => n.numEtud === 21212006 && n.idModule === "JIN702C"); + db.deleteWhere( + "notes", + (n) => n.numEtud === 21212006 && n.idModule === "JIN702C", + ); assertEquals(db.getTable("notes").length, 3); }); diff --git a/tests/unit/ue_modules_test.ts b/tests/unit/ue_modules_test.ts index 8037998..7b2761d 100644 --- a/tests/unit/ue_modules_test.ts +++ b/tests/unit/ue_modules_test.ts @@ -33,7 +33,9 @@ Deno.test("mock API: GET /ue-modules?idPromo filters by promo", async () => { const filtered = ueModules.filter((u) => u.idPromo === "4AFISE25/26"); mockFetch({ "/ue-modules": filtered }); try { - const res = await fetch("http://localhost/api/ue-modules?idPromo=4AFISE25%2F26"); + const res = await fetch( + "http://localhost/api/ue-modules?idPromo=4AFISE25%2F26", + ); const data: UeModule[] = await res.json(); assertEquals(data.length, 2); assertEquals(data.every((u) => u.idPromo === "4AFISE25/26"), true); @@ -56,8 +58,15 @@ Deno.test("mock API: GET /ue-modules?idUE filters by UE", async () => { }); Deno.test("mock API: POST /ue-modules creates association (201)", async () => { - const newUeModule: UeModule = { idModule: "JIN705C", idUE: 2, idPromo: "3AFISE25/26", coeff: 3.0 }; - mockFetch({ "/ue-modules": { method: "POST", status: 201, body: newUeModule } }); + const newUeModule: UeModule = { + idModule: "JIN705C", + idUE: 2, + idPromo: "3AFISE25/26", + coeff: 3.0, + }; + mockFetch({ + "/ue-modules": { method: "POST", status: 201, body: newUeModule }, + }); try { const res = await fetch("http://localhost/api/ue-modules", { method: "POST", @@ -90,7 +99,9 @@ Deno.test("mock API: POST /ue-modules 400 on missing fields", async () => { Deno.test("mock API: GET /ue-modules/:idModule/:idUE/:idPromo returns association (employee)", async () => { mockFetch({ "/ue-modules/JIN702C/1/4AFISE25": ueModules[0] }); try { - const res = await fetch("http://localhost/api/ue-modules/JIN702C/1/4AFISE25%2F26"); + const res = await fetch( + "http://localhost/api/ue-modules/JIN702C/1/4AFISE25%2F26", + ); assertEquals(res.status, 200); const data: UeModule = await res.json(); assertEquals(data.coeff, 3.0); @@ -102,7 +113,9 @@ Deno.test("mock API: GET /ue-modules/:idModule/:idUE/:idPromo returns associatio Deno.test("mock API: GET /ue-modules/:idModule/:idUE/:idPromo 403 for non-employee", async () => { mockFetch({ "/ue-modules/JIN702C/1/4AFISE25": { status: 403 } }); try { - const res = await fetch("http://localhost/api/ue-modules/JIN702C/1/4AFISE25%2F26"); + const res = await fetch( + "http://localhost/api/ue-modules/JIN702C/1/4AFISE25%2F26", + ); assertEquals(res.status, 403); } finally { restoreFetch(); @@ -111,13 +124,22 @@ Deno.test("mock API: GET /ue-modules/:idModule/:idUE/:idPromo 403 for non-employ Deno.test("mock API: PUT /ue-modules/:idModule/:idUE/:idPromo updates coeff", async () => { const updated: UeModule = { ...ueModules[0], coeff: 5.0 }; - mockFetch({ "/ue-modules/JIN702C/1/4AFISE25": { method: "PUT", status: 200, body: updated } }); - try { - const res = await fetch("http://localhost/api/ue-modules/JIN702C/1/4AFISE25%2F26", { + mockFetch({ + "/ue-modules/JIN702C/1/4AFISE25": { method: "PUT", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ coeff: 5.0 }), - }); + status: 200, + body: updated, + }, + }); + try { + const res = await fetch( + "http://localhost/api/ue-modules/JIN702C/1/4AFISE25%2F26", + { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ coeff: 5.0 }), + }, + ); assertEquals(res.status, 200); const data: UeModule = await res.json(); assertEquals(data.coeff, 5.0); @@ -127,9 +149,14 @@ Deno.test("mock API: PUT /ue-modules/:idModule/:idUE/:idPromo updates coeff", as }); Deno.test("mock API: DELETE /ue-modules/:idModule/:idUE/:idPromo returns 204", async () => { - mockFetch({ "/ue-modules/JIN702C/1/4AFISE25": { method: "DELETE", status: 204 } }); + mockFetch({ + "/ue-modules/JIN702C/1/4AFISE25": { method: "DELETE", status: 204 }, + }); try { - const res = await fetch("http://localhost/api/ue-modules/JIN702C/1/4AFISE25%2F26", { method: "DELETE" }); + const res = await fetch( + "http://localhost/api/ue-modules/JIN702C/1/4AFISE25%2F26", + { method: "DELETE" }, + ); assertEquals(res.status, 204); } finally { restoreFetch(); @@ -140,34 +167,56 @@ Deno.test("mock API: DELETE /ue-modules/:idModule/:idUE/:idPromo returns 204", a Deno.test("mock DB: find ue-module by composite key", () => { const db = createMockDb({ tables: { ueModules: [...ueModules] } }); - const u = db.findOne("ueModules", (u) => u.idModule === "JIN702C" && u.idUE === 1 && u.idPromo === "4AFISE25/26"); + const u = db.findOne( + "ueModules", + (u) => + u.idModule === "JIN702C" && u.idUE === 1 && u.idPromo === "4AFISE25/26", + ); assertExists(u); assertEquals(u.coeff, 3.0); }); Deno.test("mock DB: filter ue-modules by promo", () => { const db = createMockDb({ tables: { ueModules: [...ueModules] } }); - const rows = db.findMany("ueModules", (u) => u.idPromo === "4AFISE25/26"); + const rows = db.findMany( + "ueModules", + (u) => u.idPromo === "4AFISE25/26", + ); assertEquals(rows.length, 2); }); Deno.test("mock DB: insert ue-module", () => { const db = createMockDb({ tables: { ueModules: [...ueModules] } }); - db.insert("ueModules", { idModule: "JIN705C", idUE: 2, idPromo: "3AFISE25/26", coeff: 1.5 }); + db.insert("ueModules", { + idModule: "JIN705C", + idUE: 2, + idPromo: "3AFISE25/26", + coeff: 1.5, + }); assertEquals(db.getTable("ueModules").length, 4); }); Deno.test("mock DB: update ue-module coeff", () => { const db = createMockDb({ tables: { ueModules: [...ueModules] } }); - db.updateWhere("ueModules", (u) => u.idModule === "JIN702C" && u.idUE === 1, { coeff: 6.0 }); + db.updateWhere( + "ueModules", + (u) => u.idModule === "JIN702C" && u.idUE === 1, + { coeff: 6.0 }, + ); assertEquals( - db.findOne("ueModules", (u) => u.idModule === "JIN702C" && u.idUE === 1)?.coeff, + db.findOne( + "ueModules", + (u) => u.idModule === "JIN702C" && u.idUE === 1, + )?.coeff, 6.0, ); }); Deno.test("mock DB: delete ue-module", () => { const db = createMockDb({ tables: { ueModules: [...ueModules] } }); - db.deleteWhere("ueModules", (u) => u.idModule === "JIN702C" && u.idUE === 1); + db.deleteWhere( + "ueModules", + (u) => u.idModule === "JIN702C" && u.idUE === 1, + ); assertEquals(db.getTable("ueModules").length, 2); }); diff --git a/tests/unit/ues_test.ts b/tests/unit/ues_test.ts index 5d36a95..f823f7d 100644 --- a/tests/unit/ues_test.ts +++ b/tests/unit/ues_test.ts @@ -42,7 +42,9 @@ Deno.test("mock API: GET /ues/:id returns one UE", async () => { }); Deno.test("mock API: GET /ues/:id 404 when not found", async () => { - mockFetch({ "/ues/99": { status: 404, body: { error: "Ressource introuvable" } } }); + mockFetch({ + "/ues/99": { status: 404, body: { error: "Ressource introuvable" } }, + }); try { const res = await fetch("http://localhost/api/ues/99"); assertEquals(res.status, 404); @@ -125,7 +127,9 @@ Deno.test("mock API: DELETE /ues/:id returns 204", async () => { Deno.test("mock API: DELETE /ues/:id 404 when not found", async () => { mockFetch({ "/ues/99": { method: "DELETE", status: 404 } }); try { - const res = await fetch("http://localhost/api/ues/99", { method: "DELETE" }); + const res = await fetch("http://localhost/api/ues/99", { + method: "DELETE", + }); assertEquals(res.status, 404); } finally { restoreFetch(); -- 2.52.0