Session 10: Internal auth refactor, prefetch cascade keys, Sentry, welcome email (1286 tests)
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user