Merge pull request #87 from christroutner/dh-logs-coverage

feat(logs): Increased logs controller to 100% test coverage
This commit is contained in:
Chris Troutner
2020-11-17 08:05:25 -08:00
committed by GitHub
3 changed files with 321 additions and 27 deletions
+42 -27
View File
@@ -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) {
+251
View File
@@ -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')
}
})
})
})
+28
View File
@@ -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
}