Files
gridpilot.gg/apps/website/lib/gateways/AuthGateway.test.ts
2026-01-01 22:46:59 +01:00

350 lines
10 KiB
TypeScript

/**
* TDD Tests for AuthGateway
*
* These tests verify the authentication gateway logic following TDD principles:
* 1. Write failing tests first
* 2. Implement minimal code to pass
* 3. Refactor while keeping tests green
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { AuthGateway, AuthGatewayConfig } from './AuthGateway';
import type { AuthContextValue } from '@/lib/auth/AuthContext';
import type { SessionViewModel } from '@/lib/view-models/SessionViewModel';
// Mock SessionViewModel factory
function createMockSession(overrides: Partial<SessionViewModel> = {}): SessionViewModel {
const baseSession = {
isAuthenticated: true,
userId: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
role: undefined,
};
// Handle the case where overrides might have a user object
// (for backward compatibility with existing test patterns)
if (overrides.user) {
const { user, ...rest } = overrides;
return {
...baseSession,
...rest,
userId: user.userId || baseSession.userId,
email: user.email || baseSession.email,
displayName: user.displayName || baseSession.displayName,
role: user.role,
};
}
return {
...baseSession,
...overrides,
};
}
// Mock AuthContext factory
function createMockAuthContext(overrides: Partial<AuthContextValue> = {}): AuthContextValue {
return {
session: null,
loading: false,
login: async () => {},
logout: async () => {},
refreshSession: async () => {},
...overrides,
};
}
describe('AuthGateway', () => {
describe('Basic Authentication', () => {
it('should allow access when user is authenticated with no role requirements', () => {
const authContext = createMockAuthContext({
session: createMockSession(),
});
const gateway = new AuthGateway(authContext, {});
expect(gateway.canAccess()).toBe(true);
expect(gateway.isAuthenticated()).toBe(true);
expect(gateway.isLoading()).toBe(false);
});
it('should deny access when user is not authenticated', () => {
const authContext = createMockAuthContext({
session: null,
});
const gateway = new AuthGateway(authContext, {});
expect(gateway.canAccess()).toBe(false);
expect(gateway.isAuthenticated()).toBe(false);
expect(gateway.isLoading()).toBe(false);
});
it('should deny access when auth context is loading', () => {
const authContext = createMockAuthContext({
session: null,
loading: true,
});
const gateway = new AuthGateway(authContext, {});
expect(gateway.canAccess()).toBe(false);
expect(gateway.isLoading()).toBe(true);
});
});
describe('Role-Based Access Control', () => {
// Note: AuthorizationBlocker currently returns 'enabled' for all authenticated users
// in demo mode. These tests document the intended behavior for when role-based
// access control is fully implemented.
it('should allow access when user has required role', () => {
const authContext = createMockAuthContext({
session: createMockSession({
user: {
userId: 'user-123',
email: 'admin@example.com',
displayName: 'Admin User',
role: 'admin',
},
}),
});
const gateway = new AuthGateway(authContext, {
requiredRoles: ['admin'],
});
expect(gateway.canAccess()).toBe(true);
});
it('should deny access when user lacks required role', () => {
const authContext = createMockAuthContext({
session: createMockSession({
user: {
userId: 'user-123',
email: 'user@example.com',
displayName: 'Regular User',
role: 'user',
},
}),
});
const gateway = new AuthGateway(authContext, {
requiredRoles: ['admin'],
});
expect(gateway.canAccess()).toBe(false);
expect(gateway.getBlockMessage()).toContain('admin');
});
});
describe('Redirect Configuration', () => {
it('should use default redirect path when not specified', () => {
const authContext = createMockAuthContext({
session: null,
});
const gateway = new AuthGateway(authContext, {});
expect(gateway.getUnauthorizedRedirectPath()).toBe('/auth/login');
});
it('should use custom redirect path when specified', () => {
const authContext = createMockAuthContext({
session: null,
});
const gateway = new AuthGateway(authContext, {
unauthorizedRedirectPath: '/custom-login',
});
expect(gateway.getUnauthorizedRedirectPath()).toBe('/custom-login');
});
it('should respect redirectOnUnauthorized configuration', () => {
const authContext = createMockAuthContext({
session: null,
});
const gateway = new AuthGateway(authContext, {
redirectOnUnauthorized: false,
});
expect(gateway.redirectIfUnauthorized()).toBe(false);
});
it('should indicate redirect is needed when unauthorized and redirect enabled', () => {
const authContext = createMockAuthContext({
session: null,
});
const gateway = new AuthGateway(authContext, {
redirectOnUnauthorized: true,
});
expect(gateway.redirectIfUnauthorized()).toBe(true);
});
});
describe('Access State', () => {
it('should return complete access state', () => {
const authContext = createMockAuthContext({
session: createMockSession(),
});
const gateway = new AuthGateway(authContext, {});
const state = gateway.getAccessState();
expect(state).toEqual({
canAccess: true,
reason: 'Access granted',
isLoading: false,
isAuthenticated: true,
});
});
it('should return loading state correctly', () => {
const authContext = createMockAuthContext({
session: null,
loading: true,
});
const gateway = new AuthGateway(authContext, {});
const state = gateway.getAccessState();
expect(state.isLoading).toBe(true);
expect(state.canAccess).toBe(false);
});
});
describe('Session Refresh', () => {
it('should update access state after session refresh', () => {
const authContext = createMockAuthContext({
session: null,
});
const gateway = new AuthGateway(authContext, {});
expect(gateway.canAccess()).toBe(false);
// Simulate session refresh
authContext.session = createMockSession();
gateway.refresh();
expect(gateway.canAccess()).toBe(true);
expect(gateway.isAuthenticated()).toBe(true);
});
});
describe('Edge Cases', () => {
it('should handle undefined session gracefully', () => {
const authContext = createMockAuthContext({
session: undefined as any,
});
const gateway = new AuthGateway(authContext, {});
expect(gateway.canAccess()).toBe(false);
expect(gateway.isAuthenticated()).toBe(false);
});
it('should handle empty required roles array', () => {
const authContext = createMockAuthContext({
session: createMockSession(),
});
const gateway = new AuthGateway(authContext, {
requiredRoles: [],
});
expect(gateway.canAccess()).toBe(true);
});
it('should handle session with no user object', () => {
const authContext = createMockAuthContext({
session: {
isAuthenticated: true,
user: null as any,
},
});
const gateway = new AuthGateway(authContext, {});
expect(gateway.canAccess()).toBe(true); // Authenticated but no user
expect(gateway.isAuthenticated()).toBe(true);
});
it('should handle case sensitivity in role matching', () => {
const authContext = createMockAuthContext({
session: createMockSession({
user: {
userId: 'user-123',
email: 'admin@example.com',
displayName: 'Admin User',
role: 'ADMIN', // uppercase
},
}),
});
const gateway = new AuthGateway(authContext, {
requiredRoles: ['admin'], // lowercase
});
// Role matching is case-sensitive
expect(gateway.canAccess()).toBe(false);
expect(gateway.getBlockMessage()).toContain('admin');
});
});
describe('Error Handling', () => {
it('should throw error when enforceAccess is called without access', () => {
const authContext = createMockAuthContext({
session: null,
});
const gateway = new AuthGateway(authContext, {});
expect(() => gateway.enforceAccess()).toThrow('Access denied');
});
it('should not throw error when enforceAccess is called with access', () => {
const authContext = createMockAuthContext({
session: createMockSession(),
});
const gateway = new AuthGateway(authContext, {});
expect(() => gateway.enforceAccess()).not.toThrow();
});
});
describe('Block Messages', () => {
it('should provide appropriate block message for unauthenticated user', () => {
const authContext = createMockAuthContext({
session: null,
});
const gateway = new AuthGateway(authContext, {});
const message = gateway.getBlockMessage();
// Current behavior: AuthorizationBlocker returns "You must be logged in to access this area."
expect(message).toContain('logged in');
});
it('should provide appropriate block message for missing roles', () => {
const authContext = createMockAuthContext({
session: createMockSession({
user: {
userId: 'user-123',
email: 'user@example.com',
displayName: 'Regular User',
role: 'user',
},
}),
});
const gateway = new AuthGateway(authContext, {
requiredRoles: ['admin'],
});
const canAccess = gateway.canAccess();
const state = gateway.getAccessState();
expect(canAccess).toBe(false);
expect(state.reason).toContain('admin');
});
it('should provide appropriate block message when loading', () => {
const authContext = createMockAuthContext({
session: null,
loading: true,
});
const gateway = new AuthGateway(authContext, {});
const message = gateway.getBlockMessage();
// Current behavior: AuthorizationBlocker returns "You must be logged in to access this area."
expect(message).toContain('logged in');
});
});
});