import { describe, it, expect, vi, beforeEach } from 'vitest'; import type { NextRequest } from 'next/server'; const mockGetSession = vi.fn(); // Mock Next.js server components vi.mock('next/server', () => ({ NextResponse: { next: vi.fn(() => ({ headers: { set: vi.fn(), }, })), redirect: vi.fn((url: URL) => ({ headers: { set: vi.fn(), }, url: url.toString(), })), }, })); // Mock SessionGateway so tests can control session behavior via `mockGetSession`. vi.mock('@/lib/gateways/SessionGateway', () => ({ SessionGateway: class { getSession = mockGetSession; }, })); vi.mock('@/lib/auth/RouteAccessPolicy', () => ({ RouteAccessPolicy: vi.fn().mockImplementation(() => ({ isPublic: vi.fn(), isAuthPage: vi.fn(), requiredRoles: vi.fn(), })), })); vi.mock('@/lib/auth/PathnameInterpreter', () => ({ PathnameInterpreter: vi.fn().mockImplementation(() => ({ interpret: vi.fn((pathname: string) => ({ locale: null, logicalPathname: pathname, })), })), })); vi.mock('@/lib/auth/AuthRedirectBuilder', () => ({ AuthRedirectBuilder: vi.fn().mockImplementation(() => ({ toLogin: vi.fn(({ currentPathname }) => `/auth/login?returnTo=${encodeURIComponent(currentPathname)}`), awayFromAuthPage: vi.fn(({ session }) => { const role = session.user?.role; if (role === 'sponsor') return '/sponsor/dashboard'; if (role === 'admin' || role === 'league-admin' || role === 'league-steward' || role === 'league-owner' || role === 'system-owner' || role === 'super-admin') return '/admin'; return '/dashboard'; }), })), })); vi.mock('@/lib/auth/ReturnToSanitizer', () => ({ ReturnToSanitizer: vi.fn().mockImplementation(() => ({ sanitizeReturnTo: vi.fn((input, fallback) => input || fallback), })), })); vi.mock('@/lib/auth/RoutePathBuilder', () => ({ RoutePathBuilder: vi.fn().mockImplementation(() => ({ build: vi.fn((routeId, params, options) => { const paths: Record = { 'auth.login': '/auth/login', 'protected.dashboard': '/dashboard', 'sponsor.dashboard': '/sponsor/dashboard', 'admin': '/admin', }; const path = paths[routeId] || '/'; return options?.locale ? `/${options.locale}${path}` : path; }), })), })); vi.mock('@/lib/routing/RouteConfig', () => ({ routes: { auth: { login: '/auth/login', signup: '/auth/signup', forgotPassword: '/auth/forgot-password', resetPassword: '/auth/reset-password' }, public: { home: '/', leagues: '/leagues', drivers: '/drivers', teams: '/teams', leaderboards: '/leaderboards', races: '/races', sponsorSignup: '/sponsor/signup' }, protected: { dashboard: '/dashboard', onboarding: '/onboarding', profile: '/profile', profileSettings: '/profile/settings' }, sponsor: { root: '/sponsor', dashboard: '/sponsor/dashboard', billing: '/sponsor/billing' }, admin: { root: '/admin', users: '/admin/users' }, league: { detail: (id: string) => `/leagues/${id}`, scheduleAdmin: (id: string) => `/leagues/${id}/schedule/admin` }, race: { root: '/races', detail: (id: string) => `/races/${id}`, stewarding: (id: string) => `/races/${id}/stewarding` }, team: { root: '/teams', detail: (id: string) => `/teams/${id}` }, driver: { root: '/drivers', detail: (id: string) => `/drivers/${id}` }, error: { notFound: '/404', serverError: '/500' }, }, routeMatchers: { isInGroup: (path: string, group: string) => { const groups: Record = { auth: ['/auth/login', '/auth/signup', '/auth/forgot-password', '/auth/reset-password'], sponsor: ['/sponsor', '/sponsor/dashboard', '/sponsor/billing'], admin: ['/admin', '/admin/users'], }; return groups[group]?.some((prefix) => path.startsWith(prefix)) ?? false; }, isPublic: (path: string) => { const publicExact = new Set([ '/', '/leagues', '/drivers', '/teams', '/leaderboards', '/races', '/sponsor/signup', '/auth/login', '/auth/signup', '/auth/forgot-password', '/auth/reset-password', '/404', '/500', ]); // Check exact matches if (publicExact.has(path)) return true; // Treat top-level detail pages as public (e2e relies on this) // Examples: /leagues/:id, /races/:id, /drivers/:id, /teams/:id const segments = path.split('/').filter(Boolean); if (segments.length === 2) { const [group, slug] = segments; if (group === 'leagues' && slug !== 'create') return true; if (group === 'races') return true; if (group === 'drivers') return true; if (group === 'teams') return true; } return false; }, requiresAuth: (path: string) => { const publicPaths = ['/', '/leagues', '/drivers', '/teams', '/leaderboards', '/races', '/sponsor/signup', '/auth/login', '/auth/signup', '/auth/forgot-password', '/auth/reset-password', '/404', '/500']; return !publicPaths.includes(path) && !path.startsWith('/leagues/') && !path.startsWith('/drivers/') && !path.startsWith('/teams/') && !path.startsWith('/races/'); }, requiresRole: (path: string) => { if (path.startsWith('/admin')) return ['league-admin']; if (path.startsWith('/sponsor')) return ['sponsor']; return null; }, }, })); // Import middleware after mocks import { middleware } from './middleware'; describe('Middleware - Route Protection', () => { let mockRequest: NextRequest; beforeEach(() => { vi.clearAllMocks(); mockGetSession.mockReset(); mockRequest = { url: 'http://localhost:3000', nextUrl: { pathname: '/' }, headers: { set: vi.fn(), get: vi.fn(), }, cookies: { get: vi.fn(), getAll: vi.fn(), set: vi.fn(), delete: vi.fn(), }, } as any; }); describe('x-pathname header', () => { it('should set x-pathname header for all requests', async () => { mockRequest.nextUrl.pathname = '/dashboard'; mockGetSession.mockResolvedValue(null); // No session to trigger redirect const response = await middleware(mockRequest); // The response should have headers.set called // For redirect responses, check that the mock was set up correctly expect(response.headers).toBeDefined(); expect(response.headers.set).toBeDefined(); }); }); describe('Public routes', () => { it('should allow access to public routes without authentication', async () => { const publicRoutes = ['/', '/leagues', '/drivers', '/teams', '/leaderboards', '/races', '/sponsor/signup']; for (const route of publicRoutes) { mockRequest.nextUrl.pathname = route; const response = await middleware(mockRequest); // Should call NextResponse.next() (no redirect) expect(response).toBeDefined(); } }); }); describe('Protected routes without authentication', () => { it('should redirect to login with returnTo parameter', async () => { mockRequest.nextUrl.pathname = '/dashboard'; mockGetSession.mockResolvedValue(null); const response = await middleware(mockRequest); expect(response.url).toContain('/auth/login'); expect(response.url).toContain('returnTo=%2Fdashboard'); }); }); describe('Protected routes with authentication', () => { it('should allow access to protected routes with valid session', async () => { mockRequest.nextUrl.pathname = '/dashboard'; mockGetSession.mockResolvedValue({ token: 'test-token', user: { userId: '123', role: 'user', email: 'test@example.com', displayName: 'Test User' }, }); const response = await middleware(mockRequest); expect(response.url).toBeUndefined(); }); it('should redirect authenticated users away from auth pages', async () => { mockRequest.nextUrl.pathname = '/auth/login'; mockGetSession.mockResolvedValue({ token: 'test-token', user: { userId: '123', role: 'user', email: 'test@example.com', displayName: 'Test User' }, }); const response = await middleware(mockRequest); expect(response.url).toContain('/dashboard'); }); }); describe('Role-based access control', () => { it('should allow admin user to access admin routes', async () => { mockRequest.nextUrl.pathname = '/admin/users'; mockGetSession.mockResolvedValue({ token: 'test-token', user: { userId: '123', role: 'league-admin', email: 'test@example.com', displayName: 'Test User' }, }); const response = await middleware(mockRequest); expect(response.url).toBeUndefined(); }); it('should block regular user from admin routes', async () => { mockRequest.nextUrl.pathname = '/admin/users'; mockGetSession.mockResolvedValue({ token: 'test-token', user: { userId: '123', role: 'user', email: 'test@example.com', displayName: 'Test User' }, }); const response = await middleware(mockRequest); expect(response.url).toContain('/auth/login'); }); it('should allow sponsor user to access sponsor routes', async () => { mockRequest.nextUrl.pathname = '/sponsor/dashboard'; mockGetSession.mockResolvedValue({ token: 'test-token', user: { userId: '123', role: 'sponsor', email: 'test@example.com', displayName: 'Test User' }, }); const response = await middleware(mockRequest); expect(response.url).toBeUndefined(); }); it('should block regular user from sponsor routes', async () => { mockRequest.nextUrl.pathname = '/sponsor/dashboard'; mockGetSession.mockResolvedValue({ token: 'test-token', user: { userId: '123', role: 'user', email: 'test@example.com', displayName: 'Test User' }, }); const response = await middleware(mockRequest); expect(response.url).toContain('/auth/login'); }); }); describe('Parameterized routes', () => { it('should allow public access to parameterized public routes', async () => { mockRequest.nextUrl.pathname = '/leagues/123'; const response = await middleware(mockRequest); expect(response).toBeDefined(); }); it('should redirect parameterized protected routes without auth', async () => { mockRequest.nextUrl.pathname = '/leagues/123/schedule/admin'; mockGetSession.mockResolvedValue(null); const response = await middleware(mockRequest); expect(response.url).toContain('/auth/login'); expect(response.url).toContain('returnTo=%2Fleagues%2F123%2Fschedule%2Fadmin'); }); it('should allow admin access to parameterized admin routes', async () => { mockRequest.nextUrl.pathname = '/leagues/123/schedule/admin'; mockGetSession.mockResolvedValue({ token: 'test-token', user: { userId: '123', role: 'league-admin', email: 'test@example.com', displayName: 'Test User' }, }); const response = await middleware(mockRequest); expect(response.url).toBeUndefined(); }); }); describe('Edge cases', () => { it('should handle missing session gracefully', async () => { mockRequest.nextUrl.pathname = '/dashboard'; mockGetSession.mockResolvedValue(null); const response = await middleware(mockRequest); expect(response.url).toContain('/auth/login'); }); it('should handle session without user role', async () => { mockRequest.nextUrl.pathname = '/admin/users'; mockGetSession.mockResolvedValue({ token: 'test-token', user: { userId: '123', email: 'test@example.com', displayName: 'Test User' }, // no role }); const response = await middleware(mockRequest); expect(response.url).toContain('/auth/login'); }); it('should preserve locale in redirects', async () => { mockRequest.nextUrl.pathname = '/de/dashboard'; mockGetSession.mockResolvedValue(null); const response = await middleware(mockRequest); expect(response.url).toContain('/de/auth/login'); }); }); });