Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]
CMD ["node", "index.js"]
16 changes: 0 additions & 16 deletions server/app.js

This file was deleted.

23 changes: 23 additions & 0 deletions server/app/__tests__/app.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const request = require("supertest");
const app = require("../app");

describe('GET /', () => {
it('responds with a 200 status code and welcome message', async () => {
const response = await request(app).get('/');
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({
message: "Welcome to the API",
availableEndpoints: [
"/data",
"/data/:id"
]
});
});
});

describe('GET /data', () => {
it('responds with a 200 status code', async () => {
const response = await request(app).get('/data');
expect(response.statusCode).toBe(200);
});
});
21 changes: 21 additions & 0 deletions server/app/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const express = require("express");
const app = express();
const dataRouter = require("../routes/data");
const {cors, logger, response, unexpectedError} = require("../middleware/index");

app.use(express.json());
app.use(logger);
app.use(cors);
app.use(response);

app.get('/', (req, res) => {
res.json({
message: 'Welcome to the API',
availableEndpoints: ['/data', '/data/:id']
});
});
app.use('/data', dataRouter);

app.use(unexpectedError);

module.exports = app;
80 changes: 80 additions & 0 deletions server/controllers/__tests__/data.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
const supertest = require('supertest');
const app = require('../../app/app');
const DataModel = require('../../models/data');

jest.mock('../../models/data');

// TODO: extract these constants to a shared file
const SUCCESS = 'success';
const FAIL = 'fail';
const ERROR = 'error';

// TODO: beforeeach -> reset database to empty or tests values

describe('Data Controller - Success', () => {
it('should fetch all data', async () => {
DataModel.getData.mockReturnValue([{ id: 1, name: 'Test Data' }]);
await supertest(app)
.get('/data')
.then((response) => {
expect(response.statusCode).toBe(200);
expect(response.body.status).toEqual(SUCCESS);
expect(response.body.data).toEqual([{ id: 1, name: 'Test Data' }]);
});
});

it('should fetch data by ID', async () => {
DataModel.getDataById.mockReturnValue({ id: 2, name: 'Specific Data' });
await supertest(app)
.get('/data/2')
.then((response) => {
expect(response.statusCode).toBe(200);
expect(response.body.status).toEqual(SUCCESS);
expect(response.body.data).toEqual({ id: 2, name: 'Specific Data' });
});
});


it('should write data', async () => {
DataModel.saveData.mockReturnValue({ status: SUCCESS, data: null });
await supertest(app)
.post('/data')
.send({ name: 'Linux' })
.set('X-Plugin-Name', 'os')
.set('X-Machine-Id', 'abcd')
.then((response) => {
expect(response.statusCode).toBe(200);
expect(response.body.status).toEqual(SUCCESS);
expect(response.body.data).toEqual(null);
});
});
});

describe('Data Controller - Fail', () => {
it('should fail if X-Machine-Id header is missing', async () => {
await supertest(app)
.post('/data')
.set('X-Plugin-Name', 'os')
.send({ name: 'Linux' })
.then((response) => {
expect(response.statusCode).toBe(400);
expect(response.body.status).toEqual(FAIL);
expect(response.body.data).toEqual([{"location": "headers", "msg": "Invalid value", "path": "x-machine-id", "type": "field"}]);
});
});
});

describe('Data Controller - Error', () => {
it('should error if something unexpected happens', async () => {
DataModel.getData.mockImplementation(() => {
throw new Error('Unexpected error occurred.');
});
await supertest(app)
.get('/data')
.then((response) => {
expect(response.statusCode).toBe(500);
expect(response.body.status).toEqual(ERROR);
expect(response.body.message).toEqual('Unexpected error occurred.');
});
});
});
19 changes: 11 additions & 8 deletions server/controllers/data.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
const DataModel = require('../models/data');
const { validationResult } = require('express-validator');

const data = {
getData: function(req, res) {
const data = DataModel.getData();
res.send(data);
res.sendData(data);
},
getDataById: function(req, res) {
const data = DataModel.getDataById(req.params.id);
res.send(data);
res.sendData(data);
},
saveData: function(req, res) {
let pluginType = req.header("X-Plugin-Name");
let machineId = req.header("X-Machine-Id");
let msg = `Data from plugin '${pluginType}' received successfully.`;
console.log(msg);
DataModel.saveData(req.body, pluginType, machineId);
res.status(200).send(msg);
const result = validationResult(req);
if (result.isEmpty()) {
let pluginType = req.header("X-Plugin-Name");
let machineId = req.header("X-Machine-Id");
DataModel.saveData(req.body, pluginType, machineId);
return res.sendData(null);
}
res.sendFail(result.array());
},
}

