@unito/integration-sdk
Version:
Integration SDK
311 lines (310 loc) • 15.7 kB
JavaScript
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;
}
});
});