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:
parent
9f75f2160c
commit
9f12cd610a
1 changed files with 102 additions and 10 deletions
|
|
@ -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();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue