test : changed test format + added playwright support
Check Deno code / Check Deno code (pull_request) Has been cancelled
Tests / Unit tests (pull_request) Has been cancelled
Tests / Integration tests (pull_request) Has been cancelled
Check Deno code / Check Deno code (push) Has been cancelled
Tests / Unit tests (push) Has been cancelled
Tests / Integration tests (push) Has been cancelled

This commit was merged in pull request #153.
This commit is contained in:
2026-05-03 21:52:02 +02:00
parent ed2fe69f54
commit 951c9c1fea
52 changed files with 3576 additions and 5212 deletions
+271 -82
View File
@@ -1,19 +1,28 @@
// Integration tests for /ajustements — Drizzle ORM direct on real DB
// E2E tests for /ajustements endpoints — handler + real DB
import { assertEquals, assertExists, assertRejects } from "@std/assert";
import { assertEquals, assertExists } from "@std/assert";
import {
makeContextWithAffiliation,
makeEmployeeContext,
makeGetRequest,
makeJsonRequest,
} from "../helpers/handler.ts";
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";
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: "integration ajustements: list all ajustements",
name: "e2e ajustements: GET /ajustements returns all",
async fn() {
await truncateAll();
await seedPromotions([{ id: "P1" }]);
@@ -24,114 +33,196 @@ Deno.test({
}]);
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);
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: "integration ajustements: create and retrieve by composite key",
name: "e2e ajustements: GET /ajustements?numEtud filters by student",
async fn() {
await truncateAll();
await seedPromotions([{ id: "P1" }]);
const [s] = await seedStudents([{
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 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);
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:
"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",
"e2e ajustements: POST /ajustements creates ajustement (201) as employee",
async fn() {
await truncateAll();
await seedPromotions([{ id: "P1" }]);
const [s] = await seedStudents([{
nom: "Durand",
prenom: "Claire",
nom: "Leroy",
prenom: "Paul",
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({
const res = await ajustementsHandler.POST!(
makeJsonRequest("/ajustements", "POST", {
numEtud: s.numEtud,
idUE: ue.id,
valeur: 13.0,
})
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: "integration ajustements: update valeur",
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 [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);
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: "integration ajustements: delete removes the ajustement",
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" }]);
@@ -140,20 +231,118 @@ Deno.test({
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 [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) }),
);
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);
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,
+173 -81
View File
@@ -1,62 +1,134 @@
// Integration tests for /enseignements — Drizzle ORM direct on real DB
// E2E tests for /enseignements endpoints — handler + real DB
import { assertEquals, assertExists, assertRejects } from "@std/assert";
import { assertEquals, assertExists } from "@std/assert";
import {
makeContextWithAffiliation,
makeEmployeeContext,
makeGetRequest,
makeJsonRequest,
} from "../helpers/handler.ts";
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";
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: "integration enseignements: list all enseignements",
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" }, { id: "M2", nom: "Mod B" }]);
await seedModules([{ id: "M1", nom: "Mod A" }]);
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);
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: "integration enseignements: create and retrieve by composite key",
name: "e2e enseignements: POST /enseignements 403 for non-employee",
async fn() {
await truncateAll();
await seedUsers([{ id: "prof.moreau", nom: "Moreau", prenom: "Sophie" }]);
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,
});
const [created] = await testDb
.insert(enseignements)
.values({ idProf: "prof.moreau", idModule: "M1", idPromo: "P1" })
.returning();
assertExists(created);
assertEquals(created.idProf, "prof.moreau");
// --- GET /enseignements/:idProf/:idModule/:idPromo ---
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);
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,
@@ -64,28 +136,47 @@ Deno.test({
Deno.test({
name:
"integration enseignements: get by composite key returns null when not found",
"e2e enseignements: GET /enseignements/:idProf/:idModule/:idPromo 403 for non-employee",
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);
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: "integration enseignements: duplicate composite key insert fails",
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" }]);
@@ -96,52 +187,53 @@ Deno.test({
idModule: "M1",
idPromo: "P1",
}]);
await assertRejects(() =>
testDb.insert(enseignements).values({
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: "integration enseignements: delete removes the enseignement",
name:
"e2e enseignements: DELETE /enseignements/:idProf/:idModule/:idPromo 403 for non-employee",
async fn() {
await truncateAll();
await seedUsers([{ id: "prof.dupont", nom: "Dupont", prenom: "Jean" }]);
await seedModules([{ id: "M1", nom: "Mod A" }]);
await seedPromotions([{ id: "P1" }]);
await seedEnseignements([{
idProf: "prof.dupont",
idModule: "M1",
idPromo: "P1",
}]);
await 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);
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,
+170 -64
View File
@@ -1,103 +1,209 @@
// #113 - Integration tests for /modules endpoints
// #113 - E2E tests for /modules endpoints
import { assertEquals, assertExists, assertRejects } from "@std/assert";
import { seedModules, testDb, truncateAll } from "../helpers/db_integration.ts";
import { modules } from "$root/databases/schema.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
import { assertEquals } from "@std/assert";
import {
makeContextWithAffiliation,
makeEmployeeContext,
makeGetRequest,
makeJsonRequest,
} from "../helpers/handler.ts";
import { seedModules, 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";
// --- GET /modules ---
Deno.test({
name: "integration modules: list all modules",
name: "e2e modules: GET /modules returns all as employee",
async fn() {
await truncateAll();
await seedModules([{ id: "MATH101", nom: "Mathématiques" }, {
id: "INFO101",
nom: "Informatique",
}]);
const rows = await testDb.select().from(modules);
assertEquals(rows.length, 2);
const res = await modulesHandler.GET!(
makeGetRequest("/modules"),
makeEmployeeContext(),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.length, 2);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration modules: create and retrieve by id",
async fn() {
await truncateAll();
const [created] = await testDb.insert(modules).values({
id: "PHYS101",
nom: "Physique",
}).returning();
assertExists(created);
assertEquals(created.id, "PHYS101");
const row = await testDb
.select()
.from(modules)
.where(eq(modules.id, "PHYS101"))
.then((r) => r[0] ?? null);
assertExists(row);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration modules: get by id returns null when not found",
async fn() {
await truncateAll();
const row = await testDb
.select()
.from(modules)
.where(eq(modules.id, "NONEXISTENT"))
.then((r) => r[0] ?? null);
assertEquals(row, null);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration modules: duplicate id insert fails",
name: "e2e modules: GET /modules returns all for non-employee",
async fn() {
await truncateAll();
await seedModules([{ id: "MATH101", nom: "Mathématiques" }]);
await assertRejects(() =>
testDb.insert(modules).values({ id: "MATH101", nom: "Doublon" })
const res = await modulesHandler.GET!(
makeGetRequest("/modules"),
makeContextWithAffiliation("student"),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.length, 1);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- POST /modules ---
Deno.test({
name: "e2e modules: POST /modules creates module (201)",
async fn() {
await truncateAll();
const res = await modulesHandler.POST!(
makeJsonRequest("/modules", "POST", { id: "PHYS101", nom: "Physique" }),
makeEmployeeContext(),
);
assertEquals(res.status, 201);
const body = await res.json();
assertEquals(body.id, "PHYS101");
assertEquals(body.nom, "Physique");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration modules: update nom",
name: "e2e modules: POST /modules 409 on duplicate id",
async fn() {
await truncateAll();
await seedModules([{ id: "MATH101", nom: "Mathématiques" }]);
const res = await modulesHandler.POST!(
makeJsonRequest("/modules", "POST", { id: "MATH101", nom: "Doublon" }),
makeEmployeeContext(),
);
assertEquals(res.status, 409);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e modules: POST /modules 400 on missing fields",
async fn() {
await truncateAll();
const res = await modulesHandler.POST!(
makeJsonRequest("/modules", "POST", { id: "X" }),
makeEmployeeContext(),
);
assertEquals(res.status, 400);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e modules: POST /modules 403 for non-employee",
async fn() {
await truncateAll();
const res = await modulesHandler.POST!(
makeJsonRequest("/modules", "POST", { id: "X", nom: "Y" }),
makeContextWithAffiliation("student"),
);
assertEquals(res.status, 403);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- GET /modules/:id ---
Deno.test({
name: "e2e modules: GET /modules/:id returns module",
async fn() {
await truncateAll();
await seedModules([{ id: "ELEC201", nom: "Électronique" }]);
const [updated] = await testDb
.update(modules)
.set({ nom: "Électronique numérique" })
.where(eq(modules.id, "ELEC201"))
.returning();
assertEquals(updated.nom, "Électronique numérique");
const res = await moduleHandler.GET!(
makeGetRequest("/modules/ELEC201"),
makeEmployeeContext({ idModule: "ELEC201" }),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.nom, "Électronique");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration modules: delete removes the module",
name: "e2e modules: GET /modules/:id 404 when not found",
async fn() {
await truncateAll();
await seedModules([{ id: "BIO101", nom: "Biologie" }]);
await testDb.delete(modules).where(eq(modules.id, "BIO101"));
const row = await testDb
.select()
.from(modules)
.where(eq(modules.id, "BIO101"))
.then((r) => r[0] ?? null);
assertEquals(row, null);
const res = await moduleHandler.GET!(
makeGetRequest("/modules/GHOST"),
makeEmployeeContext({ idModule: "GHOST" }),
);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- PUT /modules/:id ---
Deno.test({
name: "e2e modules: PUT /modules/:id updates nom",
async fn() {
await truncateAll();
await seedModules([{ id: "CHIM101", nom: "Chimie" }]);
const res = await moduleHandler.PUT!(
makeJsonRequest("/modules/CHIM101", "PUT", { nom: "Chimie organique" }),
makeEmployeeContext({ idModule: "CHIM101" }),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.nom, "Chimie organique");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e modules: PUT /modules/:id 404 when not found",
async fn() {
await truncateAll();
const res = await moduleHandler.PUT!(
makeJsonRequest("/modules/GHOST", "PUT", { nom: "X" }),
makeEmployeeContext({ idModule: "GHOST" }),
);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- DELETE /modules/:id ---
Deno.test({
name: "e2e modules: DELETE /modules/:id returns 204",
async fn() {
await truncateAll();
await seedModules([{ id: "BIO101", nom: "Biologie" }]);
const res = await moduleHandler.DELETE!(
makeGetRequest("/modules/BIO101"),
makeEmployeeContext({ idModule: "BIO101" }),
);
assertEquals(res.status, 204);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e modules: DELETE /modules/:id 404 when not found",
async fn() {
await truncateAll();
const res = await moduleHandler.DELETE!(
makeGetRequest("/modules/GHOST"),
makeEmployeeContext({ idModule: "GHOST" }),
);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
+230 -101
View File
@@ -1,153 +1,282 @@
// Integration tests for /notes — Drizzle ORM direct on real DB
// E2E tests for /notes endpoints — handler + real DB
import { assertEquals, assertExists, assertRejects } from "@std/assert";
import { assertEquals, assertExists } from "@std/assert";
import {
makeEmployeeContext,
makeGetRequest,
makeJsonRequest,
} from "../helpers/handler.ts";
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";
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: "integration notes: list all notes",
name: "e2e notes: GET /notes returns all notes",
async fn() {
await truncateAll();
await seedPromotions([{ id: "PROMO-2024" }]);
await seedPromotions([{ id: "P1" }]);
const [s] = await seedStudents([{
nom: "Dupont",
prenom: "Jean",
idPromo: "PROMO-2024",
idPromo: "P1",
}]);
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);
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: "integration notes: create and retrieve by composite key",
name: "e2e notes: GET /notes?numEtud filters by student",
async fn() {
await truncateAll();
await seedPromotions([{ id: "PROMO-2024" }]);
const [s] = await seedStudents([{
await seedPromotions([{ id: "P1" }]);
const [s1] = await seedStudents([{
nom: "Dupont",
prenom: "Jean",
idPromo: "P1",
}]);
const [s2] = await seedStudents([{
nom: "Martin",
prenom: "Alice",
idPromo: "PROMO-2024",
idPromo: "P1",
}]);
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,
})
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: "integration notes: update note value",
name: "e2e notes: GET /notes?numEtud=NaN returns 400",
async fn() {
await truncateAll();
await seedPromotions([{ id: "PROMO-2024" }]);
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: "PROMO-2024",
idPromo: "P1",
}]);
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);
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: "integration notes: delete removes the note",
name: "e2e notes: GET /notes/:numEtud/:idModule 404 when not found",
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 res = await noteHandler.GET!(
makeGetRequest("/notes/99999/GHOST"),
makeEmployeeContext({ numEtud: "99999", idModule: "GHOST" }),
);
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);
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,
+60
View File
@@ -0,0 +1,60 @@
// #115 - E2E tests for GET /permissions
import { assertEquals, assertExists } from "@std/assert";
import { makeEmployeeContext, makeGetRequest } from "../helpers/handler.ts";
import { seedPermissions, truncateAll } from "../helpers/db_integration.ts";
import { handler as permissionsHandler } from "$apps/admin/api/permissions.ts";
const PERMISSIONS = [
{ id: "note_read", nom: "Consulter les notes des étudiants" },
{ id: "note_write", nom: "Saisir et modifier les notes" },
{ id: "student_read", nom: "Consulter la liste des étudiants" },
{
id: "student_write",
nom: "Gérer les étudiants (ajout, modification, suppression)",
},
{ id: "module_read", nom: "Consulter les modules et enseignements" },
{ id: "module_write", nom: "Gérer les modules et enseignements" },
{ id: "user_read", nom: "Consulter les utilisateurs et leurs rôles" },
{ id: "user_write", nom: "Gérer les utilisateurs et leurs rôles" },
{ id: "role_write", nom: "Gérer les rôles et leurs permissions" },
];
Deno.test({
name: "e2e permissions: GET /permissions returns all 9 permissions",
async fn() {
await truncateAll();
await seedPermissions(PERMISSIONS);
const res = await permissionsHandler.GET!(
makeGetRequest("/permissions"),
makeEmployeeContext(),
);
assertEquals(res.status, 200);
const text = await res.text();
const data = JSON.parse(text);
assertEquals(data.length, 9);
assertExists(data.find((p: { id: string }) => p.id === "student_read"));
assertExists(data.find((p: { id: string }) => p.id === "role_write"));
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e permissions: GET /permissions - all entries have id and nom",
async fn() {
await truncateAll();
await seedPermissions(PERMISSIONS);
const res = await permissionsHandler.GET!(
makeGetRequest("/permissions"),
makeEmployeeContext(),
);
const data: { id: string; nom: string }[] = await res.json();
for (const p of data) {
assertEquals(typeof p.id, "string");
assertEquals(typeof p.nom, "string");
}
},
sanitizeResources: false,
sanitizeOps: false,
});
+156 -56
View File
@@ -1,111 +1,211 @@
// #110 - Integration tests for /promotions endpoints
// #110 - E2E tests for /promotions endpoints
import { assertEquals, assertExists } from "@std/assert";
import { assertEquals } from "@std/assert";
import {
seedPromotions,
testDb,
truncateAll,
} from "../helpers/db_integration.ts";
import { promotions } from "$root/databases/schema.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
makeContextWithAffiliation,
makeEmployeeContext,
makeGetRequest,
makeJsonRequest,
} from "../helpers/handler.ts";
import { seedPromotions, truncateAll } from "../helpers/db_integration.ts";
import { handler as promotionsHandler } from "$apps/students/api/promotions.ts";
import { handler as promotionHandler } from "$apps/students/api/promotions/[idPromo].ts";
// --- GET /promotions ---
Deno.test({
name: "integration promotions: list all",
name: "e2e promotions: GET /promotions returns all as employee",
async fn() {
await truncateAll();
await seedPromotions([
{ id: "PEIP1-2024", annee: "2024" },
{ id: "PEIP2-2024", annee: "2024" },
]);
const rows = await testDb.select().from(promotions);
assertEquals(rows.length, 2);
const res = await promotionsHandler.GET!(
makeGetRequest("/promotions"),
makeEmployeeContext(),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.length, 2);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration promotions: create and retrieve by id",
name: "e2e promotions: GET /promotions returns empty for non-employee",
async fn() {
await truncateAll();
const [created] = await testDb
.insert(promotions)
.values({ id: "INFO3-2025", annee: "2025" })
.returning();
assertExists(created);
assertEquals(created.id, "INFO3-2025");
assertEquals(created.annee, "2025");
const row = await testDb
.select()
.from(promotions)
.where(eq(promotions.id, "INFO3-2025"))
.then((r) => r[0] ?? null);
assertExists(row);
await seedPromotions([{ id: "PEIP1-2024", annee: "2024" }]);
const res = await promotionsHandler.GET!(
makeGetRequest("/promotions"),
makeContextWithAffiliation("student"),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.length, 0);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- POST /promotions ---
Deno.test({
name: "integration promotions: get by id returns null when not found",
name: "e2e promotions: POST /promotions creates promotion (201)",
async fn() {
await truncateAll();
const row = await testDb
.select()
.from(promotions)
.where(eq(promotions.id, "NONEXISTENT"))
.then((r) => r[0] ?? null);
assertEquals(row, null);
const res = await promotionsHandler.POST!(
makeJsonRequest("/promotions", "POST", {
idPromo: "INFO3-2025",
annee: "2025",
}),
makeEmployeeContext(),
);
assertEquals(res.status, 201);
const body = await res.json();
assertEquals(body.id, "INFO3-2025");
assertEquals(body.annee, "2025");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration promotions: update annee",
name: "e2e promotions: POST /promotions 403 for non-employee",
async fn() {
await truncateAll();
const res = await promotionsHandler.POST!(
makeJsonRequest("/promotions", "POST", { idPromo: "X", annee: "2025" }),
makeContextWithAffiliation("student"),
);
assertEquals(res.status, 403);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e promotions: POST /promotions 400 on missing fields",
async fn() {
await truncateAll();
const res = await promotionsHandler.POST!(
makeJsonRequest("/promotions", "POST", { idPromo: "X" }),
makeEmployeeContext(),
);
assertEquals(res.status, 400);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- GET /promotions/:idPromo ---
Deno.test({
name: "e2e promotions: GET /promotions/:id returns promotion",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2024", annee: "2024" }]);
const res = await promotionHandler.GET!(
makeGetRequest("/promotions/INFO3-2024"),
makeEmployeeContext({ idPromo: "INFO3-2024" }),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.id, "INFO3-2024");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e promotions: GET /promotions/:id 404 when not found",
async fn() {
await truncateAll();
const res = await promotionHandler.GET!(
makeGetRequest("/promotions/GHOST"),
makeEmployeeContext({ idPromo: "GHOST" }),
);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e promotions: GET /promotions/:id 403 for non-employee",
async fn() {
await truncateAll();
const res = await promotionHandler.GET!(
makeGetRequest("/promotions/INFO3-2024"),
makeContextWithAffiliation("student", { idPromo: "INFO3-2024" }),
);
assertEquals(res.status, 403);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- PUT /promotions/:idPromo ---
Deno.test({
name: "e2e promotions: PUT /promotions/:id updates annee",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2023", annee: "2023" }]);
const [updated] = await testDb
.update(promotions)
.set({ annee: "2024" })
.where(eq(promotions.id, "INFO3-2023"))
.returning();
assertExists(updated);
assertEquals(updated.annee, "2024");
const res = await promotionHandler.PUT!(
makeJsonRequest("/promotions/INFO3-2023", "PUT", { annee: "2024" }),
makeEmployeeContext({ idPromo: "INFO3-2023" }),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.annee, "2024");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration promotions: delete removes the row",
name: "e2e promotions: PUT /promotions/:id 404 when not found",
async fn() {
await truncateAll();
const res = await promotionHandler.PUT!(
makeJsonRequest("/promotions/GHOST", "PUT", { annee: "2025" }),
makeEmployeeContext({ idPromo: "GHOST" }),
);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- DELETE /promotions/:idPromo ---
Deno.test({
name: "e2e promotions: DELETE /promotions/:id returns 204",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2022", annee: "2022" }]);
await testDb.delete(promotions).where(eq(promotions.id, "INFO3-2022"));
const row = await testDb
.select()
.from(promotions)
.where(eq(promotions.id, "INFO3-2022"))
.then((r) => r[0] ?? null);
assertEquals(row, null);
const res = await promotionHandler.DELETE!(
makeGetRequest("/promotions/INFO3-2022"),
makeEmployeeContext({ idPromo: "INFO3-2022" }),
);
assertEquals(res.status, 204);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration promotions: update non-existent returns empty",
name: "e2e promotions: DELETE /promotions/:id 404 when not found",
async fn() {
await truncateAll();
const result = await testDb
.update(promotions)
.set({ annee: "2099" })
.where(eq(promotions.id, "GHOST"))
.returning();
assertEquals(result.length, 0);
const res = await promotionHandler.DELETE!(
makeGetRequest("/promotions/GHOST"),
makeEmployeeContext({ idPromo: "GHOST" }),
);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
+592
View File
@@ -0,0 +1,592 @@
// 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 } 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/admin/api/ues.ts";
import { handler as ueModulesHandler } from "$apps/admin/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,
});
Deno.test({
name: "robustness: POST /modules malformed JSON → 500",
async fn() {
await truncateAll();
const res = await modulesHandler.POST!(
makeMalformedRequest("/modules"),
makeEmployeeContext(),
);
assertEquals(res.status, 500);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "robustness: POST /enseignements malformed JSON → 500",
async fn() {
await truncateAll();
const res = await enseignementsHandler.POST!(
makeMalformedRequest("/enseignements"),
makeEmployeeContext(),
);
assertEquals(res.status, 500);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "robustness: POST /users malformed JSON → 500",
async fn() {
await truncateAll();
const res = await usersHandler.POST!(
makeMalformedRequest("/users"),
makeEmployeeContext(),
);
assertEquals(res.status, 500);
},
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: POST /modules sans body → 500",
async fn() {
await truncateAll();
const res = await modulesHandler.POST!(
makeEmptyBodyRequest("/modules"),
makeEmployeeContext(),
);
assertEquals(res.status, 500);
},
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: POST /modules id=espaces → 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 /ues nom=espaces → 400",
async fn() {
await truncateAll();
const res = await uesHandler.POST!(
makeJsonRequest("/ues", "POST", { nom: " " }),
makeEmployeeContext(),
);
assertEquals(res.status, 400);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "robustness: POST /users id=espaces → 400",
async fn() {
await truncateAll();
const res = await usersHandler.POST!(
makeJsonRequest("/users", "POST", { id: " ", nom: "X", prenom: "Y" }),
makeEmployeeContext(),
);
assertEquals(res.status, 400);
},
sanitizeResources: false,
sanitizeOps: false,
});
// =============================================================================
// MAUVAIS TYPES
// =============================================================================
Deno.test({
name: "robustness: POST /notes note=string → 400",
async fn() {
await truncateAll();
await seedPromotions([{ id: "P1" }]);
const [s] = await seedStudents([{
nom: "Test",
prenom: "User",
idPromo: "P1",
}]);
await seedModules([{ id: "M1", nom: "Mod" }]);
const res = await notesHandler.POST!(
makeJsonRequest("/notes", "POST", {
note: "pas-un-nombre",
numEtud: s.numEtud,
idModule: "M1",
}),
makeEmployeeContext(),
);
assertEquals(res.status, 400);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "robustness: PUT /modules/:id nom=number → 400",
async fn() {
await truncateAll();
await seedModules([{ id: "M1", nom: "Mod" }]);
const res = await moduleHandler.PUT!(
makeJsonRequest("/modules/M1", "PUT", { nom: 42 }),
makeEmployeeContext({ idModule: "M1" }),
);
assertEquals(res.status, 400);
},
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: POST /notes note > 20 → 400",
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(),
);
assertEquals(res.status, 400);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "robustness: POST /notes note < 0 → 400",
async fn() {
await truncateAll();
await seedPromotions([{ id: "P1" }]);
const [s] = await seedStudents([{
nom: "Test",
prenom: "User",
idPromo: "P1",
}]);
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);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "robustness: POST /ue-modules coeff négatif → 400",
async fn() {
await truncateAll();
await seedPromotions([{ id: "P1" }]);
await seedModules([{ id: "M1", nom: "Mod" }]);
const [ue] = await seedUes([{ nom: "UE Info" }]);
const res = await ueModulesHandler.POST!(
makeJsonRequest("/ue-modules", "POST", {
idModule: "M1",
idUE: ue.id,
idPromo: "P1",
coeff: -3,
}),
makeEmployeeContext(),
);
assertEquals(res.status, 400);
},
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,
});
+121 -69
View File
@@ -1,122 +1,174 @@
// #112 - Integration tests for /roles endpoints
// #112 - E2E tests for /roles endpoints
import { assertEquals, assertExists } from "@std/assert";
import {
makeEmployeeContext,
makeGetRequest,
makeJsonRequest,
} from "../helpers/handler.ts";
import { seedRoles, testDb, truncateAll } from "../helpers/db_integration.ts";
import { permissions, rolePermissions, roles } from "$root/databases/schema.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
import { permissions } from "$root/databases/schema.ts";
import { handler as rolesHandler } from "$apps/admin/api/roles.ts";
import { handler as roleHandler } from "$apps/admin/api/roles/[idRole].ts";
// --- GET /roles ---
Deno.test({
name: "integration roles: list all roles",
name: "e2e roles: GET /roles returns all with permissions",
async fn() {
await truncateAll();
await seedRoles([{ nom: "admin" }, { nom: "employee" }]);
const rows = await testDb.select().from(roles);
assertEquals(rows.length, 2);
const res = await rolesHandler.GET!(
makeGetRequest("/roles"),
makeEmployeeContext(),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.length, 2);
assertExists(body[0].permissions);
assertEquals(Array.isArray(body[0].permissions), true);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- POST /roles ---
Deno.test({
name: "integration roles: create and retrieve by id",
name: "e2e roles: POST /roles creates role (201)",
async fn() {
await truncateAll();
const [created] = await testDb.insert(roles).values({ nom: "viewer" })
.returning();
assertExists(created.id);
assertEquals(created.nom, "viewer");
const row = await testDb
.select()
.from(roles)
.where(eq(roles.id, created.id))
.then((r) => r[0] ?? null);
assertExists(row);
const res = await rolesHandler.POST!(
makeJsonRequest("/roles", "POST", { nom: "viewer" }),
makeEmployeeContext(),
);
assertEquals(res.status, 201);
const body = await res.json();
assertExists(body.id);
assertEquals(body.nom, "viewer");
assertEquals(body.permissions, []);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration roles: assign and retrieve permissions",
name: "e2e roles: POST /roles 400 on missing nom",
async fn() {
await truncateAll();
const res = await rolesHandler.POST!(
makeJsonRequest("/roles", "POST", {}),
makeEmployeeContext(),
);
assertEquals(res.status, 400);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- GET /roles/:id ---
Deno.test({
name: "e2e roles: GET /roles/:id returns role with permissions",
async fn() {
await truncateAll();
const [role] = await seedRoles([{ nom: "admin" }]);
await testDb.insert(permissions).values([
{ id: "student_read", nom: "Consulter les élèves" },
{ id: "student_write", nom: "Gérer les élèves" },
]);
await testDb.insert(rolePermissions).values([
{ idRole: role.id, idPermission: "student_read" },
{ idRole: role.id, idPermission: "student_write" },
]);
const perms = await testDb
.select()
.from(rolePermissions)
.where(eq(rolePermissions.idRole, role.id));
assertEquals(perms.length, 2);
const res = await roleHandler.GET!(
makeGetRequest(`/roles/${role.id}`),
makeEmployeeContext({ idRole: String(role.id) }),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.nom, "admin");
assertEquals(Array.isArray(body.permissions), true);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration roles: update role nom",
name: "e2e roles: GET /roles/:id 404 when not found",
async fn() {
await truncateAll();
const res = await roleHandler.GET!(
makeGetRequest("/roles/9999"),
makeEmployeeContext({ idRole: "9999" }),
);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- PUT /roles/:id ---
Deno.test({
name: "e2e roles: PUT /roles/:id updates nom and permissions",
async fn() {
await truncateAll();
const [role] = await seedRoles([{ nom: "employee" }]);
const [updated] = await testDb
.update(roles)
.set({ nom: "teacher" })
.where(eq(roles.id, role.id))
.returning();
assertEquals(updated.nom, "teacher");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration roles: reset permissions on update",
async fn() {
await truncateAll();
const [role] = await seedRoles([{ nom: "admin" }]);
await testDb.insert(permissions).values([
{ id: "note_read", nom: "Consulter les notes" },
{ id: "note_write", nom: "Gérer les notes" },
]);
await testDb.insert(rolePermissions).values([
{ idRole: role.id, idPermission: "note_read" },
]);
// reset
await testDb.delete(rolePermissions).where(
eq(rolePermissions.idRole, role.id),
const res = await roleHandler.PUT!(
makeJsonRequest(`/roles/${role.id}`, "PUT", {
nom: "teacher",
permissions: ["note_read"],
}),
makeEmployeeContext({ idRole: String(role.id) }),
);
await testDb.insert(rolePermissions).values([
{ idRole: role.id, idPermission: "note_write" },
]);
const perms = await testDb
.select()
.from(rolePermissions)
.where(eq(rolePermissions.idRole, role.id));
assertEquals(perms.length, 1);
assertEquals(perms[0].idPermission, "note_write");
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.nom, "teacher");
assertEquals(body.permissions, ["note_read"]);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration roles: delete role removes it",
name: "e2e roles: PUT /roles/:id 404 when not found",
async fn() {
await truncateAll();
const res = await roleHandler.PUT!(
makeJsonRequest("/roles/9999", "PUT", { nom: "ghost", permissions: [] }),
makeEmployeeContext({ idRole: "9999" }),
);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- DELETE /roles/:id ---
Deno.test({
name: "e2e roles: DELETE /roles/:id returns 204",
async fn() {
await truncateAll();
const [role] = await seedRoles([{ nom: "moderator" }]);
await testDb.delete(roles).where(eq(roles.id, role.id));
const row = await testDb
.select()
.from(roles)
.where(eq(roles.id, role.id))
.then((r) => r[0] ?? null);
assertEquals(row, null);
const res = await roleHandler.DELETE!(
makeGetRequest(`/roles/${role.id}`),
makeEmployeeContext({ idRole: String(role.id) }),
);
assertEquals(res.status, 204);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e roles: DELETE /roles/:id 404 when not found",
async fn() {
await truncateAll();
const res = await roleHandler.DELETE!(
makeGetRequest("/roles/9999"),
makeEmployeeContext({ idRole: "9999" }),
);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
+193 -78
View File
@@ -1,18 +1,25 @@
// #109 - Integration tests for /students endpoints
// Teste les opérations DB directement avec une vraie base de données
// #109 - E2E tests for /students endpoints
// Appelle les handlers Fresh directement avec un vrai contexte + vraie DB
import { assertEquals, assertExists } from "@std/assert";
import {
makeContextWithAffiliation,
makeEmployeeContext,
makeGetRequest,
makeJsonRequest,
} from "../helpers/handler.ts";
import {
seedPromotions,
seedStudents,
testDb,
truncateAll,
} from "../helpers/db_integration.ts";
import { students } from "$root/databases/schema.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
import { handler as studentsHandler } from "$apps/students/api/students.ts";
import { handler as studentHandler } from "$apps/students/api/students/[numEtud].ts";
// --- GET /students ---
Deno.test({
name: "integration students: list all students",
name: "e2e students: GET /students returns all students as employee",
async fn() {
await truncateAll();
await seedPromotions([{ id: "PEIP1-2024" }]);
@@ -21,15 +28,42 @@ Deno.test({
{ nom: "Martin", prenom: "Alice", idPromo: "PEIP1-2024" },
]);
const rows = await testDb.select().from(students);
assertEquals(rows.length, 2);
const req = makeGetRequest("/students");
const ctx = makeEmployeeContext();
const res = await studentsHandler.GET!(req, ctx);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.length, 2);
assertExists(body.find((s: { nom: string }) => s.nom === "Dupont"));
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration students: filter by idPromo",
name: "e2e students: GET /students returns empty array for non-employee",
async fn() {
await truncateAll();
await seedPromotions([{ id: "PEIP1-2024" }]);
await seedStudents([
{ nom: "Dupont", prenom: "Jean", idPromo: "PEIP1-2024" },
]);
const req = makeGetRequest("/students");
const ctx = makeContextWithAffiliation("student");
const res = await studentsHandler.GET!(req, ctx);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.length, 0);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e students: GET /students?idPromo filters by promotion",
async fn() {
await truncateAll();
await seedPromotions([{ id: "PEIP1-2024" }, { id: "PEIP2-2024" }]);
@@ -39,63 +73,140 @@ Deno.test({
{ nom: "Durand", prenom: "Claire", idPromo: "PEIP2-2024" },
]);
const rows = await testDb
.select()
.from(students)
.where(eq(students.idPromo, "PEIP1-2024"));
assertEquals(rows.length, 2);
assertEquals(rows.every((s) => s.idPromo === "PEIP1-2024"), true);
const req = makeGetRequest("/students", { idPromo: "PEIP1-2024" });
const ctx = makeEmployeeContext();
const res = await studentsHandler.GET!(req, ctx);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.length, 2);
assertEquals(
body.every((s: { idPromo: string }) => s.idPromo === "PEIP1-2024"),
true,
);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- POST /students ---
Deno.test({
name: "integration students: create and retrieve by numEtud",
name: "e2e students: POST /students creates a student (201)",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2024" }]);
const [created] = await testDb
.insert(students)
.values({ nom: "Leroy", prenom: "Paul", idPromo: "INFO3-2024" })
.returning();
const req = makeJsonRequest("/students", "POST", {
nom: "Leroy",
prenom: "Paul",
idPromo: "INFO3-2024",
});
const ctx = makeEmployeeContext();
const res = await studentsHandler.POST!(req, ctx);
assertExists(created.numEtud);
const row = await testDb
.select()
.from(students)
.where(eq(students.numEtud, created.numEtud))
.then((r) => r[0] ?? null);
assertExists(row);
assertEquals(row.nom, "Leroy");
assertEquals(row.idPromo, "INFO3-2024");
assertEquals(res.status, 201);
const body = await res.json();
assertExists(body.numEtud);
assertEquals(body.nom, "Leroy");
assertEquals(body.idPromo, "INFO3-2024");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration students: get by numEtud returns null when not found",
name: "e2e students: POST /students 403 for non-employee",
async fn() {
await truncateAll();
const row = await testDb
.select()
.from(students)
.where(eq(students.numEtud, 999999))
.then((r) => r[0] ?? null);
const req = makeJsonRequest("/students", "POST", {
nom: "Test",
prenom: "User",
idPromo: "PEIP1-2024",
});
const ctx = makeContextWithAffiliation("student");
const res = await studentsHandler.POST!(req, ctx);
assertEquals(row, null);
assertEquals(res.status, 403);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration students: update student fields",
name: "e2e students: POST /students 400 when missing required fields",
async fn() {
await truncateAll();
const req = makeJsonRequest("/students", "POST", { nom: "Leroy" });
const ctx = makeEmployeeContext();
const res = await studentsHandler.POST!(req, ctx);
assertEquals(res.status, 400);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- GET /students/:numEtud ---
Deno.test({
name: "e2e students: GET /students/:numEtud returns student",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2024" }]);
const [s] = await seedStudents([
{ nom: "Bernard", prenom: "Lucie", idPromo: "INFO3-2024" },
]);
const req = makeGetRequest(`/students/${s.numEtud}`);
const ctx = makeEmployeeContext({ numEtud: String(s.numEtud) });
const res = await studentHandler.GET!(req, ctx);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.numEtud, s.numEtud);
assertEquals(body.nom, "Bernard");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e students: GET /students/:numEtud 404 when not found",
async fn() {
await truncateAll();
const req = makeGetRequest("/students/999999");
const ctx = makeEmployeeContext({ numEtud: "999999" });
const res = await studentHandler.GET!(req, ctx);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e students: GET /students/:numEtud 403 for non-employee",
async fn() {
await truncateAll();
const req = makeGetRequest("/students/12345");
const ctx = makeContextWithAffiliation("student", { numEtud: "12345" });
const res = await studentHandler.GET!(req, ctx);
assertEquals(res.status, 403);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- PUT /students/:numEtud ---
Deno.test({
name: "e2e students: PUT /students/:numEtud updates student",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2024" }, { id: "INFO4-2024" }]);
@@ -103,21 +214,47 @@ Deno.test({
{ nom: "Petit", prenom: "Hugo", idPromo: "INFO3-2024" },
]);
const [updated] = await testDb
.update(students)
.set({ nom: "Grand", idPromo: "INFO4-2024" })
.where(eq(students.numEtud, s.numEtud))
.returning();
const req = makeJsonRequest(`/students/${s.numEtud}`, "PUT", {
nom: "Grand",
prenom: "Hugo",
idPromo: "INFO4-2024",
});
const ctx = makeEmployeeContext({ numEtud: String(s.numEtud) });
const res = await studentHandler.PUT!(req, ctx);
assertEquals(updated.nom, "Grand");
assertEquals(updated.idPromo, "INFO4-2024");
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.nom, "Grand");
assertEquals(body.idPromo, "INFO4-2024");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration students: delete student",
name: "e2e students: PUT /students/:numEtud 404 when not found",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2024" }]);
const req = makeJsonRequest("/students/999999", "PUT", {
nom: "Ghost",
prenom: "Ghost",
idPromo: "INFO3-2024",
});
const ctx = makeEmployeeContext({ numEtud: "999999" });
const res = await studentHandler.PUT!(req, ctx);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- DELETE /students/:numEtud ---
Deno.test({
name: "e2e students: DELETE /students/:numEtud returns 204",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2024" }]);
@@ -125,48 +262,26 @@ Deno.test({
{ nom: "Thomas", prenom: "Eva", idPromo: "INFO3-2024" },
]);
await testDb.delete(students).where(eq(students.numEtud, s.numEtud));
const req = makeGetRequest(`/students/${s.numEtud}`);
const ctx = makeEmployeeContext({ numEtud: String(s.numEtud) });
const res = await studentHandler.DELETE!(req, ctx);
const row = await testDb
.select()
.from(students)
.where(eq(students.numEtud, s.numEtud))
.then((r) => r[0] ?? null);
assertEquals(row, null);
assertEquals(res.status, 204);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration students: update non-existent student returns empty",
name: "e2e students: DELETE /students/:numEtud 404 when not found",
async fn() {
await truncateAll();
const result = await testDb
.update(students)
.set({ nom: "Ghost" })
.where(eq(students.numEtud, 999999))
.returning();
const req = makeGetRequest("/students/999999");
const ctx = makeEmployeeContext({ numEtud: "999999" });
const res = await studentHandler.DELETE!(req, ctx);
assertEquals(result.length, 0);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration students: delete non-existent student returns empty",
async fn() {
await truncateAll();
const result = await testDb
.delete(students)
.where(eq(students.numEtud, 999999))
.returning();
assertEquals(result.length, 0);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
+248 -119
View File
@@ -1,19 +1,28 @@
// Integration tests for /ue-modules — Drizzle ORM direct on real DB
// E2E tests for /ue-modules endpoints — handler + real DB
import { assertEquals, assertExists, assertRejects } from "@std/assert";
import { assertEquals, assertExists } from "@std/assert";
import {
makeContextWithAffiliation,
makeEmployeeContext,
makeGetRequest,
makeJsonRequest,
} from "../helpers/handler.ts";
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";
import { handler as ueModulesHandler } from "$apps/admin/api/ue-modules.ts";
import { handler as ueModuleHandler } from "$apps/admin/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: "integration ue_modules: list all associations",
name: "e2e ue_modules: GET /ue-modules returns all associations",
async fn() {
await truncateAll();
await seedPromotions([{ id: "P1" }]);
@@ -23,41 +32,113 @@ Deno.test({
{ 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);
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: "integration ue_modules: create and retrieve by composite key",
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 Maths" }]);
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,
});
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);
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,
});
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);
// --- 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,
@@ -65,118 +146,166 @@ Deno.test({
Deno.test({
name:
"integration ue_modules: get by composite key returns null when not found",
"e2e ue_modules: GET /ue-modules/:idModule/:idUE/:idPromo 403 for non-employee",
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({
const res = await ueModuleHandler.GET!(
makeGetRequest("/ue-modules/M1/1/P1"),
makeContextWithAffiliation("student", {
idModule: "M1",
idUE: ue.id,
idUE: "1",
idPromo: "P1",
coeff: 5.0,
})
}),
);
assertEquals(res.status, 403);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration ue_modules: update coeff",
name:
"e2e ue_modules: GET /ue-modules/:idModule/:idUE/:idPromo 404 when not found",
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 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,
});
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);
// --- 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: "integration ue_modules: delete removes the association",
name:
"e2e ue_modules: PUT /ue-modules/:idModule/:idUE/:idPromo 403 for non-employee",
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);
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,
+126 -38
View File
@@ -1,89 +1,177 @@
// Integration tests for /ues — Drizzle ORM direct on real DB
// E2E tests for /ues endpoints — handler + 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";
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/admin/api/ues.ts";
import { handler as ueHandler } from "$apps/admin/api/ues/[idUE].ts";
// --- GET /ues ---
Deno.test({
name: "integration ues: list all UEs",
name: "e2e ues: GET /ues returns 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);
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: "integration ues: create and retrieve by id",
name: "e2e ues: GET /ues returns empty when no UEs",
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");
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: "integration ues: get by id returns null when not found",
name: "e2e ues: POST /ues creates UE (201)",
async fn() {
await truncateAll();
const row = await testDb.select().from(ues).where(eq(ues.id, 99999)).then((
r,
) => r[0] ?? null);
assertEquals(row, null);
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: "integration ues: update nom",
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 [updated] = await testDb.update(ues).set({
nom: "UE Chimie organique",
}).where(eq(ues.id, ue.id)).returning();
assertEquals(updated.nom, "UE Chimie organique");
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: "integration ues: delete removes the UE",
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" }]);
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);
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: "integration ues: nom is required (not null)",
name: "e2e ues: DELETE /ues/:id 404 when not found",
async fn() {
await truncateAll();
// deno-lint-ignore no-explicit-any
await assertRejects(() => testDb.insert(ues).values({ nom: null as any }));
const res = await ueHandler.DELETE!(
makeGetRequest("/ues/99999"),
makeEmployeeContext({ idUE: "99999" }),
);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
+208 -27
View File
@@ -1,57 +1,238 @@
// E2E tests for /users endpoints — handler + real DB
import { assertEquals, assertExists } from "@std/assert";
import {
closeTestPool,
makeEmployeeContext,
makeGetRequest,
makeJsonRequest,
} from "../helpers/handler.ts";
import {
seedRoles,
seedUsers,
testDb,
truncateAll,
} from "../helpers/db_integration.ts";
import { users } from "$root/databases/schema.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: "integration: GET /users - DB round trip",
name: "e2e users: GET /users returns all users",
async fn() {
await truncateAll();
const [role] = await seedRoles([{ nom: "employee" }]);
await seedUsers([
{ id: "dupont.jean", nom: "Dupont", prenom: "Jean", idRole: role.id },
{ id: "martin.alice", nom: "Martin", prenom: "Alice", idRole: role.id },
{ id: "dupont.jean", nom: "Dupont", prenom: "Jean" },
{ id: "martin.alice", nom: "Martin", prenom: "Alice" },
]);
const rows = await testDb.select().from(users);
assertEquals(rows.length, 2);
assertExists(rows.find((u) => u.id === "dupont.jean"));
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: "integration: INSERT user and retrieve by id",
name: "e2e users: GET /users returns empty when no users",
async fn() {
await truncateAll();
const [role] = await seedRoles([{ nom: "admin" }]);
const [created] = await testDb.insert(users).values({
id: "durand.claire",
nom: "Durand",
prenom: "Claire",
idRole: role.id,
}).returning();
assertExists(created);
assertEquals(created.id, "durand.claire");
assertEquals(created.nom, "Durand");
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: "integration: cleanup - close pool",
name: "e2e users: GET /users?idRole filters by role",
async fn() {
await closeTestPool();
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,