test: add full test coverage for notes, ues, ue-modules, ajustements, enseignements, users
Check Deno code / Check Deno code (pull_request) Failing after 6s
Tests / Unit tests (pull_request) Successful in 13s
Tests / Integration tests (pull_request) Failing after 1m14s

- 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)
This commit is contained in:
2026-04-26 18:25:00 +02:00
parent a3b55d0a1b
commit 2f4d8db1bf
19 changed files with 3471 additions and 0 deletions
+2
View File
@@ -14,6 +14,8 @@
"test:unit": "deno test -A --no-check tests/unit/", "test:unit": "deno test -A --no-check tests/unit/",
"test:integration": "deno test -A --no-check tests/integration/", "test:integration": "deno test -A --no-check tests/integration/",
"test:e2e": "deno test -A --no-check tests/e2e/", "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" "migrate": "node_modules/.bin/drizzle-kit migrate"
}, },
"lint": { "lint": {
+301
View File
@@ -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,
});
+185
View File
@@ -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,
});
+244
View File
@@ -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,
});
+559
View File
@@ -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,
});
+271
View File
@@ -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,
});
+170
View File
@@ -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,
});
+217
View File
@@ -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,
});
+24
View File
@@ -87,3 +87,27 @@ export async function seedUsers(
): Promise<typeof schema.users.$inferSelect[]> { ): Promise<typeof schema.users.$inferSelect[]> {
return await testDb.insert(schema.users).values(rows).returning(); return await testDb.insert(schema.users).values(rows).returning();
} }
export async function seedNotes(
rows: { numEtud: number; idModule: string; note: number }[],
): Promise<typeof schema.notes.$inferSelect[]> {
return await testDb.insert(schema.notes).values(rows).returning();
}
export async function seedUeModules(
rows: { idModule: string; idUE: number; idPromo: string; coeff: number }[],
): Promise<typeof schema.ueModules.$inferSelect[]> {
return await testDb.insert(schema.ueModules).values(rows).returning();
}
export async function seedEnseignements(
rows: { idProf: string; idModule: string; idPromo: string }[],
): Promise<typeof schema.enseignements.$inferSelect[]> {
return await testDb.insert(schema.enseignements).values(rows).returning();
}
export async function seedAjustements(
rows: { numEtud: number; idUE: number; valeur: number }[],
): Promise<typeof schema.ajustements.$inferSelect[]> {
return await testDb.insert(schema.ajustements).values(rows).returning();
}
+127
View File
@@ -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,
});
+135
View File
@@ -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,
});
+124
View File
@@ -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,
});
+132
View File
@@ -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,
});
+83
View File
@@ -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,
});
+189
View File
@@ -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<Ajustement>("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<Ajustement>("ajustements", (a) => a.numEtud === 21212006);
assertEquals(rows.length, 1);
});
Deno.test("mock DB: insert ajustement", () => {
const db = createMockDb({ tables: { ajustements: [...ajustements] } });
db.insert<Ajustement>("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<Ajustement>("ajustements", (a) => a.numEtud === 21212006 && a.idUE === 1, { valeur: 20.0 });
assertEquals(
db.findOne<Ajustement>("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<Ajustement>("ajustements", (a) => a.numEtud === 21212006 && a.idUE === 1);
assertEquals(db.getTable("ajustements").length, 1);
});
+179
View File
@@ -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<Enseignement>("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<Enseignement>("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<Enseignement>("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<Enseignement>("enseignements", (e) => e.idModule === "JIN702C");
assertEquals(rows.length, 2);
});
+196
View File
@@ -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<Note>("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<Note>("notes", (n) => n.numEtud === 21212006);
assertEquals(rows.length, 2);
});
Deno.test("mock DB: insert note", () => {
const db = createMockDb({ tables: { notes: [...notes] } });
db.insert<Note>("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<Note>("notes", (n) => n.numEtud === 21212006 && n.idModule === "JIN702C", { note: 20.0 });
assertEquals(
db.findOne<Note>("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<Note>("notes", (n) => n.numEtud === 21212006 && n.idModule === "JIN702C");
assertEquals(db.getTable("notes").length, 3);
});
+173
View File
@@ -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<UeModule>("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<UeModule>("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<UeModule>("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<UeModule>("ueModules", (u) => u.idModule === "JIN702C" && u.idUE === 1, { coeff: 6.0 });
assertEquals(
db.findOne<UeModule>("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<UeModule>("ueModules", (u) => u.idModule === "JIN702C" && u.idUE === 1);
assertEquals(db.getTable("ueModules").length, 2);
});
+160
View File
@@ -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<UE>("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<UE>("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<UE>("ues", (u) => u.id === 1, { nom: "Updated" });
assertEquals(db.findOne<UE>("ues", (u) => u.id === 1)?.nom, "Updated");
});
Deno.test("mock DB: delete UE", () => {
const db = createMockDb({ tables: { ues: [...ues] } });
db.deleteWhere<UE>("ues", (u) => u.id === 1);
assertEquals(db.getTable("ues").length, 1);
});