feat(control-library): document-grouped batching, generation strategy tracking, sort by source
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
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>
This commit is contained in:
322
admin-compliance/app/sdk/control-library/__tests__/page.test.tsx
Normal file
322
admin-compliance/app/sdk/control-library/__tests__/page.test.tsx
Normal file
@@ -0,0 +1,322 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user