feat(frontend-auth): complete NextAuth.js Keycloak integration with middleware, hooks, and API utility
- Add middleware.ts for route protection (redirects unauthenticated users to /login) - Add useActiveClub() hook for managing active club context (localStorage + session) - Add apiClient() fetch wrapper with automatic Authorization + X-Tenant-Id headers - Configure vitest with jsdom environment and global test setup - Add comprehensive test coverage: 16/16 tests passing (hooks + API utility) - Install test dependencies: vitest, @testing-library/react, @vitejs/plugin-react, happy-dom Task 10 COMPLETE - all acceptance criteria met
This commit is contained in:
127
frontend/src/hooks/__tests__/useActiveClub.test.ts
Normal file
127
frontend/src/hooks/__tests__/useActiveClub.test.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useActiveClub } from '../useActiveClub';
|
||||
import type { Session } from 'next-auth';
|
||||
|
||||
const mockUseSession = vi.fn();
|
||||
|
||||
vi.mock('next-auth/react', () => ({
|
||||
useSession: () => mockUseSession(),
|
||||
}));
|
||||
|
||||
describe('useActiveClub', () => {
|
||||
let localStorageData: Record<string, string> = {};
|
||||
|
||||
beforeEach(() => {
|
||||
localStorageData = {};
|
||||
|
||||
mockUseSession.mockReturnValue({
|
||||
data: {
|
||||
user: {
|
||||
id: '1',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
clubs: {
|
||||
'club-1': 'owner',
|
||||
'club-2': 'member',
|
||||
'club-3': 'admin',
|
||||
},
|
||||
},
|
||||
accessToken: 'mock-token',
|
||||
expires: '2099-01-01',
|
||||
},
|
||||
status: 'authenticated',
|
||||
});
|
||||
|
||||
vi.mocked(localStorage.getItem).mockImplementation((key: string) => {
|
||||
return localStorageData[key] || null;
|
||||
});
|
||||
|
||||
vi.mocked(localStorage.setItem).mockImplementation((key: string, value: string) => {
|
||||
localStorageData[key] = value;
|
||||
});
|
||||
|
||||
vi.mocked(localStorage.clear).mockImplementation(() => {
|
||||
localStorageData = {};
|
||||
});
|
||||
});
|
||||
|
||||
it('should return first club from session when localStorage is empty', () => {
|
||||
const { result } = renderHook(() => useActiveClub());
|
||||
|
||||
expect(result.current.activeClubId).toBe('club-1');
|
||||
expect(result.current.role).toBe('owner');
|
||||
});
|
||||
|
||||
it('should return active club from localStorage if valid', () => {
|
||||
localStorageData['activeClubId'] = 'club-2';
|
||||
|
||||
const { result } = renderHook(() => useActiveClub());
|
||||
|
||||
expect(result.current.activeClubId).toBe('club-2');
|
||||
expect(result.current.role).toBe('member');
|
||||
});
|
||||
|
||||
it('should fallback to first club if localStorage contains invalid club ID', () => {
|
||||
localStorageData['activeClubId'] = 'invalid-club';
|
||||
|
||||
const { result } = renderHook(() => useActiveClub());
|
||||
|
||||
expect(result.current.activeClubId).toBe('club-1');
|
||||
expect(result.current.role).toBe('owner');
|
||||
});
|
||||
|
||||
it('should update localStorage when setActiveClub is called', () => {
|
||||
const { result } = renderHook(() => useActiveClub());
|
||||
|
||||
act(() => {
|
||||
result.current.setActiveClub('club-3');
|
||||
});
|
||||
|
||||
expect(result.current.activeClubId).toBe('club-3');
|
||||
expect(result.current.role).toBe('admin');
|
||||
expect(localStorageData['activeClubId']).toBe('club-3');
|
||||
});
|
||||
|
||||
it('should return null when no session exists', () => {
|
||||
mockUseSession.mockReturnValueOnce({
|
||||
data: null,
|
||||
status: 'unauthenticated',
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useActiveClub());
|
||||
|
||||
expect(result.current.activeClubId).toBeNull();
|
||||
expect(result.current.role).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when user has no clubs', () => {
|
||||
mockUseSession.mockReturnValueOnce({
|
||||
data: {
|
||||
user: {
|
||||
id: '1',
|
||||
name: 'Test User',
|
||||
clubs: {},
|
||||
},
|
||||
accessToken: 'mock-token',
|
||||
expires: '2099-01-01',
|
||||
},
|
||||
status: 'authenticated',
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useActiveClub());
|
||||
|
||||
expect(result.current.activeClubId).toBeNull();
|
||||
expect(result.current.role).toBeNull();
|
||||
});
|
||||
|
||||
it('should return all clubs from session', () => {
|
||||
const { result } = renderHook(() => useActiveClub());
|
||||
|
||||
expect(result.current.clubs).toEqual({
|
||||
'club-1': 'owner',
|
||||
'club-2': 'member',
|
||||
'club-3': 'admin',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user