diff --git a/cucumber.js b/cucumber.js index 026460b84aa59298a096b1fc965232b44f848f7a..02d6548d452261f96aad2b7fed5e6f5e1e857abd 100644 --- a/cucumber.js +++ b/cucumber.js @@ -1,3 +1,9 @@ module.exports = { - default: `--format-options '{"snippetInterface": "synchronous"}'` -} \ No newline at end of file + default: { + formatOptions: { + snippetInterface: "synchronous" + }, + paths: [ 'features/**/*.feature' ], + require: [ 'test/cucumber/**/*.steps.js' ], + }, +}; diff --git a/dist/adapter.esm.js b/dist/adapter.esm.js index d79a030d469bd3fde4ab8fe19dad0a056f8ed65e..d1375d79711b391d24dd4a450e40e7c39679edcb 100644 --- a/dist/adapter.esm.js +++ b/dist/adapter.esm.js @@ -1,4 +1,3 @@ -import Validator from 'swagger-model-validator'; import axios from 'axios'; /** @@ -9,7 +8,6 @@ class Adapter { this.protocol = protocol; this.config = config; this.axios = axios; - this.validator = new Validator(protocol.schema); } /** @@ -25,13 +23,7 @@ class Adapter { * @returns {object} */ validate(message) { - return this.validator.validate( - message, - this.protocol.schema.components.schemas.Message, - this.protocol.schema.components.schemas, - false, - true - ); + return this.protocol.validate(message); } /** @@ -113,8 +105,7 @@ class Adapter { return response; }) .catch((error) => { - console.error(error); - return Promise.reject(error.response); + return Promise.reject(error); }); } @@ -145,7 +136,7 @@ class Adapter { return response; }) .catch((error) => { - return Promise.reject(error.response); + return Promise.reject(error); }); } @@ -178,7 +169,7 @@ class Adapter { return response; }) .catch((error) => { - return Promise.reject(error.response); + return Promise.reject(error); }); } } diff --git a/dist/adapter.js b/dist/adapter.js index 565f78dbfd4301d50e51ddcf7981f1c69943232d..8471c08bd7c2e6e46adc4b88bd58d89c6f25865d 100644 --- a/dist/adapter.js +++ b/dist/adapter.js @@ -1,6 +1,5 @@ 'use strict'; -var Validator = require('swagger-model-validator'); var axios = require('axios'); /** @@ -11,7 +10,6 @@ class Adapter { this.protocol = protocol; this.config = config; this.axios = axios; - this.validator = new Validator(protocol.schema); } /** @@ -27,13 +25,7 @@ class Adapter { * @returns {object} */ validate(message) { - return this.validator.validate( - message, - this.protocol.schema.components.schemas.Message, - this.protocol.schema.components.schemas, - false, - true - ); + return this.protocol.validate(message); } /** @@ -115,8 +107,7 @@ class Adapter { return response; }) .catch((error) => { - console.error(error); - return Promise.reject(error.response); + return Promise.reject(error); }); } @@ -147,7 +138,7 @@ class Adapter { return response; }) .catch((error) => { - return Promise.reject(error.response); + return Promise.reject(error); }); } @@ -180,7 +171,7 @@ class Adapter { return response; }) .catch((error) => { - return Promise.reject(error.response); + return Promise.reject(error); }); } } diff --git a/dist/protocol.esm.js b/dist/protocol.esm.js index 0b9a29f0b53148df63a5751b5fafdd8cb1d6a27a..3bf81eb3c7112b251af0953d7a71517b88fe1477 100644 --- a/dist/protocol.esm.js +++ b/dist/protocol.esm.js @@ -37,8 +37,8 @@ class GenericProtocol { validate(message) { return this.validator.validate( message, - this.protocol.schema.components.schemas.Message, - this.protocol.schema.components.schemas, + this.schema.components.schemas.Message, + this.schema.components.schemas, false, true ); diff --git a/dist/protocol.js b/dist/protocol.js index c5004e1f84c4cfe22664804963513cc6fd43a744..f5e618c4e13101919101429ef372ec5b7f958ef0 100644 --- a/dist/protocol.js +++ b/dist/protocol.js @@ -39,8 +39,8 @@ class GenericProtocol { validate(message) { return this.validator.validate( message, - this.protocol.schema.components.schemas.Message, - this.protocol.schema.components.schemas, + this.schema.components.schemas.Message, + this.schema.components.schemas, false, true ); diff --git a/features/adapter/auth.js b/features/adapter/auth.js deleted file mode 100644 index 4a64298638c8e7ba476739f09325a5c2c98b7f72..0000000000000000000000000000000000000000 --- a/features/adapter/auth.js +++ /dev/null @@ -1,72 +0,0 @@ -const assert = require('assert'); -const { Before, Given, When, Then } = require('@cucumber/cucumber'); - -const axios = require("axios"); -const MockAdapter = require("axios-mock-adapter"); - -// This sets the mock adapter on the default instance -const mockAxios = new MockAdapter(axios); - -const { fixtures } = require('../../test/fixtures/server'); - -const mockValidConfig = fixtures.get('config-valid'); -const mockInvalidConfig = fixtures.get('config-invalid'); - -const mockSchema = require('../../test/mock/swagger.json'); -const { Adapter } = require('../../dist/adapter'); -const { GenericProtocol } = require('../../dist/protocol'); - - -let decodeTracker; -class TrackedGenericProtocol extends GenericProtocol { - decode() { - return super.decode() - } -} - -Before(function() { - this.mockAxios = mockAxios; - this.mockAxios.reset(); - - this.mockAxios.onGet( - `${mockValidConfig.api}/token`, - { params: { client_id: mockValidConfig.client_id, secret: mockValidConfig.secret } } - ).reply(200, fixtures.get('response-valid-token')); - - this.mockAxios.onGet( - `${mockInvalidConfig.api}/token`, - { params: { client_id: mockInvalidConfig.client_id, secret: mockInvalidConfig.secret } } - ).reply(403, fixtures.get('response-denied-token')); - -}); - -Given('valid config', function() { - this.schema = mockSchema; - this.config = mockValidConfig -}); - -When('the adapter instance is created', function() { - let mockProtocol = new GenericProtocol(this.schema); - let mockAdapter = new Adapter(mockProtocol, this.config); - this.adapter = mockAdapter; -}); - -When('the auth method is called', async function() { - await this.adapter.auth(); -}); - -Then(('the adapter credentials are populated'), function() { - assert.equal(this.adapter.credentials.token, fixtures.get('response-valid-token').token); -}); - -Given('invalid config', function() { - this.schema = mockSchema; - this.config = mockInvalidConfig; -}); - -Then('the adapter auth fails', async function() { - this.adapter.auth() - .catch((error) => { - assert.equal(error.response.status, 403); - }); -}); \ No newline at end of file diff --git a/features/adapter/poll.js b/features/adapter/poll.js deleted file mode 100644 index 76c537abe675d25078e0d3d5732f5109300b59b7..0000000000000000000000000000000000000000 --- a/features/adapter/poll.js +++ /dev/null @@ -1,36 +0,0 @@ -const assert = require('assert'); -const { Before, Given, When, Then } = require('@cucumber/cucumber'); - -const { fixtures } = require('../../test/fixtures/server'); - -const mockValidConfig = fixtures.get('config-valid'); - -const xMessageResponse = function(xMessages) { - const message = fixtures.get('message-vehicle-status'); - let response = []; - for (let i=0; i<xMessages; i++) { - response.push({ - topic: "broadcast", - message: JSON.stringify(message) - }); - } - return response; -}; - -When('the queue contains {int} messages', function(xMessages) { - const response = xMessageResponse(xMessages); - this.mockAxios.onGet( - `${mockValidConfig.api}/receive`, - ).reply(200, response); -}); - -When('the poll method is called', async function() { - this.messages = await this.adapter.poll() - .then((response) => { - return response.data; - }); -}); - -Then('a successful response is returned with {int} messages', function(xMessages) { - assert.equal(this.messages.length, xMessages); -}); \ No newline at end of file diff --git a/features/adapter_authenticates.feature b/features/adapter_auth.feature similarity index 100% rename from features/adapter_authenticates.feature rename to features/adapter_auth.feature diff --git a/features/adapter_receives.feature b/features/adapter_poll.feature similarity index 57% rename from features/adapter_receives.feature rename to features/adapter_poll.feature index 6582ca348bf392cf0ebd6f636e3564f0f2304ce5..9ecc9e9553dd86c54bda7aa66d36950eed619741 100644 --- a/features/adapter_receives.feature +++ b/features/adapter_poll.feature @@ -9,7 +9,7 @@ Feature: Can the adapter receive messages? Given valid config When the adapter instance is created When the auth method is called - When the queue contains 0 messages + When a mock receive API response is configured to return 0 messages When the poll method is called Then a successful response is returned with 0 messages @@ -17,14 +17,26 @@ Feature: Can the adapter receive messages? Given valid config When the adapter instance is created When the auth method is called - When the queue contains 2 messages + When a mock receive API response is configured to return 2 messages When the poll method is called Then a successful response is returned with 2 messages + Then the protocol "validate" method is called 2 times + Then the protocol "decode" method is called 2 times Scenario: 10 messages are received succecssfully if the queue contains 10 messages Given valid config When the adapter instance is created When the auth method is called - When the queue contains 10 messages + When a mock receive API response is configured to return 10 messages When the poll method is called Then a successful response is returned with 10 messages + Then the protocol "validate" method is called 10 times + Then the protocol "decode" method is called 10 times + + Scenario: An invalid token returns a forbidden response + Given valid config + When the adapter instance is created + When the auth method is called + When a mock receive API response is configured to return an error + When the poll method is called + Then an error response is returned with status 403 diff --git a/features/adapter_publish.feature b/features/adapter_publish.feature new file mode 100644 index 0000000000000000000000000000000000000000..7e7e7de1681a3220f61b829b457e7e35904ca7c4 --- /dev/null +++ b/features/adapter_publish.feature @@ -0,0 +1,18 @@ +Feature: Can the adapter publish messages? + The adapter publish method works as expected + + Scenario: A message can be published successfully + Given valid config + When the adapter instance is created + When the auth method is called + When a mock send API response is configured to return success + When the publish method is called + Then a successful response is returned with status 200 + + Scenario: A failed publish returns a 403 + Given valid config + When the adapter instance is created + When the auth method is called + When a mock send API response is configured to return an error + When the publish method is called + Then an error response is returned with status 403 diff --git a/features/adapter_validates.feature b/features/adapter_validate.feature similarity index 100% rename from features/adapter_validates.feature rename to features/adapter_validate.feature diff --git a/features/schema_validates.feature b/features/schema_validate.feature similarity index 100% rename from features/schema_validates.feature rename to features/schema_validate.feature diff --git a/package.json b/package.json index 90ecb8937ae25cb72e8db21c759563ad50f10001..e48afd31bf2f76a86c06ce1b9da0c9e3d8139986 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "lint": "yarn lint:js && yarn lint:prettier", "lintfix": "prettier --write --list-different . && yarn lint:js --fix", "prepare": "husky install", - "test": "yarn build && yarn cucumber-js", + "test": "yarn build && yarn cucumber", + "cucumber": "cucumber-js", "build": "cross-env NODE_ENV=production rollup -c" }, "lint-staged": { diff --git a/src/adapter/index.js b/src/adapter/index.js index ad41f54b7132c5101bc435ce72e0fa4fde02592a..b4dc835da3d6987198bd2ae3e185a4bef200efec 100644 --- a/src/adapter/index.js +++ b/src/adapter/index.js @@ -1,4 +1,3 @@ -import Validator from 'swagger-model-validator'; import axios from 'axios'; /** @@ -9,7 +8,6 @@ export class Adapter { this.protocol = protocol; this.config = config; this.axios = axios; - this.validator = new Validator(protocol.schema); } /** @@ -25,13 +23,7 @@ export class Adapter { * @returns {object} */ validate(message) { - return this.validator.validate( - message, - this.protocol.schema.components.schemas.Message, - this.protocol.schema.components.schemas, - false, - true - ); + return this.protocol.validate(message); } /** @@ -113,8 +105,7 @@ export class Adapter { return response; }) .catch((error) => { - console.error(error); - return Promise.reject(error.response); + return Promise.reject(error); }); } @@ -145,7 +136,7 @@ export class Adapter { return response; }) .catch((error) => { - return Promise.reject(error.response); + return Promise.reject(error); }); } @@ -178,7 +169,7 @@ export class Adapter { return response; }) .catch((error) => { - return Promise.reject(error.response); + return Promise.reject(error); }); } } diff --git a/src/protocol/index.js b/src/protocol/index.js index 74676ddb91706e411b78024dc8ad3e3081995dd5..646194a6b0b046b5ce65c7ca528c7646f1d8f361 100644 --- a/src/protocol/index.js +++ b/src/protocol/index.js @@ -37,8 +37,8 @@ export class GenericProtocol { validate(message) { return this.validator.validate( message, - this.protocol.schema.components.schemas.Message, - this.protocol.schema.components.schemas, + this.schema.components.schemas.Message, + this.schema.components.schemas, false, true ); diff --git a/test/cucumber/adapter/auth.steps.js b/test/cucumber/adapter/auth.steps.js new file mode 100644 index 0000000000000000000000000000000000000000..110d21ce7e617fa2c3f1fd8c8b29079508efac73 --- /dev/null +++ b/test/cucumber/adapter/auth.steps.js @@ -0,0 +1,41 @@ +const assert = require('assert'); +const { Given, When, Then } = require('@cucumber/cucumber'); + +const { fixtures } = require('../../fixtures/server'); + +const mockValidConfig = fixtures.get('config-valid'); +const mockInvalidConfig = fixtures.get('config-invalid'); + +const mockSchema = require('../../mock/swagger.json'); +const { Adapter } = require('../../../dist/adapter'); + + +Given('valid config', function() { + this.config = mockValidConfig +}); + +When('the adapter instance is created', function() { + //let mockProtocol = new GenericProtocol(this.schema); + let mockAdapter = new Adapter(this.protocol, this.config); + this.adapter = mockAdapter; +}); + +When('the auth method is called', async function() { + await this.adapter.auth(); +}); + +Then('the adapter credentials are populated', function() { + assert.equal(this.adapter.credentials.token, fixtures.get('response-valid-token').token); +}); + +Given('invalid config', function() { + this.schema = mockSchema; + this.config = mockInvalidConfig; +}); + +Then('the adapter auth fails', function() { + this.adapter.auth() + .catch((error) => { + assert.equal(error.response.status, 403); + }); +}); \ No newline at end of file diff --git a/test/cucumber/adapter/before.steps.js b/test/cucumber/adapter/before.steps.js new file mode 100644 index 0000000000000000000000000000000000000000..9471285eb547e173f243c4af1a42add30edf807f --- /dev/null +++ b/test/cucumber/adapter/before.steps.js @@ -0,0 +1,81 @@ +const assert = require('assert'); +const { Before } = require('@cucumber/cucumber'); + +const axios = require("axios"); +const MockAdapter = require("axios-mock-adapter"); + +// This sets the mock adapter on the default instance +const mockAxios = new MockAdapter(axios); + +const { fixtures } = require('../../fixtures/server'); + +const mockValidConfig = fixtures.get('config-valid'); +const mockInvalidConfig = fixtures.get('config-invalid'); + +const mockSchema = require('../../mock/swagger.json'); +const { GenericProtocol } = require('../../../dist/protocol'); + +/** + * Use assert.CallTracker to track internal method calls + * Instead of adding trackers to each method, create a + * single tracked stub function and then use the parameters + * to record what is being tracked. + */ +const tracker = new assert.CallTracker(); +const trackedFunction = function(method, params) { + // do nothing; +}; +const recorder = tracker.calls(trackedFunction); + +class TrackedGenericProtocol extends GenericProtocol { + constructor(schema, services) { + super(schema, services); + this.recorder = services.recorder; + this.tracker = services.tracker; + } + encode(type, message) { + this.recorder('encode', {type, message}); + return super.encode(type, message) + } + decode(type, message) { + this.recorder('decode', {type, message}); + return super.decode(type, message) + } + validate(message) { + this.recorder('validate', {message}); + return super.validate(message); + } + getTrackedCalls(method) { + let calls = this.tracker.getCalls(this.recorder); + let methodCalls = calls.filter(call => call.arguments[0] === method); + return methodCalls; + } + resetTracker() { + this.tracker.reset(); + } +} + +Before(function() { + this.schema = mockSchema; + this.tracker = tracker; + this.recorder = recorder; + let services = { + recorder: this.recorder, + tracker: this.tracker + }; + this.protocol = new TrackedGenericProtocol(this.schema, services); + this.protocol.resetTracker(); + this.mockAxios = mockAxios; + this.mockAxios.reset(); + + this.mockAxios.onGet( + `${mockValidConfig.api}/token`, + { params: { client_id: mockValidConfig.client_id, secret: mockValidConfig.secret } } + ).reply(200, fixtures.get('response-valid-token')); + + this.mockAxios.onGet( + `${mockInvalidConfig.api}/token`, + { params: { client_id: mockInvalidConfig.client_id, secret: mockInvalidConfig.secret } } + ).reply(403, fixtures.get('response-denied-token')); + +}); \ No newline at end of file diff --git a/test/cucumber/adapter/poll.steps.js b/test/cucumber/adapter/poll.steps.js new file mode 100644 index 0000000000000000000000000000000000000000..3d7ddaf7c3d68532947b627f9c44dc2d08afa19c --- /dev/null +++ b/test/cucumber/adapter/poll.steps.js @@ -0,0 +1,54 @@ +const assert = require('assert'); +const { When, Then } = require('@cucumber/cucumber'); + +const { fixtures } = require('../../fixtures/server'); + +const mockValidConfig = fixtures.get('config-valid'); + +const xMessageResponse = function(xMessages) { + const message = fixtures.get('message-vehicle-status'); + let response = []; + for (let i=0; i<xMessages; i++) { + response.push({ + topic: "broadcast", + message: JSON.stringify(message) + }); + } + return response; +}; + +When('a mock receive API response is configured to return {int} messages', function(xMessages) { + const response = xMessageResponse(xMessages); + this.mockAxios.onGet( + `${mockValidConfig.api}/receive`, + ).reply(200, response); +}); + +When('a mock receive API response is configured to return an error', function() { + this.mockAxios.onGet( + `${mockValidConfig.api}/receive`, + ).reply(403, { message: 'Token expired' }) +}); + +When('the poll method is called', function() { + this.call = this.adapter.poll(); +}); + +Then('a successful response is returned with {int} messages', function(xMessages) { + this.call + .then(response => { + assert.equal(response.data.length, xMessages); + }); +}); + +Then('the protocol {string} method is called {int} times', function(method, xInvokes) { + const decodes = this.protocol.getTrackedCalls(method); + assert.equal(decodes.length, xInvokes); +}); + +Then('an error response is returned with status {int}', function(expectedStatus) { + this.call + .catch((error) => { + assert.equal(error.response.status, expectedStatus); + }); +}); diff --git a/test/cucumber/adapter/publish.steps.js b/test/cucumber/adapter/publish.steps.js new file mode 100644 index 0000000000000000000000000000000000000000..055c7fd12a483f2aa980798d4c9435e44370dac2 --- /dev/null +++ b/test/cucumber/adapter/publish.steps.js @@ -0,0 +1,34 @@ +const assert = require('assert'); +const { When, Then } = require('@cucumber/cucumber'); + +const { fixtures } = require('../../fixtures/server'); + +const mockValidConfig = fixtures.get('config-valid'); + +When('a mock send API response is configured to return success', function() { + const response = {}; + this.mockAxios.onPost( + `${mockValidConfig.api}/send`, + ).reply(200, response); +}); + +When('a mock send API response is configured to return an error', function() { + this.mockAxios.onPost( + `${mockValidConfig.api}/send`, + ).reply(403, { message: 'Token expired' }) +}); + +When('the publish method is called', function() { + const message = fixtures.get('message-vehicle-status'); + this.message = message; + const topic = message.metadata.destination; + const body = JSON.stringify(message); + this.call = this.adapter.publish(topic, body); +}); + +Then('a successful response is returned with status {int}', function(expectedStatus) { + this.call + .then(response => { + assert.equal(response.status, expectedStatus); + }); +}); \ No newline at end of file diff --git a/features/adapter/validate.js b/test/cucumber/adapter/validate.steps.js similarity index 83% rename from features/adapter/validate.js rename to test/cucumber/adapter/validate.steps.js index db57e8e3cf9c7484445bbd05a71f9470269a2330..86596aa9dce908ee169b3917ead13ffa87c70952 100644 --- a/features/adapter/validate.js +++ b/test/cucumber/adapter/validate.steps.js @@ -1,7 +1,7 @@ const assert = require('assert'); -const { Before, Given, When, Then } = require('@cucumber/cucumber'); +const { Given, When, Then } = require('@cucumber/cucumber'); -const { fixtures } = require('../../test/fixtures/server'); +const { fixtures } = require('../../fixtures/server'); Given('a valid message', function() { this.message = fixtures.get('message-vehicle-status'); diff --git a/features/schema/validate.js b/test/cucumber/schema/validate.steps.js similarity index 100% rename from features/schema/validate.js rename to test/cucumber/schema/validate.steps.js