fix issues
This commit is contained in:
340
apps/website/lib/gateways/RouteGuard.test.tsx
Normal file
340
apps/website/lib/gateways/RouteGuard.test.tsx
Normal file
@@ -0,0 +1,340 @@
|
||||
/**
|
||||
* TDD Tests for RouteGuard Component
|
||||
*
|
||||
* These tests verify the RouteGuard component logic following TDD principles.
|
||||
* Note: These are integration tests that verify the component behavior.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { RouteGuard } from './RouteGuard';
|
||||
import { useAuth } from '@/lib/auth/AuthContext';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import type { AuthContextValue } from '@/lib/auth/AuthContext';
|
||||
import type { SessionViewModel } from '@/lib/view-models/SessionViewModel';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/lib/auth/AuthContext');
|
||||
vi.mock('next/navigation');
|
||||
|
||||
// Mock SessionViewModel factory
|
||||
function createMockSession(overrides: Partial<SessionViewModel> = {}): SessionViewModel {
|
||||
return {
|
||||
isAuthenticated: true,
|
||||
user: {
|
||||
userId: 'user-123',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
...overrides.user,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// Mock AuthContext factory
|
||||
function createMockAuthContext(overrides: Partial<AuthContextValue> = {}): AuthContextValue {
|
||||
return {
|
||||
session: null,
|
||||
loading: false,
|
||||
login: vi.fn(),
|
||||
logout: vi.fn(),
|
||||
refreshSession: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('RouteGuard', () => {
|
||||
const mockUseAuth = vi.mocked(useAuth);
|
||||
const mockUseRouter = vi.mocked(useRouter);
|
||||
|
||||
let mockRouter: { push: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockRouter = { push: vi.fn() };
|
||||
mockUseRouter.mockReturnValue(mockRouter as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Authentication State', () => {
|
||||
it('should render children when user is authenticated', async () => {
|
||||
const mockAuthContext = createMockAuthContext({
|
||||
session: createMockSession(),
|
||||
loading: false,
|
||||
});
|
||||
mockUseAuth.mockReturnValue(mockAuthContext);
|
||||
|
||||
render(
|
||||
<RouteGuard>
|
||||
<div data-testid="protected-content">Protected Content</div>
|
||||
</RouteGuard>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('protected-content')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show loading state when auth context is loading', () => {
|
||||
const mockAuthContext = createMockAuthContext({
|
||||
session: null,
|
||||
loading: true,
|
||||
});
|
||||
mockUseAuth.mockReturnValue(mockAuthContext);
|
||||
|
||||
render(
|
||||
<RouteGuard>
|
||||
<div data-testid="protected-content">Protected Content</div>
|
||||
</RouteGuard>
|
||||
);
|
||||
|
||||
// Should show loading state, not children
|
||||
expect(screen.queryByTestId('protected-content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should redirect when user is not authenticated', async () => {
|
||||
const mockAuthContext = createMockAuthContext({
|
||||
session: null,
|
||||
loading: false,
|
||||
});
|
||||
mockUseAuth.mockReturnValue(mockAuthContext);
|
||||
|
||||
render(
|
||||
<RouteGuard>
|
||||
<div data-testid="protected-content">Protected Content</div>
|
||||
</RouteGuard>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/auth/login');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom Configuration', () => {
|
||||
it('should use custom redirect path when specified', async () => {
|
||||
const mockAuthContext = createMockAuthContext({
|
||||
session: null,
|
||||
loading: false,
|
||||
});
|
||||
mockUseAuth.mockReturnValue(mockAuthContext);
|
||||
|
||||
render(
|
||||
<RouteGuard config={{ unauthorizedRedirectPath: '/custom-login' }}>
|
||||
<div data-testid="protected-content">Protected Content</div>
|
||||
</RouteGuard>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/custom-login');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not redirect when redirectOnUnauthorized is false', async () => {
|
||||
const mockAuthContext = createMockAuthContext({
|
||||
session: null,
|
||||
loading: false,
|
||||
});
|
||||
mockUseAuth.mockReturnValue(mockAuthContext);
|
||||
|
||||
render(
|
||||
<RouteGuard config={{ redirectOnUnauthorized: false }}>
|
||||
<div data-testid="protected-content">Protected Content</div>
|
||||
</RouteGuard>
|
||||
);
|
||||
|
||||
// Wait for any potential redirects
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
expect(mockRouter.push).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show unauthorized component when redirect is disabled', async () => {
|
||||
const mockAuthContext = createMockAuthContext({
|
||||
session: null,
|
||||
loading: false,
|
||||
});
|
||||
mockUseAuth.mockReturnValue(mockAuthContext);
|
||||
|
||||
const unauthorizedComponent = <div data-testid="unauthorized">Access Denied</div>;
|
||||
|
||||
render(
|
||||
<RouteGuard
|
||||
config={{ redirectOnUnauthorized: false }}
|
||||
unauthorizedComponent={unauthorizedComponent}
|
||||
>
|
||||
<div data-testid="protected-content">Protected Content</div>
|
||||
</RouteGuard>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('unauthorized')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom Loading Component', () => {
|
||||
it('should show custom loading component when specified', () => {
|
||||
const mockAuthContext = createMockAuthContext({
|
||||
session: null,
|
||||
loading: true,
|
||||
});
|
||||
mockUseAuth.mockReturnValue(mockAuthContext);
|
||||
|
||||
const loadingComponent = <div data-testid="custom-loading">Custom Loading...</div>;
|
||||
|
||||
render(
|
||||
<RouteGuard loadingComponent={loadingComponent}>
|
||||
<div data-testid="protected-content">Protected Content</div>
|
||||
</RouteGuard>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('custom-loading')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('protected-content')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Role-Based Access', () => {
|
||||
it('should allow access when user has required role', async () => {
|
||||
const mockAuthContext = createMockAuthContext({
|
||||
session: createMockSession({
|
||||
user: {
|
||||
userId: 'user-123',
|
||||
email: 'admin@example.com',
|
||||
displayName: 'Admin User',
|
||||
role: 'admin',
|
||||
},
|
||||
}),
|
||||
loading: false,
|
||||
});
|
||||
mockUseAuth.mockReturnValue(mockAuthContext);
|
||||
|
||||
render(
|
||||
<RouteGuard config={{ requiredRoles: ['admin'] }}>
|
||||
<div data-testid="protected-content">Protected Content</div>
|
||||
</RouteGuard>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('protected-content')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect when user lacks required role', async () => {
|
||||
const mockAuthContext = createMockAuthContext({
|
||||
session: createMockSession({
|
||||
user: {
|
||||
userId: 'user-123',
|
||||
email: 'user@example.com',
|
||||
displayName: 'Regular User',
|
||||
role: 'user',
|
||||
},
|
||||
}),
|
||||
loading: false,
|
||||
});
|
||||
mockUseAuth.mockReturnValue(mockAuthContext);
|
||||
|
||||
render(
|
||||
<RouteGuard config={{ requiredRoles: ['admin'] }}>
|
||||
<div data-testid="protected-content">Protected Content</div>
|
||||
</RouteGuard>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/auth/login');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle undefined session gracefully', async () => {
|
||||
const mockAuthContext = createMockAuthContext({
|
||||
session: undefined as any,
|
||||
loading: false,
|
||||
});
|
||||
mockUseAuth.mockReturnValue(mockAuthContext);
|
||||
|
||||
render(
|
||||
<RouteGuard>
|
||||
<div data-testid="protected-content">Protected Content</div>
|
||||
</RouteGuard>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/auth/login');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty required roles array', async () => {
|
||||
const mockAuthContext = createMockAuthContext({
|
||||
session: createMockSession(),
|
||||
loading: false,
|
||||
});
|
||||
mockUseAuth.mockReturnValue(mockAuthContext);
|
||||
|
||||
render(
|
||||
<RouteGuard config={{ requiredRoles: [] }}>
|
||||
<div data-testid="protected-content">Protected Content</div>
|
||||
</RouteGuard>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('protected-content')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle rapid session state changes', async () => {
|
||||
const mockAuthContext = createMockAuthContext({
|
||||
session: null,
|
||||
loading: true,
|
||||
});
|
||||
mockUseAuth.mockReturnValue(mockAuthContext);
|
||||
|
||||
const { rerender } = render(
|
||||
<RouteGuard>
|
||||
<div data-testid="protected-content">Protected Content</div>
|
||||
</RouteGuard>
|
||||
);
|
||||
|
||||
// Simulate session becoming available
|
||||
mockAuthContext.session = createMockSession();
|
||||
mockAuthContext.loading = false;
|
||||
|
||||
rerender(
|
||||
<RouteGuard>
|
||||
<div data-testid="protected-content">Protected Content</div>
|
||||
</RouteGuard>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('protected-content')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Redirect Timing', () => {
|
||||
it('should wait before redirecting (500ms delay)', async () => {
|
||||
const mockAuthContext = createMockAuthContext({
|
||||
session: null,
|
||||
loading: false,
|
||||
});
|
||||
mockUseAuth.mockReturnValue(mockAuthContext);
|
||||
|
||||
render(
|
||||
<RouteGuard>
|
||||
<div data-testid="protected-content">Protected Content</div>
|
||||
</RouteGuard>
|
||||
);
|
||||
|
||||
// Should not redirect immediately
|
||||
expect(mockRouter.push).not.toHaveBeenCalled();
|
||||
|
||||
// Wait for the delay
|
||||
await waitFor(() => {
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/auth/login');
|
||||
}, { timeout: 1000 });
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user