Files
flyer-crawler.projectium.com/src/services/sentry.server.test.ts
Torben Sorensen c78323275b
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 2m28s
more unit tests - done for now
2026-01-29 16:21:48 -08:00

1186 lines
39 KiB
TypeScript

// src/services/sentry.server.test.ts
/**
* Comprehensive unit tests for the Sentry server-side initialization and configuration.
*
* This test file covers:
* - Sentry.init() is called with correct configuration
* - Environment-based configuration (development vs production)
* - DSN configuration
* - Integration setup (server-side integrations)
* - Error handling when Sentry is not configured
* - Initialization behavior based on environment
* - Middleware behavior (requestHandler and errorHandler)
* - captureException, captureMessage, setUser, addBreadcrumb functions
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { Request, Response, NextFunction } from 'express';
// Use vi.hoisted to define mocks that need to be available before vi.mock runs
const { mockSentry, mockLogger } = vi.hoisted(() => ({
mockSentry: {
init: vi.fn(),
captureException: vi.fn(() => 'mock-event-id'),
captureMessage: vi.fn(() => 'mock-message-id'),
setContext: vi.fn(),
setUser: vi.fn(),
addBreadcrumb: vi.fn(),
},
mockLogger: {
info: vi.fn(),
debug: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
}));
vi.mock('@sentry/node', () => mockSentry);
vi.mock('./logger.server', () => ({
logger: mockLogger,
}));
describe('sentry.server', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.unstubAllEnvs();
});
// ============================================================================
// SECTION 1: Tests with Sentry DISABLED (default test environment)
// ============================================================================
describe('with Sentry disabled (isSentryConfigured=false)', () => {
beforeEach(() => {
vi.resetModules();
// Mock config/env module - Sentry is NOT configured
vi.doMock('../config/env', () => ({
config: {
sentry: {
dsn: '',
environment: 'test',
debug: false,
},
server: {
nodeEnv: 'test',
},
},
isSentryConfigured: false,
isProduction: false,
isTest: true,
}));
});
it('should not initialize Sentry when DSN is not configured', async () => {
const { initSentry } = await import('./sentry.server');
initSentry();
expect(mockSentry.init).not.toHaveBeenCalled();
expect(mockLogger.info).toHaveBeenCalledWith(
'[Sentry] Error tracking disabled (SENTRY_DSN not configured)',
);
});
it('should return null from captureException when not configured', async () => {
const { captureException } = await import('./sentry.server');
const result = captureException(new Error('test error'));
expect(result).toBeNull();
expect(mockSentry.captureException).not.toHaveBeenCalled();
});
it('should return null from captureException with context when not configured', async () => {
const { captureException } = await import('./sentry.server');
const result = captureException(new Error('test error'), { userId: '123', action: 'test' });
expect(result).toBeNull();
expect(mockSentry.setContext).not.toHaveBeenCalled();
expect(mockSentry.captureException).not.toHaveBeenCalled();
});
it('should return null from captureMessage when not configured', async () => {
const { captureMessage } = await import('./sentry.server');
const result = captureMessage('test message');
expect(result).toBeNull();
expect(mockSentry.captureMessage).not.toHaveBeenCalled();
});
it('should return null from captureMessage with custom level when not configured', async () => {
const { captureMessage } = await import('./sentry.server');
const result = captureMessage('warning message', 'warning');
expect(result).toBeNull();
expect(mockSentry.captureMessage).not.toHaveBeenCalled();
});
it('should not set user when not configured', async () => {
const { setUser } = await import('./sentry.server');
setUser({ id: '123', email: 'test@example.com' });
expect(mockSentry.setUser).not.toHaveBeenCalled();
});
it('should not clear user when not configured', async () => {
const { setUser } = await import('./sentry.server');
setUser(null);
expect(mockSentry.setUser).not.toHaveBeenCalled();
});
it('should not add breadcrumb when not configured', async () => {
const { addBreadcrumb } = await import('./sentry.server');
addBreadcrumb({ message: 'test breadcrumb', category: 'test' });
expect(mockSentry.addBreadcrumb).not.toHaveBeenCalled();
});
it('should return no-op middleware when not configured', async () => {
const { getSentryMiddleware } = await import('./sentry.server');
const middleware = getSentryMiddleware();
expect(middleware.requestHandler).toBeDefined();
expect(middleware.errorHandler).toBeDefined();
// Test that requestHandler just calls next()
const req = {} as Request;
const res = {} as Response;
const next = vi.fn() as unknown as NextFunction;
middleware.requestHandler(req, res, next);
expect(next).toHaveBeenCalledTimes(1);
expect(next).toHaveBeenCalledWith();
});
it('should have errorHandler that passes error to next() when not configured', async () => {
const { getSentryMiddleware } = await import('./sentry.server');
const middleware = getSentryMiddleware();
const error = new Error('test error');
const req = {} as Request;
const res = {} as Response;
const next = vi.fn() as unknown as NextFunction;
middleware.errorHandler(error, req, res, next);
expect(next).toHaveBeenCalledTimes(1);
expect(next).toHaveBeenCalledWith(error);
expect(mockSentry.captureException).not.toHaveBeenCalled();
});
});
// ============================================================================
// SECTION 2: Tests with Sentry configured but in TEST environment
// ============================================================================
describe('with Sentry configured but in test environment (isTest=true)', () => {
beforeEach(() => {
vi.resetModules();
vi.doMock('../config/env', () => ({
config: {
sentry: {
dsn: 'https://key@sentry.example.com/123',
environment: 'test',
debug: false,
},
server: {
nodeEnv: 'test',
},
},
isSentryConfigured: true,
isProduction: false,
isTest: true,
}));
});
it('should skip initialization in test environment even when configured', async () => {
const { initSentry } = await import('./sentry.server');
initSentry();
expect(mockSentry.init).not.toHaveBeenCalled();
expect(mockLogger.debug).toHaveBeenCalledWith(
'[Sentry] Skipping initialization in test environment',
);
});
it('should return null from captureException in test environment', async () => {
const { captureException } = await import('./sentry.server');
const result = captureException(new Error('test error'));
expect(result).toBeNull();
expect(mockSentry.captureException).not.toHaveBeenCalled();
});
it('should return null from captureMessage in test environment', async () => {
const { captureMessage } = await import('./sentry.server');
const result = captureMessage('test message');
expect(result).toBeNull();
});
it('should not set user in test environment', async () => {
const { setUser } = await import('./sentry.server');
setUser({ id: '123' });
expect(mockSentry.setUser).not.toHaveBeenCalled();
});
it('should not add breadcrumb in test environment', async () => {
const { addBreadcrumb } = await import('./sentry.server');
addBreadcrumb({ message: 'test' });
expect(mockSentry.addBreadcrumb).not.toHaveBeenCalled();
});
it('should return no-op middleware in test environment', async () => {
const { getSentryMiddleware } = await import('./sentry.server');
const middleware = getSentryMiddleware();
const next = vi.fn();
middleware.requestHandler({} as Request, {} as Response, next);
expect(next).toHaveBeenCalledWith();
const error = new Error('test');
middleware.errorHandler(error, {} as Request, {} as Response, next);
expect(next).toHaveBeenCalledWith(error);
});
});
// ============================================================================
// SECTION 3: Tests with Sentry ENABLED in development environment
// ============================================================================
describe('with Sentry enabled in development environment', () => {
beforeEach(() => {
vi.resetModules();
vi.doMock('../config/env', () => ({
config: {
sentry: {
dsn: 'https://key@bugsink.example.com/123',
environment: 'development',
debug: true,
},
server: {
nodeEnv: 'development',
},
},
isSentryConfigured: true,
isProduction: false,
isTest: false,
}));
});
it('should initialize Sentry with correct configuration in development', async () => {
const { initSentry } = await import('./sentry.server');
initSentry();
expect(mockSentry.init).toHaveBeenCalledTimes(1);
expect(mockSentry.init).toHaveBeenCalledWith(
expect.objectContaining({
dsn: 'https://key@bugsink.example.com/123',
environment: 'development',
debug: true,
tracesSampleRate: 0,
}),
);
});
it('should log initialization message with environment', async () => {
const { initSentry } = await import('./sentry.server');
initSentry();
expect(mockLogger.info).toHaveBeenCalledWith(
{ environment: 'development' },
'[Sentry] Error tracking initialized',
);
});
it('should capture exception and return event ID when configured', async () => {
const { captureException } = await import('./sentry.server');
const testError = new Error('Test error for Sentry');
const result = captureException(testError);
expect(result).toBe('mock-event-id');
expect(mockSentry.captureException).toHaveBeenCalledWith(testError);
});
it('should set context before capturing exception when context is provided', async () => {
const { captureException } = await import('./sentry.server');
const testError = new Error('Test error');
const context = { userId: '123', action: 'test-action' };
captureException(testError, context);
expect(mockSentry.setContext).toHaveBeenCalledWith('additional', context);
expect(mockSentry.captureException).toHaveBeenCalledWith(testError);
});
it('should not set context when context is not provided', async () => {
const { captureException } = await import('./sentry.server');
const testError = new Error('Test error');
captureException(testError);
expect(mockSentry.setContext).not.toHaveBeenCalled();
expect(mockSentry.captureException).toHaveBeenCalledWith(testError);
});
it('should capture message with default info level', async () => {
const { captureMessage } = await import('./sentry.server');
const result = captureMessage('Test message');
expect(result).toBe('mock-message-id');
expect(mockSentry.captureMessage).toHaveBeenCalledWith('Test message', 'info');
});
it('should capture message with custom severity level', async () => {
const { captureMessage } = await import('./sentry.server');
captureMessage('Error occurred', 'error');
expect(mockSentry.captureMessage).toHaveBeenCalledWith('Error occurred', 'error');
});
it('should capture message with warning level', async () => {
const { captureMessage } = await import('./sentry.server');
captureMessage('Warning message', 'warning');
expect(mockSentry.captureMessage).toHaveBeenCalledWith('Warning message', 'warning');
});
it('should set user context when provided', async () => {
const { setUser } = await import('./sentry.server');
setUser({ id: 'user-123', email: 'user@example.com', username: 'testuser' });
expect(mockSentry.setUser).toHaveBeenCalledWith({
id: 'user-123',
email: 'user@example.com',
username: 'testuser',
});
});
it('should set user with id only', async () => {
const { setUser } = await import('./sentry.server');
setUser({ id: 'user-456' });
expect(mockSentry.setUser).toHaveBeenCalledWith({ id: 'user-456' });
});
it('should clear user by passing null', async () => {
const { setUser } = await import('./sentry.server');
setUser(null);
expect(mockSentry.setUser).toHaveBeenCalledWith(null);
});
it('should add breadcrumb with message', async () => {
const { addBreadcrumb } = await import('./sentry.server');
addBreadcrumb({ message: 'User clicked button' });
expect(mockSentry.addBreadcrumb).toHaveBeenCalledWith({ message: 'User clicked button' });
});
it('should add breadcrumb with category and level', async () => {
const { addBreadcrumb } = await import('./sentry.server');
addBreadcrumb({ message: 'Navigation', category: 'navigation', level: 'info' });
expect(mockSentry.addBreadcrumb).toHaveBeenCalledWith({
message: 'Navigation',
category: 'navigation',
level: 'info',
});
});
it('should add breadcrumb with data', async () => {
const { addBreadcrumb } = await import('./sentry.server');
addBreadcrumb({
message: 'API call',
category: 'http',
data: { url: '/api/v1/test', method: 'GET', status_code: 200 },
});
expect(mockSentry.addBreadcrumb).toHaveBeenCalledWith({
message: 'API call',
category: 'http',
data: { url: '/api/v1/test', method: 'GET', status_code: 200 },
});
});
});
// ============================================================================
// SECTION 4: Tests with Sentry ENABLED in production environment
// ============================================================================
describe('with Sentry enabled in production environment', () => {
beforeEach(() => {
vi.resetModules();
vi.doMock('../config/env', () => ({
config: {
sentry: {
dsn: 'https://prodkey@bugsink.projectium.com/1',
environment: 'production',
debug: false,
},
server: {
nodeEnv: 'production',
},
},
isSentryConfigured: true,
isProduction: true,
isTest: false,
}));
});
it('should initialize Sentry with production configuration', async () => {
const { initSentry } = await import('./sentry.server');
initSentry();
expect(mockSentry.init).toHaveBeenCalledTimes(1);
expect(mockSentry.init).toHaveBeenCalledWith(
expect.objectContaining({
dsn: 'https://prodkey@bugsink.projectium.com/1',
environment: 'production',
debug: false,
tracesSampleRate: 0,
}),
);
});
it('should log initialization with production environment', async () => {
const { initSentry } = await import('./sentry.server');
initSentry();
expect(mockLogger.info).toHaveBeenCalledWith(
{ environment: 'production' },
'[Sentry] Error tracking initialized',
);
});
it('should include beforeSend callback in init config', async () => {
const { initSentry } = await import('./sentry.server');
initSentry();
const initCall = mockSentry.init.mock.calls[0][0];
expect(initCall.beforeSend).toBeDefined();
expect(typeof initCall.beforeSend).toBe('function');
});
});
// ============================================================================
// SECTION 5: Tests for getSentryMiddleware when Sentry is enabled
// ============================================================================
describe('getSentryMiddleware when Sentry is enabled', () => {
beforeEach(() => {
vi.resetModules();
vi.doMock('../config/env', () => ({
config: {
sentry: {
dsn: 'https://key@sentry.example.com/123',
environment: 'development',
debug: false,
},
server: {
nodeEnv: 'development',
},
},
isSentryConfigured: true,
isProduction: false,
isTest: false,
}));
});
it('should return functional middleware when Sentry is configured', async () => {
const { getSentryMiddleware } = await import('./sentry.server');
const middleware = getSentryMiddleware();
expect(middleware.requestHandler).toBeDefined();
expect(middleware.errorHandler).toBeDefined();
});
it('requestHandler should call next() in SDK v8+ compatibility mode', async () => {
const { getSentryMiddleware } = await import('./sentry.server');
const middleware = getSentryMiddleware();
const req = {} as Request;
const res = {} as Response;
const next = vi.fn();
middleware.requestHandler(req, res, next);
expect(next).toHaveBeenCalledTimes(1);
expect(next).toHaveBeenCalledWith();
});
it('errorHandler should capture 5xx errors to Sentry', async () => {
const { getSentryMiddleware } = await import('./sentry.server');
const middleware = getSentryMiddleware();
const error = Object.assign(new Error('Internal Server Error'), { statusCode: 500 });
const req = {} as Request;
const res = {} as Response;
const next = vi.fn();
middleware.errorHandler(error, req, res, next);
expect(mockSentry.captureException).toHaveBeenCalledWith(error);
expect(next).toHaveBeenCalledWith(error);
});
it('errorHandler should capture 502 Bad Gateway errors', async () => {
const { getSentryMiddleware } = await import('./sentry.server');
const middleware = getSentryMiddleware();
const error = Object.assign(new Error('Bad Gateway'), { statusCode: 502 });
const req = {} as Request;
const res = {} as Response;
const next = vi.fn();
middleware.errorHandler(error, req, res, next);
expect(mockSentry.captureException).toHaveBeenCalledWith(error);
expect(next).toHaveBeenCalledWith(error);
});
it('errorHandler should capture 503 Service Unavailable errors', async () => {
const { getSentryMiddleware } = await import('./sentry.server');
const middleware = getSentryMiddleware();
const error = Object.assign(new Error('Service Unavailable'), { statusCode: 503 });
const req = {} as Request;
const res = {} as Response;
const next = vi.fn();
middleware.errorHandler(error, req, res, next);
expect(mockSentry.captureException).toHaveBeenCalledWith(error);
expect(next).toHaveBeenCalledWith(error);
});
it('errorHandler should NOT capture 4xx errors', async () => {
const { getSentryMiddleware } = await import('./sentry.server');
const middleware = getSentryMiddleware();
const error = Object.assign(new Error('Not Found'), { statusCode: 404 });
const req = {} as Request;
const res = {} as Response;
const next = vi.fn();
middleware.errorHandler(error, req, res, next);
expect(mockSentry.captureException).not.toHaveBeenCalled();
expect(next).toHaveBeenCalledWith(error);
});
it('errorHandler should NOT capture 400 Bad Request errors', async () => {
const { getSentryMiddleware } = await import('./sentry.server');
const middleware = getSentryMiddleware();
const error = Object.assign(new Error('Bad Request'), { statusCode: 400 });
const req = {} as Request;
const res = {} as Response;
const next = vi.fn();
middleware.errorHandler(error, req, res, next);
expect(mockSentry.captureException).not.toHaveBeenCalled();
expect(next).toHaveBeenCalledWith(error);
});
it('errorHandler should NOT capture 401 Unauthorized errors', async () => {
const { getSentryMiddleware } = await import('./sentry.server');
const middleware = getSentryMiddleware();
const error = Object.assign(new Error('Unauthorized'), { statusCode: 401 });
const req = {} as Request;
const res = {} as Response;
const next = vi.fn();
middleware.errorHandler(error, req, res, next);
expect(mockSentry.captureException).not.toHaveBeenCalled();
expect(next).toHaveBeenCalledWith(error);
});
it('errorHandler should NOT capture 403 Forbidden errors', async () => {
const { getSentryMiddleware } = await import('./sentry.server');
const middleware = getSentryMiddleware();
const error = Object.assign(new Error('Forbidden'), { statusCode: 403 });
const req = {} as Request;
const res = {} as Response;
const next = vi.fn();
middleware.errorHandler(error, req, res, next);
expect(mockSentry.captureException).not.toHaveBeenCalled();
expect(next).toHaveBeenCalledWith(error);
});
it('errorHandler should NOT capture 422 Unprocessable Entity errors', async () => {
const { getSentryMiddleware } = await import('./sentry.server');
const middleware = getSentryMiddleware();
const error = Object.assign(new Error('Unprocessable Entity'), { statusCode: 422 });
const req = {} as Request;
const res = {} as Response;
const next = vi.fn();
middleware.errorHandler(error, req, res, next);
expect(mockSentry.captureException).not.toHaveBeenCalled();
expect(next).toHaveBeenCalledWith(error);
});
it('errorHandler should use status property if statusCode is not present', async () => {
const { getSentryMiddleware } = await import('./sentry.server');
const middleware = getSentryMiddleware();
const error = Object.assign(new Error('Service Error'), { status: 503 });
const req = {} as Request;
const res = {} as Response;
const next = vi.fn();
middleware.errorHandler(error, req, res, next);
expect(mockSentry.captureException).toHaveBeenCalledWith(error);
expect(next).toHaveBeenCalledWith(error);
});
it('errorHandler should default to 500 if no status code is present', async () => {
const { getSentryMiddleware } = await import('./sentry.server');
const middleware = getSentryMiddleware();
const error = new Error('Unknown error');
const req = {} as Request;
const res = {} as Response;
const next = vi.fn();
middleware.errorHandler(error, req, res, next);
// Default status 500 >= 500, so it should capture
expect(mockSentry.captureException).toHaveBeenCalledWith(error);
expect(next).toHaveBeenCalledWith(error);
});
it('errorHandler should always pass error to next middleware', async () => {
const { getSentryMiddleware } = await import('./sentry.server');
const middleware = getSentryMiddleware();
const error404 = Object.assign(new Error('Not Found'), { statusCode: 404 });
const error500 = Object.assign(new Error('Server Error'), { statusCode: 500 });
const req = {} as Request;
const res = {} as Response;
const next = vi.fn();
middleware.errorHandler(error404, req, res, next);
expect(next).toHaveBeenCalledWith(error404);
next.mockClear();
middleware.errorHandler(error500, req, res, next);
expect(next).toHaveBeenCalledWith(error500);
});
});
// ============================================================================
// SECTION 6: Tests for beforeSend callback behavior
// ============================================================================
describe('beforeSend callback behavior', () => {
it('should return event from beforeSend in production (no logging)', () => {
// Simulate the beforeSend logic when isProduction is true
const isProduction = true;
const mockEvent = { event_id: '123' };
const beforeSend = (event: { event_id: string }, hint: { originalException?: Error }) => {
if (!isProduction && hint.originalException) {
// Would log here in real implementation
}
return event;
};
const result = beforeSend(mockEvent, {});
expect(result).toBe(mockEvent);
});
it('should return event and allow logging in development with original exception', () => {
// Simulate the beforeSend logic when isProduction is false
const isProduction = false;
const mockEvent = { event_id: '456' };
const mockException = new Error('test error');
const beforeSend = (event: { event_id: string }, hint: { originalException?: Error }) => {
if (!isProduction && hint.originalException) {
// In real implementation, this would log
}
return event;
};
const result = beforeSend(mockEvent, { originalException: mockException });
expect(result).toBe(mockEvent);
});
it('should return event in development without original exception', () => {
const isProduction = false;
const mockEvent = { event_id: '789' };
const beforeSend = (event: { event_id: string }, hint: { originalException?: Error }) => {
if (!isProduction && hint.originalException) {
// Would not log since no originalException
}
return event;
};
const result = beforeSend(mockEvent, {});
expect(result).toBe(mockEvent);
});
});
// ============================================================================
// SECTION 7: Tests for Sentry environment fallback
// ============================================================================
describe('environment configuration fallback', () => {
beforeEach(() => {
vi.resetModules();
});
it('should use sentry.environment when provided', async () => {
vi.doMock('../config/env', () => ({
config: {
sentry: {
dsn: 'https://key@sentry.example.com/123',
environment: 'staging',
debug: false,
},
server: {
nodeEnv: 'production',
},
},
isSentryConfigured: true,
isProduction: true,
isTest: false,
}));
const { initSentry } = await import('./sentry.server');
initSentry();
expect(mockSentry.init).toHaveBeenCalledWith(
expect.objectContaining({
environment: 'staging',
}),
);
expect(mockLogger.info).toHaveBeenCalledWith(
{ environment: 'staging' },
'[Sentry] Error tracking initialized',
);
});
it('should fall back to server.nodeEnv when sentry.environment is not provided', async () => {
vi.doMock('../config/env', () => ({
config: {
sentry: {
dsn: 'https://key@sentry.example.com/123',
environment: undefined,
debug: false,
},
server: {
nodeEnv: 'production',
},
},
isSentryConfigured: true,
isProduction: true,
isTest: false,
}));
const { initSentry } = await import('./sentry.server');
initSentry();
expect(mockSentry.init).toHaveBeenCalledWith(
expect.objectContaining({
environment: 'production',
}),
);
expect(mockLogger.info).toHaveBeenCalledWith(
{ environment: 'production' },
'[Sentry] Error tracking initialized',
);
});
});
// ============================================================================
// SECTION 8: Tests for Sentry re-export
// ============================================================================
describe('Sentry re-export', () => {
beforeEach(() => {
vi.resetModules();
vi.doMock('../config/env', () => ({
config: {
sentry: {
dsn: '',
environment: 'test',
debug: false,
},
server: {
nodeEnv: 'test',
},
},
isSentryConfigured: false,
isProduction: false,
isTest: true,
}));
});
it('should re-export Sentry object for advanced usage', async () => {
const { Sentry } = await import('./sentry.server');
expect(Sentry).toBeDefined();
expect(Sentry.init).toBeDefined();
expect(Sentry.captureException).toBeDefined();
expect(Sentry.captureMessage).toBeDefined();
expect(Sentry.setUser).toBeDefined();
expect(Sentry.addBreadcrumb).toBeDefined();
});
});
// ============================================================================
// SECTION 9: Tests for status code extraction logic
// ============================================================================
describe('error handler status code extraction logic', () => {
it('should correctly identify 5xx errors for Sentry capture', () => {
const shouldCapture = (statusCode: number) => statusCode >= 500;
expect(shouldCapture(500)).toBe(true);
expect(shouldCapture(501)).toBe(true);
expect(shouldCapture(502)).toBe(true);
expect(shouldCapture(503)).toBe(true);
expect(shouldCapture(504)).toBe(true);
expect(shouldCapture(599)).toBe(true);
});
it('should not capture 4xx errors', () => {
const shouldCapture = (statusCode: number) => statusCode >= 500;
expect(shouldCapture(400)).toBe(false);
expect(shouldCapture(401)).toBe(false);
expect(shouldCapture(403)).toBe(false);
expect(shouldCapture(404)).toBe(false);
expect(shouldCapture(422)).toBe(false);
expect(shouldCapture(499)).toBe(false);
});
it('should not capture 3xx or lower status codes', () => {
const shouldCapture = (statusCode: number) => statusCode >= 500;
expect(shouldCapture(200)).toBe(false);
expect(shouldCapture(201)).toBe(false);
expect(shouldCapture(301)).toBe(false);
expect(shouldCapture(302)).toBe(false);
});
it('should extract statusCode from error object', () => {
const getStatusCode = (err: Error & { statusCode?: number; status?: number }) =>
err.statusCode || err.status || 500;
const errorWithStatusCode = Object.assign(new Error('test'), { statusCode: 503 });
expect(getStatusCode(errorWithStatusCode)).toBe(503);
});
it('should extract status from error object when statusCode is missing', () => {
const getStatusCode = (err: Error & { statusCode?: number; status?: number }) =>
err.statusCode || err.status || 500;
const errorWithStatus = Object.assign(new Error('test'), { status: 502 });
expect(getStatusCode(errorWithStatus)).toBe(502);
});
it('should default to 500 for plain Error objects', () => {
const getStatusCode = (err: Error & { statusCode?: number; status?: number }) =>
err.statusCode || err.status || 500;
const plainError = new Error('test');
expect(getStatusCode(plainError)).toBe(500);
});
it('should prefer statusCode over status when both are present', () => {
const getStatusCode = (err: Error & { statusCode?: number; status?: number }) =>
err.statusCode || err.status || 500;
const errorWithBoth = Object.assign(new Error('test'), { statusCode: 500, status: 404 });
expect(getStatusCode(errorWithBoth)).toBe(500);
});
});
// ============================================================================
// SECTION 10: Tests for guard condition logic
// ============================================================================
describe('isSentryConfigured and isTest guard logic', () => {
it('should block execution when Sentry is not configured', () => {
const isSentryConfigured = false;
const isTest = false;
const shouldExecute = isSentryConfigured && !isTest;
expect(shouldExecute).toBe(false);
});
it('should block execution in test environment even when configured', () => {
const isSentryConfigured = true;
const isTest = true;
const shouldExecute = isSentryConfigured && !isTest;
expect(shouldExecute).toBe(false);
});
it('should allow execution when configured and not in test', () => {
const isSentryConfigured = true;
const isTest = false;
const shouldExecute = isSentryConfigured && !isTest;
expect(shouldExecute).toBe(true);
});
it('should block execution when both conditions are false', () => {
const isSentryConfigured = false;
const isTest = true;
const shouldExecute = isSentryConfigured && !isTest;
expect(shouldExecute).toBe(false);
});
});
// ============================================================================
// SECTION 11: Tests for captureMessage severity levels
// ============================================================================
describe('captureMessage severity levels', () => {
it('should support fatal level', () => {
const validLevels = ['fatal', 'error', 'warning', 'log', 'info', 'debug'];
expect(validLevels).toContain('fatal');
});
it('should support error level', () => {
const validLevels = ['fatal', 'error', 'warning', 'log', 'info', 'debug'];
expect(validLevels).toContain('error');
});
it('should support warning level', () => {
const validLevels = ['fatal', 'error', 'warning', 'log', 'info', 'debug'];
expect(validLevels).toContain('warning');
});
it('should support log level', () => {
const validLevels = ['fatal', 'error', 'warning', 'log', 'info', 'debug'];
expect(validLevels).toContain('log');
});
it('should support info level', () => {
const validLevels = ['fatal', 'error', 'warning', 'log', 'info', 'debug'];
expect(validLevels).toContain('info');
});
it('should support debug level', () => {
const validLevels = ['fatal', 'error', 'warning', 'log', 'info', 'debug'];
expect(validLevels).toContain('debug');
});
it('should have info as default level', () => {
const defaultLevel = 'info';
expect(defaultLevel).toBe('info');
});
});
// ============================================================================
// SECTION 12: Tests for setUser parameter variations
// ============================================================================
describe('setUser parameter variations', () => {
it('should accept user object with id only', () => {
const user = { id: '123' };
expect(user.id).toBe('123');
expect(user).not.toHaveProperty('email');
expect(user).not.toHaveProperty('username');
});
it('should accept user object with id and email', () => {
const user = { id: '123', email: 'test@example.com' };
expect(user.id).toBe('123');
expect(user.email).toBe('test@example.com');
expect(user).not.toHaveProperty('username');
});
it('should accept user object with all fields', () => {
const user = { id: '123', email: 'test@example.com', username: 'testuser' };
expect(user.id).toBe('123');
expect(user.email).toBe('test@example.com');
expect(user.username).toBe('testuser');
});
it('should accept null to clear user', () => {
const user = null;
expect(user).toBeNull();
});
});
// ============================================================================
// SECTION 13: Tests for addBreadcrumb parameter variations
// ============================================================================
describe('addBreadcrumb parameter variations', () => {
it('should accept breadcrumb with message only', () => {
const breadcrumb = { message: 'User clicked button' };
expect(breadcrumb.message).toBe('User clicked button');
});
it('should accept breadcrumb with message and category', () => {
const breadcrumb = { message: 'Navigation', category: 'navigation' };
expect(breadcrumb.message).toBe('Navigation');
expect(breadcrumb.category).toBe('navigation');
});
it('should accept breadcrumb with level', () => {
const breadcrumb = { message: 'Error occurred', level: 'error' as const };
expect(breadcrumb.level).toBe('error');
});
it('should accept breadcrumb with data object', () => {
const breadcrumb = {
message: 'API call',
category: 'http',
data: { url: '/api/v1/test', method: 'GET', status_code: 200 },
};
expect(breadcrumb.data).toEqual({ url: '/api/v1/test', method: 'GET', status_code: 200 });
});
it('should accept breadcrumb with timestamp', () => {
const now = Date.now();
const breadcrumb = { message: 'Event', timestamp: now };
expect(breadcrumb.timestamp).toBe(now);
});
});
// ============================================================================
// SECTION 14: Tests for debug mode configuration
// ============================================================================
describe('debug mode configuration', () => {
beforeEach(() => {
vi.resetModules();
});
it('should pass debug: true to Sentry.init when configured', async () => {
vi.doMock('../config/env', () => ({
config: {
sentry: {
dsn: 'https://key@sentry.example.com/123',
environment: 'development',
debug: true,
},
server: {
nodeEnv: 'development',
},
},
isSentryConfigured: true,
isProduction: false,
isTest: false,
}));
const { initSentry } = await import('./sentry.server');
initSentry();
expect(mockSentry.init).toHaveBeenCalledWith(
expect.objectContaining({
debug: true,
}),
);
});
it('should pass debug: false to Sentry.init when configured', async () => {
vi.doMock('../config/env', () => ({
config: {
sentry: {
dsn: 'https://key@sentry.example.com/123',
environment: 'production',
debug: false,
},
server: {
nodeEnv: 'production',
},
},
isSentryConfigured: true,
isProduction: true,
isTest: false,
}));
const { initSentry } = await import('./sentry.server');
initSentry();
expect(mockSentry.init).toHaveBeenCalledWith(
expect.objectContaining({
debug: false,
}),
);
});
});
// ============================================================================
// SECTION 15: Tests for tracesSampleRate configuration
// ============================================================================
describe('tracesSampleRate configuration', () => {
beforeEach(() => {
vi.resetModules();
});
it('should set tracesSampleRate to 0 (performance monitoring disabled)', async () => {
vi.doMock('../config/env', () => ({
config: {
sentry: {
dsn: 'https://key@sentry.example.com/123',
environment: 'production',
debug: false,
},
server: {
nodeEnv: 'production',
},
},
isSentryConfigured: true,
isProduction: true,
isTest: false,
}));
const { initSentry } = await import('./sentry.server');
initSentry();
expect(mockSentry.init).toHaveBeenCalledWith(
expect.objectContaining({
tracesSampleRate: 0,
}),
);
});
});
});