All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 31s
CI/CD / test-python-backend-compliance (push) Successful in 31s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 18s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Successful in 2s
- Group chunks by regulation_code before batching for better LLM context - Add generation_strategy column (ungrouped=v1, document_grouped=v2) - Add v1/v2 badge to control cards in frontend - Add sort-by-source option with visual group headers - Add frontend page tests (18 tests) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
323 lines
9.7 KiB
TypeScript
323 lines
9.7 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
import { render, screen, waitFor, fireEvent, act } from '@testing-library/react'
|
|
import ControlLibraryPage from '../page'
|
|
|
|
// ============================================================================
|
|
// Mock data
|
|
// ============================================================================
|
|
|
|
const MOCK_FRAMEWORK = {
|
|
id: 'fw-1',
|
|
framework_id: 'bp_security_v1',
|
|
name: 'BreakPilot Security',
|
|
version: '1.0',
|
|
description: 'Test framework',
|
|
release_state: 'draft',
|
|
}
|
|
|
|
const MOCK_CONTROL = {
|
|
id: 'ctrl-1',
|
|
framework_id: 'fw-1',
|
|
control_id: 'AUTH-001',
|
|
title: 'Multi-Factor Authentication',
|
|
objective: 'Require MFA for all admin accounts.',
|
|
rationale: 'Passwords alone are insufficient.',
|
|
scope: {},
|
|
requirements: ['MFA for admin'],
|
|
test_procedure: ['Test admin login'],
|
|
evidence: [{ type: 'config', description: 'MFA enabled' }],
|
|
severity: 'high',
|
|
risk_score: 4.0,
|
|
implementation_effort: 'm',
|
|
evidence_confidence: null,
|
|
open_anchors: [{ framework: 'OWASP', ref: 'V2.8', url: 'https://owasp.org' }],
|
|
release_state: 'draft',
|
|
tags: ['mfa'],
|
|
license_rule: 1,
|
|
source_original_text: null,
|
|
source_citation: { source: 'DSGVO' },
|
|
customer_visible: true,
|
|
verification_method: 'automated',
|
|
category: 'authentication',
|
|
target_audience: 'developer',
|
|
generation_metadata: null,
|
|
generation_strategy: 'ungrouped',
|
|
created_at: '2026-03-15T10:00:00+00:00',
|
|
updated_at: '2026-03-15T10:00:00+00:00',
|
|
}
|
|
|
|
const MOCK_META = {
|
|
total: 1,
|
|
domains: [{ domain: 'AUTH', count: 1 }],
|
|
sources: [{ source: 'DSGVO', count: 1 }],
|
|
no_source_count: 0,
|
|
}
|
|
|
|
// ============================================================================
|
|
// Fetch mock
|
|
// ============================================================================
|
|
|
|
function createFetchMock(overrides?: Record<string, unknown>) {
|
|
const responses: Record<string, unknown> = {
|
|
frameworks: [MOCK_FRAMEWORK],
|
|
controls: [MOCK_CONTROL],
|
|
'controls-count': { total: 1 },
|
|
'controls-meta': MOCK_META,
|
|
...overrides,
|
|
}
|
|
|
|
return vi.fn((url: string) => {
|
|
const urlStr = typeof url === 'string' ? url : ''
|
|
// Match endpoint param
|
|
const match = urlStr.match(/endpoint=([^&]+)/)
|
|
const endpoint = match?.[1] || ''
|
|
const data = responses[endpoint] ?? []
|
|
|
|
return Promise.resolve({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(data),
|
|
})
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tests
|
|
// ============================================================================
|
|
|
|
describe('ControlLibraryPage', () => {
|
|
let fetchMock: ReturnType<typeof createFetchMock>
|
|
|
|
beforeEach(() => {
|
|
fetchMock = createFetchMock()
|
|
global.fetch = fetchMock as unknown as typeof fetch
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
it('renders the page header', async () => {
|
|
render(<ControlLibraryPage />)
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Canonical Control Library')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('shows control count from meta', async () => {
|
|
fetchMock = createFetchMock({ 'controls-meta': { ...MOCK_META, total: 42 } })
|
|
global.fetch = fetchMock as unknown as typeof fetch
|
|
|
|
render(<ControlLibraryPage />)
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/42 Security Controls/)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('renders control list with data', async () => {
|
|
render(<ControlLibraryPage />)
|
|
await waitFor(() => {
|
|
expect(screen.getByText('AUTH-001')).toBeInTheDocument()
|
|
expect(screen.getByText('Multi-Factor Authentication')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('shows timestamp on control cards', async () => {
|
|
render(<ControlLibraryPage />)
|
|
await waitFor(() => {
|
|
// The date should be rendered in German locale format
|
|
expect(screen.getByText(/15\.03\.26/)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('shows source citation on control cards', async () => {
|
|
render(<ControlLibraryPage />)
|
|
await waitFor(() => {
|
|
expect(screen.getByText('DSGVO')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('fetches with limit and offset params', async () => {
|
|
render(<ControlLibraryPage />)
|
|
await waitFor(() => {
|
|
expect(fetchMock).toHaveBeenCalled()
|
|
})
|
|
|
|
// Find the controls fetch call
|
|
const controlsCalls = fetchMock.mock.calls.filter(
|
|
(call: unknown[]) => typeof call[0] === 'string' && (call[0] as string).includes('endpoint=controls&')
|
|
)
|
|
expect(controlsCalls.length).toBeGreaterThan(0)
|
|
|
|
const url = controlsCalls[0][0] as string
|
|
expect(url).toContain('limit=50')
|
|
expect(url).toContain('offset=0')
|
|
})
|
|
|
|
it('fetches controls-count alongside controls', async () => {
|
|
render(<ControlLibraryPage />)
|
|
await waitFor(() => {
|
|
const countCalls = fetchMock.mock.calls.filter(
|
|
(call: unknown[]) => typeof call[0] === 'string' && (call[0] as string).includes('endpoint=controls-count')
|
|
)
|
|
expect(countCalls.length).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
|
|
it('fetches controls-meta on mount', async () => {
|
|
render(<ControlLibraryPage />)
|
|
await waitFor(() => {
|
|
const metaCalls = fetchMock.mock.calls.filter(
|
|
(call: unknown[]) => typeof call[0] === 'string' && (call[0] as string).includes('endpoint=controls-meta')
|
|
)
|
|
expect(metaCalls.length).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
|
|
it('renders domain dropdown from meta', async () => {
|
|
render(<ControlLibraryPage />)
|
|
await waitFor(() => {
|
|
expect(screen.getByText('AUTH (1)')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('renders source dropdown from meta', async () => {
|
|
render(<ControlLibraryPage />)
|
|
await waitFor(() => {
|
|
// The source option should appear in the dropdown
|
|
expect(screen.getByText('DSGVO (1)')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('has sort dropdown with all sort options', async () => {
|
|
render(<ControlLibraryPage />)
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Sortierung: ID')).toBeInTheDocument()
|
|
expect(screen.getByText('Nach Quelle')).toBeInTheDocument()
|
|
expect(screen.getByText('Neueste zuerst')).toBeInTheDocument()
|
|
expect(screen.getByText('Aelteste zuerst')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('sends sort params when sorting by newest', async () => {
|
|
render(<ControlLibraryPage />)
|
|
await waitFor(() => {
|
|
expect(screen.getByText('AUTH-001')).toBeInTheDocument()
|
|
})
|
|
|
|
// Clear previous calls
|
|
fetchMock.mockClear()
|
|
|
|
// Change sort to newest
|
|
const sortSelect = screen.getByDisplayValue('Sortierung: ID')
|
|
await act(async () => {
|
|
fireEvent.change(sortSelect, { target: { value: 'newest' } })
|
|
})
|
|
|
|
await waitFor(() => {
|
|
const controlsCalls = fetchMock.mock.calls.filter(
|
|
(call: unknown[]) => typeof call[0] === 'string' && (call[0] as string).includes('endpoint=controls&')
|
|
)
|
|
expect(controlsCalls.length).toBeGreaterThan(0)
|
|
const url = controlsCalls[0][0] as string
|
|
expect(url).toContain('sort=created_at')
|
|
expect(url).toContain('order=desc')
|
|
})
|
|
})
|
|
|
|
it('sends search param after debounce', async () => {
|
|
render(<ControlLibraryPage />)
|
|
await waitFor(() => {
|
|
expect(screen.getByText('AUTH-001')).toBeInTheDocument()
|
|
})
|
|
|
|
fetchMock.mockClear()
|
|
|
|
const searchInput = screen.getByPlaceholderText(/Controls durchsuchen/)
|
|
await act(async () => {
|
|
fireEvent.change(searchInput, { target: { value: 'encryption' } })
|
|
})
|
|
|
|
// Wait for debounce (400ms)
|
|
await waitFor(
|
|
() => {
|
|
const controlsCalls = fetchMock.mock.calls.filter(
|
|
(call: unknown[]) => typeof call[0] === 'string' && (call[0] as string).includes('search=encryption')
|
|
)
|
|
expect(controlsCalls.length).toBeGreaterThan(0)
|
|
},
|
|
{ timeout: 1000 }
|
|
)
|
|
})
|
|
|
|
it('shows empty state when no controls', async () => {
|
|
fetchMock = createFetchMock({
|
|
controls: [],
|
|
'controls-count': { total: 0 },
|
|
'controls-meta': { ...MOCK_META, total: 0 },
|
|
})
|
|
global.fetch = fetchMock as unknown as typeof fetch
|
|
|
|
render(<ControlLibraryPage />)
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Noch keine Controls/)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('shows "Keine Controls gefunden" when filter matches nothing', async () => {
|
|
fetchMock = createFetchMock({
|
|
controls: [],
|
|
'controls-count': { total: 0 },
|
|
'controls-meta': { ...MOCK_META, total: 50 },
|
|
})
|
|
global.fetch = fetchMock as unknown as typeof fetch
|
|
|
|
render(<ControlLibraryPage />)
|
|
|
|
// Wait for initial load to finish
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText(/Controls durchsuchen/)).toBeInTheDocument()
|
|
})
|
|
|
|
// Trigger a search to have a filter active
|
|
const searchInput = screen.getByPlaceholderText(/Controls durchsuchen/)
|
|
await act(async () => {
|
|
fireEvent.change(searchInput, { target: { value: 'zzzzzzz' } })
|
|
})
|
|
|
|
await waitFor(
|
|
() => {
|
|
expect(screen.getByText('Keine Controls gefunden.')).toBeInTheDocument()
|
|
},
|
|
{ timeout: 1000 }
|
|
)
|
|
})
|
|
|
|
it('has a refresh button', async () => {
|
|
render(<ControlLibraryPage />)
|
|
await waitFor(() => {
|
|
expect(screen.getByTitle('Aktualisieren')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('renders pagination info', async () => {
|
|
render(<ControlLibraryPage />)
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Seite 1 von 1/)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('shows pagination buttons for many controls', async () => {
|
|
fetchMock = createFetchMock({
|
|
'controls-count': { total: 150 },
|
|
'controls-meta': { ...MOCK_META, total: 150 },
|
|
})
|
|
global.fetch = fetchMock as unknown as typeof fetch
|
|
|
|
render(<ControlLibraryPage />)
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Seite 1 von 3/)).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|