Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/api tests #68

Open
wants to merge 44 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
e460fd4
Implement a first REST API test using the Jest framework.
JMLX42 Oct 3, 2016
73709a7
Add missing 'superagent' dev dependency.
JMLX42 Oct 3, 2016
409382f
Bootstrap the tests for the /vote API endpoints.
JMLX42 Oct 3, 2016
ba4130d
Make a dummy test fail to make sure it will result in a failed Travis…
JMLX42 Oct 3, 2016
025579e
Fix dummy test.
JMLX42 Oct 3, 2016
bed18a7
Add the /script/add-app.js script to create an App using the command …
JMLX42 Oct 5, 2016
42e398e
Create a test App during the provisioning of the "local" machine.
JMLX42 Oct 5, 2016
5a07ca9
Make the test App settings available in the API config.json file.
JMLX42 Oct 5, 2016
58803d2
Add tests for the JWT authentication on /api/user.
JMLX42 Oct 5, 2016
6c29681
Add tests for the POST /api/oauth API endpoint.
JMLX42 Oct 5, 2016
94c2e66
Fix API config.json generation to be done in two steps.
JMLX42 Oct 5, 2016
edc5b29
Dump the database before each test suite, and restore it after each t…
JMLX42 Oct 6, 2016
da12b9b
Add more tests for /api/vote.
JMLX42 Oct 6, 2016
f0bc7c3
Fix the blockchain-miner upstart to properly exec testrpc.
JMLX42 Oct 8, 2016
036d061
Update the "local" machine conf to use testrpc when the provider is "…
JMLX42 Oct 8, 2016
653d45e
Add tests for Vote.voteContractAddress and Vote.voteContractABI.
JMLX42 Oct 8, 2016
2da9274
Fix empty /api/npm-shrinkwrap.json.
JMLX42 Oct 8, 2016
9969425
Fix broken comparison in local.yml.
JMLX42 Oct 8, 2016
a32631f
Add the /ping API endpoint and the corresponding test.
JMLX42 Oct 8, 2016
2e7e430
Update run-tests.sh to show the relevant logs when the API tests fail.
JMLX42 Oct 8, 2016
e154f4c
Set +e in run-tests.sh.
JMLX42 Oct 8, 2016
caa924d
Fix Vote.schema.pre('validate') to handle the metafetch error.
JMLX42 Oct 8, 2016
0a2b919
Refactor all API tests to use a common getAPIURL() function.
JMLX42 Oct 8, 2016
3497c97
Fix run-tests.sh to have return code != 0 when an error occurred.
JMLX42 Oct 8, 2016
b68c4bd
Use bunyan to the API logs.
JMLX42 Oct 8, 2016
eeea380
Add JSON pretty-printing for the test logs.
JMLX42 Oct 8, 2016
a06e403
Add JSON logs for the API.
JMLX42 Oct 8, 2016
551d0e1
Add the --verbose option for run-tests.sh.
JMLX42 Oct 8, 2016
e793b57
Update npm-shrinkwrap.json for /api.
JMLX42 Oct 8, 2016
4ef09f5
Fix Vote.pre('validate') to instanciate an actual Error object.
JMLX42 Oct 8, 2016
16d8dc4
Use meetup.com the API test URL.
JMLX42 Oct 8, 2016
1aa772e
Disable JSON pretty printing in run-tests.sh.
JMLX42 Oct 8, 2016
caed7c7
Fix development blockchain upstart scripts to read the private key fr…
JMLX42 Oct 8, 2016
5072641
Fix vote.create() to respond with an {error} object.
JMLX42 Oct 8, 2016
aa58965
Bootstrap the /ballot API endpoint tests.
JMLX42 Oct 10, 2016
88486d1
Mark the "unknown ballot" error as "no retry".
JMLX42 Oct 10, 2016
58796e7
Reset the queues and restart the blockchain miner after a test suite.
JMLX42 Oct 10, 2016
fbdc588
Add a test to make sure a user cannot vote twice.
JMLX42 Oct 10, 2016
4deb061
Add support for --gasLimit when running testrpc.
JMLX42 Oct 10, 2016
6c72767
Merge remote-tracking branch 'origin/master' into feature/api-tests
JMLX42 Oct 15, 2016
945c4d8
Fix broken reference to voteConsumer.run.
JMLX42 Oct 16, 2016
5f7bb9f
Set vote_consumer_count to 2.
JMLX42 Oct 16, 2016
84d62f1
Fix vote and ballot tests.
JMLX42 Oct 18, 2016
e2f00f4
Raise the POST /vote test timeout delay to 15sec to avoid false negat…
JMLX42 Oct 18, 2016
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 .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ env:
script:
- sudo ln -s $TRAVIS_BUILD_DIR /vagrant
- cd /vagrant && sudo bash ./deployment/ansible.sh
- sudo ./script/run-tests.sh
- sudo ./script/run-tests.sh --verbose

