diff --git a/drizzle.config.ts b/drizzle.config.ts index 27c4a86..aa57f48 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -2,7 +2,9 @@ import { defineConfig } from "drizzle-kit"; import process from "node:process"; const url = process.env.DATABASE_URL ?? - `postgresql://${process.env.POSTGRES_USER}:${process.env.POSTGRES_PASS}@${process.env.POSTGRES_HOST ?? "localhost"}:${process.env.POSTGRES_PORT ?? 5432}/${process.env.POSTGRES_DB}`; + `postgresql://${process.env.POSTGRES_USER}:${process.env.POSTGRES_PASS}@${ + process.env.POSTGRES_HOST ?? "localhost" + }:${process.env.POSTGRES_PORT ?? 5432}/${process.env.POSTGRES_DB}`; export default defineConfig({ dialect: "postgresql", diff --git a/tests/e2e/users_test.ts b/tests/e2e/users_test.ts new file mode 100644 index 0000000..ab08c08 --- /dev/null +++ b/tests/e2e/users_test.ts @@ -0,0 +1,250 @@ +// #111 - E2E tests for /users endpoints + +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(); + 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 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?idRole filters by role", + async fn() { + await truncateAll(); + const [role1] = await seedRoles([{ nom: "admin" }]); + const [role2] = await seedRoles([{ nom: "employee" }]); + await seedUsers([ + { id: "u1", nom: "A", prenom: "A", idRole: role1.id }, + { id: "u2", nom: "B", prenom: "B", 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, "u1"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// --- POST /users --- + +Deno.test({ + name: "e2e users: POST /users creates user (201)", + async fn() { + await truncateAll(); + const [role] = await seedRoles([{ nom: "employee" }]); + const res = await usersHandler.POST!( + makeJsonRequest("/users", "POST", { + id: "nouveau.user", + nom: "Nouveau", + prenom: "User", + idRole: role.id, + }), + makeEmployeeContext(), + ); + assertEquals(res.status, 201); + const body = await res.json(); + assertEquals(body.id, "nouveau.user"); + assertExists(body.nom); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "e2e users: POST /users 409 on duplicate id", + async fn() { + await truncateAll(); + const [role] = await seedRoles([{ nom: "employee" }]); + await seedUsers([{ + id: "dup.user", + nom: "A", + prenom: "A", + idRole: role.id, + }]); + const res = await usersHandler.POST!( + makeJsonRequest("/users", "POST", { + id: "dup.user", + nom: "B", + prenom: "B", + idRole: role.id, + }), + makeEmployeeContext(), + ); + assertEquals(res.status, 409); + }, + 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, +}); + +// --- GET /users/:id --- + +Deno.test({ + name: "e2e users: GET /users/:id returns user", + async fn() { + await truncateAll(); + const [role] = await seedRoles([{ nom: "employee" }]); + await seedUsers([{ + id: "test.user", + nom: "Test", + prenom: "User", + idRole: role.id, + }]); + const res = await userHandler.GET!( + makeGetRequest("/users/test.user"), + makeEmployeeContext({ id: "test.user" }), + ); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.id, "test.user"); + }, + 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"), + makeEmployeeContext({ id: "ghost" }), + ); + 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(); + const [role] = await seedRoles([{ nom: "employee" }]); + await seedUsers([{ + id: "upd.user", + nom: "Old", + prenom: "Name", + idRole: role.id, + }]); + const res = await userHandler.PUT!( + makeJsonRequest("/users/upd.user", "PUT", { + nom: "New", + prenom: "Name", + idRole: role.id, + }), + makeEmployeeContext({ id: "upd.user" }), + ); + assertEquals(res.status, 200); + const body = await res.json(); + assertEquals(body.nom, "New"); + }, + 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", "PUT", { + nom: "X", + prenom: "Y", + idRole: 1, + }), + makeEmployeeContext({ id: "ghost" }), + ); + 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(); + const [role] = await seedRoles([{ nom: "employee" }]); + await seedUsers([{ + id: "del.user", + nom: "Del", + prenom: "Me", + idRole: role.id, + }]); + const res = await userHandler.DELETE!( + makeGetRequest("/users/del.user"), + makeEmployeeContext({ id: "del.user" }), + ); + 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"), + makeEmployeeContext({ id: "ghost" }), + ); + assertEquals(res.status, 404); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/integration/users_test.ts b/tests/integration/users_test.ts index e0d5ae9..4080e4e 100644 --- a/tests/integration/users_test.ts +++ b/tests/integration/users_test.ts @@ -1,24 +1,24 @@ +// #111 - Integration tests for /users endpoints + import { assertEquals, assertExists } from "@std/assert"; import { - closeTestPool, seedRoles, seedUsers, testDb, truncateAll, } from "../helpers/db_integration.ts"; import { users } from "$root/databases/schema.ts"; +import { eq } from "npm:drizzle-orm@0.45.2"; Deno.test({ - name: "integration: GET /users - DB round trip", + name: "integration users: list 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 }, ]); - const rows = await testDb.select().from(users); assertEquals(rows.length, 2); assertExists(rows.find((u) => u.id === "dupont.jean")); @@ -28,30 +28,110 @@ Deno.test({ }); Deno.test({ - name: "integration: INSERT user and retrieve by id", + name: "integration users: filter by idRole", 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 [role1] = await seedRoles([{ nom: "admin" }]); + const [role2] = await seedRoles([{ nom: "employee" }]); + await seedUsers([ + { id: "u1", nom: "A", prenom: "A", idRole: role1.id }, + { id: "u2", nom: "B", prenom: "B", idRole: role2.id }, + ]); + const rows = await testDb + .select() + .from(users) + .where(eq(users.idRole, role1.id)); + assertEquals(rows.length, 1); + assertEquals(rows[0].id, "u1"); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ - name: "integration: cleanup - close pool", + name: "integration users: create and retrieve by id", async fn() { - await closeTestPool(); + 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"); + + const row = await testDb + .select() + .from(users) + .where(eq(users.id, "durand.claire")) + .then((r) => r[0] ?? null); + assertExists(row); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration users: get by id returns null when not found", + async fn() { + await truncateAll(); + const row = await testDb + .select() + .from(users) + .where(eq(users.id, "nonexistent")) + .then((r) => r[0] ?? null); + assertEquals(row, null); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration users: update user fields", + async fn() { + await truncateAll(); + const [role] = await seedRoles([{ nom: "employee" }]); + await seedUsers([{ + id: "test.user", + nom: "Test", + prenom: "User", + idRole: role.id, + }]); + const [updated] = await testDb + .update(users) + .set({ nom: "Updated", prenom: "Name" }) + .where(eq(users.id, "test.user")) + .returning(); + assertExists(updated); + assertEquals(updated.nom, "Updated"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "integration users: delete user", + async fn() { + await truncateAll(); + const [role] = await seedRoles([{ nom: "employee" }]); + await seedUsers([{ + id: "to.delete", + nom: "Del", + prenom: "Me", + idRole: role.id, + }]); + await testDb.delete(users).where(eq(users.id, "to.delete")); + const row = await testDb + .select() + .from(users) + .where(eq(users.id, "to.delete")) + .then((r) => r[0] ?? null); + assertEquals(row, null); }, sanitizeResources: false, sanitizeOps: false, diff --git a/tests/unit/users_test.ts b/tests/unit/users_test.ts new file mode 100644 index 0000000..317ec3f --- /dev/null +++ b/tests/unit/users_test.ts @@ -0,0 +1,216 @@ +import { assertEquals } from "@std/assert"; +import { getFetchCalls, mockFetch, restoreFetch } from "../helpers/api_mock.ts"; + +const BASE = "http://localhost/apps/admin/api/users"; + +const users = [ + { id: "dupont.jean", nom: "Dupont", prenom: "Jean", idRole: 1 }, + { id: "martin.alice", nom: "Martin", prenom: "Alice", idRole: 2 }, +]; + +// --- GET /users --- + +Deno.test("GET /users - returns all users", async () => { + mockFetch({ [BASE]: users }); + try { + const res = await fetch(BASE); + assertEquals(res.status, 200); + const data = await res.json(); + assertEquals(data.length, 2); + assertEquals(data[0].id, "dupont.jean"); + } finally { + restoreFetch(); + } +}); + +Deno.test("GET /users - filters by idRole", async () => { + const filtered = users.filter((u) => u.idRole === 1); + mockFetch({ [`${BASE}?idRole=1`]: filtered }); + try { + const res = await fetch(`${BASE}?idRole=1`); + assertEquals(res.status, 200); + const data = await res.json(); + assertEquals(data.length, 1); + assertEquals(data[0].idRole, 1); + } finally { + restoreFetch(); + } +}); + +// --- POST /users --- + +Deno.test("POST /users - creates a user and returns 201", async () => { + const newUser = { + id: "durand.claire", + nom: "Durand", + prenom: "Claire", + idRole: 1, + }; + mockFetch({ [BASE]: { method: "POST", status: 201, body: newUser } }); + try { + const res = await fetch(BASE, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(newUser), + }); + assertEquals(res.status, 201); + const data = await res.json(); + assertEquals(data.id, "durand.claire"); + assertEquals(data.nom, "Durand"); + } finally { + restoreFetch(); + } +}); + +Deno.test("POST /users - returns 409 on duplicate id", async () => { + mockFetch({ + [BASE]: { + method: "POST", + status: 409, + body: { error: "Un utilisateur avec cet identifiant existe déjà" }, + }, + }); + try { + const res = await fetch(BASE, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(users[0]), + }); + assertEquals(res.status, 409); + const data = await res.json(); + assertEquals(typeof data.error, "string"); + } finally { + restoreFetch(); + } +}); + +Deno.test("POST /users - returns 400 on missing fields", async () => { + mockFetch({ [BASE]: { method: "POST", status: 400 } }); + try { + const res = await fetch(BASE, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ id: "x" }), + }); + assertEquals(res.status, 400); + } finally { + restoreFetch(); + } +}); + +// --- GET /users/{id} --- + +Deno.test("GET /users/{id} - returns a user by id", async () => { + mockFetch({ [`${BASE}/dupont.jean`]: users[0] }); + try { + const res = await fetch(`${BASE}/dupont.jean`); + assertEquals(res.status, 200); + const data = await res.json(); + assertEquals(data.id, "dupont.jean"); + assertEquals(data.prenom, "Jean"); + } finally { + restoreFetch(); + } +}); + +Deno.test("GET /users/{id} - returns 404 for unknown id", async () => { + mockFetch({ + [`${BASE}/inconnu`]: { + status: 404, + body: { error: "Ressource introuvable" }, + }, + }); + try { + const res = await fetch(`${BASE}/inconnu`); + assertEquals(res.status, 404); + const data = await res.json(); + assertEquals(typeof data.error, "string"); + } finally { + restoreFetch(); + } +}); + +// --- PUT /users/{id} --- + +Deno.test("PUT /users/{id} - updates a user", async () => { + const updated = { ...users[0], prenom: "Jean-Pierre" }; + mockFetch({ + [`${BASE}/dupont.jean`]: { method: "PUT", status: 200, body: updated }, + }); + try { + const res = await fetch(`${BASE}/dupont.jean`, { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ nom: "Dupont", prenom: "Jean-Pierre", idRole: 1 }), + }); + assertEquals(res.status, 200); + const data = await res.json(); + assertEquals(data.prenom, "Jean-Pierre"); + } finally { + restoreFetch(); + } +}); + +Deno.test("PUT /users/{id} - returns 404 for unknown id", async () => { + mockFetch({ + [`${BASE}/inconnu`]: { + method: "PUT", + status: 404, + body: { error: "Ressource introuvable" }, + }, + }); + try { + const res = await fetch(`${BASE}/inconnu`, { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ nom: "X", prenom: "Y", idRole: 1 }), + }); + assertEquals(res.status, 404); + } finally { + restoreFetch(); + } +}); + +// --- DELETE /users/{id} --- + +Deno.test("DELETE /users/{id} - deletes a user and returns 204", async () => { + mockFetch({ + [`${BASE}/dupont.jean`]: { method: "DELETE", status: 204 }, + }); + try { + const res = await fetch(`${BASE}/dupont.jean`, { method: "DELETE" }); + assertEquals(res.status, 204); + } finally { + restoreFetch(); + } +}); + +Deno.test("DELETE /users/{id} - returns 404 for unknown id", async () => { + mockFetch({ + [`${BASE}/inconnu`]: { + method: "DELETE", + status: 404, + body: { error: "Ressource introuvable" }, + }, + }); + try { + const res = await fetch(`${BASE}/inconnu`, { method: "DELETE" }); + assertEquals(res.status, 404); + } finally { + restoreFetch(); + } +}); + +// --- getFetchCalls --- + +Deno.test("GET /users - call is tracked", async () => { + mockFetch({ [BASE]: users }); + try { + await fetch(BASE); + const calls = getFetchCalls(); + assertEquals(calls.length, 1); + assertEquals(calls[0].method, "GET"); + } finally { + restoreFetch(); + } +});