From a6042087dce3c22834f49391126026dfaeccbee7 Mon Sep 17 00:00:00 2001 From: Djalim Simaila Date: Sun, 3 May 2026 21:52:02 +0200 Subject: [PATCH] test : changed test format + added playwright support --- deno.json | 1 + logic/grades.ts | 85 ++++ logic/maquette.ts | 90 +++++ .../admin/(_islands)/ImportMaquette.tsx | 91 +---- .../(apps)/notes/(_islands)/ImportNotes.tsx | 50 +-- routes/(apps)/notes/(_islands)/NoteRecap.tsx | 43 +- routes/(apps)/notes/(_islands)/NotesView.tsx | 37 +- tests/{e2e => database}/.gitkeep | 0 tests/database/ajustements_test.ts | 160 ++++++++ tests/database/enseignements_test.ts | 148 +++++++ tests/database/modules_test.ts | 104 +++++ tests/database/notes_test.ts | 154 ++++++++ tests/database/promotions_test.ts | 112 ++++++ tests/database/roles_test.ts | 123 ++++++ tests/database/students_test.ts | 173 +++++++++ tests/database/ue_modules_test.ts | 183 +++++++++ tests/database/ues_test.ts | 90 +++++ tests/database/users_test.ts | 58 +++ tests/e2e/ajustements_test.ts | 349 ----------------- tests/e2e/basic_test.ts | 75 ++++ tests/e2e/enseignements_test.ts | 240 ------------ tests/e2e/modules_test.ts | 210 ---------- tests/e2e/notes_test.ts | 283 -------------- tests/e2e/promotions_test.ts | 212 ---------- tests/e2e/roles_test.ts | 175 --------- tests/e2e/students_test.ts | 288 -------------- tests/e2e/ue_modules_test.ts | 312 --------------- tests/e2e/ues_test.ts | 178 --------- tests/e2e/users_test.ts | 239 ------------ tests/integration/ajustements_test.ts | 353 +++++++++++++---- tests/integration/enseignements_test.ts | 254 ++++++++---- tests/integration/modules_test.ts | 234 ++++++++--- tests/integration/notes_test.ts | 331 +++++++++++----- .../{e2e => integration}/permissions_test.ts | 0 tests/integration/promotions_test.ts | 212 +++++++--- tests/{e2e => integration}/robustness_test.ts | 0 tests/integration/roles_test.ts | 190 +++++---- tests/integration/students_test.ts | 271 +++++++++---- tests/integration/ue_modules_test.ts | 367 ++++++++++++------ tests/integration/ues_test.ts | 164 ++++++-- tests/integration/users_test.ts | 235 +++++++++-- tests/unit/ajustements_test.ts | 224 ----------- tests/unit/enseignements_test.ts | 239 ------------ tests/unit/grades_test.ts | 70 ++++ tests/unit/modules_test.ts | 171 -------- tests/unit/notes_test.ts | 224 ----------- tests/unit/permissions_test.ts | 65 ---- tests/unit/promotions_test.ts | 160 -------- tests/unit/roles_test.ts | 159 -------- tests/unit/students_test.ts | 216 ----------- tests/unit/ue_modules_test.ts | 222 ----------- tests/unit/ues_test.ts | 164 -------- 52 files changed, 3576 insertions(+), 5212 deletions(-) create mode 100644 logic/grades.ts create mode 100644 logic/maquette.ts rename tests/{e2e => database}/.gitkeep (100%) create mode 100644 tests/database/ajustements_test.ts create mode 100644 tests/database/enseignements_test.ts create mode 100644 tests/database/modules_test.ts create mode 100644 tests/database/notes_test.ts create mode 100644 tests/database/promotions_test.ts create mode 100644 tests/database/roles_test.ts create mode 100644 tests/database/students_test.ts create mode 100644 tests/database/ue_modules_test.ts create mode 100644 tests/database/ues_test.ts create mode 100644 tests/database/users_test.ts delete mode 100644 tests/e2e/ajustements_test.ts create mode 100644 tests/e2e/basic_test.ts delete mode 100644 tests/e2e/enseignements_test.ts delete mode 100644 tests/e2e/modules_test.ts delete mode 100644 tests/e2e/notes_test.ts delete mode 100644 tests/e2e/promotions_test.ts delete mode 100644 tests/e2e/roles_test.ts delete mode 100644 tests/e2e/students_test.ts delete mode 100644 tests/e2e/ue_modules_test.ts delete mode 100644 tests/e2e/ues_test.ts delete mode 100644 tests/e2e/users_test.ts rename tests/{e2e => integration}/permissions_test.ts (100%) rename tests/{e2e => integration}/robustness_test.ts (100%) delete mode 100644 tests/unit/ajustements_test.ts delete mode 100644 tests/unit/enseignements_test.ts create mode 100644 tests/unit/grades_test.ts delete mode 100644 tests/unit/modules_test.ts delete mode 100644 tests/unit/notes_test.ts delete mode 100644 tests/unit/permissions_test.ts delete mode 100644 tests/unit/promotions_test.ts delete mode 100644 tests/unit/roles_test.ts delete mode 100644 tests/unit/students_test.ts delete mode 100644 tests/unit/ue_modules_test.ts delete mode 100644 tests/unit/ues_test.ts diff --git a/deno.json b/deno.json index 97ab295..40c3b4e 100644 --- a/deno.json +++ b/deno.json @@ -12,6 +12,7 @@ "update": "deno run -A -r https://fresh.deno.dev/update .", "test": "deno test -A --no-check tests/", "test:unit": "deno test -A --no-check tests/unit/", + "test:database": "deno test -A --no-check tests/database/", "test:integration": "deno test -A --no-check tests/integration/", "test:e2e": "deno test -A --no-check tests/e2e/", "test:coverage": "deno test -A --no-check --coverage=coverage tests/ && deno coverage coverage --exclude=tests/", diff --git a/logic/grades.ts b/logic/grades.ts new file mode 100644 index 0000000..a76f00c --- /dev/null +++ b/logic/grades.ts @@ -0,0 +1,85 @@ +/** + * Logique métier pour le calcul des notes et moyennes. + */ + +export interface Note { + note: number; + noteSession2: number | null; +} + +export interface UEModule { + idModule: string; + coeff: number; +} + +export interface Ajustement { + valeur: number; + malus: number; +} + +/** + * Retourne la note effective (Session 2 si présente, sinon Session 1). + */ +export function getEffectiveNote(n: Note): number { + return n.noteSession2 ?? n.note; +} + +/** + * Calcule la moyenne pondérée d'une liste de modules. + * Retourne null si aucun module n'est noté. + */ +export function calculateWeightedAverage( + ueModules: UEModule[], + notesMap: Record, +): number | null { + let weightedSum = 0; + let coveredCoeff = 0; + + for (const um of ueModules) { + const noteObj = notesMap[um.idModule]; + if (noteObj) { + const val = getEffectiveNote(noteObj); + weightedSum += val * um.coeff; + coveredCoeff += um.coeff; + } + } + + if (coveredCoeff === 0) return null; + return weightedSum / coveredCoeff; +} + +/** + * Applique l'ajustement et le malus à une moyenne. + * L'ajustement REMPLACE la moyenne calculée si présent. + */ +export function applyAjustement( + calculatedAvg: number | null, + ajustement: Ajustement | null, +): number | null { + let finalAvg = calculatedAvg; + + if (ajustement) { + // L'ajustement remplace la moyenne + finalAvg = ajustement.valeur; + if (ajustement.malus > 0) { + finalAvg = (finalAvg ?? 0) - ajustement.malus; + } + } + + return finalAvg; +} + +/** + * Arrondit une note à 2 décimales. + */ +export function roundGrade(grade: number): number { + return Math.round(grade * 100) / 100; +} + +/** + * Formate une note pour l'affichage (2 décimales). + */ +export function formatGrade(grade: number | null): string { + if (grade === null) return "—"; + return grade.toFixed(2); +} diff --git a/logic/maquette.ts b/logic/maquette.ts new file mode 100644 index 0000000..aa808f3 --- /dev/null +++ b/logic/maquette.ts @@ -0,0 +1,90 @@ +// @deno-types="https://cdn.sheetjs.com/xlsx-0.20.3/package/types/index.d.ts" +import * as XLSX from "https://cdn.sheetjs.com/xlsx-0.20.3/package/xlsx.mjs"; + +export type ParsedUE = { + code: string | null; + name: string; + ects: number | null; + modules: ParsedModule[]; +}; + +export type ParsedModule = { + code: string; + name: string; + coeff: number; +}; + +export type ParsedYear = { + label: string; + ues: ParsedUE[]; +}; + +/** + * Analyse un classeur Excel pour en extraire la maquette pédagogique. + */ +export function parseMaquette(workbook: XLSX.WorkBook): ParsedYear[] { + const sheet = workbook.Sheets[workbook.SheetNames[0]]; + const rows = XLSX.utils.sheet_to_json<(string | number | null)[]>(sheet, { + header: 1, + }); + + const years: ParsedYear[] = []; + let currentYear: ParsedYear | null = null; + let currentUE: ParsedUE | null = null; + let moduleIndex = 0; + + for (const row of rows) { + if (!row || row.length === 0) continue; + + const col0 = row[0] != null ? String(row[0]).trim() : ""; + + // Detect year row: INFO3A, INFO4A, INFO5A, INFOAPP3A, INFOAPP4A, etc. + if (/^(INFO|INFOAPP)\s*\d+A$/i.test(col0)) { + currentYear = { label: col0, ues: [] }; + years.push(currentYear); + currentUE = null; + continue; + } + + // Detect UE row: col[0] === "UE" or starts with "UE" (e.g., "UE51") + if (col0 === "UE" || (col0.startsWith("UE") && /^UE\d/.test(col0))) { + const ueCode = row[1] != null ? String(row[1]).trim() : null; + const ueName = row[2] != null ? String(row[2]).trim() : "UE sans nom"; + const ects = typeof row[4] === "number" ? row[4] : null; + + currentUE = { code: ueCode, name: ueName, ects, modules: [] }; + if (currentYear) { + currentYear.ues.push(currentUE); + } else { + // No year detected yet — create a default one + currentYear = { label: "Maquette", ues: [currentUE] }; + years.push(currentYear); + } + moduleIndex = 0; + continue; + } + + // Detect semester header rows — just skip, don't reset UE + if (/^SEM\s*\d/i.test(col0)) { + currentUE = null; + continue; + } + + // Detect module row: inside a UE, col[3] has a name, col[5] is a number (coeff) + if (currentUE && row[3] != null && typeof row[5] === "number") { + const modName = String(row[3]).trim(); + if (!modName) continue; + + let modCode = row[1] != null ? String(row[1]).trim() : ""; + if (!modCode) { + const uePrefix = (currentUE.code || "MOD").replace(/[^A-Z0-9]/gi, ""); + modCode = `${uePrefix}_${String(moduleIndex).padStart(2, "0")}`; + } + + currentUE.modules.push({ code: modCode, name: modName, coeff: row[5] }); + moduleIndex++; + } + } + + return years; +} diff --git a/routes/(apps)/admin/(_islands)/ImportMaquette.tsx b/routes/(apps)/admin/(_islands)/ImportMaquette.tsx index 5a03b6e..b4c6527 100644 --- a/routes/(apps)/admin/(_islands)/ImportMaquette.tsx +++ b/routes/(apps)/admin/(_islands)/ImportMaquette.tsx @@ -2,98 +2,19 @@ import * as XLSX from "https://cdn.sheetjs.com/xlsx-0.20.3/package/xlsx.mjs"; import { useEffect, useRef } from "preact/hooks"; import { useSignal } from "@preact/signals"; +import { + parseMaquette, + type ParsedModule, + type ParsedUE, + type ParsedYear, +} from "$root/logic/maquette.ts"; import ImportResultPopup, { type ImportDetail, type ImportResult, } from "$root/defaults/ImportResultPopup.tsx"; -type ParsedUE = { - code: string | null; - name: string; - ects: number | null; - modules: ParsedModule[]; -}; - -type ParsedModule = { - code: string; - name: string; - coeff: number; -}; - -type ParsedYear = { - label: string; - ues: ParsedUE[]; -}; - type Promo = { id: string; annee: string | null }; -function parseMaquette(workbook: XLSX.WorkBook): ParsedYear[] { - const sheet = workbook.Sheets[workbook.SheetNames[0]]; - const rows = XLSX.utils.sheet_to_json<(string | number | null)[]>(sheet, { - header: 1, - }); - - const years: ParsedYear[] = []; - let currentYear: ParsedYear | null = null; - let currentUE: ParsedUE | null = null; - let moduleIndex = 0; - - for (const row of rows) { - if (!row || row.length === 0) continue; - - const col0 = row[0] != null ? String(row[0]).trim() : ""; - - // Detect year row: INFO3A, INFO4A, INFO5A, INFOAPP3A, INFOAPP4A, etc. - if (/^(INFO|INFOAPP)\s*\d+A$/i.test(col0)) { - currentYear = { label: col0, ues: [] }; - years.push(currentYear); - currentUE = null; - continue; - } - - // Detect UE row: col[0] === "UE" or starts with "UE" (e.g., "UE51") - if (col0 === "UE" || (col0.startsWith("UE") && /^UE\d/.test(col0))) { - const ueCode = row[1] != null ? String(row[1]).trim() : null; - const ueName = row[2] != null ? String(row[2]).trim() : "UE sans nom"; - const ects = typeof row[4] === "number" ? row[4] : null; - - currentUE = { code: ueCode, name: ueName, ects, modules: [] }; - if (currentYear) { - currentYear.ues.push(currentUE); - } else { - // No year detected yet — create a default one - currentYear = { label: "Maquette", ues: [currentUE] }; - years.push(currentYear); - } - moduleIndex = 0; - continue; - } - - // Detect semester header rows — just skip, don't reset UE - if (/^SEM\s*\d/i.test(col0)) { - currentUE = null; - continue; - } - - // Detect module row: inside a UE, col[3] has a name, col[5] is a number (coeff) - if (currentUE && row[3] != null && typeof row[5] === "number") { - const modName = String(row[3]).trim(); - if (!modName) continue; - - let modCode = row[1] != null ? String(row[1]).trim() : ""; - if (!modCode) { - const uePrefix = (currentUE.code || "MOD").replace(/[^A-Z0-9]/gi, ""); - modCode = `${uePrefix}_${String(moduleIndex).padStart(2, "0")}`; - } - - currentUE.modules.push({ code: modCode, name: modName, coeff: row[5] }); - moduleIndex++; - } - } - - return years; -} - export default function ImportMaquette() { const file = useSignal(null); const dragging = useSignal(false); diff --git a/routes/(apps)/notes/(_islands)/ImportNotes.tsx b/routes/(apps)/notes/(_islands)/ImportNotes.tsx index 1855520..55f6eee 100644 --- a/routes/(apps)/notes/(_islands)/ImportNotes.tsx +++ b/routes/(apps)/notes/(_islands)/ImportNotes.tsx @@ -2,6 +2,11 @@ import * as XLSX from "https://cdn.sheetjs.com/xlsx-0.20.3/package/xlsx.mjs"; import { useEffect, useRef } from "preact/hooks"; import { useSignal } from "@preact/signals"; +import { + calculateWeightedAverage, + getEffectiveNote, + roundGrade, +} from "$root/logic/grades.ts"; import ImportResultPopup, { type ImportDetail, type ImportResult, @@ -393,19 +398,19 @@ export default function ImportNotes() { const ueMods = orderedCols.filter( (c) => c.type === "module" && c.ueId === col.ueId, ); - let total = 0, coeffSum = 0; - for (const um of ueMods) { + + const notesRecord: Record = {}; + ueMods.forEach(um => { const n = sNotes.get(um.id); - if (n && um.coeff) { - total += n.note * um.coeff; - coeffSum += um.coeff; - } - } - row.push( - coeffSum > 0 - ? Math.round((total / coeffSum) * 100) / 100 - : null, + if (n) notesRecord[um.id] = n; + }); + + const avg = calculateWeightedAverage( + ueMods.map(m => ({ idModule: m.id, coeff: m.coeff ?? 0 })), + notesRecord ); + + row.push(avg !== null ? roundGrade(avg) : null); } } s1Rows.push(row); @@ -425,20 +430,19 @@ export default function ImportNotes() { const ueMods = orderedCols.filter( (c) => c.type === "module" && c.ueId === col.ueId, ); - let total = 0, coeffSum = 0; - for (const um of ueMods) { + + const notesRecord: Record = {}; + ueMods.forEach(um => { const n = sNotes.get(um.id); - if (n && um.coeff) { - const noteVal = n.noteSession2 ?? n.note; - total += noteVal * um.coeff; - coeffSum += um.coeff; - } - } - row.push( - coeffSum > 0 - ? Math.round((total / coeffSum) * 100) / 100 - : null, + if (n) notesRecord[um.id] = n; + }); + + const avg = calculateWeightedAverage( + ueMods.map(m => ({ idModule: m.id, coeff: m.coeff ?? 0 })), + notesRecord ); + + row.push(avg !== null ? roundGrade(avg) : null); } } s2Rows.push(row); diff --git a/routes/(apps)/notes/(_islands)/NoteRecap.tsx b/routes/(apps)/notes/(_islands)/NoteRecap.tsx index 5a516f0..579456c 100644 --- a/routes/(apps)/notes/(_islands)/NoteRecap.tsx +++ b/routes/(apps)/notes/(_islands)/NoteRecap.tsx @@ -1,6 +1,12 @@ import { useEffect, useState } from "preact/hooks"; +import { + applyAjustement, + calculateWeightedAverage, + getEffectiveNote, +} from "$root/logic/grades.ts"; type Student = { +// ... numEtud: number; nom: string; prenom: string; @@ -37,11 +43,6 @@ function noteClass(n: number): string { return n >= 10 ? "note-chip note-chip--ok" : "note-chip note-chip--fail"; } -/** Returns the effective note (session 2 if exists, otherwise session 1). */ -function effectiveNote(n: Note): number { - return n.noteSession2 ?? n.note; -} - export default function NoteRecap({ numEtud }: Props) { const [student, setStudent] = useState(null); const [ueList, setUeList] = useState([]); @@ -108,19 +109,6 @@ export default function NoteRecap({ numEtud }: Props) { load(); }, [numEtud]); - function calcAvg(ueMods: UEModule[]): number | null { - let total = 0, - coeff = 0; - for (const um of ueMods) { - const n = noteMap.get(um.idModule); - if (n === undefined) return null; - const val = effectiveNote(n); - total += val * um.coeff; - coeff += um.coeff; - } - return coeff > 0 ? total / coeff : null; - } - async function saveNote( idModule: string, field: "note" | "noteSession2", @@ -280,18 +268,11 @@ export default function NoteRecap({ numEtud }: Props) {

) : ueList.map((ue) => { - const ueMods = ueModules.filter((um) => um.idUE === ue.id); - const avg = calcAvg(ueMods); - const ajust = ajustements.find((a) => a.idUE === ue.id); - - // Final displayed average: if ajust.valeur exists it replaces avg, then subtract malus - let finalAvg = avg; - if (ajust) { - finalAvg = ajust.valeur; - if (ajust.malus > 0) { - finalAvg = (finalAvg ?? 0) - ajust.malus; - } - } + const ueMods = ueList.length > 0 ? ueModules.filter((um) => um.idUE === ue.id) : []; + const notesRecord = Object.fromEntries(noteMap); + const avg = calculateWeightedAverage(ueMods, notesRecord); + const ajust = ajustements.find((a) => a.idUE === ue.id) ?? null; + const finalAvg = applyAjustement(avg, ajust); return (
@@ -341,7 +322,7 @@ export default function NoteRecap({ numEtud }: Props) { const noteVal = noteObj?.note; const noteS2 = noteObj?.noteSession2; const effective = noteObj - ? effectiveNote(noteObj) + ? getEffectiveNote(noteObj) : undefined; const nomMod = moduleMap.get(um.idModule) ?? um.idModule; diff --git a/routes/(apps)/notes/(_islands)/NotesView.tsx b/routes/(apps)/notes/(_islands)/NotesView.tsx index 35cc897..608c140 100644 --- a/routes/(apps)/notes/(_islands)/NotesView.tsx +++ b/routes/(apps)/notes/(_islands)/NotesView.tsx @@ -1,4 +1,9 @@ import { useEffect, useState } from "preact/hooks"; +import { + applyAjustement, + calculateWeightedAverage, + getEffectiveNote, +} from "$root/logic/grades.ts"; type Note = { numEtud: number; @@ -6,6 +11,7 @@ type Note = { note: number; noteSession2: number | null; }; +// ... rest of types unchanged ... type UE = { id: number; nom: string }; type UEModule = { idModule: string; @@ -36,11 +42,6 @@ function avgClass(avg: number | null): string { return avg >= 10 ? "avg-good" : "avg-warn"; } -/** Returns the effective note (session 2 if exists, otherwise session 1). */ -function effectiveNote(n: Note): number { - return n.noteSession2 ?? n.note; -} - export default function NotesView({ numEtud, prenom }: Props) { const [notes, setNotes] = useState([]); const [ues, setUes] = useState([]); @@ -175,29 +176,9 @@ export default function NotesView({ numEtud, prenom }: Props) { if (!ue) return null; const ueModsForUE = filteredUeModules.filter((um) => um.idUE === ueId); - let weightedSum = 0; - let coveredCoeff = 0; - ueModsForUE.forEach((um) => { - const noteObj = noteMap[um.idModule]; - if (noteObj) { - const val = effectiveNote(noteObj); - weightedSum += val * um.coeff; - coveredCoeff += um.coeff; - } - }); - - const avg = coveredCoeff > 0 ? weightedSum / coveredCoeff : null; + const avg = calculateWeightedAverage(ueModsForUE, noteMap); const ajust = ajMap[ueId] ?? null; - - // If ajust.valeur exists, it replaces the calculated average - // Then malus is subtracted - let finalAvg: number | null = avg; - if (ajust) { - finalAvg = ajust.valeur; - if (ajust.malus > 0) { - finalAvg = (finalAvg ?? 0) - ajust.malus; - } - } + const finalAvg = applyAjustement(avg, ajust); return (
@@ -218,7 +199,7 @@ export default function NotesView({ numEtud, prenom }: Props) { {ueModsForUE.map((um) => { const mod = moduleMap[um.idModule]; const noteObj = noteMap[um.idModule] ?? null; - const effective = noteObj ? effectiveNote(noteObj) : null; + const effective = noteObj ? getEffectiveNote(noteObj) : null; const hasS2 = noteObj?.noteSession2 != null; return ( diff --git a/tests/e2e/.gitkeep b/tests/database/.gitkeep similarity index 100% rename from tests/e2e/.gitkeep rename to tests/database/.gitkeep diff --git a/tests/database/ajustements_test.ts b/tests/database/ajustements_test.ts new file mode 100644 index 0000000..49e6fcd --- /dev/null +++ b/tests/database/ajustements_test.ts @@ -0,0 +1,160 @@ +// Integration tests for /ajustements — Drizzle ORM direct on real DB + +import { assertEquals, assertExists, assertRejects } from "@std/assert"; +import { + seedAjustements, + seedPromotions, + seedStudents, + seedUes, + testDb, + truncateAll, +} from "../helpers/db_integration.ts"; +import { ajustements } from "$root/databases/schema.ts"; +import { and, eq } from "npm:drizzle-orm@0.45.2"; + +Deno.test({ + name: "integration ajustements: list all ajustements", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ + nom: "Dupont", + prenom: "Jean", + idPromo: "P1", + }]); + const [ue] = await seedUes([{ nom: "UE Info" }]); + await seedAjustements([{ numEtud: s.numEtud, idUE: ue.id, valeur: 13.0 }]); + const rows = await testDb.select().from(ajustements); + assertEquals(rows.length, 1); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ajustements: create and retrieve by composite key", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ + nom: "Martin", + prenom: "Alice", + idPromo: "P1", + }]); + const [ue] = await seedUes([{ nom: "UE Maths" }]); + + const [created] = await testDb + .insert(ajustements) + .values({ numEtud: s.numEtud, idUE: ue.id, valeur: 15.5 }) + .returning(); + assertExists(created); + assertEquals(created.valeur, 15.5); + + const row = await testDb + .select() + .from(ajustements) + .where( + and(eq(ajustements.numEtud, s.numEtud), eq(ajustements.idUE, ue.id)), + ) + .then((r) => r[0] ?? null); + assertExists(row); + assertEquals(row.valeur, 15.5); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: + "integration ajustements: get by composite key returns null when not found", + async fn() { + await truncateAll(); + const row = await testDb + .select() + .from(ajustements) + .where(and(eq(ajustements.numEtud, 99999), eq(ajustements.idUE, 99))) + .then((r) => r[0] ?? null); + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ajustements: duplicate composite key insert fails", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ + nom: "Durand", + prenom: "Claire", + idPromo: "P1", + }]); + const [ue] = await seedUes([{ nom: "UE Info" }]); + await seedAjustements([{ numEtud: s.numEtud, idUE: ue.id, valeur: 12.0 }]); + await assertRejects(() => + testDb.insert(ajustements).values({ + numEtud: s.numEtud, + idUE: ue.id, + valeur: 13.0, + }) + ); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ajustements: update valeur", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ + nom: "Bernard", + prenom: "Lucie", + idPromo: "P1", + }]); + const [ue] = await seedUes([{ nom: "UE Physique" }]); + await seedAjustements([{ numEtud: s.numEtud, idUE: ue.id, valeur: 10.0 }]); + + const [updated] = await testDb + .update(ajustements) + .set({ valeur: 18.0 }) + .where( + and(eq(ajustements.numEtud, s.numEtud), eq(ajustements.idUE, ue.id)), + ) + .returning(); + assertEquals(updated.valeur, 18.0); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ajustements: delete removes the ajustement", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + const [s] = await seedStudents([{ + nom: "Thomas", + prenom: "Eva", + idPromo: "P1", + }]); + const [ue] = await seedUes([{ nom: "UE Chimie" }]); + await seedAjustements([{ numEtud: s.numEtud, idUE: ue.id, valeur: 11.0 }]); + + await testDb.delete(ajustements).where( + and(eq(ajustements.numEtud, s.numEtud), eq(ajustements.idUE, ue.id)), + ); + const row = await testDb + .select() + .from(ajustements) + .where( + and(eq(ajustements.numEtud, s.numEtud), eq(ajustements.idUE, ue.id)), + ) + .then((r) => r[0] ?? null); + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/database/enseignements_test.ts b/tests/database/enseignements_test.ts new file mode 100644 index 0000000..40086a9 --- /dev/null +++ b/tests/database/enseignements_test.ts @@ -0,0 +1,148 @@ +// Integration tests for /enseignements — Drizzle ORM direct on real DB + +import { assertEquals, assertExists, assertRejects } from "@std/assert"; +import { + seedEnseignements, + seedModules, + seedPromotions, + seedUsers, + testDb, + truncateAll, +} from "../helpers/db_integration.ts"; +import { enseignements } from "$root/databases/schema.ts"; +import { and, eq } from "npm:drizzle-orm@0.45.2"; + +Deno.test({ + name: "integration enseignements: list all enseignements", + async fn() { + await truncateAll(); + await seedUsers([{ id: "prof.dupont", nom: "Dupont", prenom: "Jean" }]); + await seedModules([{ id: "M1", nom: "Mod A" }, { id: "M2", nom: "Mod B" }]); + await seedPromotions([{ id: "P1" }]); + await seedEnseignements([ + { idProf: "prof.dupont", idModule: "M1", idPromo: "P1" }, + { idProf: "prof.dupont", idModule: "M2", idPromo: "P1" }, + ]); + const rows = await testDb.select().from(enseignements); + assertEquals(rows.length, 2); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration enseignements: create and retrieve by composite key", + async fn() { + await truncateAll(); + await seedUsers([{ id: "prof.moreau", nom: "Moreau", prenom: "Sophie" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + await seedPromotions([{ id: "P1" }]); + + const [created] = await testDb + .insert(enseignements) + .values({ idProf: "prof.moreau", idModule: "M1", idPromo: "P1" }) + .returning(); + assertExists(created); + assertEquals(created.idProf, "prof.moreau"); + + const row = await testDb + .select() + .from(enseignements) + .where( + and( + eq(enseignements.idProf, "prof.moreau"), + eq(enseignements.idModule, "M1"), + eq(enseignements.idPromo, "P1"), + ), + ) + .then((r) => r[0] ?? null); + assertExists(row); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: + "integration enseignements: get by composite key returns null when not found", + async fn() { + await truncateAll(); + const row = await testDb + .select() + .from(enseignements) + .where( + and( + eq(enseignements.idProf, "ghost"), + eq(enseignements.idModule, "GHOST"), + eq(enseignements.idPromo, "GHOST"), + ), + ) + .then((r) => r[0] ?? null); + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration enseignements: duplicate composite key insert fails", + async fn() { + await truncateAll(); + await seedUsers([{ id: "prof.dupont", nom: "Dupont", prenom: "Jean" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + await seedPromotions([{ id: "P1" }]); + await seedEnseignements([{ + idProf: "prof.dupont", + idModule: "M1", + idPromo: "P1", + }]); + await assertRejects(() => + testDb.insert(enseignements).values({ + idProf: "prof.dupont", + idModule: "M1", + idPromo: "P1", + }) + ); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration enseignements: delete removes the enseignement", + async fn() { + await truncateAll(); + await seedUsers([{ id: "prof.dupont", nom: "Dupont", prenom: "Jean" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + await seedPromotions([{ id: "P1" }]); + await seedEnseignements([{ + idProf: "prof.dupont", + idModule: "M1", + idPromo: "P1", + }]); + + await testDb + .delete(enseignements) + .where( + and( + eq(enseignements.idProf, "prof.dupont"), + eq(enseignements.idModule, "M1"), + eq(enseignements.idPromo, "P1"), + ), + ); + const row = await testDb + .select() + .from(enseignements) + .where( + and( + eq(enseignements.idProf, "prof.dupont"), + eq(enseignements.idModule, "M1"), + eq(enseignements.idPromo, "P1"), + ), + ) + .then((r) => r[0] ?? null); + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/database/modules_test.ts b/tests/database/modules_test.ts new file mode 100644 index 0000000..df32fba --- /dev/null +++ b/tests/database/modules_test.ts @@ -0,0 +1,104 @@ +// #113 - Integration 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"; + +Deno.test({ + name: "integration modules: list all modules", + 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); + }, + 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", + async fn() { + await truncateAll(); + await seedModules([{ id: "MATH101", nom: "Mathématiques" }]); + await assertRejects(() => + testDb.insert(modules).values({ id: "MATH101", nom: "Doublon" }) + ); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration modules: update nom", + 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"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration modules: delete removes the module", + 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); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/database/notes_test.ts b/tests/database/notes_test.ts new file mode 100644 index 0000000..b9018b9 --- /dev/null +++ b/tests/database/notes_test.ts @@ -0,0 +1,154 @@ +// Integration tests for /notes — Drizzle ORM direct on real DB + +import { assertEquals, assertExists, assertRejects } from "@std/assert"; +import { + seedModules, + seedNotes, + seedPromotions, + seedStudents, + testDb, + truncateAll, +} from "../helpers/db_integration.ts"; +import { notes } from "$root/databases/schema.ts"; +import { and, eq } from "npm:drizzle-orm@0.45.2"; + +Deno.test({ + name: "integration notes: list all notes", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "PROMO-2024" }]); + const [s] = await seedStudents([{ + nom: "Dupont", + prenom: "Jean", + idPromo: "PROMO-2024", + }]); + await seedModules([{ id: "MOD101", nom: "Module A" }]); + await seedNotes([{ numEtud: s.numEtud, idModule: "MOD101", note: 15.5 }]); + const rows = await testDb.select().from(notes); + assertEquals(rows.length, 1); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration notes: create and retrieve by composite key", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "PROMO-2024" }]); + const [s] = await seedStudents([{ + nom: "Martin", + prenom: "Alice", + idPromo: "PROMO-2024", + }]); + await seedModules([{ id: "MOD102", nom: "Module B" }]); + + const [created] = await testDb.insert(notes).values({ + numEtud: s.numEtud, + idModule: "MOD102", + note: 12.0, + }).returning(); + assertExists(created); + assertEquals(created.note, 12.0); + + const row = await testDb + .select() + .from(notes) + .where(and(eq(notes.numEtud, s.numEtud), eq(notes.idModule, "MOD102"))) + .then((r) => r[0] ?? null); + assertExists(row); + assertEquals(row.note, 12.0); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration notes: get by composite key returns null when not found", + async fn() { + await truncateAll(); + const row = await testDb + .select() + .from(notes) + .where(and(eq(notes.numEtud, 99999), eq(notes.idModule, "GHOST"))) + .then((r) => r[0] ?? null); + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration notes: duplicate composite key insert fails", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "PROMO-2024" }]); + const [s] = await seedStudents([{ + nom: "Durand", + prenom: "Claire", + idPromo: "PROMO-2024", + }]); + await seedModules([{ id: "MOD103", nom: "Module C" }]); + await seedNotes([{ numEtud: s.numEtud, idModule: "MOD103", note: 10.0 }]); + await assertRejects(() => + testDb.insert(notes).values({ + numEtud: s.numEtud, + idModule: "MOD103", + note: 11.0, + }) + ); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration notes: update note value", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "PROMO-2024" }]); + const [s] = await seedStudents([{ + nom: "Bernard", + prenom: "Lucie", + idPromo: "PROMO-2024", + }]); + await seedModules([{ id: "MOD104", nom: "Module D" }]); + await seedNotes([{ numEtud: s.numEtud, idModule: "MOD104", note: 8.0 }]); + + const [updated] = await testDb + .update(notes) + .set({ note: 16.0 }) + .where(and(eq(notes.numEtud, s.numEtud), eq(notes.idModule, "MOD104"))) + .returning(); + assertEquals(updated.note, 16.0); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration notes: delete removes the note", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "PROMO-2024" }]); + const [s] = await seedStudents([{ + nom: "Thomas", + prenom: "Eva", + idPromo: "PROMO-2024", + }]); + await seedModules([{ id: "MOD105", nom: "Module E" }]); + await seedNotes([{ numEtud: s.numEtud, idModule: "MOD105", note: 14.0 }]); + + await testDb.delete(notes).where( + and(eq(notes.numEtud, s.numEtud), eq(notes.idModule, "MOD105")), + ); + const row = await testDb + .select() + .from(notes) + .where(and(eq(notes.numEtud, s.numEtud), eq(notes.idModule, "MOD105"))) + .then((r) => r[0] ?? null); + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/database/promotions_test.ts b/tests/database/promotions_test.ts new file mode 100644 index 0000000..07b24fd --- /dev/null +++ b/tests/database/promotions_test.ts @@ -0,0 +1,112 @@ +// #110 - Integration tests for /promotions endpoints + +import { assertEquals, assertExists } 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"; + +Deno.test({ + name: "integration promotions: list all", + 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); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration promotions: create and retrieve by id", + 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); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration promotions: get by id returns null when not found", + async fn() { + await truncateAll(); + const row = await testDb + .select() + .from(promotions) + .where(eq(promotions.id, "NONEXISTENT")) + .then((r) => r[0] ?? null); + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration promotions: update 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"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration promotions: delete removes the row", + 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); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration promotions: update non-existent returns empty", + async fn() { + await truncateAll(); + const result = await testDb + .update(promotions) + .set({ annee: "2099" }) + .where(eq(promotions.id, "GHOST")) + .returning(); + assertEquals(result.length, 0); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/database/roles_test.ts b/tests/database/roles_test.ts new file mode 100644 index 0000000..9fb7a6c --- /dev/null +++ b/tests/database/roles_test.ts @@ -0,0 +1,123 @@ +// #112 - Integration tests for /roles endpoints + +import { assertEquals, assertExists } from "@std/assert"; +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"; + +Deno.test({ + name: "integration roles: list all roles", + async fn() { + await truncateAll(); + await seedRoles([{ nom: "admin" }, { nom: "employee" }]); + const rows = await testDb.select().from(roles); + assertEquals(rows.length, 2); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration roles: create and retrieve by id", + 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); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration roles: assign and retrieve 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); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration roles: update role nom", + 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), + ); + 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"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration roles: delete role removes it", + 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); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/database/students_test.ts b/tests/database/students_test.ts new file mode 100644 index 0000000..bb53d6f --- /dev/null +++ b/tests/database/students_test.ts @@ -0,0 +1,173 @@ +// #109 - Integration tests for /students endpoints +// Teste les opérations DB directement avec une vraie base de données + +import { assertEquals, assertExists } from "@std/assert"; +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"; + +Deno.test({ + name: "integration students: list all students", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "PEIP1-2024" }]); + await seedStudents([ + { nom: "Dupont", prenom: "Jean", idPromo: "PEIP1-2024" }, + { nom: "Martin", prenom: "Alice", idPromo: "PEIP1-2024" }, + ]); + + const rows = await testDb.select().from(students); + assertEquals(rows.length, 2); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration students: filter by idPromo", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "PEIP1-2024" }, { id: "PEIP2-2024" }]); + await seedStudents([ + { nom: "Dupont", prenom: "Jean", idPromo: "PEIP1-2024" }, + { nom: "Martin", prenom: "Alice", idPromo: "PEIP1-2024" }, + { 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); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration students: create and retrieve by numEtud", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "INFO3-2024" }]); + + const [created] = await testDb + .insert(students) + .values({ nom: "Leroy", prenom: "Paul", idPromo: "INFO3-2024" }) + .returning(); + + 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"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration students: get by numEtud returns null when not found", + async fn() { + await truncateAll(); + + const row = await testDb + .select() + .from(students) + .where(eq(students.numEtud, 999999)) + .then((r) => r[0] ?? null); + + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration students: update student fields", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "INFO3-2024" }, { id: "INFO4-2024" }]); + const [s] = await seedStudents([ + { 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(); + + assertEquals(updated.nom, "Grand"); + assertEquals(updated.idPromo, "INFO4-2024"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration students: delete student", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "INFO3-2024" }]); + const [s] = await seedStudents([ + { nom: "Thomas", prenom: "Eva", idPromo: "INFO3-2024" }, + ]); + + await testDb.delete(students).where(eq(students.numEtud, s.numEtud)); + + const row = await testDb + .select() + .from(students) + .where(eq(students.numEtud, s.numEtud)) + .then((r) => r[0] ?? null); + + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration students: update non-existent student returns empty", + async fn() { + await truncateAll(); + + const result = await testDb + .update(students) + .set({ nom: "Ghost" }) + .where(eq(students.numEtud, 999999)) + .returning(); + + 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); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/database/ue_modules_test.ts b/tests/database/ue_modules_test.ts new file mode 100644 index 0000000..9aaab2a --- /dev/null +++ b/tests/database/ue_modules_test.ts @@ -0,0 +1,183 @@ +// Integration tests for /ue-modules — Drizzle ORM direct on real DB + +import { assertEquals, assertExists, assertRejects } from "@std/assert"; +import { + seedModules, + seedPromotions, + seedUeModules, + seedUes, + testDb, + truncateAll, +} from "../helpers/db_integration.ts"; +import { ueModules } from "$root/databases/schema.ts"; +import { and, eq } from "npm:drizzle-orm@0.45.2"; + +Deno.test({ + name: "integration ue_modules: list all associations", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod A" }, { id: "M2", nom: "Mod B" }]); + const [ue] = await seedUes([{ nom: "UE Info" }]); + await seedUeModules([ + { idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: 2.0 }, + { idModule: "M2", idUE: ue.id, idPromo: "P1", coeff: 3.0 }, + ]); + const rows = await testDb.select().from(ueModules); + assertEquals(rows.length, 2); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ue_modules: create and retrieve by composite key", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + const [ue] = await seedUes([{ nom: "UE Maths" }]); + + const [created] = await testDb + .insert(ueModules) + .values({ idModule: "M1", idUE: ue.id, idPromo: "P1", coeff: 4.0 }) + .returning(); + assertExists(created); + assertEquals(created.coeff, 4.0); + + const row = await testDb + .select() + .from(ueModules) + .where( + and( + eq(ueModules.idModule, "M1"), + eq(ueModules.idUE, ue.id), + eq(ueModules.idPromo, "P1"), + ), + ) + .then((r) => r[0] ?? null); + assertExists(row); + assertEquals(row.coeff, 4.0); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: + "integration ue_modules: get by composite key returns null when not found", + async fn() { + await truncateAll(); + const row = await testDb + .select() + .from(ueModules) + .where( + and( + eq(ueModules.idModule, "GHOST"), + eq(ueModules.idUE, 99), + eq(ueModules.idPromo, "GHOST"), + ), + ) + .then((r) => r[0] ?? null); + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ue_modules: duplicate composite key insert fails", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + const [ue] = await seedUes([{ nom: "UE Info" }]); + await seedUeModules([{ + idModule: "M1", + idUE: ue.id, + idPromo: "P1", + coeff: 2.0, + }]); + await assertRejects(() => + testDb.insert(ueModules).values({ + idModule: "M1", + idUE: ue.id, + idPromo: "P1", + coeff: 5.0, + }) + ); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ue_modules: update coeff", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + const [ue] = await seedUes([{ nom: "UE Info" }]); + await seedUeModules([{ + idModule: "M1", + idUE: ue.id, + idPromo: "P1", + coeff: 2.0, + }]); + + const [updated] = await testDb + .update(ueModules) + .set({ coeff: 6.0 }) + .where( + and( + eq(ueModules.idModule, "M1"), + eq(ueModules.idUE, ue.id), + eq(ueModules.idPromo, "P1"), + ), + ) + .returning(); + assertEquals(updated.coeff, 6.0); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ue_modules: delete removes the association", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "P1" }]); + await seedModules([{ id: "M1", nom: "Mod A" }]); + const [ue] = await seedUes([{ nom: "UE Info" }]); + await seedUeModules([{ + idModule: "M1", + idUE: ue.id, + idPromo: "P1", + coeff: 2.0, + }]); + + await testDb + .delete(ueModules) + .where( + and( + eq(ueModules.idModule, "M1"), + eq(ueModules.idUE, ue.id), + eq(ueModules.idPromo, "P1"), + ), + ); + const row = await testDb + .select() + .from(ueModules) + .where( + and( + eq(ueModules.idModule, "M1"), + eq(ueModules.idUE, ue.id), + eq(ueModules.idPromo, "P1"), + ), + ) + .then((r) => r[0] ?? null); + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/database/ues_test.ts b/tests/database/ues_test.ts new file mode 100644 index 0000000..790330a --- /dev/null +++ b/tests/database/ues_test.ts @@ -0,0 +1,90 @@ +// Integration tests for /ues — Drizzle ORM direct on real DB + +import { assertEquals, assertExists, assertRejects } from "@std/assert"; +import { seedUes, testDb, truncateAll } from "../helpers/db_integration.ts"; +import { ues } from "$root/databases/schema.ts"; +import { eq } from "npm:drizzle-orm@0.45.2"; + +Deno.test({ + name: "integration ues: list all UEs", + async fn() { + await truncateAll(); + await seedUes([{ nom: "UE Informatique" }, { nom: "UE Mathématiques" }]); + const rows = await testDb.select().from(ues); + assertEquals(rows.length, 2); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ues: create and retrieve by id", + async fn() { + await truncateAll(); + const [created] = await testDb.insert(ues).values({ nom: "UE Physique" }) + .returning(); + assertExists(created); + assertExists(created.id); + assertEquals(created.nom, "UE Physique"); + + const row = await testDb.select().from(ues).where(eq(ues.id, created.id)) + .then((r) => r[0] ?? null); + assertExists(row); + assertEquals(row.nom, "UE Physique"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ues: get by id returns null when not found", + async fn() { + await truncateAll(); + const row = await testDb.select().from(ues).where(eq(ues.id, 99999)).then(( + r, + ) => r[0] ?? null); + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ues: update nom", + async fn() { + await truncateAll(); + const [ue] = await seedUes([{ nom: "UE Chimie" }]); + const [updated] = await testDb.update(ues).set({ + nom: "UE Chimie organique", + }).where(eq(ues.id, ue.id)).returning(); + assertEquals(updated.nom, "UE Chimie organique"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ues: delete removes the UE", + async fn() { + await truncateAll(); + const [ue] = await seedUes([{ nom: "UE à supprimer" }]); + await testDb.delete(ues).where(eq(ues.id, ue.id)); + const row = await testDb.select().from(ues).where(eq(ues.id, ue.id)).then(( + r, + ) => r[0] ?? null); + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration ues: nom is required (not null)", + async fn() { + await truncateAll(); + // deno-lint-ignore no-explicit-any + await assertRejects(() => testDb.insert(ues).values({ nom: null as any })); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/database/users_test.ts b/tests/database/users_test.ts new file mode 100644 index 0000000..e0d5ae9 --- /dev/null +++ b/tests/database/users_test.ts @@ -0,0 +1,58 @@ +import { assertEquals, assertExists } from "@std/assert"; +import { + closeTestPool, + seedRoles, + seedUsers, + testDb, + truncateAll, +} from "../helpers/db_integration.ts"; +import { users } from "$root/databases/schema.ts"; + +Deno.test({ + name: "integration: GET /users - DB round trip", + 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 }, + ]); + + const rows = await testDb.select().from(users); + assertEquals(rows.length, 2); + assertExists(rows.find((u) => u.id === "dupont.jean")); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration: INSERT user and retrieve by id", + 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"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration: cleanup - close pool", + async fn() { + await closeTestPool(); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/e2e/ajustements_test.ts b/tests/e2e/ajustements_test.ts deleted file mode 100644 index 2ca2ef7..0000000 --- a/tests/e2e/ajustements_test.ts +++ /dev/null @@ -1,349 +0,0 @@ -// E2E tests for /ajustements endpoints — handler + real DB - -import { assertEquals, assertExists } from "@std/assert"; -import { - makeContextWithAffiliation, - makeEmployeeContext, - makeGetRequest, - makeJsonRequest, -} from "../helpers/handler.ts"; -import { - seedAjustements, - seedPromotions, - seedStudents, - seedUes, - truncateAll, -} from "../helpers/db_integration.ts"; -import { handler as ajustementsHandler } from "$apps/notes/api/ajustements.ts"; -import { handler as ajustementHandler } from "$apps/notes/api/ajustements/[numEtud]/[idUE].ts"; -import { ajustements as ajustementsTable } from "$root/databases/schema.ts"; -import { testDb } from "../helpers/db_integration.ts"; - -// --- GET /ajustements --- - -Deno.test({ - name: "e2e ajustements: GET /ajustements returns all", - async fn() { - await truncateAll(); - await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ - nom: "Dupont", - prenom: "Jean", - idPromo: "P1", - }]); - const [ue] = await seedUes([{ nom: "UE Info" }]); - await seedAjustements([{ numEtud: s.numEtud, idUE: ue.id, valeur: 13.0 }]); - const res = await ajustementsHandler.GET!( - makeGetRequest("/ajustements"), - makeEmployeeContext(), - ); - assertEquals(res.status, 200); - const body = await res.json(); - assertEquals(body.length, 1); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e ajustements: GET /ajustements?numEtud filters by student", - async fn() { - await truncateAll(); - await seedPromotions([{ id: "P1" }]); - const [s1] = await seedStudents([{ - nom: "Dupont", - prenom: "Jean", - idPromo: "P1", - }]); - const [s2] = await seedStudents([{ - nom: "Martin", - prenom: "Alice", - idPromo: "P1", - }]); - const [ue] = await seedUes([{ nom: "UE Info" }]); - await seedAjustements([ - { numEtud: s1.numEtud, idUE: ue.id, valeur: 13.0 }, - { numEtud: s2.numEtud, idUE: ue.id, valeur: 15.0 }, - ]); - const res = await ajustementsHandler.GET!( - makeGetRequest("/ajustements", { numEtud: String(s1.numEtud) }), - makeEmployeeContext(), - ); - assertEquals(res.status, 200); - const body = await res.json(); - assertEquals(body.length, 1); - assertEquals(body[0].numEtud, s1.numEtud); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e ajustements: GET /ajustements?numEtud=NaN returns 400", - async fn() { - await truncateAll(); - const res = await ajustementsHandler.GET!( - makeGetRequest("/ajustements", { numEtud: "abc" }), - makeEmployeeContext(), - ); - assertEquals(res.status, 400); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -// --- POST /ajustements --- - -Deno.test({ - name: - "e2e ajustements: POST /ajustements creates ajustement (201) as employee", - async fn() { - await truncateAll(); - await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ - nom: "Leroy", - prenom: "Paul", - idPromo: "P1", - }]); - const [ue] = await seedUes([{ nom: "UE Info" }]); - const res = await ajustementsHandler.POST!( - makeJsonRequest("/ajustements", "POST", { - numEtud: s.numEtud, - idUE: ue.id, - valeur: 14.5, - }), - makeEmployeeContext(), - ); - assertEquals(res.status, 201); - const body = await res.json(); - assertExists(body.numEtud); - assertEquals(body.valeur, 14.5); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e ajustements: POST /ajustements 403 for non-employee", - async fn() { - await truncateAll(); - const res = await ajustementsHandler.POST!( - makeJsonRequest("/ajustements", "POST", { - numEtud: 1, - idUE: 1, - valeur: 10.0, - }), - makeContextWithAffiliation("student"), - ); - assertEquals(res.status, 403); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e ajustements: POST /ajustements 400 on missing fields", - async fn() { - await truncateAll(); - const res = await ajustementsHandler.POST!( - makeJsonRequest("/ajustements", "POST", { numEtud: 12345 }), - makeEmployeeContext(), - ); - assertEquals(res.status, 400); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -// --- GET /ajustements/:numEtud/:idUE --- - -Deno.test({ - name: - "e2e ajustements: GET /ajustements/:numEtud/:idUE returns correct ajustement (employee)", - async fn() { - await truncateAll(); - await seedPromotions([{ id: "P1" }]); - const [s1, s2] = await seedStudents([ - { nom: "Bernard", prenom: "Lucie", idPromo: "P1" }, - { nom: "Dupont", prenom: "Jean", idPromo: "P1" }, - ]); - const [ue1, ue2] = await seedUes([{ nom: "UE Maths" }, { nom: "UE Info" }]); - // Plusieurs lignes partageant numEtud=s1 — le handler doit discriminer par idUE - await seedAjustements([ - { numEtud: s1.numEtud, idUE: ue1.id, valeur: 16.0 }, - { numEtud: s1.numEtud, idUE: ue2.id, valeur: 8.0 }, - { numEtud: s2.numEtud, idUE: ue1.id, valeur: 12.0 }, - ]); - const res = await ajustementHandler.GET!( - makeGetRequest(`/ajustements/${s1.numEtud}/${ue1.id}`), - makeEmployeeContext({ - numEtud: String(s1.numEtud), - idUE: String(ue1.id), - }), - ); - assertEquals(res.status, 200); - const body = await res.json(); - assertEquals(body.valeur, 16.0); - assertEquals(body.numEtud, s1.numEtud); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e ajustements: GET /ajustements/:numEtud/:idUE 403 for non-employee", - async fn() { - await truncateAll(); - const res = await ajustementHandler.GET!( - makeGetRequest("/ajustements/1/1"), - makeContextWithAffiliation("student", { numEtud: "1", idUE: "1" }), - ); - assertEquals(res.status, 403); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e ajustements: GET /ajustements/:numEtud/:idUE 404 when not found", - async fn() { - await truncateAll(); - const res = await ajustementHandler.GET!( - makeGetRequest("/ajustements/99999/99"), - makeEmployeeContext({ numEtud: "99999", idUE: "99" }), - ); - assertEquals(res.status, 404); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -// --- PUT /ajustements/:numEtud/:idUE --- - -Deno.test({ - name: - "e2e ajustements: PUT /ajustements/:numEtud/:idUE updates only targeted row (employee)", - async fn() { - await truncateAll(); - await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ - nom: "Thomas", - prenom: "Eva", - idPromo: "P1", - }]); - const [ue1, ue2] = await seedUes([{ nom: "UE Physique" }, { - nom: "UE Chimie", - }]); - // Deux ajustements pour le même étudiant — seul ue1 doit être modifié - await seedAjustements([ - { numEtud: s.numEtud, idUE: ue1.id, valeur: 10.0 }, - { numEtud: s.numEtud, idUE: ue2.id, valeur: 7.0 }, - ]); - const res = await ajustementHandler.PUT!( - makeJsonRequest(`/ajustements/${s.numEtud}/${ue1.id}`, "PUT", { - valeur: 19.0, - }), - makeEmployeeContext({ numEtud: String(s.numEtud), idUE: String(ue1.id) }), - ); - assertEquals(res.status, 200); - const body = await res.json(); - assertEquals(body.valeur, 19.0); - // ue2 doit rester intact - const unchanged = await testDb.select().from(ajustementsTable); - const ue2Row = unchanged.find((a) => a.idUE === ue2.id); - assertEquals(ue2Row?.valeur, 7.0); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e ajustements: PUT /ajustements/:numEtud/:idUE 403 for non-employee", - async fn() { - await truncateAll(); - const res = await ajustementHandler.PUT!( - makeJsonRequest("/ajustements/1/1", "PUT", { valeur: 10.0 }), - makeContextWithAffiliation("student", { numEtud: "1", idUE: "1" }), - ); - assertEquals(res.status, 403); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e ajustements: PUT /ajustements/:numEtud/:idUE 404 when not found", - async fn() { - await truncateAll(); - const res = await ajustementHandler.PUT!( - makeJsonRequest("/ajustements/99999/99", "PUT", { valeur: 10.0 }), - makeEmployeeContext({ numEtud: "99999", idUE: "99" }), - ); - assertEquals(res.status, 404); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -// --- DELETE /ajustements/:numEtud/:idUE --- - -Deno.test({ - name: - "e2e ajustements: DELETE /ajustements/:numEtud/:idUE deletes only targeted row (employee)", - async fn() { - await truncateAll(); - await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ - nom: "Petit", - prenom: "Hugo", - idPromo: "P1", - }]); - const [ue1, ue2] = await seedUes([{ nom: "UE Chimie" }, { nom: "UE Bio" }]); - // Deux ajustements pour le même étudiant — seul ue1 doit être supprimé - await seedAjustements([ - { numEtud: s.numEtud, idUE: ue1.id, valeur: 11.0 }, - { numEtud: s.numEtud, idUE: ue2.id, valeur: 14.0 }, - ]); - const res = await ajustementHandler.DELETE!( - makeGetRequest(`/ajustements/${s.numEtud}/${ue1.id}`), - makeEmployeeContext({ numEtud: String(s.numEtud), idUE: String(ue1.id) }), - ); - assertEquals(res.status, 204); - // ue2 doit toujours exister - const remaining = await testDb.select().from(ajustementsTable); - assertEquals(remaining.length, 1); - assertEquals(remaining[0].idUE, ue2.id); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: - "e2e ajustements: DELETE /ajustements/:numEtud/:idUE 403 for non-employee", - async fn() { - await truncateAll(); - const res = await ajustementHandler.DELETE!( - makeGetRequest("/ajustements/1/1"), - makeContextWithAffiliation("student", { numEtud: "1", idUE: "1" }), - ); - assertEquals(res.status, 403); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: - "e2e ajustements: DELETE /ajustements/:numEtud/:idUE 404 when not found", - async fn() { - await truncateAll(); - const res = await ajustementHandler.DELETE!( - makeGetRequest("/ajustements/99999/99"), - makeEmployeeContext({ numEtud: "99999", idUE: "99" }), - ); - assertEquals(res.status, 404); - }, - sanitizeResources: false, - sanitizeOps: false, -}); diff --git a/tests/e2e/basic_test.ts b/tests/e2e/basic_test.ts new file mode 100644 index 0000000..58c9552 --- /dev/null +++ b/tests/e2e/basic_test.ts @@ -0,0 +1,75 @@ +import { chromium, Browser, Page } from "npm:playwright"; +import { assertEquals } from "@std/assert"; + +const BASE_URL = Deno.env.get("BASE_URL") || "http://localhost:8000"; + +Deno.test({ + name: "E2E: Guest navigation flow", + async fn() { + const browser: Browser = await chromium.launch({ + args: ["--no-sandbox", "--disable-setuid-sandbox"], + }); + const context = await browser.newContext(); + const page: Page = await context.newPage(); + + try { + // 1. Home page + console.log(`Navigating to ${BASE_URL}...`); + await page.goto(BASE_URL); + + const title = await page.innerText("h2"); + assertEquals(title, "PolyMPR"); + + const loginLink = await page.getAttribute("a[href='/login']", "href"); + assertEquals(loginLink, "/login"); + + // 2. Click login + await page.click("text=Se connecter"); + await page.waitForURL("**/login**"); + console.log("Reached login page."); + + } finally { + await browser.close(); + } + }, + // On ignore si le serveur n'est pas joignable (hors CI) + ignore: Deno.env.get("CI") === undefined && !(await isServerUp()), +}); + +Deno.test({ + name: "E2E: App Dashboard accessibility (requires login)", + async fn() { + const browser: Browser = await chromium.launch({ + args: ["--no-sandbox", "--disable-setuid-sandbox"], + }); + const page: Page = await browser.newPage(); + + try { + // Tenter d'accéder à /apps sans être connecté + await page.goto(`${BASE_URL}/apps`); + + // On devrait être redirigé vers /login ou voir un message d'erreur + const url = page.url(); + if (url.includes("/login")) { + console.log("Correctly redirected to login."); + } else { + // Si ton middleware ne redirige pas mais affiche une erreur + const body = await page.innerText("body"); + console.log("Landing page url:", url); + } + } finally { + await browser.close(); + } + }, + ignore: Deno.env.get("CI") === undefined && !(await isServerUp()), +}); + +async function isServerUp() { + try { + const res = await fetch(BASE_URL); + await res.body?.cancel(); + return res.ok; + } catch { + return false; + } +} diff --git a/tests/e2e/enseignements_test.ts b/tests/e2e/enseignements_test.ts deleted file mode 100644 index 32c9326..0000000 --- a/tests/e2e/enseignements_test.ts +++ /dev/null @@ -1,240 +0,0 @@ -// E2E tests for /enseignements endpoints — handler + real DB - -import { assertEquals, assertExists } from "@std/assert"; -import { - makeContextWithAffiliation, - makeEmployeeContext, - makeGetRequest, - makeJsonRequest, -} from "../helpers/handler.ts"; -import { - seedEnseignements, - seedModules, - seedPromotions, - seedUsers, - truncateAll, -} from "../helpers/db_integration.ts"; -import { handler as enseignementsHandler } from "$apps/admin/api/enseignements.ts"; -import { handler as enseignementHandler } from "$apps/admin/api/enseignements/[idProf]/[idModule]/[idPromo].ts"; - -// --- POST /enseignements --- - -Deno.test({ - name: - "e2e enseignements: POST /enseignements creates enseignement (201) as employee", - async fn() { - await truncateAll(); - await seedUsers([{ id: "prof.dupont", nom: "Dupont", prenom: "Jean" }]); - await seedModules([{ id: "M1", nom: "Mod A" }]); - await seedPromotions([{ id: "P1" }]); - const res = await enseignementsHandler.POST!( - makeJsonRequest("/enseignements", "POST", { - idProf: "prof.dupont", - idModule: "M1", - idPromo: "P1", - }), - makeEmployeeContext(), - ); - assertEquals(res.status, 201); - const body = await res.json(); - assertExists(body.idProf); - assertEquals(body.idModule, "M1"); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e enseignements: POST /enseignements 403 for non-employee", - async fn() { - await truncateAll(); - const res = await enseignementsHandler.POST!( - makeJsonRequest("/enseignements", "POST", { - idProf: "p", - idModule: "M1", - idPromo: "P1", - }), - makeContextWithAffiliation("student"), - ); - assertEquals(res.status, 403); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e enseignements: POST /enseignements 400 on missing fields", - async fn() { - await truncateAll(); - const res = await enseignementsHandler.POST!( - makeJsonRequest("/enseignements", "POST", { idProf: "prof.dupont" }), - makeEmployeeContext(), - ); - assertEquals(res.status, 400); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e enseignements: POST /enseignements 409 on duplicate", - async fn() { - await truncateAll(); - await seedUsers([{ id: "prof.dupont", nom: "Dupont", prenom: "Jean" }]); - await seedModules([{ id: "M1", nom: "Mod A" }]); - await seedPromotions([{ id: "P1" }]); - await seedEnseignements([{ - idProf: "prof.dupont", - idModule: "M1", - idPromo: "P1", - }]); - const res = await enseignementsHandler.POST!( - makeJsonRequest("/enseignements", "POST", { - idProf: "prof.dupont", - idModule: "M1", - idPromo: "P1", - }), - makeEmployeeContext(), - ); - assertEquals(res.status, 409); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -// --- GET /enseignements/:idProf/:idModule/:idPromo --- - -Deno.test({ - name: - "e2e enseignements: GET /enseignements/:idProf/:idModule/:idPromo returns enseignement (employee)", - async fn() { - await truncateAll(); - await seedUsers([{ id: "prof.dupont", nom: "Dupont", prenom: "Jean" }]); - await seedModules([{ id: "M1", nom: "Mod A" }]); - await seedPromotions([{ id: "P1" }]); - await seedEnseignements([{ - idProf: "prof.dupont", - idModule: "M1", - idPromo: "P1", - }]); - const res = await enseignementHandler.GET!( - makeGetRequest("/enseignements/prof.dupont/M1/P1"), - makeEmployeeContext({ - idProf: "prof.dupont", - idModule: "M1", - idPromo: "P1", - }), - ); - assertEquals(res.status, 200); - const body = await res.json(); - assertEquals(body.idProf, "prof.dupont"); - assertEquals(body.idModule, "M1"); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: - "e2e enseignements: GET /enseignements/:idProf/:idModule/:idPromo 403 for non-employee", - async fn() { - await truncateAll(); - const res = await enseignementHandler.GET!( - makeGetRequest("/enseignements/p/M1/P1"), - makeContextWithAffiliation("student", { - idProf: "p", - idModule: "M1", - idPromo: "P1", - }), - ); - assertEquals(res.status, 403); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: - "e2e enseignements: GET /enseignements/:idProf/:idModule/:idPromo 404 when not found", - async fn() { - await truncateAll(); - const res = await enseignementHandler.GET!( - makeGetRequest("/enseignements/ghost/GHOST/GHOST"), - makeEmployeeContext({ - idProf: "ghost", - idModule: "GHOST", - idPromo: "GHOST", - }), - ); - assertEquals(res.status, 404); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -// --- DELETE /enseignements/:idProf/:idModule/:idPromo --- - -Deno.test({ - name: - "e2e enseignements: DELETE /enseignements/:idProf/:idModule/:idPromo returns 204 (employee)", - async fn() { - await truncateAll(); - await seedUsers([{ id: "prof.dupont", nom: "Dupont", prenom: "Jean" }]); - await seedModules([{ id: "M1", nom: "Mod A" }]); - await seedPromotions([{ id: "P1" }]); - await seedEnseignements([{ - idProf: "prof.dupont", - idModule: "M1", - idPromo: "P1", - }]); - const res = await enseignementHandler.DELETE!( - makeGetRequest("/enseignements/prof.dupont/M1/P1"), - makeEmployeeContext({ - idProf: "prof.dupont", - idModule: "M1", - idPromo: "P1", - }), - ); - assertEquals(res.status, 204); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: - "e2e enseignements: DELETE /enseignements/:idProf/:idModule/:idPromo 403 for non-employee", - async fn() { - await truncateAll(); - const res = await enseignementHandler.DELETE!( - makeGetRequest("/enseignements/p/M1/P1"), - makeContextWithAffiliation("student", { - idProf: "p", - idModule: "M1", - idPromo: "P1", - }), - ); - assertEquals(res.status, 403); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: - "e2e enseignements: DELETE /enseignements/:idProf/:idModule/:idPromo 404 when not found", - async fn() { - await truncateAll(); - const res = await enseignementHandler.DELETE!( - makeGetRequest("/enseignements/ghost/GHOST/GHOST"), - makeEmployeeContext({ - idProf: "ghost", - idModule: "GHOST", - idPromo: "GHOST", - }), - ); - assertEquals(res.status, 404); - }, - sanitizeResources: false, - sanitizeOps: false, -}); diff --git a/tests/e2e/modules_test.ts b/tests/e2e/modules_test.ts deleted file mode 100644 index 3077062..0000000 --- a/tests/e2e/modules_test.ts +++ /dev/null @@ -1,210 +0,0 @@ -// #113 - E2E tests for /modules endpoints - -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: "e2e modules: GET /modules returns all as employee", - async fn() { - await truncateAll(); - await seedModules([{ id: "MATH101", nom: "Mathématiques" }, { - id: "INFO101", - nom: "Informatique", - }]); - 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: "e2e modules: GET /modules returns all for non-employee", - async fn() { - await truncateAll(); - await seedModules([{ id: "MATH101", nom: "Mathématiques" }]); - 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: "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 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: "e2e modules: GET /modules/:id 404 when not found", - async fn() { - await truncateAll(); - 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, -}); diff --git a/tests/e2e/notes_test.ts b/tests/e2e/notes_test.ts deleted file mode 100644 index ee1f491..0000000 --- a/tests/e2e/notes_test.ts +++ /dev/null @@ -1,283 +0,0 @@ -// E2E tests for /notes endpoints — handler + real DB - -import { assertEquals, assertExists } from "@std/assert"; -import { - makeEmployeeContext, - makeGetRequest, - makeJsonRequest, -} from "../helpers/handler.ts"; -import { - seedModules, - seedNotes, - seedPromotions, - seedStudents, - truncateAll, -} from "../helpers/db_integration.ts"; -import { handler as notesHandler } from "$apps/notes/api/notes.ts"; -import { handler as noteHandler } from "$apps/notes/api/notes/[numEtud]/[idModule].ts"; - -// --- GET /notes --- - -Deno.test({ - name: "e2e notes: GET /notes returns all notes", - async fn() { - await truncateAll(); - await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ - nom: "Dupont", - prenom: "Jean", - idPromo: "P1", - }]); - await seedModules([{ id: "M1", nom: "Mod A" }, { id: "M2", nom: "Mod B" }]); - await seedNotes([ - { numEtud: s.numEtud, idModule: "M1", note: 15.0 }, - { numEtud: s.numEtud, idModule: "M2", note: 12.0 }, - ]); - const res = await notesHandler.GET!( - makeGetRequest("/notes"), - makeEmployeeContext(), - ); - assertEquals(res.status, 200); - const body = await res.json(); - assertEquals(body.length, 2); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e notes: GET /notes?numEtud filters by student", - async fn() { - await truncateAll(); - await seedPromotions([{ id: "P1" }]); - const [s1] = await seedStudents([{ - nom: "Dupont", - prenom: "Jean", - idPromo: "P1", - }]); - const [s2] = await seedStudents([{ - nom: "Martin", - prenom: "Alice", - idPromo: "P1", - }]); - await seedModules([{ id: "M1", nom: "Mod A" }]); - await seedNotes([ - { numEtud: s1.numEtud, idModule: "M1", note: 15.0 }, - { numEtud: s2.numEtud, idModule: "M1", note: 12.0 }, - ]); - const res = await notesHandler.GET!( - makeGetRequest("/notes", { numEtud: String(s1.numEtud) }), - makeEmployeeContext(), - ); - assertEquals(res.status, 200); - const body = await res.json(); - assertEquals(body.length, 1); - assertEquals(body[0].numEtud, s1.numEtud); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e notes: GET /notes?numEtud=NaN returns 400", - async fn() { - await truncateAll(); - const res = await notesHandler.GET!( - makeGetRequest("/notes", { numEtud: "abc" }), - makeEmployeeContext(), - ); - assertEquals(res.status, 400); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e notes: GET /notes?idModule filters by module", - async fn() { - await truncateAll(); - await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ - nom: "Dupont", - prenom: "Jean", - idPromo: "P1", - }]); - await seedModules([{ id: "M1", nom: "Mod A" }, { id: "M2", nom: "Mod B" }]); - await seedNotes([ - { numEtud: s.numEtud, idModule: "M1", note: 15.0 }, - { numEtud: s.numEtud, idModule: "M2", note: 10.0 }, - ]); - const res = await notesHandler.GET!( - makeGetRequest("/notes", { idModule: "M1" }), - makeEmployeeContext(), - ); - assertEquals(res.status, 200); - const body = await res.json(); - assertEquals(body.length, 1); - assertEquals(body[0].idModule, "M1"); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -// --- POST /notes --- - -Deno.test({ - name: "e2e notes: POST /notes creates note (201)", - async fn() { - await truncateAll(); - await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ - nom: "Leroy", - prenom: "Paul", - idPromo: "P1", - }]); - await seedModules([{ id: "M1", nom: "Mod A" }]); - const res = await notesHandler.POST!( - makeJsonRequest("/notes", "POST", { - numEtud: s.numEtud, - idModule: "M1", - note: 14.0, - }), - makeEmployeeContext(), - ); - assertEquals(res.status, 201); - const body = await res.json(); - assertExists(body.numEtud); - assertEquals(body.note, 14.0); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e notes: POST /notes 400 on missing fields", - async fn() { - await truncateAll(); - const res = await notesHandler.POST!( - makeJsonRequest("/notes", "POST", { numEtud: 12345 }), - makeEmployeeContext(), - ); - assertEquals(res.status, 400); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -// --- GET /notes/:numEtud/:idModule --- - -Deno.test({ - name: "e2e notes: GET /notes/:numEtud/:idModule returns note", - async fn() { - await truncateAll(); - await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ - nom: "Bernard", - prenom: "Lucie", - idPromo: "P1", - }]); - await seedModules([{ id: "M1", nom: "Mod A" }]); - await seedNotes([{ numEtud: s.numEtud, idModule: "M1", note: 18.0 }]); - const res = await noteHandler.GET!( - makeGetRequest(`/notes/${s.numEtud}/M1`), - makeEmployeeContext({ numEtud: String(s.numEtud), idModule: "M1" }), - ); - assertEquals(res.status, 200); - const body = await res.json(); - assertEquals(body.note, 18.0); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e notes: GET /notes/:numEtud/:idModule 404 when not found", - async fn() { - await truncateAll(); - const res = await noteHandler.GET!( - makeGetRequest("/notes/99999/GHOST"), - makeEmployeeContext({ numEtud: "99999", idModule: "GHOST" }), - ); - assertEquals(res.status, 404); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -// --- PUT /notes/:numEtud/:idModule --- - -Deno.test({ - name: "e2e notes: PUT /notes/:numEtud/:idModule updates note", - async fn() { - await truncateAll(); - await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ - nom: "Thomas", - prenom: "Eva", - idPromo: "P1", - }]); - await seedModules([{ id: "M1", nom: "Mod A" }]); - await seedNotes([{ numEtud: s.numEtud, idModule: "M1", note: 10.0 }]); - const res = await noteHandler.PUT!( - makeJsonRequest(`/notes/${s.numEtud}/M1`, "PUT", { note: 16.0 }), - makeEmployeeContext({ numEtud: String(s.numEtud), idModule: "M1" }), - ); - assertEquals(res.status, 200); - const body = await res.json(); - assertEquals(body.note, 16.0); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e notes: PUT /notes/:numEtud/:idModule 404 when not found", - async fn() { - await truncateAll(); - const res = await noteHandler.PUT!( - makeJsonRequest("/notes/99999/GHOST", "PUT", { note: 10.0 }), - makeEmployeeContext({ numEtud: "99999", idModule: "GHOST" }), - ); - assertEquals(res.status, 404); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -// --- DELETE /notes/:numEtud/:idModule --- - -Deno.test({ - name: "e2e notes: DELETE /notes/:numEtud/:idModule returns 204", - async fn() { - await truncateAll(); - await seedPromotions([{ id: "P1" }]); - const [s] = await seedStudents([{ - nom: "Petit", - prenom: "Hugo", - idPromo: "P1", - }]); - await seedModules([{ id: "M1", nom: "Mod A" }]); - await seedNotes([{ numEtud: s.numEtud, idModule: "M1", note: 9.0 }]); - const res = await noteHandler.DELETE!( - makeGetRequest(`/notes/${s.numEtud}/M1`), - makeEmployeeContext({ numEtud: String(s.numEtud), idModule: "M1" }), - ); - assertEquals(res.status, 204); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e notes: DELETE /notes/:numEtud/:idModule 404 when not found", - async fn() { - await truncateAll(); - const res = await noteHandler.DELETE!( - makeGetRequest("/notes/99999/GHOST"), - makeEmployeeContext({ numEtud: "99999", idModule: "GHOST" }), - ); - assertEquals(res.status, 404); - }, - sanitizeResources: false, - sanitizeOps: false, -}); diff --git a/tests/e2e/promotions_test.ts b/tests/e2e/promotions_test.ts deleted file mode 100644 index b296229..0000000 --- a/tests/e2e/promotions_test.ts +++ /dev/null @@ -1,212 +0,0 @@ -// #110 - E2E tests for /promotions endpoints - -import { assertEquals } from "@std/assert"; -import { - 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: "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 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: "e2e promotions: GET /promotions returns empty for non-employee", - async fn() { - await truncateAll(); - 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: "e2e promotions: POST /promotions creates promotion (201)", - async fn() { - await truncateAll(); - 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: "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 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: "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" }]); - const res = await promotionHandler.DELETE!( - makeGetRequest("/promotions/INFO3-2022"), - makeEmployeeContext({ idPromo: "INFO3-2022" }), - ); - assertEquals(res.status, 204); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e promotions: DELETE /promotions/:id 404 when not found", - async fn() { - await truncateAll(); - const res = await promotionHandler.DELETE!( - makeGetRequest("/promotions/GHOST"), - makeEmployeeContext({ idPromo: "GHOST" }), - ); - assertEquals(res.status, 404); - }, - sanitizeResources: false, - sanitizeOps: false, -}); diff --git a/tests/e2e/roles_test.ts b/tests/e2e/roles_test.ts deleted file mode 100644 index 8026434..0000000 --- a/tests/e2e/roles_test.ts +++ /dev/null @@ -1,175 +0,0 @@ -// #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 } 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: "e2e roles: GET /roles returns all with permissions", - async fn() { - await truncateAll(); - await seedRoles([{ nom: "admin" }, { nom: "employee" }]); - 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: "e2e roles: POST /roles creates role (201)", - async fn() { - await truncateAll(); - 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: "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" }, - ]); - 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: "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" }]); - await testDb.insert(permissions).values([ - { id: "note_read", nom: "Consulter les notes" }, - ]); - const res = await roleHandler.PUT!( - makeJsonRequest(`/roles/${role.id}`, "PUT", { - nom: "teacher", - permissions: ["note_read"], - }), - makeEmployeeContext({ idRole: String(role.id) }), - ); - 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: "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" }]); - 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, -}); diff --git a/tests/e2e/students_test.ts b/tests/e2e/students_test.ts deleted file mode 100644 index e02103f..0000000 --- a/tests/e2e/students_test.ts +++ /dev/null @@ -1,288 +0,0 @@ -// #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, - truncateAll, -} from "../helpers/db_integration.ts"; -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: "e2e students: GET /students returns all students as employee", - async fn() { - await truncateAll(); - await seedPromotions([{ id: "PEIP1-2024" }]); - await seedStudents([ - { nom: "Dupont", prenom: "Jean", idPromo: "PEIP1-2024" }, - { nom: "Martin", prenom: "Alice", idPromo: "PEIP1-2024" }, - ]); - - 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: "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" }]); - await seedStudents([ - { nom: "Dupont", prenom: "Jean", idPromo: "PEIP1-2024" }, - { nom: "Martin", prenom: "Alice", idPromo: "PEIP1-2024" }, - { nom: "Durand", prenom: "Claire", idPromo: "PEIP2-2024" }, - ]); - - 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: "e2e students: POST /students creates a student (201)", - async fn() { - await truncateAll(); - await seedPromotions([{ id: "INFO3-2024" }]); - - const req = makeJsonRequest("/students", "POST", { - nom: "Leroy", - prenom: "Paul", - idPromo: "INFO3-2024", - }); - const ctx = makeEmployeeContext(); - const res = await studentsHandler.POST!(req, ctx); - - 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: "e2e students: POST /students 403 for non-employee", - async fn() { - await truncateAll(); - - const req = makeJsonRequest("/students", "POST", { - nom: "Test", - prenom: "User", - idPromo: "PEIP1-2024", - }); - const ctx = makeContextWithAffiliation("student"); - const res = await studentsHandler.POST!(req, ctx); - - assertEquals(res.status, 403); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - 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" }]); - const [s] = await seedStudents([ - { nom: "Petit", prenom: "Hugo", idPromo: "INFO3-2024" }, - ]); - - 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(res.status, 200); - const body = await res.json(); - assertEquals(body.nom, "Grand"); - assertEquals(body.idPromo, "INFO4-2024"); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - 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" }]); - const [s] = await seedStudents([ - { nom: "Thomas", prenom: "Eva", idPromo: "INFO3-2024" }, - ]); - - const req = makeGetRequest(`/students/${s.numEtud}`); - const ctx = makeEmployeeContext({ numEtud: String(s.numEtud) }); - const res = await studentHandler.DELETE!(req, ctx); - - assertEquals(res.status, 204); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e students: DELETE /students/:numEtud 404 when not found", - async fn() { - await truncateAll(); - - const req = makeGetRequest("/students/999999"); - const ctx = makeEmployeeContext({ numEtud: "999999" }); - const res = await studentHandler.DELETE!(req, ctx); - - assertEquals(res.status, 404); - }, - sanitizeResources: false, - sanitizeOps: false, -}); diff --git a/tests/e2e/ue_modules_test.ts b/tests/e2e/ue_modules_test.ts deleted file mode 100644 index 30dba17..0000000 --- a/tests/e2e/ue_modules_test.ts +++ /dev/null @@ -1,312 +0,0 @@ -// 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/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: "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, -}); diff --git a/tests/e2e/ues_test.ts b/tests/e2e/ues_test.ts deleted file mode 100644 index d5d726d..0000000 --- a/tests/e2e/ues_test.ts +++ /dev/null @@ -1,178 +0,0 @@ -// E2E tests for /ues endpoints — handler + real DB - -import { assertEquals, assertExists } from "@std/assert"; -import { - makeEmployeeContext, - makeGetRequest, - makeJsonRequest, -} from "../helpers/handler.ts"; -import { seedUes, truncateAll } from "../helpers/db_integration.ts"; -import { handler as uesHandler } from "$apps/admin/api/ues.ts"; -import { handler as ueHandler } from "$apps/admin/api/ues/[idUE].ts"; - -// --- GET /ues --- - -Deno.test({ - name: "e2e ues: GET /ues returns all UEs", - async fn() { - await truncateAll(); - await seedUes([{ nom: "UE Informatique" }, { nom: "UE Mathématiques" }]); - const res = await uesHandler.GET!( - makeGetRequest("/ues"), - makeEmployeeContext(), - ); - assertEquals(res.status, 200); - const body = await res.json(); - assertEquals(body.length, 2); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e ues: GET /ues returns empty when no UEs", - async fn() { - await truncateAll(); - const res = await uesHandler.GET!( - makeGetRequest("/ues"), - makeEmployeeContext(), - ); - assertEquals(res.status, 200); - const body = await res.json(); - assertEquals(body.length, 0); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -// --- POST /ues --- - -Deno.test({ - name: "e2e ues: POST /ues creates UE (201)", - async fn() { - await truncateAll(); - const res = await uesHandler.POST!( - makeJsonRequest("/ues", "POST", { nom: "UE Physique" }), - makeEmployeeContext(), - ); - assertEquals(res.status, 201); - const body = await res.json(); - assertExists(body.id); - assertEquals(body.nom, "UE Physique"); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e ues: POST /ues 400 on missing nom", - async fn() { - await truncateAll(); - const res = await uesHandler.POST!( - makeJsonRequest("/ues", "POST", {}), - makeEmployeeContext(), - ); - assertEquals(res.status, 400); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -// --- GET /ues/:id --- - -Deno.test({ - name: "e2e ues: GET /ues/:id returns UE", - async fn() { - await truncateAll(); - const [ue] = await seedUes([{ nom: "UE Chimie" }]); - const res = await ueHandler.GET!( - makeGetRequest(`/ues/${ue.id}`), - makeEmployeeContext({ idUE: String(ue.id) }), - ); - assertEquals(res.status, 200); - const body = await res.json(); - assertEquals(body.nom, "UE Chimie"); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e ues: GET /ues/:id 404 when not found", - async fn() { - await truncateAll(); - const res = await ueHandler.GET!( - makeGetRequest("/ues/99999"), - makeEmployeeContext({ idUE: "99999" }), - ); - assertEquals(res.status, 404); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -// --- PUT /ues/:id --- - -Deno.test({ - name: "e2e ues: PUT /ues/:id updates nom", - async fn() { - await truncateAll(); - const [ue] = await seedUes([{ nom: "UE Biologie" }]); - const res = await ueHandler.PUT!( - makeJsonRequest(`/ues/${ue.id}`, "PUT", { - nom: "UE Biologie moléculaire", - }), - makeEmployeeContext({ idUE: String(ue.id) }), - ); - assertEquals(res.status, 200); - const body = await res.json(); - assertEquals(body.nom, "UE Biologie moléculaire"); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e ues: PUT /ues/:id 404 when not found", - async fn() { - await truncateAll(); - const res = await ueHandler.PUT!( - makeJsonRequest("/ues/99999", "PUT", { nom: "X" }), - makeEmployeeContext({ idUE: "99999" }), - ); - assertEquals(res.status, 404); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -// --- DELETE /ues/:id --- - -Deno.test({ - name: "e2e ues: DELETE /ues/:id returns 204", - async fn() { - await truncateAll(); - const [ue] = await seedUes([{ nom: "UE à supprimer" }]); - const res = await ueHandler.DELETE!( - makeGetRequest(`/ues/${ue.id}`), - makeEmployeeContext({ idUE: String(ue.id) }), - ); - assertEquals(res.status, 204); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e ues: DELETE /ues/:id 404 when not found", - async fn() { - await truncateAll(); - const res = await ueHandler.DELETE!( - makeGetRequest("/ues/99999"), - makeEmployeeContext({ idUE: "99999" }), - ); - assertEquals(res.status, 404); - }, - sanitizeResources: false, - sanitizeOps: false, -}); diff --git a/tests/e2e/users_test.ts b/tests/e2e/users_test.ts deleted file mode 100644 index 830aefa..0000000 --- a/tests/e2e/users_test.ts +++ /dev/null @@ -1,239 +0,0 @@ -// E2E tests for /users endpoints — handler + real DB - -import { assertEquals, assertExists } from "@std/assert"; -import { - makeEmployeeContext, - makeGetRequest, - makeJsonRequest, -} from "../helpers/handler.ts"; -import { - seedRoles, - seedUsers, - truncateAll, -} from "../helpers/db_integration.ts"; -import { handler as usersHandler } from "$apps/admin/api/users.ts"; -import { handler as userHandler } from "$apps/admin/api/users/[id].ts"; - -// --- GET /users --- - -Deno.test({ - name: "e2e users: GET /users returns all users", - async fn() { - await truncateAll(); - await seedUsers([ - { id: "dupont.jean", nom: "Dupont", prenom: "Jean" }, - { id: "martin.alice", nom: "Martin", prenom: "Alice" }, - ]); - const res = await usersHandler.GET!( - makeGetRequest("/users"), - makeEmployeeContext(), - ); - assertEquals(res.status, 200); - const body = await res.json(); - assertEquals(body.length, 2); - assertExists(body.find((u: { id: string }) => u.id === "dupont.jean")); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e users: GET /users returns empty when no users", - async fn() { - await truncateAll(); - const res = await usersHandler.GET!( - makeGetRequest("/users"), - makeEmployeeContext(), - ); - assertEquals(res.status, 200); - const body = await res.json(); - assertEquals(body.length, 0); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e users: GET /users?idRole filters by role", - async fn() { - await truncateAll(); - const [role1] = await seedRoles([{ nom: "admin" }]); - const [role2] = await seedRoles([{ nom: "employee" }]); - await seedUsers([ - { id: "admin.user", nom: "Admin", prenom: "User", idRole: role1.id }, - { id: "emp.user", nom: "Emp", prenom: "User", idRole: role2.id }, - ]); - const res = await usersHandler.GET!( - makeGetRequest("/users", { idRole: String(role1.id) }), - makeEmployeeContext(), - ); - assertEquals(res.status, 200); - const body = await res.json(); - assertEquals(body.length, 1); - assertEquals(body[0].id, "admin.user"); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -// --- POST /users --- - -Deno.test({ - name: "e2e users: POST /users creates user (201)", - async fn() { - await truncateAll(); - const res = await usersHandler.POST!( - makeJsonRequest("/users", "POST", { - id: "new.user", - nom: "New", - prenom: "User", - }), - makeEmployeeContext(), - ); - assertEquals(res.status, 201); - const body = await res.json(); - assertEquals(body.id, "new.user"); - assertEquals(body.nom, "New"); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e users: POST /users 400 on missing fields", - async fn() { - await truncateAll(); - const res = await usersHandler.POST!( - makeJsonRequest("/users", "POST", { id: "x" }), - makeEmployeeContext(), - ); - assertEquals(res.status, 400); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e users: POST /users 409 on duplicate id", - async fn() { - await truncateAll(); - await seedUsers([{ id: "dupont.jean", nom: "Dupont", prenom: "Jean" }]); - const res = await usersHandler.POST!( - makeJsonRequest("/users", "POST", { - id: "dupont.jean", - nom: "Doublon", - prenom: "X", - }), - makeEmployeeContext(), - ); - assertEquals(res.status, 409); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -// --- GET /users/:id --- - -Deno.test({ - name: "e2e users: GET /users/:id returns user", - async fn() { - await truncateAll(); - await seedUsers([{ id: "bernard.lucie", nom: "Bernard", prenom: "Lucie" }]); - const res = await userHandler.GET!( - makeGetRequest("/users/bernard.lucie"), - makeEmployeeContext({ id: "bernard.lucie" }), - ); - assertEquals(res.status, 200); - const body = await res.json(); - assertEquals(body.id, "bernard.lucie"); - assertEquals(body.nom, "Bernard"); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e users: GET /users/:id 404 when not found", - async fn() { - await truncateAll(); - const res = await userHandler.GET!( - makeGetRequest("/users/ghost.user"), - makeEmployeeContext({ id: "ghost.user" }), - ); - assertEquals(res.status, 404); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -// --- PUT /users/:id --- - -Deno.test({ - name: "e2e users: PUT /users/:id updates user", - async fn() { - await truncateAll(); - await seedUsers([{ id: "thomas.eva", nom: "Thomas", prenom: "Eva" }]); - const res = await userHandler.PUT!( - makeJsonRequest("/users/thomas.eva", "PUT", { - nom: "Thomas-Modifié", - prenom: "Eva", - idRole: null, - }), - makeEmployeeContext({ id: "thomas.eva" }), - ); - assertEquals(res.status, 200); - const body = await res.json(); - assertEquals(body.nom, "Thomas-Modifié"); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e users: PUT /users/:id 404 when not found", - async fn() { - await truncateAll(); - const res = await userHandler.PUT!( - makeJsonRequest("/users/ghost.user", "PUT", { - nom: "X", - prenom: "Y", - idRole: null, - }), - makeEmployeeContext({ id: "ghost.user" }), - ); - assertEquals(res.status, 404); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -// --- DELETE /users/:id --- - -Deno.test({ - name: "e2e users: DELETE /users/:id returns 204", - async fn() { - await truncateAll(); - await seedUsers([{ id: "petit.hugo", nom: "Petit", prenom: "Hugo" }]); - const res = await userHandler.DELETE!( - makeGetRequest("/users/petit.hugo"), - makeEmployeeContext({ id: "petit.hugo" }), - ); - assertEquals(res.status, 204); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "e2e users: DELETE /users/:id 404 when not found", - async fn() { - await truncateAll(); - const res = await userHandler.DELETE!( - makeGetRequest("/users/ghost.user"), - makeEmployeeContext({ id: "ghost.user" }), - ); - assertEquals(res.status, 404); - }, - sanitizeResources: false, - sanitizeOps: false, -}); diff --git a/tests/integration/ajustements_test.ts b/tests/integration/ajustements_test.ts index 49e6fcd..2ca2ef7 100644 --- a/tests/integration/ajustements_test.ts +++ b/tests/integration/ajustements_test.ts @@ -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, diff --git a/tests/integration/enseignements_test.ts b/tests/integration/enseignements_test.ts index 40086a9..32c9326 100644 --- a/tests/integration/enseignements_test.ts +++ b/tests/integration/enseignements_test.ts @@ -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, diff --git a/tests/integration/modules_test.ts b/tests/integration/modules_test.ts index df32fba..3077062 100644 --- a/tests/integration/modules_test.ts +++ b/tests/integration/modules_test.ts @@ -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, diff --git a/tests/integration/notes_test.ts b/tests/integration/notes_test.ts index b9018b9..ee1f491 100644 --- a/tests/integration/notes_test.ts +++ b/tests/integration/notes_test.ts @@ -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, diff --git a/tests/e2e/permissions_test.ts b/tests/integration/permissions_test.ts similarity index 100% rename from tests/e2e/permissions_test.ts rename to tests/integration/permissions_test.ts diff --git a/tests/integration/promotions_test.ts b/tests/integration/promotions_test.ts index 07b24fd..b296229 100644 --- a/tests/integration/promotions_test.ts +++ b/tests/integration/promotions_test.ts @@ -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, diff --git a/tests/e2e/robustness_test.ts b/tests/integration/robustness_test.ts similarity index 100% rename from tests/e2e/robustness_test.ts rename to tests/integration/robustness_test.ts diff --git a/tests/integration/roles_test.ts b/tests/integration/roles_test.ts index 9fb7a6c..8026434 100644 --- a/tests/integration/roles_test.ts +++ b/tests/integration/roles_test.ts @@ -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, diff --git a/tests/integration/students_test.ts b/tests/integration/students_test.ts index bb53d6f..e02103f 100644 --- a/tests/integration/students_test.ts +++ b/tests/integration/students_test.ts @@ -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, diff --git a/tests/integration/ue_modules_test.ts b/tests/integration/ue_modules_test.ts index 9aaab2a..30dba17 100644 --- a/tests/integration/ue_modules_test.ts +++ b/tests/integration/ue_modules_test.ts @@ -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, diff --git a/tests/integration/ues_test.ts b/tests/integration/ues_test.ts index 790330a..d5d726d 100644 --- a/tests/integration/ues_test.ts +++ b/tests/integration/ues_test.ts @@ -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, diff --git a/tests/integration/users_test.ts b/tests/integration/users_test.ts index e0d5ae9..830aefa 100644 --- a/tests/integration/users_test.ts +++ b/tests/integration/users_test.ts @@ -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, diff --git a/tests/unit/ajustements_test.ts b/tests/unit/ajustements_test.ts deleted file mode 100644 index 8820c23..0000000 --- a/tests/unit/ajustements_test.ts +++ /dev/null @@ -1,224 +0,0 @@ -// Unit tests for /ajustements endpoints — fixtures, mock API, mock DB - -import { assertEquals, assertExists } from "@std/assert"; -import { mockFetch, restoreFetch } from "../helpers/api_mock.ts"; -import { createMockDb } from "../helpers/db_mock.ts"; -import { type Ajustement, ajustements } from "../helpers/fixtures.ts"; - -// --- Fixtures --- - -Deno.test("ajustements: fixtures have correct shape", () => { - assertEquals(ajustements.length, 2); - assertEquals(typeof ajustements[0].numEtud, "number"); - assertEquals(typeof ajustements[0].idUE, "number"); - assertEquals(typeof ajustements[0].valeur, "number"); -}); - -// --- Mock API --- - -Deno.test("mock API: GET /ajustements returns list", async () => { - mockFetch({ "/ajustements": ajustements }); - try { - const res = await fetch("http://localhost/api/ajustements"); - assertEquals(res.status, 200); - const data: Ajustement[] = await res.json(); - assertEquals(data.length, 2); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /ajustements?numEtud filters by student", async () => { - const filtered = ajustements.filter((a) => a.numEtud === 21212006); - mockFetch({ "/ajustements": filtered }); - try { - const res = await fetch( - "http://localhost/api/ajustements?numEtud=21212006", - ); - const data: Ajustement[] = await res.json(); - assertEquals(data.length, 1); - assertEquals(data[0].numEtud, 21212006); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /ajustements?numEtud=NaN returns 400", async () => { - mockFetch({ "/ajustements": { status: 400 } }); - try { - const res = await fetch("http://localhost/api/ajustements?numEtud=abc"); - assertEquals(res.status, 400); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: POST /ajustements creates ajustement (201) as employee", async () => { - const newAjust: Ajustement = { numEtud: 21212007, idUE: 2, valeur: 14.0 }; - mockFetch({ - "/ajustements": { method: "POST", status: 201, body: newAjust }, - }); - try { - const res = await fetch("http://localhost/api/ajustements", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify(newAjust), - }); - assertEquals(res.status, 201); - const data: Ajustement = await res.json(); - assertEquals(data.numEtud, 21212007); - assertEquals(data.valeur, 14.0); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: POST /ajustements 403 for non-employee", async () => { - mockFetch({ "/ajustements": { method: "POST", status: 403 } }); - try { - const res = await fetch("http://localhost/api/ajustements", { - method: "POST", - }); - assertEquals(res.status, 403); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: POST /ajustements 400 on missing fields", async () => { - mockFetch({ "/ajustements": { method: "POST", status: 400 } }); - try { - const res = await fetch("http://localhost/api/ajustements", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ numEtud: 21212006 }), - }); - assertEquals(res.status, 400); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /ajustements/:numEtud/:idUE returns ajustement (employee)", async () => { - mockFetch({ "/ajustements/21212006/1": ajustements[0] }); - try { - const res = await fetch("http://localhost/api/ajustements/21212006/1"); - assertEquals(res.status, 200); - const data: Ajustement = await res.json(); - assertEquals(data.valeur, 13.25); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /ajustements/:numEtud/:idUE 403 for non-employee", async () => { - mockFetch({ "/ajustements/21212006/1": { status: 403 } }); - try { - const res = await fetch("http://localhost/api/ajustements/21212006/1"); - assertEquals(res.status, 403); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /ajustements/:numEtud/:idUE 404 when not found", async () => { - mockFetch({ - "/ajustements/99999/9": { - status: 404, - body: { error: "Ajustement introuvable" }, - }, - }); - try { - const res = await fetch("http://localhost/api/ajustements/99999/9"); - assertEquals(res.status, 404); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: PUT /ajustements/:numEtud/:idUE updates valeur", async () => { - const updated: Ajustement = { ...ajustements[0], valeur: 18.0 }; - mockFetch({ - "/ajustements/21212006/1": { method: "PUT", status: 200, body: updated }, - }); - try { - const res = await fetch("http://localhost/api/ajustements/21212006/1", { - method: "PUT", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ valeur: 18.0 }), - }); - assertEquals(res.status, 200); - const data: Ajustement = await res.json(); - assertEquals(data.valeur, 18.0); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: DELETE /ajustements/:numEtud/:idUE returns 204", async () => { - mockFetch({ "/ajustements/21212006/1": { method: "DELETE", status: 204 } }); - try { - const res = await fetch("http://localhost/api/ajustements/21212006/1", { - method: "DELETE", - }); - assertEquals(res.status, 204); - } finally { - restoreFetch(); - } -}); - -// --- Mock DB --- - -Deno.test("mock DB: find ajustement by composite key", () => { - const db = createMockDb({ tables: { ajustements: [...ajustements] } }); - const a = db.findOne( - "ajustements", - (a) => a.numEtud === 21212006 && a.idUE === 1, - ); - assertExists(a); - assertEquals(a.valeur, 13.25); -}); - -Deno.test("mock DB: filter ajustements by numEtud", () => { - const db = createMockDb({ tables: { ajustements: [...ajustements] } }); - const rows = db.findMany( - "ajustements", - (a) => a.numEtud === 21212006, - ); - assertEquals(rows.length, 1); -}); - -Deno.test("mock DB: insert ajustement", () => { - const db = createMockDb({ tables: { ajustements: [...ajustements] } }); - db.insert("ajustements", { - numEtud: 21212007, - idUE: 2, - valeur: 14.0, - }); - assertEquals(db.getTable("ajustements").length, 3); -}); - -Deno.test("mock DB: update ajustement valeur", () => { - const db = createMockDb({ tables: { ajustements: [...ajustements] } }); - db.updateWhere( - "ajustements", - (a) => a.numEtud === 21212006 && a.idUE === 1, - { valeur: 20.0 }, - ); - assertEquals( - db.findOne( - "ajustements", - (a) => a.numEtud === 21212006 && a.idUE === 1, - )?.valeur, - 20.0, - ); -}); - -Deno.test("mock DB: delete ajustement", () => { - const db = createMockDb({ tables: { ajustements: [...ajustements] } }); - db.deleteWhere( - "ajustements", - (a) => a.numEtud === 21212006 && a.idUE === 1, - ); - assertEquals(db.getTable("ajustements").length, 1); -}); diff --git a/tests/unit/enseignements_test.ts b/tests/unit/enseignements_test.ts deleted file mode 100644 index d1e3b04..0000000 --- a/tests/unit/enseignements_test.ts +++ /dev/null @@ -1,239 +0,0 @@ -// Unit tests for /enseignements endpoints — fixtures, mock API, mock DB - -import { assertEquals, assertExists } from "@std/assert"; -import { mockFetch, restoreFetch } from "../helpers/api_mock.ts"; -import { createMockDb } from "../helpers/db_mock.ts"; -import { enseignements } from "../helpers/fixtures.ts"; - -interface Enseignement { - idProf: string; - idModule: string; - idPromo: string; -} - -// --- Fixtures --- - -Deno.test("enseignements: fixtures have correct shape", () => { - assertEquals(enseignements.length, 3); - assertEquals(typeof enseignements[0].idModule, "string"); - assertEquals(typeof enseignements[0].idPromo, "string"); -}); - -// --- Mock API --- - -Deno.test("mock API: POST /enseignements creates enseignement (201) as employee", async () => { - const newEns: Enseignement = { - idProf: "prof.dupont", - idModule: "JIN702C", - idPromo: "4AFISE25/26", - }; - mockFetch({ - "/enseignements": { method: "POST", status: 201, body: newEns }, - }); - try { - const res = await fetch("http://localhost/api/enseignements", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify(newEns), - }); - assertEquals(res.status, 201); - const data: Enseignement = await res.json(); - assertEquals(data.idModule, "JIN702C"); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: POST /enseignements 403 for non-employee", async () => { - mockFetch({ "/enseignements": { method: "POST", status: 403 } }); - try { - const res = await fetch("http://localhost/api/enseignements", { - method: "POST", - }); - assertEquals(res.status, 403); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: POST /enseignements 400 on missing fields", async () => { - mockFetch({ "/enseignements": { method: "POST", status: 400 } }); - try { - const res = await fetch("http://localhost/api/enseignements", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ idProf: "prof.dupont" }), - }); - assertEquals(res.status, 400); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: POST /enseignements 409 on duplicate", async () => { - mockFetch({ - "/enseignements": { - method: "POST", - status: 409, - body: { error: "Cet enseignement existe déjà." }, - }, - }); - try { - const res = await fetch("http://localhost/api/enseignements", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - idProf: "prof.dupont", - idModule: "JIN702C", - idPromo: "4AFISE25/26", - }), - }); - assertEquals(res.status, 409); - const data = await res.json(); - assertExists(data.error); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /enseignements/:idProf/:idModule/:idPromo returns enseignement (employee)", async () => { - const ens: Enseignement = { - idProf: "prof.dupont", - idModule: "JIN702C", - idPromo: "4AFISE25/26", - }; - mockFetch({ "/enseignements/prof.dupont/JIN702C/4AFISE25": ens }); - try { - const res = await fetch( - "http://localhost/api/enseignements/prof.dupont/JIN702C/4AFISE25%2F26", - ); - assertEquals(res.status, 200); - const data: Enseignement = await res.json(); - assertEquals(data.idProf, "prof.dupont"); - assertEquals(data.idModule, "JIN702C"); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /enseignements/:idProf/:idModule/:idPromo 403 for non-employee", async () => { - mockFetch({ "/enseignements/prof.dupont/JIN702C/4AFISE25": { status: 403 } }); - try { - const res = await fetch( - "http://localhost/api/enseignements/prof.dupont/JIN702C/4AFISE25%2F26", - ); - assertEquals(res.status, 403); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /enseignements/:idProf/:idModule/:idPromo 404 when not found", async () => { - mockFetch({ - "/enseignements/ghost/GHOST/GHOST": { - status: 404, - body: { error: "Ressource introuvable" }, - }, - }); - try { - const res = await fetch( - "http://localhost/api/enseignements/ghost/GHOST/GHOST", - ); - assertEquals(res.status, 404); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: DELETE /enseignements/:idProf/:idModule/:idPromo returns 204 (employee)", async () => { - mockFetch({ - "/enseignements/prof.dupont/JIN702C/4AFISE25": { - method: "DELETE", - status: 204, - }, - }); - try { - const res = await fetch( - "http://localhost/api/enseignements/prof.dupont/JIN702C/4AFISE25%2F26", - { - method: "DELETE", - }, - ); - assertEquals(res.status, 204); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: DELETE /enseignements/:idProf/:idModule/:idPromo 403 for non-employee", async () => { - mockFetch({ - "/enseignements/prof.dupont/JIN702C/4AFISE25": { - method: "DELETE", - status: 403, - }, - }); - try { - const res = await fetch( - "http://localhost/api/enseignements/prof.dupont/JIN702C/4AFISE25%2F26", - { - method: "DELETE", - }, - ); - assertEquals(res.status, 403); - } finally { - restoreFetch(); - } -}); - -// --- Mock DB --- - -Deno.test("mock DB: find enseignement by composite key", () => { - const data = [ - { idProf: "prof.dupont", idModule: "JIN702C", idPromo: "4AFISE25/26" }, - { idProf: "prof.moreau", idModule: "JIN703C", idPromo: "4AFISE25/26" }, - ]; - const db = createMockDb({ tables: { enseignements: data } }); - const e = db.findOne( - "enseignements", - (e) => e.idProf === "prof.dupont" && e.idModule === "JIN702C", - ); - assertExists(e); - assertEquals(e.idPromo, "4AFISE25/26"); -}); - -Deno.test("mock DB: insert enseignement", () => { - const db = createMockDb({ tables: { enseignements: [] } }); - db.insert("enseignements", { - idProf: "prof.dupont", - idModule: "JIN702C", - idPromo: "4AFISE25/26", - }); - assertEquals(db.getTable("enseignements").length, 1); -}); - -Deno.test("mock DB: delete enseignement", () => { - const data = [ - { idProf: "prof.dupont", idModule: "JIN702C", idPromo: "4AFISE25/26" }, - { idProf: "prof.moreau", idModule: "JIN703C", idPromo: "4AFISE25/26" }, - ]; - const db = createMockDb({ tables: { enseignements: data } }); - db.deleteWhere( - "enseignements", - (e) => e.idProf === "prof.dupont", - ); - assertEquals(db.getTable("enseignements").length, 1); -}); - -Deno.test("mock DB: filter enseignements by idModule", () => { - const data = [ - { idProf: "prof.dupont", idModule: "JIN702C", idPromo: "4AFISE25/26" }, - { idProf: "prof.dupont", idModule: "JIN702C", idPromo: "3AFISE25/26" }, - { idProf: "prof.moreau", idModule: "JIN703C", idPromo: "4AFISE25/26" }, - ]; - const db = createMockDb({ tables: { enseignements: data } }); - const rows = db.findMany( - "enseignements", - (e) => e.idModule === "JIN702C", - ); - assertEquals(rows.length, 2); -}); diff --git a/tests/unit/grades_test.ts b/tests/unit/grades_test.ts new file mode 100644 index 0000000..2ee6c5b --- /dev/null +++ b/tests/unit/grades_test.ts @@ -0,0 +1,70 @@ +import { assertEquals } from "@std/assert"; +import { + calculateWeightedAverage, + getEffectiveNote, + applyAjustement, + Note, + UEModule, + Ajustement +} from "../../logic/grades.ts"; + +Deno.test("grades logic: getEffectiveNote uses session 2 if present", () => { + const note: Note = { note: 12, noteSession2: 15 }; + assertEquals(getEffectiveNote(note), 15); +}); + +Deno.test("grades logic: getEffectiveNote uses session 1 if session 2 is null", () => { + const note: Note = { note: 12, noteSession2: null }; + assertEquals(getEffectiveNote(note), 12); +}); + +Deno.test("grades logic: calculateWeightedAverage computes correctly", () => { + const ueModules: UEModule[] = [ + { idModule: "M1", coeff: 2 }, + { idModule: "M2", coeff: 3 }, + ]; + const notesMap: Record = { + "M1": { note: 10, noteSession2: null }, + "M2": { note: 15, noteSession2: null }, + }; + // (10*2 + 15*3) / 5 = (20 + 45) / 5 = 65 / 5 = 13 + assertEquals(calculateWeightedAverage(ueModules, notesMap), 13); +}); + +Deno.test("grades logic: calculateWeightedAverage handles missing notes", () => { + const ueModules: UEModule[] = [ + { idModule: "M1", coeff: 2 }, + { idModule: "M2", coeff: 3 }, + ]; + const notesMap: Record = { + "M1": { note: 10, noteSession2: null }, + // M2 manquante + }; + // (10*2) / 2 = 10 + assertEquals(calculateWeightedAverage(ueModules, notesMap), 10); +}); + +Deno.test("grades logic: calculateWeightedAverage returns null if no notes", () => { + const ueModules: UEModule[] = [ + { idModule: "M1", coeff: 2 }, + ]; + const notesMap: Record = {}; + assertEquals(calculateWeightedAverage(ueModules, notesMap), null); +}); + +Deno.test("grades logic: applyAjustement replaces calculated average", () => { + const calculatedAvg = 12; + const ajustement: Ajustement = { valeur: 14, malus: 0 }; + assertEquals(applyAjustement(calculatedAvg, ajustement), 14); +}); + +Deno.test("grades logic: applyAjustement subtracts malus", () => { + const calculatedAvg = 12; + const ajustement: Ajustement = { valeur: 14, malus: 2 }; + assertEquals(applyAjustement(calculatedAvg, ajustement), 12); +}); + +Deno.test("grades logic: applyAjustement returns calculated average if no ajustement", () => { + const calculatedAvg = 12; + assertEquals(applyAjustement(calculatedAvg, null), 12); +}); diff --git a/tests/unit/modules_test.ts b/tests/unit/modules_test.ts deleted file mode 100644 index e94cdc4..0000000 --- a/tests/unit/modules_test.ts +++ /dev/null @@ -1,171 +0,0 @@ -// #113 - Unit tests for /modules endpoints - -import { assertEquals, assertExists } from "@std/assert"; -import { mockFetch, restoreFetch } from "../helpers/api_mock.ts"; -import { createMockDb } from "../helpers/db_mock.ts"; -import { type Module, modules } from "../helpers/fixtures.ts"; - -// --- Fixtures --- - -Deno.test("modules: fixtures have correct shape", () => { - assertEquals(modules.length, 3); - assertEquals(typeof modules[0].id, "string"); - assertEquals(typeof modules[0].nom, "string"); -}); - -// --- Mock API --- - -Deno.test("mock API: GET /modules returns list", async () => { - mockFetch({ "/modules": modules }); - try { - const res = await fetch("http://localhost/api/modules"); - assertEquals(res.status, 200); - const data: Module[] = await res.json(); - assertEquals(data.length, 3); - assertExists(data.find((m) => m.id === "JIN702C")); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /modules/:id returns one module", async () => { - mockFetch({ "/modules/JIN702C": modules[0] }); - try { - const res = await fetch("http://localhost/api/modules/JIN702C"); - assertEquals(res.status, 200); - const data: Module = await res.json(); - assertEquals(data.id, "JIN702C"); - assertEquals(data.nom, "Optimisation"); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /modules/:id 404 when not found", async () => { - mockFetch({ - "/modules/UNKNOWN": { - status: 404, - body: { error: "Ressource introuvable" }, - }, - }); - try { - const res = await fetch("http://localhost/api/modules/UNKNOWN"); - assertEquals(res.status, 404); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: POST /modules creates module (201)", async () => { - const newModule: Module = { id: "NEW101", nom: "Nouveau Module" }; - mockFetch({ "/modules": { method: "POST", status: 201, body: newModule } }); - try { - const res = await fetch("http://localhost/api/modules", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify(newModule), - }); - assertEquals(res.status, 201); - const data: Module = await res.json(); - assertEquals(data.id, "NEW101"); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: POST /modules 409 on duplicate id", async () => { - mockFetch({ - "/modules": { - method: "POST", - status: 409, - body: { error: "Un module avec cet identifiant existe déjà" }, - }, - }); - try { - const res = await fetch("http://localhost/api/modules", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify(modules[0]), - }); - assertEquals(res.status, 409); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: POST /modules 400 on missing fields", async () => { - mockFetch({ "/modules": { method: "POST", status: 400 } }); - try { - const res = await fetch("http://localhost/api/modules", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ id: "X" }), - }); - assertEquals(res.status, 400); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: PUT /modules/:id updates nom", async () => { - const updated: Module = { id: "JIN702C", nom: "Optimisation avancée" }; - mockFetch({ - "/modules/JIN702C": { method: "PUT", status: 200, body: updated }, - }); - try { - const res = await fetch("http://localhost/api/modules/JIN702C", { - method: "PUT", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ nom: "Optimisation avancée" }), - }); - assertEquals(res.status, 200); - const data: Module = await res.json(); - assertEquals(data.nom, "Optimisation avancée"); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: DELETE /modules/:id returns 204", async () => { - mockFetch({ "/modules/JIN702C": { method: "DELETE", status: 204 } }); - try { - const res = await fetch("http://localhost/api/modules/JIN702C", { - method: "DELETE", - }); - assertEquals(res.status, 204); - } finally { - restoreFetch(); - } -}); - -// --- Mock DB --- - -Deno.test("mock DB: find module by id", () => { - const db = createMockDb({ tables: { modules: [...modules] } }); - const m = db.findOne("modules", (m) => m.id === "JIN702C"); - assertExists(m); - assertEquals(m.nom, "Optimisation"); -}); - -Deno.test("mock DB: insert module", () => { - const db = createMockDb({ tables: { modules: [...modules] } }); - db.insert("modules", { id: "NEW101", nom: "Nouveau" }); - assertEquals(db.getTable("modules").length, 4); -}); - -Deno.test("mock DB: update module nom", () => { - const db = createMockDb({ tables: { modules: [...modules] } }); - db.updateWhere("modules", (m) => m.id === "JIN702C", { - nom: "Updated", - }); - assertEquals( - db.findOne("modules", (m) => m.id === "JIN702C")?.nom, - "Updated", - ); -}); - -Deno.test("mock DB: delete module", () => { - const db = createMockDb({ tables: { modules: [...modules] } }); - db.deleteWhere("modules", (m) => m.id === "JIN702C"); - assertEquals(db.getTable("modules").length, 2); -}); diff --git a/tests/unit/notes_test.ts b/tests/unit/notes_test.ts deleted file mode 100644 index 9e13794..0000000 --- a/tests/unit/notes_test.ts +++ /dev/null @@ -1,224 +0,0 @@ -// Unit tests for /notes endpoints — fixtures, mock API, mock DB - -import { assertEquals, assertExists } from "@std/assert"; -import { mockFetch, restoreFetch } from "../helpers/api_mock.ts"; -import { createMockDb } from "../helpers/db_mock.ts"; -import { type Note, notes } from "../helpers/fixtures.ts"; - -// --- Fixtures --- - -Deno.test("notes: fixtures have correct shape", () => { - assertEquals(notes.length, 4); - assertEquals(typeof notes[0].note, "number"); - assertEquals(typeof notes[0].numEtud, "number"); - assertEquals(typeof notes[0].idModule, "string"); -}); - -Deno.test("notes: fixtures use decimal values", () => { - assertEquals(notes[0].note, 15.5); -}); - -// --- Mock API --- - -Deno.test("mock API: GET /notes returns list", async () => { - mockFetch({ "/notes": notes }); - try { - const res = await fetch("http://localhost/api/notes"); - assertEquals(res.status, 200); - const data: Note[] = await res.json(); - assertEquals(data.length, 4); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /notes?numEtud filters by student", async () => { - const filtered = notes.filter((n) => n.numEtud === 21212006); - mockFetch({ "/notes": filtered }); - try { - const res = await fetch("http://localhost/api/notes?numEtud=21212006"); - const data: Note[] = await res.json(); - assertEquals(data.length, 2); - assertEquals(data.every((n) => n.numEtud === 21212006), true); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /notes?idModule filters by module", async () => { - const filtered = notes.filter((n) => n.idModule === "JIN702C"); - mockFetch({ "/notes": filtered }); - try { - const res = await fetch("http://localhost/api/notes?idModule=JIN702C"); - const data: Note[] = await res.json(); - assertEquals(data.length, 2); - assertEquals(data.every((n) => n.idModule === "JIN702C"), true); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /notes?numEtud=NaN returns 400", async () => { - mockFetch({ "/notes": { status: 400 } }); - try { - const res = await fetch("http://localhost/api/notes?numEtud=abc"); - assertEquals(res.status, 400); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: POST /notes creates note (201)", async () => { - const newNote: Note = { note: 14.0, numEtud: 21212006, idModule: "JIN704C" }; - mockFetch({ "/notes": { method: "POST", status: 201, body: newNote } }); - try { - const res = await fetch("http://localhost/api/notes", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify(newNote), - }); - assertEquals(res.status, 201); - const data: Note = await res.json(); - assertEquals(data.note, 14.0); - assertEquals(data.numEtud, 21212006); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: POST /notes 400 on missing fields", async () => { - mockFetch({ "/notes": { method: "POST", status: 400 } }); - try { - const res = await fetch("http://localhost/api/notes", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ numEtud: 21212006 }), - }); - assertEquals(res.status, 400); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /notes/:numEtud/:idModule returns note", async () => { - mockFetch({ "/notes/21212006/JIN702C": notes[0] }); - try { - const res = await fetch("http://localhost/api/notes/21212006/JIN702C"); - assertEquals(res.status, 200); - const data: Note = await res.json(); - assertEquals(data.note, 15.5); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /notes/:numEtud/:idModule 404 when not found", async () => { - mockFetch({ - "/notes/99999/GHOST": { - status: 404, - body: { error: "Ressource introuvable" }, - }, - }); - try { - const res = await fetch("http://localhost/api/notes/99999/GHOST"); - assertEquals(res.status, 404); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: PUT /notes/:numEtud/:idModule updates note", async () => { - const updated: Note = { ...notes[0], note: 17.0 }; - mockFetch({ - "/notes/21212006/JIN702C": { method: "PUT", status: 200, body: updated }, - }); - try { - const res = await fetch("http://localhost/api/notes/21212006/JIN702C", { - method: "PUT", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ note: 17.0 }), - }); - assertEquals(res.status, 200); - const data: Note = await res.json(); - assertEquals(data.note, 17.0); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: DELETE /notes/:numEtud/:idModule returns 204", async () => { - mockFetch({ "/notes/21212006/JIN702C": { method: "DELETE", status: 204 } }); - try { - const res = await fetch("http://localhost/api/notes/21212006/JIN702C", { - method: "DELETE", - }); - assertEquals(res.status, 204); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: DELETE /notes/:numEtud/:idModule 404 when not found", async () => { - mockFetch({ "/notes/99999/GHOST": { method: "DELETE", status: 404 } }); - try { - const res = await fetch("http://localhost/api/notes/99999/GHOST", { - method: "DELETE", - }); - assertEquals(res.status, 404); - } finally { - restoreFetch(); - } -}); - -// --- Mock DB --- - -Deno.test("mock DB: find note by composite key", () => { - const db = createMockDb({ tables: { notes: [...notes] } }); - const n = db.findOne( - "notes", - (n) => n.numEtud === 21212006 && n.idModule === "JIN702C", - ); - assertExists(n); - assertEquals(n.note, 15.5); -}); - -Deno.test("mock DB: filter notes by numEtud", () => { - const db = createMockDb({ tables: { notes: [...notes] } }); - const rows = db.findMany("notes", (n) => n.numEtud === 21212006); - assertEquals(rows.length, 2); -}); - -Deno.test("mock DB: insert note", () => { - const db = createMockDb({ tables: { notes: [...notes] } }); - db.insert("notes", { - note: 10.0, - numEtud: 21212006, - idModule: "JIN704C", - }); - assertEquals(db.getTable("notes").length, 5); -}); - -Deno.test("mock DB: update note value", () => { - const db = createMockDb({ tables: { notes: [...notes] } }); - db.updateWhere( - "notes", - (n) => n.numEtud === 21212006 && n.idModule === "JIN702C", - { note: 20.0 }, - ); - assertEquals( - db.findOne( - "notes", - (n) => n.numEtud === 21212006 && n.idModule === "JIN702C", - )?.note, - 20.0, - ); -}); - -Deno.test("mock DB: delete note", () => { - const db = createMockDb({ tables: { notes: [...notes] } }); - db.deleteWhere( - "notes", - (n) => n.numEtud === 21212006 && n.idModule === "JIN702C", - ); - assertEquals(db.getTable("notes").length, 3); -}); diff --git a/tests/unit/permissions_test.ts b/tests/unit/permissions_test.ts deleted file mode 100644 index c3a8052..0000000 --- a/tests/unit/permissions_test.ts +++ /dev/null @@ -1,65 +0,0 @@ -// #115 - Unit tests for GET /permissions - -import { assertEquals, assertExists } from "@std/assert"; -import { mockFetch, restoreFetch } from "../helpers/api_mock.ts"; - -interface Permission { - id: string; - nom: string; -} - -const EXPECTED_PERMISSIONS: Permission[] = [ - { id: "student_read", nom: "Consulter les élèves" }, - { id: "student_write", nom: "Gérer les élèves" }, - { id: "note_read", nom: "Consulter les notes" }, - { id: "note_write", nom: "Gérer les notes" }, - { id: "module_read", nom: "Consulter les modules" }, - { id: "module_write", nom: "Gérer les modules" }, - { id: "user_read", nom: "Consulter les utilisateurs" }, - { id: "user_write", nom: "Gérer les utilisateurs" }, - { id: "role_write", nom: "Gérer les rôles" }, -]; - -Deno.test("permissions: known permission ids", () => { - const ids = EXPECTED_PERMISSIONS.map((p) => p.id); - assertEquals(ids.includes("student_read"), true); - assertEquals(ids.includes("student_write"), true); - assertEquals(ids.includes("note_read"), true); - assertEquals(ids.includes("role_write"), true); - assertEquals(ids.length, 9); -}); - -Deno.test("permissions: all permissions have string id and nom", () => { - for (const p of EXPECTED_PERMISSIONS) { - assertEquals(typeof p.id, "string"); - assertEquals(typeof p.nom, "string"); - } -}); - -Deno.test("mock API: GET /permissions returns all permissions", async () => { - mockFetch({ "/permissions": EXPECTED_PERMISSIONS }); - try { - const res = await fetch("http://localhost/api/permissions"); - assertEquals(res.status, 200); - const data: Permission[] = await res.json(); - assertEquals(data.length, 9); - assertExists(data.find((p) => p.id === "student_read")); - assertExists(data.find((p) => p.id === "role_write")); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /permissions - each permission has id and nom", async () => { - mockFetch({ "/permissions": EXPECTED_PERMISSIONS }); - try { - const res = await fetch("http://localhost/api/permissions"); - const data: Permission[] = await res.json(); - for (const p of data) { - assertExists(p.id); - assertExists(p.nom); - } - } finally { - restoreFetch(); - } -}); diff --git a/tests/unit/promotions_test.ts b/tests/unit/promotions_test.ts deleted file mode 100644 index b725bc5..0000000 --- a/tests/unit/promotions_test.ts +++ /dev/null @@ -1,160 +0,0 @@ -// #110 - Unit tests for /promotions endpoints - -import { assertEquals, assertExists } from "@std/assert"; -import { mockFetch, restoreFetch } from "../helpers/api_mock.ts"; -import { createMockDb } from "../helpers/db_mock.ts"; -import { type Promotion, promotions } from "../helpers/fixtures.ts"; - -// --- Fixtures --- - -Deno.test("promotions: fixtures have correct shape", () => { - assertEquals(promotions.length, 3); - assertEquals(typeof promotions[0].idPromo, "string"); - assertEquals(typeof promotions[0].annee, "string"); -}); - -// --- Mock API --- - -Deno.test("mock API: GET /promotions returns list", async () => { - mockFetch({ "/promotions": promotions }); - try { - const res = await fetch("http://localhost/api/promotions"); - assertEquals(res.status, 200); - const data: Promotion[] = await res.json(); - assertEquals(data.length, 3); - assertExists(data.find((p) => p.idPromo === "4AFISE25/26")); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /promotions/:id returns one", async () => { - mockFetch({ "/promotions/4AFISE25%2F26": promotions[0] }); - try { - const res = await fetch("http://localhost/api/promotions/4AFISE25%2F26"); - assertEquals(res.status, 200); - const data: Promotion = await res.json(); - assertEquals(data.idPromo, "4AFISE25/26"); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /promotions/:id 404 when not found", async () => { - mockFetch({ - "/promotions/UNKNOWN": { - status: 404, - body: { error: "Ressource introuvable" }, - }, - }); - try { - const res = await fetch("http://localhost/api/promotions/UNKNOWN"); - assertEquals(res.status, 404); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: POST /promotions creates promotion (201)", async () => { - const newPromo: Promotion = { idPromo: "NEW2025", annee: "2025" }; - mockFetch({ "/promotions": { method: "POST", status: 201, body: newPromo } }); - try { - const res = await fetch("http://localhost/api/promotions", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ idPromo: "NEW2025", annee: "2025" }), - }); - assertEquals(res.status, 201); - const data: Promotion = await res.json(); - assertEquals(data.idPromo, "NEW2025"); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: POST /promotions 400 on missing fields", async () => { - mockFetch({ "/promotions": { method: "POST", status: 400 } }); - try { - const res = await fetch("http://localhost/api/promotions", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ idPromo: "NEW2025" }), - }); - assertEquals(res.status, 400); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: PUT /promotions/:id updates promotion", async () => { - const updated = { idPromo: "4AFISE25/26", annee: "2026" }; - mockFetch({ - "/promotions/4AFISE25%2F26": { method: "PUT", status: 200, body: updated }, - }); - try { - const res = await fetch("http://localhost/api/promotions/4AFISE25%2F26", { - method: "PUT", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ annee: "2026" }), - }); - assertEquals(res.status, 200); - const data: Promotion = await res.json(); - assertEquals(data.annee, "2026"); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: DELETE /promotions/:id returns 204", async () => { - mockFetch({ "/promotions/4AFISE25%2F26": { method: "DELETE", status: 204 } }); - try { - const res = await fetch("http://localhost/api/promotions/4AFISE25%2F26", { - method: "DELETE", - }); - assertEquals(res.status, 204); - } finally { - restoreFetch(); - } -}); - -// --- Mock DB --- - -Deno.test("mock DB: find promotion by idPromo", () => { - const db = createMockDb({ tables: { promotions: [...promotions] } }); - const p = db.findOne( - "promotions", - (r) => r.idPromo === "4AFISE25/26", - ); - assertExists(p); - assertEquals(p.annee, "2025"); -}); - -Deno.test("mock DB: insert promotion", () => { - const db = createMockDb({ tables: { promotions: [...promotions] } }); - db.insert("promotions", { idPromo: "NEW2025", annee: "2025" }); - assertEquals(db.getTable("promotions").length, 4); -}); - -Deno.test("mock DB: update promotion annee", () => { - const db = createMockDb({ tables: { promotions: [...promotions] } }); - db.updateWhere( - "promotions", - (p) => p.idPromo === "4AFISE25/26", - { annee: "2026" }, - ); - assertEquals( - db.findOne("promotions", (p) => p.idPromo === "4AFISE25/26") - ?.annee, - "2026", - ); -}); - -Deno.test("mock DB: delete promotion", () => { - const db = createMockDb({ tables: { promotions: [...promotions] } }); - const count = db.deleteWhere( - "promotions", - (p) => p.idPromo === "4AFISE25/26", - ); - assertEquals(count, 1); - assertEquals(db.getTable("promotions").length, 2); -}); diff --git a/tests/unit/roles_test.ts b/tests/unit/roles_test.ts deleted file mode 100644 index eeae55e..0000000 --- a/tests/unit/roles_test.ts +++ /dev/null @@ -1,159 +0,0 @@ -// #112 - Unit tests for /roles endpoints - -import { assertEquals, assertExists } from "@std/assert"; -import { mockFetch, restoreFetch } from "../helpers/api_mock.ts"; -import { createMockDb } from "../helpers/db_mock.ts"; - -interface Role { - id: number; - nom: string; - permissions: string[]; -} - -const roles: Role[] = [ - { id: 1, nom: "admin", permissions: ["student_read", "student_write"] }, - { id: 2, nom: "employee", permissions: ["student_read"] }, -]; - -// --- Fixtures --- - -Deno.test("roles: fixtures have correct shape", () => { - assertEquals(roles.length, 2); - assertEquals(typeof roles[0].id, "number"); - assertEquals(typeof roles[0].nom, "string"); - assertEquals(Array.isArray(roles[0].permissions), true); -}); - -Deno.test("roles: permissions are strings", () => { - assertEquals(roles[0].permissions.every((p) => typeof p === "string"), true); -}); - -// --- Mock API --- - -Deno.test("mock API: GET /roles returns list with permissions", async () => { - mockFetch({ "/roles": roles }); - try { - const res = await fetch("http://localhost/api/roles"); - assertEquals(res.status, 200); - const data: Role[] = await res.json(); - assertEquals(data.length, 2); - assertExists(data.find((r) => r.nom === "admin")); - assertEquals(data[0].permissions.length, 2); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /roles/:id returns role", async () => { - mockFetch({ "/roles/1": roles[0] }); - try { - const res = await fetch("http://localhost/api/roles/1"); - assertEquals(res.status, 200); - const data: Role = await res.json(); - assertEquals(data.nom, "admin"); - assertEquals(data.permissions.length, 2); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /roles/:id 404 when not found", async () => { - mockFetch({ - "/roles/99": { status: 404, body: { error: "Ressource introuvable" } }, - }); - try { - const res = await fetch("http://localhost/api/roles/99"); - assertEquals(res.status, 404); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: POST /roles creates role (201)", async () => { - const newRole: Role = { id: 3, nom: "viewer", permissions: [] }; - mockFetch({ "/roles": { method: "POST", status: 201, body: newRole } }); - try { - const res = await fetch("http://localhost/api/roles", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ nom: "viewer" }), - }); - assertEquals(res.status, 201); - const data: Role = await res.json(); - assertEquals(data.nom, "viewer"); - assertEquals(data.permissions.length, 0); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: POST /roles 400 on missing nom", async () => { - mockFetch({ "/roles": { method: "POST", status: 400 } }); - try { - const res = await fetch("http://localhost/api/roles", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({}), - }); - assertEquals(res.status, 400); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: PUT /roles/:id updates role and permissions", async () => { - const updated: Role = { id: 2, nom: "teacher", permissions: ["note_read"] }; - mockFetch({ "/roles/2": { method: "PUT", status: 200, body: updated } }); - try { - const res = await fetch("http://localhost/api/roles/2", { - method: "PUT", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ nom: "teacher", permissions: ["note_read"] }), - }); - assertEquals(res.status, 200); - const data: Role = await res.json(); - assertEquals(data.nom, "teacher"); - assertEquals(data.permissions, ["note_read"]); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: DELETE /roles/:id returns 204", async () => { - mockFetch({ "/roles/2": { method: "DELETE", status: 204 } }); - try { - const res = await fetch("http://localhost/api/roles/2", { - method: "DELETE", - }); - assertEquals(res.status, 204); - } finally { - restoreFetch(); - } -}); - -// --- Mock DB --- - -Deno.test("mock DB: find role by id", () => { - const db = createMockDb({ tables: { roles: [...roles] } }); - const r = db.findOne("roles", (r) => r.id === 1); - assertExists(r); - assertEquals(r.nom, "admin"); -}); - -Deno.test("mock DB: insert role", () => { - const db = createMockDb({ tables: { roles: [...roles] } }); - db.insert("roles", { id: 3, nom: "viewer", permissions: [] }); - assertEquals(db.getTable("roles").length, 3); -}); - -Deno.test("mock DB: update role nom", () => { - const db = createMockDb({ tables: { roles: [...roles] } }); - db.updateWhere("roles", (r) => r.id === 2, { nom: "teacher" }); - assertEquals(db.findOne("roles", (r) => r.id === 2)?.nom, "teacher"); -}); - -Deno.test("mock DB: delete role", () => { - const db = createMockDb({ tables: { roles: [...roles] } }); - db.deleteWhere("roles", (r) => r.id === 1); - assertEquals(db.getTable("roles").length, 1); -}); diff --git a/tests/unit/students_test.ts b/tests/unit/students_test.ts deleted file mode 100644 index ded2ff2..0000000 --- a/tests/unit/students_test.ts +++ /dev/null @@ -1,216 +0,0 @@ -// #109 - Unit tests for /students endpoints -// Tests purs : fixtures, mock API, mock DB — aucun appel réseau réel - -import { assertEquals, assertExists } from "@std/assert"; -import { getFetchCalls, mockFetch, restoreFetch } from "../helpers/api_mock.ts"; -import { createMockDb } from "../helpers/db_mock.ts"; -import { type Student, students } from "../helpers/fixtures.ts"; - -// --- Fixtures --- - -Deno.test("students: fixtures have correct shape", () => { - assertEquals(students.length, 3); - assertEquals(typeof students[0].numEtud, "number"); - assertEquals(typeof students[0].nom, "string"); - assertEquals(typeof students[0].prenom, "string"); - assertEquals(typeof students[0].idPromo, "string"); -}); - -Deno.test("students: two students belong to the same promo", () => { - const promo4 = students.filter((s) => s.idPromo === "4AFISE25/26"); - assertEquals(promo4.length, 2); -}); - -// --- Mock API - GET /students --- - -Deno.test("mock API: GET /students returns list", async () => { - mockFetch({ "/students": students }); - try { - const res = await fetch("http://localhost/api/students"); - assertEquals(res.status, 200); - const data: Student[] = await res.json(); - assertEquals(data.length, 3); - assertExists(data.find((s) => s.nom === "Dupont")); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /students?idPromo filters by promo", async () => { - const filtered = students.filter((s) => s.idPromo === "4AFISE25/26"); - mockFetch({ "/students": filtered }); - try { - const res = await fetch( - "http://localhost/api/students?idPromo=4AFISE25/26", - ); - const data: Student[] = await res.json(); - assertEquals(data.length, 2); - assertEquals(data.every((s) => s.idPromo === "4AFISE25/26"), true); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /students/:numEtud returns one student", async () => { - mockFetch({ "/students/21212006": students[0] }); - try { - const res = await fetch("http://localhost/api/students/21212006"); - assertEquals(res.status, 200); - const data: Student = await res.json(); - assertEquals(data.numEtud, 21212006); - assertEquals(data.nom, "Dupont"); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /students/:numEtud 404 when not found", async () => { - mockFetch({ - "/students/99999": { - status: 404, - body: { error: "Ressource introuvable" }, - }, - }); - try { - const res = await fetch("http://localhost/api/students/99999"); - assertEquals(res.status, 404); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: POST /students creates student", async () => { - const newStudent = students[0]; - mockFetch({ "/students": { method: "POST", status: 201, body: newStudent } }); - try { - const res = await fetch("http://localhost/api/students", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - nom: "Dupont", - prenom: "Jean", - idPromo: "4AFISE25/26", - }), - }); - assertEquals(res.status, 201); - const data: Student = await res.json(); - assertEquals(data.nom, "Dupont"); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: PUT /students/:numEtud updates student", async () => { - const updated = { ...students[0], nom: "Dupont-Modifié" }; - mockFetch({ - "/students/21212006": { method: "PUT", status: 200, body: updated }, - }); - try { - const res = await fetch("http://localhost/api/students/21212006", { - method: "PUT", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - nom: "Dupont-Modifié", - prenom: "Jean", - idPromo: "4AFISE25/26", - }), - }); - assertEquals(res.status, 200); - const data: Student = await res.json(); - assertEquals(data.nom, "Dupont-Modifié"); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: DELETE /students/:numEtud returns 204", async () => { - mockFetch({ "/students/21212006": { method: "DELETE", status: 204 } }); - try { - const res = await fetch("http://localhost/api/students/21212006", { - method: "DELETE", - }); - assertEquals(res.status, 204); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: POST /students 400 on missing fields", async () => { - mockFetch({ "/students": { method: "POST", status: 400 } }); - try { - const res = await fetch("http://localhost/api/students", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ nom: "Test" }), - }); - assertEquals(res.status, 400); - } finally { - restoreFetch(); - } -}); - -// --- Mock DB --- - -Deno.test("mock DB: find student by numEtud", () => { - const db = createMockDb({ tables: { students: [...students] } }); - const s = db.findOne("students", (r) => r.numEtud === 21212006); - assertExists(s); - assertEquals(s.nom, "Dupont"); -}); - -Deno.test("mock DB: filter students by idPromo", () => { - const db = createMockDb({ tables: { students: [...students] } }); - const rows = db.findMany( - "students", - (s) => s.idPromo === "4AFISE25/26", - ); - assertEquals(rows.length, 2); -}); - -Deno.test("mock DB: insert student increments count", () => { - const db = createMockDb({ tables: { students: [...students] } }); - db.insert("students", { - numEtud: 21212099, - nom: "Test", - prenom: "Ing", - idPromo: "4AFISE25/26", - }); - assertEquals(db.getTable("students").length, 4); -}); - -Deno.test("mock DB: update student nom", () => { - const db = createMockDb({ tables: { students: [...students] } }); - const count = db.updateWhere( - "students", - (s) => s.numEtud === 21212006, - { nom: "Nouveau" }, - ); - assertEquals(count, 1); - assertEquals( - db.findOne("students", (s) => s.numEtud === 21212006)?.nom, - "Nouveau", - ); -}); - -Deno.test("mock DB: delete student removes exactly one", () => { - const db = createMockDb({ tables: { students: [...students] } }); - const count = db.deleteWhere( - "students", - (s) => s.numEtud === 21212006, - ); - assertEquals(count, 1); - assertEquals(db.getTable("students").length, 2); -}); - -Deno.test("mock API: getFetchCalls tracks student requests", async () => { - mockFetch({ "/students": students }); - try { - await fetch("http://localhost/api/students"); - await fetch("http://localhost/api/students?idPromo=4AFISE25/26"); - const calls = getFetchCalls(); - assertEquals(calls.length, 2); - assertEquals(calls[0].method, "GET"); - } finally { - restoreFetch(); - } -}); diff --git a/tests/unit/ue_modules_test.ts b/tests/unit/ue_modules_test.ts deleted file mode 100644 index 7b2761d..0000000 --- a/tests/unit/ue_modules_test.ts +++ /dev/null @@ -1,222 +0,0 @@ -// Unit tests for /ue-modules endpoints — fixtures, mock API, mock DB - -import { assertEquals, assertExists } from "@std/assert"; -import { mockFetch, restoreFetch } from "../helpers/api_mock.ts"; -import { createMockDb } from "../helpers/db_mock.ts"; -import { type UeModule, ueModules } from "../helpers/fixtures.ts"; - -// --- Fixtures --- - -Deno.test("ue_modules: fixtures have correct shape", () => { - assertEquals(ueModules.length, 3); - assertEquals(typeof ueModules[0].idModule, "string"); - assertEquals(typeof ueModules[0].idUE, "number"); - assertEquals(typeof ueModules[0].idPromo, "string"); - assertEquals(typeof ueModules[0].coeff, "number"); -}); - -// --- Mock API --- - -Deno.test("mock API: GET /ue-modules returns list", async () => { - mockFetch({ "/ue-modules": ueModules }); - try { - const res = await fetch("http://localhost/api/ue-modules"); - assertEquals(res.status, 200); - const data: UeModule[] = await res.json(); - assertEquals(data.length, 3); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /ue-modules?idPromo filters by promo", async () => { - const filtered = ueModules.filter((u) => u.idPromo === "4AFISE25/26"); - mockFetch({ "/ue-modules": filtered }); - try { - const res = await fetch( - "http://localhost/api/ue-modules?idPromo=4AFISE25%2F26", - ); - const data: UeModule[] = await res.json(); - assertEquals(data.length, 2); - assertEquals(data.every((u) => u.idPromo === "4AFISE25/26"), true); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /ue-modules?idUE filters by UE", async () => { - const filtered = ueModules.filter((u) => u.idUE === 1); - mockFetch({ "/ue-modules": filtered }); - try { - const res = await fetch("http://localhost/api/ue-modules?idUE=1"); - const data: UeModule[] = await res.json(); - assertEquals(data.length, 2); - assertEquals(data.every((u) => u.idUE === 1), true); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: POST /ue-modules creates association (201)", async () => { - const newUeModule: UeModule = { - idModule: "JIN705C", - idUE: 2, - idPromo: "3AFISE25/26", - coeff: 3.0, - }; - mockFetch({ - "/ue-modules": { method: "POST", status: 201, body: newUeModule }, - }); - try { - const res = await fetch("http://localhost/api/ue-modules", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify(newUeModule), - }); - assertEquals(res.status, 201); - const data: UeModule = await res.json(); - assertEquals(data.idModule, "JIN705C"); - assertEquals(data.coeff, 3.0); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: POST /ue-modules 400 on missing fields", async () => { - mockFetch({ "/ue-modules": { method: "POST", status: 400 } }); - try { - const res = await fetch("http://localhost/api/ue-modules", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ idModule: "X" }), - }); - assertEquals(res.status, 400); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /ue-modules/:idModule/:idUE/:idPromo returns association (employee)", async () => { - mockFetch({ "/ue-modules/JIN702C/1/4AFISE25": ueModules[0] }); - try { - const res = await fetch( - "http://localhost/api/ue-modules/JIN702C/1/4AFISE25%2F26", - ); - assertEquals(res.status, 200); - const data: UeModule = await res.json(); - assertEquals(data.coeff, 3.0); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /ue-modules/:idModule/:idUE/:idPromo 403 for non-employee", async () => { - mockFetch({ "/ue-modules/JIN702C/1/4AFISE25": { status: 403 } }); - try { - const res = await fetch( - "http://localhost/api/ue-modules/JIN702C/1/4AFISE25%2F26", - ); - assertEquals(res.status, 403); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: PUT /ue-modules/:idModule/:idUE/:idPromo updates coeff", async () => { - const updated: UeModule = { ...ueModules[0], coeff: 5.0 }; - mockFetch({ - "/ue-modules/JIN702C/1/4AFISE25": { - method: "PUT", - status: 200, - body: updated, - }, - }); - try { - const res = await fetch( - "http://localhost/api/ue-modules/JIN702C/1/4AFISE25%2F26", - { - method: "PUT", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ coeff: 5.0 }), - }, - ); - assertEquals(res.status, 200); - const data: UeModule = await res.json(); - assertEquals(data.coeff, 5.0); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: DELETE /ue-modules/:idModule/:idUE/:idPromo returns 204", async () => { - mockFetch({ - "/ue-modules/JIN702C/1/4AFISE25": { method: "DELETE", status: 204 }, - }); - try { - const res = await fetch( - "http://localhost/api/ue-modules/JIN702C/1/4AFISE25%2F26", - { method: "DELETE" }, - ); - assertEquals(res.status, 204); - } finally { - restoreFetch(); - } -}); - -// --- Mock DB --- - -Deno.test("mock DB: find ue-module by composite key", () => { - const db = createMockDb({ tables: { ueModules: [...ueModules] } }); - const u = db.findOne( - "ueModules", - (u) => - u.idModule === "JIN702C" && u.idUE === 1 && u.idPromo === "4AFISE25/26", - ); - assertExists(u); - assertEquals(u.coeff, 3.0); -}); - -Deno.test("mock DB: filter ue-modules by promo", () => { - const db = createMockDb({ tables: { ueModules: [...ueModules] } }); - const rows = db.findMany( - "ueModules", - (u) => u.idPromo === "4AFISE25/26", - ); - assertEquals(rows.length, 2); -}); - -Deno.test("mock DB: insert ue-module", () => { - const db = createMockDb({ tables: { ueModules: [...ueModules] } }); - db.insert("ueModules", { - idModule: "JIN705C", - idUE: 2, - idPromo: "3AFISE25/26", - coeff: 1.5, - }); - assertEquals(db.getTable("ueModules").length, 4); -}); - -Deno.test("mock DB: update ue-module coeff", () => { - const db = createMockDb({ tables: { ueModules: [...ueModules] } }); - db.updateWhere( - "ueModules", - (u) => u.idModule === "JIN702C" && u.idUE === 1, - { coeff: 6.0 }, - ); - assertEquals( - db.findOne( - "ueModules", - (u) => u.idModule === "JIN702C" && u.idUE === 1, - )?.coeff, - 6.0, - ); -}); - -Deno.test("mock DB: delete ue-module", () => { - const db = createMockDb({ tables: { ueModules: [...ueModules] } }); - db.deleteWhere( - "ueModules", - (u) => u.idModule === "JIN702C" && u.idUE === 1, - ); - assertEquals(db.getTable("ueModules").length, 2); -}); diff --git a/tests/unit/ues_test.ts b/tests/unit/ues_test.ts deleted file mode 100644 index f823f7d..0000000 --- a/tests/unit/ues_test.ts +++ /dev/null @@ -1,164 +0,0 @@ -// Unit tests for /ues endpoints — fixtures, mock API, mock DB - -import { assertEquals, assertExists } from "@std/assert"; -import { mockFetch, restoreFetch } from "../helpers/api_mock.ts"; -import { createMockDb } from "../helpers/db_mock.ts"; -import { type UE, ues } from "../helpers/fixtures.ts"; - -// --- Fixtures --- - -Deno.test("ues: fixtures have correct shape", () => { - assertEquals(ues.length, 2); - assertEquals(typeof ues[0].id, "number"); - assertEquals(typeof ues[0].nom, "string"); -}); - -// --- Mock API --- - -Deno.test("mock API: GET /ues returns list", async () => { - mockFetch({ "/ues": ues }); - try { - const res = await fetch("http://localhost/api/ues"); - assertEquals(res.status, 200); - const data: UE[] = await res.json(); - assertEquals(data.length, 2); - assertExists(data.find((u) => u.nom === "UE Informatique")); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /ues/:id returns one UE", async () => { - mockFetch({ "/ues/1": ues[0] }); - try { - const res = await fetch("http://localhost/api/ues/1"); - assertEquals(res.status, 200); - const data: UE = await res.json(); - assertEquals(data.id, 1); - assertEquals(data.nom, "UE Informatique"); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: GET /ues/:id 404 when not found", async () => { - mockFetch({ - "/ues/99": { status: 404, body: { error: "Ressource introuvable" } }, - }); - try { - const res = await fetch("http://localhost/api/ues/99"); - assertEquals(res.status, 404); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: POST /ues creates UE (201)", async () => { - const newUE: UE = { id: 3, nom: "UE Physique" }; - mockFetch({ "/ues": { method: "POST", status: 201, body: newUE } }); - try { - const res = await fetch("http://localhost/api/ues", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ nom: "UE Physique" }), - }); - assertEquals(res.status, 201); - const data: UE = await res.json(); - assertEquals(data.nom, "UE Physique"); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: POST /ues 400 on missing nom", async () => { - mockFetch({ "/ues": { method: "POST", status: 400 } }); - try { - const res = await fetch("http://localhost/api/ues", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({}), - }); - assertEquals(res.status, 400); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: PUT /ues/:id updates nom", async () => { - const updated: UE = { id: 1, nom: "UE Informatique avancée" }; - mockFetch({ "/ues/1": { method: "PUT", status: 200, body: updated } }); - try { - const res = await fetch("http://localhost/api/ues/1", { - method: "PUT", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ nom: "UE Informatique avancée" }), - }); - assertEquals(res.status, 200); - const data: UE = await res.json(); - assertEquals(data.nom, "UE Informatique avancée"); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: PUT /ues/:id 404 when not found", async () => { - mockFetch({ "/ues/99": { method: "PUT", status: 404 } }); - try { - const res = await fetch("http://localhost/api/ues/99", { - method: "PUT", - body: JSON.stringify({ nom: "X" }), - }); - assertEquals(res.status, 404); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: DELETE /ues/:id returns 204", async () => { - mockFetch({ "/ues/1": { method: "DELETE", status: 204 } }); - try { - const res = await fetch("http://localhost/api/ues/1", { method: "DELETE" }); - assertEquals(res.status, 204); - } finally { - restoreFetch(); - } -}); - -Deno.test("mock API: DELETE /ues/:id 404 when not found", async () => { - mockFetch({ "/ues/99": { method: "DELETE", status: 404 } }); - try { - const res = await fetch("http://localhost/api/ues/99", { - method: "DELETE", - }); - assertEquals(res.status, 404); - } finally { - restoreFetch(); - } -}); - -// --- Mock DB --- - -Deno.test("mock DB: find UE by id", () => { - const db = createMockDb({ tables: { ues: [...ues] } }); - const u = db.findOne("ues", (u) => u.id === 1); - assertExists(u); - assertEquals(u.nom, "UE Informatique"); -}); - -Deno.test("mock DB: insert UE", () => { - const db = createMockDb({ tables: { ues: [...ues] } }); - db.insert("ues", { id: 3, nom: "UE Physique" }); - assertEquals(db.getTable("ues").length, 3); -}); - -Deno.test("mock DB: update UE nom", () => { - const db = createMockDb({ tables: { ues: [...ues] } }); - db.updateWhere("ues", (u) => u.id === 1, { nom: "Updated" }); - assertEquals(db.findOne("ues", (u) => u.id === 1)?.nom, "Updated"); -}); - -Deno.test("mock DB: delete UE", () => { - const db = createMockDb({ tables: { ues: [...ues] } }); - db.deleteWhere("ues", (u) => u.id === 1); - assertEquals(db.getTable("ues").length, 1); -});