// Mocks the admin API via cy.intercept — no live backend. const adminBody = { id: 1, email: "ops@example.com", name: "Ops", createdAt: "2026-08-01T00:00:00.000Z", lastLoginAt: "2026-08-28T09:00:00.000Z", }; function monitoringFixture() { return { generatedAt: "2026-08-28T09:15:00.000Z", services: [ { key: "postgres", status: "up", latencyMs: 3.2, detail: null, checkedAt: "2026-08-28T09:15:00.000Z", }, { key: "api", status: "up", latencyMs: 0, detail: "uptime 3 h 12 min · RSS 120 Mo", checkedAt: "2026-08-28T09:15:00.000Z", }, { key: "intent-service", status: "down", latencyMs: null, detail: "fetch failed", checkedAt: "2026-08-28T09:15:00.000Z", }, { key: "tech-step-llm-worker", status: "degraded", latencyMs: null, detail: "dernier battement il y a 9 j", checkedAt: "2026-08-28T09:15:00.000Z", lastRunAt: "2026-08-19T03:00:00.000Z", lastResult: { job: "audit-low-confidence", ok: true, counts: { suggestions: 2 } }, }, ], }; } describe("Admin monitoring", () => { beforeEach(() => { cy.viewport(1400, 900); cy.intercept("GET", "**/admin/api/auth/me", { statusCode: 200, body: adminBody }); }); // Plain URL/method matcher is enough here: the API's `/admin/api/monitoring` // can no longer collide with the page route `/admin/monitoring` (see // `mountRouter("/admin/api", ...)`'s comment in the API's `app.ts` for the // incident that made that distinction necessary in the first place). This // used to need a `resourceType`-based workaround for that collision, which // also surfaced an unrelated Cypress bug: on this machine's Chrome build, a // *declarative* `resourceType` route matcher silently never caught a // request whose own `req.resourceType` genuinely was that type — worth // remembering if a `resourceType` matcher elsewhere in this suite ever // starts failing the same way. function interceptMonitoringApi(response: { statusCode: number; body: unknown }) { return cy.intercept("GET", "**/admin/api/monitoring", response); } it("renders one card per service with its status and details", () => { interceptMonitoringApi({ statusCode: 200, body: monitoringFixture() }).as("getMonitoring"); cy.visit("/admin/monitoring"); cy.wait("@getMonitoring"); cy.get(".monitoring-card").should("have.length", 4); cy.contains(".monitoring-card", "Base de données") .should("have.class", "monitoring-card--up") .and("contain.text", "3.2 ms"); cy.contains(".monitoring-card", "Service NLP (spaCy)") .should("have.class", "monitoring-card--down") .and("contain.text", "Hors service"); cy.contains(".monitoring-card", "Worker LLM") .should("have.class", "monitoring-card--degraded") .and("contain.text", "audit-low-confidence"); }); it("shows an error state when the request fails", () => { // Aliased and waited on (unlike the other tests' plain intercepts) so // this genuinely proves the 500 came from the mock — without a live API // server, an unmatched request fails at the network level too, which // `MonitoringPage`'s `.catch()` can't tell apart from a real 500 (see // the comment above `interceptMonitoringApi`, this is exactly the class // of bug that let this test pass for the wrong reason before). interceptMonitoringApi({ statusCode: 500, body: { code: 5000, message: "x" } }).as( "getMonitoringError", ); cy.visit("/admin/monitoring"); cy.wait("@getMonitoringError"); cy.contains("Impossible de charger").should("be.visible"); }); });