Files
goodgo-platform/apps/web/__tests__/middleware.spec.ts
2026-05-07 13:08:20 +07:00

205 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 /pricing', () => {
middleware(makeRequest('/pricing', 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();
});
});