b0930b8da2
- ajustements [numEtud]/[idUE]: fix .where() missing and() — PUT/DELETE were applying only numEtud condition, modifying all rows for a student - modules/users/enseignements POST: add try/catch, return 500 on invalid JSON - modules/[idModule] PUT: add try/catch + type check on nom (string required) - modules POST: add .trim() check to reject whitespace-only id/nom - users POST: add .trim() check to reject whitespace-only id/nom/prenom - ues POST: add .trim() check to reject whitespace-only nom - notes POST: add type check (typeof number) and bounds check (0 ≤ note ≤ 20) - ue-modules POST: add coeff >= 0 validation Update robustness tests to reflect fixed behavior (remove [BUG] labels, replace assertRejects with status code assertions).
593 lines
17 KiB
TypeScript
593 lines
17 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 } 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,
|
|
});
|
|
|
|
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,
|
|
});
|