A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
262 lines
7.8 KiB
TypeScript
262 lines
7.8 KiB
TypeScript
/**
|
|
* RiskHeatmap Component Tests
|
|
*
|
|
* Tests fuer die erweiterte Risiko-Matrix-Visualisierung
|
|
*/
|
|
|
|
import { describe, it, expect, vi } from 'vitest'
|
|
import { render, screen, fireEvent } from '@testing-library/react'
|
|
import RiskHeatmap, {
|
|
calculateRiskLevel,
|
|
MiniRiskMatrix,
|
|
RiskDistribution,
|
|
type Risk,
|
|
type Control,
|
|
} from '../../components/compliance/charts/RiskHeatmap'
|
|
|
|
// Mock-Daten
|
|
const mockRisks: Risk[] = [
|
|
{
|
|
id: '1',
|
|
risk_id: 'RISK-001',
|
|
title: 'Data Breach Risk',
|
|
description: 'Risiko eines Datenverlusts',
|
|
category: 'data_breach',
|
|
likelihood: 4,
|
|
impact: 5,
|
|
inherent_risk: 'critical',
|
|
mitigating_controls: ['PRIV-001', 'CRYPTO-001'],
|
|
residual_likelihood: 2,
|
|
residual_impact: 3,
|
|
residual_risk: 'medium',
|
|
owner: 'Security Team',
|
|
status: 'open',
|
|
treatment_plan: 'Encryption implementieren',
|
|
},
|
|
{
|
|
id: '2',
|
|
risk_id: 'RISK-002',
|
|
title: 'Compliance Gap',
|
|
category: 'compliance_gap',
|
|
likelihood: 3,
|
|
impact: 3,
|
|
inherent_risk: 'medium',
|
|
status: 'mitigated',
|
|
},
|
|
{
|
|
id: '3',
|
|
risk_id: 'RISK-003',
|
|
title: 'Vendor Risk',
|
|
category: 'vendor_risk',
|
|
likelihood: 2,
|
|
impact: 2,
|
|
inherent_risk: 'low',
|
|
status: 'accepted',
|
|
},
|
|
]
|
|
|
|
const mockControls: Control[] = [
|
|
{
|
|
id: 'c1',
|
|
control_id: 'PRIV-001',
|
|
title: 'Datenschutz-Massnahme',
|
|
domain: 'priv',
|
|
status: 'implemented',
|
|
},
|
|
{
|
|
id: 'c2',
|
|
control_id: 'CRYPTO-001',
|
|
title: 'Verschluesselung',
|
|
domain: 'crypto',
|
|
status: 'implemented',
|
|
},
|
|
]
|
|
|
|
describe('calculateRiskLevel', () => {
|
|
it('should return low for score 1-5', () => {
|
|
expect(calculateRiskLevel(1, 1)).toBe('low')
|
|
expect(calculateRiskLevel(1, 5)).toBe('low')
|
|
expect(calculateRiskLevel(5, 1)).toBe('low')
|
|
})
|
|
|
|
it('should return medium for score 6-11', () => {
|
|
expect(calculateRiskLevel(2, 3)).toBe('medium')
|
|
expect(calculateRiskLevel(3, 3)).toBe('medium')
|
|
expect(calculateRiskLevel(2, 5)).toBe('medium')
|
|
})
|
|
|
|
it('should return high for score 12-19', () => {
|
|
expect(calculateRiskLevel(3, 4)).toBe('high')
|
|
expect(calculateRiskLevel(4, 4)).toBe('high')
|
|
expect(calculateRiskLevel(3, 5)).toBe('high')
|
|
})
|
|
|
|
it('should return critical for score 20-25', () => {
|
|
expect(calculateRiskLevel(4, 5)).toBe('critical')
|
|
expect(calculateRiskLevel(5, 4)).toBe('critical')
|
|
expect(calculateRiskLevel(5, 5)).toBe('critical')
|
|
})
|
|
})
|
|
|
|
describe('RiskHeatmap', () => {
|
|
it('should render the component with risks', () => {
|
|
render(<RiskHeatmap risks={mockRisks} />)
|
|
|
|
// Statistik-Header sollte sichtbar sein
|
|
expect(screen.getByText('3')).toBeInTheDocument() // Gesamt
|
|
expect(screen.getByText('Critical')).toBeInTheDocument()
|
|
expect(screen.getByText('Medium')).toBeInTheDocument()
|
|
expect(screen.getByText('Low')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render empty state when no risks', () => {
|
|
render(<RiskHeatmap risks={[]} />)
|
|
|
|
expect(screen.getByText(/Keine Risiken vorhanden/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should display risk badges in cells', () => {
|
|
render(<RiskHeatmap risks={mockRisks} />)
|
|
|
|
// Risk-IDs sollten als Badges angezeigt werden
|
|
expect(screen.getByText('R001')).toBeInTheDocument()
|
|
expect(screen.getByText('R002')).toBeInTheDocument()
|
|
expect(screen.getByText('R003')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should support German language', () => {
|
|
render(<RiskHeatmap risks={mockRisks} lang="de" />)
|
|
|
|
expect(screen.getByText('Gesamt')).toBeInTheDocument()
|
|
expect(screen.getByText('Alle Kategorien')).toBeInTheDocument()
|
|
expect(screen.getByText('Alle Status')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should support English language', () => {
|
|
render(<RiskHeatmap risks={mockRisks} lang="en" />)
|
|
|
|
expect(screen.getByText('Total')).toBeInTheDocument()
|
|
expect(screen.getByText('All Categories')).toBeInTheDocument()
|
|
expect(screen.getByText('All Status')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should call onRiskClick when risk is clicked', () => {
|
|
const onRiskClick = vi.fn()
|
|
render(<RiskHeatmap risks={mockRisks} onRiskClick={onRiskClick} />)
|
|
|
|
const riskBadge = screen.getByText('R001')
|
|
fireEvent.click(riskBadge)
|
|
|
|
expect(onRiskClick).toHaveBeenCalledWith(mockRisks[0])
|
|
})
|
|
|
|
it('should filter by category', () => {
|
|
render(<RiskHeatmap risks={mockRisks} />)
|
|
|
|
const categorySelect = screen.getByDisplayValue('Alle Kategorien')
|
|
fireEvent.change(categorySelect, { target: { value: 'data_breach' } })
|
|
|
|
// Nur RISK-001 sollte sichtbar sein
|
|
expect(screen.getByText('R001')).toBeInTheDocument()
|
|
expect(screen.queryByText('R002')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should filter by status', () => {
|
|
render(<RiskHeatmap risks={mockRisks} />)
|
|
|
|
const statusSelect = screen.getByDisplayValue('Alle Status')
|
|
fireEvent.change(statusSelect, { target: { value: 'mitigated' } })
|
|
|
|
// Nur RISK-002 sollte sichtbar sein
|
|
expect(screen.queryByText('R001')).not.toBeInTheDocument()
|
|
expect(screen.getByText('R002')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show comparison view when enabled', () => {
|
|
render(<RiskHeatmap risks={mockRisks} showComparison={true} />)
|
|
|
|
// View-Mode Buttons sollten sichtbar sein
|
|
expect(screen.getByText('Vergleich')).toBeInTheDocument()
|
|
expect(screen.getByText('Residual')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should display linked controls for risk', () => {
|
|
render(<RiskHeatmap risks={mockRisks} controls={mockControls} />)
|
|
|
|
// Klicke auf das erste Risiko
|
|
const riskBadge = screen.getByText('R001')
|
|
fireEvent.click(riskBadge)
|
|
|
|
// Controls sollten in den Details erscheinen
|
|
expect(screen.getByText('PRIV-001')).toBeInTheDocument()
|
|
expect(screen.getByText('CRYPTO-001')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show risk movement summary in comparison mode', () => {
|
|
render(<RiskHeatmap risks={mockRisks} showComparison={true} />)
|
|
|
|
// Wechsle in Vergleichsmodus
|
|
fireEvent.click(screen.getByText('Vergleich'))
|
|
|
|
// Risikoveraenderung sollte angezeigt werden
|
|
expect(screen.getByText(/Critical reduziert|Critical reduced/i)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('MiniRiskMatrix', () => {
|
|
it('should render a compact matrix', () => {
|
|
const { container } = render(<MiniRiskMatrix risks={mockRisks} size="sm" />)
|
|
|
|
// 5x5 Grid = 25 Zellen
|
|
const cells = container.querySelectorAll('.w-6.h-6')
|
|
expect(cells.length).toBe(25)
|
|
})
|
|
|
|
it('should show risk counts in cells', () => {
|
|
render(<MiniRiskMatrix risks={mockRisks} />)
|
|
|
|
// RISK-001 bei L4/I5 = 1
|
|
// RISK-002 bei L3/I3 = 1
|
|
// RISK-003 bei L2/I2 = 1
|
|
const countElements = screen.getAllByText('1')
|
|
expect(countElements.length).toBeGreaterThanOrEqual(3)
|
|
})
|
|
|
|
it('should support different sizes', () => {
|
|
const { container, rerender } = render(<MiniRiskMatrix risks={mockRisks} size="sm" />)
|
|
expect(container.querySelector('.w-6')).toBeInTheDocument()
|
|
|
|
rerender(<MiniRiskMatrix risks={mockRisks} size="md" />)
|
|
expect(container.querySelector('.w-8')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('RiskDistribution', () => {
|
|
it('should render distribution bars', () => {
|
|
render(<RiskDistribution risks={mockRisks} />)
|
|
|
|
expect(screen.getByText('critical')).toBeInTheDocument()
|
|
expect(screen.getByText('high')).toBeInTheDocument()
|
|
expect(screen.getByText('medium')).toBeInTheDocument()
|
|
expect(screen.getByText('low')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show correct counts', () => {
|
|
render(<RiskDistribution risks={mockRisks} />)
|
|
|
|
// 1 critical, 0 high, 1 medium, 1 low
|
|
const counts = screen.getAllByText('1')
|
|
expect(counts.length).toBe(3) // critical, medium, low
|
|
|
|
// 0 high
|
|
expect(screen.getByText('0')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should support language prop', () => {
|
|
render(<RiskDistribution risks={mockRisks} lang="en" />)
|
|
|
|
// Englische Bezeichnungen werden verwendet
|
|
expect(screen.getByText('critical')).toBeInTheDocument()
|
|
})
|
|
})
|