import { describe, expect, it, vi, beforeEach } from 'vitest'; import { NextRequest, NextResponse } from 'next/server'; // Hoist mock so it's available in the vi.mock factory const { mockIntlMiddleware } = vi.hoisted(() => ({ mockIntlMiddleware: vi.fn(() => NextResponse.next()), })); vi.mock('next-intl/middleware', () => ({ default: vi.fn(() => mockIntlMiddleware), })); // Mock routing config vi.mock('@/i18n/routing', () => ({ routing: { locales: ['vi', 'en'], defaultLocale: 'vi', localePrefix: 'as-needed', }, })); import { middleware, config } from '../middleware'; function createRequest( pathname: string, options?: { cookies?: Record; origin?: string }, ): NextRequest { const origin = options?.origin ?? 'http://localhost:3200'; const url = new URL(pathname, origin); const request = new NextRequest(url); if (options?.cookies) { for (const [name, value] of Object.entries(options.cookies)) { request.cookies.set(name, value); } } return request; } describe('middleware', () => { beforeEach(() => { vi.clearAllMocks(); mockIntlMiddleware.mockReturnValue(NextResponse.next()); }); describe('matcher config', () => { it('exports a valid matcher that excludes api and static routes', () => { expect(config).toBeDefined(); expect(config.matcher).toEqual([ '/((?!api|_next/static|_next/image|favicon.ico|public).*)', ]); }); it('matcher pattern matches normal routes', () => { const pattern = new RegExp(config.matcher[0]); expect(pattern.test('/dashboard')).toBe(true); expect(pattern.test('/login')).toBe(true); expect(pattern.test('/vi/search')).toBe(true); }); }); describe('public paths — unauthenticated access', () => { it('allows unauthenticated access to the root path /', () => { const request = createRequest('/'); const response = middleware(request); // Should not redirect to login — should pass through to intlMiddleware expect(response.headers.get('location')).toBeNull(); }); it('allows unauthenticated access to /login', () => { const request = createRequest('/login'); const response = middleware(request); expect(response.headers.get('location')).toBeNull(); }); it('allows unauthenticated access to /register', () => { const request = createRequest('/register'); const response = middleware(request); expect(response.headers.get('location')).toBeNull(); }); it('allows unauthenticated access to /search', () => { const request = createRequest('/search'); const response = middleware(request); expect(response.headers.get('location')).toBeNull(); }); it('allows unauthenticated access to /search/anything', () => { const request = createRequest('/search/filters?type=APARTMENT'); const response = middleware(request); expect(response.headers.get('location')).toBeNull(); }); it('allows unauthenticated access to /listings', () => { const request = createRequest('/listings'); const response = middleware(request); expect(response.headers.get('location')).toBeNull(); }); it('allows unauthenticated access to /listings/:id', () => { const request = createRequest('/listings/abc-123'); const response = middleware(request); expect(response.headers.get('location')).toBeNull(); }); it('allows unauthenticated access to /auth/callback', () => { const request = createRequest('/auth/callback'); const response = middleware(request); expect(response.headers.get('location')).toBeNull(); }); it('allows unauthenticated access to /auth/callback/google', () => { const request = createRequest('/auth/callback/google'); const response = middleware(request); expect(response.headers.get('location')).toBeNull(); }); }); describe('auth redirect — protected routes require authentication', () => { it('redirects unauthenticated users from /dashboard to /login', () => { const request = createRequest('/dashboard'); const response = middleware(request); const location = response.headers.get('location'); expect(location).toBeTruthy(); const redirectUrl = new URL(location!); expect(redirectUrl.pathname).toBe('/login'); expect(redirectUrl.searchParams.get('redirect')).toBe('/dashboard'); }); it('redirects unauthenticated users from /profile to /login', () => { const request = createRequest('/profile'); const response = middleware(request); const location = response.headers.get('location'); expect(location).toBeTruthy(); const redirectUrl = new URL(location!); expect(redirectUrl.pathname).toBe('/login'); expect(redirectUrl.searchParams.get('redirect')).toBe('/profile'); }); it('redirects unauthenticated users from /my-listings to /login with redirect param', () => { const request = createRequest('/my-listings'); const response = middleware(request); const location = response.headers.get('location'); expect(location).toBeTruthy(); const redirectUrl = new URL(location!); expect(redirectUrl.pathname).toBe('/login'); expect(redirectUrl.searchParams.get('redirect')).toBe('/my-listings'); }); it('allows authenticated users to access /dashboard', () => { const request = createRequest('/dashboard', { cookies: { goodgo_authenticated: 'true' }, }); const response = middleware(request); expect(response.headers.get('location')).toBeNull(); }); it('allows authenticated users to access /profile', () => { const request = createRequest('/profile', { cookies: { goodgo_authenticated: 'true' }, }); const response = middleware(request); expect(response.headers.get('location')).toBeNull(); }); }); describe('auth-only redirect — authenticated users bounce from login/register', () => { it('redirects authenticated users from /login to /dashboard', () => { const request = createRequest('/login', { cookies: { goodgo_authenticated: 'true' }, }); const response = middleware(request); const location = response.headers.get('location'); expect(location).toBeTruthy(); const redirectUrl = new URL(location!); expect(redirectUrl.pathname).toBe('/dashboard'); }); it('redirects authenticated users from /register to /dashboard', () => { const request = createRequest('/register', { cookies: { goodgo_authenticated: 'true' }, }); const response = middleware(request); const location = response.headers.get('location'); expect(location).toBeTruthy(); const redirectUrl = new URL(location!); expect(redirectUrl.pathname).toBe('/dashboard'); }); it('does NOT redirect authenticated users from other public paths', () => { const request = createRequest('/search', { cookies: { goodgo_authenticated: 'true' }, }); const response = middleware(request); expect(response.headers.get('location')).toBeNull(); }); }); describe('locale stripping — paths with locale prefix', () => { it('strips /vi prefix and allows public path', () => { const request = createRequest('/vi/login'); const response = middleware(request); // /login is public, no redirect to /login (would be self-redirect) expect(response.headers.get('location')).toBeNull(); }); it('strips /en prefix and allows public path', () => { const request = createRequest('/en/search'); const response = middleware(request); expect(response.headers.get('location')).toBeNull(); }); it('strips /vi prefix and redirects protected path to login', () => { const request = createRequest('/vi/dashboard'); const response = middleware(request); const location = response.headers.get('location'); expect(location).toBeTruthy(); const redirectUrl = new URL(location!); expect(redirectUrl.pathname).toBe('/login'); expect(redirectUrl.searchParams.get('redirect')).toBe('/dashboard'); }); it('strips /en prefix and redirects protected path to login', () => { const request = createRequest('/en/profile'); const response = middleware(request); const location = response.headers.get('location'); expect(location).toBeTruthy(); const redirectUrl = new URL(location!); expect(redirectUrl.pathname).toBe('/login'); expect(redirectUrl.searchParams.get('redirect')).toBe('/profile'); }); it('strips /vi prefix for auth-only redirect for authenticated users', () => { const request = createRequest('/vi/login', { cookies: { goodgo_authenticated: 'true' }, }); const response = middleware(request); const location = response.headers.get('location'); expect(location).toBeTruthy(); const redirectUrl = new URL(location!); expect(redirectUrl.pathname).toBe('/dashboard'); }); it('strips /en prefix for auth-only redirect for authenticated users', () => { const request = createRequest('/en/register', { cookies: { goodgo_authenticated: 'true' }, }); const response = middleware(request); const location = response.headers.get('location'); expect(location).toBeTruthy(); const redirectUrl = new URL(location!); expect(redirectUrl.pathname).toBe('/dashboard'); }); }); describe('intl middleware delegation', () => { it('delegates to intlMiddleware for authenticated public paths', () => { const request = createRequest('/search', { cookies: { goodgo_authenticated: 'true' }, }); middleware(request); expect(mockIntlMiddleware).toHaveBeenCalledWith(request); }); it('delegates to intlMiddleware for unauthenticated public paths', () => { const request = createRequest('/'); middleware(request); expect(mockIntlMiddleware).toHaveBeenCalledWith(request); }); it('delegates to intlMiddleware for authenticated protected paths', () => { const request = createRequest('/dashboard', { cookies: { goodgo_authenticated: 'true' }, }); middleware(request); expect(mockIntlMiddleware).toHaveBeenCalledWith(request); }); it('does NOT delegate to intlMiddleware when redirecting to login', () => { const request = createRequest('/dashboard'); middleware(request); expect(mockIntlMiddleware).not.toHaveBeenCalled(); }); it('does NOT delegate to intlMiddleware when redirecting to dashboard', () => { const request = createRequest('/login', { cookies: { goodgo_authenticated: 'true' }, }); middleware(request); expect(mockIntlMiddleware).not.toHaveBeenCalled(); }); }); describe('edge cases', () => { it('handles path with trailing slash', () => { const request = createRequest('/dashboard/'); const response = middleware(request); const location = response.headers.get('location'); expect(location).toBeTruthy(); const redirectUrl = new URL(location!); expect(redirectUrl.pathname).toBe('/login'); }); it('handles deeply nested protected path', () => { const request = createRequest('/dashboard/analytics/reports'); const response = middleware(request); const location = response.headers.get('location'); expect(location).toBeTruthy(); const redirectUrl = new URL(location!); expect(redirectUrl.pathname).toBe('/login'); expect(redirectUrl.searchParams.get('redirect')).toBe( '/dashboard/analytics/reports', ); }); it('handles root path with locale prefix /vi', () => { const request = createRequest('/vi'); const response = middleware(request); // /vi -> stripped to / -> public, no redirect expect(response.headers.get('location')).toBeNull(); }); it('handles root path with locale prefix /en', () => { const request = createRequest('/en'); const response = middleware(request); expect(response.headers.get('location')).toBeNull(); }); it('does not treat partial locale match as locale prefix', () => { // /video should NOT strip "vi" prefix — "vi" must be followed by / or end const request = createRequest('/video'); const response = middleware(request); // /video is not public, so should redirect to login const location = response.headers.get('location'); expect(location).toBeTruthy(); const redirectUrl = new URL(location!); expect(redirectUrl.pathname).toBe('/login'); }); it('preserves redirect path through query string in login redirect', () => { const request = createRequest('/settings?tab=profile'); const response = middleware(request); const location = response.headers.get('location'); expect(location).toBeTruthy(); const redirectUrl = new URL(location!); expect(redirectUrl.pathname).toBe('/login'); // The redirect param should contain the stripped path expect(redirectUrl.searchParams.get('redirect')).toBe('/settings'); }); it('handles cookie with empty string value as having the cookie', () => { const request = createRequest('/dashboard', { cookies: { goodgo_authenticated: '' }, }); const response = middleware(request); // cookies.has() returns true even with empty value expect(response.headers.get('location')).toBeNull(); }); }); });