352 lines
13 KiB
TypeScript
352 lines
13 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import IdentityCheckOverlay from '@/components/IdentityCheckOverlay'
|
|
import { inventoryApi } from '@/lib/api'
|
|
import * as toast from 'react-hot-toast'
|
|
|
|
vi.mock('@/lib/api')
|
|
vi.mock('react-hot-toast')
|
|
|
|
// ============================================================================
|
|
// HELPER: Render with Props
|
|
// ============================================================================
|
|
|
|
const renderIdentityCheckOverlay = (props = {}) => {
|
|
const defaultProps = {
|
|
show: true,
|
|
users: [
|
|
{ id: 1, username: 'admin', email: 'admin@test.com' },
|
|
{ id: 2, username: 'operator', email: 'op@test.com' },
|
|
],
|
|
onAuthenticated: vi.fn(),
|
|
...props,
|
|
}
|
|
return render(<IdentityCheckOverlay {...defaultProps} />)
|
|
}
|
|
|
|
describe('IdentityCheckOverlay Component', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS: Rendering & Visibility
|
|
// ============================================================================
|
|
|
|
describe('Rendering', () => {
|
|
it('should render overlay when show is true', () => {
|
|
const { container } = renderIdentityCheckOverlay({ show: true })
|
|
const overlay = container.querySelector('.fixed')
|
|
expect(overlay).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not render when show is false', () => {
|
|
const { container } = renderIdentityCheckOverlay({ show: false })
|
|
expect(container.querySelector('.fixed')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should display overlay title', () => {
|
|
const { container } = renderIdentityCheckOverlay()
|
|
expect(container.textContent).toContain('Protocol Access')
|
|
})
|
|
|
|
it('should display overlay subtitle', () => {
|
|
const { container } = renderIdentityCheckOverlay()
|
|
expect(container.textContent).toContain('Select operator')
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS: User List Rendering
|
|
// ============================================================================
|
|
|
|
describe('User Selection', () => {
|
|
it('should render user buttons', () => {
|
|
const users = [
|
|
{ id: 1, username: 'admin', email: 'admin@test.com' },
|
|
{ id: 2, username: 'operator', email: 'op@test.com' },
|
|
]
|
|
const { container } = renderIdentityCheckOverlay({ users })
|
|
expect(container.textContent).toContain('admin')
|
|
expect(container.textContent).toContain('operator')
|
|
})
|
|
|
|
it('should display user names in clickable buttons', () => {
|
|
const { container } = renderIdentityCheckOverlay()
|
|
const buttons = container.querySelectorAll('button')
|
|
const userButtons = Array.from(buttons).filter(btn =>
|
|
btn.textContent?.includes('admin')
|
|
)
|
|
expect(userButtons.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should render with empty user list', () => {
|
|
const { container } = renderIdentityCheckOverlay({ users: [] })
|
|
const modal = container.querySelector('.fixed')
|
|
expect(modal).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render with single user', () => {
|
|
const { container } = renderIdentityCheckOverlay({
|
|
users: [{ id: 1, username: 'solo', email: 'solo@test.com' }],
|
|
})
|
|
expect(container.textContent).toContain('solo')
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS: Login Form Rendering
|
|
// ============================================================================
|
|
|
|
describe('Login Form', () => {
|
|
it('should render form with proper modal structure', () => {
|
|
const { container } = renderIdentityCheckOverlay()
|
|
const modal = container.querySelector('.fixed')
|
|
expect(modal).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render local login options for users', async () => {
|
|
const { container } = renderIdentityCheckOverlay()
|
|
const users = container.querySelectorAll('button')
|
|
expect(users.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should have interactive form elements', async () => {
|
|
const { container } = renderIdentityCheckOverlay()
|
|
const buttons = container.querySelectorAll('button')
|
|
buttons.forEach(btn => {
|
|
expect(btn).toHaveProperty('onclick')
|
|
})
|
|
})
|
|
|
|
it('should display login options', () => {
|
|
const { container } = renderIdentityCheckOverlay()
|
|
const modal = container.querySelector('.fixed')
|
|
expect(modal?.textContent).toContain('Protocol Access')
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS: Form Validation
|
|
// ============================================================================
|
|
|
|
describe('Form Validation', () => {
|
|
it('should render form with username/password inputs', async () => {
|
|
const onAuthenticated = vi.fn()
|
|
const { container } = renderIdentityCheckOverlay({ onAuthenticated })
|
|
const inputs = container.querySelectorAll('input')
|
|
expect(inputs.length).toBeGreaterThanOrEqual(0)
|
|
})
|
|
|
|
it('should render user selection for local login', async () => {
|
|
const { container } = renderIdentityCheckOverlay()
|
|
const users = container.textContent
|
|
expect(users).toContain('admin')
|
|
})
|
|
|
|
it('should render form fields in overlay', async () => {
|
|
const { container } = renderIdentityCheckOverlay()
|
|
const modal = container.querySelector('.fixed')
|
|
expect(modal).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle API rejection on invalid credentials', async () => {
|
|
vi.mocked(inventoryApi.login).mockRejectedValue(
|
|
new Error('Invalid credentials')
|
|
)
|
|
const { container } = renderIdentityCheckOverlay()
|
|
const overlay = container.querySelector('.fixed')
|
|
expect(overlay).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS: LDAP Authentication Flow
|
|
// ============================================================================
|
|
|
|
describe('LDAP Authentication', () => {
|
|
it('should mock API for successful LDAP login', async () => {
|
|
vi.mocked(inventoryApi.login).mockResolvedValue({
|
|
id: 1,
|
|
username: 'enterprise_user',
|
|
token: 'mock-token',
|
|
})
|
|
const onAuthenticated = vi.fn()
|
|
const { container } = renderIdentityCheckOverlay({ onAuthenticated })
|
|
expect(inventoryApi.login).toBeDefined()
|
|
})
|
|
|
|
it('should render form even if LDAP server is unavailable', async () => {
|
|
vi.mocked(inventoryApi.login).mockRejectedValue(
|
|
new Error('LDAP server unreachable')
|
|
)
|
|
const { container } = renderIdentityCheckOverlay()
|
|
expect(container.querySelector('.fixed')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render form if user lacks group membership', async () => {
|
|
vi.mocked(inventoryApi.login).mockRejectedValue(
|
|
new Error('User not in authorized group')
|
|
)
|
|
const { container } = renderIdentityCheckOverlay()
|
|
expect(container.querySelector('.fixed')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS: Token Storage
|
|
// ============================================================================
|
|
|
|
describe('Token Storage & Callback', () => {
|
|
it('should pass onAuthenticated callback to component', async () => {
|
|
const mockUser = {
|
|
id: 1,
|
|
username: 'testuser',
|
|
token: 'mock-jwt-token',
|
|
}
|
|
vi.mocked(inventoryApi.login).mockResolvedValue(mockUser)
|
|
const onAuthenticated = vi.fn()
|
|
const { container } = renderIdentityCheckOverlay({ onAuthenticated })
|
|
expect(onAuthenticated).toBeDefined()
|
|
})
|
|
|
|
it('should render with mock authentication response', async () => {
|
|
const mockUser = {
|
|
id: 1,
|
|
username: 'operator',
|
|
token: 'token-123',
|
|
}
|
|
vi.mocked(inventoryApi.login).mockResolvedValue(mockUser)
|
|
const { container } = renderIdentityCheckOverlay()
|
|
expect(container.querySelector('.fixed')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render with JWT token mock', async () => {
|
|
const mockUser = {
|
|
id: 1,
|
|
username: 'admin',
|
|
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
|
}
|
|
vi.mocked(inventoryApi.login).mockResolvedValue(mockUser)
|
|
const onAuthenticated = vi.fn()
|
|
const { container } = renderIdentityCheckOverlay({ onAuthenticated })
|
|
expect(onAuthenticated).toBeDefined()
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS: Error Handling
|
|
// ============================================================================
|
|
|
|
describe('Error Handling', () => {
|
|
it('should render form even if login API fails', async () => {
|
|
const toastDefaultSpy = vi.spyOn(toast, 'default').mockImplementation(vi.fn())
|
|
vi.mocked(inventoryApi.login).mockRejectedValue(
|
|
new Error('Login failed')
|
|
)
|
|
const { container } = renderIdentityCheckOverlay()
|
|
expect(container.querySelector('.fixed')).toBeInTheDocument()
|
|
toastDefaultSpy.mockRestore()
|
|
})
|
|
|
|
it('should render form during network error', async () => {
|
|
vi.mocked(inventoryApi.login).mockRejectedValue(
|
|
new Error('Network error')
|
|
)
|
|
const { container } = renderIdentityCheckOverlay()
|
|
expect(container.querySelector('.fixed')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render form on invalid password', async () => {
|
|
vi.mocked(inventoryApi.login).mockRejectedValue(
|
|
new Error('Invalid password')
|
|
)
|
|
const { container } = renderIdentityCheckOverlay()
|
|
expect(container.querySelector('.fixed')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render form on auth failure', async () => {
|
|
vi.mocked(inventoryApi.login).mockRejectedValue(
|
|
new Error('Authentication failed')
|
|
)
|
|
const { container } = renderIdentityCheckOverlay()
|
|
expect(container.querySelector('.fixed')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render form during API timeout', async () => {
|
|
vi.mocked(inventoryApi.login).mockImplementation(
|
|
() => new Promise((_, reject) =>
|
|
setTimeout(() => reject(new Error('Timeout')), 100)
|
|
)
|
|
)
|
|
const { container } = renderIdentityCheckOverlay()
|
|
expect(container.querySelector('.fixed')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS: Accessibility
|
|
// ============================================================================
|
|
|
|
describe('Accessibility', () => {
|
|
it('should render proper modal structure', () => {
|
|
const { container } = renderIdentityCheckOverlay()
|
|
const modal = container.querySelector('.fixed')
|
|
expect(modal?.className).toContain('fixed')
|
|
})
|
|
|
|
it('should have interactive button elements', () => {
|
|
const { container } = renderIdentityCheckOverlay()
|
|
const buttons = container.querySelectorAll('button')
|
|
expect(buttons.length).toBeGreaterThan(0)
|
|
buttons.forEach(btn => expect(btn).toHaveProperty('click'))
|
|
})
|
|
|
|
it('should have form input elements', () => {
|
|
const { container } = renderIdentityCheckOverlay()
|
|
const inputs = container.querySelectorAll('input')
|
|
inputs.forEach(input => {
|
|
expect(input.type).toBeDefined()
|
|
})
|
|
})
|
|
|
|
it('should render icons in interface', () => {
|
|
const { container } = renderIdentityCheckOverlay()
|
|
const svgs = container.querySelectorAll('svg')
|
|
expect(svgs.length).toBeGreaterThanOrEqual(0)
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS: State Management
|
|
// ============================================================================
|
|
|
|
describe('State Management', () => {
|
|
it('should render form with auth callback', async () => {
|
|
vi.mocked(inventoryApi.login).mockResolvedValue({
|
|
id: 1,
|
|
username: 'user',
|
|
token: 'token',
|
|
})
|
|
const onAuthenticated = vi.fn()
|
|
const { container } = renderIdentityCheckOverlay({ onAuthenticated })
|
|
expect(onAuthenticated).toBeDefined()
|
|
})
|
|
|
|
it('should display user list across renders', () => {
|
|
const users = [
|
|
{ id: 1, username: 'user1', email: 'user1@test.com' },
|
|
{ id: 2, username: 'user2', email: 'user2@test.com' },
|
|
]
|
|
const { container } = renderIdentityCheckOverlay({ users })
|
|
expect(container.textContent).toContain('user1')
|
|
expect(container.textContent).toContain('user2')
|
|
})
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
})
|