-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #178 from beansupreme/add-test-suite
Add test suite
- Loading branch information
Showing
27 changed files
with
14,183 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,119 @@ | ||
## NRTS PRC API (master) | ||
## ACRFD API (master) | ||
|
||
Minimal api for prc, stateless JWT & public API for NRTS-PRC | ||
Minimal API for the ACRFD [Public](https://github.com/bcgov/nrts-prc-public) and [Admin](https://github.com/bcgov/nrts-prc-admin) apps | ||
|
||
## How to run this | ||
|
||
Start the server by running `npm start` | ||
|
||
Check the swagger-ui on `http://localhost:3000/docs` | ||
Check the swagger-ui on `http://localhost:3000/api/docs/` | ||
|
||
5) POST `http://localhost:3000/api/login/token` with the following body | ||
1) POST `http://localhost:3000/api/login/token` with the following body | ||
`` | ||
{ | ||
"username": "username", | ||
"password": "password" | ||
"username": #{username}, | ||
"password": #{password} | ||
} | ||
`` | ||
|
||
and take the token that you get in the response | ||
|
||
6) GET `http://localhost:3000/api/application` again with the following header | ||
``Authorization: Bearer _TOKEN_``, replacing `_TOKEN_ ` with the value you got from request #4 | ||
2) GET `http://localhost:3000/api/application` again with the following header | ||
``Authorization: Bearer _TOKEN_``, replacing `_TOKEN_ ` with the value you got from that request | ||
|
||
## Initial Setup | ||
|
||
1) Start server and create database by running `npm start` in root | ||
|
||
2) Add Admin user to users collection | ||
|
||
`` | ||
db.users.insert({ "username": #{username}, "password": #{password}, roles: [['sysadmin'],['public']] }) | ||
`` | ||
|
||
3) Seed local database as described in [seed README](seed/README.md) | ||
|
||
## Testing | ||
|
||
This project is using [jest](http://jestjs.io/) as a testing framework. You can run tests with | ||
`yarn test` or `jest`. Running either command with the `--watch` flag will re-run the tests every time a file is changed. | ||
|
||
To run the tests in one file, simply pass the path of the file name e.g. `jest api/test/search.test.js --watch`. To run only one test in that file, chain the `.only` command e.g. `test.only("Search returns results", () => {})`. | ||
|
||
The **_MOST IMPORTANT_** thing to know about this project's test environment is the router setup. At the time of writing this, it wasn't possible to get [swagger-tools](https://github.com/apigee-127/swagger-tools) router working in the test environment. As a result, all tests **_COMPLETELY bypass_ the real life swagger-tools router**. Instead, a middleware router called [supertest](https://github.com/visionmedia/supertest) is used to map routes to controller actions. In each controller test, you will need to add code like the following: | ||
|
||
```javascript | ||
const test_helper = require('./test_helper'); | ||
const app = test_helper.app; | ||
const featureController = require('../controllers/feature.js'); | ||
const fieldNames = ['tags', 'properties', 'applicationID']; | ||
|
||
app.get('/api/feature/:id', function(req, res) { | ||
let params = test_helper.buildParams({'featureId': req.params.id}); | ||
let paramsWithFeatureId = test_helper.createPublicSwaggerParams(fieldNames, params); | ||
return featureController.protectedGet(paramsWithFeatureId, res); | ||
}); | ||
|
||
test("GET /api/feature/:id returns 200", done => { | ||
request(app) | ||
.get('/api/feature/AAABBB') | ||
.expect(200) | ||
.then(done) | ||
}); | ||
``` | ||
|
||
This code will stand in for the swagger-tools router, and help build the objects that swagger-tools magically generates when HTTP calls go through it's router. The above code will send an object like below to the `api/controllers/feature.js` controller `protectedGet` function as the first parameter (typically called `args`). | ||
|
||
```javascript | ||
{ | ||
swagger: { | ||
params: { | ||
auth_payload: { | ||
scopes: ['sysadmin', 'public'], | ||
userID: null | ||
}, | ||
fields: { | ||
value: ['tags', 'properties', 'applicationID'] | ||
}, | ||
featureId: { | ||
value: 'AAABBB' | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
|
||
Unfortunately, this results in a lot of boilerplate code in each of the controller tests. There are some helpers to reduce the amount you need to write, but you will still need to check the parameter field names sent by your middleware router match what the controller(and swagger router) expect. However, this method results in pretty effective integration tests as they exercise the controller code and save objects in the database. | ||
|
||
|
||
## Test Database | ||
The tests run on an in-memory MongoDB server, using the [mongodb-memory-server](https://github.com/nodkz/mongodb-memory-server) package. The setup can be viewed at [test_helper.js](api/test/test_helper.js), and additional config in [config/mongoose_options.js]. It is currently configured to wipe out the database after each test run to prevent database pollution. | ||
|
||
[Factory-Girl](https://github.com/aexmachina/factory-girl) is used to easily create models(persisted to db) for testing purposes. | ||
|
||
## Mocking http requests | ||
External http calls (such as GETs to BCGW) are mocked with a tool called [nock](https://github.com/nock/nock). Currently sample JSON responses are stored in the [test/fixtures](test/fixtures) directory. This allows you to intercept a call to an external service such as bcgw, and respond with your own sample data. | ||
|
||
```javascript | ||
const bcgwDomain = 'https://openmaps.gov.bc.ca'; | ||
const searchPath = '/geo/pub/FOOO'; | ||
const crownlandsResponse = require('./fixtures/crownlands_response.json'); | ||
var bcgw = nock(bcgwDomain); | ||
let dispositionId = 666666; | ||
|
||
beforeEach(() => { | ||
bcgw.get(searchPath + urlEncodedDispositionId) | ||
.reply(200, crownlandsResponse); | ||
}); | ||
|
||
test('returns the features data from bcgw', done => { | ||
request(app).get('/api/public/search/bcgw/dispositionTransactionId/' + dispositionId) | ||
.expect(200) | ||
.then(response => { | ||
let firstFeature = response.body.features[0]; | ||
expect(firstFeature).toHaveProperty('properties'); | ||
expect(firstFeature.properties).toHaveProperty('DISPOSITION_TRANSACTION_SID'); | ||
done(); | ||
}); | ||
}); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
const actions = require('./actions'); | ||
const Organization = require('./models/organization'); | ||
|
||
describe('#publish', () => { | ||
describe('with an object that has already been published', () => { | ||
test('it returns 409 with a status message', done => { | ||
let publishedOrg = new Organization({tags: ['public']}); | ||
actions.publish(publishedOrg) | ||
.catch(error => { | ||
expect(error.code).toEqual(409); | ||
expect(error.message).toEqual('Object already published'); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('with an object that has not been published', () => { | ||
test('it adds the public tag and saves it', () => { | ||
let newOrg = new Organization({tags: []}); | ||
actions.publish(newOrg); | ||
expect(newOrg.tags[0]).toEqual(expect.arrayContaining(['public'])); | ||
}); | ||
}); | ||
}); | ||
|
||
test('Testing publish.', () => { | ||
var o = {}; | ||
o.tags = [['sysadmin']]; | ||
|
||
expect(actions.isPublished(o)).toEqual(undefined); | ||
|
||
o.tags = [['sysadmin'], ['public']]; | ||
expect(actions.isPublished(o)).toEqual(['public']); | ||
}); | ||
|
||
describe('#isPublished', () => { | ||
let organization = new Organization({}); | ||
|
||
test('it returns the array of public tags', () => { | ||
organization.tags = [['sysadmin'], ['public']]; | ||
expect(actions.isPublished(organization)).toEqual(expect.arrayContaining(['public'])); | ||
}); | ||
|
||
test('it returns undefined if there is no matching public tag', () => { | ||
organization.tags = [['sysadmin']]; | ||
expect(actions.isPublished(organization)).toBeUndefined(); | ||
}); | ||
}); | ||
|
||
describe('#unpublish', () => { | ||
describe('with an object that has been published', () => { | ||
test('it removes the public tag and saves it', () => { | ||
let publishedOrg = new Organization({tags: ['public']}); | ||
actions.unPublish(publishedOrg) | ||
expect(publishedOrg.tags).toHaveLength(0) | ||
}); | ||
}); | ||
|
||
describe('with an object that is unpublished', () => { | ||
test('it returns 409 with a status message', done => { | ||
let newOrg = new Organization({tags: []}); | ||
actions.unPublish(newOrg) | ||
.catch(error => { | ||
expect(error.code).toEqual(409); | ||
expect(error.message).toEqual('Object already unpublished'); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('#delete', () => { | ||
test('it removes the public tag', () => { | ||
let publishedOrg = new Organization({tags: ['public']}); | ||
actions.delete(publishedOrg); | ||
expect(publishedOrg.tags).toHaveLength(0); | ||
}); | ||
|
||
test('it soft-deletes the object', () => { | ||
let newOrg = new Organization({tags: []}); | ||
actions.delete(newOrg); | ||
expect(newOrg.isDeleted).toEqual(true); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
const mongoose = require('mongoose'); | ||
mongoose.Promise = global.Promise; | ||
const Review = require('./review'); | ||
const Application = require('./application'); | ||
const User = require('./user'); | ||
|
||
describe('Review', () => { | ||
describe('_addedBy', () => { | ||
test('it references a user', () => { | ||
let jordan = new User({username: 'Jordan', password: 'likescoff33'}); | ||
let review = new Review({_addedBy: jordan.id}); | ||
|
||
review.save((error) => { | ||
expect(error).toBeUndefined(); | ||
}); | ||
expect(review._addedBy).toEqual(jordan.id); | ||
}); | ||
}); | ||
|
||
describe('_applications', () => { | ||
test('it references many applications', () => { | ||
let skiResort = new Application({name: 'Amazing new resort'}); | ||
let bikeShed = new Application({name: 'Boring bike shed'}); | ||
let review = new Review({_applications: [skiResort.id, bikeShed.id]}); | ||
|
||
review.save((error) => { | ||
expect(error).toBeUndefined(); | ||
}); | ||
expect(review._applications).toContain(skiResort.id, bikeShed.id); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
const mongoose = require('mongoose'); | ||
mongoose.Promise = global.Promise; | ||
const User = require('./user'); | ||
|
||
|
||
describe('User', () => { | ||
describe('username', () => { | ||
test('saves a valid username', () => { | ||
let user = new User({username: '[email protected]', password: 'Password1!'}); | ||
user.save((error) => { | ||
expect(error.errors).toBeUndefined(); | ||
}); | ||
expect(user.username).toEqual('[email protected]'); | ||
}); | ||
|
||
test('cannot be blank', () => { | ||
let blankUser = new User({username: null, password: ''}); | ||
blankUser.save((error) => { | ||
expect(error.errors).toBeDefined(); | ||
let usernameErrors = error.errors.username; | ||
expect(usernameErrors).toBeDefined(); | ||
expect(usernameErrors.message).toEqual('Please fill in a username'); | ||
}); | ||
}); | ||
|
||
test('downcases username', () => { | ||
let weirdCaps = new User({username: 'tOOmAnYCAps', password: 'Password1!'}); | ||
weirdCaps.save((error) => { | ||
expect(error.errors).toBeUndefined(); | ||
}); | ||
expect(weirdCaps.username).toEqual('toomanycaps'); | ||
}); | ||
}); | ||
|
||
describe('password', () =>{ | ||
test('requires a password', () => { | ||
let blankPassword = new User({username: 'coolguy'}); | ||
blankPassword.save(function(error) { | ||
let passwordErrors = error.errors.password; | ||
expect(passwordErrors).toBeDefined(); | ||
expect(passwordErrors.message).toEqual('Please fill in a password'); | ||
}); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.