Files
goodgo-platform/apps/web/__tests__/middleware.spec.ts
Ho Ngoc Hai 0924f0cb9b test(web): add middleware + i18n + messages test suites (GOO-60)
Cover frontend middleware auth/locale logic, i18n config/routing/request/navigation,
and vi/en message parity — 91 new tests across 6 files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 10:47:30 +07:00

360 lines
13 KiB
TypeScript

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<string, string>; 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();
});
});
});