Skip to content
This repository was archived by the owner on Nov 14, 2025. It is now read-only.

Commit 5fd738c

Browse files
sapientpantsclaude
andcommitted
feat: implement ComplianceReportRepository and mapper
- Add ComplianceReportMapper to transform API compliance reports to domain aggregates - Create ComplianceReportRepository with full IComplianceReportRepository interface - Support all compliance report types (OWASP_TOP_10, SANS_TOP_25, MISRA_C) - Handle repository ID fetching with fallback mechanism similar to QualityMetricsRepository - Implement fresh data retrieval on every request (no caching per requirements) - Add comprehensive test coverage with 43 tests across mapper and repository - Update infrastructure exports to include new compliance report components - Fix API model structure to match actual DeepSource GraphQL response - Remove unsupported 'key' property from ComplianceCategory domain type 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 1777732 commit 5fd738c

File tree

6 files changed

+1333
-0
lines changed

6 files changed

+1333
-0
lines changed
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
/**
2+
* @fileoverview Tests for ComplianceReportMapper
3+
*/
4+
5+
import { describe, it, expect } from '@jest/globals';
6+
import { ComplianceReportMapper } from '../compliance-report.mapper.js';
7+
import { ComplianceReport as ApiComplianceReport } from '../../../deepsource.js';
8+
import { ReportType } from '../../../types/report-types.js';
9+
import { ComplianceReport } from '../../../domain/aggregates/compliance-report/compliance-report.aggregate.js';
10+
11+
describe('ComplianceReportMapper', () => {
12+
const mockApiReport: ApiComplianceReport = {
13+
key: ReportType.OWASP_TOP_10,
14+
title: 'OWASP Top 10',
15+
currentValue: 85,
16+
status: 'READY',
17+
securityIssueStats: [
18+
{
19+
key: 'A01',
20+
title: 'A01: Broken Access Control',
21+
occurrence: {
22+
critical: 2,
23+
major: 5,
24+
minor: 3,
25+
total: 11,
26+
},
27+
},
28+
{
29+
key: 'A02',
30+
title: 'A02: Cryptographic Failures',
31+
occurrence: {
32+
critical: 0,
33+
major: 1,
34+
minor: 4,
35+
total: 7,
36+
},
37+
},
38+
{
39+
key: 'A03',
40+
title: 'A03: Injection',
41+
occurrence: {
42+
critical: 1,
43+
major: 0,
44+
minor: 0,
45+
total: 1,
46+
},
47+
},
48+
],
49+
trends: [],
50+
};
51+
52+
describe('toDomain', () => {
53+
it('should map API compliance report to domain aggregate', () => {
54+
const projectKey = 'test-project';
55+
const repositoryId = 'repo-123';
56+
57+
const report = ComplianceReportMapper.toDomain(mockApiReport, projectKey, repositoryId);
58+
59+
expect(report).toBeInstanceOf(ComplianceReport);
60+
expect(report.projectKey).toBe(projectKey);
61+
expect(report.repositoryId).toBe(repositoryId);
62+
expect(report.reportType).toBe(ReportType.OWASP_TOP_10);
63+
expect(report.categories).toHaveLength(3);
64+
});
65+
66+
it('should map categories correctly', () => {
67+
const report = ComplianceReportMapper.toDomain(mockApiReport, 'test-project', 'repo-123');
68+
69+
const categories = report.categories;
70+
expect(categories[0].name).toBe('A01: Broken Access Control');
71+
expect(categories[0].severity).toBe('CRITICAL');
72+
expect(categories[0].issueCount.count).toBe(11); // 2+5+3+1
73+
74+
expect(categories[1].name).toBe('A02: Cryptographic Failures');
75+
expect(categories[1].severity).toBe('MAJOR'); // Highest severity present
76+
expect(categories[1].issueCount.count).toBe(7); // 0+1+4+2
77+
78+
expect(categories[2].name).toBe('A03: Injection');
79+
expect(categories[2].severity).toBe('CRITICAL');
80+
expect(categories[2].issueCount.count).toBe(1); // 1+0+0+0
81+
});
82+
83+
it('should handle empty security issue stats', () => {
84+
const emptyReport = {
85+
...mockApiReport,
86+
securityIssueStats: [],
87+
};
88+
89+
const report = ComplianceReportMapper.toDomain(emptyReport, 'test-project', 'repo-123');
90+
91+
expect(report.categories).toHaveLength(0);
92+
});
93+
});
94+
95+
describe('mapStatus', () => {
96+
it('should map API status to domain status', () => {
97+
expect(ComplianceReportMapper.mapStatus('READY')).toBe('READY');
98+
expect(ComplianceReportMapper.mapStatus('COMPLETED')).toBe('READY');
99+
expect(ComplianceReportMapper.mapStatus('GENERATING')).toBe('GENERATING');
100+
expect(ComplianceReportMapper.mapStatus('PENDING')).toBe('GENERATING');
101+
expect(ComplianceReportMapper.mapStatus('ERROR')).toBe('ERROR');
102+
expect(ComplianceReportMapper.mapStatus('FAILED')).toBe('ERROR');
103+
});
104+
105+
it('should default to PENDING for unknown status', () => {
106+
expect(ComplianceReportMapper.mapStatus('UNKNOWN')).toBe('PENDING');
107+
expect(ComplianceReportMapper.mapStatus(undefined)).toBe('PENDING');
108+
});
109+
});
110+
111+
describe('mapHighestSeverity', () => {
112+
it('should return CRITICAL when critical issues present', () => {
113+
const occurrence = { critical: 1, major: 5, minor: 3, total: 9 };
114+
expect(ComplianceReportMapper.mapHighestSeverity(occurrence)).toBe('CRITICAL');
115+
});
116+
117+
it('should return MAJOR when no critical but major issues present', () => {
118+
const occurrence = { critical: 0, major: 2, minor: 5, total: 7 };
119+
expect(ComplianceReportMapper.mapHighestSeverity(occurrence)).toBe('MAJOR');
120+
});
121+
122+
it('should return MINOR when no critical/major but minor issues present', () => {
123+
const occurrence = { critical: 0, major: 0, minor: 3, total: 3 };
124+
expect(ComplianceReportMapper.mapHighestSeverity(occurrence)).toBe('MINOR');
125+
});
126+
127+
it('should return INFO when no specific severity issues present', () => {
128+
const occurrence = { critical: 0, major: 0, minor: 0, total: 2 };
129+
expect(ComplianceReportMapper.mapHighestSeverity(occurrence)).toBe('INFO');
130+
});
131+
132+
it('should return INFO when no issues present', () => {
133+
const occurrence = { critical: 0, major: 0, minor: 0, total: 0 };
134+
expect(ComplianceReportMapper.mapHighestSeverity(occurrence)).toBe('INFO');
135+
});
136+
});
137+
138+
describe('toPersistence', () => {
139+
it('should map domain aggregate to persistence format', () => {
140+
const report = ComplianceReportMapper.toDomain(mockApiReport, 'test-project', 'repo-123');
141+
142+
const persistence = ComplianceReportMapper.toPersistence(report);
143+
144+
expect(persistence.id).toBe(report.id);
145+
expect(persistence.projectKey).toBe('test-project');
146+
expect(persistence.repositoryId).toBe('repo-123');
147+
expect(persistence.reportType).toBe(ReportType.OWASP_TOP_10);
148+
expect(persistence.categories).toEqual(report.categories);
149+
expect(persistence.generatedAt).toBeInstanceOf(Date);
150+
expect(persistence.lastUpdated).toBeInstanceOf(Date);
151+
});
152+
});
153+
154+
describe('toDomainFromList', () => {
155+
it('should map multiple API reports to domain aggregates', () => {
156+
const apiReports = [
157+
mockApiReport,
158+
{
159+
...mockApiReport,
160+
key: ReportType.SANS_TOP_25,
161+
title: 'SANS Top 25',
162+
securityIssueStats: [
163+
{
164+
key: 'CWE-79',
165+
title: 'Cross-site Scripting',
166+
occurrence: {
167+
critical: 1,
168+
major: 2,
169+
minor: 1,
170+
total: 4,
171+
},
172+
},
173+
],
174+
},
175+
];
176+
177+
const reports = ComplianceReportMapper.toDomainFromList(
178+
apiReports,
179+
'test-project',
180+
'repo-123'
181+
);
182+
183+
expect(reports).toHaveLength(2);
184+
expect(reports[0].reportType).toBe(ReportType.OWASP_TOP_10);
185+
expect(reports[1].reportType).toBe(ReportType.SANS_TOP_25);
186+
expect(reports[1].categories).toHaveLength(1);
187+
});
188+
189+
it('should handle empty reports list', () => {
190+
const reports = ComplianceReportMapper.toDomainFromList([], 'test-project', 'repo-123');
191+
expect(reports).toEqual([]);
192+
});
193+
});
194+
195+
describe('createCategoryFromStat', () => {
196+
it('should create a compliance category from security issue stat', () => {
197+
const stat = {
198+
key: 'A01',
199+
title: 'Broken Access Control',
200+
occurrence: {
201+
critical: 2,
202+
major: 3,
203+
minor: 1,
204+
total: 6,
205+
},
206+
};
207+
208+
const category = ComplianceReportMapper.createCategoryFromStat(stat, ReportType.OWASP_TOP_10);
209+
210+
expect(category.name).toBe('Broken Access Control');
211+
expect(category.description).toBe('OWASP_TOP_10 category: A01');
212+
expect(category.issueCount.count).toBe(6); // 2+3+1+0
213+
expect(category.nonCompliant.count).toBe(6);
214+
expect(category.compliant.count).toBe(0);
215+
expect(category.severity).toBe('CRITICAL');
216+
});
217+
});
218+
219+
describe('estimateComplianceScore', () => {
220+
it('should return 100 for empty categories', () => {
221+
const score = ComplianceReportMapper.estimateComplianceScore([]);
222+
expect(score).toBe(100);
223+
});
224+
225+
it('should calculate score based on severity and issue counts', () => {
226+
const categories = [
227+
{
228+
name: 'Test Category 1',
229+
description: 'Test',
230+
compliant: { count: 0 } as any,
231+
nonCompliant: { count: 5 } as any,
232+
issueCount: { count: 5 } as any,
233+
severity: 'CRITICAL' as const,
234+
},
235+
{
236+
name: 'Test Category 2',
237+
description: 'Test',
238+
compliant: { count: 0 } as any,
239+
nonCompliant: { count: 3 } as any,
240+
issueCount: { count: 3 } as any,
241+
severity: 'MAJOR' as const,
242+
},
243+
];
244+
245+
const score = ComplianceReportMapper.estimateComplianceScore(categories);
246+
247+
// Expected: 100 - (1 * 20 + 1 * 10 + 8 * 2) = 100 - 46 = 54
248+
expect(score).toBe(54);
249+
});
250+
251+
it('should not go below 0', () => {
252+
const categories = Array(10).fill({
253+
name: 'Critical Category',
254+
description: 'Test',
255+
compliant: { count: 0 } as any,
256+
nonCompliant: { count: 10 } as any,
257+
issueCount: { count: 10 } as any,
258+
severity: 'CRITICAL' as const,
259+
});
260+
261+
const score = ComplianceReportMapper.estimateComplianceScore(categories);
262+
expect(score).toBe(0);
263+
});
264+
265+
it('should not go above 100', () => {
266+
const categories = [
267+
{
268+
name: 'Minor Category',
269+
description: 'Test',
270+
compliant: { count: 0 } as any,
271+
nonCompliant: { count: 1 } as any,
272+
issueCount: { count: 1 } as any,
273+
severity: 'INFO' as const,
274+
},
275+
];
276+
277+
const score = ComplianceReportMapper.estimateComplianceScore(categories);
278+
expect(score).toBe(98); // 100 - 2 = 98
279+
});
280+
});
281+
});

0 commit comments

Comments
 (0)