diff --git a/agenda-mairie.test.ts b/agenda-mairie.test.ts new file mode 100644 index 0000000..26afc41 --- /dev/null +++ b/agenda-mairie.test.ts @@ -0,0 +1,117 @@ +import { describe, expect, test } from "bun:test"; +import { + calculerPeriode, + construireUrlPage, + recupererAgendaPublie, +} from "./extension/agenda-mairie.js"; + +function evenement(surcharges: Record = {}) { + return { + uid: "uid-1", + titre: "Atelier", + debut: new Date("2026-07-10T16:00:00Z"), + fin: new Date("2026-07-10T18:00:00Z"), + journeeEntiere: false, + ...surcharges, + }; +} + +describe("calculerPeriode", () => { + test("debut = mois d'aujourd'hui ; fin = mois du dernier début", () => { + const evenements = [ + evenement({ debut: new Date("2026-07-20T10:00:00Z") }), + evenement({ debut: new Date("2026-09-05T10:00:00Z") }), + evenement({ debut: new Date("2026-08-01T10:00:00Z") }), + ]; + const aujourdhui = new Date("2026-07-17T09:00:00Z"); + + expect(calculerPeriode(evenements, aujourdhui)).toEqual({ + debut: "07/2026", + fin: "09/2026", + }); + }); + + test("liste vide → période bornée au mois courant", () => { + const aujourdhui = new Date("2026-07-17T09:00:00Z"); + expect(calculerPeriode([], aujourdhui)).toEqual({ debut: "07/2026", fin: "07/2026" }); + }); +}); + +describe("construireUrlPage", () => { + const periode = { debut: "07/2026", fin: "09/2026" }; + + test("page 1 → URL de base sans segment /page/", () => { + const url = construireUrlPage(1, periode); + expect(url).toBe( + "https://www.ville-cugnaux.fr/mes-loisirs/agenda/?f=1&theme=&date=periode&date_debut=07%2F2026&date_fin=09%2F2026&public=", + ); + }); + + test("page 3 → segment /page/3/", () => { + const url = construireUrlPage(3, periode); + expect(url).toContain("/mes-loisirs/agenda/page/3/?f=1"); + expect(url).toContain("date_debut=07%2F2026"); + expect(url).toContain("date_fin=09%2F2026"); + }); +}); + +describe("recupererAgendaPublie", () => { + const periode = { debut: "07/2026", fin: "09/2026" }; + const reponseOk = (html: string) => Promise.resolve({ ok: true, text: () => Promise.resolve(html) }); + + test("3 pages non vides puis une vide → cartes agrégées, arrêt sur la vide", async () => { + const pages = [ + [{ date: "2026-07-01", titre: "A", lien: "l1" }], + [{ date: "2026-07-02", titre: "B", lien: "l2" }], + [{ date: "2026-07-03", titre: "C", lien: "l3" }], + [], + ]; + const fetchImpl = (url: string) => reponseOk(url); + let appel = 0; + const extraire = () => pages[appel++]; + + const res = await recupererAgendaPublie(fetchImpl, extraire, periode); + + expect(res).toEqual({ + ok: true, + cartes: [ + { date: "2026-07-01", titre: "A", lien: "l1" }, + { date: "2026-07-02", titre: "B", lien: "l2" }, + { date: "2026-07-03", titre: "C", lien: "l3" }, + ], + }); + expect(appel).toBe(4); // 3 pages pleines + la vide qui stoppe + }); + + test("plafond de sécurité respecté (extracteur toujours non vide)", async () => { + let appels = 0; + const fetchImpl = (url: string) => { + appels++; + return reponseOk(url); + }; + const extraire = () => [{ date: "2026-07-01", titre: "A", lien: "l" }]; + + const res = await recupererAgendaPublie(fetchImpl, extraire, periode); + + expect(res.ok).toBe(true); + expect(appels).toBe(12); // plafond + }); + + test("fetch qui throw → { ok:false } (jamais d'exception)", async () => { + const fetchImpl = () => Promise.reject(new Error("réseau")); + const extraire = () => []; + + const res = await recupererAgendaPublie(fetchImpl, extraire, periode); + + expect(res).toEqual({ ok: false }); + }); + + test("réponse HTTP non-ok (500) → { ok:false }", async () => { + const fetchImpl = () => Promise.resolve({ ok: false, text: () => Promise.resolve("") }); + const extraire = () => [{ date: "2026-07-01", titre: "A", lien: "l" }]; + + const res = await recupererAgendaPublie(fetchImpl, extraire, periode); + + expect(res).toEqual({ ok: false }); + }); +}); diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 1d0c67c..1f2064d 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -181,3 +181,40 @@ sont le vrai risque, invisible sous `TZ=Europe/Paris`) ; `formulaire-mairie.js` est fin, impur et **non testé unitairement** — aucun harnais DOM dans le repo, et happy-dom n'émulerait de toute façon pas Gravity Forms. C'est le test humain qui tranche. + +## 2026-07-17 — [T2] Publication mairie : dérivée, paginée, seam pur/impur + +Le statut *publié* est **dérivé** de l'agenda public de la mairie (pas de tag), +en retrouvant l'événement Nextcloud par **date locale de Cugnaux + titre +normalisé**. Il donne aussi le **lien direct** (lu dans la carte). + +**Corrections factuelles vérifiées en phase plan :** + +- **L'agenda est PAGINÉ** : `.../agenda/page/N/?f=1&…`. Un seul fetch « période » + ne suffit pas → on boucle jusqu'à une page vide (plafond de sécurité anti-boucle). +- **Le slug porte des suffixes WordPress** (« Atelier dessin » → + `/agenda/atelier-dessin-2/`) : on matche sur **date + titre**, jamais sur le + slug. Le lien est **lu** dans la carte, jamais reconstruit. +- `