UNPKG

@auth0/nextjs-auth0

Version:
166 lines (165 loc) 6.94 kB
import { NextRequest, NextResponse } from "next/server.js"; import * as jose from "jose"; import { http, HttpResponse } from "msw"; import { setupServer } from "msw/node"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { Auth0Client } from "./client.js"; // Basic constants for testing const domain = "https://auth0.local"; const alg = "RS256"; const sub = "test-sub"; const sid = "test-sid"; const scope = "openid profile email offline_access"; const testAuth0ClientConfig = { domain, clientId: "test-client-id", clientSecret: "test-client-secret", appBaseUrl: "https://example.org", secret: "test-secret-long-enough-for-hs256-test-secret-long-enough-for-hs256" }; let keyPair; const refreshedAccessToken = "msw-refreshed-access-token"; const refreshedRefreshToken = "msw-refreshed-refresh-token"; const refreshedExpiresIn = 3600; const issuer = domain; const audience = testAuth0ClientConfig.clientId; const initialName = "initialName"; const updatedName = "updatedName"; const generateToken = async (claims) => await new jose.SignJWT({ sid, sub, auth_time: Math.floor(Date.now() / 1000), nonce: "nonce-value", jti: Date.now().toString(), ...(claims && { ...claims }) }) .setProtectedHeader({ alg }) .setIssuer(issuer) .setAudience(audience) .setIssuedAt() .setExpirationTime("1h") .sign(keyPair.privateKey); const handlers = [ // OIDC Discovery Endpoint http.get(`${domain}/.well-known/openid-configuration`, () => { return HttpResponse.json({ issuer: issuer, token_endpoint: `${domain}/oauth/token`, jwks_uri: `${domain}/.well-known/jwks.json` }); }), // JWKS Endpoint http.get(`${domain}/.well-known/jwks.json`, async () => { const jwk = await jose.exportJWK(keyPair.publicKey); return HttpResponse.json({ keys: [jwk] }); }), // Token Endpoint (for refresh token grant) http.post(`${domain}/oauth/token`, async ({ request }) => { const body = await request.formData(); if (body.get("grant_type") === "refresh_token" && body.get("refresh_token")) { return HttpResponse.json({ access_token: refreshedAccessToken, refresh_token: refreshedRefreshToken, id_token: await generateToken({ name: updatedName }), token_type: "Bearer", expires_in: refreshedExpiresIn, scope }); } // Fallback for unexpected grant types or errors return HttpResponse.json({ error: "invalid_grant", error_description: "Unsupported grant type" }, { status: 400 }); }) ]; const server = setupServer(...handlers); beforeAll(async () => { keyPair = await jose.generateKeyPair(alg); server.listen({ onUnhandledRequest: "error" }); }); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); /** * Creates initial session data for tests. */ async function createInitialSession() { return { user: { sub, name: initialName }, tokenSet: { accessToken: "test-access-token", refreshToken: "test-refresh-token", idToken: await generateToken({ name: initialName }), scope, expiresAt: Math.floor(Date.now() / 1000) + 3600 // Expires in 1 hour }, internal: { sid, createdAt: Date.now() / 1000 } }; } describe("Auth0Client - getAccessToken", () => { let mockSaveToSession; let auth0Client; beforeEach(async () => { // Instantiate Auth0Client normally, it will use intercepted fetch auth0Client = new Auth0Client(testAuth0ClientConfig); // Mock saveToSession to avoid cookie/request context issues mockSaveToSession = vi .spyOn(Auth0Client.prototype, "saveToSession") .mockResolvedValue(undefined); // Mock successful save const initialSession = await createInitialSession(); // Mock getSession specifically for this test vi.spyOn(Auth0Client.prototype, "getSession").mockResolvedValue(initialSession); }); afterEach(() => { vi.restoreAllMocks(); // Restore mocks after each test }); /** * Test Case: Pages Router Overload - getAccessToken(req, res, options) * Verifies that when called with req/res objects and refresh: true (with a valid token), * it refreshes the token. */ it("should refresh token and save session for pages-router overload when refresh is true (with valid token)", async () => { // Pages router overload requires req/res objects const mockReq = new NextRequest(`https://${testAuth0ClientConfig.appBaseUrl}/api/test`); const mockRes = new NextResponse(); // --- Execution --- const result = await auth0Client.getAccessToken(mockReq, mockRes, { refresh: true }); // --- Assertions --- expect(result).not.toBeNull(); expect(result?.token).toBe(refreshedAccessToken); // From msw handler // Check if expiresAt is close to the expected value based on the mock server response. // We use toBeCloseTo to account for minor timing differences between the client // calculating the expiration and the test assertion running. const expectedExpiresAtRough = Math.floor(Date.now() / 1000) + refreshedExpiresIn; // The '0' precision checks for equality at the integer second level. expect(result?.expiresAt).toBeCloseTo(expectedExpiresAtRough, 0); expect(mockSaveToSession).toHaveBeenCalledOnce(); // Verify user profile data is updated in saved session const savedSessionData = mockSaveToSession.mock.calls[0][0]; expect(savedSessionData.user.name).toBe(updatedName); }); /** * Test Case: App Router Overload - getAccessToken(options) * Verifies that when called without req/res objects and refresh: true (with a valid token), * it refreshes the token. */ it("should refresh token for app-router overload when refresh is true (with valid token)", async () => { // --- Execution --- const result = await auth0Client.getAccessToken({ refresh: true }); // --- Assertions --- expect(result).not.toBeNull(); expect(result?.token).toBe(refreshedAccessToken); const expectedExpiresAtRough = Math.floor(Date.now() / 1000) + refreshedExpiresIn; expect(result?.expiresAt).toBeCloseTo(expectedExpiresAtRough, 0); expect(mockSaveToSession).toHaveBeenCalledOnce(); // Verify user profile data is updated in saved session const savedSessionData = mockSaveToSession.mock.calls[0][0]; expect(savedSessionData.user.name).toBe(updatedName); }); });