From b3e0ef866f11f5f99ad5cb63dc8d217909c54834 Mon Sep 17 00:00:00 2001
From: Dan Jones <dan.jones@noc.ac.uk>
Date: Tue, 17 Jan 2023 12:19:30 +0000
Subject: [PATCH] test: add poll and publish tests

+ refactor cucumber step functions into test folder
+ rename features after tested adapter methods
+ refactor adapter.validate to call adapter.protocol.validate
---
 cucumber.js                                   | 10 ++-
 dist/adapter.esm.js                           | 17 +---
 dist/adapter.js                               | 17 +---
 dist/protocol.esm.js                          |  4 +-
 dist/protocol.js                              |  4 +-
 features/adapter/auth.js                      | 72 -----------------
 features/adapter/poll.js                      | 36 ---------
 ...enticates.feature => adapter_auth.feature} |  0
 ..._receives.feature => adapter_poll.feature} | 18 ++++-
 features/adapter_publish.feature              | 18 +++++
 ...dates.feature => adapter_validate.feature} |  0
 ...idates.feature => schema_validate.feature} |  0
 package.json                                  |  3 +-
 src/adapter/index.js                          | 17 +---
 src/protocol/index.js                         |  4 +-
 test/cucumber/adapter/auth.steps.js           | 41 ++++++++++
 test/cucumber/adapter/before.steps.js         | 81 +++++++++++++++++++
 test/cucumber/adapter/poll.steps.js           | 54 +++++++++++++
 test/cucumber/adapter/publish.steps.js        | 34 ++++++++
 .../cucumber/adapter/validate.steps.js        |  4 +-
 .../cucumber/schema/validate.steps.js         |  0
 21 files changed, 273 insertions(+), 161 deletions(-)
 delete mode 100644 features/adapter/auth.js
 delete mode 100644 features/adapter/poll.js
 rename features/{adapter_authenticates.feature => adapter_auth.feature} (100%)
 rename features/{adapter_receives.feature => adapter_poll.feature} (57%)
 create mode 100644 features/adapter_publish.feature
 rename features/{adapter_validates.feature => adapter_validate.feature} (100%)
 rename features/{schema_validates.feature => schema_validate.feature} (100%)
 create mode 100644 test/cucumber/adapter/auth.steps.js
 create mode 100644 test/cucumber/adapter/before.steps.js
 create mode 100644 test/cucumber/adapter/poll.steps.js
 create mode 100644 test/cucumber/adapter/publish.steps.js
 rename features/adapter/validate.js => test/cucumber/adapter/validate.steps.js (83%)
 rename features/schema/validate.js => test/cucumber/schema/validate.steps.js (100%)

diff --git a/cucumber.js b/cucumber.js
index 026460b..02d6548 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 d79a030..d1375d7 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 565f78d..8471c08 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 0b9a29f..3bf81eb 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 c5004e1..f5e618c 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 4a64298..0000000
--- 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 76c537a..0000000
--- 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 6582ca3..9ecc9e9 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 0000000..7e7e7de
--- /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 90ecb89..e48afd3 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 ad41f54..b4dc835 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 74676dd..646194a 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 0000000..110d21c
--- /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 0000000..9471285
--- /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 0000000..3d7ddaf
--- /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 0000000..055c7fd
--- /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 db57e8e..86596aa 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
-- 
GitLab