413ebaeaee
- Wrap Navigation tests in RoleSwitcherProvider to fix 6 test failures - Use remote CI server URL (euchre-ci.notsosm.art) for acceptance tests in CI - Fix DATABASE_URL reference from vars to secrets in PR workflow
209 lines
5.6 KiB
TypeScript
209 lines
5.6 KiB
TypeScript
/**
|
|
* Epic 1: Authentication & User Management
|
|
* Component Test: Navigation
|
|
*
|
|
* Tests the navigation component for proper display based on session state
|
|
*/
|
|
|
|
import { describe, it, expect, mock, beforeEach, afterEach } from 'bun:test'
|
|
import { render, screen, waitFor } from '@testing-library/react'
|
|
import Navigation from '@/components/Navigation'
|
|
import { RoleSwitcherProvider } from '@/components/RoleSwitcher'
|
|
|
|
// Mock next/link
|
|
mock.module('next/link', () => ({
|
|
default: ({ children, href }: { children: React.ReactNode; href: string }) => (
|
|
<a href={href}>{children}</a>
|
|
),
|
|
}))
|
|
|
|
// Mock the SessionProvider
|
|
mock.module('@/components/SessionProvider', () => ({
|
|
useSession: mock(() => ({ session: null, loading: false, refreshSession: mock(() => {}) })),
|
|
}))
|
|
|
|
// Mock the auth-client
|
|
mock.module('@/lib/auth-client', () => ({
|
|
authClient: {
|
|
signOut: mock(() => {}),
|
|
},
|
|
}))
|
|
|
|
// Mock fetch for role API call
|
|
global.fetch = mock(async () => new Response()) as any
|
|
|
|
function renderNavigation() {
|
|
return render(
|
|
<RoleSwitcherProvider>
|
|
<Navigation />
|
|
</RoleSwitcherProvider>
|
|
)
|
|
}
|
|
|
|
import { useSession as useSessionOriginal } from '@/components/SessionProvider'
|
|
const useSession = useSessionOriginal as any
|
|
|
|
describe('Epic 1: Navigation Component', () => {
|
|
beforeEach(() => {
|
|
// Don't clear all mocks as it might affect bun-setup.ts
|
|
// Set up default fetch mock
|
|
(global.fetch as any).mockImplementation?.(async (url: any) => {
|
|
if (url?.toString().includes('/api/users/')) {
|
|
return new Response(JSON.stringify({ role: 'player' }), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
}
|
|
return new Response(JSON.stringify({}), { status: 200 })
|
|
})
|
|
})
|
|
|
|
afterEach(() => {
|
|
// Don't clear mocks - let them persist for other test files
|
|
// Navigation relies on module mocks set up at file level
|
|
})
|
|
|
|
it('renders the logo and basic links when not logged in', () => {
|
|
(useSession).mockReturnValue({
|
|
session: null,
|
|
loading: false,
|
|
refreshSession: mock(() => {}),
|
|
})
|
|
|
|
renderNavigation()
|
|
|
|
expect(screen.getByText('EuchreCamp')).toBeInTheDocument()
|
|
expect(screen.getByText('Rankings')).toBeInTheDocument()
|
|
expect(screen.getByText('Sign in')).toBeInTheDocument()
|
|
expect(screen.getByText('Sign up')).toBeInTheDocument()
|
|
})
|
|
|
|
it('shows user menu when logged in', async () => {
|
|
(useSession).mockReturnValue({
|
|
session: {
|
|
user: {
|
|
id: 'user-123',
|
|
email: 'test@example.com',
|
|
name: 'Test User',
|
|
role: 'player'
|
|
},
|
|
session: { token: 'abc123' },
|
|
},
|
|
loading: false,
|
|
refreshSession: mock(() => {}),
|
|
})
|
|
|
|
renderNavigation()
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Test User')).toBeInTheDocument()
|
|
})
|
|
|
|
expect(screen.getByText('Sign out')).toBeInTheDocument()
|
|
expect(screen.getByText('Tournaments')).toBeInTheDocument()
|
|
})
|
|
|
|
it('shows Tournaments link when logged in', async () => {
|
|
(useSession).mockReturnValue({
|
|
session: {
|
|
user: {
|
|
id: 'user-123',
|
|
email: 'test@example.com',
|
|
name: 'Test User',
|
|
role: 'player'
|
|
},
|
|
session: { token: 'abc123' },
|
|
},
|
|
loading: false,
|
|
refreshSession: mock(() => {}),
|
|
})
|
|
|
|
renderNavigation()
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Tournaments')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('shows admin link for club_admin role', async () => {
|
|
(useSession).mockReturnValue({
|
|
session: {
|
|
user: {
|
|
id: 'admin-123',
|
|
email: 'admin@example.com',
|
|
name: 'Admin User',
|
|
role: 'club_admin',
|
|
},
|
|
session: { token: 'abc123' },
|
|
},
|
|
loading: false,
|
|
refreshSession: mock(() => {}),
|
|
});
|
|
|
|
// Mock fetch to return club_admin role
|
|
(global.fetch as any).mockImplementation(async (url: any) => {
|
|
if (url?.toString().includes('/api/users/admin-123/role')) {
|
|
return new Response(JSON.stringify({ role: 'club_admin' }), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
}
|
|
return new Response(JSON.stringify({}), { status: 200 })
|
|
})
|
|
|
|
renderNavigation()
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Admin')).toBeInTheDocument()
|
|
}, { timeout: 3000 })
|
|
expect(screen.getByText('Tournaments')).toBeInTheDocument()
|
|
})
|
|
|
|
it('hides admin link for non-admin users', async () => {
|
|
(useSession).mockReturnValue({
|
|
session: {
|
|
user: {
|
|
id: 'player-123',
|
|
email: 'player@example.com',
|
|
name: 'Player User',
|
|
role: 'player',
|
|
},
|
|
session: { token: 'abc123' },
|
|
},
|
|
loading: false,
|
|
refreshSession: mock(() => {}),
|
|
});
|
|
|
|
// Mock fetch to return player role (already set in beforeEach, but explicit here)
|
|
(global.fetch as any).mockImplementation(async (url: any) => {
|
|
if (url?.toString().includes('/api/users/')) {
|
|
return {
|
|
json: () => Promise.resolve({ role: 'player' }),
|
|
} as Response
|
|
}
|
|
return {
|
|
json: () => Promise.resolve({}),
|
|
} as Response
|
|
})
|
|
|
|
renderNavigation()
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Tournaments')).toBeInTheDocument()
|
|
})
|
|
expect(screen.queryByText('Admin')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('shows loading state', () => {
|
|
(useSession).mockReturnValue({
|
|
session: null,
|
|
loading: true,
|
|
refreshSession: mock(() => {}),
|
|
})
|
|
|
|
renderNavigation()
|
|
|
|
expect(screen.getByText('Loading...')).toBeInTheDocument()
|
|
})
|
|
})
|