deploy:
provider: script
Expand Down
1 change: 1 addition & 0 deletions api/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ config.json
*.tmp

dist
jest
4,363 changes: 2,249 additions & 2,114 deletions api/npm-shrinkwrap.json

Large diffs are not rendered by default.

29 changes: 26 additions & 3 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
"babel-cli": "^6.14.0",
"babel-preset-es2015": "^6.14.0",
"bcrypt": "0.8.5",
"bunyan": "^1.8.1",
"client-sessions": "0.7.0",
"ethereumjs-tx": "^1.1.1",
"ethereumjs-util": "^4.0.1",
"express-bunyan-logger": "^1.3.1",
"follow-redirects": "0.2.0",
"html-entities": "1.2.0",
"js-beautify": "1.6.2",
Expand Down Expand Up @@ -48,14 +50,35 @@
"node": ">=0.10.22",
"npm": ">=1.3.14"
},
"babel": {
"presets": [
"es2015"
],
"plugins": [
"transform-async-to-generator"
]
},
"scripts": {
"build": "rm -rf dist ; babel src --out-dir dist --source-maps inline --presets es2015",
"build": "rm -rf dist ; babel src --out-dir dist --source-maps inline",
"start": "service cocorico-api-web stop ; nodemon dist/index.js -w src -d 5 --exec 'npm run build && nodejs'",
"doc": "apidoc -i ./src/routes/ -o ../app/public/documentation -c .",
"test": "eslint src scripts"
"test": "eslint src scripts && jest test"
},
"devDependencies": {
"babel-eslint": "^6.1.2",
"eslint": "^3.6.0"
"babel-jest": "^16.0.0",
"babel-plugin-transform-async-to-generator": "^6.16.0",
"babel-polyfill": "^6.16.0",
"eslint": "^3.6.0",
"eth-lightwallet": "^2.5.2",
"jest": "^16.0.0",
"promise": "^7.1.1",
"superagent": "^2.3.0",
"superagent-promise": "^1.1.0",
"timeout-as-promise": "^1.0.0"
},
"jest": {
"setupTestFrameworkScriptFile": "test/index.js",
"verbose": true
}
}
2 changes: 2 additions & 0 deletions api/scripts/add-admin.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#!/usr/bin/env headstone

var keystone = require('keystone');
var Admin = keystone.list('Admin');

Expand Down
30 changes: 30 additions & 0 deletions api/scripts/add-app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env headstone

var keystone = require('keystone');
var App = keystone.list('App');

module.exports = function(title, secret, url, done) {
App.model.findOne({title: title})
.exec((err, app) => {
if (!app) {
var newApp = new App.model({
title: title,
secret: secret,
validURLs: [url],
});

return newApp.save((saveErr, savedApp) => {
if (saveErr) {
console.log(saveErr);
return done(saveErr);
}

console.log('App created with ID ' + savedApp.id + '.');
return done();
});
} else {
console.log('App already exists with ID ' + app.id + '.');
return done();
}
});
};
1 change: 1 addition & 0 deletions api/scripts/import-pages.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env headstone

var async = require('async');
var keystone = require('keystone');
var fs = require('fs');
Expand Down
5 changes: 5 additions & 0 deletions api/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ var config = require('/opt/cocorico/api-web/config.json');
var keystone = require('keystone');
var srs = require('secure-random-string');

var log = require('bunyan').createLogger({name: 'api-web'});

