From e75098083acf6252387669c80b9887cb6bbe55c7 Mon Sep 17 00:00:00 2001 From: Djalim Simaila Date: Sun, 26 Apr 2026 14:06:12 +0200 Subject: [PATCH 1/2] test(roles): add unit, integration and e2e tests for /roles (#112) - unit: fixture shapes, mock API (GET/POST/PUT/DELETE), mock DB CRUD - integration: list, create, assign permissions, update, reset perms, delete - e2e: handler calls with mock context + real DB, covers 400/404 cases --- tests/e2e/roles_test.ts | 172 ++++++++++++++++++++++++++++++++ tests/integration/roles_test.ts | 120 ++++++++++++++++++++++ tests/unit/roles_test.ts | 155 ++++++++++++++++++++++++++++ 3 files changed, 447 insertions(+) create mode 100644 tests/e2e/roles_test.ts create mode 100644 tests/integration/roles_test.ts create mode 100644 tests/unit/roles_test.ts diff --git a/tests/e2e/roles_test.ts b/tests/e2e/roles_test.ts new file mode 100644 index 0000000..5decace --- /dev/null +++ b/tests/e2e/roles_test.ts @@ -0,0 +1,172 @@ +// #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/integration/roles_test.ts b/tests/integration/roles_test.ts new file mode 100644 index 0000000..c7c4307 --- /dev/null +++ b/tests/integration/roles_test.ts @@ -0,0 +1,120 @@ +// #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/unit/roles_test.ts b/tests/unit/roles_test.ts new file mode 100644 index 0000000..7cca58d --- /dev/null +++ b/tests/unit/roles_test.ts @@ -0,0 +1,155 @@ +// #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); +}); -- 2.52.0 From f038e4020bd0121d5f85489967714761a88fafa2 Mon Sep 17 00:00:00 2001 From: Djalim Simaila Date: Sun, 26 Apr 2026 14:18:55 +0200 Subject: [PATCH 2/2] style: fix deno fmt and lint --- tests/e2e/roles_test.ts | 5 ++++- tests/integration/roles_test.ts | 7 +++++-- tests/unit/roles_test.ts | 8 ++++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/e2e/roles_test.ts b/tests/e2e/roles_test.ts index 5decace..8026434 100644 --- a/tests/e2e/roles_test.ts +++ b/tests/e2e/roles_test.ts @@ -18,7 +18,10 @@ Deno.test({ async fn() { await truncateAll(); await seedRoles([{ nom: "admin" }, { nom: "employee" }]); - const res = await rolesHandler.GET!(makeGetRequest("/roles"), makeEmployeeContext()); + const res = await rolesHandler.GET!( + makeGetRequest("/roles"), + makeEmployeeContext(), + ); assertEquals(res.status, 200); const body = await res.json(); assertEquals(body.length, 2); diff --git a/tests/integration/roles_test.ts b/tests/integration/roles_test.ts index c7c4307..9fb7a6c 100644 --- a/tests/integration/roles_test.ts +++ b/tests/integration/roles_test.ts @@ -21,7 +21,8 @@ Deno.test({ name: "integration roles: create and retrieve by id", async fn() { await truncateAll(); - const [created] = await testDb.insert(roles).values({ nom: "viewer" }).returning(); + const [created] = await testDb.insert(roles).values({ nom: "viewer" }) + .returning(); assertExists(created.id); assertEquals(created.nom, "viewer"); const row = await testDb @@ -87,7 +88,9 @@ Deno.test({ { idRole: role.id, idPermission: "note_read" }, ]); // reset - await testDb.delete(rolePermissions).where(eq(rolePermissions.idRole, role.id)); + await testDb.delete(rolePermissions).where( + eq(rolePermissions.idRole, role.id), + ); await testDb.insert(rolePermissions).values([ { idRole: role.id, idPermission: "note_write" }, ]); diff --git a/tests/unit/roles_test.ts b/tests/unit/roles_test.ts index 7cca58d..eeae55e 100644 --- a/tests/unit/roles_test.ts +++ b/tests/unit/roles_test.ts @@ -58,7 +58,9 @@ Deno.test("mock API: GET /roles/:id returns role", async () => { }); Deno.test("mock API: GET /roles/:id 404 when not found", async () => { - mockFetch({ "/roles/99": { status: 404, body: { error: "Ressource introuvable" } } }); + mockFetch({ + "/roles/99": { status: 404, body: { error: "Ressource introuvable" } }, + }); try { const res = await fetch("http://localhost/api/roles/99"); assertEquals(res.status, 404); @@ -120,7 +122,9 @@ Deno.test("mock API: PUT /roles/:id updates role and permissions", async () => { 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" }); + const res = await fetch("http://localhost/api/roles/2", { + method: "DELETE", + }); assertEquals(res.status, 204); } finally { restoreFetch(); -- 2.52.0