test(260721-n49): cover tenant-scoped vs global-fallback getBlastRadius branches

- Add mimecast_tenants routing branch to stageQueries and company_id to
  report fixtures
- New test: enabled mimecast_tenants row -> getBlastRadius called with
  { client, cacheScope: companyId } and the tenant SQL is issued
- New parameterized test: null companyId and companyId-with-no-enabled-row
  -> getBlastRadius called with no second argument (global fallback)
This commit is contained in:
lorentz 2026-07-21 16:45:44 -04:00
parent 9f75f2160c
commit 9f12cd610a

View file

@ -247,12 +247,14 @@ interface ReportFixtureRow {
title: string | null;
created_at: string;
requester_email: string | null;
company_id: string | null;
}
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 }>;
mimecastTenants?: Array<{ client_id: string; client_secret: string; base_url: string | null }>;
}
function stageQueries(rows: StagedRows) {
@ -260,6 +262,9 @@ function stageQueries(rows: StagedRows) {
if (sql.includes('INSERT INTO classifications')) {
return { rows: [{ id: 'classification-1', created_at: '2026-07-16T00:00:00.000Z' }], rowCount: 1 };
}
if (sql.includes('FROM mimecast_tenants')) {
return { rows: rows.mimecastTenants ?? [], rowCount: rows.mimecastTenants?.length ?? 0 };
}
if (sql.includes('FROM reports')) {
return { rows: rows.reports ?? [], rowCount: rows.reports?.length ?? 0 };
}
@ -288,7 +293,7 @@ describe('classifyCampaign', () => {
it('returns exactly one verdict with the full payload shape (returns exactly one verdict)', async () => {
stageQueries({
reports: [
{ id: 'report-1', title: cleanSpamMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL },
{ id: 'report-1', title: cleanSpamMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL , company_id: null},
],
messages: [toMessageRow('message-1', 'report-1', cleanSpamMessage)],
indicators: [],
@ -320,7 +325,7 @@ describe('classifyCampaign', () => {
it('inserts exactly one append-only classifications row with no ON CONFLICT', async () => {
stageQueries({
reports: [
{ id: 'report-1', title: cleanSpamMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL },
{ id: 'report-1', title: cleanSpamMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL , company_id: null},
],
messages: [toMessageRow('message-1', 'report-1', cleanSpamMessage)],
indicators: [],
@ -353,7 +358,7 @@ describe('classifyCampaign', () => {
async (_label, fixture) => {
stageQueries({
reports: [
{ id: 'report-1', title: fixture.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL },
{ id: 'report-1', title: fixture.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL , company_id: null},
],
messages: [toMessageRow('message-1', 'report-1', fixture)],
indicators: [],
@ -383,7 +388,7 @@ describe('classifyCampaign', () => {
async (_label, fixture) => {
stageQueries({
reports: [
{ id: 'report-1', title: fixture.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL },
{ id: 'report-1', title: fixture.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL , company_id: null},
],
messages: [toMessageRow('message-1', 'report-1', fixture)],
indicators: [],
@ -410,7 +415,7 @@ describe('classifyCampaign', () => {
it('classifies a real non-simulation signal as THREAT with destructive recommended actions (threat tier)', async () => {
stageQueries({
reports: [
{ id: 'report-1', title: threatMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL },
{ id: 'report-1', title: threatMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL , company_id: null},
],
messages: [toMessageRow('message-1', 'report-1', threatMessage)],
indicators: [],
@ -442,8 +447,8 @@ describe('classifyCampaign', () => {
const sharedUrl = 'http://evil-shared.example.test/payload';
stageQueries({
reports: [
{ id: 'report-1', title: 'Invoice attached', created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL },
{ id: 'report-2', title: 'Invoice attached', created_at: '2026-07-15T02:00:00.000Z', requester_email: 'reporter2@wulfconsulting.test' },
{ 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},
],
messages: [
toMessageRow('message-1', 'report-1', cleanSpamMessage),
@ -473,7 +478,7 @@ describe('classifyCampaign', () => {
it('classifies a clean campaign with no indicators and no delivery/click signal as SPAM (spam vs unwanted tier)', async () => {
stageQueries({
reports: [
{ id: 'report-1', title: cleanSpamMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL },
{ id: 'report-1', title: cleanSpamMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL , company_id: null},
],
messages: [toMessageRow('message-1', 'report-1', cleanSpamMessage)],
indicators: [],
@ -497,7 +502,7 @@ describe('classifyCampaign', () => {
it('classifies a suspicious-but-contained campaign (one url indicator, delivery contained to reporter) as UNWANTED (spam vs unwanted tier)', async () => {
stageQueries({
reports: [
{ id: 'report-1', title: suspiciousUnwantedMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL },
{ id: 'report-1', title: suspiciousUnwantedMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL , company_id: null},
],
messages: [toMessageRow('message-1', 'report-1', suspiciousUnwantedMessage)],
indicators: [
@ -529,7 +534,7 @@ describe('classifyCampaign', () => {
}));
stageQueries({
reports: [
{ id: 'report-1', title: cleanSpamMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL },
{ id: 'report-1', title: cleanSpamMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL , company_id: null},
],
messages: [toMessageRow('message-1', 'report-1', cleanSpamMessage)],
indicators: manyIndicators,
@ -552,4 +557,91 @@ describe('classifyCampaign', () => {
expect(reason.length).toBeLessThan(300);
}
});
// ===========================================================================
// 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();
}
);
});