import { NextRequest } from "next/server"; import { beforeAll, describe, expect, it, vi } from "vitest"; // Geprüft wird die Wegeführung im Proxy, nicht Auth.js. // // Der Wrapper `auth(handler)` gehört Auth.js und hängt die entschlüsselte // Sitzung als `request.auth` an; das ist fremder Code und hier durch eine // Attrappe ersetzt. Was hier zur Debatte steht, ist die Entscheidung, die // proxy.ts daraus ableitet — und die hat eine Schleife als Fehlerfall, die // sich im Betrieb als „die Anmeldeseite lädt endlos neu" zeigt und beim // Lesen des Codes leicht übersehen wird. // // Die Attrappe gibt den umschlossenen Handler bewusst als **Zusage** zurück, // weil Auth.js das in der Funktionsform ebenfalls tut. Eine Attrappe, die // ihn direkt liefert, wäre freundlicher als die Wirklichkeit und liesse ein // fehlendes `await` in proxy.ts durchgehen — genau der Fehler, der beim // ersten Ausprobieren jede Anfrage in einen 404 laufen liess. vi.mock("next-auth", () => ({ default: () => ({ auth: async (handler: (req: NextRequest) => unknown) => (req: NextRequest) => handler(req), }), })); type Session = { user: { id: string } } | null; // Bewusst locker getippt: Auth.js gibt dem umschlossenen Handler die // Signatur eines Route Handlers (Anfrage plus Kontext), und der zweite // Parameter spielt hier keine Rolle. let proxy: (req: NextRequest) => unknown; beforeAll(async () => { const mod = await import("@/proxy"); proxy = mod.proxy as unknown as typeof proxy; }); async function visit(path: string, session: Session): Promise { const request = new NextRequest(`http://localhost${path}`); Object.assign(request, { auth: session }); return (await proxy(request)) as Response; } const SIGNED_IN: Session = { user: { id: "11111111-1111-1111-1111-111111111111" } }; /** NextResponse.next() setzt diesen Header; eine Weiterleitung tut es nicht. */ function passedThrough(res: Response): boolean { return res.headers.get("x-middleware-next") === "1"; } describe("proxy", () => { it("schickt eine Anfrage ohne Sitzung auf die Anmeldeseite", async () => { const res = await visit("/employees", null); expect(res.status).toBe(307); expect(new URL(res.headers.get("location")!).pathname).toBe("/login"); }); it("lässt die Anmeldeseite ohne Sitzung durch", async () => { expect(passedThrough(await visit("/login", null))).toBe(true); }); it("lässt Auth.js' eigene Endpunkte ohne Sitzung durch", async () => { // Ohne diese Ausnahme landet der Rückweg von Entra auf /login und die // Anmeldung kommt nie zustande — der Code steht in der URL, aber es // entsteht nie ein Sitzungscookie. expect(passedThrough(await visit("/api/auth/callback/microsoft-entra-id", null))).toBe(true); expect(passedThrough(await visit("/api/auth/signin", null))).toBe(true); }); it("lässt eine angemeldete Anfrage durch", async () => { expect(passedThrough(await visit("/employees", SIGNED_IN))).toBe(true); }); it("holt eine angemeldete Person von der Anmeldeseite zurück in die Anwendung", async () => { const res = await visit("/login", SIGNED_IN); expect(res.status).toBe(307); expect(new URL(res.headers.get("location")!).pathname).toBe("/"); }); it("belässt eine angemeldete Person auf /login, solange eine Meldung ansteht", async () => { // Der Fall: angemeldet, aber ohne HR-Freischaltung. Das Layout leitet // nach /login?error=no_hr_access. Würde der Proxy auch das nach / // zurückschicken, liefen beide gegeneinander und der Grund wäre nie zu // lesen. expect(passedThrough(await visit("/login?error=no_hr_access", SIGNED_IN))).toBe(true); }); it("nimmt eine Sitzung ohne Kennung nicht als angemeldet", async () => { // Kommt vor, wenn das Token aus einer älteren Fassung stammt: gültig // signiert, aber ohne `uid`. Ohne diese Prüfung käme die Person durch // und liefe in Abfragen mit userId = null — die geben nichts zurück, und // die Anwendung sähe aus, als wären alle Daten weg. const res = await visit("/employees", { user: {} } as unknown as Session); expect(res.status).toBe(307); expect(new URL(res.headers.get("location")!).pathname).toBe("/login"); }); });