Skip to content

Commit 06d342f

Browse files
authored
upgrade hapi to v17, add Boom, add test for route validation (#22)
* upgrade hapi to v17, add Boom, add test for route validation
1 parent 3092cbc commit 06d342f

File tree

10 files changed

+1495
-111
lines changed

10 files changed

+1495
-111
lines changed

.eslintignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
node_modules/*
1+
node_modules/*

.eslintrc.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
{
2-
"extends": "hapi"
2+
"extends": "hapi",
3+
"parserOptions": {
4+
"ecmaVersion": 2017
5+
}
36
}

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
language: node_js
22
node_js:
3-
- "5.2"
3+
- "8.9.0"

README.md

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44

55
Example Hapi-backed API Server with testing, CI, and Swagger documentation generator.
66

7-
Updated and tested with latest hapi packages as of 8/4/2016.
7+
Updated and tested with latest hapi packages as of 1/16/2018.
88

99
## How to run
10-
Requires Node v5.2.0+
10+
Requires Node v8.9.0+
1111
```sh
12-
npm install #install dependencies
13-
npm start # start server
14-
npm test # run tests
12+
yarn install #install dependencies
13+
yarn start # start server
14+
yarn test # run tests
1515
```
1616

1717
## Details
@@ -35,15 +35,13 @@ Requires Node v5.2.0+
3535
- Code Coverage
3636
- `lab` analyzes the code and returns the code coverage ratio when running the test. It also points out which lines of code are missing coverage. A nice reminder to write tests for any newly added functionality.
3737
- Documentation
38-
- `hapi-swagger` is configured in `app.js` and generates a very nice html page with an interactive Swagger compatible API.
38+
- `hapi-swagger` is configured in `app.js` and generates a very nice html page with an interactive Swagger compatible API.
3939
- Once your server is running locally, visit [http://localhost:3000](http://localhost:3000) to check out the docs.
40-
- My typical workflow is to write the documentation first (by setting up the hapi routing #2BirdsWith1Stone), then to write the functional tests, then a combination of code and unit tests ala [TDD](http://www.jamesshore.com/Blog/Red-Green-Refactor.html) until I'm satisfied with the results.
40+
- My typical workflow is to write the documentation first (by setting up the hapi routing #2BirdsWith1Stone), then to write the functional tests, then a combination of code and unit tests ala [TDD](http://www.jamesshore.com/Blog/Red-Green-Refactor.html) until I'm satisfied with the results.
4141

4242
- CI
4343
- This repo also integrates [TravisCI](https://travis-ci.org/), which runs the tests defined above on every pull request, blocking a merge if the test does not pass. Not very useful for a one person project, but crucial when a team of developers is involved.
4444

4545
## TODO
4646
* add stubbing framework to imitate external service calls.
4747
* Figure out an easy way to test multiple hapi services together, in a microservice environment.
48-
* Integrate `boom` for API error responses.
49-
* Add example for testing hapi route validation (6/17/2017)

app.js

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,22 @@ const Pack = require('./package.json');
88
const Fs = require('fs');
99
const _ = require('lodash');
1010

11-
const server = new Hapi.Server();
12-
server.connection({
11+
const server = new Hapi.Server({
1312
host: 'localhost',
14-
port: process.env.PORT || 3000
13+
port: process.env.PORT
1514
});
1615

17-
// register server components. We use swagger to generate service documentation
18-
server.register([
19-
Inert,
20-
Vision,
21-
{
22-
'register': HapiSwagger,
23-
'options': {
16+
(async () => {
17+
18+
const HapiSwaggerConfig = {
19+
plugin: HapiSwagger,
20+
options: {
2421
info: {
2522
title: Pack.name,
2623
description: Pack.description,
2724
version: Pack.version
2825
},
29-
enableDocumentation: true,
26+
swaggerUI: true,
3027
basePath: '/',
3128
pathPrefixSize: 2,
3229
jsonPath: '/docs/swagger.json',
@@ -36,14 +33,16 @@ server.register([
3633
{ name: 'api' }
3734
],
3835
documentationPath: '/',
39-
securityDefinitions: []
36+
securityDefinitions: {}
4037
}
41-
}],
42-
(err) => {
38+
};
4339

44-
if (err) {
45-
throw err;
46-
}
40+
/* register plugins */
41+
await server.register([
42+
Inert,
43+
Vision,
44+
HapiSwaggerConfig
45+
]);
4746

4847
// require routes
4948
Fs.readdirSync('routes').forEach((file) => {
@@ -54,14 +53,9 @@ server.register([
5453
});
5554
});
5655

57-
server.start((err) => {
58-
59-
if (err) {
60-
console.log(err);
61-
}
56+
await server.start();
6257

63-
console.log('Server running at:', server.info.uri);
64-
});
65-
});
58+
console.log('Server running at:', server.info.uri);
59+
})();
6660

