'use strict'; const test = require('tap').test; const util = require('util'); const path = require('path'); const sandbox = require('@log4js-node/sandboxed-module'); const debug = require('debug')('log4js:test.configuration-validation'); const deepFreeze = require('deep-freeze'); const log4js = require('../../lib/log4js'); const configuration = require('../../lib/configuration'); const testAppender = (label, result) => ({ configure: function (config, layouts, findAppender) { debug(`testAppender(${label}).configure called, with config: ${util.inspect(config)}`); result.configureCalled = true; result.type = config.type; result.label = label; result.config = config; result.layouts = layouts; result.findAppender = findAppender; return { }; } }); test('log4js configuration validation', (batch) => { batch.test('should give error if config is just plain silly', (t) => { [null, undefined, '', ' ', []].forEach((config) => { const expectedError = new Error( `Problem with log4js configuration: (${util.inspect(config)}) - must be an object.` ); t.throws( () => configuration.configure(config), expectedError ); }); t.end(); }); batch.test('should give error if config is an empty object', (t) => { t.throws( () => log4js.configure({}), '- must have a property "appenders" of type object.' ); t.end(); }); batch.test('should give error if config has no appenders', (t) => { t.throws( () => log4js.configure({ categories: {} }), '- must have a property "appenders" of type object.' ); t.end(); }); batch.test('should give error if config has no categories', (t) => { t.throws( () => log4js.configure({ appenders: { out: { type: 'stdout' } } }), '- must have a property "categories" of type object.' ); t.end(); }); batch.test('should give error if appenders is not an object', (t) => { t.throws( () => log4js.configure({ appenders: [], categories: [] }), '- must have a property "appenders" of type object.' ); t.end(); }); batch.test('should give error if appenders are not all valid', (t) => { t.throws( () => log4js.configure({ appenders: { thing: 'cheese' }, categories: {} }), '- appender "thing" is not valid (must be an object with property "type")' ); t.end(); }); batch.test('should require at least one appender', (t) => { t.throws( () => log4js.configure({ appenders: {}, categories: {} }), '- must define at least one appender.' ); t.end(); }); batch.test('should give error if categories are not all valid', (t) => { t.throws( () => log4js.configure({ appenders: { stdout: { type: 'stdout' } }, categories: { thing: 'cheese' } }), '- category "thing" is not valid (must be an object with properties "appenders" and "level")' ); t.end(); }); batch.test('should give error if default category not defined', (t) => { t.throws( () => log4js.configure({ appenders: { stdout: { type: 'stdout' } }, categories: { thing: { appenders: ['stdout'], level: 'ERROR' } } }), '- must define a "default" category.' ); t.end(); }); batch.test('should require at least one category', (t) => { t.throws( () => log4js.configure({ appenders: { stdout: { type: 'stdout' } }, categories: {} }), '- must define at least one category.' ); t.end(); }); batch.test('should give error if category.appenders is not an array', (t) => { t.throws( () => log4js.configure({ appenders: { stdout: { type: 'stdout' } }, categories: { thing: { appenders: {}, level: 'ERROR' } } }), '- category "thing" is not valid (appenders must be an array of appender names)' ); t.end(); }); batch.test('should give error if category.appenders is empty', (t) => { t.throws( () => log4js.configure({ appenders: { stdout: { type: 'stdout' } }, categories: { thing: { appenders: [], level: 'ERROR' } } }), '- category "thing" is not valid (appenders must contain at least one appender name)' ); t.end(); }); batch.test('should give error if categories do not refer to valid appenders', (t) => { t.throws( () => log4js.configure({ appenders: { stdout: { type: 'stdout' } }, categories: { thing: { appenders: ['cheese'], level: 'ERROR' } } }), '- category "thing" is not valid (appender "cheese" is not defined)' ); t.end(); }); batch.test('should give error if category level is not valid', (t) => { t.throws( () => log4js.configure({ appenders: { stdout: { type: 'stdout' } }, categories: { default: { appenders: ['stdout'], level: 'Biscuits' } } }), '- category "default" is not valid (level "Biscuits" not recognised; valid levels are ALL, TRACE' ); t.end(); }); batch.test('should give error if category enableCallStack is not valid', (t) => { t.throws( () => log4js.configure({ appenders: { stdout: { type: 'stdout' } }, categories: { default: { appenders: ['stdout'], level: 'Debug', enableCallStack: '123' } } }), '- category "default" is not valid (enableCallStack must be boolean type)' ); t.end(); }); batch.test('should give error if appender type cannot be found', (t) => { t.throws( () => log4js.configure({ appenders: { thing: { type: 'cheese' } }, categories: { default: { appenders: ['thing'], level: 'ERROR' } } }), '- appender "thing" is not valid (type "cheese" could not be found)' ); t.end(); }); batch.test('should create appender instances', (t) => { const thing = {}; const sandboxedLog4js = sandbox.require( '../../lib/log4js', { requires: { cheese: testAppender('cheesy', thing) }, ignoreMissing: true } ); sandboxedLog4js.configure({ appenders: { thing: { type: 'cheese' } }, categories: { default: { appenders: ['thing'], level: 'ERROR' } } }); t.ok(thing.configureCalled); t.equal(thing.type, 'cheese'); t.end(); }); batch.test('should not throw error if configure object is freezed', (t) => { t.doesNotThrow(() => log4js.configure(deepFreeze({ appenders: { dateFile: { type: 'dateFile', filename: 'test/tap/freeze-date-file-test', alwaysIncludePattern: false } }, categories: { default: { appenders: ['dateFile'], level: log4js.levels.ERROR } } }))); t.end(); }); batch.test('should load appenders from core first', (t) => { const result = {}; const sandboxedLog4js = sandbox.require( '../../lib/log4js', { requires: { './cheese': testAppender('correct', result), cheese: testAppender('wrong', result) }, ignoreMissing: true } ); sandboxedLog4js.configure({ appenders: { thing: { type: 'cheese' } }, categories: { default: { appenders: ['thing'], level: 'ERROR' } } }); t.ok(result.configureCalled); t.equal(result.type, 'cheese'); t.equal(result.label, 'correct'); t.end(); }); batch.test('should load appenders relative to main file if not in core, or node_modules', (t) => { const result = {}; const mainPath = path.dirname(require.main.filename); const sandboxConfig = { ignoreMissing: true, requires: {} }; sandboxConfig.requires[`${mainPath}/cheese`] = testAppender('correct', result); // add this one, because when we're running coverage the main path is a bit different sandboxConfig.requires[ `${path.join(mainPath, '../../node_modules/nyc/bin/cheese')}` ] = testAppender('correct', result); // in node v6, there's an extra layer of node modules for some reason, so add this one to work around it sandboxConfig.requires[ `${path.join(mainPath, '../../node_modules/tap/node_modules/nyc/bin/cheese')}` ] = testAppender('correct', result); const sandboxedLog4js = sandbox.require('../../lib/log4js', sandboxConfig); sandboxedLog4js.configure({ appenders: { thing: { type: 'cheese' } }, categories: { default: { appenders: ['thing'], level: 'ERROR' } } }); t.ok(result.configureCalled); t.equal(result.type, 'cheese'); t.equal(result.label, 'correct'); t.end(); }); batch.test('should load appenders relative to process.cwd if not found in core, node_modules', (t) => { const result = {}; const fakeProcess = new Proxy(process, { get(target, key) { if (key === 'cwd') { return () => '/var/lib/cheese'; } return target[key]; } }); const sandboxedLog4js = sandbox.require( '../../lib/log4js', { ignoreMissing: true, requires: { '/var/lib/cheese/cheese': testAppender('correct', result), }, globals: { process: fakeProcess } } ); sandboxedLog4js.configure({ appenders: { thing: { type: 'cheese' } }, categories: { default: { appenders: ['thing'], level: 'ERROR' } } }); t.ok(result.configureCalled); t.equal(result.type, 'cheese'); t.equal(result.label, 'correct'); t.end(); }); batch.test('should pass config, layout, findAppender to appenders', (t) => { const result = {}; const sandboxedLog4js = sandbox.require( '../../lib/log4js', { ignoreMissing: true, requires: { cheese: testAppender('cheesy', result), notCheese: testAppender('notCheesy', {}) } } ); sandboxedLog4js.configure({ appenders: { thing: { type: 'cheese', foo: 'bar' }, thing2: { type: 'notCheese' } }, categories: { default: { appenders: ['thing'], level: 'ERROR' } } }); t.ok(result.configureCalled); t.equal(result.type, 'cheese'); t.equal(result.config.foo, 'bar'); t.type(result.layouts, 'object'); t.type(result.layouts.basicLayout, 'function'); t.type(result.findAppender, 'function'); t.type(result.findAppender('thing2'), 'object'); t.end(); }); batch.test('should not give error if level object is used instead of string', (t) => { t.doesNotThrow(() => log4js.configure({ appenders: { thing: { type: 'stdout' } }, categories: { default: { appenders: ['thing'], level: log4js.levels.ERROR } } })); t.end(); }); batch.end(); });