- 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)
206 lines
6.0 KiB
TypeScript
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='),
|
|
});
|
|
});
|
|
});
|
|
});
|