diff --git a/.taprc.yaml b/.taprc.yaml index 8e8a0bfd..07652a14 100644 --- a/.taprc.yaml +++ b/.taprc.yaml @@ -1,5 +1,8 @@ coverage: true +coverage-map: 'coverage-map.js' + reporter: terse files: + - 'lib/**/*.test.js' - 'test/**/*.test.js' diff --git a/coverage-map.js b/coverage-map.js new file mode 100644 index 00000000..ca58935a --- /dev/null +++ b/coverage-map.js @@ -0,0 +1,9 @@ +'use strict' + +module.exports = testFile => { + // Ignore coverage on files that do not have a direct corollary. + if (testFile.startsWith('test/')) return false + + // Indicate the matching name, sans '.test.js', should be checked for coverage. + return testFile.replace(/\.test\.js$/, '.js') +} diff --git a/test/lib/colors.test.js b/lib/colors.test.js similarity index 96% rename from test/lib/colors.test.js rename to lib/colors.test.js index 2a4d3b12..89de15be 100644 --- a/test/lib/colors.test.js +++ b/lib/colors.test.js @@ -1,8 +1,8 @@ 'use strict' const { test } = require('tap') -const getColorizerPrivate = require('../../lib/colors') -const { colorizerFactory: getColorizerPublic } = require('../../index') +const getColorizerPrivate = require('./colors') +const { colorizerFactory: getColorizerPublic } = require('../index') const testDefaultColorizer = getColorizer => async t => { const colorizer = getColorizer() diff --git a/lib/utils/build-safe-sonic-boom.test.js b/lib/utils/build-safe-sonic-boom.test.js new file mode 100644 index 00000000..6510bb8b --- /dev/null +++ b/lib/utils/build-safe-sonic-boom.test.js @@ -0,0 +1,47 @@ +'use strict' + +const tap = require('tap') +const rimraf = require('rimraf') +const fs = require('fs') +const { join } = require('path') + +const buildSafeSonicBoom = require('./build-safe-sonic-boom') + +function noop () {} + +const file = () => { + const dest = join(__dirname, `${process.pid}-${process.hrtime().toString()}`) + const fd = fs.openSync(dest, 'w') + return { dest, fd } +} + +tap.test('should not write when error emitted and code is "EPIPE"', async t => { + t.plan(1) + + const { fd, dest } = file() + const stream = buildSafeSonicBoom({ sync: true, fd, mkdir: true }) + t.teardown(() => rimraf(dest, noop)) + + stream.emit('error', { code: 'EPIPE' }) + stream.write('will not work') + + const dataFile = fs.readFileSync(dest) + t.equal(dataFile.length, 0) +}) + +tap.test('should stream.write works when error code is not "EPIPE"', async t => { + t.plan(3) + const { fd, dest } = file() + const stream = buildSafeSonicBoom({ sync: true, fd, mkdir: true }) + + t.teardown(() => rimraf(dest, noop)) + + stream.on('error', () => t.pass('error emitted')) + + stream.emit('error', 'fake error description') + + t.ok(stream.write('will work')) + + const dataFile = fs.readFileSync(dest) + t.equal(dataFile.toString(), 'will work') +}) diff --git a/lib/utils/create-date.test.js b/lib/utils/create-date.test.js new file mode 100644 index 00000000..8ab36da2 --- /dev/null +++ b/lib/utils/create-date.test.js @@ -0,0 +1,20 @@ +'use strict' + +const tap = require('tap') +const createDate = require('./create-date') + +const wanted = 1624450038567 + +tap.test('accepts arguments the Date constructor would accept', async t => { + t.plan(2) + t.same(createDate(1624450038567).getTime(), wanted) + t.same(createDate('2021-06-23T12:07:18.567Z').getTime(), wanted) +}) + +tap.test('accepts epoch as a string', async t => { + // If Date() accepts this argument, the createDate function is not needed + // and can be replaced with Date() + t.plan(2) + t.notSame(new Date('16244500385-67').getTime(), wanted) + t.same(createDate('1624450038567').getTime(), wanted) +}) diff --git a/lib/utils/delete-log-property.test.js b/lib/utils/delete-log-property.test.js new file mode 100644 index 00000000..580eb918 --- /dev/null +++ b/lib/utils/delete-log-property.test.js @@ -0,0 +1,31 @@ +'use strict' + +const tap = require('tap') +const { createCopier } = require('fast-copy') +const fastCopy = createCopier({}) +const deleteLogProperty = require('./delete-log-property') + +const logData = { + level: 30, + data1: { + data2: { 'data-3': 'bar' } + } +} + +tap.test('deleteLogProperty deletes property of depth 1', async t => { + const log = fastCopy(logData) + deleteLogProperty(log, 'data1') + t.same(log, { level: 30 }) +}) + +tap.test('deleteLogProperty deletes property of depth 2', async t => { + const log = fastCopy(logData) + deleteLogProperty(log, 'data1.data2') + t.same(log, { level: 30, data1: { } }) +}) + +tap.test('deleteLogProperty deletes property of depth 3', async t => { + const log = fastCopy(logData) + deleteLogProperty(log, 'data1.data2.data-3') + t.same(log, { level: 30, data1: { data2: { } } }) +}) diff --git a/lib/utils/filter-log.test.js b/lib/utils/filter-log.test.js new file mode 100644 index 00000000..3ba54298 --- /dev/null +++ b/lib/utils/filter-log.test.js @@ -0,0 +1,117 @@ +'use strict' + +const tap = require('tap') +const filterLog = require('./filter-log') + +const logData = { + level: 30, + time: 1522431328992, + data1: { + data2: { 'data-3': 'bar' }, + error: new Error('test') + } +} +const logData2 = Object.assign({ + 'logging.domain.corp/operation': { + id: 'foo', + producer: 'bar' + } +}, logData) + +tap.test('#filterLog with an ignoreKeys option', t => { + t.test('filterLog removes single entry', async t => { + const result = filterLog({ log: logData, ignoreKeys: ['data1.data2.data-3'] }) + t.same(result, { level: 30, time: 1522431328992, data1: { data2: { }, error: new Error('test') } }) + }) + + t.test('filterLog removes multiple entries', async t => { + const result = filterLog({ log: logData, ignoreKeys: ['time', 'data1'] }) + t.same(result, { level: 30 }) + }) + + t.test('filterLog keeps error instance', async t => { + const result = filterLog({ log: logData, ignoreKeys: [] }) + t.equal(logData.data1.error, result.data1.error) + }) + + t.test('filterLog removes entry with escape sequence', async t => { + const result = filterLog({ log: logData2, ignoreKeys: ['data1', 'logging\\.domain\\.corp/operation'] }) + t.same(result, { level: 30, time: 1522431328992 }) + }) + + t.test('filterLog removes entry with escape sequence nested', async t => { + const result = filterLog({ log: logData2, ignoreKeys: ['data1', 'logging\\.domain\\.corp/operation.producer'] }) + t.same(result, { level: 30, time: 1522431328992, 'logging.domain.corp/operation': { id: 'foo' } }) + }) + + t.end() +}) + +const ignoreKeysArray = [ + undefined, + ['level'], + ['level', 'data1.data2.data-3'] +] +ignoreKeysArray.forEach(ignoreKeys => { + tap.test(`#filterLog with an includeKeys option when the ignoreKeys being ${ignoreKeys}`, t => { + t.test('filterLog include nothing', async t => { + const result = filterLog({ log: logData, ignoreKeys, includeKeys: [] }) + t.same(result, {}) + }) + + t.test('filterLog include single entry', async t => { + const result = filterLog({ log: logData, ignoreKeys, includeKeys: ['time'] }) + t.same(result, { time: 1522431328992 }) + }) + + t.test('filterLog include multiple entries', async t => { + const result = filterLog({ log: logData, ignoreKeys, includeKeys: ['time', 'data1'] }) + t.same(result, { + time: 1522431328992, + data1: { + data2: { 'data-3': 'bar' }, + error: new Error('test') + } + }) + }) + + t.end() + }) +}) + +tap.test('#filterLog with circular references', t => { + const logData = { + level: 30, + time: 1522431328992, + data1: 'test' + } + logData.circular = logData + + t.test('filterLog removes single entry', async t => { + const result = filterLog({ log: logData, ignoreKeys: ['data1'] }) + + t.same(result.circular.level, result.level) + t.same(result.circular.time, result.time) + + delete result.circular + t.same(result, { level: 30, time: 1522431328992 }) + }) + + t.test('filterLog includes single entry', async t => { + const result = filterLog({ log: logData, includeKeys: ['data1'] }) + + t.same(result, { data1: 'test' }) + }) + + t.test('filterLog includes circular keys', async t => { + const result = filterLog({ log: logData, includeKeys: ['level', 'circular'] }) + + t.same(result.circular.level, logData.level) + t.same(result.circular.time, logData.time) + + delete result.circular + t.same(result, { level: 30 }) + }) + + t.end() +}) diff --git a/lib/utils/format-time.test.js b/lib/utils/format-time.test.js new file mode 100644 index 00000000..e0e70122 --- /dev/null +++ b/lib/utils/format-time.test.js @@ -0,0 +1,65 @@ +'use strict' + +process.env.TZ = 'UTC' + +const tap = require('tap') +const formatTime = require('./format-time') + +const dateStr = '2019-04-06T13:30:00.000-04:00' +const epoch = new Date(dateStr) +const epochMS = epoch.getTime() + +tap.test('passes through epoch if `translateTime` is `false`', async t => { + const formattedTime = formatTime(epochMS) + t.equal(formattedTime, epochMS) +}) + +tap.test('translates epoch milliseconds if `translateTime` is `true`', async t => { + const formattedTime = formatTime(epochMS, true) + t.equal(formattedTime, '17:30:00.000') +}) + +tap.test('translates epoch milliseconds to UTC string given format', async t => { + const formattedTime = formatTime(epochMS, 'd mmm yyyy H:MM') + t.equal(formattedTime, '6 Apr 2019 17:30') +}) + +tap.test('translates epoch milliseconds to SYS:STANDARD', async t => { + const formattedTime = formatTime(epochMS, 'SYS:STANDARD') + t.match(formattedTime, /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [-+]?\d{4}/) +}) + +tap.test('translates epoch milliseconds to SYS:', async t => { + const formattedTime = formatTime(epochMS, 'SYS:d mmm yyyy H:MM') + t.match(formattedTime, /\d{1} \w{3} \d{4} \d{1,2}:\d{2}/) +}) + +tap.test('passes through date string if `translateTime` is `false`', async t => { + const formattedTime = formatTime(dateStr) + t.equal(formattedTime, dateStr) +}) + +tap.test('translates date string if `translateTime` is `true`', async t => { + const formattedTime = formatTime(dateStr, true) + t.equal(formattedTime, '17:30:00.000') +}) + +tap.test('translates date string to UTC string given format', async t => { + const formattedTime = formatTime(dateStr, 'd mmm yyyy H:MM') + t.equal(formattedTime, '6 Apr 2019 17:30') +}) + +tap.test('translates date string to SYS:STANDARD', async t => { + const formattedTime = formatTime(dateStr, 'SYS:STANDARD') + t.match(formattedTime, /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [-+]?\d{4}/) +}) + +tap.test('translates date string to UTC:', async t => { + const formattedTime = formatTime(dateStr, 'UTC:d mmm yyyy H:MM') + t.equal(formattedTime, '6 Apr 2019 17:30') +}) + +tap.test('translates date string to SYS:', async t => { + const formattedTime = formatTime(dateStr, 'SYS:d mmm yyyy H:MM') + t.match(formattedTime, /\d{1} \w{3} \d{4} \d{1,2}:\d{2}/) +}) diff --git a/lib/utils/get-property-value.test.js b/lib/utils/get-property-value.test.js new file mode 100644 index 00000000..1aa804b3 --- /dev/null +++ b/lib/utils/get-property-value.test.js @@ -0,0 +1,31 @@ +'use strict' + +const tap = require('tap') +const getPropertyValue = require('./get-property-value') + +tap.test('getPropertyValue returns the value of the property', async t => { + const result = getPropertyValue({ + foo: 'bar' + }, 'foo') + t.same(result, 'bar') +}) + +tap.test('getPropertyValue returns the value of the nested property', async t => { + const result = getPropertyValue({ extra: { foo: { value: 'bar' } } }, 'extra.foo.value') + t.same(result, 'bar') +}) + +tap.test('getPropertyValue returns the value of the nested property using the array of nested property keys', async t => { + const result = getPropertyValue({ extra: { foo: { value: 'bar' } } }, ['extra', 'foo', 'value']) + t.same(result, 'bar') +}) + +tap.test('getPropertyValue returns undefined for non-existing properties', async t => { + const result = getPropertyValue({ extra: { foo: { value: 'bar' } } }, 'extra.foo.value-2') + t.same(result, undefined) +}) + +tap.test('getPropertyValue returns undefined for non-existing properties using the array of nested property keys', async t => { + const result = getPropertyValue({ extra: { foo: { value: 'bar' } } }, ['extra', 'foo', 'value-2']) + t.same(result, undefined) +}) diff --git a/lib/utils/handle-custom-levels-names-opts.test.js b/lib/utils/handle-custom-levels-names-opts.test.js new file mode 100644 index 00000000..489e3cf1 --- /dev/null +++ b/lib/utils/handle-custom-levels-names-opts.test.js @@ -0,0 +1,31 @@ +'use strict' + +const tap = require('tap') +const handleCustomLevelsNamesOpts = require('./handle-custom-levels-names-opts') + +tap.test('returns a empty object `{}` for unknown parameter', async t => { + const handledCustomLevelNames = handleCustomLevelsNamesOpts(123) + t.same(handledCustomLevelNames, {}) +}) + +tap.test('returns a filled object for string parameter', async t => { + const handledCustomLevelNames = handleCustomLevelsNamesOpts('ok:10,warn:20,error:35') + t.same(handledCustomLevelNames, { + ok: 10, + warn: 20, + error: 35 + }) +}) + +tap.test('returns a filled object for object parameter', async t => { + const handledCustomLevelNames = handleCustomLevelsNamesOpts({ + ok: 10, + warn: 20, + error: 35 + }) + t.same(handledCustomLevelNames, { + ok: 10, + warn: 20, + error: 35 + }) +}) diff --git a/lib/utils/handle-custom-levels-opts.test.js b/lib/utils/handle-custom-levels-opts.test.js new file mode 100644 index 00000000..fae50a53 --- /dev/null +++ b/lib/utils/handle-custom-levels-opts.test.js @@ -0,0 +1,33 @@ +'use strict' + +const tap = require('tap') +const handleCustomLevelsOpts = require('./handle-custom-levels-opts') + +tap.test('returns a empty object `{}` for unknown parameter', async t => { + const handledCustomLevel = handleCustomLevelsOpts(123) + t.same(handledCustomLevel, {}) +}) + +tap.test('returns a filled object for string parameter', async t => { + const handledCustomLevel = handleCustomLevelsOpts('ok:10,warn:20,error:35') + t.same(handledCustomLevel, { + 10: 'OK', + 20: 'WARN', + 35: 'ERROR', + default: 'USERLVL' + }) +}) + +tap.test('returns a filled object for object parameter', async t => { + const handledCustomLevel = handleCustomLevelsOpts({ + ok: 10, + warn: 20, + error: 35 + }) + t.same(handledCustomLevel, { + 10: 'OK', + 20: 'WARN', + 35: 'ERROR', + default: 'USERLVL' + }) +}) diff --git a/lib/utils/index.js b/lib/utils/index.js index d119a464..bcd0b54c 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -3,7 +3,7 @@ const { readdirSync } = require('fs') const { basename } = require('path') -const files = readdirSync(__dirname) +const files = readdirSync(__dirname).filter(f => f.endsWith('.test.js') === false) for (const file of files) { if (file === 'index.js') continue diff --git a/lib/utils/interpret-conditionals.test.js b/lib/utils/interpret-conditionals.test.js new file mode 100644 index 00000000..6615552a --- /dev/null +++ b/lib/utils/interpret-conditionals.test.js @@ -0,0 +1,69 @@ +'use strict' + +const tap = require('tap') +const { createCopier } = require('fast-copy') +const fastCopy = createCopier({}) +const interpretConditionals = require('./interpret-conditionals') + +const logData = { + level: 30, + data1: { + data2: 'bar' + }, + msg: 'foo' +} + +tap.test('interpretConditionals translates if / else statement to found property value', async t => { + const log = fastCopy(logData) + t.equal(interpretConditionals('{level} - {if data1.data2}{data1.data2}{end}', log), '{level} - bar') +}) + +tap.test('interpretConditionals translates if / else statement to found property value and leave unmatched property key untouched', async t => { + const log = fastCopy(logData) + t.equal(interpretConditionals('{level} - {if data1.data2}{data1.data2} ({msg}){end}', log), '{level} - bar ({msg})') +}) + +tap.test('interpretConditionals removes non-terminated if statements', async t => { + const log = fastCopy(logData) + t.equal(interpretConditionals('{level} - {if data1.data2}{data1.data2}', log), '{level} - {data1.data2}') +}) + +tap.test('interpretConditionals removes floating end statements', async t => { + const log = fastCopy(logData) + t.equal(interpretConditionals('{level} - {data1.data2}{end}', log), '{level} - {data1.data2}') +}) + +tap.test('interpretConditionals removes floating end statements within translated if / end statements', async t => { + const log = fastCopy(logData) + t.equal(interpretConditionals('{level} - {if msg}({msg}){end}{end}', log), '{level} - (foo)') +}) + +tap.test('interpretConditionals removes if / end blocks if existent condition key does not match existent property key', async t => { + const log = fastCopy(logData) + t.equal(interpretConditionals('{level}{if msg}{data1.data2}{end}', log), '{level}') +}) + +tap.test('interpretConditionals removes if / end blocks if non-existent condition key does not match existent property key', async t => { + const log = fastCopy(logData) + t.equal(interpretConditionals('{level}{if foo}{msg}{end}', log), '{level}') +}) + +tap.test('interpretConditionals removes if / end blocks if existent condition key does not match non-existent property key', async t => { + const log = fastCopy(logData) + t.equal(interpretConditionals('{level}{if msg}{foo}{end}', log), '{level}') +}) + +tap.test('interpretConditionals removes if / end blocks if non-existent condition key does not match non-existent property key', async t => { + const log = fastCopy(logData) + t.equal(interpretConditionals('{level}{if foo}{bar}{end}', log), '{level}') +}) + +tap.test('interpretConditionals removes if / end blocks if nested condition key does not match property key', async t => { + const log = fastCopy(logData) + t.equal(interpretConditionals('{level}{if data1.msg}{data1.data2}{end}', log), '{level}') +}) + +tap.test('interpretConditionals removes nested if / end statement blocks', async t => { + const log = fastCopy(logData) + t.equal(interpretConditionals('{if msg}{if data1.data2}{msg}{data1.data2}{end}{end}', log), 'foo{data1.data2}') +}) diff --git a/lib/utils/is-object.test.js b/lib/utils/is-object.test.js new file mode 100644 index 00000000..85ab6029 --- /dev/null +++ b/lib/utils/is-object.test.js @@ -0,0 +1,10 @@ +'use strict' + +const tap = require('tap') +const isObject = require('./is-object') + +tap.test('returns correct answer', async t => { + t.equal(isObject({}), true) + t.equal(isObject([]), false) + t.equal(isObject(42), false) +}) diff --git a/lib/utils/is-valid-date.test.js b/lib/utils/is-valid-date.test.js new file mode 100644 index 00000000..bb9e0d1c --- /dev/null +++ b/lib/utils/is-valid-date.test.js @@ -0,0 +1,16 @@ +'use strict' + +process.env.TZ = 'UTC' + +const tap = require('tap') +const isValidDate = require('./is-valid-date') + +tap.test('returns true for valid dates', async t => { + t.same(isValidDate(new Date()), true) +}) + +tap.test('returns false for non-dates and invalid dates', async t => { + t.plan(2) + t.same(isValidDate('20210621'), false) + t.same(isValidDate(new Date('2021-41-99')), false) +}) diff --git a/lib/utils/join-lines-with-indentation.test.js b/lib/utils/join-lines-with-indentation.test.js new file mode 100644 index 00000000..7cf60b24 --- /dev/null +++ b/lib/utils/join-lines-with-indentation.test.js @@ -0,0 +1,16 @@ +'use strict' + +const tap = require('tap') +const joinLinesWithIndentation = require('./join-lines-with-indentation') + +tap.test('joinLinesWithIndentation adds indentation to beginning of subsequent lines', async t => { + const input = 'foo\nbar\nbaz' + const result = joinLinesWithIndentation({ input }) + t.equal(result, 'foo\n bar\n baz') +}) + +tap.test('joinLinesWithIndentation accepts custom indentation, line breaks, and eol', async t => { + const input = 'foo\nbar\r\nbaz' + const result = joinLinesWithIndentation({ input, ident: ' ', eol: '^' }) + t.equal(result, 'foo^ bar^ baz') +}) diff --git a/lib/utils/noop.test.js b/lib/utils/noop.test.js new file mode 100644 index 00000000..5acfd7c1 --- /dev/null +++ b/lib/utils/noop.test.js @@ -0,0 +1,12 @@ +'use strict' + +const tap = require('tap') +const noop = require('./noop') + +tap.test('is a function', async t => { + t.type(noop, Function) +}) + +tap.test('does nothing', async t => { + t.equal(noop('stuff'), undefined) +}) diff --git a/lib/utils/prettify-error-log.test.js b/lib/utils/prettify-error-log.test.js new file mode 100644 index 00000000..d2363f6f --- /dev/null +++ b/lib/utils/prettify-error-log.test.js @@ -0,0 +1,22 @@ +'use strict' + +const tap = require('tap') +const prettifyErrorLog = require('./prettify-error-log') + +tap.test('returns string with default settings', async t => { + const err = Error('Something went wrong') + const str = prettifyErrorLog({ log: err }) + t.ok(str.startsWith(' Error: Something went wrong')) +}) + +tap.test('returns string with custom ident', async t => { + const err = Error('Something went wrong') + const str = prettifyErrorLog({ log: err, ident: ' ' }) + t.ok(str.startsWith(' Error: Something went wrong')) +}) + +tap.test('returns string with custom eol', async t => { + const err = Error('Something went wrong') + const str = prettifyErrorLog({ log: err, eol: '\r\n' }) + t.ok(str.startsWith(' Error: Something went wrong\r\n')) +}) diff --git a/lib/utils/prettify-error.test.js b/lib/utils/prettify-error.test.js new file mode 100644 index 00000000..ba26944a --- /dev/null +++ b/lib/utils/prettify-error.test.js @@ -0,0 +1,14 @@ +'use strict' + +const tap = require('tap') +const stringifySafe = require('fast-safe-stringify') +const prettifyError = require('./prettify-error') + +tap.test('prettifies error', t => { + const error = Error('Bad error!') + const lines = stringifySafe(error, Object.getOwnPropertyNames(error), 2) + + const prettyError = prettifyError({ keyName: 'errorKey', lines, ident: ' ', eol: '\n' }) + t.match(prettyError, /\s*errorKey: {\n\s*"stack":[\s\S]*"message": "Bad error!"/) + t.end() +}) diff --git a/lib/utils/prettify-level.test.js b/lib/utils/prettify-level.test.js new file mode 100644 index 00000000..438cbe8a --- /dev/null +++ b/lib/utils/prettify-level.test.js @@ -0,0 +1,27 @@ +'use strict' + +const tap = require('tap') +const prettifyLevel = require('./prettify-level') +const getColorizer = require('../colors') + +tap.test('returns `undefined` for unknown level', async t => { + const colorized = prettifyLevel({ log: {} }) + t.equal(colorized, undefined) +}) + +tap.test('returns non-colorized value for default colorizer', async t => { + const log = { + level: 30 + } + const colorized = prettifyLevel({ log }) + t.equal(colorized, 'INFO') +}) + +tap.test('returns colorized value for color colorizer', async t => { + const log = { + level: 30 + } + const colorizer = getColorizer(true) + const colorized = prettifyLevel({ log, colorizer }) + t.equal(colorized, '\u001B[32mINFO\u001B[39m') +}) diff --git a/lib/utils/prettify-message.test.js b/lib/utils/prettify-message.test.js new file mode 100644 index 00000000..061f7131 --- /dev/null +++ b/lib/utils/prettify-message.test.js @@ -0,0 +1,99 @@ +'use strict' + +const tap = require('tap') +const prettifyMessage = require('./prettify-message') +const getColorizer = require('../colors') + +tap.test('returns `undefined` if `messageKey` not found', async t => { + const str = prettifyMessage({ log: {} }) + t.equal(str, undefined) +}) + +tap.test('returns `undefined` if `messageKey` not string', async t => { + const str = prettifyMessage({ log: { msg: {} } }) + t.equal(str, undefined) +}) + +tap.test('returns non-colorized value for default colorizer', async t => { + const str = prettifyMessage({ log: { msg: 'foo' } }) + t.equal(str, 'foo') +}) + +tap.test('returns non-colorized value for alternate `messageKey`', async t => { + const str = prettifyMessage({ log: { message: 'foo' }, messageKey: 'message' }) + t.equal(str, 'foo') +}) + +tap.test('returns colorized value for color colorizer', async t => { + const colorizer = getColorizer(true) + const str = prettifyMessage({ log: { msg: 'foo' }, colorizer }) + t.equal(str, '\u001B[36mfoo\u001B[39m') +}) + +tap.test('returns colorized value for color colorizer for alternate `messageKey`', async t => { + const colorizer = getColorizer(true) + const str = prettifyMessage({ log: { message: 'foo' }, messageKey: 'message', colorizer }) + t.equal(str, '\u001B[36mfoo\u001B[39m') +}) + +tap.test('returns message formatted by `messageFormat` option', async t => { + const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule' }, messageFormat: '{context} - {msg}' }) + t.equal(str, 'appModule - foo') +}) + +tap.test('returns message formatted by `messageFormat` option - missing prop', async t => { + const str = prettifyMessage({ log: { context: 'appModule' }, messageFormat: '{context} - {msg}' }) + t.equal(str, 'appModule - ') +}) + +tap.test('returns message formatted by `messageFormat` option - levelLabel & useOnlyCustomProps false', async t => { + const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule', level: 30 }, messageFormat: '[{level}] {levelLabel} {context} - {msg}', customLevels: {} }) + t.equal(str, '[30] INFO appModule - foo') +}) + +tap.test('returns message formatted by `messageFormat` option - levelLabel & useOnlyCustomProps true', async t => { + const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule', level: 30 }, messageFormat: '[{level}] {levelLabel} {context} - {msg}', customLevels: { 30: 'CHECK' }, useOnlyCustomProps: true }) + t.equal(str, '[30] CHECK appModule - foo') +}) + +tap.test('returns message formatted by `messageFormat` option - levelLabel & customLevels', async t => { + const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule', level: 123 }, messageFormat: '[{level}] {levelLabel} {context} - {msg}', customLevels: { 123: 'CUSTOM' } }) + t.equal(str, '[123] CUSTOM appModule - foo') +}) + +tap.test('returns message formatted by `messageFormat` option - levelLabel, customLevels & useOnlyCustomProps', async t => { + const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule', level: 123 }, messageFormat: '[{level}] {levelLabel} {context} - {msg}', customLevels: { 123: 'CUSTOM' }, useOnlyCustomProps: true }) + t.equal(str, '[123] CUSTOM appModule - foo') +}) + +tap.test('returns message formatted by `messageFormat` option - levelLabel, customLevels & useOnlyCustomProps false', async t => { + const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule', level: 40 }, messageFormat: '[{level}] {levelLabel} {context} - {msg}', customLevels: { 123: 'CUSTOM' }, useOnlyCustomProps: false }) + t.equal(str, '[40] WARN appModule - foo') +}) + +tap.test('`messageFormat` supports nested curly brackets', async t => { + const str = prettifyMessage({ log: { level: 30 }, messageFormat: '{{level}}-{level}-{{level}-{level}}' }) + t.equal(str, '{30}-30-{30-30}') +}) + +tap.test('`messageFormat` supports nested object', async t => { + const str = prettifyMessage({ log: { level: 30, request: { url: 'localhost/test' }, msg: 'foo' }, messageFormat: '{request.url} - param: {request.params.process} - {msg}' }) + t.equal(str, 'localhost/test - param: - foo') +}) + +tap.test('`messageFormat` supports conditional blocks', async t => { + const str = prettifyMessage({ log: { level: 30, req: { id: 'foo' } }, messageFormat: '{level} | {if req.id}({req.id}){end}{if msg}{msg}{end}' }) + t.equal(str, '30 | (foo)') +}) + +tap.test('`messageFormat` supports function definition', async t => { + const str = prettifyMessage({ + log: { level: 30, request: { url: 'localhost/test' }, msg: 'incoming request' }, + messageFormat: (log, messageKey, levelLabel) => { + let msg = log[messageKey] + if (msg === 'incoming request') msg = `--> ${log.request.url}` + return msg + } + }) + t.equal(str, '--> localhost/test') +}) diff --git a/lib/utils/prettify-metadata.test.js b/lib/utils/prettify-metadata.test.js new file mode 100644 index 00000000..a1342fb7 --- /dev/null +++ b/lib/utils/prettify-metadata.test.js @@ -0,0 +1,84 @@ +'use strict' + +const tap = require('tap') +const prettifyMetadata = require('./prettify-metadata') + +tap.test('returns `undefined` if no metadata present', async t => { + const str = prettifyMetadata({ log: {} }) + t.equal(str, undefined) +}) + +tap.test('works with only `name` present', async t => { + const str = prettifyMetadata({ log: { name: 'foo' } }) + t.equal(str, '(foo)') +}) + +tap.test('works with only `pid` present', async t => { + const str = prettifyMetadata({ log: { pid: '1234' } }) + t.equal(str, '(1234)') +}) + +tap.test('works with only `hostname` present', async t => { + const str = prettifyMetadata({ log: { hostname: 'bar' } }) + t.equal(str, '(on bar)') +}) + +tap.test('works with only `name` & `pid` present', async t => { + const str = prettifyMetadata({ log: { name: 'foo', pid: '1234' } }) + t.equal(str, '(foo/1234)') +}) + +tap.test('works with only `name` & `hostname` present', async t => { + const str = prettifyMetadata({ log: { name: 'foo', hostname: 'bar' } }) + t.equal(str, '(foo on bar)') +}) + +tap.test('works with only `pid` & `hostname` present', async t => { + const str = prettifyMetadata({ log: { pid: '1234', hostname: 'bar' } }) + t.equal(str, '(1234 on bar)') +}) + +tap.test('works with only `name`, `pid`, & `hostname` present', async t => { + const str = prettifyMetadata({ log: { name: 'foo', pid: '1234', hostname: 'bar' } }) + t.equal(str, '(foo/1234 on bar)') +}) + +tap.test('works with only `name` & `caller` present', async t => { + const str = prettifyMetadata({ log: { name: 'foo', caller: 'baz' } }) + t.equal(str, '(foo) ') +}) + +tap.test('works with only `pid` & `caller` present', async t => { + const str = prettifyMetadata({ log: { pid: '1234', caller: 'baz' } }) + t.equal(str, '(1234) ') +}) + +tap.test('works with only `hostname` & `caller` present', async t => { + const str = prettifyMetadata({ log: { hostname: 'bar', caller: 'baz' } }) + t.equal(str, '(on bar) ') +}) + +tap.test('works with only `name`, `pid`, & `caller` present', async t => { + const str = prettifyMetadata({ log: { name: 'foo', pid: '1234', caller: 'baz' } }) + t.equal(str, '(foo/1234) ') +}) + +tap.test('works with only `name`, `hostname`, & `caller` present', async t => { + const str = prettifyMetadata({ log: { name: 'foo', hostname: 'bar', caller: 'baz' } }) + t.equal(str, '(foo on bar) ') +}) + +tap.test('works with only `caller` present', async t => { + const str = prettifyMetadata({ log: { caller: 'baz' } }) + t.equal(str, '') +}) + +tap.test('works with only `pid`, `hostname`, & `caller` present', async t => { + const str = prettifyMetadata({ log: { pid: '1234', hostname: 'bar', caller: 'baz' } }) + t.equal(str, '(1234 on bar) ') +}) + +tap.test('works with all four present', async t => { + const str = prettifyMetadata({ log: { name: 'foo', pid: '1234', hostname: 'bar', caller: 'baz' } }) + t.equal(str, '(foo/1234 on bar) ') +}) diff --git a/lib/utils/prettify-object.test.js b/lib/utils/prettify-object.test.js new file mode 100644 index 00000000..7d67d912 --- /dev/null +++ b/lib/utils/prettify-object.test.js @@ -0,0 +1,51 @@ +'use strict' + +const tap = require('tap') +const prettifyObject = require('./prettify-object') + +tap.test('returns empty string if no properties present', async t => { + const str = prettifyObject({ input: {} }) + t.equal(str, '') +}) + +tap.test('works with single level properties', async t => { + const str = prettifyObject({ input: { foo: 'bar' } }) + t.equal(str, ' foo: "bar"\n') +}) + +tap.test('works with multiple level properties', async t => { + const str = prettifyObject({ input: { foo: { bar: 'baz' } } }) + t.equal(str, ' foo: {\n "bar": "baz"\n }\n') +}) + +tap.test('skips specified keys', async t => { + const str = prettifyObject({ input: { foo: 'bar', hello: 'world' }, skipKeys: ['foo'] }) + t.equal(str, ' hello: "world"\n') +}) + +tap.test('ignores predefined keys', async t => { + const str = prettifyObject({ input: { foo: 'bar', pid: 12345 } }) + t.equal(str, ' foo: "bar"\n') +}) + +tap.test('ignores escaped backslashes in string values', async t => { + const str = prettifyObject({ input: { foo_regexp: '\\[^\\w\\s]\\' } }) + t.equal(str, ' foo_regexp: "\\[^\\w\\s]\\"\n') +}) + +tap.test('ignores escaped backslashes in string values (singleLine option)', async t => { + const str = prettifyObject({ input: { foo_regexp: '\\[^\\w\\s]\\' }, singleLine: true }) + t.equal(str, '{"foo_regexp":"\\[^\\w\\s]\\"}\n') +}) + +tap.test('works with error props', async t => { + const err = Error('Something went wrong') + const serializedError = { + message: err.message, + stack: err.stack + } + const str = prettifyObject({ input: { error: serializedError } }) + t.ok(str.startsWith(' error:')) + t.ok(str.includes(' "message": "Something went wrong",')) + t.ok(str.includes(' Error: Something went wrong')) +}) diff --git a/lib/utils/prettify-time.test.js b/lib/utils/prettify-time.test.js new file mode 100644 index 00000000..5a3cc504 --- /dev/null +++ b/lib/utils/prettify-time.test.js @@ -0,0 +1,102 @@ +'use strict' + +process.env.TZ = 'UTC' + +const tap = require('tap') +const prettifyTime = require('./prettify-time') + +tap.test('returns `undefined` if `time` or `timestamp` not in log', async t => { + const str = prettifyTime({ log: {} }) + t.equal(str, undefined) +}) + +tap.test('returns prettified formatted time from custom field', async t => { + const log = { customtime: 1554642900000 } + let str = prettifyTime({ log, translateFormat: true, timestampKey: 'customtime' }) + t.equal(str, '[13:15:00.000]') + + str = prettifyTime({ log, translateFormat: false, timestampKey: 'customtime' }) + t.equal(str, '[1554642900000]') +}) + +tap.test('returns prettified formatted time', async t => { + let log = { time: 1554642900000 } + let str = prettifyTime({ log, translateFormat: true }) + t.equal(str, '[13:15:00.000]') + + log = { timestamp: 1554642900000 } + str = prettifyTime({ log, translateFormat: true }) + t.equal(str, '[13:15:00.000]') + + log = { time: '2019-04-07T09:15:00.000-04:00' } + str = prettifyTime({ log, translateFormat: true }) + t.equal(str, '[13:15:00.000]') + + log = { timestamp: '2019-04-07T09:15:00.000-04:00' } + str = prettifyTime({ log, translateFormat: true }) + t.equal(str, '[13:15:00.000]') + + log = { time: 1554642900000 } + str = prettifyTime({ log, translateFormat: 'd mmm yyyy H:MM' }) + t.equal(str, '[7 Apr 2019 13:15]') + + log = { timestamp: 1554642900000 } + str = prettifyTime({ log, translateFormat: 'd mmm yyyy H:MM' }) + t.equal(str, '[7 Apr 2019 13:15]') + + log = { time: '2019-04-07T09:15:00.000-04:00' } + str = prettifyTime({ log, translateFormat: 'd mmm yyyy H:MM' }) + t.equal(str, '[7 Apr 2019 13:15]') + + log = { timestamp: '2019-04-07T09:15:00.000-04:00' } + str = prettifyTime({ log, translateFormat: 'd mmm yyyy H:MM' }) + t.equal(str, '[7 Apr 2019 13:15]') +}) + +tap.test('passes through value', async t => { + let log = { time: 1554642900000 } + let str = prettifyTime({ log }) + t.equal(str, '[1554642900000]') + + log = { timestamp: 1554642900000 } + str = prettifyTime({ log }) + t.equal(str, '[1554642900000]') + + log = { time: '2019-04-07T09:15:00.000-04:00' } + str = prettifyTime({ log }) + t.equal(str, '[2019-04-07T09:15:00.000-04:00]') + + log = { timestamp: '2019-04-07T09:15:00.000-04:00' } + str = prettifyTime({ log }) + t.equal(str, '[2019-04-07T09:15:00.000-04:00]') +}) + +tap.test('handles the 0 timestamp', async t => { + let log = { time: 0 } + let str = prettifyTime({ log }) + t.equal(str, '[0]') + + log = { timestamp: 0 } + str = prettifyTime({ log }) + t.equal(str, '[0]') +}) + +tap.test('works with epoch as a number or string', (t) => { + t.plan(3) + const epoch = 1522431328992 + const asNumber = prettifyTime({ + log: { time: epoch, msg: 'foo' }, + translateFormat: true + }) + const asString = prettifyTime({ + log: { time: `${epoch}`, msg: 'foo' }, + translateFormat: true + }) + const invalid = prettifyTime({ + log: { time: '2 days ago', msg: 'foo' }, + translateFormat: true + }) + t.same(asString, '[17:35:28.992]') + t.same(asNumber, '[17:35:28.992]') + t.same(invalid, '[2 days ago]') +}) diff --git a/lib/utils/split-property-key.test.js b/lib/utils/split-property-key.test.js new file mode 100644 index 00000000..425f9a06 --- /dev/null +++ b/lib/utils/split-property-key.test.js @@ -0,0 +1,29 @@ +'use strict' + +const tap = require('tap') +const splitPropertyKey = require('./split-property-key') + +tap.test('splitPropertyKey does not change key', async t => { + const result = splitPropertyKey('data1') + t.same(result, ['data1']) +}) + +tap.test('splitPropertyKey splits nested key', async t => { + const result = splitPropertyKey('data1.data2.data-3') + t.same(result, ['data1', 'data2', 'data-3']) +}) + +tap.test('splitPropertyKey splits nested keys ending with a dot', async t => { + const result = splitPropertyKey('data1.data2.data-3.') + t.same(result, ['data1', 'data2', 'data-3']) +}) + +tap.test('splitPropertyKey splits nested escaped key', async t => { + const result = splitPropertyKey('logging\\.domain\\.corp/operation.foo.bar-2') + t.same(result, ['logging.domain.corp/operation', 'foo', 'bar-2']) +}) + +tap.test('splitPropertyKey splits nested escaped key with special characters', async t => { + const result = splitPropertyKey('logging\\.domain\\.corp/operation.!\t@#$%^&*()_+=-<>.bar\\.2') + t.same(result, ['logging.domain.corp/operation', '!\t@#$%^&*()_+=-<>', 'bar.2']) +}) diff --git a/test/lib/utils.internals.test.js b/test/lib/utils.internals.test.js deleted file mode 100644 index 74836e37..00000000 --- a/test/lib/utils.internals.test.js +++ /dev/null @@ -1,291 +0,0 @@ -'use strict' - -process.env.TZ = 'UTC' - -const tap = require('tap') -const { createCopier } = require('fast-copy') -const stringifySafe = require('fast-safe-stringify') -const utils = require('../../lib/utils') -const fastCopy = createCopier({}) - -tap.test('#joinLinesWithIndentation', t => { - t.test('joinLinesWithIndentation adds indentation to beginning of subsequent lines', async t => { - const input = 'foo\nbar\nbaz' - const result = utils.joinLinesWithIndentation({ input }) - t.equal(result, 'foo\n bar\n baz') - }) - - t.test('joinLinesWithIndentation accepts custom indentation, line breaks, and eol', async t => { - const input = 'foo\nbar\r\nbaz' - const result = utils.joinLinesWithIndentation({ input, ident: ' ', eol: '^' }) - t.equal(result, 'foo^ bar^ baz') - }) - - t.end() -}) - -tap.test('#formatTime', t => { - const dateStr = '2019-04-06T13:30:00.000-04:00' - const epoch = new Date(dateStr) - const epochMS = epoch.getTime() - - t.test('passes through epoch if `translateTime` is `false`', async t => { - const formattedTime = utils.formatTime(epochMS) - t.equal(formattedTime, epochMS) - }) - - t.test('translates epoch milliseconds if `translateTime` is `true`', async t => { - const formattedTime = utils.formatTime(epochMS, true) - t.equal(formattedTime, '17:30:00.000') - }) - - t.test('translates epoch milliseconds to UTC string given format', async t => { - const formattedTime = utils.formatTime(epochMS, 'd mmm yyyy H:MM') - t.equal(formattedTime, '6 Apr 2019 17:30') - }) - - t.test('translates epoch milliseconds to SYS:STANDARD', async t => { - const formattedTime = utils.formatTime(epochMS, 'SYS:STANDARD') - t.match(formattedTime, /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [-+]?\d{4}/) - }) - - t.test('translates epoch milliseconds to SYS:', async t => { - const formattedTime = utils.formatTime(epochMS, 'SYS:d mmm yyyy H:MM') - t.match(formattedTime, /\d{1} \w{3} \d{4} \d{1,2}:\d{2}/) - }) - - t.test('passes through date string if `translateTime` is `false`', async t => { - const formattedTime = utils.formatTime(dateStr) - t.equal(formattedTime, dateStr) - }) - - t.test('translates date string if `translateTime` is `true`', async t => { - const formattedTime = utils.formatTime(dateStr, true) - t.equal(formattedTime, '17:30:00.000') - }) - - t.test('translates date string to UTC string given format', async t => { - const formattedTime = utils.formatTime(dateStr, 'd mmm yyyy H:MM') - t.equal(formattedTime, '6 Apr 2019 17:30') - }) - - t.test('translates date string to SYS:STANDARD', async t => { - const formattedTime = utils.formatTime(dateStr, 'SYS:STANDARD') - t.match(formattedTime, /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [-+]?\d{4}/) - }) - - t.test('translates date string to UTC:', async t => { - const formattedTime = utils.formatTime(dateStr, 'UTC:d mmm yyyy H:MM') - t.equal(formattedTime, '6 Apr 2019 17:30') - }) - - t.test('translates date string to SYS:', async t => { - const formattedTime = utils.formatTime(dateStr, 'SYS:d mmm yyyy H:MM') - t.match(formattedTime, /\d{1} \w{3} \d{4} \d{1,2}:\d{2}/) - }) - - t.end() -}) - -tap.test('#createDate', t => { - const wanted = 1624450038567 - - t.test('accepts arguments the Date constructor would accept', async t => { - t.plan(2) - t.same(utils.createDate(1624450038567).getTime(), wanted) - t.same(utils.createDate('2021-06-23T12:07:18.567Z').getTime(), wanted) - }) - - t.test('accepts epoch as a string', async t => { - // If Date() accepts this argument, the createDate function is not needed - // and can be replaced with Date() - t.plan(2) - t.notSame(new Date('16244500385-67').getTime(), wanted) - t.same(utils.createDate('1624450038567').getTime(), wanted) - }) - - t.end() -}) - -tap.test('#isValidDate', t => { - t.test('returns true for valid dates', async t => { - t.same(utils.isValidDate(new Date()), true) - }) - - t.test('returns false for non-dates and invalid dates', async t => { - t.plan(2) - t.same(utils.isValidDate('20210621'), false) - t.same(utils.isValidDate(new Date('2021-41-99')), false) - }) - - t.end() -}) - -tap.test('#prettifyError', t => { - t.test('prettifies error', t => { - const error = Error('Bad error!') - const lines = stringifySafe(error, Object.getOwnPropertyNames(error), 2) - - const prettyError = utils.prettifyError({ keyName: 'errorKey', lines, ident: ' ', eol: '\n' }) - t.match(prettyError, /\s*errorKey: {\n\s*"stack":[\s\S]*"message": "Bad error!"/) - t.end() - }) - - t.end() -}) - -tap.test('#deleteLogProperty', t => { - const logData = { - level: 30, - data1: { - data2: { 'data-3': 'bar' } - } - } - - t.test('deleteLogProperty deletes property of depth 1', async t => { - const log = fastCopy(logData) - utils.deleteLogProperty(log, 'data1') - t.same(log, { level: 30 }) - }) - - t.test('deleteLogProperty deletes property of depth 2', async t => { - const log = fastCopy(logData) - utils.deleteLogProperty(log, 'data1.data2') - t.same(log, { level: 30, data1: { } }) - }) - - t.test('deleteLogProperty deletes property of depth 3', async t => { - const log = fastCopy(logData) - utils.deleteLogProperty(log, 'data1.data2.data-3') - t.same(log, { level: 30, data1: { data2: { } } }) - }) - - t.end() -}) - -tap.test('#splitPropertyKey', t => { - t.test('splitPropertyKey does not change key', async t => { - const result = utils.splitPropertyKey('data1') - t.same(result, ['data1']) - }) - - t.test('splitPropertyKey splits nested key', async t => { - const result = utils.splitPropertyKey('data1.data2.data-3') - t.same(result, ['data1', 'data2', 'data-3']) - }) - - t.test('splitPropertyKey splits nested keys ending with a dot', async t => { - const result = utils.splitPropertyKey('data1.data2.data-3.') - t.same(result, ['data1', 'data2', 'data-3']) - }) - - t.test('splitPropertyKey splits nested escaped key', async t => { - const result = utils.splitPropertyKey('logging\\.domain\\.corp/operation.foo.bar-2') - t.same(result, ['logging.domain.corp/operation', 'foo', 'bar-2']) - }) - - t.test('splitPropertyKey splits nested escaped key with special characters', async t => { - const result = utils.splitPropertyKey('logging\\.domain\\.corp/operation.!\t@#$%^&*()_+=-<>.bar\\.2') - t.same(result, ['logging.domain.corp/operation', '!\t@#$%^&*()_+=-<>', 'bar.2']) - }) - - t.end() -}) - -tap.test('#getPropertyValue', t => { - t.test('getPropertyValue returns the value of the property', async t => { - const result = utils.getPropertyValue({ - foo: 'bar' - }, 'foo') - t.same(result, 'bar') - }) - - t.test('getPropertyValue returns the value of the nested property', async t => { - const result = utils.getPropertyValue({ extra: { foo: { value: 'bar' } } }, 'extra.foo.value') - t.same(result, 'bar') - }) - - t.test('getPropertyValue returns the value of the nested property using the array of nested property keys', async t => { - const result = utils.getPropertyValue({ extra: { foo: { value: 'bar' } } }, ['extra', 'foo', 'value']) - t.same(result, 'bar') - }) - - t.test('getPropertyValue returns undefined for non-existing properties', async t => { - const result = utils.getPropertyValue({ extra: { foo: { value: 'bar' } } }, 'extra.foo.value-2') - t.same(result, undefined) - }) - - t.test('getPropertyValue returns undefined for non-existing properties using the array of nested property keys', async t => { - const result = utils.getPropertyValue({ extra: { foo: { value: 'bar' } } }, ['extra', 'foo', 'value-2']) - t.same(result, undefined) - }) - - t.end() -}) - -tap.test('#interpretConditionals', t => { - const logData = { - level: 30, - data1: { - data2: 'bar' - }, - msg: 'foo' - } - - t.test('interpretConditionals translates if / else statement to found property value', async t => { - const log = fastCopy(logData) - t.equal(utils.interpretConditionals('{level} - {if data1.data2}{data1.data2}{end}', log), '{level} - bar') - }) - - t.test('interpretConditionals translates if / else statement to found property value and leave unmatched property key untouched', async t => { - const log = fastCopy(logData) - t.equal(utils.interpretConditionals('{level} - {if data1.data2}{data1.data2} ({msg}){end}', log), '{level} - bar ({msg})') - }) - - t.test('interpretConditionals removes non-terminated if statements', async t => { - const log = fastCopy(logData) - t.equal(utils.interpretConditionals('{level} - {if data1.data2}{data1.data2}', log), '{level} - {data1.data2}') - }) - - t.test('interpretConditionals removes floating end statements', async t => { - const log = fastCopy(logData) - t.equal(utils.interpretConditionals('{level} - {data1.data2}{end}', log), '{level} - {data1.data2}') - }) - - t.test('interpretConditionals removes floating end statements within translated if / end statements', async t => { - const log = fastCopy(logData) - t.equal(utils.interpretConditionals('{level} - {if msg}({msg}){end}{end}', log), '{level} - (foo)') - }) - - t.test('interpretConditionals removes if / end blocks if existent condition key does not match existent property key', async t => { - const log = fastCopy(logData) - t.equal(utils.interpretConditionals('{level}{if msg}{data1.data2}{end}', log), '{level}') - }) - - t.test('interpretConditionals removes if / end blocks if non-existent condition key does not match existent property key', async t => { - const log = fastCopy(logData) - t.equal(utils.interpretConditionals('{level}{if foo}{msg}{end}', log), '{level}') - }) - - t.test('interpretConditionals removes if / end blocks if existent condition key does not match non-existent property key', async t => { - const log = fastCopy(logData) - t.equal(utils.interpretConditionals('{level}{if msg}{foo}{end}', log), '{level}') - }) - - t.test('interpretConditionals removes if / end blocks if non-existent condition key does not match non-existent property key', async t => { - const log = fastCopy(logData) - t.equal(utils.interpretConditionals('{level}{if foo}{bar}{end}', log), '{level}') - }) - - t.test('interpretConditionals removes if / end blocks if nested condition key does not match property key', async t => { - const log = fastCopy(logData) - t.equal(utils.interpretConditionals('{level}{if data1.msg}{data1.data2}{end}', log), '{level}') - }) - - t.test('interpretConditionals removes nested if / end statement blocks', async t => { - const log = fastCopy(logData) - t.equal(utils.interpretConditionals('{if msg}{if data1.data2}{msg}{data1.data2}{end}{end}', log), 'foo{data1.data2}') - }) - - t.end() -}) diff --git a/test/lib/utils.public.test.js b/test/lib/utils.public.test.js deleted file mode 100644 index 113a73e6..00000000 --- a/test/lib/utils.public.test.js +++ /dev/null @@ -1,634 +0,0 @@ -'use strict' - -process.env.TZ = 'UTC' - -const tap = require('tap') -const getColorizer = require('../../lib/colors') -const utils = require('../../lib/utils') -const rimraf = require('rimraf') -const { join } = require('path') -const fs = require('fs') - -tap.test('prettifyErrorLog', t => { - const { prettifyErrorLog } = utils - - t.test('returns string with default settings', async t => { - const err = Error('Something went wrong') - const str = prettifyErrorLog({ log: err }) - t.ok(str.startsWith(' Error: Something went wrong')) - }) - - t.test('returns string with custom ident', async t => { - const err = Error('Something went wrong') - const str = prettifyErrorLog({ log: err, ident: ' ' }) - t.ok(str.startsWith(' Error: Something went wrong')) - }) - - t.test('returns string with custom eol', async t => { - const err = Error('Something went wrong') - const str = prettifyErrorLog({ log: err, eol: '\r\n' }) - t.ok(str.startsWith(' Error: Something went wrong\r\n')) - }) - - t.end() -}) - -tap.test('prettifyLevel', t => { - const { prettifyLevel } = utils - - t.test('returns `undefined` for unknown level', async t => { - const colorized = prettifyLevel({ log: {} }) - t.equal(colorized, undefined) - }) - - t.test('returns non-colorized value for default colorizer', async t => { - const log = { - level: 30 - } - const colorized = prettifyLevel({ log }) - t.equal(colorized, 'INFO') - }) - - t.test('returns colorized value for color colorizer', async t => { - const log = { - level: 30 - } - const colorizer = getColorizer(true) - const colorized = prettifyLevel({ log, colorizer }) - t.equal(colorized, '\u001B[32mINFO\u001B[39m') - }) - - t.end() -}) - -tap.test('prettifyMessage', t => { - const { prettifyMessage } = utils - - t.test('returns `undefined` if `messageKey` not found', async t => { - const str = prettifyMessage({ log: {} }) - t.equal(str, undefined) - }) - - t.test('returns `undefined` if `messageKey` not string', async t => { - const str = prettifyMessage({ log: { msg: {} } }) - t.equal(str, undefined) - }) - - t.test('returns non-colorized value for default colorizer', async t => { - const str = prettifyMessage({ log: { msg: 'foo' } }) - t.equal(str, 'foo') - }) - - t.test('returns non-colorized value for alternate `messageKey`', async t => { - const str = prettifyMessage({ log: { message: 'foo' }, messageKey: 'message' }) - t.equal(str, 'foo') - }) - - t.test('returns colorized value for color colorizer', async t => { - const colorizer = getColorizer(true) - const str = prettifyMessage({ log: { msg: 'foo' }, colorizer }) - t.equal(str, '\u001B[36mfoo\u001B[39m') - }) - - t.test('returns colorized value for color colorizer for alternate `messageKey`', async t => { - const colorizer = getColorizer(true) - const str = prettifyMessage({ log: { message: 'foo' }, messageKey: 'message', colorizer }) - t.equal(str, '\u001B[36mfoo\u001B[39m') - }) - - t.test('returns message formatted by `messageFormat` option', async t => { - const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule' }, messageFormat: '{context} - {msg}' }) - t.equal(str, 'appModule - foo') - }) - - t.test('returns message formatted by `messageFormat` option - missing prop', async t => { - const str = prettifyMessage({ log: { context: 'appModule' }, messageFormat: '{context} - {msg}' }) - t.equal(str, 'appModule - ') - }) - - t.test('returns message formatted by `messageFormat` option - levelLabel & useOnlyCustomProps false', async t => { - const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule', level: 30 }, messageFormat: '[{level}] {levelLabel} {context} - {msg}', customLevels: {} }) - t.equal(str, '[30] INFO appModule - foo') - }) - - t.test('returns message formatted by `messageFormat` option - levelLabel & useOnlyCustomProps true', async t => { - const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule', level: 30 }, messageFormat: '[{level}] {levelLabel} {context} - {msg}', customLevels: { 30: 'CHECK' }, useOnlyCustomProps: true }) - t.equal(str, '[30] CHECK appModule - foo') - }) - - t.test('returns message formatted by `messageFormat` option - levelLabel & customLevels', async t => { - const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule', level: 123 }, messageFormat: '[{level}] {levelLabel} {context} - {msg}', customLevels: { 123: 'CUSTOM' } }) - t.equal(str, '[123] CUSTOM appModule - foo') - }) - - t.test('returns message formatted by `messageFormat` option - levelLabel, customLevels & useOnlyCustomProps', async t => { - const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule', level: 123 }, messageFormat: '[{level}] {levelLabel} {context} - {msg}', customLevels: { 123: 'CUSTOM' }, useOnlyCustomProps: true }) - t.equal(str, '[123] CUSTOM appModule - foo') - }) - - t.test('returns message formatted by `messageFormat` option - levelLabel, customLevels & useOnlyCustomProps false', async t => { - const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule', level: 40 }, messageFormat: '[{level}] {levelLabel} {context} - {msg}', customLevels: { 123: 'CUSTOM' }, useOnlyCustomProps: false }) - t.equal(str, '[40] WARN appModule - foo') - }) - - t.test('`messageFormat` supports nested curly brackets', async t => { - const str = prettifyMessage({ log: { level: 30 }, messageFormat: '{{level}}-{level}-{{level}-{level}}' }) - t.equal(str, '{30}-30-{30-30}') - }) - - t.test('`messageFormat` supports nested object', async t => { - const str = prettifyMessage({ log: { level: 30, request: { url: 'localhost/test' }, msg: 'foo' }, messageFormat: '{request.url} - param: {request.params.process} - {msg}' }) - t.equal(str, 'localhost/test - param: - foo') - }) - - t.test('`messageFormat` supports conditional blocks', async t => { - const str = prettifyMessage({ log: { level: 30, req: { id: 'foo' } }, messageFormat: '{level} | {if req.id}({req.id}){end}{if msg}{msg}{end}' }) - t.equal(str, '30 | (foo)') - }) - - t.test('`messageFormat` supports function definition', async t => { - const str = prettifyMessage({ - log: { level: 30, request: { url: 'localhost/test' }, msg: 'incoming request' }, - messageFormat: (log, messageKey, levelLabel) => { - let msg = log[messageKey] - if (msg === 'incoming request') msg = `--> ${log.request.url}` - return msg - } - }) - t.equal(str, '--> localhost/test') - }) - - t.end() -}) - -tap.test('prettifyMetadata', t => { - const { prettifyMetadata } = utils - - t.test('returns `undefined` if no metadata present', async t => { - const str = prettifyMetadata({ log: {} }) - t.equal(str, undefined) - }) - - t.test('works with only `name` present', async t => { - const str = prettifyMetadata({ log: { name: 'foo' } }) - t.equal(str, '(foo)') - }) - - t.test('works with only `pid` present', async t => { - const str = prettifyMetadata({ log: { pid: '1234' } }) - t.equal(str, '(1234)') - }) - - t.test('works with only `hostname` present', async t => { - const str = prettifyMetadata({ log: { hostname: 'bar' } }) - t.equal(str, '(on bar)') - }) - - t.test('works with only `name` & `pid` present', async t => { - const str = prettifyMetadata({ log: { name: 'foo', pid: '1234' } }) - t.equal(str, '(foo/1234)') - }) - - t.test('works with only `name` & `hostname` present', async t => { - const str = prettifyMetadata({ log: { name: 'foo', hostname: 'bar' } }) - t.equal(str, '(foo on bar)') - }) - - t.test('works with only `pid` & `hostname` present', async t => { - const str = prettifyMetadata({ log: { pid: '1234', hostname: 'bar' } }) - t.equal(str, '(1234 on bar)') - }) - - t.test('works with only `name`, `pid`, & `hostname` present', async t => { - const str = prettifyMetadata({ log: { name: 'foo', pid: '1234', hostname: 'bar' } }) - t.equal(str, '(foo/1234 on bar)') - }) - - t.test('works with only `name` & `caller` present', async t => { - const str = prettifyMetadata({ log: { name: 'foo', caller: 'baz' } }) - t.equal(str, '(foo) ') - }) - - t.test('works with only `pid` & `caller` present', async t => { - const str = prettifyMetadata({ log: { pid: '1234', caller: 'baz' } }) - t.equal(str, '(1234) ') - }) - - t.test('works with only `hostname` & `caller` present', async t => { - const str = prettifyMetadata({ log: { hostname: 'bar', caller: 'baz' } }) - t.equal(str, '(on bar) ') - }) - - t.test('works with only `name`, `pid`, & `caller` present', async t => { - const str = prettifyMetadata({ log: { name: 'foo', pid: '1234', caller: 'baz' } }) - t.equal(str, '(foo/1234) ') - }) - - t.test('works with only `name`, `hostname`, & `caller` present', async t => { - const str = prettifyMetadata({ log: { name: 'foo', hostname: 'bar', caller: 'baz' } }) - t.equal(str, '(foo on bar) ') - }) - - t.test('works with only `caller` present', async t => { - const str = prettifyMetadata({ log: { caller: 'baz' } }) - t.equal(str, '') - }) - - t.test('works with only `pid`, `hostname`, & `caller` present', async t => { - const str = prettifyMetadata({ log: { pid: '1234', hostname: 'bar', caller: 'baz' } }) - t.equal(str, '(1234 on bar) ') - }) - - t.test('works with all four present', async t => { - const str = prettifyMetadata({ log: { name: 'foo', pid: '1234', hostname: 'bar', caller: 'baz' } }) - t.equal(str, '(foo/1234 on bar) ') - }) - - t.end() -}) - -tap.test('prettifyObject', t => { - const { prettifyObject } = utils - - t.test('returns empty string if no properties present', async t => { - const str = prettifyObject({ input: {} }) - t.equal(str, '') - }) - - t.test('works with single level properties', async t => { - const str = prettifyObject({ input: { foo: 'bar' } }) - t.equal(str, ' foo: "bar"\n') - }) - - t.test('works with multiple level properties', async t => { - const str = prettifyObject({ input: { foo: { bar: 'baz' } } }) - t.equal(str, ' foo: {\n "bar": "baz"\n }\n') - }) - - t.test('skips specified keys', async t => { - const str = prettifyObject({ input: { foo: 'bar', hello: 'world' }, skipKeys: ['foo'] }) - t.equal(str, ' hello: "world"\n') - }) - - t.test('ignores predefined keys', async t => { - const str = prettifyObject({ input: { foo: 'bar', pid: 12345 } }) - t.equal(str, ' foo: "bar"\n') - }) - - t.test('ignores escaped backslashes in string values', async t => { - const str = prettifyObject({ input: { foo_regexp: '\\[^\\w\\s]\\' } }) - t.equal(str, ' foo_regexp: "\\[^\\w\\s]\\"\n') - }) - - t.test('ignores escaped backslashes in string values (singleLine option)', async t => { - const str = prettifyObject({ input: { foo_regexp: '\\[^\\w\\s]\\' }, singleLine: true }) - t.equal(str, '{"foo_regexp":"\\[^\\w\\s]\\"}\n') - }) - - t.test('works with error props', async t => { - const err = Error('Something went wrong') - const serializedError = { - message: err.message, - stack: err.stack - } - const str = prettifyObject({ input: { error: serializedError } }) - t.ok(str.startsWith(' error:')) - t.ok(str.includes(' "message": "Something went wrong",')) - t.ok(str.includes(' Error: Something went wrong')) - }) - - t.end() -}) - -tap.test('prettifyTime', t => { - const { prettifyTime } = utils - - t.test('returns `undefined` if `time` or `timestamp` not in log', async t => { - const str = prettifyTime({ log: {} }) - t.equal(str, undefined) - }) - - t.test('returns prettified formatted time from custom field', async t => { - const log = { customtime: 1554642900000 } - let str = prettifyTime({ log, translateFormat: true, timestampKey: 'customtime' }) - t.equal(str, '[13:15:00.000]') - - str = prettifyTime({ log, translateFormat: false, timestampKey: 'customtime' }) - t.equal(str, '[1554642900000]') - }) - - t.test('returns prettified formatted time', async t => { - let log = { time: 1554642900000 } - let str = prettifyTime({ log, translateFormat: true }) - t.equal(str, '[13:15:00.000]') - - log = { timestamp: 1554642900000 } - str = prettifyTime({ log, translateFormat: true }) - t.equal(str, '[13:15:00.000]') - - log = { time: '2019-04-07T09:15:00.000-04:00' } - str = prettifyTime({ log, translateFormat: true }) - t.equal(str, '[13:15:00.000]') - - log = { timestamp: '2019-04-07T09:15:00.000-04:00' } - str = prettifyTime({ log, translateFormat: true }) - t.equal(str, '[13:15:00.000]') - - log = { time: 1554642900000 } - str = prettifyTime({ log, translateFormat: 'd mmm yyyy H:MM' }) - t.equal(str, '[7 Apr 2019 13:15]') - - log = { timestamp: 1554642900000 } - str = prettifyTime({ log, translateFormat: 'd mmm yyyy H:MM' }) - t.equal(str, '[7 Apr 2019 13:15]') - - log = { time: '2019-04-07T09:15:00.000-04:00' } - str = prettifyTime({ log, translateFormat: 'd mmm yyyy H:MM' }) - t.equal(str, '[7 Apr 2019 13:15]') - - log = { timestamp: '2019-04-07T09:15:00.000-04:00' } - str = prettifyTime({ log, translateFormat: 'd mmm yyyy H:MM' }) - t.equal(str, '[7 Apr 2019 13:15]') - }) - - t.test('passes through value', async t => { - let log = { time: 1554642900000 } - let str = prettifyTime({ log }) - t.equal(str, '[1554642900000]') - - log = { timestamp: 1554642900000 } - str = prettifyTime({ log }) - t.equal(str, '[1554642900000]') - - log = { time: '2019-04-07T09:15:00.000-04:00' } - str = prettifyTime({ log }) - t.equal(str, '[2019-04-07T09:15:00.000-04:00]') - - log = { timestamp: '2019-04-07T09:15:00.000-04:00' } - str = prettifyTime({ log }) - t.equal(str, '[2019-04-07T09:15:00.000-04:00]') - }) - - t.test('handles the 0 timestamp', async t => { - let log = { time: 0 } - let str = prettifyTime({ log }) - t.equal(str, '[0]') - - log = { timestamp: 0 } - str = prettifyTime({ log }) - t.equal(str, '[0]') - }) - - t.test('works with epoch as a number or string', (t) => { - t.plan(3) - const epoch = 1522431328992 - const asNumber = prettifyTime({ - log: { time: epoch, msg: 'foo' }, - translateFormat: true - }) - const asString = prettifyTime({ - log: { time: `${epoch}`, msg: 'foo' }, - translateFormat: true - }) - const invalid = prettifyTime({ - log: { time: '2 days ago', msg: 'foo' }, - translateFormat: true - }) - t.same(asString, '[17:35:28.992]') - t.same(asNumber, '[17:35:28.992]') - t.same(invalid, '[2 days ago]') - }) - - t.end() -}) - -const logData = { - level: 30, - time: 1522431328992, - data1: { - data2: { 'data-3': 'bar' }, - error: new Error('test') - } -} -const logData2 = Object.assign({ - 'logging.domain.corp/operation': { - id: 'foo', - producer: 'bar' - } -}, logData) - -tap.test('#filterLog with an ignoreKeys option', t => { - const { filterLog } = utils - - t.test('filterLog removes single entry', async t => { - const result = filterLog({ log: logData, ignoreKeys: ['data1.data2.data-3'] }) - t.same(result, { level: 30, time: 1522431328992, data1: { data2: { }, error: new Error('test') } }) - }) - - t.test('filterLog removes multiple entries', async t => { - const result = filterLog({ log: logData, ignoreKeys: ['time', 'data1'] }) - t.same(result, { level: 30 }) - }) - - t.test('filterLog keeps error instance', async t => { - const result = filterLog({ log: logData, ignoreKeys: [] }) - t.equal(logData.data1.error, result.data1.error) - }) - - t.test('filterLog removes entry with escape sequence', async t => { - const result = filterLog({ log: logData2, ignoreKeys: ['data1', 'logging\\.domain\\.corp/operation'] }) - t.same(result, { level: 30, time: 1522431328992 }) - }) - - t.test('filterLog removes entry with escape sequence nested', async t => { - const result = filterLog({ log: logData2, ignoreKeys: ['data1', 'logging\\.domain\\.corp/operation.producer'] }) - t.same(result, { level: 30, time: 1522431328992, 'logging.domain.corp/operation': { id: 'foo' } }) - }) - - t.end() -}) - -const ignoreKeysArray = [ - undefined, - ['level'], - ['level', 'data1.data2.data-3'] -] -ignoreKeysArray.forEach(ignoreKeys => { - tap.test(`#filterLog with an includeKeys option when the ignoreKeys being ${ignoreKeys}`, t => { - const { filterLog } = utils - - t.test('filterLog include nothing', async t => { - const result = filterLog({ log: logData, ignoreKeys, includeKeys: [] }) - t.same(result, {}) - }) - - t.test('filterLog include single entry', async t => { - const result = filterLog({ log: logData, ignoreKeys, includeKeys: ['time'] }) - t.same(result, { time: 1522431328992 }) - }) - - t.test('filterLog include multiple entries', async t => { - const result = filterLog({ log: logData, ignoreKeys, includeKeys: ['time', 'data1'] }) - t.same(result, { - time: 1522431328992, - data1: { - data2: { 'data-3': 'bar' }, - error: new Error('test') - } - }) - }) - - t.end() - }) -}) - -tap.test('#filterLog with circular references', t => { - const { filterLog } = utils - const logData = { - level: 30, - time: 1522431328992, - data1: 'test' - } - logData.circular = logData - - t.test('filterLog removes single entry', async t => { - const result = filterLog({ log: logData, ignoreKeys: ['data1'] }) - - t.same(result.circular.level, result.level) - t.same(result.circular.time, result.time) - - delete result.circular - t.same(result, { level: 30, time: 1522431328992 }) - }) - - t.test('filterLog includes single entry', async t => { - const result = filterLog({ log: logData, includeKeys: ['data1'] }) - - t.same(result, { data1: 'test' }) - }) - - t.test('filterLog includes circular keys', async t => { - const result = filterLog({ log: logData, includeKeys: ['level', 'circular'] }) - - t.same(result.circular.level, logData.level) - t.same(result.circular.time, logData.time) - - delete result.circular - t.same(result, { level: 30 }) - }) - - t.end() -}) - -tap.test('buildSafeSonicBoom', t => { - const { buildSafeSonicBoom } = utils - - function noop () {} - - const file = () => { - const dest = join(__dirname, `${process.pid}-${process.hrtime().toString()}`) - const fd = fs.openSync(dest, 'w') - return { dest, fd } - } - - t.test('should not write when error emitted and code is "EPIPE"', async t => { - t.plan(1) - - const { fd, dest } = file() - const stream = buildSafeSonicBoom({ sync: true, fd, mkdir: true }) - t.teardown(() => rimraf(dest, noop)) - - stream.emit('error', { code: 'EPIPE' }) - stream.write('will not work') - - const dataFile = fs.readFileSync(dest) - t.equal(dataFile.length, 0) - }) - - t.test('should stream.write works when error code is not "EPIPE"', async t => { - t.plan(3) - const { fd, dest } = file() - const stream = buildSafeSonicBoom({ sync: true, fd, mkdir: true }) - - t.teardown(() => rimraf(dest, noop)) - - stream.on('error', () => t.pass('error emitted')) - - stream.emit('error', 'fake error description') - - t.ok(stream.write('will work')) - - const dataFile = fs.readFileSync(dest) - t.equal(dataFile.toString(), 'will work') - }) - - t.end() -}) - -tap.test('handleCustomLevelsOpts', t => { - const { handleCustomLevelsOpts } = utils - - t.test('returns a empty object `{}` for unknown parameter', async t => { - const handledCustomLevel = handleCustomLevelsOpts(123) - t.same(handledCustomLevel, {}) - }) - - t.test('returns a filled object for string parameter', async t => { - const handledCustomLevel = handleCustomLevelsOpts('ok:10,warn:20,error:35') - t.same(handledCustomLevel, { - 10: 'OK', - 20: 'WARN', - 35: 'ERROR', - default: 'USERLVL' - }) - }) - - t.test('returns a filled object for object parameter', async t => { - const handledCustomLevel = handleCustomLevelsOpts({ - ok: 10, - warn: 20, - error: 35 - }) - t.same(handledCustomLevel, { - 10: 'OK', - 20: 'WARN', - 35: 'ERROR', - default: 'USERLVL' - }) - }) - - t.end() -}) - -tap.test('handleCustomLevelsNamesOpts', t => { - const { handleCustomLevelsNamesOpts } = utils - - t.test('returns a empty object `{}` for unknown parameter', async t => { - const handledCustomLevelNames = handleCustomLevelsNamesOpts(123) - t.same(handledCustomLevelNames, {}) - }) - - t.test('returns a filled object for string parameter', async t => { - const handledCustomLevelNames = handleCustomLevelsNamesOpts('ok:10,warn:20,error:35') - t.same(handledCustomLevelNames, { - ok: 10, - warn: 20, - error: 35 - }) - }) - - t.test('returns a filled object for object parameter', async t => { - const handledCustomLevelNames = handleCustomLevelsNamesOpts({ - ok: 10, - warn: 20, - error: 35 - }) - t.same(handledCustomLevelNames, { - ok: 10, - warn: 20, - error: 35 - }) - }) - - t.end() -})