Files
goodgo-platform/apps/web/__tests__/middleware.spec.ts
Ho Ngoc Hai be47c26031 test(web): add component tests for 3 more untested components (GOO-54)
- ExportPdfButton (3 tests): default label, missing-target error, custom filename
- ValuationHistoryChart (3 tests): null <2 points, header/description, recharts mounting
- NotificationBell (9 tests): aria-label, badge display + 99+ cap, auth-gated
  fetchUnreadCount, dropdown toggle, empty state, item rendering, mark-all visibility

All 15 new tests pass via direct vitest. Cumulative GOO-54 progress: 29 spec
files, ~143 tests across H1-H5.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-24 12:57:35 +07:00

200 lines
7.3 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 { beforeEach, describe, expect, it, vi } from 'vitest';
import type { NextRequest } from 'next/server';
// ---------------------------------------------------------------------------
// 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();
});
});