Files
PolyMPR/tests/e2e/robustness_test.ts
T
djalim 2f4d8db1bf
Check Deno code / Check Deno code (pull_request) Failing after 6s
Tests / Unit tests (pull_request) Successful in 13s
Tests / Integration tests (pull_request) Failing after 1m14s
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)
2026-04-26 18:25:00 +02:00

560 lines
18 KiB
TypeScript

// Robustness tests — input validation & side-effect isolation
//
// Chaque test documente le comportement réel du handler face à des entrées invalides.
// Les tests marqués [BUG] représentent le comportement ATTENDU — ils échouent
// intentionnellement pour exposer un bug dans le handler ciblé.
import { assertEquals, assertRejects } from "@std/assert";
import {
makeContextWithAffiliation,
makeEmployeeContext,
makeGetRequest,
makeJsonRequest,
} from "../helpers/handler.ts";
import {
seedModules,
seedPromotions,
seedStudents,
seedUes,
truncateAll,
} from "../helpers/db_integration.ts";
import { handler as modulesHandler } from "$apps/admin/api/modules.ts";
import { handler as moduleHandler } from "$apps/admin/api/modules/[idModule].ts";
import { handler as notesHandler } from "$apps/notes/api/notes.ts";
import { handler as uesHandler } from "$apps/notes/api/ues.ts";
import { handler as ueModulesHandler } from "$apps/notes/api/ue-modules.ts";
import { handler as ajustementsHandler } from "$apps/notes/api/ajustements.ts";
import { handler as enseignementsHandler } from "$apps/admin/api/enseignements.ts";
import { handler as usersHandler } from "$apps/admin/api/users.ts";
// Helper : request POST avec un body JSON invalide
function makeMalformedRequest(path: string): Request {
return new Request(`http://localhost${path}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: "{ ceci n'est pas du json }",
});
}
// Helper : request POST sans body du tout
function makeEmptyBodyRequest(path: string, method = "POST"): Request {
return new Request(`http://localhost${path}`, { method });
}
// =============================================================================
// JSON MALFORMÉ
// =============================================================================
// Handlers AVEC try/catch → retournent 500
// Handlers SANS try/catch → throwent (assertRejects)
Deno.test({
name: "robustness: POST /notes malformed JSON → 500 (try/catch présent)",
async fn() {
await truncateAll();
const res = await notesHandler.POST!(
makeMalformedRequest("/notes"),
makeEmployeeContext(),
);
assertEquals(res.status, 500);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "robustness: POST /ues malformed JSON → 500 (try/catch présent)",
async fn() {
await truncateAll();
const res = await uesHandler.POST!(
makeMalformedRequest("/ues"),
makeEmployeeContext(),
);
assertEquals(res.status, 500);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "robustness: POST /ue-modules malformed JSON → 500 (try/catch présent)",
async fn() {
await truncateAll();
const res = await ueModulesHandler.POST!(
makeMalformedRequest("/ue-modules"),
makeEmployeeContext(),
);
assertEquals(res.status, 500);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "robustness: POST /ajustements malformed JSON → 500 (try/catch présent)",
async fn() {
await truncateAll();
const res = await ajustementsHandler.POST!(
makeMalformedRequest("/ajustements"),
makeEmployeeContext(),
);
assertEquals(res.status, 500);
},
sanitizeResources: false,
sanitizeOps: false,
});
// Handlers SANS try/catch — throwent au lieu de retourner 500
// [BUG] Ces handlers devraient retourner 500, pas throw
Deno.test({
name: "robustness [BUG]: POST /modules malformed JSON → throw (pas de try/catch)",
async fn() {
await truncateAll();
await assertRejects(() =>
modulesHandler.POST!(
makeMalformedRequest("/modules"),
makeEmployeeContext(),
)
);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "robustness [BUG]: POST /enseignements malformed JSON → throw (pas de try/catch)",
async fn() {
await truncateAll();
await assertRejects(() =>
enseignementsHandler.POST!(
makeMalformedRequest("/enseignements"),
makeEmployeeContext(),
)
);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "robustness [BUG]: POST /users malformed JSON → throw (pas de try/catch)",
async fn() {
await truncateAll();
await assertRejects(() =>
usersHandler.POST!(
makeMalformedRequest("/users"),
makeEmployeeContext(),
)
);
},
sanitizeResources: false,
sanitizeOps: false,
});
// =============================================================================
// BODY ABSENT
// =============================================================================
Deno.test({
name: "robustness: POST /notes sans body → 500",
async fn() {
await truncateAll();
const res = await notesHandler.POST!(
makeEmptyBodyRequest("/notes"),
makeEmployeeContext(),
);
assertEquals(res.status, 500);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "robustness [BUG]: POST /modules sans body → throw (pas de try/catch)",
async fn() {
await truncateAll();
await assertRejects(() =>
modulesHandler.POST!(
makeEmptyBodyRequest("/modules"),
makeEmployeeContext(),
)
);
},
sanitizeResources: false,
sanitizeOps: false,
});
// =============================================================================
// CHAÎNES VIDES — comportement correct ✓
// =============================================================================
Deno.test({
name: "robustness: POST /modules id vide → 400",
async fn() {
await truncateAll();
const res = await modulesHandler.POST!(
makeJsonRequest("/modules", "POST", { id: "", nom: "Test" }),
makeEmployeeContext(),
);
assertEquals(res.status, 400);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "robustness: POST /modules nom vide → 400",
async fn() {
await truncateAll();
const res = await modulesHandler.POST!(
makeJsonRequest("/modules", "POST", { id: "M1", nom: "" }),
makeEmployeeContext(),
);
assertEquals(res.status, 400);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "robustness: POST /ues nom vide → 400",
async fn() {
await truncateAll();
const res = await uesHandler.POST!(
makeJsonRequest("/ues", "POST", { nom: "" }),
makeEmployeeContext(),
);
assertEquals(res.status, 400);
},
sanitizeResources: false,
sanitizeOps: false,
});
// =============================================================================
// CHAÎNES AVEC ESPACES SEULS — [BUG] passent !field et s'insèrent en DB
// =============================================================================
Deno.test({
name: "robustness [BUG]: POST /modules id=espaces → devrait être 400, retourne 201",
async fn() {
await truncateAll();
const res = await modulesHandler.POST!(
makeJsonRequest("/modules", "POST", { id: " ", nom: "Test" }),
makeEmployeeContext(),
);
// Le handler vérifie !body.id → " " est truthy → passe → s'insère
// Comportement attendu : 400
// Comportement réel : 201 (bug : pas de trim())
assertEquals(res.status, 400); // ← va échouer, expose le bug
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "robustness [BUG]: POST /ues nom=espaces → devrait être 400, retourne 201",
async fn() {
await truncateAll();
const res = await uesHandler.POST!(
makeJsonRequest("/ues", "POST", { nom: " " }),
makeEmployeeContext(),
);
assertEquals(res.status, 400); // ← va échouer, expose le bug
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "robustness [BUG]: POST /users id=espaces → devrait être 400, retourne 201",
async fn() {
await truncateAll();
await assertRejects(
// sans try/catch + whitespace id → s'insère (ou throw si DB rejette)
// Dans tous les cas le handler ne valide pas correctement
async () => {
const res = await usersHandler.POST!(
makeJsonRequest("/users", "POST", { id: " ", nom: "X", prenom: "Y" }),
makeEmployeeContext(),
);
// Si pas de throw : le handler a inséré des espaces en DB
assertEquals(res.status, 400);
},
);
},
sanitizeResources: false,
sanitizeOps: false,
});
// =============================================================================
// MAUVAIS TYPES
// =============================================================================
Deno.test({
name: "robustness [BUG]: POST /notes note=string → devrait être 400, retourne 500",
async fn() {
await truncateAll();
await seedPromotions([{ id: "P1" }]);
const [s] = await seedStudents([{ nom: "Test", prenom: "User", idPromo: "P1" }]);
await seedModules([{ id: "M1", nom: "Mod" }]);
const res = await notesHandler.POST!(
makeJsonRequest("/notes", "POST", { note: "pas-un-nombre", numEtud: s.numEtud, idModule: "M1" }),
makeEmployeeContext(),
);
// "pas-un-nombre" !== undefined → passe la validation → DB rejette → 500
// Comportement attendu : 400 (validation de type)
// Comportement réel : 500
assertEquals(res.status, 400); // ← va échouer, expose le bug
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "robustness [BUG]: PUT /modules/:id nom=number → devrait être 400, throw ou insère",
async fn() {
await truncateAll();
await seedModules([{ id: "M1", nom: "Mod" }]);
await assertRejects(() =>
moduleHandler.PUT!(
makeJsonRequest("/modules/M1", "PUT", { nom: 42 }),
makeEmployeeContext({ idModule: "M1" }),
)
);
},
sanitizeResources: false,
sanitizeOps: false,
});
// =============================================================================
// VALEUR ZÉRO — falsy bug sur numEtud/idUE
// =============================================================================
Deno.test({
name: "robustness [BUG]: POST /ajustements numEtud=0 → 400 pour mauvaise raison",
async fn() {
await truncateAll();
const [ue] = await seedUes([{ nom: "UE Info" }]);
const res = await ajustementsHandler.POST!(
makeJsonRequest("/ajustements", "POST", { numEtud: 0, idUE: ue.id, valeur: 10.0 }),
makeEmployeeContext(),
);
// !0 === true → retourne 400 à cause du falsy check, pas d'une vraie validation
// Comportement attendu : 422 ou message d'erreur explicite sur numEtud invalide
// Comportement réel : 400 générique "champs requis"
assertEquals(res.status, 400); // passe, mais pour la mauvaise raison — le message est trompeur
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "robustness [BUG]: POST /ajustements idUE=0 → 400 pour mauvaise raison",
async fn() {
await truncateAll();
await seedPromotions([{ id: "P1" }]);
const [s] = await seedStudents([{ nom: "Test", prenom: "User", idPromo: "P1" }]);
const res = await ajustementsHandler.POST!(
makeJsonRequest("/ajustements", "POST", { numEtud: s.numEtud, idUE: 0, valeur: 10.0 }),
makeEmployeeContext(),
);
assertEquals(res.status, 400); // !0 → 400, message trompeur
},
sanitizeResources: false,
sanitizeOps: false,
});
// =============================================================================
// VALEUR ZÉRO CORRECTEMENT GÉRÉE — coeff=0 est valide
// =============================================================================
Deno.test({
name: "robustness: POST /ue-modules coeff=0 → 201 (zéro est une valeur valide)",
async fn() {
await truncateAll();
await seedPromotions([{ id: "P1" }]);
await seedModules([{ id: "M1", nom: "Mod" }]);
const [ue] = await seedUes([{ nom: "UE Info" }]);
const res = await ueModulesHandler.POST!(
makeJsonRequest("/ue-modules", "POST", { idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: 0 }),
makeEmployeeContext(),
);
// coeff === undefined → false pour 0 → passe ✓
assertEquals(res.status, 201);
},
sanitizeResources: false,
sanitizeOps: false,
});
// =============================================================================
// INJECTION SQL DANS LES PARAMÈTRES D'URL
// Drizzle utilise des requêtes paramétrées → les injections sont neutralisées
// =============================================================================
Deno.test({
name: "robustness: GET /modules avec SQL injection dans id → 404 (Drizzle paramètre)",
async fn() {
await truncateAll();
const injectionId = "'; DROP TABLE modules; --";
const res = await moduleHandler.GET!(
makeGetRequest(`/modules/${encodeURIComponent(injectionId)}`),
makeEmployeeContext({ idModule: injectionId }),
);
// Drizzle génère WHERE id = $1 avec $1 = "'; DROP TABLE modules; --"
// Aucune injection possible → module non trouvé → 404
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "robustness: POST /modules avec SQL injection dans id → s'insère littéralement (safe)",
async fn() {
await truncateAll();
const injectionId = "'; DROP TABLE modules; --";
const res = await modulesHandler.POST!(
makeJsonRequest("/modules", "POST", { id: injectionId, nom: "Test" }),
makeEmployeeContext(),
);
// Drizzle paramètre la valeur → s'insère comme une chaîne ordinaire → 201
assertEquals(res.status, 201);
},
sanitizeResources: false,
sanitizeOps: false,
});
// =============================================================================
// ABSENCE DE VALIDATION MÉTIER — valeurs hors limites acceptées
// =============================================================================
Deno.test({
name: "robustness [BUG]: POST /notes note > 20 → devrait être 400, retourne 201",
async fn() {
await truncateAll();
await seedPromotions([{ id: "P1" }]);
const [s] = await seedStudents([{ nom: "Test", prenom: "User", idPromo: "P1" }]);
await seedModules([{ id: "M1", nom: "Mod" }]);
const res = await notesHandler.POST!(
makeJsonRequest("/notes", "POST", { note: 999, numEtud: s.numEtud, idModule: "M1" }),
makeEmployeeContext(),
);
// Aucune validation de borne → 999 s'insère → 201
assertEquals(res.status, 400); // ← va échouer, expose le manque de validation métier
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "robustness [BUG]: POST /notes note < 0 → devrait être 400, retourne 201",
async fn() {
await truncateAll();
await seedPromotions([{ id: "P1" }]);
const [s] = await seedStudents([{ nom: "Test", prenom: "User", idPromo: "P1" }]);
await seedModules([{ id: "M1", nom: "Mod" }]);
const res = await notesHandler.POST!(
makeJsonRequest("/notes", "POST", { note: -5, numEtud: s.numEtud, idModule: "M1" }),
makeEmployeeContext(),
);
assertEquals(res.status, 400); // ← va échouer
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "robustness [BUG]: POST /ue-modules coeff négatif → devrait être 400, retourne 201",
async fn() {
await truncateAll();
await seedPromotions([{ id: "P1" }]);
await seedModules([{ id: "M1", nom: "Mod" }]);
const [ue] = await seedUes([{ nom: "UE Info" }]);
const res = await ueModulesHandler.POST!(
makeJsonRequest("/ue-modules", "POST", { idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: -3 }),
makeEmployeeContext(),
);
assertEquals(res.status, 400); // ← va échouer
},
sanitizeResources: false,
sanitizeOps: false,
});
// =============================================================================
// ISOLATION DES EFFETS DE BORD
// Vérification que truncateAll() isole correctement chaque test
// =============================================================================
Deno.test({
name: "robustness: isolation — données du test précédent non visibles",
async fn() {
// Ce test crée un module
await truncateAll();
await modulesHandler.POST!(
makeJsonRequest("/modules", "POST", { id: "ISOLATION-TEST", nom: "Test" }),
makeEmployeeContext(),
);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "robustness: isolation — truncateAll efface bien les données du test précédent",
async fn() {
await truncateAll();
// Le module créé dans le test précédent ne doit plus exister
const res = await moduleHandler.GET!(
makeGetRequest("/modules/ISOLATION-TEST"),
makeEmployeeContext({ idModule: "ISOLATION-TEST" }),
);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
// =============================================================================
// CHAMPS SUPPLÉMENTAIRES INCONNUS — doivent être ignorés silencieusement
// =============================================================================
Deno.test({
name: "robustness: POST /modules avec champs inconnus → 201 (champs ignorés)",
async fn() {
await truncateAll();
const res = await modulesHandler.POST!(
makeJsonRequest("/modules", "POST", {
id: "M-EXTRA",
nom: "Test",
champInconnu: "valeur",
_admin: true,
__proto__: { polluted: true },
}),
makeEmployeeContext(),
);
assertEquals(res.status, 201);
},
sanitizeResources: false,
sanitizeOps: false,
});
// =============================================================================
// ACCÈS NON AUTHENTIFIÉ — vérification que l'état auth est bien contrôlé
// =============================================================================
Deno.test({
name: "robustness: POST /modules sans affiliation employee → 403",
async fn() {
await truncateAll();
for (const role of ["student", "alumni", "", "EMPLOYEE", "admin"]) {
const res = await modulesHandler.POST!(
makeJsonRequest("/modules", "POST", { id: `M-${role}`, nom: "Test" }),
makeContextWithAffiliation(role),
);
assertEquals(res.status, 403, `role "${role}" devrait être 403`);
}
},
sanitizeResources: false,
sanitizeOps: false,
});