2026-07-16 08:20:49 -04:00
import { describe , it , expect , vi , beforeEach } from 'vitest' ;
// Mock postgresClient BEFORE importing the module under test — mirrors
// campaign-grouping-service.test.ts's vi.mock() factory-mocking discipline.
const queryMock = vi . fn ( ) ;
vi . mock ( './postgres-client' , ( ) = > ( {
postgresClient : {
query : ( . . . args : unknown [ ] ) = > queryMock ( . . . args ) ,
} ,
} ) ) ;
// Mock getBlastRadius entirely — mirrors mimecast-blast-radius.test.ts's
// sibling-service mocking pattern. No real Mimecast/Postgres calls happen.
const getBlastRadiusMock = vi . fn ( ) ;
vi . mock ( './mimecast-blast-radius' , ( ) = > ( {
getBlastRadius : ( . . . args : unknown [ ] ) = > getBlastRadiusMock ( . . . args ) ,
} ) ) ;
// eslint-disable-next-line import/first -- imported after vi.mock hoisting
2026-07-16 08:14:51 -04:00
import {
KNOWN_SIMULATION_SENDERS ,
domainMatchesAllowlist ,
isKnownSimulationSender ,
effectiveAuthResults ,
hasHardAuthFail ,
computeConfidence ,
mapVerdictToActions ,
computeRequiresApproval ,
2026-07-16 08:20:49 -04:00
classifyCampaign ,
2026-07-16 08:14:51 -04:00
} from './campaign-classifier' ;
2026-07-16 08:20:49 -04:00
// eslint-disable-next-line import/first -- imported after vi.mock hoisting
2026-07-16 08:14:51 -04:00
import {
knowbe4SimMessage ,
bsnSimMessage ,
threatMessage ,
cleanSpamMessage ,
2026-07-16 08:20:49 -04:00
suspiciousUnwantedMessage ,
2026-07-16 08:14:51 -04:00
} from './campaign-classifier.fixtures' ;
2026-07-16 08:20:49 -04:00
import type { NormalizedMessage } from './eml-parser' ;
2026-07-16 08:14:51 -04:00
describe ( 'KNOWN_SIMULATION_SENDERS' , ( ) = > {
it ( 'includes both the KnowBe4 and Breach Secure Now sender domains' , ( ) = > {
const allDomains = KNOWN_SIMULATION_SENDERS . flatMap ( ( entry ) = > entry . domains ) ;
expect ( allDomains ) . toContain ( 'it-support.care' ) ;
expect ( allDomains ) . toContain ( 'breachsecurenow.com' ) ;
} ) ;
} ) ;
describe ( 'domainMatchesAllowlist' , ( ) = > {
it ( 'matches an exact allowlisted domain' , ( ) = > {
expect ( domainMatchesAllowlist ( 'it-support.care' ) ) . toBe ( true ) ;
} ) ;
it ( 'matches a proper subdomain of an allowlisted domain' , ( ) = > {
expect ( domainMatchesAllowlist ( 'sub.it-support.care' ) ) . toBe ( true ) ;
expect ( domainMatchesAllowlist ( 'em8721.breachsecurenow.com' ) ) . toBe ( true ) ;
} ) ;
it ( 'does NOT match a bare substring / suffix-spoofed domain (T-19-01)' , ( ) = > {
expect ( domainMatchesAllowlist ( 'it-support.care.attacker.net' ) ) . toBe ( false ) ;
expect ( domainMatchesAllowlist ( 'evil-it-support.care' ) ) . toBe ( false ) ;
} ) ;
2026-07-17 07:19:18 -04:00
it ( 'matches the 3 KnowBe4 domains confirmed via Seubert ticket 699456 (260717-a19)' , ( ) = > {
expect ( domainMatchesAllowlist ( 'customer-portal.info' ) ) . toBe ( true ) ;
expect ( domainMatchesAllowlist ( 'cloud-service-care.com' ) ) . toBe ( true ) ;
expect ( domainMatchesAllowlist ( 'bankonlinesupport.com' ) ) . toBe ( true ) ;
} ) ;
it ( 'matches a subdomain of the newly-added customer-portal.info' , ( ) = > {
expect ( domainMatchesAllowlist ( 'mail.customer-portal.info' ) ) . toBe ( true ) ;
} ) ;
it ( 'does NOT match a suffix-spoofed variant of the newly-added domain' , ( ) = > {
expect ( domainMatchesAllowlist ( 'customer-portal.info.attacker.net' ) ) . toBe ( false ) ;
} ) ;
2026-07-16 08:14:51 -04:00
} ) ;
describe ( 'isKnownSimulationSender' , ( ) = > {
it ( 'matches on From domain (knowbe4SimMessage)' , ( ) = > {
expect ( isKnownSimulationSender ( knowbe4SimMessage ) ) . toBe ( true ) ;
} ) ;
it ( 'matches on Return-Path domain when From.domain is null (Pitfall 3, bsnSimMessage)' , ( ) = > {
expect ( bsnSimMessage . from . domain ) . toBeNull ( ) ;
expect ( isKnownSimulationSender ( bsnSimMessage ) ) . toBe ( true ) ;
} ) ;
it ( 'does not match a non-allowlisted sender' , ( ) = > {
expect ( isKnownSimulationSender ( threatMessage ) ) . toBe ( false ) ;
expect ( isKnownSimulationSender ( cleanSpamMessage ) ) . toBe ( false ) ;
} ) ;
} ) ;
describe ( 'effectiveAuthResults' , ( ) = > {
it ( 'returns authResultsOriginal when present (Pitfall 1)' , ( ) = > {
expect ( effectiveAuthResults ( knowbe4SimMessage ) ) . toEqual ( knowbe4SimMessage . authResultsOriginal ) ;
} ) ;
it ( 'falls back to authResults when authResultsOriginal is null' , ( ) = > {
expect ( threatMessage . authResultsOriginal ) . toBeNull ( ) ;
expect ( effectiveAuthResults ( threatMessage ) ) . toEqual ( threatMessage . authResults ) ;
} ) ;
} ) ;
describe ( 'hasHardAuthFail' , ( ) = > {
it ( 'is true when spf is fail' , ( ) = > {
expect ( hasHardAuthFail ( { spf : 'fail' } ) ) . toBe ( true ) ;
} ) ;
it ( 'is true when dkim is fail' , ( ) = > {
expect ( hasHardAuthFail ( { dkim : 'fail' } ) ) . toBe ( true ) ;
} ) ;
it ( 'is true when dmarc is fail' , ( ) = > {
expect ( hasHardAuthFail ( { dmarc : 'fail' } ) ) . toBe ( true ) ;
} ) ;
it ( 'is false for none/neutral/undefined verdicts' , ( ) = > {
expect ( hasHardAuthFail ( { spf : 'none' , dkim : 'neutral' } ) ) . toBe ( false ) ;
expect ( hasHardAuthFail ( { } ) ) . toBe ( false ) ;
} ) ;
it ( 'is false when all verdicts pass' , ( ) = > {
expect ( hasHardAuthFail ( { spf : 'pass' , dkim : 'pass' , dmarc : 'pass' } ) ) . toBe ( false ) ;
} ) ;
} ) ;
describe ( 'computeConfidence' , ( ) = > {
it ( 'is 1.0 with no deductions and no reasons when all evidence is present (confidence deduction baseline)' , ( ) = > {
const result = computeConfidence ( {
hasAnyMessage : true ,
blastRadiusStatus : 'ok' ,
hasAttachmentOrUrlIndicators : true ,
} ) ;
expect ( result . confidence ) . toBe ( 1.0 ) ;
expect ( result . reasons ) . toHaveLength ( 0 ) ;
} ) ;
it ( 'deducts 0.4 and names the reason when no message was parsed (confidence deduction)' , ( ) = > {
const result = computeConfidence ( {
hasAnyMessage : false ,
blastRadiusStatus : 'ok' ,
hasAttachmentOrUrlIndicators : true ,
} ) ;
expect ( result . confidence ) . toBe ( 0.6 ) ;
expect ( result . reasons ) . toHaveLength ( 1 ) ;
expect ( result . reasons [ 0 ] ) . toMatch ( /message/i ) ;
} ) ;
it ( 'deducts 0.3 and names the reason when blast-radius is unavailable (confidence deduction)' , ( ) = > {
const result = computeConfidence ( {
hasAnyMessage : true ,
blastRadiusStatus : 'unavailable' ,
hasAttachmentOrUrlIndicators : true ,
} ) ;
expect ( result . confidence ) . toBe ( 0.7 ) ;
expect ( result . reasons [ 0 ] ) . toMatch ( /mimecast|blast/i ) ;
} ) ;
it ( 'deducts 0.2 and names the reason when no attachment/url indicators are found (confidence deduction)' , ( ) = > {
const result = computeConfidence ( {
hasAnyMessage : true ,
blastRadiusStatus : 'ok' ,
hasAttachmentOrUrlIndicators : false ,
} ) ;
expect ( result . confidence ) . toBe ( 0.8 ) ;
expect ( result . reasons [ 0 ] ) . toMatch ( /indicator/i ) ;
} ) ;
it ( 'floors at 0.10 with all three named reasons when every evidence source is missing (confidence deduction)' , ( ) = > {
const result = computeConfidence ( {
hasAnyMessage : false ,
blastRadiusStatus : 'unavailable' ,
hasAttachmentOrUrlIndicators : false ,
} ) ;
expect ( result . confidence ) . toBe ( 0.1 ) ;
expect ( result . reasons ) . toHaveLength ( 3 ) ;
} ) ;
} ) ;
describe ( 'mapVerdictToActions' , ( ) = > {
it ( 'maps SPAM to no_action' , ( ) = > {
expect ( mapVerdictToActions ( 'SPAM' , { clicked : 0 } ) ) . toEqual ( [ 'no_action' ] ) ;
} ) ;
it ( 'maps UNWANTED to warn_user' , ( ) = > {
expect ( mapVerdictToActions ( 'UNWANTED' , { clicked : 0 } ) ) . toEqual ( [ 'warn_user' ] ) ;
} ) ;
it ( 'maps THREAT with no clicks to block_sender + purge_message' , ( ) = > {
expect ( mapVerdictToActions ( 'THREAT' , { clicked : 0 } ) ) . toEqual ( [ 'block_sender' , 'purge_message' ] ) ;
} ) ;
it ( 'maps THREAT with clicks to also include reset_password/isolate_endpoint/disable_forwarding_rule' , ( ) = > {
const actions = mapVerdictToActions ( 'THREAT' , { clicked : 1 } ) ;
expect ( actions ) . toContain ( 'block_sender' ) ;
expect ( actions ) . toContain ( 'purge_message' ) ;
expect ( actions ) . toContain ( 'reset_password' ) ;
expect ( actions ) . toContain ( 'isolate_endpoint' ) ;
expect ( actions ) . toContain ( 'disable_forwarding_rule' ) ;
} ) ;
2026-07-16 19:36:53 -04:00
it ( 'maps USER_AWARENESS to exactly acknowledge_user' , ( ) = > {
expect ( mapVerdictToActions ( 'USER_AWARENESS' , { clicked : 0 } ) ) . toEqual ( [ 'acknowledge_user' ] ) ;
} ) ;
2026-07-16 08:14:51 -04:00
} ) ;
describe ( 'computeRequiresApproval' , ( ) = > {
it ( 'is false for disable_forwarding_rule alone (requires_approval invariant)' , ( ) = > {
expect ( computeRequiresApproval ( [ 'disable_forwarding_rule' ] ) ) . toBe ( false ) ;
} ) ;
it ( 'is true when disable_forwarding_rule is combined with a destructive action (requires_approval invariant)' , ( ) = > {
expect ( computeRequiresApproval ( [ 'disable_forwarding_rule' , 'block_sender' ] ) ) . toBe ( true ) ;
} ) ;
it . each ( [ 'block_sender' , 'purge_message' , 'reset_password' , 'isolate_endpoint' ] ) (
'is true for %s alone (requires_approval invariant)' ,
( action ) = > {
expect ( computeRequiresApproval ( [ action ] ) ) . toBe ( true ) ;
}
) ;
it ( 'is false for no_action and warn_user (requires_approval invariant)' , ( ) = > {
expect ( computeRequiresApproval ( [ 'no_action' ] ) ) . toBe ( false ) ;
expect ( computeRequiresApproval ( [ 'warn_user' ] ) ) . toBe ( false ) ;
} ) ;
2026-07-16 19:36:53 -04:00
it ( 'is false for acknowledge_user (USER_AWARENESS is never destructive)' , ( ) = > {
expect ( computeRequiresApproval ( [ 'acknowledge_user' ] ) ) . toBe ( false ) ;
} ) ;
2026-07-16 08:14:51 -04:00
} ) ;
2026-07-16 08:20:49 -04:00
// =============================================================================
// classifyCampaign — mocked-DB orchestration tests (CLASSIFY-01/02/03/04/06,
// D-02/D-03/D-04/D-06)
//
// `queryMock` routes staged rows based on a distinguishing SQL substring per
// call (`FROM reports`, `FROM messages`, `FROM indicators`,
// `INSERT INTO classifications`) — NOT by call order — mirroring
// campaign-grouping-service.test.ts's makeClient() discipline.
// =============================================================================
interface ReportFixtureRow {
id : string ;
title : string | null ;
created_at : string ;
requester_email : string | null ;
2026-07-21 16:45:44 -04:00
company_id : string | null ;
2026-07-16 08:20:49 -04:00
}
interface StagedRows {
reports? : ReportFixtureRow [ ] ;
messages? : Array < { id : string ; report_id : string ; headers : NormalizedMessage } > ;
indicators? : Array < { id : string ; message_id : string ; indicator_type : string ; value : string } > ;
2026-07-21 16:45:44 -04:00
mimecastTenants? : Array < { client_id : string ; client_secret : string ; base_url : string | null } > ;
2026-07-16 08:20:49 -04:00
}
function stageQueries ( rows : StagedRows ) {
queryMock . mockImplementation ( async ( sql : string ) = > {
if ( sql . includes ( 'INSERT INTO classifications' ) ) {
return { rows : [ { id : 'classification-1' , created_at : '2026-07-16T00:00:00.000Z' } ] , rowCount : 1 } ;
}
2026-07-21 16:45:44 -04:00
if ( sql . includes ( 'FROM mimecast_tenants' ) ) {
return { rows : rows.mimecastTenants ? ? [ ] , rowCount : rows.mimecastTenants?.length ? ? 0 } ;
}
2026-07-16 08:20:49 -04:00
if ( sql . includes ( 'FROM reports' ) ) {
return { rows : rows.reports ? ? [ ] , rowCount : rows.reports?.length ? ? 0 } ;
}
if ( sql . includes ( 'FROM messages' ) ) {
return { rows : rows.messages ? ? [ ] , rowCount : rows.messages?.length ? ? 0 } ;
}
if ( sql . includes ( 'FROM indicators' ) ) {
return { rows : rows.indicators ? ? [ ] , rowCount : rows.indicators?.length ? ? 0 } ;
}
throw new Error ( ` Unstaged query in test mock: ${ sql } ` ) ;
} ) ;
}
function toMessageRow ( id : string , reportId : string , fixture : NormalizedMessage ) {
return { id , report_id : reportId , headers : fixture } ;
}
const REPORTER_EMAIL = 'reporter@wulfconsulting.test' ;
describe ( 'classifyCampaign' , ( ) = > {
beforeEach ( ( ) = > {
queryMock . mockReset ( ) ;
getBlastRadiusMock . mockReset ( ) ;
} ) ;
it ( 'returns exactly one verdict with the full payload shape (returns exactly one verdict)' , async ( ) = > {
stageQueries ( {
reports : [
2026-07-21 16:45:44 -04:00
{ id : 'report-1' , title : cleanSpamMessage.subject , created_at : '2026-07-15T00:00:00.000Z' , requester_email : REPORTER_EMAIL , company_id : null } ,
2026-07-16 08:20:49 -04:00
] ,
messages : [ toMessageRow ( 'message-1' , 'report-1' , cleanSpamMessage ) ] ,
indicators : [ ] ,
} ) ;
getBlastRadiusMock . mockResolvedValue ( {
status : 'ok' ,
matched : 1 ,
delivered : 0 ,
held : 1 ,
rejected : 0 ,
clicked : 0 ,
perRecipient : [ ] ,
source : 'fan-out' ,
} ) ;
const result = await classifyCampaign ( 'campaign-1' ) ;
expect ( [ 'SPAM' , 'UNWANTED' , 'THREAT' ] ) . toContain ( result . verdict ) ;
expect ( typeof result . id ) . toBe ( 'string' ) ;
expect ( result . campaignId ) . toBe ( 'campaign-1' ) ;
expect ( typeof result . confidence ) . toBe ( 'number' ) ;
expect ( typeof result . summary ) . toBe ( 'string' ) ;
expect ( Array . isArray ( result . reasons ) ) . toBe ( true ) ;
expect ( Array . isArray ( result . recommendedActions ) ) . toBe ( true ) ;
expect ( typeof result . requiresApproval ) . toBe ( 'boolean' ) ;
expect ( typeof result . createdAt ) . toBe ( 'string' ) ;
} ) ;
it ( 'inserts exactly one append-only classifications row with no ON CONFLICT' , async ( ) = > {
stageQueries ( {
reports : [
2026-07-21 16:45:44 -04:00
{ id : 'report-1' , title : cleanSpamMessage.subject , created_at : '2026-07-15T00:00:00.000Z' , requester_email : REPORTER_EMAIL , company_id : null } ,
2026-07-16 08:20:49 -04:00
] ,
messages : [ toMessageRow ( 'message-1' , 'report-1' , cleanSpamMessage ) ] ,
indicators : [ ] ,
} ) ;
getBlastRadiusMock . mockResolvedValue ( {
status : 'ok' ,
matched : 1 ,
delivered : 0 ,
held : 1 ,
rejected : 0 ,
clicked : 0 ,
perRecipient : [ ] ,
source : 'fan-out' ,
} ) ;
await classifyCampaign ( 'campaign-1' ) ;
const insertCalls = queryMock . mock . calls . filter (
( [ sql ] ) = > typeof sql === 'string' && sql . includes ( 'INSERT INTO classifications' )
) ;
expect ( insertCalls ) . toHaveLength ( 1 ) ;
expect ( insertCalls [ 0 ] [ 0 ] ) . not . toMatch ( /ON CONFLICT/i ) ;
} ) ;
it . each ( [
[ 'knowbe4 (From match)' , knowbe4SimMessage ] ,
[ 'breach-secure-now (Return-Path match)' , bsnSimMessage ] ,
] ) (
'never classifies a known simulation sender as THREAT despite a hard auth fail and delivered>0 (simulation allowlist: %s)' ,
async ( _label , fixture ) = > {
stageQueries ( {
reports : [
2026-07-21 16:45:44 -04:00
{ id : 'report-1' , title : fixture.subject , created_at : '2026-07-15T00:00:00.000Z' , requester_email : REPORTER_EMAIL , company_id : null } ,
2026-07-16 08:20:49 -04:00
] ,
messages : [ toMessageRow ( 'message-1' , 'report-1' , fixture ) ] ,
indicators : [ ] ,
} ) ;
getBlastRadiusMock . mockResolvedValue ( {
status : 'ok' ,
matched : 1 ,
delivered : 1 ,
held : 0 ,
rejected : 0 ,
clicked : 0 ,
perRecipient : [ { recipient : REPORTER_EMAIL , status : 'delivered' } ] ,
source : 'fan-out' ,
} ) ;
const result = await classifyCampaign ( 'campaign-1' ) ;
expect ( result . verdict ) . not . toBe ( 'THREAT' ) ;
}
) ;
2026-07-16 19:36:53 -04:00
it . each ( [
[ 'knowbe4 (From match)' , knowbe4SimMessage ] ,
[ 'breach-secure-now (Return-Path match)' , bsnSimMessage ] ,
] ) (
'classifies a known simulation sender as USER_AWARENESS with acknowledge_user recommended and requiresApproval false (%s)' ,
async ( _label , fixture ) = > {
stageQueries ( {
reports : [
2026-07-21 16:45:44 -04:00
{ id : 'report-1' , title : fixture.subject , created_at : '2026-07-15T00:00:00.000Z' , requester_email : REPORTER_EMAIL , company_id : null } ,
2026-07-16 19:36:53 -04:00
] ,
messages : [ toMessageRow ( 'message-1' , 'report-1' , fixture ) ] ,
indicators : [ ] ,
} ) ;
getBlastRadiusMock . mockResolvedValue ( {
status : 'ok' ,
matched : 1 ,
delivered : 1 ,
held : 0 ,
rejected : 0 ,
clicked : 0 ,
perRecipient : [ { recipient : REPORTER_EMAIL , status : 'delivered' } ] ,
source : 'fan-out' ,
} ) ;
const result = await classifyCampaign ( 'campaign-1' ) ;
expect ( result . verdict ) . toBe ( 'USER_AWARENESS' ) ;
expect ( result . recommendedActions ) . toEqual ( [ 'acknowledge_user' ] ) ;
expect ( result . requiresApproval ) . toBe ( false ) ;
}
) ;
2026-07-16 08:20:49 -04:00
it ( 'classifies a real non-simulation signal as THREAT with destructive recommended actions (threat tier)' , async ( ) = > {
stageQueries ( {
reports : [
2026-07-21 16:45:44 -04:00
{ id : 'report-1' , title : threatMessage.subject , created_at : '2026-07-15T00:00:00.000Z' , requester_email : REPORTER_EMAIL , company_id : null } ,
2026-07-16 08:20:49 -04:00
] ,
messages : [ toMessageRow ( 'message-1' , 'report-1' , threatMessage ) ] ,
indicators : [ ] ,
} ) ;
getBlastRadiusMock . mockResolvedValue ( {
status : 'ok' ,
matched : 3 ,
delivered : 3 ,
held : 0 ,
rejected : 0 ,
clicked : 0 ,
perRecipient : [
{ recipient : REPORTER_EMAIL , status : 'delivered' } ,
{ recipient : 'victim2@wulfconsulting.test' , status : 'delivered' } ,
{ recipient : 'victim3@wulfconsulting.test' , status : 'delivered' } ,
] ,
source : 'fan-out' ,
} ) ;
const result = await classifyCampaign ( 'campaign-1' ) ;
expect ( result . verdict ) . toBe ( 'THREAT' ) ;
expect ( result . recommendedActions ) . toContain ( 'block_sender' ) ;
expect ( result . recommendedActions ) . toContain ( 'purge_message' ) ;
expect ( result . requiresApproval ) . toBe ( true ) ;
} ) ;
it ( 'classifies THREAT via the known-bad-indicator OR-branch when auth passes across 2 messages (threat tier known-bad indicator)' , async ( ) = > {
const sharedUrl = 'http://evil-shared.example.test/payload' ;
stageQueries ( {
reports : [
2026-07-21 16:45:44 -04:00
{ id : 'report-1' , title : 'Invoice attached' , created_at : '2026-07-15T00:00:00.000Z' , requester_email : REPORTER_EMAIL , company_id : null } ,
{ id : 'report-2' , title : 'Invoice attached' , created_at : '2026-07-15T02:00:00.000Z' , requester_email : 'reporter2@wulfconsulting.test' , company_id : null } ,
2026-07-16 08:20:49 -04:00
] ,
messages : [
toMessageRow ( 'message-1' , 'report-1' , cleanSpamMessage ) ,
toMessageRow ( 'message-2' , 'report-2' , cleanSpamMessage ) ,
] ,
indicators : [
{ id : 'ind-1' , message_id : 'message-1' , indicator_type : 'url' , value : sharedUrl } ,
{ id : 'ind-2' , message_id : 'message-2' , indicator_type : 'url' , value : sharedUrl } ,
] ,
} ) ;
getBlastRadiusMock . mockResolvedValue ( {
status : 'ok' ,
matched : 1 ,
delivered : 1 ,
held : 0 ,
rejected : 0 ,
clicked : 0 ,
perRecipient : [ { recipient : REPORTER_EMAIL , status : 'delivered' } ] ,
source : 'fan-out' ,
} ) ;
const result = await classifyCampaign ( 'campaign-1' ) ;
expect ( result . verdict ) . toBe ( 'THREAT' ) ;
} ) ;
it ( 'classifies a clean campaign with no indicators and no delivery/click signal as SPAM (spam vs unwanted tier)' , async ( ) = > {
stageQueries ( {
reports : [
2026-07-21 16:45:44 -04:00
{ id : 'report-1' , title : cleanSpamMessage.subject , created_at : '2026-07-15T00:00:00.000Z' , requester_email : REPORTER_EMAIL , company_id : null } ,
2026-07-16 08:20:49 -04:00
] ,
messages : [ toMessageRow ( 'message-1' , 'report-1' , cleanSpamMessage ) ] ,
indicators : [ ] ,
} ) ;
getBlastRadiusMock . mockResolvedValue ( {
status : 'ok' ,
matched : 1 ,
delivered : 0 ,
held : 1 ,
rejected : 0 ,
clicked : 0 ,
perRecipient : [ ] ,
source : 'fan-out' ,
} ) ;
const result = await classifyCampaign ( 'campaign-1' ) ;
expect ( result . verdict ) . toBe ( 'SPAM' ) ;
} ) ;
it ( 'classifies a suspicious-but-contained campaign (one url indicator, delivery contained to reporter) as UNWANTED (spam vs unwanted tier)' , async ( ) = > {
stageQueries ( {
reports : [
2026-07-21 16:45:44 -04:00
{ id : 'report-1' , title : suspiciousUnwantedMessage.subject , created_at : '2026-07-15T00:00:00.000Z' , requester_email : REPORTER_EMAIL , company_id : null } ,
2026-07-16 08:20:49 -04:00
] ,
messages : [ toMessageRow ( 'message-1' , 'report-1' , suspiciousUnwantedMessage ) ] ,
indicators : [
{ id : 'ind-1' , message_id : 'message-1' , indicator_type : 'url' , value : 'http://promo.some-vendor.net/deal' } ,
] ,
} ) ;
getBlastRadiusMock . mockResolvedValue ( {
status : 'ok' ,
matched : 1 ,
delivered : 1 ,
held : 0 ,
rejected : 0 ,
clicked : 0 ,
perRecipient : [ { recipient : REPORTER_EMAIL , status : 'delivered' } ] ,
source : 'fan-out' ,
} ) ;
const result = await classifyCampaign ( 'campaign-1' ) ;
expect ( result . verdict ) . toBe ( 'UNWANTED' ) ;
} ) ;
it ( 'keeps persisted reasons short and free of raw body text even with many indicators (evidence bounding)' , async ( ) = > {
const manyIndicators = Array . from ( { length : 50 } , ( _ , i ) = > ( {
id : ` ind- ${ i } ` ,
message_id : 'message-1' ,
indicator_type : 'url' ,
value : ` http://spammy- ${ i } .example.test/x ` ,
} ) ) ;
stageQueries ( {
reports : [
2026-07-21 16:45:44 -04:00
{ id : 'report-1' , title : cleanSpamMessage.subject , created_at : '2026-07-15T00:00:00.000Z' , requester_email : REPORTER_EMAIL , company_id : null } ,
2026-07-16 08:20:49 -04:00
] ,
messages : [ toMessageRow ( 'message-1' , 'report-1' , cleanSpamMessage ) ] ,
indicators : manyIndicators ,
} ) ;
getBlastRadiusMock . mockResolvedValue ( {
status : 'ok' ,
matched : 1 ,
delivered : 0 ,
held : 1 ,
rejected : 0 ,
clicked : 0 ,
perRecipient : [ ] ,
source : 'fan-out' ,
} ) ;
const result = await classifyCampaign ( 'campaign-1' ) ;
expect ( result . reasons . length ) . toBeLessThanOrEqual ( 5 ) ;
for ( const reason of result . reasons ) {
expect ( reason . length ) . toBeLessThan ( 300 ) ;
}
} ) ;
2026-07-21 16:45:44 -04:00
// ===========================================================================
// Bug 2 (D-05) parity: per-company Mimecast tenant resolution vs. global
// env fallback — mirrors app/api/phishing/campaigns/[id]/route.ts's
// already-tested tenant-resolution block (260721-n49).
// ===========================================================================
it ( 'resolves the reporting company\'s own Mimecast tenant and scopes getBlastRadius when an enabled mimecast_tenants row exists (tenant resolution)' , async ( ) = > {
stageQueries ( {
reports : [
{
id : 'report-1' ,
title : cleanSpamMessage.subject ,
created_at : '2026-07-15T00:00:00.000Z' ,
requester_email : REPORTER_EMAIL ,
company_id : '29683407' ,
} ,
] ,
messages : [ toMessageRow ( 'message-1' , 'report-1' , cleanSpamMessage ) ] ,
indicators : [ ] ,
mimecastTenants : [
{ client_id : 'tenant-client-id' , client_secret : 'tenant-client-secret' , base_url : 'https://eu-api.mimecast.com' } ,
] ,
} ) ;
getBlastRadiusMock . mockResolvedValue ( {
status : 'ok' ,
matched : 1 ,
delivered : 0 ,
held : 1 ,
rejected : 0 ,
clicked : 0 ,
perRecipient : [ ] ,
source : 'fan-out' ,
} ) ;
await classifyCampaign ( 'campaign-1' ) ;
expect ( getBlastRadiusMock ) . toHaveBeenCalledTimes ( 1 ) ;
const [ , tenantOptions ] = getBlastRadiusMock . mock . calls [ 0 ] ;
expect ( tenantOptions ) . toBeDefined ( ) ;
expect ( tenantOptions . cacheScope ) . toBe ( '29683407' ) ;
expect ( tenantOptions . client ) . toBeTruthy ( ) ;
const tenantQueryCalls = queryMock . mock . calls . filter (
( [ sql ] ) = > typeof sql === 'string' && sql . includes ( 'FROM mimecast_tenants' )
) ;
expect ( tenantQueryCalls ) . toHaveLength ( 1 ) ;
expect ( tenantQueryCalls [ 0 ] [ 0 ] ) . toMatch ( /company_id = \$1 AND enabled = true/ ) ;
} ) ;
it . each ( [
[ 'no companyId on the primary report' , null , [ ] as Array < { client_id : string ; client_secret : string ; base_url : string | null } > ] ,
[ 'companyId set but no enabled tenant row' , '29683407' , [ ] ] ,
] ) (
'calls getBlastRadius with no tenant scoping when %s (global fallback preserved)' ,
async ( _label , companyId , mimecastTenants ) = > {
stageQueries ( {
reports : [
{
id : 'report-1' ,
title : cleanSpamMessage.subject ,
created_at : '2026-07-15T00:00:00.000Z' ,
requester_email : REPORTER_EMAIL ,
company_id : companyId ,
} ,
] ,
messages : [ toMessageRow ( 'message-1' , 'report-1' , cleanSpamMessage ) ] ,
indicators : [ ] ,
mimecastTenants ,
} ) ;
getBlastRadiusMock . mockResolvedValue ( {
status : 'ok' ,
matched : 1 ,
delivered : 0 ,
held : 1 ,
rejected : 0 ,
clicked : 0 ,
perRecipient : [ ] ,
source : 'fan-out' ,
} ) ;
await classifyCampaign ( 'campaign-1' ) ;
expect ( getBlastRadiusMock ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( getBlastRadiusMock . mock . calls [ 0 ] [ 1 ] ) . toBeUndefined ( ) ;
}
) ;
2026-07-16 08:20:49 -04:00
} ) ;