- blast-radius: getHeldMessages called with same start/end window as searchDeliveredMessages - blast-radius: unrelated-sender held row excluded from held/matched and perRecipient; matching-sender held row still counts and overrides - client: getHeldMessages threads start/end into POST body data[0] when provided, omits them when not
146 lines
5.2 KiB
TypeScript
146 lines
5.2 KiB
TypeScript
/**
|
|
* mimecast-client.ts — isMimecastConfigured()/getMimecastClient() factory tests.
|
|
*
|
|
* Mirrors pax8-factory.test.ts's beforeEach env-var-delete + reset seam
|
|
* pattern. Does not test MimecastClient's other 20+ methods — out of scope
|
|
* for this phase (Phase 17, Plan 01, Task 1).
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import {
|
|
isMimecastConfigured,
|
|
getMimecastClient,
|
|
getMimecastClientForTenant,
|
|
_resetMimecastClient,
|
|
} from './mimecast-client';
|
|
|
|
beforeEach(() => {
|
|
delete process.env.MIMECAST_CLIENT_ID;
|
|
delete process.env.MIMECAST_CLIENT_SECRET;
|
|
_resetMimecastClient();
|
|
});
|
|
|
|
describe('isMimecastConfigured', () => {
|
|
it('returns false when neither env var is set', () => {
|
|
expect(isMimecastConfigured()).toBe(false);
|
|
});
|
|
|
|
it('returns false when only MIMECAST_CLIENT_ID is set', () => {
|
|
process.env.MIMECAST_CLIENT_ID = 'id1';
|
|
expect(isMimecastConfigured()).toBe(false);
|
|
});
|
|
|
|
it('returns false when only MIMECAST_CLIENT_SECRET is set', () => {
|
|
process.env.MIMECAST_CLIENT_SECRET = 'secret1';
|
|
expect(isMimecastConfigured()).toBe(false);
|
|
});
|
|
|
|
it('returns true when both env vars are set', () => {
|
|
process.env.MIMECAST_CLIENT_ID = 'id1';
|
|
process.env.MIMECAST_CLIENT_SECRET = 'secret1';
|
|
expect(isMimecastConfigured()).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('getMimecastClient', () => {
|
|
it('throws the exact configuration error when not configured', () => {
|
|
expect(() => getMimecastClient()).toThrow(
|
|
'MIMECAST_CLIENT_ID and MIMECAST_CLIENT_SECRET must be set'
|
|
);
|
|
});
|
|
|
|
it('returns the same cached instance on repeated calls', () => {
|
|
process.env.MIMECAST_CLIENT_ID = 'id1';
|
|
process.env.MIMECAST_CLIENT_SECRET = 'secret1';
|
|
const first = getMimecastClient();
|
|
const second = getMimecastClient();
|
|
expect(second).toBe(first);
|
|
});
|
|
|
|
it('rebuilds a new instance after _resetMimecastClient()', () => {
|
|
process.env.MIMECAST_CLIENT_ID = 'id1';
|
|
process.env.MIMECAST_CLIENT_SECRET = 'secret1';
|
|
const first = getMimecastClient();
|
|
_resetMimecastClient();
|
|
const second = getMimecastClient();
|
|
expect(second).not.toBe(first);
|
|
});
|
|
});
|
|
|
|
describe('getMimecastClientForTenant', () => {
|
|
// Fake credentials only — never real mimecast_tenants values.
|
|
const FAKE_TENANT = { client_id: 'tid', client_secret: 'tsecret', base_url: 'https://tenant.example' };
|
|
|
|
it('returns a MimecastClient instance exposing the fan-out methods', () => {
|
|
const client = getMimecastClientForTenant(FAKE_TENANT);
|
|
expect(client).toBeTruthy();
|
|
expect(typeof client.searchDeliveredMessages).toBe('function');
|
|
expect(typeof client.getHeldMessages).toBe('function');
|
|
expect(typeof client.getThreatEvents).toBe('function');
|
|
});
|
|
|
|
it('returns a NEW instance on each call — never the cached global', () => {
|
|
const first = getMimecastClientForTenant(FAKE_TENANT);
|
|
const second = getMimecastClientForTenant(FAKE_TENANT);
|
|
expect(second).not.toBe(first);
|
|
});
|
|
|
|
it('does not populate or replace the cached global getMimecastClient() instance', () => {
|
|
process.env.MIMECAST_CLIENT_ID = 'id1';
|
|
process.env.MIMECAST_CLIENT_SECRET = 'secret1';
|
|
|
|
getMimecastClientForTenant(FAKE_TENANT);
|
|
const global1 = getMimecastClient();
|
|
const global2 = getMimecastClient();
|
|
|
|
// The global singleton is unaffected by tenant-client construction —
|
|
// still cached and independent of the tenant instance.
|
|
expect(global2).toBe(global1);
|
|
expect(global1).not.toBe(getMimecastClientForTenant(FAKE_TENANT));
|
|
});
|
|
|
|
it('does not throw when base_url is omitted (defaults to the Mimecast API host)', () => {
|
|
expect(() => getMimecastClientForTenant({ client_id: 'tid', client_secret: 'tsecret' })).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('getHeldMessages date-scoping', () => {
|
|
// Fake credentials only — never real mimecast_tenants values.
|
|
const FAKE_TENANT = { client_id: 'tid', client_secret: 'tsecret', base_url: 'https://tenant.example' };
|
|
|
|
it('includes start/end in the POST body data[0] when provided', async () => {
|
|
const client = getMimecastClientForTenant(FAKE_TENANT);
|
|
const requestSpy = vi
|
|
.spyOn(client as any, 'request')
|
|
.mockResolvedValue({ data: [], meta: { pagination: {} } });
|
|
|
|
await client.getHeldMessages({
|
|
recipient: 'r@x.test',
|
|
start: '2026-07-14T00:00:00+0000',
|
|
end: '2026-07-15T00:00:00+0000',
|
|
});
|
|
|
|
expect(requestSpy).toHaveBeenCalledTimes(1);
|
|
const body: any = requestSpy.mock.calls[0][2];
|
|
expect(body.data[0]).toMatchObject({
|
|
admin: true,
|
|
start: '2026-07-14T00:00:00+0000',
|
|
end: '2026-07-15T00:00:00+0000',
|
|
searchBy: { fieldName: 'recipient', value: 'r@x.test' },
|
|
});
|
|
});
|
|
|
|
it('omits start/end from the POST body data[0] when not provided', async () => {
|
|
const client = getMimecastClientForTenant(FAKE_TENANT);
|
|
const requestSpy = vi
|
|
.spyOn(client as any, 'request')
|
|
.mockResolvedValue({ data: [], meta: { pagination: {} } });
|
|
|
|
await client.getHeldMessages({ recipient: 'r@x.test' });
|
|
|
|
expect(requestSpy).toHaveBeenCalledTimes(1);
|
|
const body: any = requestSpy.mock.calls[0][2];
|
|
expect(body.data[0]).not.toHaveProperty('start');
|
|
expect(body.data[0]).not.toHaveProperty('end');
|
|
});
|
|
});
|