feat: read raw iCalendar feed via browser session
Isolated CalDAV transport module that reuses the browser's Nextcloud session (cookies, no stored password). Returns a discriminated result and detects the HTTP-200 login-page redirect by sniffing the body for BEGIN:VCALENDAR. compterEvenements counts real VEVENT line starts as the T1 proof of life. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4889a9931a
commit
1432aba561
@@ -0,0 +1,38 @@
|
|||||||
|
// Transport CalDAV isolé : récupère le flux iCalendar brut du calendrier
|
||||||
|
// Nextcloud de l'Atelier du Huit en réutilisant la session du navigateur
|
||||||
|
// (cookies). Aucun mot de passe stocké. Le module reste pur de toute
|
||||||
|
// présentation : il renvoie un *type* d'erreur, pas un message.
|
||||||
|
|
||||||
|
export const URL_CALENDRIER =
|
||||||
|
"https://atelier-huit.frama.space/remote.php/dav/calendars/Pierre/latelier-du-huit-sbastien_shared_by_admin/?export";
|
||||||
|
|
||||||
|
// fetchImpl est injecté (obligatoire) pour la testabilité.
|
||||||
|
// Résultat discriminé : { ok:true, flux } | { ok:false, erreur }.
|
||||||
|
export async function recupererFluxCalendrier(fetchImpl) {
|
||||||
|
let reponse;
|
||||||
|
try {
|
||||||
|
reponse = await fetchImpl(URL_CALENDRIER, { credentials: "include" });
|
||||||
|
} catch {
|
||||||
|
return { ok: false, erreur: "reseau" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reponse.status === 401 || reponse.status === 403) {
|
||||||
|
return { ok: false, erreur: "non-connecte" };
|
||||||
|
}
|
||||||
|
if (!reponse.ok) {
|
||||||
|
return { ok: false, erreur: "serveur" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const flux = await reponse.text();
|
||||||
|
// Piège central : une redirection vers la page de login renvoie un 200 HTML.
|
||||||
|
if (!flux.trimStart().startsWith("BEGIN:VCALENDAR")) {
|
||||||
|
return { ok: false, erreur: "non-connecte" };
|
||||||
|
}
|
||||||
|
return { ok: true, flux };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preuve de vie de T1 : compte les vrais débuts de VEVENT (ancrage ligne, pas
|
||||||
|
// une chaîne « BEGIN:VEVENT » apparaissant dans une DESCRIPTION).
|
||||||
|
export function compterEvenements(flux) {
|
||||||
|
return (flux.match(/^BEGIN:VEVENT\s*$/gm) ?? []).length;
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import {
|
||||||
|
URL_CALENDRIER,
|
||||||
|
compterEvenements,
|
||||||
|
recupererFluxCalendrier,
|
||||||
|
} from "./extension/nextcloud.js";
|
||||||
|
|
||||||
|
const FLUX_VALIDE =
|
||||||
|
"BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Atelier\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n";
|
||||||
|
|
||||||
|
function fetchQuiRenvoie(corps: string, status = 200) {
|
||||||
|
return async () => new Response(corps, { status });
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("recupererFluxCalendrier", () => {
|
||||||
|
test("succès : 200 + VCALENDAR → { ok:true, flux }", async () => {
|
||||||
|
const res = await recupererFluxCalendrier(fetchQuiRenvoie(FLUX_VALIDE));
|
||||||
|
expect(res).toEqual({ ok: true, flux: FLUX_VALIDE });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("non-connecté : 200 + page de login HTML → non-connecte", async () => {
|
||||||
|
const res = await recupererFluxCalendrier(
|
||||||
|
fetchQuiRenvoie("<!DOCTYPE html><html><body>Login</body></html>"),
|
||||||
|
);
|
||||||
|
expect(res).toEqual({ ok: false, erreur: "non-connecte" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("non-connecté : status 401 → non-connecte", async () => {
|
||||||
|
const res = await recupererFluxCalendrier(fetchQuiRenvoie("", 401));
|
||||||
|
expect(res).toEqual({ ok: false, erreur: "non-connecte" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("non-connecté : status 403 → non-connecte", async () => {
|
||||||
|
const res = await recupererFluxCalendrier(fetchQuiRenvoie("", 403));
|
||||||
|
expect(res).toEqual({ ok: false, erreur: "non-connecte" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("serveur : status 500 → serveur", async () => {
|
||||||
|
const res = await recupererFluxCalendrier(fetchQuiRenvoie("", 500));
|
||||||
|
expect(res).toEqual({ ok: false, erreur: "serveur" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("réseau : fetch lève → reseau", async () => {
|
||||||
|
const res = await recupererFluxCalendrier(async () => {
|
||||||
|
throw new TypeError("Failed to fetch");
|
||||||
|
});
|
||||||
|
expect(res).toEqual({ ok: false, erreur: "reseau" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("BOM / espaces en tête : reste un succès", async () => {
|
||||||
|
const corps = "\r\n" + FLUX_VALIDE;
|
||||||
|
const res = await recupererFluxCalendrier(fetchQuiRenvoie(corps));
|
||||||
|
expect(res).toEqual({ ok: true, flux: corps });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("appelle fetchImpl avec URL_CALENDRIER et credentials:include", async () => {
|
||||||
|
let urlVue: unknown;
|
||||||
|
let optionsVues: unknown;
|
||||||
|
await recupererFluxCalendrier(async (url, options) => {
|
||||||
|
urlVue = url;
|
||||||
|
optionsVues = options;
|
||||||
|
return new Response(FLUX_VALIDE);
|
||||||
|
});
|
||||||
|
expect(urlVue).toBe(URL_CALENDRIER);
|
||||||
|
expect(optionsVues).toEqual({ credentials: "include" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("compterEvenements", () => {
|
||||||
|
test("compte 2 VEVENT", () => {
|
||||||
|
const flux =
|
||||||
|
"BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n";
|
||||||
|
expect(compterEvenements(flux)).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("compte 0 quand aucun VEVENT", () => {
|
||||||
|
expect(compterEvenements("BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n")).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ignore un BEGIN:VEVENT au milieu d'une DESCRIPTION", () => {
|
||||||
|
const flux =
|
||||||
|
"BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDESCRIPTION:texte BEGIN:VEVENT piège\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n";
|
||||||
|
expect(compterEvenements(flux)).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user