import authOptions from "./options"; describe("NextAuth access token refresh", () => { it("forces a Keycloak refresh for an agent credential request", async () => { const previousEnv = { issuer: process.env.KEYCLOAK_ISSUER, clientId: process.env.KEYCLOAK_CLIENT_ID, clientSecret: process.env.KEYCLOAK_CLIENT_SECRET, }; process.env.KEYCLOAK_ISSUER = "https://keycloak.example/realms/tjwater"; process.env.KEYCLOAK_CLIENT_ID = "frontend"; process.env.KEYCLOAK_CLIENT_SECRET = "secret"; const originalFetch = globalThis.fetch; const fetchMock = jest.fn().mockResolvedValue({ ok: true, json: async () => ({ access_token: "fresh-access-token", expires_in: 900, refresh_token: "rotated-refresh-token", }), }); globalThis.fetch = fetchMock as typeof fetch; try { const jwt = authOptions.callbacks?.jwt as (input: unknown) => Promise<{ accessToken?: string; refreshToken?: string; }>; const result = await jwt({ token: { accessToken: "still-fresh-access-token", accessTokenExpires: Date.now() + 600_000, accessTokenIssuedAt: Date.now(), refreshToken: "refresh-token", }, trigger: "update", session: { forceRefresh: true }, }); expect(result.accessToken).toBe("fresh-access-token"); expect(result.refreshToken).toBe("rotated-refresh-token"); expect(fetchMock).toHaveBeenCalledWith( "https://keycloak.example/realms/tjwater/protocol/openid-connect/token", expect.objectContaining({ method: "POST" }), ); } finally { if (originalFetch) globalThis.fetch = originalFetch; else delete (globalThis as { fetch?: typeof fetch }).fetch; restoreEnv("KEYCLOAK_ISSUER", previousEnv.issuer); restoreEnv("KEYCLOAK_CLIENT_ID", previousEnv.clientId); restoreEnv("KEYCLOAK_CLIENT_SECRET", previousEnv.clientSecret); } }); it.each([ ["network failure", () => Promise.reject(new Error("connection refused"))], [ "non-JSON response", () => Promise.resolve({ ok: false, json: async () => { throw new SyntaxError("Unexpected token"); }, }), ], [ "invalid token payload", () => Promise.resolve({ ok: true, json: async () => ({ access_token: " ", expires_in: 0 }), }), ], ])("returns a session error for %s", async (_label, fetchResult) => { const previousEnv = { issuer: process.env.KEYCLOAK_ISSUER, clientId: process.env.KEYCLOAK_CLIENT_ID, clientSecret: process.env.KEYCLOAK_CLIENT_SECRET, }; process.env.KEYCLOAK_ISSUER = "https://keycloak.example/realms/tjwater"; process.env.KEYCLOAK_CLIENT_ID = "frontend"; process.env.KEYCLOAK_CLIENT_SECRET = "secret"; const originalFetch = globalThis.fetch; globalThis.fetch = jest.fn(fetchResult) as unknown as typeof fetch; try { const jwt = authOptions.callbacks?.jwt as (input: unknown) => Promise<{ error?: string; }>; await expect( jwt({ token: { refreshToken: "refresh-token" }, trigger: "update", session: { forceRefresh: true }, }), ).resolves.toMatchObject({ error: "RefreshAccessTokenError" }); } finally { if (originalFetch) globalThis.fetch = originalFetch; else delete (globalThis as { fetch?: typeof fetch }).fetch; restoreEnv("KEYCLOAK_ISSUER", previousEnv.issuer); restoreEnv("KEYCLOAK_CLIENT_ID", previousEnv.clientId); restoreEnv("KEYCLOAK_CLIENT_SECRET", previousEnv.clientSecret); } }); }); const restoreEnv = (key: string, value: string | undefined) => { if (value === undefined) delete process.env[key]; else process.env[key] = value; };