diff --git a/routes/(apps)/admin/api/enseignements.ts b/routes/(apps)/admin/api/enseignements.ts new file mode 100644 index 0000000..0f6c09d --- /dev/null +++ b/routes/(apps)/admin/api/enseignements.ts @@ -0,0 +1,70 @@ +import { FreshContext, Handlers } from "$fresh/server.ts"; +import { db } from "$root/databases/db.ts"; +import { enseignements } from "$root/databases/schema.ts"; +import { AuthenticatedState } from "$root/defaults/interfaces.ts"; +import { and, eq } from "npm:drizzle-orm@0.45.2"; + +const NOT_FOUND = new Response( + JSON.stringify({ error: "Ressource introuvable" }), + { status: 404, headers: { "content-type": "application/json" } }, +); + +const FORBIDDEN = new Response(null, { status: 403 }); + +const CONFLICT = new Response( + JSON.stringify({ error: "Cet enseignement existe déjà." }), + { status: 409, headers: { "content-type": "application/json" } }, +); + +export const handler: Handlers = { + // #29 POST /enseignements + async POST( + request: Request, + context: FreshContext, + ): Promise { + if (context.state.session.eduPersonPrimaryAffiliation !== "employee") { + return FORBIDDEN; + } + + const body: { + idProf: string; + idModule: string; + idPromo: string; + } = await request.json(); + + if (!body.idProf || !body.idModule || !body.idPromo) { + return new Response(null, { status: 400 }); + } + + // Check if enseignement already exists + const existing = await db + .select() + .from(enseignements) + .where( + and( + eq(enseignements.idProf, body.idProf), + eq(enseignements.idModule, body.idModule), + eq(enseignements.idPromo, body.idPromo), + ), + ) + .then((rows) => rows[0] ?? null); + + if (existing) { + return CONFLICT; + } + + const [created] = await db + .insert(enseignements) + .values({ + idProf: body.idProf, + idModule: body.idModule, + idPromo: body.idPromo, + }) + .returning(); + + return new Response(JSON.stringify(created), { + status: 201, + headers: { "content-type": "application/json" }, + }); + }, +}; diff --git a/routes/(apps)/admin/api/enseignements/[idProf]_[idModule]_[idPromo].ts b/routes/(apps)/admin/api/enseignements/[idProf]_[idModule]_[idPromo].ts new file mode 100644 index 0000000..30dbd8a --- /dev/null +++ b/routes/(apps)/admin/api/enseignements/[idProf]_[idModule]_[idPromo].ts @@ -0,0 +1,75 @@ +import { FreshContext, Handlers } from "$fresh/server.ts"; +import { db } from "$root/databases/db.ts"; +import { enseignements } from "$root/databases/schema.ts"; +import { AuthenticatedState } from "$root/defaults/interfaces.ts"; +import { and, eq } from "npm:drizzle-orm@0.45.2"; + +const NOT_FOUND = new Response( + JSON.stringify({ error: "Ressource introuvable" }), + { status: 404, headers: { "content-type": "application/json" } }, +); + +const FORBIDDEN = new Response(null, { status: 403 }); + +export const handler: Handlers = { + // #30 GET /enseignements/{idProf}/{idModule}/{idPromo} + async GET( + _request: Request, + context: FreshContext, + ): Promise { + if (context.state.session.eduPersonPrimaryAffiliation !== "employee") { + return FORBIDDEN; + } + + const idProf = context.params.idProf; + const idModule = context.params.idModule; + const idPromo = context.params.idPromo; + + const enseignement = await db + .select() + .from(enseignements) + .where( + and( + eq(enseignements.idProf, idProf), + eq(enseignements.idModule, idModule), + eq(enseignements.idPromo, idPromo), + ), + ) + .then((rows) => rows[0] ?? null); + + if (!enseignement) return NOT_FOUND; + + return new Response(JSON.stringify(enseignement), { + headers: { "content-type": "application/json" }, + }); + }, + + // #31 DELETE /enseignements/{idProf}/{idModule}/{idPromo} + async DELETE( + _request: Request, + context: FreshContext, + ): Promise { + if (context.state.session.eduPersonPrimaryAffiliation !== "employee") { + return FORBIDDEN; + } + + const idProf = context.params.idProf; + const idModule = context.params.idModule; + const idPromo = context.params.idPromo; + + const [deleted] = await db + .delete(enseignements) + .where( + and( + eq(enseignements.idProf, idProf), + eq(enseignements.idModule, idModule), + eq(enseignements.idPromo, idPromo), + ), + ) + .returning(); + + if (!deleted) return NOT_FOUND; + + return new Response(null, { status: 204 }); + }, +}; diff --git a/tests/unit/enseignements_test.ts b/tests/unit/enseignements_test.ts new file mode 100644 index 0000000..17bff52 --- /dev/null +++ b/tests/unit/enseignements_test.ts @@ -0,0 +1,197 @@ +import { assertEquals, assertExists } from "@std/assert"; +import { getFetchCalls, mockFetch, restoreFetch } from "../helpers/api_mock.ts"; +import { createMockDb } from "../helpers/db_mock.ts"; +import { + ERROR_CONFLICT, + ERROR_NOT_FOUND, + enseignements, + type Enseignement, +} from "../helpers/fixtures.ts"; + +Deno.test("enseignements - POST 201 creates new enseignement", async () => { + const newEnseignement: Enseignement = { + idProf: 1, + idModule: "JIN705C", + idPromo: "4AFISE25/26", + }; + + mockFetch({ + "/enseignements": { + method: "POST", + status: 201, + body: newEnseignement, + }, + }); + + try { + const res = await fetch("http://localhost/api/enseignements", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(newEnseignement), + }); + assertEquals(res.status, 201); + const data = await res.json(); + assertEquals(data.idProf, 1); + assertEquals(data.idModule, "JIN705C"); + assertEquals(data.idPromo, "4AFISE25/26"); + } finally { + restoreFetch(); + } +}); + +Deno.test("enseignements - POST 409 conflict on duplicate", async () => { + mockFetch({ + "/enseignements": { + method: "POST", + status: 409, + body: ERROR_CONFLICT, + }, + }); + + try { + const res = await fetch("http://localhost/api/enseignements", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + idProf: 1, + idModule: "JIN702C", + idPromo: "4AFISE25/26", + }), + }); + assertEquals(res.status, 409); + const data = await res.json(); + assertEquals(data.error, "Ressource déjà existante"); + } finally { + restoreFetch(); + } +}); + +Deno.test( + "enseignements - GET 200 returns enseignement by composite key", + async () => { + const enseignement = enseignements[0]; + const path = "/enseignements/1/JIN702C/4AFISE25%2F26"; + + mockFetch({ + [path]: { + status: 200, + body: enseignement, + }, + }); + + try { + const res = await fetch( + "http://localhost/api/enseignements/1/JIN702C/4AFISE25%2F26", + ); + assertEquals(res.status, 200); + const data = await res.json(); + assertEquals(data.idProf, 1); + assertEquals(data.idModule, "JIN702C"); + assertEquals(data.idPromo, "4AFISE25/26"); + } finally { + restoreFetch(); + } + }, +); + +Deno.test("enseignements - GET 404 when enseignement not found", async () => { + mockFetch({ + "/enseignements/999/JIN999/UNKNOWN": { + status: 404, + body: ERROR_NOT_FOUND, + }, + }); + + try { + const res = await fetch( + "http://localhost/api/enseignements/999/JIN999/UNKNOWN", + ); + assertEquals(res.status, 404); + const data = await res.json(); + assertEquals(data.error, "Ressource introuvable"); + } finally { + restoreFetch(); + } +}); + +Deno.test("enseignements - DELETE 204 removes enseignement", async () => { + const path = "/enseignements/1/JIN702C/4AFISE25%2F26"; + + mockFetch({ + [path]: { + method: "DELETE", + status: 204, + }, + }); + + try { + const res = await fetch( + "http://localhost/api/enseignements/1/JIN702C/4AFISE25%2F26", + { + method: "DELETE", + }, + ); + assertEquals(res.status, 204); + assertEquals(res.body, null); + } finally { + restoreFetch(); + } +}); + +Deno.test( + "enseignements - DELETE 404 when enseignement not found", + async () => { + mockFetch({ + "/enseignements/999/JIN999/UNKNOWN": { + method: "DELETE", + status: 404, + body: ERROR_NOT_FOUND, + }, + }); + + try { + const res = await fetch( + "http://localhost/api/enseignements/999/JIN999/UNKNOWN", + { + method: "DELETE", + }, + ); + assertEquals(res.status, 404); + const data = await res.json(); + assertEquals(data.error, "Ressource introuvable"); + } finally { + restoreFetch(); + } + }, +); + +Deno.test("enseignements - mockDb operations", () => { + const db = createMockDb({ tables: { enseignements: [...enseignements] } }); + + // Test findOne + const found = db.findOne( + "enseignements", + (r) => + r.idProf === 1 && r.idModule === "JIN702C" && r.idPromo === "4AFISE25/26", + ); + assertExists(found); + assertEquals(found.idProf, 1); + + // Test insert + const newEnseignement: Enseignement = { + idProf: 3, + idModule: "JIN705C", + idPromo: "4AFISE25/26", + }; + db.insert("enseignements", newEnseignement); + assertEquals(db.getTable("enseignements").length, 4); + + // Test deleteWhere + const deleted = db.deleteWhere( + "enseignements", + (r) => + r.idProf === 1 && r.idModule === "JIN702C" && r.idPromo === "4AFISE25/26", + ); + assertEquals(deleted, 1); + assertEquals(db.getTable("enseignements").length, 3); +});