Files
Autonomous-Bug-Explorer/tests/modules/findings.test.ts
debian d62bd615bf
Some checks failed
ABE Exploratory Testing / explore (push) Has been cancelled
fase(5): findings module complete
2026-03-05 04:06:45 -05:00

234 lines
8.4 KiB
TypeScript

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');
}
});
});