diff --git a/src/modules/logapi/controller.js b/src/modules/logapi/controller.js index 5759f44..7dbf572 100644 --- a/src/modules/logapi/controller.js +++ b/src/modules/logapi/controller.js @@ -8,6 +8,9 @@ let _this class LogsApi { constructor () { _this = this + _this.fs = fs + _this.lineReader = lineReader + _this.config = config } /** @@ -56,30 +59,29 @@ class LogsApi { // console.log(`password: ${password}`) // Password matches the password set in the config file. - if (password === config.logPass) { + if (password === _this.config.logPass) { // Generate the full path and file name for the current log file. const fullPath = _this.generateFileName() + // console.log(`fullPath: ${JSON.stringify(fullPath, null, 2)}`) // Throw an error if the file does not exist. - fs.stat(fullPath, (err, stat) => { - if (err) { - ctx.body = { - success: false, - data: 'file does not exist' - } + if (!_this.fs.existsSync(fullPath)) { + ctx.body = { + success: false, + data: 'file does not exist' } - }) + } else { + // Read in the data from the log file. + const data = await _this.readLines(fullPath) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) - // Read in the data from the log file. - const data = await _this.readLines(fullPath) - // console.log(`data: ${JSON.stringify(data, null, 2)}`) + // Filter the logs before passing them to the front end. + const filteredData = _this.filterLogs(data) - // Filter the logs before passing them to the front end. - const filteredData = _this.filterLogs(data) - - ctx.body = { - success: true, - data: filteredData + ctx.body = { + success: true, + data: filteredData + } } // Password does not match password in config file. @@ -89,21 +91,23 @@ class LogsApi { } } } catch (err) { - if (err) { + if (err && err.message) { ctx.throw(500, err.message) } else { ctx.throw(500, 'Unhandled error') - console.log('unhandled error: ', err) } } } // Sorts the log data by their timestamp. Returns the LIMIT or less elements. - filterLogs (data) { + filterLogs (data, LIMIT = 100) { try { + if (!Array.isArray(data)) { + throw new Error('Data must be array') + } // console.log(`data: ${JSON.stringify(data, null, 2)}`) - const LIMIT = 100 // Max number of entries to return. + // const LIMIT = 100 // Max number of entries to return. // Sort the elements by date. data.sort(function (a, b) { @@ -142,7 +146,7 @@ class LogsApi { const thisYear = now.getFullYear() const filename = `koa-${ - config.env + _this.config.env }-${thisYear}-${thisMonth}-${thisDate}.log` // console.log(`filename: ${filename}`) const logDir = `${__dirname}/../../../logs/` @@ -157,23 +161,33 @@ class LogsApi { } // Promise based read-file - readFile (path, opts = 'utf8') { + /* readFile (path, opts = 'utf8') { return new Promise((resolve, reject) => { - fs.readFile(path, opts, (err, data) => { + _this.fs.readFile(path, opts, (err, data) => { if (err) reject(err) else resolve(data) }) }) - } + } */ // Returns an array with each element containing a line of the file. readLines (filename) { return new Promise((resolve, reject) => { try { + if (!filename || typeof filename !== 'string') { + throw new Error('filename must be a string') + } + // Throw an error if the file does not exist. + + if (!_this.fs.existsSync(filename)) { + throw new Error('file does not exist') + } + const data = [] + // let i = 0 - lineReader.eachLine(filename, function (line, last) { + _this.lineReader.eachLine(filename, function (line, last) { try { data.push(JSON.parse(line)) @@ -183,7 +197,8 @@ class LogsApi { if (last) return resolve(data) } catch (err) { - console.log('err: ', err) + // console.log('err: ', err) + if (last) return resolve(data) } }) } catch (err) { diff --git a/test/a06-logapi.spec.js b/test/a06-logapi.spec.js new file mode 100644 index 0000000..a4c14b9 --- /dev/null +++ b/test/a06-logapi.spec.js @@ -0,0 +1,251 @@ +const config = require('../config') +const assert = require('chai').assert + +const axios = require('axios').default +const sinon = require('sinon') + +const util = require('util') +util.inspect.defaultOptions = { depth: 1 } + +const LOCALHOST = `http://localhost:${config.port}` + +const LogsController = require('../src/modules/logapi/controller') +const mockContext = require('./mocks/ctx-mock').context +const mockData = require('./mocks/log-api-mock') + +const context = {} +let sandbox +let uut +describe('LogsApi', () => { + beforeEach(() => { + uut = new LogsController() + + sandbox = sinon.createSandbox() + }) + + afterEach(() => sandbox.restore()) + + describe('POST /logapi', () => { + it('should return false if password is not provided', async () => { + try { + const options = { + method: 'post', + url: `${LOCALHOST}/logapi`, + data: {} + } + + const result = await axios(options) + assert.isFalse(result.data.success) + } catch (err) { + assert(false, 'Unexpected result') + } + }) + it('should return log', async () => { + try { + const options = { + method: 'post', + url: `${LOCALHOST}/logapi`, + data: { + password: 'test' + } + } + + const result = await axios(options) + + assert.isTrue(result.data.success) + assert.isArray(result.data.data) + assert.property(result.data.data[0], 'message') + assert.property(result.data.data[0], 'level') + assert.property(result.data.data[0], 'timestamp') + } catch (err) { + assert(false, 'Unexpected result') + } + }) + it('should return false if files are not found!', async () => { + try { + sandbox.stub(uut, 'generateFileName').resolves('bad router') + + const ctx = mockContext() + ctx.request = { + body: { + password: 'test' + } + } + await uut.getLogs(ctx) + + assert.isFalse(ctx.body.success) + assert.include(ctx.body.data, 'file does not exist') + } catch (err) { + assert.fail('Unexpected result') + } + }) + it('should catch and handle errors', async () => { + try { + // Force an error + sandbox.stub(uut.fs, 'existsSync').throws(new Error('test error')) + + // Mock the context object. + const ctx = mockContext() + + ctx.request = { + body: { + password: 'test' + } + } + + await uut.getLogs(ctx) + + assert.fail('Unexpected result') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + it('should throw unhandled error', async () => { + try { + // Force an error + sandbox.stub(uut.fs, 'existsSync').throws(new Error()) + + // Mock the context object. + const ctx = mockContext() + + ctx.request = { + body: { + password: 'test' + } + } + + await uut.getLogs(ctx) + + assert.fail('Unexpected result') + } catch (err) { + assert.include(err.message, 'Unhandled error') + } + }) + }) + describe('#filterLogs()', () => { + it('should throw error if data is not provided', async () => { + try { + await uut.filterLogs() + + assert.fail('Unexpected result') + } catch (err) { + assert.include(err.message, 'Data must be array') + } + }) + it('should throw error if data provided is not an array', async () => { + try { + const data = 'data' + await uut.filterLogs(data) + + assert.fail('Unexpected result') + } catch (err) { + assert.include(err.message, 'Data must be array') + } + }) + it('should sort the log data', async () => { + try { + const data = mockData.data + const result = await uut.filterLogs(data) + assert.isArray(result) + assert.property(result[1], 'message') + assert.property(result[1], 'level') + assert.property(result[1], 'timestamp') + } catch (err) { + assert.fail('Unexpected result') + } + }) + it('should sort the log data with a limit', async () => { + try { + const data = mockData.data + const limit = 1 + const result = await uut.filterLogs(data, limit) + assert.isArray(result) + assert.equal(result.length, limit) + assert.property(result[0], 'message') + assert.property(result[0], 'level') + assert.property(result[0], 'timestamp') + } catch (err) { + assert.fail('Unexpected result') + } + }) + }) + + describe('#generateFileName()', () => { + it('should return file name', async () => { + try { + const fileName = await uut.generateFileName() + assert.isString(fileName) + context.fileName = fileName + } catch (err) { + assert.fail('Unexpected result') + } + }) + it('should throw error if something fails', async () => { + try { + uut.config = null + await uut.generateFileName() + assert.fail('Unexpected result') + } catch (err) { + assert.exists(err) + assert.isString(err.message) + } + }) + }) + describe('#readLines()', () => { + it('should throw error if fileName is not provided', async () => { + try { + await uut.readLines() + + assert.fail('Unexpected result') + } catch (err) { + assert.include(err.message, 'filename must be a string') + } + }) + it('should throw error if fileName provided is not string', async () => { + try { + const fileName = true + await uut.readLines(fileName) + + assert.fail('Unexpected result') + } catch (err) { + assert.include(err.message, 'filename must be a string') + } + }) + it('should throw error if the file does not exist', async () => { + try { + const fileName = 'test/logs/' + await uut.readLines(fileName) + + assert.fail('Unexpected result') + } catch (err) { + assert.include(err.message, 'file does not exist') + } + }) + it('should ignore fileReader callback errors', async () => { + try { + // https://sinonjs.org/releases/latest/stubs/ + // About yields + sandbox.stub(uut.lineReader, 'eachLine').yieldsRight({}, true) + + const fileName = context.fileName + const result = await uut.readLines(fileName) + assert.isArray(result) + } catch (err) { + assert.fail('Unexpected result') + } + }) + it('should return data', async () => { + try { + const fileName = context.fileName + const result = await uut.readLines(fileName) + + assert.isArray(result) + assert.property(result[1], 'message') + assert.property(result[1], 'level') + assert.property(result[1], 'timestamp') + } catch (err) { + assert.fail('Unexpected result') + } + }) + }) +}) diff --git a/test/mocks/log-api-mock.js b/test/mocks/log-api-mock.js new file mode 100644 index 0000000..c132917 --- /dev/null +++ b/test/mocks/log-api-mock.js @@ -0,0 +1,28 @@ +// Mocks representing an array of logs for the +// Unit tests of logapi + +const data = [ + { + message: 'Error in lib/nodemailer.js/validateEmailArray()', + level: 'error', + timestamp: '2020-11-14T12:15:55.230Z' + }, + { + message: 'Error in lib/nodemailer.js/validateEmailArray()', + level: 'error', + timestamp: '2020-11-14T12:15:55.231Z' + }, + { + message: 'Error in lib/nodemailer.js/validateEmailArray()', + level: 'error', + timestamp: '2020-11-14T12:15:55.230Z' + }, + { + message: 'Error in lib/nodemailer.js/validateEmailArray()', + level: 'error', + timestamp: '2020-11-14T12:15:55.231Z' + } +] +module.exports = { + data +}