Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website, Klausur-Service, School-Service, Voice-Service, Geo-Service, BreakPilot Drive, Agent-Core Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
325 lines
8.6 KiB
TypeScript
325 lines
8.6 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { exportToPDF, exportToZIP, downloadExport } from '../export'
|
|
import type { SDKState } from '../types'
|
|
|
|
// Mock jsPDF as a class
|
|
vi.mock('jspdf', () => {
|
|
return {
|
|
default: class MockJsPDF {
|
|
internal = {
|
|
pageSize: { getWidth: () => 210, getHeight: () => 297 },
|
|
}
|
|
setFillColor = vi.fn().mockReturnThis()
|
|
setDrawColor = vi.fn().mockReturnThis()
|
|
setTextColor = vi.fn().mockReturnThis()
|
|
setFontSize = vi.fn().mockReturnThis()
|
|
setFont = vi.fn().mockReturnThis()
|
|
setLineWidth = vi.fn().mockReturnThis()
|
|
text = vi.fn().mockReturnThis()
|
|
line = vi.fn().mockReturnThis()
|
|
rect = vi.fn().mockReturnThis()
|
|
roundedRect = vi.fn().mockReturnThis()
|
|
circle = vi.fn().mockReturnThis()
|
|
addPage = vi.fn().mockReturnThis()
|
|
setPage = vi.fn().mockReturnThis()
|
|
getNumberOfPages = vi.fn(() => 5)
|
|
splitTextToSize = vi.fn((text: string) => [text])
|
|
output = vi.fn(() => new Blob(['mock-pdf'], { type: 'application/pdf' }))
|
|
},
|
|
}
|
|
})
|
|
|
|
// Mock JSZip as a class
|
|
vi.mock('jszip', () => {
|
|
return {
|
|
default: class MockJSZip {
|
|
private mockFolder = {
|
|
file: vi.fn().mockReturnThis(),
|
|
folder: vi.fn(() => this.mockFolder),
|
|
}
|
|
folder = vi.fn(() => this.mockFolder)
|
|
generateAsync = vi.fn(() => Promise.resolve(new Blob(['mock-zip'], { type: 'application/zip' })))
|
|
},
|
|
}
|
|
})
|
|
|
|
const createMockState = (overrides: Partial<SDKState> = {}): SDKState => ({
|
|
version: '1.0.0',
|
|
lastModified: new Date('2024-01-15'),
|
|
tenantId: 'test-tenant',
|
|
userId: 'test-user',
|
|
subscription: 'PROFESSIONAL',
|
|
currentPhase: 1,
|
|
currentStep: 'use-case-workshop',
|
|
completedSteps: ['use-case-workshop', 'screening'],
|
|
checkpoints: {
|
|
'CP-UC': {
|
|
checkpointId: 'CP-UC',
|
|
passed: true,
|
|
validatedAt: new Date(),
|
|
validatedBy: 'SYSTEM',
|
|
errors: [],
|
|
warnings: [],
|
|
},
|
|
},
|
|
useCases: [
|
|
{
|
|
id: 'uc-1',
|
|
name: 'Test Use Case',
|
|
description: 'A test use case for testing',
|
|
category: 'Marketing',
|
|
stepsCompleted: 3,
|
|
steps: [
|
|
{ id: 's1', name: 'Step 1', completed: true, data: {} },
|
|
{ id: 's2', name: 'Step 2', completed: true, data: {} },
|
|
{ id: 's3', name: 'Step 3', completed: true, data: {} },
|
|
],
|
|
assessmentResult: null,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
],
|
|
activeUseCase: 'uc-1',
|
|
screening: null,
|
|
modules: [],
|
|
requirements: [],
|
|
controls: [
|
|
{
|
|
id: 'ctrl-1',
|
|
name: 'Test Control',
|
|
description: 'A test control',
|
|
type: 'TECHNICAL',
|
|
category: 'Access Control',
|
|
implementationStatus: 'IMPLEMENTED',
|
|
effectiveness: 'HIGH',
|
|
evidence: [],
|
|
owner: 'Test Owner',
|
|
dueDate: null,
|
|
},
|
|
],
|
|
evidence: [],
|
|
checklist: [],
|
|
risks: [
|
|
{
|
|
id: 'risk-1',
|
|
title: 'Test Risk',
|
|
description: 'A test risk',
|
|
category: 'Security',
|
|
likelihood: 3,
|
|
impact: 4,
|
|
severity: 'HIGH',
|
|
inherentRiskScore: 12,
|
|
residualRiskScore: 6,
|
|
status: 'MITIGATED',
|
|
mitigation: [
|
|
{
|
|
id: 'mit-1',
|
|
description: 'Test mitigation',
|
|
type: 'MITIGATE',
|
|
status: 'COMPLETED',
|
|
effectiveness: 80,
|
|
controlId: 'ctrl-1',
|
|
},
|
|
],
|
|
owner: 'Risk Owner',
|
|
relatedControls: ['ctrl-1'],
|
|
relatedRequirements: [],
|
|
},
|
|
],
|
|
aiActClassification: null,
|
|
obligations: [],
|
|
dsfa: null,
|
|
toms: [],
|
|
retentionPolicies: [],
|
|
vvt: [],
|
|
documents: [],
|
|
cookieBanner: null,
|
|
consents: [],
|
|
dsrConfig: null,
|
|
escalationWorkflows: [],
|
|
sbom: null,
|
|
securityIssues: [],
|
|
securityBacklog: [],
|
|
commandBarHistory: [],
|
|
recentSearches: [],
|
|
preferences: {
|
|
language: 'de',
|
|
theme: 'light',
|
|
compactMode: false,
|
|
showHints: true,
|
|
autoSave: true,
|
|
autoValidate: true,
|
|
allowParallelWork: true,
|
|
},
|
|
...overrides,
|
|
})
|
|
|
|
describe('exportToPDF', () => {
|
|
it('should return a Blob', async () => {
|
|
const state = createMockState()
|
|
const result = await exportToPDF(state)
|
|
|
|
expect(result).toBeInstanceOf(Blob)
|
|
})
|
|
|
|
it('should create a PDF with the correct type', async () => {
|
|
const state = createMockState()
|
|
const result = await exportToPDF(state)
|
|
|
|
expect(result.type).toBe('application/pdf')
|
|
})
|
|
|
|
it('should handle empty state', async () => {
|
|
const emptyState = createMockState({
|
|
useCases: [],
|
|
risks: [],
|
|
controls: [],
|
|
completedSteps: [],
|
|
})
|
|
|
|
const result = await exportToPDF(emptyState)
|
|
expect(result).toBeInstanceOf(Blob)
|
|
})
|
|
|
|
it('should handle state with multiple risks of different severities', async () => {
|
|
const state = createMockState({
|
|
risks: [
|
|
{
|
|
id: 'risk-1',
|
|
title: 'Critical Risk',
|
|
description: 'Critical',
|
|
category: 'Security',
|
|
likelihood: 5,
|
|
impact: 5,
|
|
severity: 'CRITICAL',
|
|
inherentRiskScore: 25,
|
|
residualRiskScore: 15,
|
|
status: 'IDENTIFIED',
|
|
mitigation: [],
|
|
owner: null,
|
|
relatedControls: [],
|
|
relatedRequirements: [],
|
|
},
|
|
{
|
|
id: 'risk-2',
|
|
title: 'Low Risk',
|
|
description: 'Low',
|
|
category: 'Operational',
|
|
likelihood: 1,
|
|
impact: 1,
|
|
severity: 'LOW',
|
|
inherentRiskScore: 1,
|
|
residualRiskScore: 1,
|
|
status: 'ACCEPTED',
|
|
mitigation: [],
|
|
owner: null,
|
|
relatedControls: [],
|
|
relatedRequirements: [],
|
|
},
|
|
],
|
|
})
|
|
|
|
const result = await exportToPDF(state)
|
|
expect(result).toBeInstanceOf(Blob)
|
|
})
|
|
})
|
|
|
|
describe('exportToZIP', () => {
|
|
it('should return a Blob', async () => {
|
|
const state = createMockState()
|
|
const result = await exportToZIP(state)
|
|
|
|
expect(result).toBeInstanceOf(Blob)
|
|
})
|
|
|
|
it('should create a ZIP with the correct type', async () => {
|
|
const state = createMockState()
|
|
const result = await exportToZIP(state)
|
|
|
|
expect(result.type).toBe('application/zip')
|
|
})
|
|
|
|
it('should handle empty state', async () => {
|
|
const emptyState = createMockState({
|
|
useCases: [],
|
|
risks: [],
|
|
controls: [],
|
|
completedSteps: [],
|
|
})
|
|
|
|
const result = await exportToZIP(emptyState)
|
|
expect(result).toBeInstanceOf(Blob)
|
|
})
|
|
|
|
it('should respect includeEvidence option', async () => {
|
|
const state = createMockState()
|
|
const result = await exportToZIP(state, { includeEvidence: false })
|
|
|
|
expect(result).toBeInstanceOf(Blob)
|
|
})
|
|
|
|
it('should respect includeDocuments option', async () => {
|
|
const state = createMockState()
|
|
const result = await exportToZIP(state, { includeDocuments: false })
|
|
|
|
expect(result).toBeInstanceOf(Blob)
|
|
})
|
|
})
|
|
|
|
describe('downloadExport', () => {
|
|
let mockCreateElement: ReturnType<typeof vi.spyOn>
|
|
let mockAppendChild: ReturnType<typeof vi.spyOn>
|
|
let mockRemoveChild: ReturnType<typeof vi.spyOn>
|
|
let mockLink: { href: string; download: string; click: ReturnType<typeof vi.fn> }
|
|
|
|
beforeEach(() => {
|
|
mockLink = {
|
|
href: '',
|
|
download: '',
|
|
click: vi.fn(),
|
|
}
|
|
|
|
mockCreateElement = vi.spyOn(document, 'createElement').mockReturnValue(mockLink as unknown as HTMLElement)
|
|
mockAppendChild = vi.spyOn(document.body, 'appendChild').mockImplementation(() => mockLink as unknown as HTMLElement)
|
|
mockRemoveChild = vi.spyOn(document.body, 'removeChild').mockImplementation(() => mockLink as unknown as HTMLElement)
|
|
})
|
|
|
|
it('should download JSON format', async () => {
|
|
const state = createMockState()
|
|
await downloadExport(state, 'json')
|
|
|
|
expect(mockLink.download).toContain('.json')
|
|
expect(mockLink.click).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should download PDF format', async () => {
|
|
const state = createMockState()
|
|
await downloadExport(state, 'pdf')
|
|
|
|
expect(mockLink.download).toContain('.pdf')
|
|
expect(mockLink.click).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should download ZIP format', async () => {
|
|
const state = createMockState()
|
|
await downloadExport(state, 'zip')
|
|
|
|
expect(mockLink.download).toContain('.zip')
|
|
expect(mockLink.click).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should include date in filename', async () => {
|
|
const state = createMockState()
|
|
await downloadExport(state, 'json')
|
|
|
|
// Check that filename contains a date pattern
|
|
expect(mockLink.download).toMatch(/ai-compliance-sdk-\d{4}-\d{2}-\d{2}\.json/)
|
|
})
|
|
|
|
it('should throw error for unknown format', async () => {
|
|
const state = createMockState()
|
|
|
|
await expect(downloadExport(state, 'unknown' as any)).rejects.toThrow('Unknown export format')
|
|
})
|
|
})
|