376 lines
23 KiB
TypeScript
376 lines
23 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { AssessorApiClient } from '../../src/api/assessor-api-client';
|
|
|
|
const timestamp = '2026-09-06T10:00:00.000Z';
|
|
|
|
describe('AssessorApiClient', () => {
|
|
it('uses the versioned same-origin API and adds the mutation header only to writes', async () => {
|
|
const fetcher = vi.fn<typeof fetch>()
|
|
.mockResolvedValueOnce(new Response(JSON.stringify({
|
|
status: 'ok', application: 'ASSESSOR', apiVersion: 1, repositoryRevision: 0, persistence: 'memory'
|
|
}), { status: 200, headers: { 'Content-Type': 'application/json' } }))
|
|
.mockResolvedValueOnce(new Response(JSON.stringify({ project: { id: 'p-1' }, repositoryRevision: 1 }), {
|
|
status: 200, headers: { 'Content-Type': 'application/json' }
|
|
}));
|
|
const client = new AssessorApiClient({ fetcher });
|
|
|
|
await client.health();
|
|
await client.saveProject({
|
|
id: 'p-1', name: 'Projekt', createdAt: '2026-07-17T08:00:00.000Z',
|
|
updatedAt: '2026-07-17T08:00:00.000Z', attributes: {}
|
|
});
|
|
|
|
expect(fetcher.mock.calls[0][0]).toBe('/api/v1/health');
|
|
expect(new Headers(fetcher.mock.calls[0][1]?.headers).has('X-Assessor-Request')).toBe(false);
|
|
expect(fetcher.mock.calls[1][0]).toBe('/api/v1/projects');
|
|
expect(new Headers(fetcher.mock.calls[1][1]?.headers).get('X-Assessor-Request')).toBe('1');
|
|
});
|
|
|
|
it('loads, updates and deletes tenant data through project endpoints', async () => {
|
|
const fetcher = vi.fn<typeof fetch>()
|
|
.mockResolvedValueOnce(new Response(JSON.stringify({ version: 1, project: { id: 'tenant-1' }, assessments: [], scanRuns: [], importFiles: [] }), { status: 200, headers: { 'Content-Type': 'application/json' } }))
|
|
.mockResolvedValueOnce(new Response(JSON.stringify({ deleted: true, repositoryRevision: 2 }), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
|
const client = new AssessorApiClient({ fetcher });
|
|
|
|
await client.exportProject('tenant-1');
|
|
await client.deleteProject('tenant-1');
|
|
|
|
expect(fetcher.mock.calls[0][0]).toBe('/api/v1/projects/tenant-1/export');
|
|
expect(fetcher.mock.calls[1][0]).toBe('/api/v1/projects/tenant-1');
|
|
expect(fetcher.mock.calls[1][1]?.method).toBe('DELETE');
|
|
expect(JSON.parse(String(fetcher.mock.calls[1][1]?.body))).toEqual({ projectId: 'tenant-1', cascade: true });
|
|
});
|
|
it('loads and saves complete finding reviews through the database API', async () => {
|
|
const review = {
|
|
id: 'review-1', target: { kind: 'occurrence' as const, occurrenceId: 'occurrence/1' }, workflowStatus: 'open' as const,
|
|
workflowLabel: 'Open', priority: 'medium' as const, includeInActionPlan: true, owner: 'Security Team',
|
|
dueAt: '2026-10-15', comment: 'Synthetic note', prerequisites: 'Window', requiredAccess: 'Admin',
|
|
nextStep: 'Remediate', acceptanceEvidence: 'Retest', markers: ['quick-win'],
|
|
createdAt: timestamp, updatedAt: timestamp, revision: 1
|
|
};
|
|
const fetcher = vi.fn<typeof fetch>()
|
|
.mockResolvedValueOnce(new Response(JSON.stringify([review]), { status: 200, headers: { 'Content-Type': 'application/json' } }))
|
|
.mockResolvedValueOnce(new Response(JSON.stringify({ review, repositoryRevision: 4 }), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
|
const client = new AssessorApiClient({ fetcher });
|
|
|
|
await client.listProjectReviews('project/one');
|
|
await client.saveReview(review, 0);
|
|
|
|
expect(fetcher.mock.calls[0][0]).toBe('/api/v1/projects/project%2Fone/reviews');
|
|
expect(fetcher.mock.calls[1][0]).toBe('/api/v1/reviews');
|
|
expect(JSON.parse(String(fetcher.mock.calls[1][1]?.body))).toEqual({ review, expectedRevision: 0 });
|
|
expect(new Headers(fetcher.mock.calls[1][1]?.headers).get('X-Assessor-Request')).toBe('1');
|
|
});
|
|
it('loads the environment snapshot with assessment and repeated scan-run filters', async () => {
|
|
const fetcher = vi.fn<typeof fetch>().mockResolvedValue(new Response(JSON.stringify({
|
|
version: 1, projectId: 'tenant/1', scanRunIds: [], generatedAt: '2026-08-23T00:00:00.000Z',
|
|
sections: [], observations: [], coverage: { observed: 0, explicitlyAbsent: 0, empty: 0, restricted: 0, incomplete: 0 }
|
|
}), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
|
const client = new AssessorApiClient({ fetcher });
|
|
|
|
await client.getEnvironmentSnapshot({ projectId: 'tenant/1', assessmentId: 'assessment/1', scanRunIds: ['run/1', 'run/2'] });
|
|
|
|
expect(fetcher.mock.calls[0][0]).toBe('/api/v1/projects/tenant%2F1/environment-snapshot?assessmentId=assessment%2F1&scanRunIds=run%2F1&scanRunIds=run%2F2');
|
|
expect(new Headers(fetcher.mock.calls[0][1]?.headers).has('X-Assessor-Request')).toBe(false);
|
|
});
|
|
it('keeps LiteLLM behind the same-origin project API', async () => {
|
|
const fetcher = vi.fn<typeof fetch>().mockResolvedValue(new Response(JSON.stringify({
|
|
version: 1, projectId: 'project/one', scope: { assessmentIds: ['assessment/one'], scanRunIds: ['run/one'] },
|
|
generationEnabled: true, state: 'empty', stale: false
|
|
}), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
|
const client = new AssessorApiClient({ fetcher });
|
|
|
|
await client.getAiOverview('project/one', ['assessment/one'], ['run/one']);
|
|
await client.generateAiOverview('project/one', {
|
|
assessmentIds: ['assessment/one'], scanRunIds: ['run/one'], idempotencyKey: 'request-1234'
|
|
});
|
|
|
|
expect(fetcher.mock.calls[0][0]).toBe('/api/v1/projects/project%2Fone/ai-overview?assessmentIds=assessment%2Fone&scanRunIds=run%2Fone');
|
|
expect(fetcher.mock.calls[1][0]).toBe('/api/v1/projects/project%2Fone/ai-overview');
|
|
expect(new Headers(fetcher.mock.calls[1][1]?.headers).get('X-Assessor-Request')).toBe('1');
|
|
});
|
|
it('keeps regulatory retrieval and decisions behind project-bound same-origin APIs', async () => {
|
|
const fetcher = vi.fn<typeof fetch>().mockResolvedValue(new Response(JSON.stringify({
|
|
version: 1, projectId: 'project/one', resultProjectionId: 'scanner-finding:occurrence/one',
|
|
generationEnabled: true, state: 'empty', decisions: []
|
|
}), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
|
const client = new AssessorApiClient({ fetcher });
|
|
|
|
await client.getRegulatoryMappings('project/one', 'scanner-finding:occurrence/one');
|
|
await client.generateRegulatoryMappings('project/one', {
|
|
resultProjectionId: 'scanner-finding:occurrence/one'
|
|
});
|
|
await client.saveRegulatoryMappingDecision('project/one', {
|
|
resultProjectionId: 'scanner-finding:occurrence/one', controlId: '5.1', decision: 'accepted'
|
|
});
|
|
|
|
expect(fetcher.mock.calls.map(call => call[0])).toEqual([
|
|
'/api/v1/projects/project%2Fone/regulatory-mappings/scanner-finding%3Aoccurrence%2Fone',
|
|
'/api/v1/projects/project%2Fone/regulatory-mapping-runs',
|
|
'/api/v1/projects/project%2Fone/regulatory-mapping-decisions'
|
|
]);
|
|
expect(new Headers(fetcher.mock.calls[0][1]?.headers).has('X-Assessor-Request')).toBe(false);
|
|
expect(new Headers(fetcher.mock.calls[1][1]?.headers).get('X-Assessor-Request')).toBe('1');
|
|
expect(new Headers(fetcher.mock.calls[2][1]?.headers).get('X-Assessor-Request')).toBe('1');
|
|
});
|
|
it('uses the complete filtered Result DB scope for regulatory mapping batches', async () => {
|
|
const fetcher = vi.fn<typeof fetch>().mockResolvedValue(new Response(JSON.stringify({
|
|
version: 1, projectId: 'project/one', scope: { assessmentIds: ['assessment/one'], scanRunIds: ['run/one'], filters: { deviation: true } },
|
|
generationEnabled: true, state: 'empty'
|
|
}), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
|
const client = new AssessorApiClient({ fetcher });
|
|
const scope = { assessmentIds: ['assessment/one'], scanRunIds: ['run/one'], filters: { deviation: true, severity: ['high' as const] } };
|
|
|
|
await client.getRegulatoryMappingBatch('project/one', scope);
|
|
await client.startRegulatoryMappingBatch('project/one', { ...scope, idempotencyKey: 'batch-request-1' });
|
|
|
|
expect(fetcher.mock.calls[0][0]).toBe('/api/v1/projects/project%2Fone/regulatory-mapping-batches?deviation=true&severity=high&assessmentIds=assessment%2Fone&scanRunIds=run%2Fone');
|
|
expect(fetcher.mock.calls[1][0]).toBe('/api/v1/projects/project%2Fone/regulatory-mapping-batches');
|
|
expect(new Headers(fetcher.mock.calls[0][1]?.headers).has('X-Assessor-Request')).toBe(false);
|
|
expect(new Headers(fetcher.mock.calls[1][1]?.headers).get('X-Assessor-Request')).toBe('1');
|
|
});
|
|
it('submits GPO backups only through the project-bound mutation endpoint', async () => {
|
|
const fetcher = vi.fn<typeof fetch>().mockResolvedValue(new Response(JSON.stringify({
|
|
version: 1, artifactId: 'artifact-1', jobId: 'job-1', sha256: 'a'.repeat(64), sizeBytes: 3,
|
|
status: 'queued', deduplicated: false, repositoryRevision: 1
|
|
}), { status: 202, headers: { 'Content-Type': 'application/json' } }));
|
|
const client = new AssessorApiClient({ fetcher });
|
|
const bytes = new Uint8Array([1, 2, 3]);
|
|
const file = { name: 'backup.zip', size: bytes.length, arrayBuffer: async () => bytes.buffer } as File;
|
|
|
|
await client.submitGpoBackup('project/one', file);
|
|
|
|
expect(fetcher.mock.calls[0][0]).toBe('/api/v1/projects/project%2Fone/gpo-backups');
|
|
expect(new Headers(fetcher.mock.calls[0][1]?.headers).get('X-Assessor-Request')).toBe('1');
|
|
expect(JSON.parse(String(fetcher.mock.calls[0][1]?.body))).toEqual({
|
|
fileName: 'backup.zip', sizeBytes: 3, bytesBase64: 'AQID'
|
|
});
|
|
});
|
|
it('loads GPO processing status through the read-only project endpoint', async () => {
|
|
const fetcher = vi.fn<typeof fetch>().mockResolvedValue(new Response(JSON.stringify({
|
|
version: 1, projectId: 'project/one', generatedAt: '2026-07-21T10:00:00.000Z', records: []
|
|
}), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
|
const client = new AssessorApiClient({ fetcher });
|
|
|
|
await client.listProjectGpoBackupStatus('project/one');
|
|
|
|
expect(fetcher.mock.calls[0][0]).toBe('/api/v1/projects/project%2Fone/gpo-backups/status');
|
|
expect(new Headers(fetcher.mock.calls[0][1]?.headers).has('X-Assessor-Request')).toBe(false);
|
|
});
|
|
it('reads policy analyses and requests deterministic GPO and baseline reprocessing', async () => {
|
|
const fetcher = vi.fn<typeof fetch>()
|
|
.mockResolvedValue(new Response(JSON.stringify({ version: 1, records: [] }), {
|
|
status: 200, headers: { 'Content-Type': 'application/json' }
|
|
}));
|
|
const client = new AssessorApiClient({ fetcher });
|
|
|
|
await client.listProjectGpoPolicyAnalyses('project/one');
|
|
await client.reprocessGpoBackup('project/one', 'artifact/one', 'run/one');
|
|
await client.listMicrosoftSecurityBaselineAnalyses('baseline/one');
|
|
await client.reprocessMicrosoftSecurityBaseline('baseline/one');
|
|
await client.deleteMicrosoftSecurityBaseline('baseline/one');
|
|
|
|
expect(fetcher.mock.calls[0][0]).toBe('/api/v1/projects/project%2Fone/policy-analyses');
|
|
expect(fetcher.mock.calls[1][0]).toBe('/api/v1/projects/project%2Fone/gpo-backups/artifact%2Fone/reprocess');
|
|
expect(JSON.parse(String(fetcher.mock.calls[1][1]?.body))).toEqual({ expectedLatestProcessingRunId: 'run/one' });
|
|
expect(fetcher.mock.calls[2][0]).toBe('/api/v1/reference-data/microsoft-security-baselines/baseline%2Fone/analysis');
|
|
expect(fetcher.mock.calls[3][0]).toBe('/api/v1/reference-data/microsoft-security-baselines/baseline%2Fone/reprocess');
|
|
expect(fetcher.mock.calls[4][0]).toBe('/api/v1/reference-data/microsoft-security-baselines/baseline%2Fone');
|
|
expect(fetcher.mock.calls[4][1]?.method).toBe('DELETE');
|
|
expect(JSON.parse(String(fetcher.mock.calls[4][1]?.body))).toEqual({ confirmDelete: true });
|
|
});
|
|
it('creates and reads environment profiles, snapshot aggregations, evaluations, and policy findings through project-bound endpoints', async () => {
|
|
const fetcher = vi.fn<typeof fetch>().mockResolvedValue(new Response(JSON.stringify({ version: 1, records: [] }), {
|
|
status: 200, headers: { 'Content-Type': 'application/json' }
|
|
}));
|
|
{
|
|
const fetcher = vi.fn<typeof fetch>().mockResolvedValue(new Response(JSON.stringify({
|
|
version: 1,
|
|
catalog: {
|
|
version: 1,
|
|
id: 'policy-evaluation-rule-catalog-1.0.0',
|
|
catalog: { id: 'policy-evaluation-rule-catalog', version: '1.0.0' },
|
|
status: 'released',
|
|
rules: []
|
|
}
|
|
}), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
|
const client = new AssessorApiClient({ fetcher });
|
|
await client.getPolicyEvaluationRuleCatalog();
|
|
expect(fetcher.mock.calls[0][0]).toBe('/api/v1/reference-data/policy-evaluation-rule-catalog');
|
|
expect(new Headers(fetcher.mock.calls[0][1]?.headers).has('X-Assessor-Request')).toBe(false);
|
|
}
|
|
|
|
const client = new AssessorApiClient({ fetcher });
|
|
const profileRequest = {
|
|
name: 'Windows Server 2022',
|
|
contexts: [{
|
|
id: 'windows-server-2022', kind: 'operating-system' as const,
|
|
productFamily: 'Windows Server', productVersion: '2022', role: 'Member Server', scope: 'computer' as const
|
|
}]
|
|
};
|
|
const aggregationRequest = { environmentProfileId: 'profile/one', sourceAnalysisRunIds: ['analysis/one'] };
|
|
|
|
const evaluationRequest = { aggregationRunId: 'aggregation/one', baselineAnalysisIds: ['baseline-analysis/one'] };
|
|
await client.listEnvironmentProfiles('project/one');
|
|
await client.createEnvironmentProfile('project/one', profileRequest);
|
|
await client.listPolicySnapshotAggregations('project/one');
|
|
await client.aggregatePolicySnapshot('project/one', aggregationRequest);
|
|
await client.listPolicyEvaluations('project/one');
|
|
await client.evaluatePolicy('project/one', evaluationRequest);
|
|
await client.listPolicyFindingRuns('project/one');
|
|
await client.createPolicyFindingRun('project/one', { evaluationRunId: 'evaluation/one' });
|
|
|
|
expect(fetcher.mock.calls[0][0]).toBe('/api/v1/projects/project%2Fone/environment-profiles');
|
|
expect(fetcher.mock.calls[1][0]).toBe('/api/v1/projects/project%2Fone/environment-profiles');
|
|
expect(JSON.parse(String(fetcher.mock.calls[1][1]?.body))).toEqual(profileRequest);
|
|
expect(fetcher.mock.calls[2][0]).toBe('/api/v1/projects/project%2Fone/policy-snapshot-aggregations');
|
|
expect(fetcher.mock.calls[3][0]).toBe('/api/v1/projects/project%2Fone/policy-snapshot-aggregations');
|
|
expect(JSON.parse(String(fetcher.mock.calls[3][1]?.body))).toEqual(aggregationRequest);
|
|
expect(new Headers(fetcher.mock.calls[0][1]?.headers).has('X-Assessor-Request')).toBe(false);
|
|
expect(new Headers(fetcher.mock.calls[1][1]?.headers).get('X-Assessor-Request')).toBe('1');
|
|
expect(fetcher.mock.calls[4][0]).toBe('/api/v1/projects/project%2Fone/policy-evaluations');
|
|
expect(fetcher.mock.calls[5][0]).toBe('/api/v1/projects/project%2Fone/policy-evaluations');
|
|
expect(JSON.parse(String(fetcher.mock.calls[5][1]?.body))).toEqual(evaluationRequest);
|
|
expect(fetcher.mock.calls[6][0]).toBe('/api/v1/projects/project%2Fone/policy-finding-runs');
|
|
expect(fetcher.mock.calls[7][0]).toBe('/api/v1/projects/project%2Fone/policy-finding-runs');
|
|
expect(JSON.parse(String(fetcher.mock.calls[7][1]?.body))).toEqual({ evaluationRunId: 'evaluation/one' });
|
|
expect(new Headers(fetcher.mock.calls[3][1]?.headers).get('X-Assessor-Request')).toBe('1');
|
|
expect(new Headers(fetcher.mock.calls[5][1]?.headers).get('X-Assessor-Request')).toBe('1');
|
|
expect(new Headers(fetcher.mock.calls[7][1]?.headers).get('X-Assessor-Request')).toBe('1');
|
|
});
|
|
|
|
it('retries failed imports and deletes GPO imports through exact project-bound mutations', async () => {
|
|
const fetcher = vi.fn<typeof fetch>()
|
|
.mockResolvedValueOnce(new Response(JSON.stringify({ version: 1, artifactId: 'artifact/one', jobId: 'job-1', status: 'queued', attempts: 0 }), { status: 200, headers: { 'Content-Type': 'application/json' } }))
|
|
.mockResolvedValueOnce(new Response(JSON.stringify({ version: 1, artifactId: 'artifact/one', deleted: true, storageRemoval: 'complete' }), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
|
const client = new AssessorApiClient({ fetcher });
|
|
|
|
await client.retryGpoBackup('project/one', 'artifact/one');
|
|
await client.deleteGpoBackup('project/one', 'artifact/one');
|
|
|
|
expect(fetcher.mock.calls[0][0]).toBe('/api/v1/projects/project%2Fone/gpo-backups/artifact%2Fone/retry');
|
|
expect(fetcher.mock.calls[0][1]?.method).toBe('POST');
|
|
expect(fetcher.mock.calls[1][0]).toBe('/api/v1/projects/project%2Fone/gpo-backups/artifact%2Fone');
|
|
expect(fetcher.mock.calls[1][1]?.method).toBe('DELETE');
|
|
expect(JSON.parse(String(fetcher.mock.calls[1][1]?.body))).toEqual({ artifactId: 'artifact/one', confirmDelete: true });
|
|
});
|
|
it('turns structured server errors into typed client errors', async () => {
|
|
const fetcher = vi.fn<typeof fetch>().mockResolvedValue(new Response(JSON.stringify({
|
|
error: { code: 'repository_conflict', message: 'Konflikt', requestId: 'request-7' }
|
|
}), { status: 409, headers: { 'Content-Type': 'application/json' } }));
|
|
const client = new AssessorApiClient({ fetcher });
|
|
|
|
await expect(client.listProjects()).rejects.toMatchObject({
|
|
status: 409, code: 'repository_conflict', requestId: 'request-7', message: 'Konflikt'
|
|
});
|
|
});
|
|
it('previews and completes import batches through the versioned workflow endpoints', async () => {
|
|
const fetcher = vi.fn<typeof fetch>()
|
|
.mockResolvedValueOnce(new Response(JSON.stringify({ version: 1, batchId: 'batch-1', createdAt: 'now', entries: [] }), {
|
|
status: 200, headers: { 'Content-Type': 'application/json' }
|
|
}))
|
|
.mockResolvedValueOnce(new Response(JSON.stringify({ batchId: 'batch-1', fingerprints: [], repositoryRevision: 1 }), {
|
|
status: 200, headers: { 'Content-Type': 'application/json' }
|
|
}));
|
|
const client = new AssessorApiClient({ fetcher });
|
|
const bytes = new TextEncoder().encode('Name\nFinding');
|
|
const file = {
|
|
name: 'findings.csv', size: bytes.length, lastModified: 0, type: 'text/csv',
|
|
arrayBuffer: async () => bytes.buffer
|
|
} as File;
|
|
|
|
await client.previewImportBatch([file], 'auto');
|
|
await client.completeImportBatch('batch-1', [{ entryId: 'batch-1:0', sha256: 'a'.repeat(64), fileName: file.name, sizeBytes: file.size, lastModified: 0 }]);
|
|
|
|
expect(fetcher.mock.calls[0][0]).toBe('/api/v1/import-batches/preview');
|
|
expect(JSON.parse(String(fetcher.mock.calls[0][1]?.body))).toMatchObject({ files: [{ fileName: 'findings.csv', bytesBase64: expect.any(String) }] });
|
|
expect(fetcher.mock.calls[1][0]).toBe('/api/v1/import-batches/complete');
|
|
expect(new Headers(fetcher.mock.calls[1][1]?.headers).get('X-Assessor-Request')).toBe('1');
|
|
});
|
|
it('queries the unified result read API with repeated facet values and encoded detail IDs', async () => {
|
|
const fetcher = vi.fn<typeof fetch>()
|
|
.mockResolvedValue(new Response(JSON.stringify({ total: 0, items: [], facets: [] }), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
}));
|
|
const client = new AssessorApiClient({ fetcher });
|
|
|
|
await client.queryUnifiedResults({
|
|
projectId: 'project/one',
|
|
scanRunIds: ['run/one', 'run/two'],
|
|
severity: ['critical', 'high'],
|
|
sourceKind: ['environment-observation'],
|
|
environmentProperty: ['domain.functional_level'],
|
|
environmentState: ['observed']
|
|
});
|
|
await client.queryUnifiedResultFacets({ sourceTool: ['pingcastle'] });
|
|
await client.listResultCategories({ categoryNamespace: ['pingcastle'] });
|
|
await client.getUnifiedResultById('scanner-finding:occurrence/one');
|
|
|
|
expect(fetcher.mock.calls[0][0]).toBe(
|
|
'/api/v1/results?projectId=project%2Fone&scanRunIds=run%2Fone&scanRunIds=run%2Ftwo&severity=critical&severity=high&sourceKind=environment-observation&environmentProperty=domain.functional_level&environmentState=observed'
|
|
);
|
|
expect(fetcher.mock.calls[1][0]).toBe('/api/v1/results/facets?sourceTool=pingcastle');
|
|
expect(fetcher.mock.calls[2][0]).toBe('/api/v1/results/categories?categoryNamespace=pingcastle');
|
|
expect(fetcher.mock.calls[3][0]).toBe('/api/v1/results/scanner-finding%3Aoccurrence%2Fone');
|
|
fetcher.mock.calls.forEach(call => {
|
|
expect(new Headers(call[1]?.headers).has('X-Assessor-Request')).toBe(false);
|
|
});
|
|
});
|
|
it('deletes an exact scan run only through its project-bound endpoint', async () => {
|
|
const fetcher = vi.fn<typeof fetch>().mockResolvedValue(new Response(JSON.stringify({
|
|
deleted: true, repositoryRevision: 3
|
|
}), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
|
const client = new AssessorApiClient({ fetcher });
|
|
|
|
await client.deleteScanRun('project/one', 'run/one');
|
|
|
|
expect(fetcher.mock.calls[0][0]).toBe('/api/v1/projects/project%2Fone/scan-runs/run%2Fone');
|
|
expect(fetcher.mock.calls[0][1]?.method).toBe('DELETE');
|
|
expect(JSON.parse(String(fetcher.mock.calls[0][1]?.body))).toEqual({
|
|
projectId: 'project/one', scanRunId: 'run/one', cascade: true
|
|
});
|
|
});
|
|
it('binds a source recovery upload to the original project, timeslot and artifact', async () => {
|
|
const fetcher = vi.fn<typeof fetch>().mockResolvedValue(new Response('{}', { status: 202 }));
|
|
const client = new AssessorApiClient({ fetcher });
|
|
const bytes = new Uint8Array([1, 2, 3]);
|
|
const file = { name: 'renamed.zip', size: bytes.length, arrayBuffer: async () => bytes.buffer } as File;
|
|
await client.submitGpoBackup('project/one', file, 'slot/one', 'source/one');
|
|
expect(fetcher.mock.calls[0][0]).toBe('/api/v1/projects/project%2Fone/gpo-backups');
|
|
expect(new Headers(fetcher.mock.calls[0][1]?.headers).get('X-Assessor-Request')).toBe('1');
|
|
expect(JSON.parse(String(fetcher.mock.calls[0][1]?.body))).toEqual({
|
|
fileName: 'renamed.zip', sizeBytes: 3, bytesBase64: 'AQID', assessmentId: 'slot/one', restoreArtifactId: 'source/one'
|
|
});
|
|
});
|
|
it('deletes an exact assessment only through its project-bound endpoint', async () => {
|
|
const fetcher = vi.fn<typeof fetch>().mockResolvedValue(new Response(JSON.stringify({
|
|
deleted: true, repositoryRevision: 4
|
|
}), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
|
const client = new AssessorApiClient({ fetcher });
|
|
|
|
await client.deleteAssessment('project/one', 'assessment/one');
|
|
|
|
expect(fetcher.mock.calls[0][0]).toBe('/api/v1/projects/project%2Fone/assessments/assessment%2Fone');
|
|
expect(fetcher.mock.calls[0][1]?.method).toBe('DELETE');
|
|
expect(JSON.parse(String(fetcher.mock.calls[0][1]?.body))).toEqual({
|
|
projectId: 'project/one', assessmentId: 'assessment/one', cascade: true
|
|
});
|
|
});
|
|
it('reads and refreshes shared CVE enrichment through same-origin endpoints', async () => {
|
|
const fetcher = vi.fn<typeof fetch>().mockResolvedValue(new Response(JSON.stringify({
|
|
version: 1, record: { cveId: 'CVE-2012-3037' }
|
|
}), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
|
const client = new AssessorApiClient({ fetcher });
|
|
|
|
await client.getCveEnrichment('CVE-2012-3037');
|
|
await client.enrichCve('CVE-2012-3037');
|
|
|
|
expect(fetcher.mock.calls[0][0]).toBe('/api/v1/cves/CVE-2012-3037');
|
|
expect(new Headers(fetcher.mock.calls[0][1]?.headers).has('X-Assessor-Request')).toBe(false);
|
|
expect(fetcher.mock.calls[1][0]).toBe('/api/v1/cves/CVE-2012-3037/enrich');
|
|
expect(fetcher.mock.calls[1][1]?.method).toBe('POST');
|
|
expect(new Headers(fetcher.mock.calls[1][1]?.headers).get('X-Assessor-Request')).toBe('1');
|
|
});
|
|
});
|