diff --git a/tests/e2e/modules_test.ts b/tests/e2e/modules_test.ts new file mode 100644 index 0000000..bc8f1c8 --- /dev/null +++ b/tests/e2e/modules_test.ts @@ -0,0 +1,204 @@ +// #113 - E2E tests for /modules endpoints + +import { assertEquals, assertExists } 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 empty 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, 0); + }, + 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/integration/modules_test.ts b/tests/integration/modules_test.ts new file mode 100644 index 0000000..f0acc7f --- /dev/null +++ b/tests/integration/modules_test.ts @@ -0,0 +1,98 @@ +// #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/unit/modules_test.ts b/tests/unit/modules_test.ts new file mode 100644 index 0000000..c9f2276 --- /dev/null +++ b/tests/unit/modules_test.ts @@ -0,0 +1,151 @@ +// #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); +});