/* eslint max-classes-per-file: ["error", 2] */ const { test } = require('tap'); const EE = require('events').EventEmitter; const levels = require('../../lib/levels'); class MockLogger { constructor() { this.level = levels.TRACE; this.messages = []; this.log = function (level, message) { this.messages.push({ level, message }); }; this.isLevelEnabled = function (level) { return level.isGreaterThanOrEqualTo(this.level); }; } } function MockRequest(remoteAddr, method, originalUrl, headers, url, custom) { this.socket = { remoteAddress: remoteAddr }; this.originalUrl = originalUrl; this.url = url; this.method = method; this.httpVersionMajor = '5'; this.httpVersionMinor = '0'; this.headers = headers || {}; if (custom) { for (const key of Object.keys(custom)) { this[key] = custom[key]; } } const self = this; Object.keys(this.headers).forEach((key) => { self.headers[key.toLowerCase()] = self.headers[key]; }); } class MockResponse extends EE { constructor() { super(); this.cachedHeaders = {}; } end() { this.emit('finish'); } setHeader(key, value) { this.cachedHeaders[key.toLowerCase()] = value; } getHeader(key) { return this.cachedHeaders[key.toLowerCase()]; } writeHead(code /* , headers */) { this.statusCode = code; return this; } } function request( cl, method, originalUrl, code, reqHeaders, resHeaders, next, url, custom = undefined ) { const req = new MockRequest( 'my.remote.addr', method, originalUrl, reqHeaders, url, custom ); const res = new MockResponse(); if (next) { next = next.bind(null, req, res, () => {}); } else { next = () => {}; } cl(req, res, next); res.writeHead(code, resHeaders).end('chunk', 'encoding'); } test('log4js connect logger', (batch) => { const clm = require('../../lib/connect-logger'); batch.test('getConnectLoggerModule', (t) => { t.type(clm, 'function', 'should return a connect logger factory'); t.test( 'should take a log4js logger and return a "connect logger"', (assert) => { const ml = new MockLogger(); const cl = clm(ml); assert.type(cl, 'function'); assert.end(); } ); t.test('log events', (assert) => { const ml = new MockLogger(); const cl = clm(ml); request(cl, 'GET', 'http://url', 200); const { messages } = ml; assert.type(messages, 'Array'); assert.equal(messages.length, 1); assert.ok(levels.INFO.isEqualTo(messages[0].level)); assert.match(messages[0].message, 'GET'); assert.match(messages[0].message, 'http://url'); assert.match(messages[0].message, 'my.remote.addr'); assert.match(messages[0].message, '200'); assert.end(); }); t.test('log events with level below logging level', (assert) => { const ml = new MockLogger(); ml.level = levels.FATAL; const cl = clm(ml); request(cl, 'GET', 'http://url', 200); assert.type(ml.messages, 'Array'); assert.equal(ml.messages.length, 0); assert.end(); }); t.test('log events with non-default level and custom format', (assert) => { const ml = new MockLogger(); ml.level = levels.INFO; const cl = clm(ml, { level: levels.WARN, format: ':method :url' }); request(cl, 'GET', 'http://url', 200); const { messages } = ml; assert.type(messages, Array); assert.equal(messages.length, 1); assert.ok(levels.WARN.isEqualTo(messages[0].level)); assert.equal(messages[0].message, 'GET http://url'); assert.end(); }); t.test('adding multiple loggers should only log once', (assert) => { const ml = new MockLogger(); ml.level = levels.INFO; const cl = clm(ml, { level: levels.WARN, format: ':method :url' }); const nextLogger = clm(ml, { level: levels.INFO, format: ':method' }); request(cl, 'GET', 'http://url', 200, null, null, nextLogger); const { messages } = ml; assert.type(messages, Array); assert.equal(messages.length, 1); assert.ok(levels.WARN.isEqualTo(messages[0].level)); assert.equal(messages[0].message, 'GET http://url'); assert.end(); }); t.end(); }); batch.test('logger with options as string', (t) => { const ml = new MockLogger(); ml.level = levels.INFO; const cl = clm(ml, ':method :url'); request(cl, 'POST', 'http://meh', 200); const { messages } = ml; t.equal(messages[0].message, 'POST http://meh'); t.end(); }); batch.test('auto log levels', (t) => { const ml = new MockLogger(); ml.level = levels.INFO; const cl = clm(ml, { level: 'auto', format: ':method :url' }); request(cl, 'GET', 'http://meh', 200); request(cl, 'GET', 'http://meh', 201); request(cl, 'GET', 'http://meh', 302); request(cl, 'GET', 'http://meh', 404); request(cl, 'GET', 'http://meh', 500); const { messages } = ml; t.test('should use INFO for 2xx', (assert) => { assert.ok(levels.INFO.isEqualTo(messages[0].level)); assert.ok(levels.INFO.isEqualTo(messages[1].level)); assert.end(); }); t.test('should use WARN for 3xx', (assert) => { assert.ok(levels.WARN.isEqualTo(messages[2].level)); assert.end(); }); t.test('should use ERROR for 4xx', (assert) => { assert.ok(levels.ERROR.isEqualTo(messages[3].level)); assert.end(); }); t.test('should use ERROR for 5xx', (assert) => { assert.ok(levels.ERROR.isEqualTo(messages[4].level)); assert.end(); }); t.end(); }); batch.test('logger with status code rules applied', (t) => { const ml = new MockLogger(); ml.level = levels.DEBUG; const clr = [ { codes: [201, 304], level: levels.DEBUG.toString() }, { from: 200, to: 299, level: levels.DEBUG.toString() }, { from: 300, to: 399, level: levels.INFO.toString() }, ]; const cl = clm(ml, { level: 'auto', format: ':method :url', statusRules: clr, }); request(cl, 'GET', 'http://meh', 200); request(cl, 'GET', 'http://meh', 201); request(cl, 'GET', 'http://meh', 302); request(cl, 'GET', 'http://meh', 304); request(cl, 'GET', 'http://meh', 404); request(cl, 'GET', 'http://meh', 500); const { messages } = ml; t.test('should use DEBUG for 2xx', (assert) => { assert.ok(levels.DEBUG.isEqualTo(messages[0].level)); assert.ok(levels.DEBUG.isEqualTo(messages[1].level)); assert.end(); }); t.test('should use WARN for 3xx, DEBUG for 304', (assert) => { assert.ok(levels.INFO.isEqualTo(messages[2].level)); assert.ok(levels.DEBUG.isEqualTo(messages[3].level)); assert.end(); }); t.test('should use ERROR for 4xx', (assert) => { assert.ok(levels.ERROR.isEqualTo(messages[4].level)); assert.end(); }); t.test('should use ERROR for 5xx', (assert) => { assert.ok(levels.ERROR.isEqualTo(messages[5].level)); assert.end(); }); t.end(); }); batch.test('format using a function', (t) => { const ml = new MockLogger(); ml.level = levels.INFO; const cl = clm(ml, () => 'I was called'); request(cl, 'GET', 'http://blah', 200); t.equal(ml.messages[0].message, 'I was called'); t.end(); }); batch.test('format using a function that also uses tokens', (t) => { const ml = new MockLogger(); ml.level = levels.INFO; const cl = clm( ml, (req, res, tokenReplacer) => `${req.method} ${tokenReplacer(':status')}` ); request(cl, 'GET', 'http://blah', 200); t.equal(ml.messages[0].message, 'GET 200'); t.end(); }); batch.test( 'format using a function, but do not log anything if the function returns nothing', (t) => { const ml = new MockLogger(); ml.level = levels.INFO; const cl = clm(ml, () => null); request(cl, 'GET', 'http://blah', 200); t.equal(ml.messages.length, 0); t.end(); } ); batch.test('format that includes request headers', (t) => { const ml = new MockLogger(); ml.level = levels.INFO; const cl = clm(ml, ':req[Content-Type]'); request(cl, 'GET', 'http://blah', 200, { 'Content-Type': 'application/json', }); t.equal(ml.messages[0].message, 'application/json'); t.end(); }); batch.test('format that includes response headers', (t) => { const ml = new MockLogger(); ml.level = levels.INFO; const cl = clm(ml, ':res[Content-Type]'); request(cl, 'GET', 'http://blah', 200, null, { 'Content-Type': 'application/cheese', }); t.equal(ml.messages[0].message, 'application/cheese'); t.end(); }); batch.test('url token should check originalUrl and url', (t) => { const ml = new MockLogger(); const cl = clm(ml, ':url'); request(cl, 'GET', null, 200, null, null, null, 'http://cheese'); t.equal(ml.messages[0].message, 'http://cheese'); t.end(); }); batch.test('log events with custom token', (t) => { const ml = new MockLogger(); ml.level = levels.INFO; const cl = clm(ml, { level: levels.INFO, format: ':method :url :custom_string', tokens: [ { token: ':custom_string', replacement: 'fooBAR', }, ], }); request(cl, 'GET', 'http://url', 200); t.type(ml.messages, 'Array'); t.equal(ml.messages.length, 1); t.ok(levels.INFO.isEqualTo(ml.messages[0].level)); t.equal(ml.messages[0].message, 'GET http://url fooBAR'); t.end(); }); batch.test('log events with custom override token', (t) => { const ml = new MockLogger(); ml.level = levels.INFO; const cl = clm(ml, { level: levels.INFO, format: ':method :url :date', tokens: [ { token: ':date', replacement: '20150310', }, ], }); request(cl, 'GET', 'http://url', 200); t.type(ml.messages, 'Array'); t.equal(ml.messages.length, 1); t.ok(levels.INFO.isEqualTo(ml.messages[0].level)); t.equal(ml.messages[0].message, 'GET http://url 20150310'); t.end(); }); batch.test('log events with custom format', (t) => { const ml = new MockLogger(); const body = { say: 'hi!' }; ml.level = levels.INFO; const cl = clm(ml, { level: levels.INFO, format: (req, res, format) => format(`:method :url ${JSON.stringify(req.body)}`), }); request( cl, 'POST', 'http://url', 200, { 'Content-Type': 'application/json' }, null, null, null, { body } ); t.ok(levels.INFO.isEqualTo(ml.messages[0].level)); t.equal(ml.messages[0].message, `POST http://url ${JSON.stringify(body)}`); t.end(); }); batch.test( 'handle weird old node versions where socket contains socket', (t) => { const ml = new MockLogger(); const cl = clm(ml, ':remote-addr'); const req = new MockRequest(null, 'GET', 'http://blah'); req.socket = { socket: { remoteAddress: 'this is weird' } }; const res = new MockResponse(); cl(req, res, () => {}); res.writeHead(200, {}); res.end('chunk', 'encoding'); t.equal(ml.messages[0].message, 'this is weird'); t.end(); } ); batch.test( 'handles as soon as any of the events end/finish/error/close triggers (only once)', (t) => { const ml = new MockLogger(); const cl = clm(ml, ':remote-addr'); const req = new MockRequest(null, 'GET', 'http://blah'); req.socket = { socket: { remoteAddress: 'this is weird' } }; const res = new MockResponse(); cl(req, res, () => {}); res.writeHead(200, {}); t.equal(ml.messages.length, 0); res.emit('end'); res.emit('finish'); res.emit('error'); res.emit('close'); t.equal(ml.messages.length, 1); t.equal(ml.messages[0].message, 'this is weird'); t.end(); } ); batch.end(); });