diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 6b3b830..d2a8d16 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -68,3 +68,12 @@ jobs: POSTGRES_PASS: test POSTGRES_DB: polympr_test run: deno task test:integration + + - name: Run e2e tests + env: + POSTGRES_HOST: 127.0.0.1 + POSTGRES_PORT: 5432 + POSTGRES_USER: test + POSTGRES_PASS: test + POSTGRES_DB: polympr_test + run: deno task test:e2e diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..d2a8d16 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,79 @@ +name: "Tests" + +on: + pull_request: + branches: + - main + - develop + push: + branches: + - develop + +jobs: + unit: + name: "Unit tests" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - name: Install dependencies + run: deno install + + - name: Run unit tests + run: deno task test:unit + + integration: + name: "Integration tests" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - name: Start postgres + run: | + sudo apt-get update -qq && sudo apt-get install -y -qq postgresql > /dev/null + PG_VER=$(ls /etc/postgresql/) + sudo sed -i "s/^#*listen_addresses\s*=.*/listen_addresses = '127.0.0.1'/" /etc/postgresql/$PG_VER/main/postgresql.conf + echo "host all all 127.0.0.1/32 md5" | sudo tee -a /etc/postgresql/$PG_VER/main/pg_hba.conf + sudo pg_ctlcluster $PG_VER main restart + until sudo -u postgres pg_isready -h 127.0.0.1; do sleep 1; done + sudo -u postgres psql -c "CREATE USER test WITH PASSWORD 'test';" + sudo -u postgres psql -c "CREATE DATABASE polympr_test OWNER test;" + sudo -u postgres psql -d polympr_test -c "GRANT ALL ON SCHEMA public TO test;" + + - name: Apply migrations + run: | + sed 's/--> statement-breakpoint/;/g' databases/migrations/0000_square_jetstream.sql | \ + PGPASSWORD=test psql -h 127.0.0.1 -U test -d polympr_test + + - name: Install dependencies + run: npm install --ignore-scripts && deno install + + - name: Run integration tests + env: + POSTGRES_HOST: 127.0.0.1 + POSTGRES_PORT: 5432 + POSTGRES_USER: test + POSTGRES_PASS: test + POSTGRES_DB: polympr_test + run: deno task test:integration + + - name: Run e2e tests + env: + POSTGRES_HOST: 127.0.0.1 + POSTGRES_PORT: 5432 + POSTGRES_USER: test + POSTGRES_PASS: test + POSTGRES_DB: polympr_test + run: deno task test:e2e diff --git a/deno.json b/deno.json index b3d8c09..ed7422e 100644 --- a/deno.json +++ b/deno.json @@ -13,6 +13,7 @@ "test": "deno test -A --no-check tests/", "test:unit": "deno test -A --no-check tests/unit/", "test:integration": "deno test -A --no-check tests/integration/", + "test:e2e": "deno test -A --no-check tests/e2e/", "migrate": "node_modules/.bin/drizzle-kit migrate" }, "lint": { diff --git a/tests/e2e/students_test.ts b/tests/e2e/students_test.ts new file mode 100644 index 0000000..07de874 --- /dev/null +++ b/tests/e2e/students_test.ts @@ -0,0 +1,285 @@ +// #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/helpers/handler.ts b/tests/helpers/handler.ts new file mode 100644 index 0000000..17aae24 --- /dev/null +++ b/tests/helpers/handler.ts @@ -0,0 +1,88 @@ +// Helper pour les tests E2E — appel direct des handlers Fresh +// sans lancer de serveur HTTP + +import { FreshContext } from "$fresh/server.ts"; +import { AuthenticatedState } from "$root/defaults/interfaces.ts"; +import { CasContent } from "$root/defaults/interfaces.ts"; + +const BASE_EMPLOYEE_SESSION: CasContent = { + amuCampus: "", + amuComposante: "", + amuDateValidation: "", + coGroup: "", + eduPersonPrimaryAffiliation: "employee", + eduPersonPrincipalName: "test.user@polytech.fr", + mail: "test.user@polytech.fr", + displayName: "Test User", + givenName: "Test", + memberOf: [], + sn: "User", + supannCivilite: "M.", + supannEntiteAffectation: "", + supannEtuAnneeInscription: "", + supannEtuEtape: "", + uid: "test.user", +}; + +/** + * Crée un FreshContext mock authentifié en tant qu'employee. + */ +export function makeEmployeeContext( + params: Record = {}, +): FreshContext { + return { + params, + state: { + isAuthenticated: true, + session: { ...BASE_EMPLOYEE_SESSION }, + availablePages: {}, + }, + render: () => Promise.resolve(new Response()), + renderNotFound: () => Promise.resolve(new Response(null, { status: 404 })), + next: () => Promise.resolve(new Response()), + } as unknown as FreshContext; +} + +/** + * Crée un FreshContext mock avec un affiliation personnalisée. + */ +export function makeContextWithAffiliation( + affiliation: string, + params: Record = {}, +): FreshContext { + const ctx = makeEmployeeContext(params); + (ctx.state as AuthenticatedState).session.eduPersonPrimaryAffiliation = + affiliation; + return ctx; +} + +/** + * Crée une Request GET simple. + */ +export function makeGetRequest( + path: string, + searchParams?: Record, +): Request { + const url = new URL(`http://localhost${path}`); + if (searchParams) { + for (const [k, v] of Object.entries(searchParams)) { + url.searchParams.set(k, v); + } + } + return new Request(url.toString()); +} + +/** + * Crée une Request POST/PUT avec un corps JSON. + */ +export function makeJsonRequest( + path: string, + method: string, + body: unknown, +): Request { + return new Request(`http://localhost${path}`, { + method, + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); +} diff --git a/tests/integration/students_test.ts b/tests/integration/students_test.ts new file mode 100644 index 0000000..bb53d6f --- /dev/null +++ b/tests/integration/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/unit/students_test.ts b/tests/unit/students_test.ts new file mode 100644 index 0000000..2b51029 --- /dev/null +++ b/tests/unit/students_test.ts @@ -0,0 +1,201 @@ +// #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(); + } +});