test: add full test coverage for notes, ues, ue-modules, ajustements, enseignements, users
- Unit tests (mock DB + API) for all missing endpoints - Integration tests (Drizzle direct) for all missing entities - E2E tests (handler + real DB) for all missing endpoints - Robustness tests: invalid inputs, SQL injection, type errors, business rule violations - Seed helpers: seedNotes, seedUeModules, seedEnseignements, seedAjustements - Add test:coverage and test:coverage:html tasks to deno.json Tests expose known handler bugs (marked [BUG] in test names): - ajustements PUT/DELETE: .where() without and() modifies all rows for student - Missing try/catch in modules, users, enseignements handlers - Whitespace accepted as valid string values - No type or business rule validation (note bounds, coeff >= 0)
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
// E2E tests for /ue-modules endpoints — handler + real DB
|
||||
|
||||
import { assertEquals, assertExists } from "@std/assert";
|
||||
import {
|
||||
makeContextWithAffiliation,
|
||||
makeEmployeeContext,
|
||||
makeGetRequest,
|
||||
makeJsonRequest,
|
||||
} from "../helpers/handler.ts";
|
||||
import {
|
||||
seedModules,
|
||||
seedPromotions,
|
||||
seedUeModules,
|
||||
seedUes,
|
||||
truncateAll,
|
||||
} from "../helpers/db_integration.ts";
|
||||
import { handler as ueModulesHandler } from "$apps/notes/api/ue-modules.ts";
|
||||
import { handler as ueModuleHandler } from "$apps/notes/api/ue-modules/[idModule]/[idUE]/[idPromo].ts";
|
||||
import { ueModules as ueModulesTable } from "$root/databases/schema.ts";
|
||||
import { testDb } from "../helpers/db_integration.ts";
|
||||
|
||||
// --- GET /ue-modules ---
|
||||
|
||||
Deno.test({
|
||||
name: "e2e ue_modules: GET /ue-modules returns all associations",
|
||||
async fn() {
|
||||
await truncateAll();
|
||||
await seedPromotions([{ id: "P1" }]);
|
||||
await seedModules([{ id: "M1", nom: "Mod A" }, { id: "M2", nom: "Mod B" }]);
|
||||
const [ue] = await seedUes([{ nom: "UE Info" }]);
|
||||
await seedUeModules([
|
||||
{ idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: 2.0 },
|
||||
{ idModule: "M2", idUE: ue.id, idPromo: "P1", coeff: 3.0 },
|
||||
]);
|
||||
const res = await ueModulesHandler.GET!(makeGetRequest("/ue-modules"), makeEmployeeContext());
|
||||
assertEquals(res.status, 200);
|
||||
const body = await res.json();
|
||||
assertEquals(body.length, 2);
|
||||
},
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: "e2e ue_modules: GET /ue-modules?idPromo filters by promo",
|
||||
async fn() {
|
||||
await truncateAll();
|
||||
await seedPromotions([{ id: "P1" }, { id: "P2" }]);
|
||||
await seedModules([{ id: "M1", nom: "Mod A" }]);
|
||||
const [ue] = await seedUes([{ nom: "UE Info" }]);
|
||||
await seedUeModules([
|
||||
{ idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: 2.0 },
|
||||
{ idModule: "M1", idUE: ue.id, idPromo: "P2", coeff: 3.0 },
|
||||
]);
|
||||
const res = await ueModulesHandler.GET!(
|
||||
makeGetRequest("/ue-modules", { idPromo: "P1" }),
|
||||
makeEmployeeContext(),
|
||||
);
|
||||
assertEquals(res.status, 200);
|
||||
const body = await res.json();
|
||||
assertEquals(body.length, 1);
|
||||
assertEquals(body[0].idPromo, "P1");
|
||||
},
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
});
|
||||
|
||||
// --- POST /ue-modules ---
|
||||
|
||||
Deno.test({
|
||||
name: "e2e ue_modules: POST /ue-modules creates association (201)",
|
||||
async fn() {
|
||||
await truncateAll();
|
||||
await seedPromotions([{ id: "P1" }]);
|
||||
await seedModules([{ id: "M1", nom: "Mod A" }]);
|
||||
const [ue] = await seedUes([{ nom: "UE Info" }]);
|
||||
const res = await ueModulesHandler.POST!(
|
||||
makeJsonRequest("/ue-modules", "POST", { idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: 4.0 }),
|
||||
makeEmployeeContext(),
|
||||
);
|
||||
assertEquals(res.status, 201);
|
||||
const body = await res.json();
|
||||
assertExists(body.idModule);
|
||||
assertEquals(body.coeff, 4.0);
|
||||
},
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: "e2e ue_modules: POST /ue-modules 400 on missing fields",
|
||||
async fn() {
|
||||
await truncateAll();
|
||||
const res = await ueModulesHandler.POST!(
|
||||
makeJsonRequest("/ue-modules", "POST", { idModule: "M1" }),
|
||||
makeEmployeeContext(),
|
||||
);
|
||||
assertEquals(res.status, 400);
|
||||
},
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
});
|
||||
|
||||
// --- GET /ue-modules/:idModule/:idUE/:idPromo ---
|
||||
|
||||
Deno.test({
|
||||
name: "e2e ue_modules: GET /ue-modules/:idModule/:idUE/:idPromo returns correct association (employee)",
|
||||
async fn() {
|
||||
await truncateAll();
|
||||
await seedPromotions([{ id: "P1" }, { id: "P2" }]);
|
||||
await seedModules([{ id: "M1", nom: "Mod A" }, { id: "M2", nom: "Mod B" }]);
|
||||
const [ue1, ue2] = await seedUes([{ nom: "UE Info" }, { nom: "UE Maths" }]);
|
||||
// Plusieurs lignes qui partagent idModule="M1" — le handler doit discriminer par idUE ET idPromo
|
||||
await seedUeModules([
|
||||
{ idModule: "M1", idUE: ue1.id, idPromo: "P1", coeff: 3.5 },
|
||||
{ idModule: "M1", idUE: ue2.id, idPromo: "P1", coeff: 1.0 },
|
||||
{ idModule: "M1", idUE: ue1.id, idPromo: "P2", coeff: 2.0 },
|
||||
{ idModule: "M2", idUE: ue1.id, idPromo: "P1", coeff: 4.0 },
|
||||
]);
|
||||
const res = await ueModuleHandler.GET!(
|
||||
makeGetRequest(`/ue-modules/M1/${ue1.id}/P1`),
|
||||
makeEmployeeContext({ idModule: "M1", idUE: String(ue1.id), idPromo: "P1" }),
|
||||
);
|
||||
assertEquals(res.status, 200);
|
||||
const body = await res.json();
|
||||
// Doit retourner exactement M1/ue1/P1 avec coeff 3.5, pas une autre ligne
|
||||
assertEquals(body.coeff, 3.5);
|
||||
assertEquals(body.idPromo, "P1");
|
||||
},
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: "e2e ue_modules: GET /ue-modules/:idModule/:idUE/:idPromo 403 for non-employee",
|
||||
async fn() {
|
||||
await truncateAll();
|
||||
const res = await ueModuleHandler.GET!(
|
||||
makeGetRequest("/ue-modules/M1/1/P1"),
|
||||
makeContextWithAffiliation("student", { idModule: "M1", idUE: "1", idPromo: "P1" }),
|
||||
);
|
||||
assertEquals(res.status, 403);
|
||||
},
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: "e2e ue_modules: GET /ue-modules/:idModule/:idUE/:idPromo 404 when not found",
|
||||
async fn() {
|
||||
await truncateAll();
|
||||
const res = await ueModuleHandler.GET!(
|
||||
makeGetRequest("/ue-modules/GHOST/1/GHOST"),
|
||||
makeEmployeeContext({ idModule: "GHOST", idUE: "1", idPromo: "GHOST" }),
|
||||
);
|
||||
assertEquals(res.status, 404);
|
||||
},
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
});
|
||||
|
||||
// --- PUT /ue-modules/:idModule/:idUE/:idPromo ---
|
||||
|
||||
Deno.test({
|
||||
name: "e2e ue_modules: PUT /ue-modules/:idModule/:idUE/:idPromo updates only the targeted row (employee)",
|
||||
async fn() {
|
||||
await truncateAll();
|
||||
await seedPromotions([{ id: "P1" }, { id: "P2" }]);
|
||||
await seedModules([{ id: "M1", nom: "Mod A" }]);
|
||||
const [ue1, ue2] = await seedUes([{ nom: "UE Info" }, { nom: "UE Maths" }]);
|
||||
// Deux lignes avec même idModule — le PUT ne doit modifier que celle ciblée
|
||||
await seedUeModules([
|
||||
{ idModule: "M1", idUE: ue1.id, idPromo: "P1", coeff: 2.0 },
|
||||
{ idModule: "M1", idUE: ue2.id, idPromo: "P2", coeff: 9.0 },
|
||||
]);
|
||||
const res = await ueModuleHandler.PUT!(
|
||||
makeJsonRequest(`/ue-modules/M1/${ue1.id}/P1`, "PUT", { coeff: 5.0 }),
|
||||
makeEmployeeContext({ idModule: "M1", idUE: String(ue1.id), idPromo: "P1" }),
|
||||
);
|
||||
assertEquals(res.status, 200);
|
||||
const body = await res.json();
|
||||
assertEquals(body.coeff, 5.0);
|
||||
assertEquals(body.idPromo, "P1");
|
||||
},
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: "e2e ue_modules: PUT /ue-modules/:idModule/:idUE/:idPromo 403 for non-employee",
|
||||
async fn() {
|
||||
await truncateAll();
|
||||
const res = await ueModuleHandler.PUT!(
|
||||
makeJsonRequest("/ue-modules/M1/1/P1", "PUT", { coeff: 5.0 }),
|
||||
makeContextWithAffiliation("student", { idModule: "M1", idUE: "1", idPromo: "P1" }),
|
||||
);
|
||||
assertEquals(res.status, 403);
|
||||
},
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: "e2e ue_modules: PUT /ue-modules/:idModule/:idUE/:idPromo 404 when not found",
|
||||
async fn() {
|
||||
await truncateAll();
|
||||
const res = await ueModuleHandler.PUT!(
|
||||
makeJsonRequest("/ue-modules/GHOST/1/GHOST", "PUT", { coeff: 5.0 }),
|
||||
makeEmployeeContext({ idModule: "GHOST", idUE: "1", idPromo: "GHOST" }),
|
||||
);
|
||||
assertEquals(res.status, 404);
|
||||
},
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
});
|
||||
|
||||
// --- DELETE /ue-modules/:idModule/:idUE/:idPromo ---
|
||||
|
||||
Deno.test({
|
||||
name: "e2e ue_modules: DELETE /ue-modules/:idModule/:idUE/:idPromo deletes only targeted row (employee)",
|
||||
async fn() {
|
||||
await truncateAll();
|
||||
await seedPromotions([{ id: "P1" }, { id: "P2" }]);
|
||||
await seedModules([{ id: "M1", nom: "Mod A" }]);
|
||||
const [ue1, ue2] = await seedUes([{ nom: "UE Info" }, { nom: "UE Maths" }]);
|
||||
// Deux lignes avec même idModule — seule celle ciblée doit être supprimée
|
||||
await seedUeModules([
|
||||
{ idModule: "M1", idUE: ue1.id, idPromo: "P1", coeff: 2.0 },
|
||||
{ idModule: "M1", idUE: ue2.id, idPromo: "P2", coeff: 4.0 },
|
||||
]);
|
||||
const res = await ueModuleHandler.DELETE!(
|
||||
makeGetRequest(`/ue-modules/M1/${ue1.id}/P1`),
|
||||
makeEmployeeContext({ idModule: "M1", idUE: String(ue1.id), idPromo: "P1" }),
|
||||
);
|
||||
assertEquals(res.status, 204);
|
||||
// L'autre ligne doit toujours exister
|
||||
const remaining = await testDb.select().from(ueModulesTable);
|
||||
assertEquals(remaining.length, 1);
|
||||
assertEquals(remaining[0].idUE, ue2.id);
|
||||
},
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: "e2e ue_modules: DELETE /ue-modules/:idModule/:idUE/:idPromo 403 for non-employee",
|
||||
async fn() {
|
||||
await truncateAll();
|
||||
const res = await ueModuleHandler.DELETE!(
|
||||
makeGetRequest("/ue-modules/M1/1/P1"),
|
||||
makeContextWithAffiliation("student", { idModule: "M1", idUE: "1", idPromo: "P1" }),
|
||||
);
|
||||
assertEquals(res.status, 403);
|
||||
},
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: "e2e ue_modules: DELETE /ue-modules/:idModule/:idUE/:idPromo 404 when not found",
|
||||
async fn() {
|
||||
await truncateAll();
|
||||
const res = await ueModuleHandler.DELETE!(
|
||||
makeGetRequest("/ue-modules/GHOST/1/GHOST"),
|
||||
makeEmployeeContext({ idModule: "GHOST", idUE: "1", idPromo: "GHOST" }),
|
||||
);
|
||||
assertEquals(res.status, 404);
|
||||
},
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
});
|
||||
Reference in New Issue
Block a user