@auth0/nextjs-auth0
Version:
Auth0 Next.js SDK
249 lines (248 loc) • 12.7 kB
JavaScript
import { NextResponse } from "next/server.js";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { AccessTokenError, AccessTokenErrorCode } from "../errors/index.js";
import { AuthClient } from "./auth-client.js"; // Import the actual class for spyOn
import { Auth0Client } from "./client.js";
// Define ENV_VARS at the top level for broader scope
const ENV_VARS = {
DOMAIN: "AUTH0_DOMAIN",
CLIENT_ID: "AUTH0_CLIENT_ID",
CLIENT_SECRET: "AUTH0_CLIENT_SECRET",
CLIENT_ASSERTION_SIGNING_KEY: "AUTH0_CLIENT_ASSERTION_SIGNING_KEY",
APP_BASE_URL: "APP_BASE_URL",
SECRET: "AUTH0_SECRET",
SCOPE: "AUTH0_SCOPE"
};
describe("Auth0Client", () => {
// Store original env vars
const originalEnv = { ...process.env };
// Clear env vars before each test
beforeEach(() => {
vi.resetModules();
// Clear all environment variables that might affect the tests
delete process.env[ENV_VARS.DOMAIN];
delete process.env[ENV_VARS.CLIENT_ID];
delete process.env[ENV_VARS.CLIENT_SECRET];
delete process.env[ENV_VARS.CLIENT_ASSERTION_SIGNING_KEY];
delete process.env[ENV_VARS.APP_BASE_URL];
delete process.env[ENV_VARS.SECRET];
delete process.env[ENV_VARS.SCOPE];
});
// Restore env vars after each test
afterEach(() => {
process.env = { ...originalEnv };
vi.restoreAllMocks(); // Restore mocks created within tests/beforeEach
});
describe("constructor validation", () => {
it("should accept clientSecret as authentication method", () => {
// Set required environment variables with clientSecret
process.env[ENV_VARS.DOMAIN] = "env.auth0.com";
process.env[ENV_VARS.CLIENT_ID] = "env_client_id";
process.env[ENV_VARS.CLIENT_SECRET] = "env_client_secret";
process.env[ENV_VARS.APP_BASE_URL] = "https://myapp.com";
process.env[ENV_VARS.SECRET] = "env_secret";
// Should not throw
const client = new Auth0Client();
// The client should be instantiated successfully
expect(client).toBeInstanceOf(Auth0Client);
});
it("should accept clientAssertionSigningKey as authentication method", () => {
// Set required environment variables with clientAssertionSigningKey instead of clientSecret
process.env[ENV_VARS.DOMAIN] = "env.auth0.com";
process.env[ENV_VARS.CLIENT_ID] = "env_client_id";
process.env[ENV_VARS.CLIENT_ASSERTION_SIGNING_KEY] = "some-signing-key";
process.env[ENV_VARS.APP_BASE_URL] = "https://myapp.com";
process.env[ENV_VARS.SECRET] = "env_secret";
// Should not throw
const client = new Auth0Client();
// The client should be instantiated successfully
expect(client).toBeInstanceOf(Auth0Client);
});
it("should prioritize options over environment variables", () => {
// Set environment variables
process.env[ENV_VARS.DOMAIN] = "env.auth0.com";
process.env[ENV_VARS.CLIENT_ID] = "env_client_id";
process.env[ENV_VARS.CLIENT_SECRET] = "env_client_secret";
process.env[ENV_VARS.APP_BASE_URL] = "https://myapp.com";
process.env[ENV_VARS.SECRET] = "env_secret";
// Provide conflicting options
const options = {
domain: "options.auth0.com",
clientId: "options_client_id",
clientSecret: "options_client_secret",
appBaseUrl: "https://options-app.com",
secret: "options_secret"
};
// Mock the validateAndExtractRequiredOptions to verify which values are used
const mockValidateAndExtractRequiredOptions = vi
.fn()
.mockReturnValue(options);
const originalValidateAndExtractRequiredOptions = Auth0Client.prototype["validateAndExtractRequiredOptions"];
Auth0Client.prototype["validateAndExtractRequiredOptions"] =
mockValidateAndExtractRequiredOptions;
try {
new Auth0Client(options);
// Check that validateAndExtractRequiredOptions was called with our options
expect(mockValidateAndExtractRequiredOptions).toHaveBeenCalledWith(options);
// The first argument of the first call should be our options object
const passedOptions = mockValidateAndExtractRequiredOptions.mock.calls[0][0];
expect(passedOptions.domain).toBe("options.auth0.com");
expect(passedOptions.clientId).toBe("options_client_id");
}
finally {
// Restore the original method
Auth0Client.prototype["validateAndExtractRequiredOptions"] =
originalValidateAndExtractRequiredOptions;
}
});
});
describe("getAccessToken", () => {
const mockSession = {
user: { sub: "user123" },
tokenSet: {
accessToken: "old_access_token",
idToken: "old_id_token",
refreshToken: "old_refresh_token",
expiresAt: Date.now() / 1000 - 3600 // Expired
},
internal: {
sid: "mock_sid",
createdAt: Date.now() / 1000 - 7200 // Some time in the past
},
createdAt: Date.now() / 1000
};
// Restore original mock for refreshed token set
const mockRefreshedTokenSet = {
accessToken: "new_access_token",
idToken: "new_id_token",
refreshToken: "new_refresh_token",
expiresAt: Date.now() / 1000 + 3600, // Not expired
scope: "openid profile email"
};
let client;
let mockGetSession;
let mockSaveToSession;
let mockGetTokenSet; // Re-declare mockGetTokenSet
beforeEach(() => {
// Reset mocks specifically if vi.restoreAllMocks isn't enough
// vi.resetAllMocks(); // Alternative to restoreAllMocks in afterEach
// Set necessary environment variables
process.env[ENV_VARS.DOMAIN] = "test.auth0.com";
process.env[ENV_VARS.CLIENT_ID] = "test_client_id";
process.env[ENV_VARS.CLIENT_SECRET] = "test_client_secret";
process.env[ENV_VARS.APP_BASE_URL] = "https://myapp.test";
process.env[ENV_VARS.SECRET] = "test_secret";
client = new Auth0Client();
// Mock internal methods of Auth0Client
mockGetSession = vi
.spyOn(Auth0Client.prototype, "getSession")
.mockResolvedValue(mockSession);
mockSaveToSession = vi
.spyOn(Auth0Client.prototype, "saveToSession")
.mockResolvedValue(undefined);
// Restore mocking of getTokenSet directly
mockGetTokenSet = vi
.spyOn(AuthClient.prototype, "getTokenSet")
.mockResolvedValue([null, mockRefreshedTokenSet]); // Simulate successful refresh
// Remove mocks for discoverAuthorizationServerMetadata and getClientAuth
// Remove fetch mock
});
it("should throw AccessTokenError if no session exists", async () => {
// Override getSession mock for this specific test
mockGetSession.mockResolvedValue(null);
// Mock request and response objects
const mockReq = { headers: new Headers() };
const mockRes = new NextResponse();
await expect(client.getAccessToken(mockReq, mockRes)).rejects.toThrowError(new AccessTokenError(AccessTokenErrorCode.MISSING_SESSION, "The user does not have an active session."));
// Ensure getTokenSet was not called
expect(mockGetTokenSet).not.toHaveBeenCalled();
});
it("should throw error from getTokenSet if refresh fails", async () => {
const refreshError = new Error("Refresh failed");
// Restore overriding the getTokenSet mock directly
mockGetTokenSet.mockResolvedValue([refreshError, null]);
// Mock request and response objects
const mockReq = { headers: new Headers() };
const mockRes = new NextResponse();
await expect(client.getAccessToken(mockReq, mockRes, { refresh: true })).rejects.toThrowError(refreshError);
// Verify save was not called
expect(mockSaveToSession).not.toHaveBeenCalled();
});
});
describe("constructor configuration", () => {
beforeEach(() => {
// Set necessary environment variables
process.env[ENV_VARS.DOMAIN] = "test.auth0.com";
process.env[ENV_VARS.CLIENT_ID] = "test_client_id";
process.env[ENV_VARS.CLIENT_SECRET] = "test_client_secret";
process.env[ENV_VARS.APP_BASE_URL] = "https://myapp.test";
process.env[ENV_VARS.SECRET] = "test_secret";
});
it("should pass transactionCookie.maxAge to TransactionStore", () => {
const customMaxAge = 1800; // 30 minutes
const client = new Auth0Client({
transactionCookie: {
maxAge: customMaxAge
}
});
// Verify that the TransactionStore was created with the correct maxAge
// We need to access the private property for testing
const transactionStore = client.transactionStore;
expect(transactionStore).toBeDefined();
// Check the cookieOptions maxAge - we need to verify it was set correctly
const cookieOptions = transactionStore.cookieOptions;
expect(cookieOptions.maxAge).toBe(customMaxAge);
});
it("should use default maxAge of 3600 when not specified", () => {
const client = new Auth0Client();
// Verify that the TransactionStore was created with the default maxAge
const transactionStore = client.transactionStore;
expect(transactionStore).toBeDefined();
// Check the cookieOptions maxAge
const cookieOptions = transactionStore.cookieOptions;
expect(cookieOptions.maxAge).toBe(3600);
});
it("should pass other transactionCookie options to TransactionStore", () => {
const customOptions = {
prefix: "__custom_txn_",
secure: true,
sameSite: "strict",
path: "/auth",
maxAge: 2700
};
const client = new Auth0Client({
transactionCookie: customOptions
});
// Verify that the TransactionStore was created with the correct options
const transactionStore = client.transactionStore;
expect(transactionStore).toBeDefined();
const cookieOptions = transactionStore.cookieOptions;
expect(cookieOptions.maxAge).toBe(customOptions.maxAge);
expect(transactionStore.transactionCookiePrefix).toBe(customOptions.prefix);
// Note: secure and sameSite are stored in cookieOptions
expect(cookieOptions.secure).toBe(customOptions.secure);
expect(cookieOptions.sameSite).toBe(customOptions.sameSite);
expect(cookieOptions.path).toBe(customOptions.path);
});
it("should pass enableParallelTransactions to TransactionStore", () => {
const client = new Auth0Client({
enableParallelTransactions: false
});
// Verify that the TransactionStore was created with the correct enableParallelTransactions
const transactionStore = client.transactionStore;
expect(transactionStore).toBeDefined();
const enableParallelTransactions = transactionStore
.enableParallelTransactions;
expect(enableParallelTransactions).toBe(false);
});
it("should default enableParallelTransactions to true when not specified", () => {
const client = new Auth0Client();
// Verify that the TransactionStore was created with the default enableParallelTransactions
const transactionStore = client.transactionStore;
expect(transactionStore).toBeDefined();
const enableParallelTransactions = transactionStore
.enableParallelTransactions;
expect(enableParallelTransactions).toBe(true);
});
});
});