From c5018d9ced772f5751cf9354a95456651ac4d251 Mon Sep 17 00:00:00 2001 From: Djalim Simaila Date: Sun, 26 Apr 2026 13:36:26 +0200 Subject: [PATCH] test(integration): add DB integration tests for students, promotions, roles, modules Covers full CRUD for each resource via testDb: - promotions: list, create, get by id, not found, update, delete - students: list, filter by promo, create, get, not found, update, delete - roles: list, create, get with permissions, update+reset perms, delete - modules: list, create, duplicate id rejection, get, not found, update, delete 27 integration tests passing in CI (act + Gitea Actions). Co-Authored-By: Claude Sonnet 4.6 --- tests/integration/modules_test.ts | 138 +++++++++++++++++++++++ tests/integration/promotions_test.ts | 122 ++++++++++++++++++++ tests/integration/roles_test.ts | 145 ++++++++++++++++++++++++ tests/integration/students_test.ts | 163 +++++++++++++++++++++++++++ tests/integration/users_test.ts | 9 -- 5 files changed, 568 insertions(+), 9 deletions(-) create mode 100644 tests/integration/modules_test.ts create mode 100644 tests/integration/promotions_test.ts create mode 100644 tests/integration/roles_test.ts create mode 100644 tests/integration/students_test.ts diff --git a/tests/integration/modules_test.ts b/tests/integration/modules_test.ts new file mode 100644 index 0000000..96dc24c --- /dev/null +++ b/tests/integration/modules_test.ts @@ -0,0 +1,138 @@ +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: GET /modules - returns 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: POST /modules - creates a module", + async fn() { + await truncateAll(); + + const [created] = await testDb + .insert(modules) + .values({ id: "PHYS101", nom: "Physique" }) + .returning(); + + assertExists(created); + assertEquals(created.id, "PHYS101"); + assertEquals(created.nom, "Physique"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration: POST /modules - rejects duplicate id", + async fn() { + await truncateAll(); + await seedModules([{ id: "MATH101", nom: "Mathématiques" }]); + + await assertRejects(() => + testDb + .insert(modules) + .values({ id: "MATH101", nom: "Maths (doublon)" }) + ); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration: GET /modules/:id - returns a specific module", + async fn() { + await truncateAll(); + await seedModules([{ id: "ELEC201", nom: "Électronique" }]); + + const row = await testDb + .select() + .from(modules) + .where(eq(modules.id, "ELEC201")) + .then((r) => r[0] ?? null); + + assertExists(row); + assertEquals(row.nom, "Électronique"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration: GET /modules/: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: PUT /modules/:id - updates a module", + async fn() { + await truncateAll(); + await seedModules([{ id: "CHIM101", nom: "Chimie" }]); + + const [updated] = await testDb + .update(modules) + .set({ nom: "Chimie organique" }) + .where(eq(modules.id, "CHIM101")) + .returning(); + + assertExists(updated); + assertEquals(updated.nom, "Chimie organique"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration: DELETE /modules/:id - deletes a module", + async fn() { + await truncateAll(); + await seedModules([{ id: "BIO101", nom: "Biologie" }]); + + const [deleted] = await testDb + .delete(modules) + .where(eq(modules.id, "BIO101")) + .returning(); + + assertExists(deleted); + + 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/integration/promotions_test.ts b/tests/integration/promotions_test.ts new file mode 100644 index 0000000..b572861 --- /dev/null +++ b/tests/integration/promotions_test.ts @@ -0,0 +1,122 @@ +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: GET /promotions - returns all promotions", + 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: POST /promotions - creates a promotion", + async fn() { + await truncateAll(); + + const [created] = await testDb + .insert(promotions) + .values({ id: "PEIP1-2025", annee: "2025" }) + .returning(); + + assertExists(created); + assertEquals(created.id, "PEIP1-2025"); + assertEquals(created.annee, "2025"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration: GET /promotions/:id - returns a specific promotion", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "INFO3-2024", annee: "2024" }]); + + const row = await testDb + .select() + .from(promotions) + .where(eq(promotions.id, "INFO3-2024")) + .then((r) => r[0] ?? null); + + assertExists(row); + assertEquals(row.id, "INFO3-2024"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration: GET /promotions/: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: PUT /promotions/:id - updates a promotion", + 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: DELETE /promotions/:id - deletes a promotion", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "INFO3-2022", annee: "2022" }]); + + const [deleted] = await testDb + .delete(promotions) + .where(eq(promotions.id, "INFO3-2022")) + .returning(); + + assertExists(deleted); + + 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, +}); diff --git a/tests/integration/roles_test.ts b/tests/integration/roles_test.ts new file mode 100644 index 0000000..937de05 --- /dev/null +++ b/tests/integration/roles_test.ts @@ -0,0 +1,145 @@ +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: GET /roles - returns 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: POST /roles - creates a role", + async fn() { + await truncateAll(); + + const [created] = await testDb + .insert(roles) + .values({ nom: "viewer" }) + .returning(); + + assertExists(created); + assertExists(created.id); + assertEquals(created.nom, "viewer"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration: 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); + assertExists(perms.find((p) => p.idPermission === "student_read")); + assertExists(perms.find((p) => p.idPermission === "student_write")); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration: PUT /roles/:id - updates role and resets permissions", + async fn() { + await truncateAll(); + const [role] = await seedRoles([{ nom: "employee" }]); + 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" }]); + + // Rename + reset permissions + await testDb + .update(roles) + .set({ nom: "teacher" }) + .where(eq(roles.id, role.id)); + await testDb + .delete(rolePermissions) + .where(eq(rolePermissions.idRole, role.id)); + await testDb + .insert(rolePermissions) + .values([{ idRole: role.id, idPermission: "note_write" }]); + + const updatedRole = await testDb + .select() + .from(roles) + .where(eq(roles.id, role.id)) + .then((r) => r[0]); + const perms = await testDb + .select() + .from(rolePermissions) + .where(eq(rolePermissions.idRole, role.id)); + + assertEquals(updatedRole.nom, "teacher"); + assertEquals(perms.length, 1); + assertEquals(perms[0].idPermission, "note_write"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration: DELETE /roles/:id - deletes role and its permissions", + async fn() { + await truncateAll(); + const [role] = await seedRoles([{ nom: "moderator" }]); + await testDb + .insert(permissions) + .values([{ id: "user_read", nom: "Consulter les utilisateurs" }]); + await testDb + .insert(rolePermissions) + .values([{ idRole: role.id, idPermission: "user_read" }]); + + await testDb + .delete(rolePermissions) + .where(eq(rolePermissions.idRole, role.id)); + const [deleted] = await testDb + .delete(roles) + .where(eq(roles.id, role.id)) + .returning(); + + assertExists(deleted); + + const remaining = await testDb + .select() + .from(rolePermissions) + .where(eq(rolePermissions.idRole, role.id)); + assertEquals(remaining.length, 0); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/integration/students_test.ts b/tests/integration/students_test.ts new file mode 100644 index 0000000..87f0ef9 --- /dev/null +++ b/tests/integration/students_test.ts @@ -0,0 +1,163 @@ +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: GET /students - returns 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" }, + { nom: "Durand", prenom: "Claire", idPromo: "PEIP1-2024" }, + ]); + + const rows = await testDb.select().from(students); + assertEquals(rows.length, 3); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration: 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 rows = await testDb + .select() + .from(students) + .where(eq(students.idPromo, "PEIP1-2024")); + + assertEquals(rows.length, 2); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration: POST /students - creates a student", + 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); + assertExists(created.numEtud); + assertEquals(created.nom, "Leroy"); + assertEquals(created.prenom, "Paul"); + assertEquals(created.idPromo, "INFO3-2024"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration: GET /students/:numEtud - returns a specific student", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "INFO3-2024" }]); + const [student] = await seedStudents([ + { nom: "Bernard", prenom: "Lucie", idPromo: "INFO3-2024" }, + ]); + + const row = await testDb + .select() + .from(students) + .where(eq(students.numEtud, student.numEtud)) + .then((r) => r[0] ?? null); + + assertExists(row); + assertEquals(row.nom, "Bernard"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration: GET /students/: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: PUT /students/:numEtud - updates a student", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "INFO3-2024" }, { id: "INFO4-2024" }]); + const [student] = await seedStudents([ + { nom: "Petit", prenom: "Hugo", idPromo: "INFO3-2024" }, + ]); + + const [updated] = await testDb + .update(students) + .set({ nom: "Grand", prenom: "Hugo", idPromo: "INFO4-2024" }) + .where(eq(students.numEtud, student.numEtud)) + .returning(); + + assertExists(updated); + assertEquals(updated.nom, "Grand"); + assertEquals(updated.idPromo, "INFO4-2024"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration: DELETE /students/:numEtud - deletes a student", + async fn() { + await truncateAll(); + await seedPromotions([{ id: "INFO3-2024" }]); + const [student] = await seedStudents([ + { nom: "Thomas", prenom: "Eva", idPromo: "INFO3-2024" }, + ]); + + const [deleted] = await testDb + .delete(students) + .where(eq(students.numEtud, student.numEtud)) + .returning(); + + assertExists(deleted); + + const row = await testDb + .select() + .from(students) + .where(eq(students.numEtud, student.numEtud)) + .then((r) => r[0] ?? null); + + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/integration/users_test.ts b/tests/integration/users_test.ts index e0d5ae9..a69a00b 100644 --- a/tests/integration/users_test.ts +++ b/tests/integration/users_test.ts @@ -1,6 +1,5 @@ import { assertEquals, assertExists } from "@std/assert"; import { - closeTestPool, seedRoles, seedUsers, testDb, @@ -48,11 +47,3 @@ Deno.test({ sanitizeOps: false, }); -Deno.test({ - name: "integration: cleanup - close pool", - async fn() { - await closeTestPool(); - }, - sanitizeResources: false, - sanitizeOps: false, -});