fase(5): findings module complete
Some checks failed
ABE Exploratory Testing / explore (push) Has been cancelled
Some checks failed
ABE Exploratory Testing / explore (push) Has been cancelled
This commit is contained in:
233
tests/modules/findings.test.ts
Normal file
233
tests/modules/findings.test.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import { Finding } from '../../src/modules/findings/domain/entities/Finding';
|
||||
import { Severity } from '../../src/modules/findings/domain/value-objects/Severity';
|
||||
import { FindingType } from '../../src/modules/findings/domain/value-objects/FindingType';
|
||||
import { FindingStatus } from '../../src/modules/findings/domain/value-objects/FindingStatus';
|
||||
import { Evidence } from '../../src/modules/findings/domain/value-objects/Evidence';
|
||||
import { CreateFindingCommand } from '../../src/modules/findings/application/commands/CreateFindingCommand';
|
||||
import { ListFindingsQuery } from '../../src/modules/findings/application/queries/ListFindingsQuery';
|
||||
import { IFindingRepository, FindingFilters } from '../../src/modules/findings/domain/ports/IFindingRepository';
|
||||
import { EventBus } from '../../src/shared/application/EventBus';
|
||||
import { DomainEvent } from '../../src/shared/domain/DomainEvent';
|
||||
import { EventHandler } from '../../src/shared/application/EventHandler';
|
||||
import { IAnomaly } from '../../src/core/interfaces';
|
||||
|
||||
// ─── Mock Repository ──────────────────────────────────────────────────────────
|
||||
|
||||
class InMemoryFindingRepository implements IFindingRepository {
|
||||
private store = new Map<string, Finding>();
|
||||
|
||||
async save(finding: Finding): Promise<void> {
|
||||
this.store.set(finding.id.toString(), finding);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Finding | undefined> {
|
||||
return this.store.get(id);
|
||||
}
|
||||
|
||||
async findAll(filters?: FindingFilters): Promise<Finding[]> {
|
||||
let findings = Array.from(this.store.values());
|
||||
if (filters?.severity) findings = findings.filter(f => f.severity.value === filters.severity);
|
||||
if (filters?.status) findings = findings.filter(f => f.status.value === filters.status);
|
||||
if (filters?.sessionId) findings = findings.filter(f => f.sessionId === filters.sessionId);
|
||||
return findings;
|
||||
}
|
||||
|
||||
async update(finding: Finding): Promise<void> {
|
||||
this.store.set(finding.id.toString(), finding);
|
||||
}
|
||||
|
||||
async count(filters?: FindingFilters): Promise<number> {
|
||||
return (await this.findAll(filters)).length;
|
||||
}
|
||||
|
||||
async countBySeverity(): Promise<Record<string, number>> {
|
||||
const result: Record<string, number> = {};
|
||||
for (const f of this.store.values()) {
|
||||
result[f.severity.value] = (result[f.severity.value] ?? 0) + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class MockEventBus implements EventBus {
|
||||
published: DomainEvent[] = [];
|
||||
async publish(event: DomainEvent): Promise<void> { this.published.push(event); }
|
||||
subscribe(_name: string, _handler: EventHandler): void {}
|
||||
}
|
||||
|
||||
// ─── Test Helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
function makeAnomaly(overrides: Partial<IAnomaly> = {}): IAnomaly {
|
||||
return {
|
||||
id: 'anom-1',
|
||||
type: 'http_error',
|
||||
severity: 'high',
|
||||
observationId: 'obs-1',
|
||||
actionTrace: [],
|
||||
description: 'HTTP 500 error',
|
||||
evidence: { rawErrors: ['Error'] },
|
||||
timestamp: Date.now(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Finding aggregate', () => {
|
||||
it('creates with open status and emits FindingCreated event', () => {
|
||||
const finding = Finding.create({
|
||||
sessionId: 'sess-1',
|
||||
severity: Severity.high(),
|
||||
type: FindingType.fromString('http_error'),
|
||||
description: 'Test finding',
|
||||
evidence: Evidence.empty(),
|
||||
actionTrace: [],
|
||||
});
|
||||
|
||||
expect(finding.status.value).toBe('open');
|
||||
expect(finding.domainEvents).toHaveLength(1);
|
||||
expect(finding.domainEvents[0].eventName).toBe('finding.created');
|
||||
});
|
||||
|
||||
it('resolves and emits FindingResolved event', () => {
|
||||
const finding = Finding.create({
|
||||
sessionId: 'sess-1',
|
||||
severity: Severity.low(),
|
||||
type: FindingType.fromString('console_error'),
|
||||
description: 'Console error',
|
||||
evidence: Evidence.empty(),
|
||||
actionTrace: [],
|
||||
});
|
||||
finding.clearEvents();
|
||||
|
||||
finding.resolve();
|
||||
|
||||
expect(finding.status.value).toBe('resolved');
|
||||
expect(finding.resolvedAt).toBeDefined();
|
||||
expect(finding.domainEvents[0].eventName).toBe('finding.resolved');
|
||||
});
|
||||
|
||||
it('transitions status: open → investigating → closed', () => {
|
||||
const finding = Finding.create({
|
||||
sessionId: 'sess-1',
|
||||
severity: Severity.medium(),
|
||||
type: FindingType.fromString('js_exception'),
|
||||
description: 'JS error',
|
||||
evidence: Evidence.empty(),
|
||||
actionTrace: [],
|
||||
});
|
||||
|
||||
finding.investigate();
|
||||
expect(finding.status.value).toBe('investigating');
|
||||
|
||||
finding.close();
|
||||
expect(finding.status.value).toBe('closed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Severity value object', () => {
|
||||
it('creates from valid string', () => {
|
||||
expect(Severity.fromString('critical').value).toBe('critical');
|
||||
});
|
||||
|
||||
it('throws on invalid severity', () => {
|
||||
expect(() => Severity.fromString('extreme')).toThrow();
|
||||
});
|
||||
|
||||
it('equals by value', () => {
|
||||
expect(Severity.high().equals(Severity.high())).toBe(true);
|
||||
expect(Severity.high().equals(Severity.low())).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FindingStatus value object', () => {
|
||||
it('creates all valid statuses', () => {
|
||||
expect(FindingStatus.open().value).toBe('open');
|
||||
expect(FindingStatus.investigating().value).toBe('investigating');
|
||||
expect(FindingStatus.resolved().value).toBe('resolved');
|
||||
expect(FindingStatus.closed().value).toBe('closed');
|
||||
});
|
||||
|
||||
it('throws on invalid status', () => {
|
||||
expect(() => FindingStatus.fromString('unknown')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CreateFindingCommand', () => {
|
||||
let repo: InMemoryFindingRepository;
|
||||
let bus: MockEventBus;
|
||||
let cmd: CreateFindingCommand;
|
||||
|
||||
beforeEach(() => {
|
||||
repo = new InMemoryFindingRepository();
|
||||
bus = new MockEventBus();
|
||||
cmd = new CreateFindingCommand(repo, bus);
|
||||
});
|
||||
|
||||
it('creates a finding from an anomaly', async () => {
|
||||
const anomaly = makeAnomaly();
|
||||
const result = await cmd.execute({ anomaly, sessionId: 'sess-1' });
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
const finding = await repo.findById(result.value.findingId);
|
||||
expect(finding).toBeDefined();
|
||||
expect(finding?.severity.value).toBe('high');
|
||||
expect(finding?.type.value).toBe('http_error');
|
||||
expect(finding?.sessionId).toBe('sess-1');
|
||||
}
|
||||
});
|
||||
|
||||
it('publishes FindingCreated event', async () => {
|
||||
await cmd.execute({ anomaly: makeAnomaly(), sessionId: 'sess-1' });
|
||||
expect(bus.published.some(e => e.eventName === 'finding.created')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns Err on invalid severity', async () => {
|
||||
const anomaly = makeAnomaly({ severity: 'extreme' as IAnomaly['severity'] });
|
||||
const result = await cmd.execute({ anomaly, sessionId: 'sess-1' });
|
||||
expect(result.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ListFindingsQuery', () => {
|
||||
let repo: InMemoryFindingRepository;
|
||||
let query: ListFindingsQuery;
|
||||
|
||||
beforeEach(async () => {
|
||||
repo = new InMemoryFindingRepository();
|
||||
const bus = new MockEventBus();
|
||||
const cmd = new CreateFindingCommand(repo, bus);
|
||||
|
||||
await cmd.execute({ anomaly: makeAnomaly({ id: 'a1', severity: 'high', type: 'http_error' }), sessionId: 'sess-1' });
|
||||
await cmd.execute({ anomaly: makeAnomaly({ id: 'a2', severity: 'low', type: 'console_error' }), sessionId: 'sess-1' });
|
||||
await cmd.execute({ anomaly: makeAnomaly({ id: 'a3', severity: 'critical', type: 'xss_reflection' }), sessionId: 'sess-2' });
|
||||
|
||||
query = new ListFindingsQuery(repo);
|
||||
});
|
||||
|
||||
it('returns all findings without filters', async () => {
|
||||
const result = await query.execute({});
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.value.total).toBe(3);
|
||||
}
|
||||
});
|
||||
|
||||
it('filters by severity', async () => {
|
||||
const result = await query.execute({ severity: 'high' });
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.value.findings.every(f => f.severity.value === 'high')).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('filters by sessionId', async () => {
|
||||
const result = await query.execute({ sessionId: 'sess-2' });
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.value.total).toBe(1);
|
||||
expect(result.value.findings[0].sessionId).toBe('sess-2');
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user