openhim-core
Version:
The OpenHIM core application that provides logging and routing of http requests
387 lines (346 loc) • 11.3 kB
JavaScript
/* eslint-env mocha */
import request from 'supertest'
import nconf from 'nconf'
import { ChannelModelAPI } from '../../src/model/channels'
import { ClientModelAPI } from '../../src/model/clients'
import { TransactionModelAPI, TransactionModel } from '../../src/model/transactions'
import * as testUtils from '../utils'
import { config } from '../../src/config'
import { ObjectId } from 'mongodb'
import { promisify } from 'util'
import * as constants from '../constants'
import sinon from 'sinon'
const { SERVER_PORTS } = constants
nconf.set('router', { httpPort: SERVER_PORTS.httpPort })
const server = require('../../src/server')
describe('Routes enabled/disabled tests', () => {
let mockServer1 = null
let mockServer2 = null
let timeoutServer = null
let restrictedServer = null
const httpPortPlus40 = constants.PORT_START + 40
const httpPortPlus41 = constants.PORT_START + 41
const httpPortPlus42 = constants.PORT_START + 42
const httpPortPlus43 = constants.PORT_START + 43
const httpPortPlus44 = constants.PORT_START + 44
const sandbox = sinon.createSandbox()
const restrictedSpy = sandbox.spy(async (req) => 'Restricted response')
const timeoutSpy = sandbox.spy(async (req) => {
await testUtils.wait(30)
return 'timeout'
})
const channel1 = new ChannelModelAPI({
name: 'TEST DATA - Mock endpoint 1',
urlPattern: '^/test/channel1$',
allow: ['PoC'],
routes: [
{
name: 'test route',
host: 'localhost',
port: httpPortPlus40,
primary: true
}, {
name: 'test route 2',
host: 'localhost',
port: httpPortPlus41
}
],
updatedBy: {
id: new ObjectId(),
name: 'Test'
}
})
const channel2 = new ChannelModelAPI({
name: 'TEST DATA - Mock endpoint 2',
urlPattern: '^/test/channel2$',
allow: ['PoC'],
routes: [
{
name: 'test route',
host: 'localhost',
port: httpPortPlus40,
status: 'disabled'
}, {
name: 'test route 2',
host: 'localhost',
port: httpPortPlus41,
primary: true,
status: 'enabled'
}
],
updatedBy: {
id: new ObjectId(),
name: 'Test'
}
})
const channel3 = new ChannelModelAPI({
name: 'TEST DATA - Mock endpoint 3',
urlPattern: '^/test/channel3$',
allow: ['PoC'],
routes: [
{
name: 'test route',
host: 'localhost',
port: httpPortPlus40,
primary: true,
status: 'enabled'
}, {
name: 'test route 2',
host: 'localhost',
port: httpPortPlus41,
primary: true,
status: 'disabled'
}
],
updatedBy: {
id: new ObjectId(),
name: 'Test'
}
})
const channel4 = new ChannelModelAPI({
name: 'TEST DATA - Mock endpoint 4',
urlPattern: '^/test/channel4$',
allow: ['PoC'],
routes: [
{
name: 'test transaction orchestration',
host: 'localhost',
port: httpPortPlus40,
primary: true,
status: 'enabled'
}
],
updatedBy: {
id: new ObjectId(),
name: 'Test'
}
})
const channel5 = new ChannelModelAPI({
name: 'TEST DATA - Mock endpoint 5',
urlPattern: '^/test/channel5$',
allow: ['PoC'],
routes: [
{
name: 'test transaction fail orchestration',
host: 'localhost',
port: httpPortPlus43,
primary: true,
status: 'enabled'
}
],
updatedBy: {
id: new ObjectId(),
name: 'Test'
}
})
const channel6 = new ChannelModelAPI({
name: 'TEST DATA - Mock endpoint 6',
urlPattern: '^/test/channel6$',
allow: ['PoC'],
routes: [
{
name: 'test route',
host: 'localhost',
port: httpPortPlus40,
primary: true,
status: 'enabled'
}, {
name: 'test route 2',
host: 'localhost',
port: httpPortPlus41,
status: 'enabled'
}
],
updatedBy: {
id: new ObjectId(),
name: 'Test'
}
})
const timeoutChannel = new ChannelModelAPI({
name: 'TEST DATA - timeoutChannel',
urlPattern: '^/test/timeoutChannel$',
allow: ['PoC'],
timeout: 20,
routes: [
{
name: 'test route',
host: 'localhost',
port: httpPortPlus44,
primary: true,
status: 'enabled'
}
],
updatedBy: {
id: new ObjectId(),
name: 'Test'
}
})
const channelRestricted = new ChannelModelAPI({
name: 'Restricted channel',
urlPattern: '^/test/restricted$',
allow: ['PoC'],
methods: ['GET'],
routes: [
{
name: 'restricted route',
host: 'localhost',
port: httpPortPlus42,
primary: true,
status: 'enabled'
}
],
updatedBy: {
id: new ObjectId(),
name: 'Test'
}
})
before(async () => {
config.authentication.enableMutualTLSAuthentication = false
config.authentication.enableBasicAuthentication = true
await Promise.all([
channel1.save(),
channel2.save(),
channel3.save(),
channel4.save(),
channel5.save(),
channel6.save(),
timeoutChannel.save(),
channelRestricted.save()
])
const testAppDoc = {
clientID: 'testApp',
clientDomain: 'test-client.jembi.org',
name: 'TEST Client',
roles: [
'OpenMRS_PoC',
'PoC'
],
passwordAlgorithm: 'sha512',
passwordHash: '28dce3506eca8bb3d9d5a9390135236e8746f15ca2d8c86b8d8e653da954e9e3632bf9d85484ee6e9b28a3ada30eec89add42012b185bd9a4a36a07ce08ce2ea',
passwordSalt: '1234567890',
cert: ''
}
await new ClientModelAPI(testAppDoc).save()
// Create mock endpoint to forward requests to
mockServer1 = await testUtils.createMockHttpServer('target1', httpPortPlus40, 200)
mockServer2 = await testUtils.createMockHttpServer('target2', httpPortPlus41, 200)
restrictedServer = await testUtils.createMockHttpServer(restrictedSpy, httpPortPlus42, 200)
timeoutServer = await testUtils.createMockHttpServer(timeoutSpy, httpPortPlus44, 200)
await promisify(server.start)({ httpPort: SERVER_PORTS.httpPort })
})
after(async () => {
await Promise.all([
ChannelModelAPI.deleteMany({}),
ClientModelAPI.deleteMany({}),
mockServer1.close(),
mockServer2.close(),
restrictedServer.close(),
timeoutServer.close(),
promisify(server.stop)()
])
})
afterEach(async () => {
sandbox.reset()
await Promise.all([
TransactionModelAPI.deleteMany({})
])
})
beforeEach(async () => { await TransactionModelAPI.deleteMany({}) })
it('should route transactions to routes that have no status specified (default: enabled)', async () => {
const res = await request(constants.HTTP_BASE_URL)
.get('/test/channel1')
.auth('testApp', 'password')
.expect(200)
res.text.should.be.exactly('target1')
// routes are async
await testUtils.pollCondition(() => TransactionModel.countDocuments().then(c => c === 1))
const trx = await TransactionModelAPI.findOne()
trx.routes.length.should.be.exactly(1)
trx.routes[0].should.have.property('name', 'test route 2')
trx.routes[0].response.body.should.be.exactly('target2')
})
it('should NOT route transactions to disabled routes', async () => {
const res = await request(constants.HTTP_BASE_URL)
.get('/test/channel2')
.auth('testApp', 'password')
.expect(200)
res.text.should.be.exactly('target2')
// routes are async
const trx = await TransactionModelAPI.findOne()
trx.routes.length.should.be.exactly(0)
})
it('should ignore disabled primary routes (multiple primary routes)', async () => {
const res = await request(constants.HTTP_BASE_URL)
.get('/test/channel3')
.auth('testApp', 'password')
.expect(200)
res.text.should.be.exactly('target1')
// routes are async
const trx = await TransactionModelAPI.findOne()
trx.routes.length.should.be.exactly(0)
})
it('should allow a request if the method is in the "methods"', async () => {
const res = await request(constants.HTTP_BASE_URL)
.get('/test/restricted')
.auth('testApp', 'password')
.expect(200)
res.text.should.be.exactly('Restricted response')
// routes are async
restrictedSpy.callCount.should.eql(1)
const [req] = restrictedSpy.firstCall.args
req.method.should.eql('GET')
})
it('should deny a request if the method is not in the "methods"', async () => {
const res = await request(constants.HTTP_BASE_URL)
.post('/test/restricted')
.auth('testApp', 'password')
.expect(405)
res.body.toString().should.eql('Request with method POST is not allowed. Only GET methods are allowed')
// routes are async
restrictedSpy.callCount.should.eql(0)
})
it('should allow a request and produce an orchestration recording the openhim\'s request and received response', async () => {
await request(constants.HTTP_BASE_URL)
.get('/test/channel4')
.auth('testApp', 'password')
.expect(200)
await testUtils.pollCondition(() => TransactionModel.countDocuments().then(c => c === 1))
const newTransaction = await TransactionModel.find()
newTransaction.length.should.be.exactly(1)
newTransaction[0].orchestrations.length.should.be.exactly(1)
newTransaction[0].orchestrations[0].name.should.eql('test transaction orchestration')
newTransaction[0].orchestrations[0].request.path.should.eql('/test/channel4')
})
it('should allow a request with multiple routes and produce an orchestration recording the openhim\'s request and received response', async () => {
await request(constants.HTTP_BASE_URL)
.get('/test/channel6')
.auth('testApp', 'password')
.expect(200)
const newTransaction = await TransactionModel.find()
newTransaction.length.should.be.exactly(1)
newTransaction[0].orchestrations.length.should.be.exactly(1)
newTransaction[0].orchestrations[0].name.should.eql('test route')
})
it('should error the request and produce an orchestration recording the openhim\'s request and received response', async () => {
await request(constants.HTTP_BASE_URL)
.get('/test/channel5')
.auth('testApp', 'password')
.expect(500)
await testUtils.pollCondition(() => TransactionModel.countDocuments().then(c => c === 1))
const newTransaction = await TransactionModel.find()
newTransaction.length.should.be.exactly(1)
newTransaction[0].orchestrations.length.should.be.exactly(1)
newTransaction[0].orchestrations[0].name.should.eql('test transaction fail orchestration')
newTransaction[0].orchestrations[0].error.message.should.eql('connect ECONNREFUSED 127.0.0.1:32043')
})
it('should respect the channel timeout', async () => {
await request(constants.HTTP_BASE_URL)
.get('/test/timeoutChannel')
.auth('testApp', 'password')
.expect(500)
timeoutSpy.callCount.should.eql(1)
const newTransaction = await TransactionModel.find()
newTransaction.length.should.be.exactly(1)
newTransaction[0].orchestrations[0].error.message.should.eql('Request took longer than 20ms')
})
})