import { jest } from '@jest/globals' import { expect as c_expect } from 'chai' // get console ref and mock before logger import const consoleProxy = console const mock = jest.spyOn(consoleProxy, 'log') // import logger after first ref import Logger from '../../src/Logger/Logger.js' // get console reference after logger import const transactionID = '123-456' function setContainerFlag(isContainer: boolean) { if (isContainer) { process.env['HYBRIDLESS_RUNTIME'] = 'true' } else { process.env['HYBRIDLESS_RUNTIME'] = undefined } } function fixLogTypePrefix(logType: string) { if (logType == 'exception') return 'error' if (logType == 'warning') return 'warn' if (logType == 'log') return 'info' return logType } function randomDeepObject(count, endKey) { if (count <= 1) return { [endKey]: 'value' } const object = {} for (let i = 0; i < count; i++) { const key = (+new Date() * Math.random()).toString(36).substring(0, 6) object[key] = randomDeepObject(Math.floor(Math.random() * (count / 2)), endKey) } return object } // eslint-disable-next-line @typescript-eslint/no-unused-vars function testLogs(isContainer: boolean, provider?: Logger) { const type = isContainer ? 'container' : 'serverless' const loggerType = !provider ? 'Console' : 'Logger' const localProvider = provider || console test(`${type} - ${loggerType} Log - Suppress sensitive info`, async () => { setContainerFlag(isContainer) localProvider.log('my password is 123') expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining((isContainer ? `${transactionID} ` : '') + '[INFO] [Logger.test.ts:') ) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining('] my password is 123') ) }) test(`${type} - ${loggerType} Log - Suppress sensitive info (object)`, async () => { setContainerFlag(isContainer) const object = { password: '123' } localProvider.log('TEST', object) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining((isContainer ? `${transactionID} ` : '') + '[INFO] [Logger.test.ts:') ) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining('] TEST {\n "password": "[MASKED]"\n}') ) // test if object is not mutate c_expect(object.password).to.be.equals('123') }) test(`${type} - ${loggerType} Log - Suppress sensitive info (long-object)`, async () => { setContainerFlag(isContainer) const object = randomDeepObject(99, 'password') localProvider.log('TEST', object) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining((isContainer ? `${transactionID} ` : '') + '[INFO] [Logger.test.ts:') ) expect(consoleProxy.log).toHaveBeenNthCalledWith(1, expect.stringContaining('] TEST')) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining('"password": "[MASKED]"') ) }) test(`${type} - ${loggerType} Log - Suppress sensitive info (object with sensitive string)`, async () => { setContainerFlag(isContainer) localProvider.log({ object: JSON.stringify({ password: '123' }) }) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining((isContainer ? `${transactionID} ` : '') + '[INFO] [Logger.test.ts:') ) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining('] {\n "object": {\n "password": "[MASKED]"\n }\n}') ) }) test(`${type} - ${loggerType} Log - Suppress sensitive info (object with sensitive number)`, async () => { setContainerFlag(isContainer) localProvider.log({ object: JSON.stringify({ password: 123 }) }) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining((isContainer ? `${transactionID} ` : '') + '[INFO] [Logger.test.ts:') ) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining('] {\n "object": {\n "password": "[MASKED]"\n }\n}') ) }) test(`${type} - ${loggerType} Log - Suppress sensitive info (null key)`, async () => { setContainerFlag(isContainer) localProvider.log({ object: JSON.stringify({ password: null }) }) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining((isContainer ? `${transactionID} ` : '') + '[INFO] [Logger.test.ts:') ) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining('] {\n "object": {\n "password": null\n }\n}') ) }) test(`${type} - ${loggerType} Log - Suppress sensitive info (null)`, async () => { setContainerFlag(isContainer) localProvider.log(null) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining((isContainer ? `${transactionID} ` : '') + '[INFO] [Logger.test.ts:') ) expect(consoleProxy.log).toHaveBeenNthCalledWith(1, expect.stringContaining('] ')) }) test(`${type} - ${loggerType} Log - Suppress sensitive info (array)`, async () => { setContainerFlag(isContainer) localProvider.log('TEST2', [{ password: '1234' }]) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining((isContainer ? `${transactionID} ` : '') + '[INFO] [Logger.test.ts:') ) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining('] TEST2 [\n {\n "password": "[MASKED]"\n }\n]') ) }) test(`${type} - ${loggerType} Log - Suppress sensitive info (token)`, async () => { setContainerFlag(isContainer) localProvider.log({ token: 'abc123xyz' }) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining((isContainer ? `${transactionID} ` : '') + '[INFO] [Logger.test.ts:') ) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining('"token": "[HASHED:') ) }) test(`${type} - ${loggerType} Log - Suppress sensitive info (key)`, async () => { setContainerFlag(isContainer) localProvider.log({ key: 'secret-key-123' }) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining((isContainer ? `${transactionID} ` : '') + '[INFO] [Logger.test.ts:') ) expect(consoleProxy.log).toHaveBeenNthCalledWith(1, expect.stringContaining('"key": "[HASHED:')) }) test(`${type} - ${loggerType} Log - Suppress sensitive info (authorization)`, async () => { setContainerFlag(isContainer) localProvider.log({ authorization: 'Bearer token123' }) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining((isContainer ? `${transactionID} ` : '') + '[INFO] [Logger.test.ts:') ) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining('"authorization": "Bearer [HASHED:') ) }) test(`${type} - ${loggerType} Log - Suppress sensitive info (accounts)`, async () => { setContainerFlag(isContainer) localProvider.log({ accounts: 'account-data' }) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining((isContainer ? `${transactionID} ` : '') + '[INFO] [Logger.test.ts:') ) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining('"accounts": "**SUPPRESSED_SENSITIVE_DATA** (12 len)"') ) }) test(`${type} - ${loggerType} Log - Circular reference (Error)`, async () => { setContainerFlag(isContainer) class SelfRefError extends Error { public self: SelfRefError constructor() { super() this.self = this } } localProvider.log(new SelfRefError()) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining((isContainer ? `${transactionID} ` : '') + '[INFO] [Logger.test.ts:') ) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining('tests/Logger/Logger.test.ts:') ) }) for (const logType of ['log', 'debug', 'info', 'warn', 'warning', 'error', 'exception']) { test(`${type} - ${loggerType} ${logType}`, async () => { setContainerFlag(isContainer) localProvider[logType](logType.toUpperCase()) // alias const logTypePrefix = fixLogTypePrefix(logType) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining( (isContainer ? `${transactionID} ` : '') + `[${logTypePrefix.toUpperCase()}] [Logger.test.ts:` ) ) expect(consoleProxy.log).toHaveBeenNthCalledWith( 1, expect.stringContaining(`${logType.toUpperCase()}`) ) }) } } describe('Logger', () => { beforeEach(() => { mock.mockClear() }) const provider = new Logger( { logLevel: 'DEBUG', sensitiveFilteringKeywords: true, }, transactionID ) provider.notGlobalLogger() test('Logger - Constructor', () => { expect(provider).toBeInstanceOf(Logger) }) // TECH-DEBT: figure out why spy is not working // // Test serverless logs // testLogs(false, provider) // testLogs(false) // // Test container logs // testLogs(true, provider) // testLogs(true) }) describe('Logger - Log level filtering', () => { function capturedLogger(logLevel: string) { const logger = new Logger({ logLevel, silent: false }, transactionID) const captured: Array<{ level: string; msg: string }> = [] ;(logger as any).pushLog = (level: string, msg: string) => captured.push({ level, msg }) return { logger, captured } } test('logLevel ERROR suppresses info and warn but not error', () => { const { logger, captured } = capturedLogger('ERROR') logger.info('should be suppressed') logger.warn('should be suppressed') expect(captured).toHaveLength(0) logger.error('should appear') expect(captured).toHaveLength(1) expect(captured[0].msg).toContain('[ERROR]') }) test('logLevel WARN suppresses debug and info but not warn or error', () => { const { logger, captured } = capturedLogger('WARN') logger.debug('should be suppressed') logger.info('should be suppressed') expect(captured).toHaveLength(0) logger.warn('should appear') logger.error('should appear') expect(captured).toHaveLength(2) }) test('logLevel INFO suppresses debug but not info, warn, or error', () => { const { logger, captured } = capturedLogger('INFO') logger.debug('should be suppressed') expect(captured).toHaveLength(0) logger.info('should appear') logger.warn('should appear') logger.error('should appear') expect(captured).toHaveLength(3) }) test('logLevel DEBUG passes all levels', () => { const { logger, captured } = capturedLogger('DEBUG') logger.debug('d') logger.info('i') logger.warn('w') logger.error('e') expect(captured).toHaveLength(4) }) }) export {}