UNPKG

@unito/integration-sdk

Version:

Integration SDK

311 lines (310 loc) 15.7 kB
import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { default as Logger, NULL_LOGGER } from '../../src/resources/logger.js'; describe('Logger', () => { it('metadata', () => { let metadata = { correlation_id: 'test' }; const logger = new Logger(metadata); // Changing initial object should not affect the logger metadata.correlation_id = 'changed'; assert.deepEqual(logger.getMetadata(), { correlation_id: 'test' }); // Changing returned metadata object should not affect the logger metadata = logger.getMetadata(); metadata.correlation_id = 'changed'; assert.deepEqual(logger.getMetadata(), { correlation_id: 'test' }); // Can overwrite existing metadata logger.setMetadata('correlation_id', 'changed'); assert.deepEqual(logger.getMetadata(), { correlation_id: 'changed' }); // Can merge metadata logger.decorate({ other: 'metadata' }); assert.deepEqual(logger.getMetadata(), { correlation_id: 'changed', other: 'metadata' }); // Can overwrite existing metadata logger.decorate({ other: 'overwrite' }); assert.deepEqual(logger.getMetadata(), { correlation_id: 'changed', other: 'overwrite' }); // Can clear metadata logger.clearMetadata(); assert.deepEqual(logger.getMetadata(), {}); }); it('respect logging levels', testContext => { const metadata = { correlation_id: '123456789' }; const logger = new Logger(metadata); const logSpy = testContext.mock.method(global.console, 'log', () => { }); assert.strictEqual(logSpy.mock.calls.length, 0); logger.log('test'); assert.strictEqual(logSpy.mock.calls.length, 1); let actual = JSON.parse(logSpy.mock.calls[0]?.arguments[0]); assert.ok(actual['date']); assert.equal(actual['correlation_id'], '123456789'); assert.equal(actual['message'], 'test'); assert.equal(actual['status'], 'log'); const errorSpy = testContext.mock.method(global.console, 'error', () => { }); assert.strictEqual(errorSpy.mock.calls.length, 0); logger.error('test'); assert.strictEqual(errorSpy.mock.calls.length, 1); actual = JSON.parse(errorSpy.mock.calls[0]?.arguments[0]); assert.ok(actual['date']); assert.equal(actual['correlation_id'], '123456789'); assert.equal(actual['message'], 'test'); assert.equal(actual['status'], 'error'); const warnSpy = testContext.mock.method(global.console, 'warn', () => { }); assert.strictEqual(warnSpy.mock.calls.length, 0); logger.warn('test'); assert.strictEqual(warnSpy.mock.calls.length, 1); actual = JSON.parse(warnSpy.mock.calls[0]?.arguments[0]); assert.ok(actual['date']); assert.equal(actual['correlation_id'], '123456789'); assert.equal(actual['message'], 'test'); assert.equal(actual['status'], 'warn'); const infoSpy = testContext.mock.method(global.console, 'info', () => { }); assert.strictEqual(infoSpy.mock.calls.length, 0); logger.info('test'); assert.strictEqual(infoSpy.mock.calls.length, 1); actual = JSON.parse(infoSpy.mock.calls[0]?.arguments[0]); assert.ok(actual['date']); assert.equal(actual['correlation_id'], '123456789'); assert.equal(actual['message'], 'test'); assert.equal(actual['status'], 'info'); const debugSpy = testContext.mock.method(global.console, 'debug', () => { }); assert.strictEqual(debugSpy.mock.calls.length, 0); logger.debug('test'); assert.strictEqual(debugSpy.mock.calls.length, 1); actual = JSON.parse(debugSpy.mock.calls[0]?.arguments[0]); assert.ok(actual['date']); assert.equal(actual['correlation_id'], '123456789'); assert.equal(actual['message'], 'test'); assert.equal(actual['status'], 'debug'); }); it('merges message payload with metadata', testContext => { const logSpy = testContext.mock.method(global.console, 'log', () => { }); assert.strictEqual(logSpy.mock.calls.length, 0); const metadata = { correlation_id: '123456789', http: { method: 'GET' } }; const logger = new Logger(metadata); logger.log('test', { error: { code: '200', message: 'Page Not Found' } }); assert.strictEqual(logSpy.mock.calls.length, 1); const actual = JSON.parse(logSpy.mock.calls[0]?.arguments[0]); assert.ok(actual['date']); assert.equal(actual['correlation_id'], '123456789'); assert.equal(actual['message'], 'test'); assert.equal(actual['status'], 'log'); assert.deepEqual(actual['http'], { method: 'GET' }); assert.deepEqual(actual['error'], { code: '200', message: 'Page Not Found' }); }); it('overwrites conflicting metadata keys (1st level) with message payload', testContext => { const logSpy = testContext.mock.method(global.console, 'log', () => { }); assert.strictEqual(logSpy.mock.calls.length, 0); const metadata = { correlation_id: '123456789', http: { method: 'GET' } }; const logger = new Logger(metadata); logger.log('test', { http: { status_code: 200 } }); assert.strictEqual(logSpy.mock.calls.length, 1); const actual = JSON.parse(logSpy.mock.calls[0]?.arguments[0]); assert.ok(actual['date']); assert.equal(actual['correlation_id'], '123456789'); assert.equal(actual['message'], 'test'); assert.equal(actual['status'], 'log'); assert.deepEqual(actual['http'], { status_code: 200 }); }); it('snakify keys of Message and Metadata', testContext => { const logSpy = testContext.mock.method(global.console, 'log', () => { }); assert.strictEqual(logSpy.mock.calls.length, 0); const metadata = { correlationId: '123456789', http: { method: 'GET', statusCode: 200 }, links: [ { id: 1, organizationId: 'a' }, { id: 2, organizationId: 'b' }, ], }; const logger = new Logger(metadata); logger.log('test', { errorContext: { errorCode: 200, errorMessage: 'Page Not Found' } }); assert.strictEqual(logSpy.mock.calls.length, 1); const actual = JSON.parse(logSpy.mock.calls[0]?.arguments[0]); assert.ok(actual['date']); assert.equal(actual['correlation_id'], '123456789'); assert.equal(actual['message'], 'test'); assert.equal(actual['status'], 'log'); assert.deepEqual(actual['http'], { method: 'GET', status_code: 200 }); assert.deepEqual(actual['error_context'], { error_code: 200, error_message: 'Page Not Found' }); assert.deepEqual(actual['links'], [ { id: 1, organization_id: 'a' }, { id: 2, organization_id: 'b' }, ]); }); it('prunes sensitive Metadata', testContext => { const logSpy = testContext.mock.method(global.console, 'log', () => { }); assert.strictEqual(logSpy.mock.calls.length, 0); const metadata = { correlationId: '123456789', http: { method: 'GET', statusCode: 200, jwt: 'deepSecret' }, user: { contact: { email: 'deep_deep_deep@email.address', firstName: 'should be snakify then hidden' } }, access_token: 'secret', items: [ { id: 1, organizationId: 'a' }, { id: 2, organizationId: 'b' }, ], }; const logger = new Logger(metadata); logger.log('test', { errorContext: { errorCode: 200, errorMessage: 'Page Not Found' } }); assert.strictEqual(logSpy.mock.calls.length, 1); const actual = JSON.parse(logSpy.mock.calls[0]?.arguments[0]); assert.ok(actual['date']); assert.equal(actual['correlation_id'], '123456789'); assert.equal(actual['message'], 'test'); assert.equal(actual['status'], 'log'); assert.equal(actual['has_sensitive_attribute'], true); assert.equal(actual['access_token'], '[REDACTED]'); assert.deepEqual(actual['http'], { method: 'GET', status_code: 200, jwt: '[REDACTED]' }); assert.deepEqual(actual['user']['contact'], { email: '[REDACTED]', first_name: '[REDACTED]' }); assert.deepEqual(actual['items'], [ { id: 1, organization_id: 'a' }, { id: 2, organization_id: 'b' }, ]); }); it(`NULL_LOGGER should not log`, testContext => { const logger = NULL_LOGGER; const logSpy = testContext.mock.method(global.console, 'log', () => { }); const errorSpy = testContext.mock.method(global.console, 'error', () => { }); const warnSpy = testContext.mock.method(global.console, 'warn', () => { }); const infoSpy = testContext.mock.method(global.console, 'info', () => { }); const debugSpy = testContext.mock.method(global.console, 'debug', () => { }); logger.log('test'); logger.info('test'); logger.warn('test'); logger.error('test'); logger.debug('test'); assert.strictEqual(logSpy.mock.calls.length, 0); assert.strictEqual(errorSpy.mock.calls.length, 0); assert.strictEqual(warnSpy.mock.calls.length, 0); assert.strictEqual(infoSpy.mock.calls.length, 0); assert.strictEqual(debugSpy.mock.calls.length, 0); }); it('colorizes logs by status codes over log levels', () => { const originalEnv = process.env.NODE_ENV; const originalIsTTY = process.stdout.isTTY; try { process.env.NODE_ENV = 'development'; Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true, }); // 4xx status with warn level, should be red const metadataWith400 = { http: { status_code: 400 }, }; const redResult = Logger['colorize']('Some error', metadataWith400, 'warn'); // warn level without status code, should be yellow const metadataNoStatus = {}; const yellowResult = Logger['colorize']('Some warning', metadataNoStatus, 'warn'); // Results should be different colors assert.notEqual(redResult, yellowResult); assert.ok(redResult.includes('\x1b[31m')); // Red color code assert.ok(yellowResult.includes('\x1b[33m')); // Yellow color code } finally { // Restore original values process.env.NODE_ENV = originalEnv; Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true, }); } }); it('colorizes logs with different status code ranges', () => { const originalEnv = process.env.NODE_ENV; const originalIsTTY = process.stdout.isTTY; try { process.env.NODE_ENV = 'development'; Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true, }); // 2xx status codes should be green const successMetadata = { http: { status_code: 200 }, }; const greenResult = Logger['colorize']('Success', successMetadata, 'info'); assert.ok(greenResult.includes('\x1b[32m')); // Green color code // 3xx status codes should be yellow const redirectMetadata = { http: { status_code: 301 }, }; const yellowResult = Logger['colorize']('Redirect', redirectMetadata, 'info'); assert.ok(yellowResult.includes('\x1b[33m')); // Yellow color code // 4xx status codes should be red const clientErrorMetadata = { http: { status_code: 404 }, }; const redResult = Logger['colorize']('Not Found', clientErrorMetadata, 'warn'); assert.ok(redResult.includes('\x1b[31m')); // Red color code // 5xx status codes should be red const serverErrorMetadata = { http: { status_code: 500 }, }; const redResult2 = Logger['colorize']('Server Error', serverErrorMetadata, 'error'); assert.ok(redResult2.includes('\x1b[31m')); // Red color code } finally { process.env.NODE_ENV = originalEnv; Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true, }); } }); it('only logs date for metadata of successful requests in development', testContext => { const originalEnv = process.env.NODE_ENV; try { process.env.NODE_ENV = 'development'; const infoSpy = testContext.mock.method(global.console, 'info', () => { }); const metadata = { correlation_id: '123456789', http: { method: 'GET', status_code: 200 }, user_id: 'user123', request_id: 'req456', }; const logger = new Logger(metadata); logger.info('Test message without error'); assert.strictEqual(infoSpy.mock.calls.length, 1); const loggedOutput = infoSpy.mock.calls[0]?.arguments[0]; assert.ok(loggedOutput.includes('Test message without error')); // Should only contain date in metadata JSON const metadataMatch = loggedOutput.match(/({.*})/); assert.ok(metadataMatch); const parsedMetadata = JSON.parse(metadataMatch[1]); assert.ok(parsedMetadata.date); assert.strictEqual(Object.keys(parsedMetadata).length, 1); // What matters: Only the date } finally { process.env.NODE_ENV = originalEnv; } }); it('logs date & error stack for metadata of failed requests in development', testContext => { const originalEnv = process.env.NODE_ENV; try { process.env.NODE_ENV = 'development'; const metadata = { correlation_id: '123456789', http: { method: 'GET', status_code: 200 }, user_id: 'user123', request_id: 'req456', }; const logger = new Logger(metadata); // Test with error metadata const errorSpy = testContext.mock.method(global.console, 'error', () => { }); logger.error('Test message with error', { error: { code: 500, message: 'Internal Server Error' } }); assert.strictEqual(errorSpy.mock.calls.length, 1); const errorLoggedOutput = errorSpy.mock.calls[0]?.arguments[0]; assert.ok(errorLoggedOutput.includes('Test message with error')); // Should contain both date and error in metadata JSON const errorMetadataMatch = errorLoggedOutput.match(/({.*})/s); assert.ok(errorMetadataMatch); const parsedErrorMetadata = JSON.parse(errorMetadataMatch[1]); assert.ok(parsedErrorMetadata.date); assert.ok(parsedErrorMetadata.error); assert.deepEqual(parsedErrorMetadata.error, { code: 500, message: 'Internal Server Error' }); assert.strictEqual(Object.keys(parsedErrorMetadata).length, 2); // What matters: Date and error only } finally { process.env.NODE_ENV = originalEnv; } }); });