test(21-01): add failing tests for triage-note formatter
- Cover verdict/confidence rendering, reasons/summary sections - Cover both BlastRadiusResult branches (ok and unavailable) - Cover remediation-state rendering (empty and populated) - Cover URL sanitization and null-verdict graceful handling
This commit is contained in:
parent
226f300513
commit
b4e05eb6ad
1 changed files with 104 additions and 0 deletions
104
lib/services/triage-note-format.test.ts
Normal file
104
lib/services/triage-note-format.test.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { formatTriageNote, type TriageNoteEvidence } from './triage-note-format';
|
||||
import type { BlastRadiusResult } from './mimecast-blast-radius';
|
||||
|
||||
function baseEvidence(overrides: Partial<TriageNoteEvidence> = {}): TriageNoteEvidence {
|
||||
return {
|
||||
campaignId: 'campaign-1',
|
||||
reportCount: 3,
|
||||
companyName: 'Acme Corp',
|
||||
subject: 'Your invoice is ready',
|
||||
verdict: 'THREAT',
|
||||
confidence: 0.85,
|
||||
summary: 'Credential-harvesting link found in message body.',
|
||||
reasons: ['Sender domain not in allowlist', 'URL matches known phishing indicator'],
|
||||
recommendedActions: ['block_sender', 'purge_message'],
|
||||
requiresApproval: true,
|
||||
blastRadius: { status: 'unavailable', reason: 'not_configured' },
|
||||
remediationActions: [],
|
||||
urls: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('formatTriageNote', () => {
|
||||
it('includes the verdict label and confidence value', () => {
|
||||
const output = formatTriageNote(baseEvidence());
|
||||
expect(output).toContain('THREAT');
|
||||
expect(output).toContain('0.85');
|
||||
});
|
||||
|
||||
it('includes a Summary line and a Reasons section listing each reason', () => {
|
||||
const output = formatTriageNote(baseEvidence());
|
||||
expect(output).toContain('Credential-harvesting link found in message body.');
|
||||
expect(output).toContain('Sender domain not in allowlist');
|
||||
expect(output).toContain('URL matches known phishing indicator');
|
||||
});
|
||||
|
||||
it('shows delivered/held/rejected/clicked counts when blast radius is ok', () => {
|
||||
const ok: BlastRadiusResult = {
|
||||
status: 'ok',
|
||||
matched: 5,
|
||||
delivered: 3,
|
||||
held: 1,
|
||||
rejected: 1,
|
||||
clicked: 2,
|
||||
perRecipient: [{ recipient: 'user@acme.example', status: 'delivered' }],
|
||||
source: 'fan-out',
|
||||
};
|
||||
const output = formatTriageNote(baseEvidence({ blastRadius: ok }));
|
||||
expect(output).toContain('3');
|
||||
expect(output).toContain('1');
|
||||
expect(output).toContain('2');
|
||||
});
|
||||
|
||||
it('shows an explicit unavailable reason when blast radius is unavailable', () => {
|
||||
const output = formatTriageNote(
|
||||
baseEvidence({ blastRadius: { status: 'unavailable', reason: 'not_configured' } })
|
||||
);
|
||||
expect(output).toContain('unavailable');
|
||||
expect(output).toContain('not_configured');
|
||||
});
|
||||
|
||||
it('states no action taken when remediationActions is empty', () => {
|
||||
const output = formatTriageNote(baseEvidence({ remediationActions: [] }));
|
||||
expect(output.toLowerCase()).toMatch(/proposed|no action/);
|
||||
});
|
||||
|
||||
it('lists each remediation action with status and approver when present', () => {
|
||||
const output = formatTriageNote(
|
||||
baseEvidence({
|
||||
remediationActions: [
|
||||
{ actionType: 'block_sender', status: 'approved', approvedBy: 'operator@example.com', approvedAt: '2026-07-01T00:00:00Z' },
|
||||
{ actionType: 'purge_message', status: 'completed', approvedBy: 'operator@example.com', approvedAt: '2026-07-01T00:00:00Z' },
|
||||
],
|
||||
})
|
||||
);
|
||||
expect(output).toContain('block_sender');
|
||||
expect(output).toContain('approved');
|
||||
expect(output).toContain('purge_message');
|
||||
expect(output).toContain('completed');
|
||||
expect(output).toContain('operator@example.com');
|
||||
});
|
||||
|
||||
it('renders indicator URLs in sanitized form, dropping query strings', () => {
|
||||
const output = formatTriageNote(baseEvidence({ urls: ['http://evil.example/p?token=leak'] }));
|
||||
expect(output).not.toContain('token=leak');
|
||||
expect(output).toContain('http://evil.example/p');
|
||||
});
|
||||
|
||||
it('never leaks a secret embedded in free-text fields (sanitizeNoteText applied to whole output)', () => {
|
||||
const output = formatTriageNote(
|
||||
baseEvidence({ summary: 'Found link http://evil.example/x?access_token=SECRETVALUE in body' })
|
||||
);
|
||||
expect(output).not.toContain('access_token=SECRETVALUE');
|
||||
});
|
||||
|
||||
it('handles null verdict/confidence gracefully without throwing', () => {
|
||||
expect(() =>
|
||||
formatTriageNote(baseEvidence({ verdict: null, confidence: null }))
|
||||
).not.toThrow();
|
||||
const output = formatTriageNote(baseEvidence({ verdict: null, confidence: null }));
|
||||
expect(output.toLowerCase()).toContain('not yet classified');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue