// Mocks the API via cy.intercept — this job doesn't run a live backend (see // .github/workflows/ci.yml). apps/api's own Mocha suite covers the real // 401 behavior (an expired/invalid JWT -> `require-auth` -> 401 // NOT_AUTHENTICATED); this only covers the *web client's* reaction to it. import { ErrorCode } from "@batch-cooking/shared"; const authenticatedProfile = { id: 1, firstName: "Alice", lastName: "Martin", email: "alice@example.com", tokenVersion: 0, houseId: 1, dietId: null, }; // Issue #108 — a session that expired (or was invalidated) server-side // mid-use only took effect on the next full reload: any other request just // surfaced a local error while the authenticated UI stayed put. `ApiClient` // now reacts to a `401 NOT_AUTHENTICATED` on any request other than the // initial `/auth/me` probe by dropping the session and bouncing to // `/login`. describe("Session expiry", () => { it("bounces to /login when a request 401s mid-session", () => { cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); // Logged in, planning loads fine. cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null }).as("getPlanning"); cy.visit("/"); cy.wait("@getPlanning"); cy.contains("h1", "Planning de la semaine").should("be.visible"); // The session dies server-side: every endpoint now 401s, including the // `/auth/me` probe a fresh load would make. const unauthenticated = { statusCode: 401, body: { code: ErrorCode.NOT_AUTHENTICATED, message: "not authenticated" }, }; cy.intercept("GET", "**/auth/me", unauthenticated); cy.intercept("GET", /\/planning\?/, unauthenticated); // The next planning fetch (a week-nav click) 401s -> bounced to /login. cy.get(".week-nav__arrow").last().click(); cy.url().should("include", "/login"); // ...and it's a real logout: reloading a guarded route redirects again // rather than showing stale authed content. cy.visit("/recettes"); cy.url().should("include", "/login"); }); it("does not bounce a logged-out visitor — the initial /auth/me 401 is the normal state", () => { cy.intercept("GET", "**/auth/me", { statusCode: 401, body: { code: ErrorCode.NOT_AUTHENTICATED, message: "not authenticated" }, }); cy.visit("/"); // `/` for a logged-out visitor is the public landing page (issue #41), // not a redirect to /login — the `/auth/me` probe 401 must not trip the // session-expiry handler. cy.contains("a", "Se connecter").should("be.visible"); cy.url().should("not.include", "/login"); }); });