6-mils
Version:
A JS library for sending, receiving, and parsing cXML messages.
681 lines (547 loc) • 20.3 kB
JavaScript
/* eslint-env mocha */
const validate = require('w3c-xml-validator')
const localServer = require('./lib/local-cxml-server.js')
const getAttributeValue = require('./lib/find-parse-attribute.js')
/**
* Code under test.
* @type {any}
*/
const cxml = require('../../main.js')
/**
* An HTTP server listening on localhost to respond to cXML requests.
* @type {Object}
*/
let server = null
before((done) => {
server = localServer()
server.on('ready', done)
})
after((done) => {
server.close()
server.on('closed', done)
})
describe('end-to-end tests', function () {
this.timeout(10000)
describe('the PunchOutRequest/Response cycle', function () {
context('success', function () {
/**
* The approximate time (in milliseconds since epoch) that the
* PunchOutSetupRequest was submitted to the remote server.
* @type {Number}
*/
let timeOfSubmission = null
/**
* The raw cXML of the order request.
* @type {String}
*/
let requestBody = null
/**
* The data emitted by events (key -> event name, value -> data).
* @type {Object}
*/
const eventData = {}
/**
* The response to the PunchOut setup request.
* @type {Object}
*/
let posres = null
before(function () {
const posreq = new cxml.PunchOutSetupRequest()
posreq.once('sending', (data) => { eventData.sending = data })
posreq.once('received', (data) => { eventData.received = data })
posreq
.setBuyerInfo({ domain: 'DUNS', id: '987654' })
.setSupplierInfo({ domain: 'DUNS', id: '123456' })
.setSenderInfo({ domain: 'NetworkId', id: 'example.com', secret: 'Open sesame!' })
server.once('request', (req) => { requestBody = req })
timeOfSubmission = Date.now()
return posreq.submit(`${server.baseUrl}/posr/success`)
.then(function (res) {
posres = res
})
})
describe('the outgoing request', function () {
it('must have a valid timestamp', function () {
/**
* The timestamp is expected to correspond to when the OrderRequest
* was sent out.
*/
const timestamp = new Date(getAttributeValue(requestBody, 'timestamp'))
const delta = Math.abs(timestamp.getTime() - timeOfSubmission)
expect(delta).to.be.lessThan(20)
})
it('must be well-formed and valid according to the DTD', function () {
return validate(requestBody)
.then(function (result) {
if (!result.isValid) {
console.error(result.errors)
}
expect(result.isValid).to.equal(true)
})
})
it('must be published in the "sending" event', function () {
const expected = requestBody
const actual = eventData.sending
expect(actual).to.equal(expected)
})
})
describe('the PunchOutSetupResponse object', function () {
it('must have the correct value for "payloadId"', function () {
const expected = '933694607739'
const actual = posres.payloadId
expect(actual).to.equal(expected)
})
it('must have the correct value for "timestamp"', function () {
const expected = '2002-08-15T08:46:00-07:00'
const actual = posres.timestamp
expect(actual).to.equal(expected)
})
it('must have the correct value for "version"', function () {
const expected = '1.2.014'
const actual = posres.version
expect(actual).to.equal(expected)
})
it('must have the correct value for "statusCode"', function () {
const expected = '200'
const actual = posres.statusCode
expect(actual).to.equal(expected)
})
it('must have the correct value for "statusText"', function () {
const expected = 'success'
const actual = posres.statusText
expect(actual).to.equal(expected)
})
it('must have the same source as was published in the "received" event', function () {
const expected = posres.toString()
const actual = eventData.received
expect(actual).to.equal(expected)
})
})
})
context('failure', function () {
/**
* The data emitted by events (key -> event name, value -> data).
* @type {Object}
*/
const eventData = {}
/**
* The PunchOut setup request.
* @type {Object}
*/
let posreq = null
/**
* The response to the PunchOut setup request.
* @type {Object}
*/
let posres = null
before(function () {
posreq = new cxml.PunchOutSetupRequest()
posreq.once('sending', (data) => { eventData.sending = data })
posreq.once('received', (data) => { eventData.received = data })
return posreq
.setBuyerInfo({ domain: 'DUNS', id: '987654' })
.setSupplierInfo({ domain: 'DUNS', id: '123456' })
.setSenderInfo({ domain: 'NetworkId', id: 'example.com', secret: 'Open sesame!' })
.submit(`${server.baseUrl}/posr/failure`)
.then(function (res) {
posres = res
})
})
describe('the outgoing request', function () {
it('must be published in the "sending" event', function () {
const expected = posreq.toString()
const actual = eventData.sending
expect(actual).to.equal(expected)
})
})
describe('the PunchOutSetupResponse object', function () {
it('must have the correct value for "payloadId"', function () {
const expected = '933634634590'
const actual = posres.payloadId
expect(actual).to.equal(expected)
})
it('must have the correct value for "timestamp"', function () {
const expected = '2002-08-15T08:48:00-07:00'
const actual = posres.timestamp
expect(actual).to.equal(expected)
})
it('must have the correct value for "version"', function () {
const expected = '1.2.014'
const actual = posres.version
expect(actual).to.equal(expected)
})
it('must have the correct value for "statusCode"', function () {
const expected = '400'
const actual = posres.statusCode
expect(actual).to.equal(expected)
})
it('must have the correct value for "statusText"', function () {
const expected = 'XML document contained a doctype but failed validation.'
const actual = posres.statusText
expect(actual).to.equal(expected)
})
it('must have the same source as was published in the "received" event', function () {
const expected = posres.toString()
const actual = eventData.received
expect(actual).to.equal(expected)
})
})
})
context('with a refused connection', function () {
/**
* The data emitted by events (key -> event name, value -> data).
* @type {Object}
*/
const eventData = {}
/**
* The PunchOut setup request.
* @type {Object}
*/
let posreq = null
/**
* A value indicating whether the "submit" method returned a Promise that
* was resolved or rejected (and if so, the error code).
* @type {String}
*/
let result = ''
before(function () {
/**
* In order to simulate a refused connection, a request is made against
* an (assumed) unbound port.
*/
const originalPortNumber = global.parseInt(/\d+$/.exec(server.baseUrl)[0])
const newBaseUrl = server.baseUrl.replace(originalPortNumber, (originalPortNumber + 1).toString())
posreq = new cxml.PunchOutSetupRequest()
posreq.once('sending', (data) => { eventData.sending = data })
posreq.once('received', (data) => { eventData.received = data })
return posreq
.setBuyerInfo({ domain: 'DUNS', id: '987654' })
.setSupplierInfo({ domain: 'DUNS', id: '123456' })
.setSenderInfo({ domain: 'NetworkId', id: 'example.com', secret: 'Open sesame!' })
.submit(`${newBaseUrl}/posr`)
.then((response) => { result = 'resolved' })
.catch((err) => { result = err.code })
})
describe('the outgoing request', function () {
it('must be published in the "sending" event', function () {
const expected = posreq.toString()
const actual = eventData.sending
expect(actual).to.equal(expected)
})
it('must not have emitted a "received" event', function () {
expect(eventData).to.not.have.property('received')
})
})
describe('the result', function () {
it('must be rejected with the expected error', function () {
expect(result).to.equal('ECONNREFUSED')
})
})
})
context('with a timeout', function () {
/**
* The data emitted by events (key -> event name, value -> data).
* @type {Object}
*/
const eventData = {}
/**
* The PunchOut setup request.
* @type {Object}
*/
let posreq = null
/**
* A value indicating whether the "submit" method returned a Promise that
* was resolved or rejected (and if so, the error code).
* @type {String}
*/
let result = ''
before(function () {
posreq = new cxml.PunchOutSetupRequest()
posreq.once('sending', (data) => { eventData.sending = data })
posreq.once('received', (data) => { eventData.received = data })
posreq.requestTimeout = 100
return posreq
.setBuyerInfo({ domain: 'DUNS', id: '987654' })
.setSupplierInfo({ domain: 'DUNS', id: '123456' })
.setSenderInfo({ domain: 'NetworkId', id: 'example.com', secret: 'Open sesame!' })
.submit(`${server.baseUrl}/posr/timeout`)
.then((response) => { result = 'resolved' })
.catch((err) => { result = err.code })
})
describe('the outgoing request', function () {
it('must be published in the "sending" event', function () {
const expected = posreq.toString()
const actual = eventData.sending
expect(actual).to.equal(expected)
})
it('must not have emitted a "received" event', function () {
expect(eventData).to.not.have.property('received')
})
})
describe('the result', function () {
it('must be rejected with the expected error', function () {
expect(result).to.equal('ETIMEDOUT')
})
})
})
context('with an HTTP error', function () {
/**
* The data emitted by events (key -> event name, value -> data).
* @type {Object}
*/
const eventData = {}
/**
* The PunchOut setup request.
* @type {Object}
*/
let posreq = null
/**
* A value indicating whether the "submit" method returned a Promise that
* was resolved or rejected.
* @type {String}
*/
let result = ''
before(function () {
posreq = new cxml.PunchOutSetupRequest()
posreq.once('sending', (data) => { eventData.sending = data })
posreq.once('received', (data) => { eventData.received = data })
return posreq
.setBuyerInfo({ domain: 'DUNS', id: '987654' })
.setSupplierInfo({ domain: 'DUNS', id: '123456' })
.setSenderInfo({ domain: 'NetworkId', id: 'example.com', secret: 'Open sesame!' })
.submit(`${server.baseUrl}/posr/500`)
.then((response) => { result = 'resolved' })
.catch(() => { result = 'rejected' })
})
describe('the outgoing request', function () {
it('must be published in the "sending" event', function () {
const expected = posreq.toString()
const actual = eventData.sending
expect(actual).to.equal(expected)
})
it('must not have emitted a "received" event', function () {
expect(eventData).to.not.have.property('received')
})
})
describe('the result', function () {
it('must be rejected', function () {
expect(result).to.equal('rejected')
})
})
})
})
describe('the OrderRequest/Response cycle', function () {
function OrderRequestFactory () {
const order = new cxml.OrderRequest({ orderId: 'TEST' })
order
.setBuyerInfo({ domain: 'DUNS', id: '987654' })
.setSupplierInfo({ domain: 'DUNS', id: '123456' })
.setSenderInfo({ domain: 'NetworkId', id: 'example.com', secret: 'Open sesame!' })
order.addItem({
name: 'Test Corp',
quantity: 1,
supplierPartId: 'TEST123',
unitPrice: 0.99,
currency: 'USD',
uom: 'EA',
classification: {
UNISPEC: 'test-123'
}
})
order.setBillingInfo({
address: {
id: '123456',
companyName: 'Test Corp'
}
})
order.setShippingInfo({
address: {
id: '98765',
nickname: 'test',
companyName: 'TEST',
countryCode: 'US',
attentionOf: 'Test Testor',
street: '1 Test Rd',
city: 'Testville',
state: 'NJ',
postalCode: '08888',
countryName: 'United States'
}
})
return order
}
context('success', function () {
/**
* The approximate time (in milliseconds since epoch) that the
* OrderRequest was instantiated.
* @type {Number}
*/
let timeOfInstantiation = null
/**
* The approximate time (in milliseconds since epoch) that the
* OrderRequest was submitted to the remote server.
* @type {Number}
*/
let timeOfSubmission = null
/**
* The raw cXML of the order request.
* @type {String}
*/
let requestBody = null
/**
* The response to the order request.
* @type {Object}
*/
let orderResponse = null
before(function () {
const order = OrderRequestFactory()
timeOfInstantiation = Date.now()
server.once('request', (req) => { requestBody = req })
timeOfSubmission = Date.now()
return order.submit(`${server.baseUrl}/order/success`)
.then(function (res) {
orderResponse = res
})
})
describe('the outgoing request', function () {
it('must have a valid timestamp', function () {
/**
* The timestamp is expected to correspond to when the OrderRequest
* was sent out.
*/
const timestamp = new Date(getAttributeValue(requestBody, 'timestamp'))
const delta = Math.abs(timestamp.getTime() - timeOfSubmission)
expect(delta).to.be.lessThan(20)
})
it('must have a valid order date', function () {
/**
* Since the order date was not specified in the construction of this
* instance, the value should have been automatically populated when
* the OrderRequest was instantiated.
*/
const orderDate = new Date(getAttributeValue(requestBody, 'orderDate'))
const delta = Math.abs(orderDate.getTime() - timeOfInstantiation)
expect(delta).to.be.lessThan(20)
})
it('must be well-formed and valid according to the DTD', function () {
return validate(requestBody)
.then(function (result) {
if (!result.isValid) {
console.error(result.errors)
}
expect(result.isValid).to.equal(true)
})
})
})
describe('the OrderResponse object', function () {
it('must have the correct value for "payloadId"', function () {
const expected = 'successful.order@test.com'
const actual = orderResponse.payloadId
expect(actual).to.equal(expected)
})
it('must have the correct value for "timestamp"', function () {
const expected = '2019-03-12T18:39:09-08:00'
const actual = orderResponse.timestamp
expect(actual).to.equal(expected)
})
it('must have the correct value for "version"', function () {
const expected = '1.2.011'
const actual = orderResponse.version
expect(actual).to.equal(expected)
})
it('must have the correct value for "statusCode"', function () {
const expected = '200'
const actual = orderResponse.statusCode
expect(actual).to.equal(expected)
})
it('must have the correct value for "statusText"', function () {
const expected = 'success'
const actual = orderResponse.statusText
expect(actual).to.equal(expected)
})
})
})
context('with an empty response body', function () {
/**
* The data emitted by events (key -> event name, value -> data).
* @type {Object}
*/
const eventData = {}
/**
* The sample order request.
* @type {Object}
*/
let orderRequest = null
/**
* The response to the order request.
* @type {Object}
*/
let orderResponse = null
/**
* The approximate time (in milliseconds since epoch) that the
* OrderResponse was received from the remote server.
* @type {Number}
*/
let timeOfResponse = null
/**
* A regular expression that describes the default format for the
* "payloadId" property value.
* @type {RegExp}
*/
const PAYLOAD_ID = /^\d+\.\d+\.\w+@6-mils$/
before(function () {
orderRequest = OrderRequestFactory()
orderRequest.once('sending', (data) => { eventData.sending = data })
orderRequest.once('received', (data) => { eventData.received = data })
return orderRequest.submit(`${server.baseUrl}/order/empty`)
.then(function (res) {
timeOfResponse = Date.now()
orderResponse = res
})
})
describe('the "sending" event', function () {
it('must be emitted with the same value as the request body', function () {
const expected = orderRequest.toString()
const actual = eventData.sending
expect(actual).to.equal(expected)
})
})
describe('the "received" event', function () {
it('must be emitted with an empty string', function () {
const expected = ''
const actual = eventData.received
expect(actual).to.equal(expected)
})
})
describe('the OrderResponse object', function () {
it('must have the correct value for "payloadId"', function () {
expect(orderResponse.payloadId).to.match(PAYLOAD_ID)
})
it('must have a valid timestamp', function () {
/**
* The timestamp is expected to correspond to when the OrderResponse
* was sent out.
*/
const timestamp = new Date(orderResponse.timestamp)
const delta = Math.abs(timestamp.getTime() - timeOfResponse)
expect(delta).to.be.lessThan(20)
})
it('must have the correct value for "version"', function () {
const expected = '1.2.045'
const actual = orderResponse.version
expect(actual).to.equal(expected)
})
it('must have the correct value for "statusCode"', function () {
const expected = '200'
const actual = orderResponse.statusCode
expect(actual).to.equal(expected)
})
it('must have the correct value for "statusText"', function () {
const expected = 'success'
const actual = orderResponse.statusText
expect(actual).to.equal(expected)
})
})
})
})
})