350 lines
10 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
}); |