6761
module.exports = server;

handlers/products.js

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict';
22

3-
const Promise = require('promise');
3+
const Boom = require('boom');
44
const _ = require('lodash');
55

66
// handlers are exported back for use in hapi routes
@@ -22,25 +22,27 @@ const ProductDatabase = [
2222
];
2323

2424
// a unit test-able function
25-
Lib.getProducts = function getProducts(id) {
25+
Lib.getProducts = async (id) => {
2626

27-
return new Promise((resolve, reject) => {
27+
if (id) {
28+
const product = await _.find(ProductDatabase, (p) => {
2829

29-
// if id passed, fetch single item
30-
if (id) {
31-
return resolve(_.find(ProductDatabase, (p) => {
30+
return p.id === id;
31+
});
3232

33-
return p.id === id;
34-
}));
33+
if (!product) {
34+
return null;
3535
}
36-
// in other cases fetch all items
37-
resolve(ProductDatabase);
38-
});
36+
37+
return product;
38+
}
39+
40+
return ProductDatabase;
3941
};
4042

4143
// hapi route handler
4244
// only this function can call reply
43-
Handlers.get = function get(req, reply) {
45+
Handlers.get = async (req, reply) => {
4446
//
4547
// Perform req processing & conversions for input here.
4648
//
@@ -50,18 +52,16 @@ Handlers.get = function get(req, reply) {
5052
id = req.params.id;
5153
}
5254

53-
// call business function
54-
Lib.getProducts(id).done((products) => {
55-
// api success
56-
reply({ result: products }).code(200);
57-
}, (err) => {
58-
// api error
59-
reply(err).code(400);
60-
});
55+
const products = await Lib.getProducts(id);
56+
57+
if (!products) {
58+
return Boom.notFound();
59+
}
60+
61+
return { result: products };
6162
};
6263

6364
module.exports = {
6465
handlers: Handlers,
65-
// we only export lib for tests
66-
lib: (process.env.NODE_ENV === 'test') ? Lib : null
66+
lib: Lib
6767
};

package.json

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
22
"name": "testing-hapi",
3-
"version": "1.0.0",
3+
"version": "2.0.0",
44
"description": "Example hapi-backed API serivce with testing and CI.",
55
"main": "app.js",
66
"scripts": {
77
"start": "node app.js",
8-
"test": "NODE_ENV=test node node_modules/lab/bin/lab -v -L -C -D -c -t 85",
8+
"test": "NODE_ENV=test node node_modules/lab/bin/lab -v -L -C -c -t 85",
99
"test-cov-html": "lab -r html -o coverage.html"
1010
},
1111
"repository": {
@@ -14,6 +14,7 @@
1414
},
1515
"keywords": [
1616
"hapi",
17+
"hapi-v17",
1718
"tests",
1819
"lab",
1920
"ci",
@@ -26,15 +27,18 @@
2627
},
2728
"homepage": "https://github.com/pashariger/testing-hapi#readme",
2829
"dependencies": {
29-
"hapi": "^14.1.0",
30-
"hapi-swagger": "^6.2.1",
31-
"inert": "^4.0.1",
32-
"joi": "^9.0.4",
33-
"promise": "^7.1.1",
34-
"vision": "^4.1.0"
30+
"boom": "^7.1.1",
31+
"hapi": "^17.2.0",
32+
"hapi-swagger": "^9.1.1",
33+
"inert": "^5.1.0",
34+
"joi": "^13.1.2",
35+
"promise": "^8.0.1",
36+
"vision": "^5.3.1"
3537
},
3638
"devDependencies": {
37-
"code": "^3.0.1",
38-
"lab": "^10.9.0"
39+
"code": "^5.2.0",
40+
"eslint-config-hapi": "^11.1.0",
41+
"eslint-plugin-hapi": "^4.1.0",
42+
"lab": "^15.2.2"
3943
}
4044
}

test/functional/products.js

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,58 +17,68 @@ const Server = require('../../app.js');
1717
// tests
1818
describe('functional tests - products', () => {
1919

20-
it('should get products', (done) => {
20+
it('should get products', async () => {
2121

2222
// make API call to self to test functionality end-to-end
23-
Server.inject({
23+
const response = await Server.inject({
2424
method: 'GET',
2525
url: '/api/products'
26-
}, (response) => {
27-
28-
expect(response.statusCode).to.equal(200);
29-
expect(response.result.result).to.have.length(2);
30-
done();
3126
});
27+
28+
expect(response.statusCode).to.equal(200);
29+
expect(response.result.result).to.have.length(2);
3230
});
3331

34-
it('should get single product', (done) => {
32+
it('should get single product', async () => {
3533

36-
Server.inject({
34+
const response = await Server.inject({
3735
method: 'GET',
3836
url: '/api/products/1'
39-
}, (response) => {
37+
});
38+
39+
expect(response.statusCode).to.equal(200);
40+
});
41+
42+
it('should return error for invalid id', async () => {
4043

41-
expect(response.statusCode).to.equal(200);
42-
done();
44+
const response = await Server.inject({
45+
method: 'GET',
46+
url: '/api/products/5'
4347
});
48+
49+
expect(response.statusCode).to.equal(404);
4450
});
4551

46-
after((done) => {
52+
it('should return error for invalid id format (validation test)', async () => {
4753

54+
const response = await Server.inject({
55+
method: 'GET',
56+
url: '/api/products/INVLAID_ID_FORMAT'
57+
});
58+
59+
expect(response.statusCode).to.equal(400);
60+
});
61+
62+
after(async () => {
4863
// placeholder to do something post tests
49-
done();
5064
});
5165
});
5266

5367
describe('functional tests - get documentation', () => {
5468

55-
it('should return documentation html', (done) => {
69+
it('should return documentation html', async () => {
5670

5771
// make API call to self to test functionality end-to-end
58-
Server.inject({
72+
const response = await Server.inject({
5973
method: 'GET',
6074
url: '/'
61-
}, (response) => {
62-
63-
expect(response.statusCode).to.equal(200);
64-
expect(response.result).to.be.a.string();
65-
done();
6675
});
67-
});
6876

69-
after((done) => {
77+
expect(response.statusCode).to.equal(200);
78+
expect(response.result).to.be.a.string();
79+
});
7080

81+
after(async () => {
7182
// placeholder to do something post tests
72-
done();
7383
});
7484
});

test/unit/products.js

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,18 @@ const ProductHandlers = require('../../handlers/products');
1515

1616
describe('unit tests - products', () => {
1717

18-
it('should return all products', (done) => {
18+
it('should return all products', async () => {
1919

2020
// test lib function
21-
ProductHandlers.lib.getProducts().done((products) => {
21+
const result = await ProductHandlers.lib.getProducts();
2222

23-
expect(products).to.be.an.array().and.have.length(2);
24-
25-
done();
26-
}, (err) => {
27-
28-
done(err);
29-
});
23+
expect(result).to.be.an.array().and.have.length(2);
3024
});
3125

32-
it('should return single product', (done) => {
33-
34-
ProductHandlers.lib.getProducts(1).done((product) => {
35-
36-
expect(product).to.be.an.object();
26+
it('should return single product', async () => {
3727

38-
done();
39-
}, (err) => {
28+
const result = await ProductHandlers.lib.getProducts(1);
4029

41-
done(err);
42-
});
30+
expect(result).to.be.an.object();
4331
});
4432
});

0 commit comments

Comments
 (0)