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; let mockPolicy: Mocked; let mockGateway: Mocked; let mockBuilder: Mocked; 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(); }); }); });