Skip to content
This repository has been archived by the owner on Apr 17, 2023. It is now read-only.

Commit

Permalink
Unit tests madness
Browse files Browse the repository at this point in the history
  • Loading branch information
wtrocki committed Aug 11, 2017
1 parent 8cfaf10 commit de0fba1
Show file tree
Hide file tree
Showing 15 changed files with 384 additions and 103 deletions.
20 changes: 19 additions & 1 deletion cloud/wfm-rest-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Supports pagination and sorting by providing additional query parameters:
- `size` number of elements to return
- `sortField` sorting field
- `order` -1 for DESC and 1 for ASC
`

Example `/workorders?page=0&size=5&sortField=id&order=-1`

> **Note** - sorting parameters are optional. When missing default sorting values are applied (10 results)
Expand Down Expand Up @@ -88,3 +88,21 @@ Example `/workorders/B1r71fOBr`
### Delete object

> DELETE {object}/:objectId
### Error handling

Api returns non 200 status in case of error.

`400` - For user input error (missing required field etc.)
`500` - For internal server errors

Additionaly error metadata is being returned:

```json
{
"code":"InvalidID",
"message":"Provided id is invalid"
}
```

> **Note:** If you apply security middleware additional `401` and `403` statuses may be returned
9 changes: 7 additions & 2 deletions cloud/wfm-rest-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
"report-dir": "coverage_report",
"check-coverage": true,
"lines": 75,
"functions": 100,
"branches": 80
"functions": 80,
"branches": 70
},
"dependencies": {
"@raincatcher/logger": "1.0.0",
Expand All @@ -44,11 +44,16 @@
"@types/express": "^4.0.35",
"@types/lodash": "^4.14.72",
"@types/mocha": "^2.2.41",
"@types/mongodb": "^2.2.9",
"@types/proxyquire": "^1.3.27",
"@types/sinon": "^2.3.3",
"@types/sinon-express-mock": "^1.3.2",
"del-cli": "^1.0.0",
"mocha": "^3.4.2",
"nyc": "^11.0.1",
"proxyquire": "^1.8.0",
"sinon": "^3.2.0",
"sinon-express-mock": "^1.3.1",
"source-map-support": "^0.4.15",
"ts-node": "^3.0.4",
"typescript": "^2.3.4"
Expand Down
3 changes: 3 additions & 0 deletions cloud/wfm-rest-api/src/WfmRestApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ export class WfmRestApi {
public createWFMRouter() {
const router: express.Router = express.Router();
const workorderController = new ApiController(router, this.workorderService, this.config.workorderApiName);
workorderController.applyAllRoutes();
const workflowController = new ApiController(router, this.workflowService, this.config.workflowApiName);
workflowController.applyAllRoutes();
const resultController = new ApiController(router, this.resultService, this.config.resultApiName);
resultController.applyAllRoutes();
return router;
}

Expand Down
4 changes: 2 additions & 2 deletions cloud/wfm-rest-api/src/data-api/MongoPaginationEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { DIRECTION, SortedPageRequest } from '../data-api/PageRequest';
import { PageResponse } from '../data-api/PageResponse';

/**
* Mongo pagination procesor
* Mongo pagination processor
* Clients may override this class to provide custom pagination parameters
*
* Note: pages are counted starting from 0.
Expand Down Expand Up @@ -66,7 +66,7 @@ export class MongoPaginationEngine {
cursor = cursor.sort(request.sortField, request.order);
}
cursor = cursor.skip(request.size * request.page).limit(request.size);
return cursor.toArray().then(function(data) {
return Promise.resolve(cursor.toArray()).then(function(data) {
const totalPages = Math.ceil(totalCount / request.size);
return {
totalPages,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { PageResponse } from '../data-api/PageResponse';
* Interface is being used internally to perform database operations for WFM models
* It's not recomended to use it for other application business logic.
*/
export interface CrudRepository {
export interface PagingDataRepository {

/**
* Retrieve list of results from database
Expand Down
195 changes: 113 additions & 82 deletions cloud/wfm-rest-api/src/impl/ApiController.ts
Original file line number Diff line number Diff line change
@@ -1,118 +1,149 @@
import { getLogger } from '@raincatcher/logger';
import * as express from 'express';
import { ApiError } from '../data-api/ApiError';
import { CrudRepository } from '../data-api/CrudRepository';
import { defaultPaginationEngine } from '../data-api/MongoPaginationEngine';
import { PagingDataRepository } from '../data-api/PagingDataRepository';
import * as errorCodes from './ErrorCodes';

/**
* Generic controller that can be used to create API for specific objects
*/
export class ApiController {
constructor(router: express.Router, repository: CrudRepository, readonly apiPrefix: string) {
getLogger().info('REST api initialization', apiPrefix);
this.buildRoutes(router, repository, apiPrefix);
constructor(readonly router: express.Router, readonly repository: PagingDataRepository, readonly apiPrefix: string) {
}

/**
* Build routes for specific element of api
*
* @param router - router used to attach api
* @param repository - repository to retrieve data
* @param apiPrefix - prefix to mount api in URI path. For example `/prefix/:id`
* Handler for list method
* Can be reused by developers that wish to mount handler directly on router
*/
protected buildRoutes(router: express.Router, repository: CrudRepository, apiPrefix: string) {
public listHandler(req: express.Request, res: express.Response) {
const self = this;
const idRoute = router.route('/' + apiPrefix + '/:id');
const objectRoute = router.route('/' + apiPrefix + '/');

objectRoute.get(function(req: express.Request, res: express.Response) {
getLogger().debug('Api list method called',
{ object: apiPrefix, body: req.query });
const page = defaultPaginationEngine.buildRequestFromQuery(req.query);
let filter = {};
if (req.query.filter) {
try {
filter = JSON.parse(req.query.filter);
} catch (err) {
getLogger().debug('Invalid filter passed');
}
}
if (req.body.filter) {
filter = req.body.filter;
getLogger().debug('Api list method called',
{ object: self.apiPrefix, body: req.query });
const page = defaultPaginationEngine.buildRequestFromQuery(req.query);
let filter = {};
if (req.query.filter) {
try {
filter = JSON.parse(req.query.filter);
} catch (err) {
getLogger().debug('Invalid filter passed');
const error: ApiError = { code: errorCodes.CLIENT_ERROR, message: 'Invalid filter query parameter' };
return res.status(400).json(error);
}
const objectList = repository.list(filter, page).then(function(data) {
res.send(data);
}).catch(function(err: ApiError) {
self.errorHandler(req, res, err);
});
}
if (req.body.filter) {
filter = req.body.filter;
}
const objectList = self.repository.list(filter, page).then(function(data) {
res.json(data);
}).catch(function(err: ApiError) {
self.errorHandler(req, res, err);
});
}

objectRoute.post(function(req: express.Request, res: express.Response) {
getLogger().debug('Api create method called',
{ object: apiPrefix, body: req.body });
/**
* Handler for get method
* Can be reused by developers that wish to mount handler directly on router
*/
public getHandler(req: express.Request, res: express.Response) {
const self = this;
getLogger().debug('Api get method called',
{ object: self.apiPrefix, params: req.params });

if (!req.body) {
const error: ApiError = { code: errorCodes.CLIENT_ERROR, message: 'Missing request body' };
return res.status(400).json(error);
}
if (!req.params.id) {
const error: ApiError = { code: errorCodes.MISSING_ID, message: 'Missing id parameter' };
return res.status(400).json(error);
}

repository.create(req.body).then(function() {
res.send();
}).catch(function(err: ApiError) {
self.errorHandler(req, res, err);
});
self.repository.get(req.params.id).then(function(data) {
res.json(data);
}).catch(function(err: ApiError) {
self.errorHandler(req, res, err);
});
}

objectRoute.put(function(req: express.Request, res: express.Response) {
getLogger().debug('Api update method called',
{ object: apiPrefix, body: req.body });
/**
* Handler for create method
* Can be reused by developers that wish to mount handler directly on router
*/
public postHandler(req: express.Request, res: express.Response) {
const self = this;
getLogger().debug('Api create method called',
{ object: self.apiPrefix, body: req.body });

if (!req.body) {
const error = { code: errorCodes.CLIENT_ERROR, message: 'Missing request body' };
return res.status(400).json(error);
}
if (!req.body) {
const error: ApiError = { code: errorCodes.CLIENT_ERROR, message: 'Missing request body' };
return res.status(400).json(error);
}

repository.update(req.body).then(function(data) {
res.send();
}).catch(function(err: ApiError) {
getLogger().error('Update error', { err: err.message, obj: req.body });
const error: ApiError = { code: errorCodes.DB_ERROR, message: 'Failed to update object' };
res.status(500).json(error);
});
self.repository.create(req.body).then(function(data) {
res.json(data);
}).catch(function(err: ApiError) {
self.errorHandler(req, res, err);
});
}

idRoute.get(function(req: express.Request, res: express.Response) {
getLogger().debug('Api get method called',
{ object: apiPrefix, params: req.params });
/**
* Delete handler
* Can be reused by developers that wish to mount handler directly on router
*/
public deleteHandler(req: express.Request, res: express.Response) {
const self = this;
getLogger().debug('Api delete method called',
{ object: self.apiPrefix, params: req.params });

if (!req.params.id) {
const error: ApiError = { code: 'MissingId', message: 'Missing id parameter' };
return res.status(400).json(error);
}
if (!req.params.id) {
const error: ApiError = { code: errorCodes.MISSING_ID, message: 'Missing id parameter' };
return res.status(400).json(error);
}

repository.get(req.params.id).then(function(data) {
res.send(data);
}).catch(function(err: ApiError) {
self.errorHandler(req, res, err);
});
self.repository.delete(req.params.id).then(function(data) {
res.json();
}).catch(function(err) {
self.errorHandler(req, res, err);
});
}

idRoute.delete(function(req: express.Request, res: express.Response) {
getLogger().debug('Api delete method called',
{ object: apiPrefix, params: req.params });
/**
* Update handler
* Can be reused by developers that wish to mount handler directly on router
*/
public putHandler(req: express.Request, res: express.Response) {
const self = this;
getLogger().debug('Api update method called',
{ object: self.apiPrefix, body: req.body });

if (!req.params.id) {
const error: ApiError = { code: 'MissingId', message: 'Missing id parameter' };
return res.status(400).json(error);
}
if (!req.body) {
const error = { code: errorCodes.CLIENT_ERROR, message: 'Missing request body' };
return res.status(400).json(error);
}

repository.delete(req.params.id).then(function(data) {
res.send(data);
}).catch(function(err) {
self.errorHandler(req, res, err);
});
self.repository.update(req.body).then(function(data) {
res.json(data);
}).catch(function(err: ApiError) {
self.errorHandler(req, res, err);
});
}

/**
* Build all CRUD routes for `apiPrefix`
*
* @param router - router used to attach api
* @param repository - repository to retrieve data
* @param apiPrefix - prefix to mount api in URI path. For example `/prefix/:id`
*/
public applyAllRoutes() {
const self = this;
const idRoute = this.router.route('/' + this.apiPrefix + '/:id');
const objectRoute = this.router.route('/' + this.apiPrefix + '/');
getLogger().info('REST api initialization', this.apiPrefix);

objectRoute.get(this.listHandler.bind(this));
objectRoute.post(this.postHandler.bind(this));
objectRoute.put(this.putHandler.bind(this));
idRoute.get(this.getHandler.bind(this));
idRoute.delete(this.deleteHandler.bind(this));
}
protected errorHandler(req: express.Request, res: express.Response, error: ApiError) {
getLogger().error('Api error', { error, obj: req.body });
res.status(500).json(error);
Expand Down
1 change: 1 addition & 0 deletions cloud/wfm-rest-api/src/impl/ErrorCodes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

export const CLIENT_ERROR = 'CLIENT_ARGUMENT_ERROR';
export const MISSING_ID = 'MISSING_ID';
export const DB_ERROR = 'DB_ERROR';
13 changes: 9 additions & 4 deletions cloud/wfm-rest-api/src/impl/MongoDbRepository.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import * as Promise from 'bluebird';
import { Cursor, Db } from 'mongodb';
import { ApiError } from '../data-api/ApiError';
import { CrudRepository } from '../data-api/CrudRepository';
import { defaultPaginationEngine } from '../data-api/MongoPaginationEngine';
import { DIRECTION, SortedPageRequest } from '../data-api/PageRequest';
import { PageResponse } from '../data-api/PageResponse';
import { PagingDataRepository } from '../data-api/PagingDataRepository';
import * as errorCodes from './ErrorCodes';

const dbError: ApiError = { code: errorCodes.DB_ERROR, message: 'MongoDbRepository database not intialized' };

/**
* Service for performing data operations on mongodb database
*/
export class MongoDbRepository implements CrudRepository {
export class MongoDbRepository implements PagingDataRepository {

public db: any;

Expand All @@ -28,8 +28,13 @@ export class MongoDbRepository implements CrudRepository {
return Promise.reject(dbError);
}
const cursor: Cursor = this.db.collection(this.collectionName).find(filter);
return cursor.count(filter).then(function(totalNumber) {
return defaultPaginationEngine.buildPageResponse(request, cursor, totalNumber);
return new Promise((resolve, reject) => {
cursor.count(filter, function(err, totalNumber) {
if (err) {
return reject(err);
}
return resolve(defaultPaginationEngine.buildPageResponse(request, cursor, totalNumber));
});
});
}

Expand Down
3 changes: 1 addition & 2 deletions cloud/wfm-rest-api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

// WFM implementation
export * from './WfmRestApi';

Expand All @@ -7,7 +6,7 @@ export * from './impl/ApiController';
export * from './impl/MongoDbRepository';

// API
export * from './data-api/CrudRepository';
export * from './data-api/PagingDataRepository';
export * from './data-api/PageRequest';
export * from './data-api/PageResponse';
export * from './data-api/ApiError';
Expand Down

0 comments on commit de0fba1

Please sign in to comment.