Files
vyndr/tests/unit/sentry.test.js
T

133 lines
4.8 KiB
JavaScript

// Sentry init wrapper — guarantees graceful no-op when SENTRY_DSN is
// absent, scrubs PII on send when configured, and exposes a usable
// surface to the rest of the codebase regardless of init state.
// Mock @sentry/node BEFORE requiring our wrapper so we control what
// init() and setupExpressErrorHandler do.
const mockSentryInit = jest.fn();
const mockSentryCapture = jest.fn();
const mockSentryHandler = jest.fn();
jest.mock('@sentry/node', () => ({
init: (...a) => mockSentryInit(...a),
captureException: (...a) => mockSentryCapture(...a),
captureMessage: jest.fn(),
setupExpressErrorHandler: (...a) => mockSentryHandler(...a),
addBreadcrumb: jest.fn(),
setUser: jest.fn(),
setTag: jest.fn(),
setContext: jest.fn(),
}));
const sentryWrapper = require('../../src/utils/sentry');
beforeEach(() => {
mockSentryInit.mockReset();
mockSentryCapture.mockReset();
mockSentryHandler.mockReset();
sentryWrapper.__internals.__resetForTests();
delete process.env.SENTRY_DSN;
});
describe('initSentry', () => {
test('no-op when SENTRY_DSN is unset (sentry.init NOT called)', () => {
sentryWrapper.initSentry();
expect(mockSentryInit).not.toHaveBeenCalled();
expect(sentryWrapper.isInitialized()).toBe(false);
});
test('initializes when SENTRY_DSN is set', () => {
process.env.SENTRY_DSN = 'https://abc@sentry.io/123';
sentryWrapper.initSentry();
expect(mockSentryInit).toHaveBeenCalledTimes(1);
expect(sentryWrapper.isInitialized()).toBe(true);
const cfg = mockSentryInit.mock.calls[0][0];
expect(cfg.dsn).toBe('https://abc@sentry.io/123');
expect(cfg.tracesSampleRate).toBe(0.1);
expect(cfg.sendDefaultPii).toBe(false);
expect(typeof cfg.beforeSend).toBe('function');
});
test('explicit dsn arg overrides env', () => {
process.env.SENTRY_DSN = 'env-dsn';
sentryWrapper.initSentry({ dsn: 'arg-dsn' });
expect(mockSentryInit.mock.calls[0][0].dsn).toBe('arg-dsn');
});
test('idempotent — second call is a no-op', () => {
process.env.SENTRY_DSN = 'https://abc@sentry.io/123';
sentryWrapper.initSentry();
sentryWrapper.initSentry();
expect(mockSentryInit).toHaveBeenCalledTimes(1);
});
});
describe('beforeSend PII scrubbing', () => {
beforeEach(() => {
process.env.SENTRY_DSN = 'https://abc@sentry.io/123';
sentryWrapper.initSentry();
});
test('strips user.ip_address and user.email', () => {
const cfg = mockSentryInit.mock.calls[0][0];
const event = { user: { id: 'u1', ip_address: '1.2.3.4', email: 'a@b.com' } };
const scrubbed = cfg.beforeSend(event);
expect(scrubbed.user.id).toBe('u1');
expect(scrubbed.user.ip_address).toBeUndefined();
expect(scrubbed.user.email).toBeUndefined();
});
test('strips cookies + authorization + internal-key headers from request', () => {
const cfg = mockSentryInit.mock.calls[0][0];
const event = {
request: {
cookies: { session: 'abc' },
headers: {
authorization: 'Bearer secret',
cookie: 'session=abc',
'x-internal-key': 'leak',
'x-vyndr-internal-key': 'leak',
'content-type': 'application/json',
},
},
};
const scrubbed = cfg.beforeSend(event);
expect(scrubbed.request.cookies).toBeUndefined();
expect(scrubbed.request.headers.authorization).toBeUndefined();
expect(scrubbed.request.headers.cookie).toBeUndefined();
expect(scrubbed.request.headers['x-internal-key']).toBeUndefined();
expect(scrubbed.request.headers['x-vyndr-internal-key']).toBeUndefined();
// Non-sensitive headers are kept.
expect(scrubbed.request.headers['content-type']).toBe('application/json');
});
});
describe('noop client surface', () => {
test('captureException is callable without DSN (no throw)', () => {
sentryWrapper.initSentry();
expect(() => sentryWrapper.Sentry.captureException(new Error('test'))).not.toThrow();
expect(mockSentryCapture).not.toHaveBeenCalled();
});
test('setupExpressErrorHandler is callable without DSN (no throw)', () => {
sentryWrapper.initSentry();
expect(() => sentryWrapper.Sentry.setupExpressErrorHandler({})).not.toThrow();
expect(mockSentryHandler).not.toHaveBeenCalled();
});
test('after init, captureException routes to real Sentry', () => {
process.env.SENTRY_DSN = 'https://abc@sentry.io/123';
sentryWrapper.initSentry();
const err = new Error('boom');
sentryWrapper.Sentry.captureException(err);
expect(mockSentryCapture).toHaveBeenCalledWith(err);
});
test('after init, setupExpressErrorHandler delegates to real Sentry', () => {
process.env.SENTRY_DSN = 'https://abc@sentry.io/123';
sentryWrapper.initSentry();
const app = {};
sentryWrapper.Sentry.setupExpressErrorHandler(app);
expect(mockSentryHandler).toHaveBeenCalledWith(app);
});
});