import type { NextRequest } from 'next/server'; import { beforeEach, describe, expect, it, vi } from 'vitest'; // --------------------------------------------------------------------------- // Minimal next/server stubs — use vi.hoisted so refs are available at mock time // --------------------------------------------------------------------------- const { mockRedirectFn, mockNextFn, mockIntlMiddleware } = vi.hoisted(() => { const mockRedirectFn = vi.fn((url: URL | string) => ({ type: 'redirect', url: typeof url === 'string' ? url : url.toString(), })); const mockNextFn = vi.fn(() => ({ type: 'next' })); const mockIntlMiddleware = vi.fn((_req: unknown) => ({ type: 'intl' })); return { mockRedirectFn, mockNextFn, mockIntlMiddleware }; }); vi.mock('next/server', () => ({ NextResponse: { redirect: mockRedirectFn, next: mockNextFn, }, })); // --------------------------------------------------------------------------- // Stub intlMiddleware — captures its input and returns a sentinel // --------------------------------------------------------------------------- vi.mock('next-intl/middleware', () => ({ default: () => mockIntlMiddleware, })); // Stub routing so the module resolves vi.mock('@/i18n/routing', () => ({ routing: { locales: ['vi', 'en'], defaultLocale: 'vi', localePrefix: 'as-needed' }, })); // --------------------------------------------------------------------------- // Now import the middleware (after mocks are registered) // --------------------------------------------------------------------------- import { middleware } from '../middleware'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function makeRequest(pathname: string, hasCookie = false): NextRequest { const url = new URL(`http://localhost${pathname}`); return { nextUrl: url, url: url.toString(), cookies: { has: (name: string) => name === 'goodgo_authenticated' && hasCookie, }, } as unknown as NextRequest; } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe('middleware – authentication guard', () => { beforeEach(() => { vi.clearAllMocks(); }); it('redirects unauthenticated user from a protected path to /login', () => { middleware(makeRequest('/dashboard', false)); expect(mockRedirectFn).toHaveBeenCalledOnce(); const calledUrl: URL = mockRedirectFn.mock.calls[0][0] as URL; expect(calledUrl.pathname).toBe('/login'); expect(calledUrl.searchParams.get('redirect')).toBe('/dashboard'); }); it('includes the redirect param for a nested protected path', () => { middleware(makeRequest('/dashboard/profile', false)); const calledUrl: URL = mockRedirectFn.mock.calls[0][0] as URL; expect(calledUrl.searchParams.get('redirect')).toBe('/dashboard/profile'); }); it('allows unauthenticated user to reach /', () => { middleware(makeRequest('/', false)); expect(mockRedirectFn).not.toHaveBeenCalled(); expect(mockIntlMiddleware).toHaveBeenCalledOnce(); }); it('allows unauthenticated user to reach /search', () => { middleware(makeRequest('/search', false)); expect(mockRedirectFn).not.toHaveBeenCalled(); }); it('allows unauthenticated user to reach /listings/123', () => { middleware(makeRequest('/listings/123', false)); expect(mockRedirectFn).not.toHaveBeenCalled(); }); it('allows unauthenticated user to reach /login', () => { middleware(makeRequest('/login', false)); expect(mockRedirectFn).not.toHaveBeenCalled(); }); it('allows unauthenticated user to reach /register', () => { middleware(makeRequest('/register', false)); expect(mockRedirectFn).not.toHaveBeenCalled(); }); it('allows unauthenticated user to reach /auth/callback/google', () => { middleware(makeRequest('/auth/callback/google', false)); expect(mockRedirectFn).not.toHaveBeenCalled(); }); it('allows authenticated user to access a protected path', () => { middleware(makeRequest('/dashboard', true)); expect(mockRedirectFn).not.toHaveBeenCalled(); expect(mockIntlMiddleware).toHaveBeenCalledOnce(); }); }); describe('middleware – auth-only redirect (already authenticated)', () => { beforeEach(() => { vi.clearAllMocks(); }); it('redirects authenticated user away from /login to /dashboard', () => { middleware(makeRequest('/login', true)); expect(mockRedirectFn).toHaveBeenCalledOnce(); const calledUrl: URL = mockRedirectFn.mock.calls[0][0] as URL; expect(calledUrl.pathname).toBe('/dashboard'); }); it('redirects authenticated user away from /register to /dashboard', () => { middleware(makeRequest('/register', true)); expect(mockRedirectFn).toHaveBeenCalledOnce(); const calledUrl: URL = mockRedirectFn.mock.calls[0][0] as URL; expect(calledUrl.pathname).toBe('/dashboard'); }); }); describe('middleware – locale prefix stripping', () => { beforeEach(() => { vi.clearAllMocks(); }); it('strips /vi locale prefix before evaluating the guard', () => { middleware(makeRequest('/vi/dashboard', false)); expect(mockRedirectFn).toHaveBeenCalledOnce(); const calledUrl: URL = mockRedirectFn.mock.calls[0][0] as URL; expect(calledUrl.pathname).toBe('/login'); expect(calledUrl.searchParams.get('redirect')).toBe('/dashboard'); }); it('strips /en locale prefix before evaluating the guard', () => { middleware(makeRequest('/en/dashboard', false)); expect(mockRedirectFn).toHaveBeenCalledOnce(); const calledUrl: URL = mockRedirectFn.mock.calls[0][0] as URL; expect(calledUrl.pathname).toBe('/login'); }); it('strips /vi locale and recognises /vi/login as auth-only path', () => { middleware(makeRequest('/vi/login', true)); expect(mockRedirectFn).toHaveBeenCalledOnce(); const calledUrl: URL = mockRedirectFn.mock.calls[0][0] as URL; expect(calledUrl.pathname).toBe('/dashboard'); }); it('strips /en locale and allows unauthenticated access to /en/', () => { middleware(makeRequest('/en/', false)); expect(mockRedirectFn).not.toHaveBeenCalled(); expect(mockIntlMiddleware).toHaveBeenCalledOnce(); }); it('passes through to intlMiddleware for locale-prefixed public paths', () => { middleware(makeRequest('/vi/search', false)); expect(mockRedirectFn).not.toHaveBeenCalled(); expect(mockIntlMiddleware).toHaveBeenCalledOnce(); }); }); describe('middleware – intl middleware delegation', () => { beforeEach(() => { vi.clearAllMocks(); }); it('delegates to intlMiddleware for all pass-through cases', () => { middleware(makeRequest('/', false)); expect(mockIntlMiddleware).toHaveBeenCalledOnce(); }); it('delegates to intlMiddleware for authenticated protected paths', () => { middleware(makeRequest('/dashboard', true)); expect(mockIntlMiddleware).toHaveBeenCalledOnce(); }); it('does NOT call intlMiddleware when redirecting to login', () => { middleware(makeRequest('/dashboard', false)); expect(mockIntlMiddleware).not.toHaveBeenCalled(); }); it('does NOT call intlMiddleware when redirecting authenticated user away from login', () => { middleware(makeRequest('/login', true)); expect(mockIntlMiddleware).not.toHaveBeenCalled(); }); });