clean routes
This commit is contained in:
223
apps/website/lib/auth/RouteGuard.test.ts
Normal file
223
apps/website/lib/auth/RouteGuard.test.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import { describe, it, expect, vi, Mocked, beforeEach } from 'vitest';
|
||||
import { RouteGuard } from './RouteGuard';
|
||||
import { PathnameInterpreter } from './PathnameInterpreter';
|
||||
import { RouteAccessPolicy } from './RouteAccessPolicy';
|
||||
import { SessionGateway } from '../gateways/SessionGateway';
|
||||
import { AuthRedirectBuilder } from './AuthRedirectBuilder';
|
||||
import type { AuthSessionDTO } from '../types/generated/AuthSessionDTO';
|
||||
|
||||
// Hoist the mock redirect function
|
||||
const mockRedirect = vi.hoisted(() => vi.fn());
|
||||
|
||||
// Mock next/navigation
|
||||
vi.mock('next/navigation', () => ({
|
||||
redirect: mockRedirect,
|
||||
}));
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('./PathnameInterpreter');
|
||||
vi.mock('./RouteAccessPolicy');
|
||||
vi.mock('../gateways/SessionGateway');
|
||||
vi.mock('./AuthRedirectBuilder');
|
||||
|
||||
describe('RouteGuard', () => {
|
||||
let routeGuard: RouteGuard;
|
||||
let mockInterpreter: Mocked<PathnameInterpreter>;
|
||||
let mockPolicy: Mocked<RouteAccessPolicy>;
|
||||
let mockGateway: Mocked<SessionGateway>;
|
||||
let mockBuilder: Mocked<AuthRedirectBuilder>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset all mocks
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Create mock instances
|
||||
mockInterpreter = {
|
||||
interpret: vi.fn(),
|
||||
} as any;
|
||||
|
||||
mockPolicy = {
|
||||
isPublic: vi.fn(),
|
||||
isAuthPage: vi.fn(),
|
||||
requiredRoles: vi.fn(),
|
||||
} as any;
|
||||
|
||||
mockGateway = {
|
||||
getSession: vi.fn(),
|
||||
} as any;
|
||||
|
||||
mockBuilder = {
|
||||
awayFromAuthPage: vi.fn(),
|
||||
toLogin: vi.fn(),
|
||||
} as any;
|
||||
|
||||
// Create RouteGuard instance
|
||||
routeGuard = new RouteGuard(
|
||||
mockInterpreter,
|
||||
mockPolicy,
|
||||
mockGateway,
|
||||
mockBuilder
|
||||
);
|
||||
});
|
||||
|
||||
describe('RED: public non-auth page → no redirect', () => {
|
||||
it('should allow access without redirect for public non-auth pages', async () => {
|
||||
// Arrange
|
||||
const pathname = '/public/page';
|
||||
mockInterpreter.interpret.mockReturnValue({ locale: 'en', logicalPathname: '/public/page' });
|
||||
mockPolicy.isPublic.mockReturnValue(true);
|
||||
mockPolicy.isAuthPage.mockReturnValue(false);
|
||||
|
||||
// Act
|
||||
await routeGuard.enforce({ pathname });
|
||||
|
||||
// Assert
|
||||
expect(mockInterpreter.interpret).toHaveBeenCalledWith(pathname);
|
||||
expect(mockPolicy.isPublic).toHaveBeenCalledWith('/public/page');
|
||||
expect(mockPolicy.isAuthPage).toHaveBeenCalledWith('/public/page');
|
||||
expect(mockGateway.getSession).not.toHaveBeenCalled();
|
||||
expect(mockRedirect).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('auth page, no session → allow', () => {
|
||||
it('should allow access to auth page when no session exists', async () => {
|
||||
// Arrange
|
||||
const pathname = '/login';
|
||||
mockInterpreter.interpret.mockReturnValue({ locale: 'en', logicalPathname: '/login' });
|
||||
mockPolicy.isPublic.mockReturnValue(false);
|
||||
mockPolicy.isAuthPage.mockReturnValue(true);
|
||||
mockGateway.getSession.mockResolvedValue(null);
|
||||
|
||||
// Act
|
||||
await routeGuard.enforce({ pathname });
|
||||
|
||||
// Assert
|
||||
expect(mockGateway.getSession).toHaveBeenCalled();
|
||||
expect(mockRedirect).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('auth page, session → away redirect', () => {
|
||||
it('should redirect away from auth page when session exists', async () => {
|
||||
// Arrange
|
||||
const pathname = '/login';
|
||||
const mockSession: AuthSessionDTO = {
|
||||
user: { userId: '123', role: 'user', email: 'test@example.com', displayName: 'Test User' },
|
||||
token: 'mock-token',
|
||||
};
|
||||
|
||||
mockInterpreter.interpret.mockReturnValue({ locale: 'en', logicalPathname: '/login' });
|
||||
mockPolicy.isPublic.mockReturnValue(false);
|
||||
mockPolicy.isAuthPage.mockReturnValue(true);
|
||||
mockGateway.getSession.mockResolvedValue(mockSession);
|
||||
mockBuilder.awayFromAuthPage.mockReturnValue('/dashboard');
|
||||
|
||||
// Act
|
||||
await routeGuard.enforce({ pathname });
|
||||
|
||||
// Assert
|
||||
expect(mockGateway.getSession).toHaveBeenCalled();
|
||||
expect(mockBuilder.awayFromAuthPage).toHaveBeenCalledWith({
|
||||
session: mockSession,
|
||||
currentPathname: '/login',
|
||||
});
|
||||
expect(mockRedirect).toHaveBeenCalledWith('/dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
describe('protected, no session → login redirect', () => {
|
||||
it('should redirect to login when accessing protected page without session', async () => {
|
||||
// Arrange
|
||||
const pathname = '/protected/dashboard';
|
||||
mockInterpreter.interpret.mockReturnValue({ locale: 'en', logicalPathname: '/protected/dashboard' });
|
||||
mockPolicy.isPublic.mockReturnValue(false);
|
||||
mockPolicy.isAuthPage.mockReturnValue(false);
|
||||
mockGateway.getSession.mockResolvedValue(null);
|
||||
mockBuilder.toLogin.mockReturnValue('/login?redirect=/protected/dashboard');
|
||||
|
||||
// Act
|
||||
await routeGuard.enforce({ pathname });
|
||||
|
||||
// Assert
|
||||
expect(mockGateway.getSession).toHaveBeenCalled();
|
||||
expect(mockBuilder.toLogin).toHaveBeenCalledWith({ currentPathname: '/protected/dashboard' });
|
||||
expect(mockRedirect).toHaveBeenCalledWith('/login?redirect=/protected/dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
describe('protected, wrong role → login', () => {
|
||||
it('should redirect to login when user lacks required role', async () => {
|
||||
// Arrange
|
||||
const pathname = '/admin/panel';
|
||||
const mockSession: AuthSessionDTO = {
|
||||
user: { userId: '123', role: 'user', email: 'test@example.com', displayName: 'Test User' },
|
||||
token: 'mock-token',
|
||||
};
|
||||
|
||||
mockInterpreter.interpret.mockReturnValue({ locale: 'en', logicalPathname: '/admin/panel' });
|
||||
mockPolicy.isPublic.mockReturnValue(false);
|
||||
mockPolicy.isAuthPage.mockReturnValue(false);
|
||||
mockGateway.getSession.mockResolvedValue(mockSession);
|
||||
mockPolicy.requiredRoles.mockReturnValue(['admin']);
|
||||
mockBuilder.toLogin.mockReturnValue('/login?redirect=/admin/panel');
|
||||
|
||||
// Act
|
||||
await routeGuard.enforce({ pathname });
|
||||
|
||||
// Assert
|
||||
expect(mockGateway.getSession).toHaveBeenCalled();
|
||||
expect(mockPolicy.requiredRoles).toHaveBeenCalledWith('/admin/panel');
|
||||
expect(mockBuilder.toLogin).toHaveBeenCalledWith({ currentPathname: '/admin/panel' });
|
||||
expect(mockRedirect).toHaveBeenCalledWith('/login?redirect=/admin/panel');
|
||||
});
|
||||
});
|
||||
|
||||
describe('protected, correct role → allow', () => {
|
||||
it('should allow access when user has required role', async () => {
|
||||
// Arrange
|
||||
const pathname = '/admin/panel';
|
||||
const mockSession: AuthSessionDTO = {
|
||||
user: { userId: '123', role: 'admin', email: 'test@example.com', displayName: 'Test User' },
|
||||
token: 'mock-token',
|
||||
};
|
||||
|
||||
mockInterpreter.interpret.mockReturnValue({ locale: 'en', logicalPathname: '/admin/panel' });
|
||||
mockPolicy.isPublic.mockReturnValue(false);
|
||||
mockPolicy.isAuthPage.mockReturnValue(false);
|
||||
mockGateway.getSession.mockResolvedValue(mockSession);
|
||||
mockPolicy.requiredRoles.mockReturnValue(['admin']);
|
||||
|
||||
// Act
|
||||
await routeGuard.enforce({ pathname });
|
||||
|
||||
// Assert
|
||||
expect(mockGateway.getSession).toHaveBeenCalled();
|
||||
expect(mockPolicy.requiredRoles).toHaveBeenCalledWith('/admin/panel');
|
||||
expect(mockRedirect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow access when no specific roles required', async () => {
|
||||
// Arrange
|
||||
const pathname = '/dashboard';
|
||||
const mockSession: AuthSessionDTO = {
|
||||
user: { userId: '123', role: 'user', email: 'test@example.com', displayName: 'Test User' },
|
||||
token: 'mock-token',
|
||||
};
|
||||
|
||||
mockInterpreter.interpret.mockReturnValue({ locale: 'en', logicalPathname: '/dashboard' });
|
||||
mockPolicy.isPublic.mockReturnValue(false);
|
||||
mockPolicy.isAuthPage.mockReturnValue(false);
|
||||
mockGateway.getSession.mockResolvedValue(mockSession);
|
||||
mockPolicy.requiredRoles.mockReturnValue(null);
|
||||
|
||||
// Act
|
||||
await routeGuard.enforce({ pathname });
|
||||
|
||||
// Assert
|
||||
expect(mockGateway.getSession).toHaveBeenCalled();
|
||||
expect(mockPolicy.requiredRoles).toHaveBeenCalledWith('/dashboard');
|
||||
expect(mockRedirect).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user