keystone.init({

'name': 'cocorico',
Expand All @@ -27,6 +29,8 @@ keystone.init({
'auth': true,
'user model': 'Admin',
'cookie secret': srs(64),

'logger': false,
});

keystone.import('models');
Expand All @@ -46,3 +50,4 @@ keystone.set('nav', {
});

keystone.start();
log.info('started');
5 changes: 5 additions & 0 deletions api/src/models/Vote.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ Vote.schema.pre('validate', function(next) {
http: { timeout: 30000 },
},
(err, meta) => {
if (err) {
callback(new Error(err));
return;
}

if (updateTitle) {
self.title = meta.title;
}
Expand Down
1 change: 1 addition & 0 deletions api/src/routes/api/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ opts.jwtFromRequest = ExtractJwt.fromAuthHeader();
// secretOrKey is not really important since we will set it dynamically according
// to the "Cocorico-App-Id" HTTP header. But it still has to be != false.
opts.secretOrKey = 'secret';
opts.ignoreExpiration = false;

// JwtStrategy reads the JWT secret from the option object above. But
// we need the secret to be the one set for the corresponding App.
Expand Down
2 changes: 1 addition & 1 deletion api/src/routes/api/ballot.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ exports.vote = function(req, res) {
res.status(err.code);
}
if (err.error) {
return res.apiError(err.error);
return res.apiResponse({error: err.error});
}
return res.apiError(err);
}
Expand Down
12 changes: 4 additions & 8 deletions api/src/routes/api/vote.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,12 @@ exports.getBySlug = function(req, res) {
}

exports.create = function(req, res) {
var app = req.user;

var url = decodeURIComponent(req.body.url);
if (!url) {
if (!req.body.url) {
return res.status(400).send({error: 'missing url'});
}

var app = req.user;
var url = decodeURIComponent(req.body.url);
var labels = [];

if (req.body.labels) {
Expand Down Expand Up @@ -92,10 +91,7 @@ exports.create = function(req, res) {
if (err.code) {
res.status(err.code);
}
if (err.error) {
return res.apiError(err.error);
}
return res.apiError(err);
return res.apiResponse({error : err});
}
return res.apiResponse({vote: vote});
}
Expand Down
32 changes: 29 additions & 3 deletions api/src/routes/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
var config = require('/opt/cocorico/api-web/config.json');

var keystone = require('keystone');
var passport = require('passport');

var config = require('/opt/cocorico/api-web/config.json');
var log = require('bunyan').createLogger({name: 'api-web'});

var importRoutes = keystone.importer(__dirname);

var routes = {
api: importRoutes('./api'),
};

log.info('imported routes');

function isAuthenticated(req, res, next) {
if (!req.isAuthenticated() || !req.user.sub)
return res.status(401).apiResponse({error: 'not authenticated'});
Expand All @@ -18,22 +22,40 @@ function isAuthenticated(req, res, next) {
// Setup Route Bindings
exports = module.exports = function(app) {

log.info('initialize passport');
app.use(passport.initialize());
app.use(passport.session());
log.info('initialized passport');

var excludes = config.env !== 'development'
? ['req', 'res', 'res-headers', 'req-headers']
: [];
app.use(require('express-bunyan-logger')({
name: 'api-web',
format: ':remote-address :incoming :method :url HTTP/:http-version :status-code :referer :user-agent[family] :user-agent[major].:user-agent[minor] :user-agent[os] :response-time ms',
excludes: excludes,
}));
app.use(require('express-bunyan-logger').errorLogger({
name: 'api-web',
format: ':remote-address :incoming :method :url HTTP/:http-version :status-code :referer :user-agent[family] :user-agent[major].:user-agent[minor] :user-agent[os] :response-time ms',
excludes: excludes,
}));

log.info('setup keystone middleware');
app.use(keystone.middleware.api);

// JWT authentication does not use sessions, so we have to check for a user
// without throwing an error if there is none.
log.info('setup JWT authentication middleware');
app.use((req, res, next) => passport.authenticate('jwt', (err, user, info) => {
if (err) {
return next(err);
}
if (!user) {
// if the JWT authentification failed
// if the JWT authentication failed
if (info) {
return res.status(401).apiResponse({
error: 'authentification failed',
error: 'authentication failed',
message: info.message,
});
}
Expand All @@ -43,6 +65,8 @@ exports = module.exports = function(app) {
return req.logIn(user, { session: false }, next);
})(req, res, next));

log.info('setup routes');

/**
* @apiDefine user A user that has been properly logged in using any of the `/auth` endpoints.
*/
Expand All @@ -51,6 +75,8 @@ exports = module.exports = function(app) {
* @apiDefine app A registered 3rd party app providing a valid OAuth token fetched using `/oauth/token`.
*/

app.get('/ping', (req, res) => res.apiResponse('pong'));

/**
* @api {post} /oauth/token Get an OAuth access token
* @apiName GetOAuthToken
Expand Down
92 changes: 92 additions & 0 deletions api/test/ballot.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
var config = require('/opt/cocorico/api-web/config.json');

import request from './getRequest';
import getAPIURL from './getAPIURL';
import getVote from './getVote';
import getUserJWT from './getUserJWT';
import getBallotTransaction from './getBallotTransaction';

describe('POST /ballot/:voteId', () => {
it('returns 200 and a valid ballot', async () => {
const vote = await getVote(true);
const tx = await getBallotTransaction(vote, 0)
const res = await request
.post(getAPIURL('/ballot/' + vote.id))
.set('Authorization', 'JWT ' + getUserJWT())
.set('Cocorico-App-Id', config.testApp.id)
.send({'transaction': tx});

expect(res.status).toBe(200);
expect(res.body.ballot).not.toBeFalsy();
expect(res.body.ballot.id).not.toBeFalsy();
expect(res.body.ballot.hash).not.toBeFalsy();
expect(res.body.ballot.updatedAt).not.toBeFalsy();
expect(res.body.ballot.createdAt).not.toBeFalsy();
expect(res.body.ballot.status).toBe('queued');
expect(res.body.proof).not.toBeFalsy();
});

it('returns 403 and an error message when the user already voted', async () => {
const vote = await getVote(true);
const tx = await getBallotTransaction(vote, 0)

try {
const res = await request
.post(getAPIURL('/ballot/' + vote.id))
.set('Authorization', 'JWT ' + getUserJWT())
.set('Cocorico-App-Id', config.testApp.id)
.send({'transaction': tx});

expect(res).toBeFalsy();
} catch (err) {
expect(err.status).toBe(403);
expect(err.response.body.error).toBe('user already voted');
}
});

// it('returns 200 and a complete ballot', async () => {
// try {
// const vote = await getVote(true);
// const tx = await getBallotTransaction(vote, 0)
// const user = getUserJWT();
//
// await request
// .post(getAPIURL('/ballot/' + vote.id))
// .set('Authorization', 'JWT ' + user)
// .set('Cocorico-App-Id', config.testApp.id)
// .send({'transaction': tx});
// await delay(10000);
//
// const res = await request
// .get(getAPIURL('/ballot/' + vote.id))
// .set('Authorization', 'JWT ' + user)
// .set('Cocorico-App-Id', config.testApp.id);
//
// } catch (e) {
//
// console.log(e.response.body);
// }
// });
});

describe('GET /ballot/:voteId', () => {
it('returns 401 and an error message when not authenticated', async () => {
try {
await request.get(getAPIURL('/ballot/424242424242424242424242'))
} catch (err) {
expect(err.response.body).toEqual({error: 'not authenticated'});
expect(err.status).toBe(401);
}
});

it('returns 404 when :voteId is invalid', async () => {
try {
await request
.get(getAPIURL('/ballot/424242424242424242424242'))
.set('Authorization', 'JWT ' + getUserJWT())
.set('Cocorico-App-Id', config.testApp.id);
} catch (err) {
expect(err.status).toBe(404);
}
});
});
6 changes: 6 additions & 0 deletions api/test/getAPIURL.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
var config = require('/opt/cocorico/api-web/config.json');

module.exports = function(route) {
// FIXME: protocole should be read from the config
return 'https://' + config.hostname + '/api' + route;
}
14 changes: 14 additions & 0 deletions api/test/getAccessToken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
var config = require('/opt/cocorico/api-web/config.json');

var request = require('superagent-promise')(require('superagent'), Promise);

module.exports = async () => {
var appId = config.testApp.id;
var appSecret = config.testApp.secret;

return request
.post('https://127.0.0.1/api/oauth/token')
.send({'grant_type': 'client_credentials'})
.set('Authorization', 'Basic ' + new Buffer(appId + ':' + appSecret).toString('base64'))
.then((res) => res.body.access_token);
}
Loading