WellNuo/web/__tests__/middleware.test.ts
Sergei 3f0fe56e02 Add protected route middleware and auth store for web app
- Implement Next.js middleware for route protection
- Create Zustand auth store for web (similar to mobile)
- Add comprehensive tests for middleware and auth store
- Protect authenticated routes (/dashboard, /profile)
- Redirect unauthenticated users to /login
- Redirect authenticated users from auth routes to /dashboard
- Handle session expiration with 401 callback
- Set access token cookie for middleware
- All tests passing (105 tests total)
2026-01-31 17:49:21 -08:00

206 lines
6.0 KiB
TypeScript

/**
* @jest-environment node
*/
import { NextRequest, NextResponse } from 'next/server';
import { middleware } from '../middleware';
// Mock NextResponse
jest.mock('next/server', () => ({
...jest.requireActual('next/server'),
NextResponse: {
next: jest.fn(() => ({ type: 'next' })),
redirect: jest.fn((url: URL) => ({ type: 'redirect', url: url.toString() })),
},
}));
describe('Protected Route Middleware', () => {
const baseUrl = 'http://localhost:3000';
// Helper to create mock request
const createRequest = (pathname: string, hasToken = false): NextRequest => {
const url = new URL(pathname, baseUrl);
const request = new NextRequest(url);
// Mock cookies
if (hasToken) {
Object.defineProperty(request, 'cookies', {
value: {
get: jest.fn((name: string) => {
if (name === 'accessToken') {
return { value: 'mock-token-123' };
}
return undefined;
}),
},
writable: true,
});
} else {
Object.defineProperty(request, 'cookies', {
value: {
get: jest.fn(() => undefined),
},
writable: true,
});
}
return request;
};
beforeEach(() => {
jest.clearAllMocks();
});
describe('Public Routes (Auth Routes)', () => {
it('should allow access to /login without token', () => {
const request = createRequest('/login', false);
const response = middleware(request);
expect(response).toEqual({ type: 'next' });
expect(NextResponse.next).toHaveBeenCalled();
});
it('should allow access to /verify-otp without token', () => {
const request = createRequest('/verify-otp', false);
const response = middleware(request);
expect(response).toEqual({ type: 'next' });
expect(NextResponse.next).toHaveBeenCalled();
});
it('should redirect authenticated user from /login to /dashboard', () => {
const request = createRequest('/login', true);
const response = middleware(request);
expect(response).toEqual({
type: 'redirect',
url: `${baseUrl}/dashboard`,
});
expect(NextResponse.redirect).toHaveBeenCalledWith(
expect.objectContaining({
href: `${baseUrl}/dashboard`,
})
);
});
it('should redirect authenticated user from /verify-otp to /dashboard', () => {
const request = createRequest('/verify-otp', true);
const response = middleware(request);
expect(response).toEqual({
type: 'redirect',
url: `${baseUrl}/dashboard`,
});
});
});
describe('Protected Routes (Main Routes)', () => {
it('should allow access to /dashboard with token', () => {
const request = createRequest('/dashboard', true);
const response = middleware(request);
expect(response).toEqual({ type: 'next' });
expect(NextResponse.next).toHaveBeenCalled();
});
it('should redirect to /login when accessing /dashboard without token', () => {
const request = createRequest('/dashboard', false);
const response = middleware(request);
expect(response).toEqual({
type: 'redirect',
url: expect.stringContaining('/login?redirect=%2Fdashboard'),
});
expect(NextResponse.redirect).toHaveBeenCalledWith(
expect.objectContaining({
href: expect.stringContaining('/login?redirect=%2Fdashboard'),
})
);
});
it('should redirect to /login when accessing /profile without token', () => {
const request = createRequest('/profile', false);
const response = middleware(request);
expect(response).toEqual({
type: 'redirect',
url: expect.stringContaining('/login?redirect=%2Fprofile'),
});
});
});
describe('Root Path Handling', () => {
it('should redirect to /dashboard when accessing / with token', () => {
const request = createRequest('/', true);
const response = middleware(request);
expect(response).toEqual({
type: 'redirect',
url: `${baseUrl}/dashboard`,
});
});
it('should redirect to /login when accessing / without token', () => {
const request = createRequest('/', false);
const response = middleware(request);
expect(response).toEqual({
type: 'redirect',
url: `${baseUrl}/login`,
});
});
});
describe('API and Static Routes', () => {
it('should allow access to API routes without token', () => {
const request = createRequest('/api/test', false);
const response = middleware(request);
expect(response).toEqual({ type: 'next' });
expect(NextResponse.next).toHaveBeenCalled();
});
it('should allow access to _next/static without token', () => {
const request = createRequest('/_next/static/chunk.js', false);
const response = middleware(request);
expect(response).toEqual({ type: 'next' });
});
it('should allow access to favicon.ico without token', () => {
const request = createRequest('/favicon.ico', false);
const response = middleware(request);
expect(response).toEqual({ type: 'next' });
});
it('should allow access to /images without token', () => {
const request = createRequest('/images/logo.png', false);
const response = middleware(request);
expect(response).toEqual({ type: 'next' });
});
});
describe('Redirect Parameter Preservation', () => {
it('should preserve original URL in redirect parameter', () => {
const request = createRequest('/dashboard/settings', false);
const response = middleware(request);
expect(response).toEqual({
type: 'redirect',
url: expect.stringContaining('/login?redirect=%2Fdashboard%2Fsettings'),
});
});
it('should preserve query parameters in redirect', () => {
const request = createRequest('/dashboard?tab=profile', false);
const response = middleware(request);
expect(response).toEqual({
type: 'redirect',
url: expect.stringContaining('/login?redirect='),
});
});
});
});