Files
TJWaterFrontend_Refine/src/app/api/auth/[...nextauth]/options.test.ts
T

112 lines
3.8 KiB
TypeScript

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;
};