Commit d295a6a0 authored by Dan Jones's avatar Dan Jones
Browse files

Merge branch '15-release-v0-1' into 'master'

Resolve "Release v0.1"

See merge request !15
1 merge request!15Resolve "Release v0.1"
Pipeline #113980 passed with stages
in 57 seconds
import axios from 'axios';
import Validator from 'swagger-model-validator';
/**
* GenericProtocol defines a simple passthru handler for messages
* This can be extended to handle different schemas
*
* The assumption is that all messages conform to a single
* wrapper schema definition. Different payloads are handled
* by a oneOf definitions within the schema determined by a
* field value.
*
* This class can be extended and overridden to handle
* different schemas and to implement the client specific
* logic related to be executed when invoked for a given
* message type.
*
* By default encode and decode are just passthru stubs
*
* decode is invoked when receiving a message from the backbone
* encode is invoked before delivering a message to the backbone
*
* The intention is that these allow you to transform the message
* and call invoke internal functions and services as required
*/
export class GenericProtocol {
constructor(schema, services) {
this.schema = schema;
this.services = services;
this.createValidator(schema);
}
/**
* Create a Validator from the provided JSONSchema
* @param {object} schema
* @returns {Validator}
*/
createValidator(schema) {
this.validator = new Validator(schema);
}
/**
* Validate that a message meets the reqiured schema
* @param {object} message
* @returns {object}
*/
validate(message) {
return this.validator.validate(
message,
this.schema.components.schemas.Message,
this.schema.components.schemas,
false,
false
);
}
/**
* Identify the payload type from the message content
* @param {object} message
* @returns {string}
*/
getType(message) {
try {
return message.payload.message_type;
} catch (error) {
return null;
}
}
/**
* Invoked on receiving a message from the backbone
*
* Whilst type isn't used in the generic stub it will
* be needed by sub-classes overriding the decode method
* @param {string} type
* @param {object} message
* @returns {*}
*/
decode(type, message) {
return message;
}
/**
* Invoked on receiving an invalid message from the backbone
*
* @param {object} message
* @param {object} validation
* @returns {*}
*/
receivedInvalid(message, validation) {
return message;
}
/**
* Optionally invoked before delivering a message to the backbone
*
* Whilst type isn't used in the generic stub it will
* be needed by sub-classes overriding the encode method
* @param {string} type
* @param {*} message
* @returns {object}
*/
encode(type, message) {
return message;
}
}
/**
* GenericSoarProtocol defines a simple passthru handler for messages
*
* This provides the same as above but loads a version of the
* SoAR message protocol
*/
export class GenericSoarProtocol extends GenericProtocol {
constructor(schema, services) {
super(schema, services);
}
/**
* Create a Validator from the provided JSONSchema
*
* If schema is a string build a URL and retrieve the
* schema from gitlab
* @param {string|object} schema
* @returns {object}
*/
createValidator(schema) {
if (typeof schema === 'string' && schema.match(/^[\w.]+$/)) {
this.loadSchema(schema).then((schema) => {
this.validator = new Validator(schema);
});
} else {
this.validator = new Validator(schema);
}
}
/**
* Load schema from gitlab by tag/branch/commitref
* @param {string} version
* @returns {object}
*/
loadSchema(version) {
this.axios = axios;
let repository =
'https://git.noc.ac.uk/communications-backbone-system/backbone-message-format';
let url = `${repository}/-/raw/${version}/project/soar/swagger.json`;
return this.axios.get(url).then((response) => {
this.schema = response.data;
return response.data;
});
}
/**
* Validate that a message meets the reqiured schema
* @param {object} message
* @returns {object}
*/
validate(message) {
return this.validator.validate(
message,
this.schema.components.schemas.MESSAGE,
this.schema.components.schemas,
false,
false
);
}
}
const assert = require('assert');
const { When, Then } = require('@cucumber/cucumber');
const { fixtures } = require('../../fixtures/server');
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
);
});
Then('the adapter auth fails', function () {
this.adapter.auth().catch((error) => {
assert.equal(error.response.status, 403);
});
});
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 = fixtures.get('schema-swagger');
const { GenericProtocol } = require('../../../dist/protocol');
const { Adapter } = require('../../../dist/adapter');
/**
* 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 TrackedAdapter extends Adapter {
setupCallTracking(recorder, tracker) {
this.recorder = recorder;
this.tracker = tracker;
}
getAuthorizationHeader() {
this.recorder('getAuthorizationHeader');
return super.getAuthorizationHeader();
}
poll(is_retry) {
this.recorder('poll', { is_retry });
return super.poll(is_retry);
}
publish(topic, body, is_retry) {
this.recorder('publish', { topic, body, is_retry });
return super.publish(topic, body, is_retry);
}
broadcast(body, is_retry) {
this.recorder('broadcast', { body, is_retry });
return super.broadcast(body, is_retry);
}
getTrackedCalls(method) {
let calls = this.tracker.getCalls(this.recorder);
let methodCalls = calls.filter((call) => call.arguments[0] === method);
return methodCalls;
}
resetTracker() {
this.tracker.reset();
}
}
class TrackedGenericProtocol extends GenericProtocol {
setupCallTracking(recorder, tracker) {
this.recorder = recorder;
this.tracker = 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.api = mockValidConfig.api;
this.schema = mockSchema;
this.tracker = tracker;
this.recorder = recorder;
let services = {
recorder,
tracker,
};
this.classes = {
TrackedAdapter,
TrackedGenericProtocol,
};
this.callCounts = {};
this.protocol = new this.classes.TrackedGenericProtocol(
this.schema,
services
);
this.protocol.setupCallTracking(this.recorder, this.tracker);
this.protocol.resetTracker();
this.mockAxios = mockAxios;
this.mockAxios.reset();
this.mockAxios.resetHistory();
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'));
});
const assert = require('assert');
const { When, Then } = require('@cucumber/cucumber');
const { fixtures } = require('../../fixtures/server');
const mockValidConfig = fixtures.get('config-valid');
When('a mock notify API response is configured to return success', function () {
const response = {};
this.mockAxios.onPost(`${mockValidConfig.api}/notify`).reply(200, response);
});
When(
'a mock notify API response is configured to return a {int} error',
function (statusCode) {
const statusMessages = {
403: 'Token expired',
503: 'Service unavailable',
};
this.mockAxios
.onPost(`${mockValidConfig.api}/notify`)
.reply(statusCode, { message: statusMessages[statusCode] });
}
);
When('the broadcast method is called', function () {
const message = fixtures.get('message-vehicle-status');
this.message = message;
const body = JSON.stringify(message);
this.call = this.adapter.broadcast(body);
this.callCounts.broadcast = this.adapter.getTrackedCalls('broadcast').length;
});
When('the broadcast method is called with is_retry on', function () {
const message = fixtures.get('message-vehicle-status');
this.message = message;
const body = JSON.stringify(message);
this.call = this.adapter.broadcast(body, true);
this.callCounts.broadcast = this.adapter.getTrackedCalls('broadcast').length;
});
Then('the broadcast method was called with is_retry on', function () {
let broadcastCalls = this.adapter.getTrackedCalls('broadcast');
let lastCall = broadcastCalls[broadcastCalls.length - 1];
assert.ok(lastCall.arguments[1].is_retry);
});
Then('the broadcast method is not called again', function () {
let newBroadcastCallCount = this.adapter.getTrackedCalls('broadcast').length;
assert.equal(this.callCounts.broadcast, newBroadcastCallCount);
});
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;
});
Given('invalid config', function () {
this.schema = mockSchema;
this.config = mockInvalidConfig;
});
When('the adapter instance is created', function () {
this.adapter = new this.classes.TrackedAdapter(this.protocol, this.config);
this.adapter.setupCallTracking(this.recorder, this.tracker);
});
Then(
'a successful response is returned with status {int}',
function (expectedStatus) {
this.call.then((response) => {
assert.equal(response.status, expectedStatus);
});
}
);
Then(
'an error response is returned with status {int}',
function (expectedStatus) {
this.call.catch((error) => {
assert.equal(error.response.status, expectedStatus);
});
}
);
Then('the credentials are deleted', function () {
assert.equal(this.adapter.credentials, null);
});
Then('the credentials are not deleted', function () {
assert.notEqual(this.adapter.credentials, null);
});
When('the {string} method call counts are checked', function (method) {
let newCallCount = this.adapter.getTrackedCalls(method).length;
this.callCounts[method] = newCallCount;
});
Then('the {string} method is not called again', function (method) {
let newCallCount = this.adapter.getTrackedCalls(method).length;
assert.equal(this.callCounts[method], newCallCount);
});
Then(
'the total number of calls to {string} was {int}',
function (method, expectedCallCount) {
let callCount = this.adapter.getTrackedCalls(method).length;
assert.equal(callCount, expectedCallCount);
}
);
Then(
'the total number of {string} requests to {string} was {int}',
function (method, endpoint, expectedCallCount) {
let url = `${this.api}${endpoint}`;
let requestHistory = this.mockAxios.history[method.toLowerCase()].filter(
(request) => request.url === url
);
assert.equal(requestHistory.length, expectedCallCount);
}
);
const assert = require('assert');
const { When, Then } = require('@cucumber/cucumber');
When('the getAuthorizationHeader method is called', function () {
this.call = this.adapter.getAuthorizationHeader();
let callCount = this.adapter.getTrackedCalls('getAuthorizationHeader').length;
this.callCounts.getAuthorizationHeader = callCount;
});
Then(
'a headers object is returned containing a bearer token authorization header',
function () {
this.call.then((headers) => {
assert.ok('Authorization' in headers);
const authHeaderWords = headers.Authorization.split(' ');
assert.ok(authHeaderWords[0] === 'Bearer');
assert.ok(authHeaderWords[1] === this.adapter.credentials.token);
});
}
);
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 a {int} error',
function (statusCode) {
const statusMessages = {
403: 'Token expired',
503: 'Service unavailable',
};
this.mockAxios
.onGet(`${mockValidConfig.api}/receive`)
.reply(statusCode, { message: statusMessages[statusCode] });
}
);
When('the poll method is called', function () {
this.call = this.adapter.poll();
this.callCounts.poll = this.adapter.getTrackedCalls('poll').length;
});
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);
}
);
When('the poll method is called with is_retry on', function () {
this.call = this.adapter.poll(true);
this.callCounts.poll = this.adapter.getTrackedCalls('poll').length;
});
Then('the poll method was called with is_retry on', function () {
let pollCalls = this.adapter.getTrackedCalls('poll');
let lastCall = pollCalls[pollCalls.length - 1];
assert.ok(lastCall.arguments[1].is_retry);
});
Then('the poll method is not called again', function () {
let newPollCallCount = this.adapter.getTrackedCalls('poll').length;
assert.equal(this.callCounts.poll, newPollCallCount);
});
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 a {int} error',
function (statusCode) {
const statusMessages = {
403: 'Token expired',
503: 'Service unavailable',
};
this.mockAxios
.onPost(`${mockValidConfig.api}/send`)
.reply(statusCode, { message: statusMessages[statusCode] });
}
);
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);
this.callCounts.publish = this.adapter.getTrackedCalls('publish').length;
});
When('the publish method is called with is_retry on', 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, true);
this.callCounts.publish = this.adapter.getTrackedCalls('publish').length;
});
Then('the publish method was called with is_retry on', function () {
let publishCalls = this.adapter.getTrackedCalls('publish');
let lastCall = publishCalls[publishCalls.length - 1];
assert.ok(lastCall.arguments[1].is_retry);
});
Then('the publish method is not called again', function () {
let newPublishCallCount = this.adapter.getTrackedCalls('publish').length;
assert.equal(this.callCounts.publish, newPublishCallCount);
});
const assert = require('assert');
const { When, Then } = require('@cucumber/cucumber');
When('the token expiry is in the future', function () {
const expiry = new Date();
expiry.setHours(expiry.getHours() + 1);
this.adapter.credentials.expiry = expiry.toISOString();
});
When('the token expiry is in the past', function () {
const expiry = new Date();
expiry.setHours(expiry.getHours() - 1);
this.adapter.credentials.expiry = expiry.toISOString();
});
// Boolean parameters are not supported
// and returns "true" would be misleading
Then('tokenValid returns true', function () {
const isValid = this.adapter.tokenValid();
assert.ok(isValid);
});
Then('tokenValid returns false', function () {
const isValid = this.adapter.tokenValid();
assert.ok(!isValid);
});
const { Given, When } = require('@cucumber/cucumber');
const { fixtures } = require('../../fixtures/server');
Given('a valid message', function () {
this.message = fixtures.get('message-vehicle-status');
});
Given('an invalid message', function () {
this.message = fixtures.get('message-vehicle-status-invalid');
});
When('the validate method is called', function () {
this.validation = this.adapter.validate(this.message);
});
const assert = require('assert');
const { Then } = require('@cucumber/cucumber');
Then('the message is returned unaltered', function () {
assert.equal(this.message, this.response);
});
const { When } = require('@cucumber/cucumber');
When('the protocol.decode method is called', function () {
const type = this.protocol.getType(this.message);
this.response = this.protocol.decode(type, this.message);
});
const { When } = require('@cucumber/cucumber');
When('the protocol.encode method is called', function () {
const type = this.protocol.getType(this.message);
this.response = this.protocol.encode(type, this.message);
});
const assert = require('assert');
const { When, Then } = require('@cucumber/cucumber');
When('protocol getType is called', function () {
this.type = this.protocol.getType(this.message);
});
Then('getType returns message.payload.message_type if present', function () {
assert.equal(this.type, this.message.payload.message_type);
});
Then(
'getType returns null if message.payload.message_type is not present',
function () {
assert.equal(this.type, null);
}
);
const assert = require('assert');
const { When, Then } = require('@cucumber/cucumber');
When('the protocol.validate method is called', function () {
this.validation = this.protocol.validate(this.message);
});
Then('the message is validated successfully', function () {
assert.equal(this.validation.valid, true);
assert.equal(this.validation.errorCount, 0);
});
Then('the message fails to validate', function () {
assert.equal(this.validation.valid, false);
assert.notEqual(this.validation.errorCount, 0);
});
const assert = require('assert');
const { Given, When, Then } = require('@cucumber/cucumber');
const OpenAPISchemaValidator = require('openapi-schema-validator').default;
const fs = require('fs');
const schemaLocation = './test/mock/swagger.json';
Given('the test schema', function () {
this.schema = JSON.parse(fs.readFileSync(schemaLocation));
});
When('it is validated', function () {
const validator = new OpenAPISchemaValidator({ version: 3 });
this.validation = validator.validate(this.schema);
});
Then('it matches the OpenAPI specification', function () {
// According to the docs this should return a valid:boolean
// but if you look at the code it just returns a list of errors
// which is empty for a valid result
assert.equal(this.validation.errors.length, 0);
});
const fs = require('fs');
const path = require('path');
exports.fixtures = {
get: function (fixtureName) {
try {
let fixtureContent = fs.readFileSync(
path.join(__dirname, `${fixtureName}.json`)
);
let fixture = JSON.parse(fixtureContent);
return fixture;
} catch (e) {
console.error('Fixture not found', fixtureName);
return null;
}
},
};
{
"metadata": {
"source": "ae",
"destination": "soar.po.ecosub.eco1",
"delivery_type": "publish",
"message_id": "test"
},
"payload": {
"message_type": "VehicleMission",
"operator_id": "po",
"vehicle_id": "eco1",
"coordinates": {
"latitude": 59.234,
"longitude": -10.432,
"depth": 50,
"projection": 4326
},
"actions": []
}
}
{
"metadata": {
"source": "ae",
"destination": "soar.po.ecosub.eco1",
"delivery_type": "publish",
"message_id": "test"
},
"payload": {
"message_type": "VehicleMission",
"operator_id": "po",
"vehicle_id": "eco1",
"coordinates": {
"latitude": 59.234,
"longitude": -10.432,
"depth": 50,
"projection": 4326
},
"actions": [
{
"action_type": "GoToWaypoint",
"coordinates": {
"latitude": 59.234,
"longitude": -10.432,
"depth": 50,
"projection": 4326
}
},
{
"action_type": "DescendToAltitude",
"coordinates": {
"latitude": 59.234,
"longitude": -10.432,
"altitude": 50,
"projection": 4326
}
}
]
}
}
{
"metadata": {
"source": "ae",
"destination": "soar.po.ecosub.eco1",
"delivery_type": "publish",
"message_id": "test"
},
"payload": {
"message_type": "VehicleStatus",
"operator_id": "po",
"vehicle_id": "eco1",
"coordinates": {
"latitude": 57.234,
"longitude": -8.432,
"depth": 50,
"projection": 4326
},
"battery_percentage": 64
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment