Skip to content

Commit 55b93a1

Browse files
committed
add unit test /users/{userId}/skills topcoder-archive#128
1 parent 68089aa commit 55b93a1

9 files changed

+2441
-10
lines changed

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,8 @@ Make sure all config values are right, and you can run on local successfully, th
102102
5. When you are running the application for the first time, It will take some time initially to download the image and install the dependencies
103103

104104
You can also head into `docker-pgsql-es` folder and run `docker-compose up -d` to have docker instances of pgsql and elasticsearch to use with the api
105+
106+
## Testing
107+
108+
- Run `npm run test` to execute unit tests
109+
- Run `npm run test:cov` to execute unit tests and generate coverage report.

app.js

+4
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,7 @@ app.use('*', (req, res) => {
112112
logger.info(`Express server listening on port ${app.get('port')}`)
113113
})
114114
})()
115+
116+
if (process.env.NODE_ENV === 'test') {
117+
module.exports = app
118+
}

config/test.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module.exports = {
2+
AUTH0_URL: 'https://topcoder-dev.auth0.com/oauth/token',
3+
AUTH0_AUDIENCE: 'https://m2m.topcoder-dev.com/',
4+
TOKEN_CACHE_TIME: 6000,
5+
AUTH0_CLIENT_ID: 'client_id',
6+
AUTH0_CLIENT_SECRET: 'secret',
7+
AUTH0_PROXY_SERVER_URL: 'proxy_url'
8+
}

package-lock.json

+2,143-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+20-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
"migrations": "node scripts/db/migrations.js",
1212
"insert-data": "node scripts/db/insert-data.js",
1313
"migrate-qldb-to-pg": "node scripts/db/migrateQldbToPg.js",
14-
"import-s3-data": "node scripts/db/importS3ToQldb.js"
14+
"import-s3-data": "node scripts/db/importS3ToQldb.js",
15+
"test": "mocha test/unit/*.test.js --timeout 30000 --require test/prepare.js --exit",
16+
"test:cov": "nyc --reporter=html --reporter=text npm run test"
1517
},
1618
"repository": {
1719
"type": "git",
@@ -47,8 +49,25 @@
4749
"winston": "^3.2.1"
4850
},
4951
"devDependencies": {
52+
"chai": "^4.3.4",
53+
"chai-as-promised": "^7.1.1",
54+
"chai-http": "^4.3.0",
55+
"mocha": "^9.1.2",
56+
"nyc": "^15.1.0",
57+
"sinon": "^11.1.2",
5058
"standard": "^14.3.0"
5159
},
60+
"standard": {
61+
"env": [
62+
"mocha"
63+
]
64+
},
65+
"nyc": {
66+
"exclude": [
67+
"src/common/logger.js",
68+
"test/unit/**"
69+
]
70+
},
5271
"engines": {
5372
"node": "12.x"
5473
}

test/prepare.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/*
2+
* Prepare for tests.
3+
*/
4+
process.env.NODE_ENV = 'test'
5+
require('../src/bootstrap')

test/unit/UserSkillsEndpoint.test.js

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
const _ = require('lodash')
2+
const config = require('config')
3+
const chai = require('chai')
4+
const chaiHttp = require('chai-http')
5+
const chaiAsPromised = require('chai-as-promised')
6+
const service = require('../../src/modules/usersSkill/service')
7+
const sinon = require('sinon')
8+
const app = require('../../app')
9+
const commonData = require('./common/commonData')
10+
11+
chai.use(chaiHttp)
12+
chai.use(chaiAsPromised)
13+
const should = chai.should()
14+
const expect = chai.expect
15+
16+
describe('Unit tests for /users/{userId}/skills', () => {
17+
afterEach(() => {
18+
sinon.restore()
19+
})
20+
describe('Test GET', () => {
21+
beforeEach(() => {
22+
sinon.stub(service, 'search').resolves({
23+
page: 1,
24+
perPage: 20,
25+
total: 1,
26+
result: [{
27+
id: 1,
28+
userId: 1,
29+
skillId: 1
30+
}]
31+
})
32+
})
33+
for (const tokenName of ['admin', 'administrator', 'topcoderUser', 'copilot', 'ubahn']) {
34+
it(`Call GET API with ${tokenName} token successfully`, async () => {
35+
const response = await chai.request(app).get(`/${config.API_VERSION}/users/test_user/skills`)
36+
.set('Authorization', 'Bearer ' + commonData[`${tokenName}Token`])
37+
should.equal(response.status, 200)
38+
should.equal(response.headers['x-page'], '1')
39+
should.equal(response.headers['x-per-page'], '20')
40+
should.equal(response.headers['x-total'], '1')
41+
should.equal(response.headers['x-total-pages'], '1')
42+
should.exist(response.headers.link)
43+
should.equal(response.body.length, 1)
44+
})
45+
}
46+
it('Call GET API with other token', async () => {
47+
const response = await chai.request(app).get(`/${config.API_VERSION}/users/test_user/skills`)
48+
.set('Authorization', `Bearer ${commonData.unKnownToken}`)
49+
should.equal(response.status, 403)
50+
})
51+
it('Call GET API without token', async () => {
52+
const response = await chai.request(app).get(`/${config.API_VERSION}/users/test_user/skills`)
53+
should.equal(response.status, 403)
54+
})
55+
})
56+
describe('Test HEAD', () => {
57+
beforeEach(() => {
58+
sinon.stub(service, 'search').resolves({
59+
page: 1,
60+
perPage: 20,
61+
total: 1,
62+
result: [{
63+
id: 1,
64+
userId: 1,
65+
skillId: 1
66+
}]
67+
})
68+
})
69+
for (const tokenName of ['admin', 'administrator', 'topcoderUser', 'copilot', 'ubahn']) {
70+
it(`Call HEAD API with ${tokenName} token successfully`, async () => {
71+
const response = await chai.request(app).head(`/${config.API_VERSION}/users/test_user/skills`)
72+
.set('Authorization', 'Bearer ' + commonData[`${tokenName}Token`])
73+
should.equal(response.status, 200)
74+
should.equal(response.headers['x-page'], '1')
75+
should.equal(response.headers['x-per-page'], '20')
76+
should.equal(response.headers['x-total'], '1')
77+
should.equal(response.headers['x-total-pages'], '1')
78+
should.exist(response.headers.link)
79+
should.equal(_.isEmpty(response.body), true)
80+
})
81+
}
82+
it('Call HEAD API with other token', async () => {
83+
const response = await chai.request(app).head(`/${config.API_VERSION}/users/test_user/skills`)
84+
.set('Authorization', `Bearer ${commonData.unKnownToken}`)
85+
should.equal(response.status, 403)
86+
})
87+
it('Call HEAD API without token', async () => {
88+
const response = await chai.request(app).head(`/${config.API_VERSION}/users/test_user/skills`)
89+
should.equal(response.status, 403)
90+
})
91+
})
92+
describe('Test POST', () => {
93+
const userSkill = {
94+
userId: 'string',
95+
skillId: 'string',
96+
metricValue: 'string',
97+
certifierId: 'string',
98+
certifiedDate: '2021-10-11T10:59:12.816Z',
99+
created: '2021-10-11T10:59:12.816Z',
100+
updated: '2021-10-11T10:59:12.817Z',
101+
createdBy: 'string',
102+
updatedBy: 'string'
103+
}
104+
let stubCreate
105+
beforeEach(() => {
106+
stubCreate = sinon.stub(service, 'create').resolves(userSkill)
107+
})
108+
for (const tokenName of ['admin', 'administrator', 'topcoderUser', 'copilot', 'ubahn']) {
109+
it(`Call POST API with ${tokenName} token successfully`, async () => {
110+
const response = await chai.request(app).post(`/${config.API_VERSION}/users/test_user/skills`)
111+
.set('Authorization', 'Bearer ' + commonData[`${tokenName}Token`])
112+
.send({ skillId: 'testSkill' })
113+
should.equal(response.status, 200)
114+
expect(response.body).to.deep.eq(userSkill)
115+
should.equal(stubCreate.calledOnce, true)
116+
expect(stubCreate.getCall(0).args[0]).to.deep.eq({ userId: 'test_user', skillId: 'testSkill' })
117+
})
118+
}
119+
it('Call POST API with other token', async () => {
120+
const response = await chai.request(app).post(`/${config.API_VERSION}/users/test_user/skills`)
121+
.set('Authorization', `Bearer ${commonData.unKnownToken}`)
122+
.send({ skillId: 'testSkill' })
123+
should.equal(response.status, 403)
124+
})
125+
it('Call POST API without token', async () => {
126+
const response = await chai.request(app).post(`/${config.API_VERSION}/users/test_user/skills`)
127+
.send({ skillId: 'testSkill' })
128+
should.equal(response.status, 403)
129+
})
130+
})
131+
})

test/unit/UserSkillsService.test.js

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/* eslint-disable no-unused-expressions */
2+
3+
const _ = require('lodash')
4+
const chai = require('chai')
5+
const chaiAsPromised = require('chai-as-promised')
6+
const sinon = require('sinon')
7+
const sequelize = require('../../src/models/index')
8+
const service = require('../../src/modules/usersSkill/service')
9+
const helper = require('../../src/common/helper')
10+
const serviceHelper = require('../../src/common/service-helper')
11+
const dbHelper = require('../../src/common/db-helper')
12+
const errors = require('../../src/common/errors')
13+
14+
chai.use(chaiAsPromised)
15+
const expect = chai.expect
16+
17+
describe('user skills service test', () => {
18+
beforeEach(() => {
19+
sinon.stub(sequelize, 'transaction').callsFake(f => f())
20+
})
21+
afterEach(() => {
22+
sinon.restore()
23+
})
24+
25+
describe('Create user skills', () => {
26+
it('Create user skills successfully', async () => {
27+
const entity = { userId: 'userId', skillId: 'skillId' }
28+
const newEntity = _.assign({ createdBy: 'test' }, entity)
29+
sinon.stub(dbHelper, 'get').resolves({})
30+
sinon.stub(dbHelper, 'makeSureUnique').resolves({})
31+
const stubDBCreate = sinon.stub(dbHelper, 'create').resolves({ toJSON: () => newEntity })
32+
const stubEsInsert = sinon.stub(serviceHelper, 'createRecordInEs').resolves({})
33+
const result = await service.create(entity, {})
34+
expect(result).to.deep.eql(newEntity)
35+
expect(stubDBCreate.calledOnce).to.be.true
36+
expect(stubEsInsert.calledOnce).to.be.true
37+
expect(stubDBCreate.getCall(0).args[1]).to.deep.eq(entity)
38+
expect(stubEsInsert.getCall(0).args[1]).to.deep.eq(newEntity)
39+
})
40+
it('Throw error when require parameters absence', async () => {
41+
await expect(service.create({ userId: 'userId' })).to.be.rejectedWith('"entity.skillId" is required')
42+
await expect(service.create({ skillId: 'skillId' })).to.be.rejectedWith('"entity.userId" is required')
43+
})
44+
it('Throw error when user id does not exist', async () => {
45+
const error = errors.newEntityNotFoundError('cannot find user where id:userId')
46+
sinon.stub(dbHelper, 'get').rejects(error)
47+
await expect(service.create({ userId: 'userId', skillId: 'skillId' })).to.eventually.rejectedWith(error)
48+
})
49+
it('Throw error when a record with same user id and skill id exist', async () => {
50+
const error = errors.newConflictError('usersSkill already exists with userId:userId,skillId:skillId')
51+
sinon.stub(dbHelper, 'get').resolves({})
52+
sinon.stub(dbHelper, 'makeSureUnique').rejects(error)
53+
await expect(service.create({ userId: 'userId', skillId: 'skillId' })).to.eventually.rejectedWith(error)
54+
})
55+
it('Throw error and does not publish error event when insert into db error', async () => {
56+
const error = new Error('db error')
57+
sinon.stub(dbHelper, 'get').resolves({})
58+
sinon.stub(dbHelper, 'makeSureUnique').resolves({})
59+
const stubPublishError = sinon.stub(helper, 'publishError')
60+
sinon.stub(dbHelper, 'create').rejects(error)
61+
await expect(service.create({ userId: 'userId', skillId: 'skillId' })).to.eventually.rejectedWith(error)
62+
expect(stubPublishError.called).to.be.false
63+
})
64+
it('Throw error and publish error event when insert into es error', async () => {
65+
const error = new Error('es error')
66+
const entity = { userId: 'userId', skillId: 'skillId' }
67+
const newEntity = _.assign({ createdBy: 'test' }, entity)
68+
sinon.stub(dbHelper, 'get').resolves({})
69+
sinon.stub(dbHelper, 'makeSureUnique').resolves({})
70+
const stubPublishError = sinon.stub(helper, 'publishError').resolves({})
71+
const stubDBCreate = sinon.stub(dbHelper, 'create').resolves({ toJSON: () => newEntity })
72+
sinon.stub(serviceHelper, 'createRecordInEs').rejects(error)
73+
await expect(service.create({ userId: 'userId', skillId: 'skillId' })).to.eventually.rejectedWith(error)
74+
expect(stubPublishError.called).to.be.true
75+
expect(stubDBCreate.called).to.be.true
76+
})
77+
})
78+
describe('Search user skills', () => {
79+
it('Throw error when require parameters absence', async () => {
80+
await expect(service.search({ })).to.be.rejectedWith(errors.BadRequestError)
81+
})
82+
it('Throw error when user id does not exist', async () => {
83+
const error = errors.newEntityNotFoundError('cannot find user where id:userId')
84+
sinon.stub(dbHelper, 'get').rejects(error)
85+
await expect(service.search({ userId: 'userId' })).to.eventually.rejectedWith(error)
86+
})
87+
it('Search user skills from es successfully', async () => {
88+
const entity = { userId: 'userId', skillId: 'skillId' }
89+
const newEntity = _.assign({ createdBy: 'test' }, entity)
90+
sinon.stub(dbHelper, 'get').resolves({})
91+
const searchEs = sinon.stub(serviceHelper, 'searchRecordInEs').resolves([newEntity])
92+
const searchDb = sinon.stub(dbHelper, 'find')
93+
const result = await service.search({ userId: 'userId' }, {})
94+
expect(result).to.deep.eql([newEntity])
95+
expect(searchEs.calledOnce).to.be.true
96+
expect(searchDb.called).to.be.false
97+
})
98+
it('Search user skills from db when es throw errors', async () => {
99+
const entity = { userId: 'userId', skillId: 'skillId' }
100+
const newEntity = _.assign({ createdBy: 'test' }, entity)
101+
sinon.stub(dbHelper, 'get').resolves({})
102+
const searchEs = sinon.stub(serviceHelper, 'searchRecordInEs').resolves(null)
103+
const searchDb = sinon.stub(dbHelper, 'find').resolves([newEntity])
104+
const result = await service.search({ userId: 'userId' }, {})
105+
expect(result).to.deep.eql({ fromDb: true, result: [newEntity], total: 1 })
106+
expect(searchEs.calledOnce).to.be.true
107+
expect(searchDb.called).to.be.true
108+
})
109+
})
110+
})

test/unit/common/commonData.js

+15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)