Expand Down
6 changes: 6 additions & 0 deletions server/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const app = require("./app/app");

const port = process.env.port || 4000;
app.listen(port, () => {
console.log(`Server listening on port ${port}!`);
});
83 changes: 83 additions & 0 deletions server/middleware/__tests__/middleware.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const request = require("supertest");
const app = require("../../app/app");
const httpMocks = require('node-mocks-http');
const { response } = require('../index');

jest.mock('../../models/data');

const SUCCESS = 'success';
const FAIL = 'fail';

describe('Cors Middleware', () => {
it('sets Access Control Allow Origin header', async () => {
const response = await request(app).get('/data');

expect(response.headers['access-control-allow-origin']).toBeDefined();
expect(response.headers['access-control-allow-origin']).toEqual('*');
});
});

describe('Logger Middleware', () => {
let originalNodeEnv = process.env.NODE_ENV;

beforeEach(() => {
process.env.NODE_ENV = 'IWantToLogEverything';
});

afterEach(() => {
process.env.NODE_ENV = originalNodeEnv;
});

it('logs input message to the Node.js console', async () => {
const spy = jest.spyOn(console, 'log');
await request(app).get('/data');

expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
});

describe('Response Middleware', () => {
let req, res, next;

beforeEach(() => {
req = httpMocks.createRequest();
res = httpMocks.createResponse();
next = jest.fn();
});

test('res.sendData adds success response method', () => {
const testData = { key: 'value' };
response(req, res, next);
res.sendData(testData);
expect(res._getData()).toEqual(JSON.stringify({ status: SUCCESS, data: testData }));
});

test('res.sendFail adds failure response method', () => {
const testReasons = ['Error reason'];
const statusCode = 400;
response(req, res, next);
res.sendFail(testReasons, statusCode);
expect(res.statusCode).toBe(statusCode);
expect(res._getData()).toEqual(JSON.stringify({ status: FAIL, data: testReasons }));
});
});

describe('Unexpected Error Middleware', () => {
beforeAll(() => {
const { getData } = require('../../models/data');
getData.mockImplementation(() => { throw new Error('Forced error') });
});

test('should respond with 500 and error message on error', async () => {
const response = await request(app)
.get('/data')
.expect('Content-Type', /json/)
.expect(500);

expect(response.body).toEqual({
status: 'error',
message: 'Forced error'
});
});
});
6 changes: 6 additions & 0 deletions server/middleware/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const cors = require("./cors");
const logger = require("./logger");
const response = require("./response");
const unexpectedError = require("./unexpectedError");

module.exports = { cors, logger, response, unexpectedError };
6 changes: 5 additions & 1 deletion server/middleware/logger.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
// TODO: consider using morgan or winston for more logging features

module.exports = function(req, res, next) {
console.log(`${new Date().toISOString()} - ${req.method} request for ${req.url}`);
if (process.env.NODE_ENV !== 'test') {
console.log(`${new Date().toISOString()} - ${req.method} request for ${req.url}`);
}
next();
}
19 changes: 19 additions & 0 deletions server/middleware/response.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/** Uniform the response object structure following JSend specification
* See: https://github.com/omniti-labs/jsend
*/

const SUCCESS = 'success';
const FAIL = 'fail';
const ERROR = 'error';

module.exports = function(req, res, next) {
res.sendData = function(data) {
res.json({ status: SUCCESS, data });
}

res.sendFail = function(reasons, statusCode = 400) {
res.status(statusCode).json({ status: FAIL, data: reasons });
}

next();
}
10 changes: 10 additions & 0 deletions server/middleware/unexpectedError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// TODO: consider using morgan or winston for more logging features

const ERROR = 'error';

module.exports = function(err, req, res, next) {
if (process.env.NODE_ENV !== 'test') {
console.error(err);
}
res.status(500).json({ status: ERROR, message: err.message });
}
4 changes: 0 additions & 4 deletions server/models/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,12 @@ db.defaults({ data: [] }).write();

let database = {
getData: function() {
console.log("Getting data from database...");
return db.get("data").value();
},
getDataById: function(id) {
console.log("Getting data by id from database...");
return db.get("data").find({"id": id}).value();
},
saveData: function(data, pluginType, machineId) {
console.log("Writing data to database...");

if(database.machineExists(machineId)) {
db.get("data")
.find({ id: machineId })
Expand Down
Loading