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

Merge branch '1-import-prototype-adapter-code-from-example-web-client' into 'dev'

Resolve "Import prototype adapter code from example-web-client"

Closes #1

See merge request !1
parents a8eef21a eb7cb068
module.exports = {
root: true,
env: {
browser: true,
node: true,
'jest/globals': true,
},
parserOptions: {
parser: '@babel/eslint-parser',
// Fix "No Babel config file detected for [file]" error
// https://github.com/babel/babel/issues/11975#issuecomment-786803214
requireConfigFile: false,
},
extends: [
'eslint:recommended',
'prettier',
],
// required to lint *.vue files
plugins: ['vue', 'jest', 'prettier'],
// add your custom rules here
rules: {
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-debugger': 'warn',
'no-unused-vars': ['error', { args: 'none' }],
},
};
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
/logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Dependency directories
node_modules/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# IDE / Editor
.idea
# macOS
.DS_Store
# Vim swap files
*.swp
###
# Place your Prettier ignore content here
###
# .gitignore content is duplicated here due to https://github.com/prettier/prettier/issues/8506
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
/logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Dependency directories
node_modules/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# IDE / Editor
.idea
# macOS
.DS_Store
# Vim swap files
*.swp
{
"trailingComma": "es5",
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"bracketSameLine": false,
"arrowParens": "always",
"endOfLine": "lf"
}
Copyright 2023 [NOC](https://noc.ac.uk)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
...@@ -2,9 +2,138 @@ ...@@ -2,9 +2,138 @@
Generic adapter for the communications-backbone. Generic adapter for the communications-backbone.
Implements: Implements:
- client credentials grant - client credentials grant
- http send/receive/notify to backbone - http send/receive/notify to backbone
- websockets send and receive - websockets send and receive
- validation of messages against a specified OpenAPI schema - validation of messages against a specified OpenAPI schema
- decode/encode stubs - decode/encode stubs
\ No newline at end of file
## Setup
```
yarn install
```
## Test
The tests are written in [cucumber](https://cucumber.io/docs/installation/javascript/)
This means we can have a common suite of [gherkin](https://github.com/cucumber/gherkin)
tests across adapter ports written in multiple languages.
```
yarn test
```
## Installing in your project
We may publish this to a public registry but for now you need to install the package
from git.
### Requirements
An `axios` library. Axios is included as a dev dependency to run the tests.
I've not installed it as a runtime dependency because in nuxt you need to
use `@nuxtjs/axios` instead.
**TODO fix this in rollup config**
### Yarn
#### Once public
```yarn
yarn add git+https://git.noc.ac.uk/communications-backbone-system/backbone-adapter-javascript.git
```
#### For now
```yarn
# You may need to tell it how to use your ssh key
eval `ssh-agent -s`
ssh-add ~/.ssh/id_rsa
export GIT_SSH_COMMAND="/usr/bin/ssh -i /home/danjon/.ssh/id_rsa -o IdentitiesOnly=yes"
# Then you can install the private repo via ssh
yarn add ssh://git@git.noc.ac.uk:communications-backbone-system/backbone-adapter-javascript.git#1-import-prototype-adapter-code-from-example-web-client
```
### NPM
```npm
npm install git+https://git.noc.ac.uk/communications-backbone-system/backbone-adapter-javascript.git
```
## Schema
The example code uses a mock schema with some example messages. The intention is the message
protocol schema is retreived from an external source.
## Config
To run the adapter you need a credentials file called `soar-config.json`.
This will be provided by the backbone operator or requested via the API.
```json
{
"api": "[backbone api root url]",
"client_id": "unique-client-id",
"client_name": "UniqueClientName",
"subscription": "dot.delimited.topic.subscription.#",
"secret": "[a generated secret]"
}
```
### Topics and Subscriptions
The topics have the following structure:
```
project.operator.vehicleType.vehicleID.[send|receive].messageType
# eg
soar.noc.autosub.ah1.send.platform-mission
# or
soar.po.ecosub.eco1.receive.platform-status
```
Subscriptions may contain single-word wildcards (*) or multi-word wildcards (#).
## Encoding and decoding
### Decoding
Decoding refers to translation from the backbone message protocol into a
native format for the client app to process.
All messages received from the backbone are parsed and validated against the
protocol schema and then passed to the protocol decode function.
By overriding the decode function the client can define local actions to be
executed when a message of a given type is received.
### Encoding
Encoding refers to translation from the client app's native format into a
message conforming to the backbone message protocol.
The equivalent encode method allows the client to define translations per
message type to transform local data into a message conforming to the
protocol schema for transmission.
Messages passed to the publish and broadcast methods should have been
encoded and validated against the protocol schema.
## Publish vs Broadcast
It is intended that all normal-operation messages will be published on
a given topic allowing clients to choose which message topics to
subscribe to.
Broadcast is provided for contingency scenarios. The intention is that
in the case of a failure/abort a message can be sent to all parties
which bypasses any existing messages in the publish queue.
The client implementation can chose to take no-action on decoding one of
these messages but they will be made available to all clients.
\ No newline at end of file
module.exports = {
extends: ['@commitlint/config-conventional'],
};
module.exports = {
default: {
formatOptions: {
snippetInterface: "synchronous"
},
paths: [ 'features/**/*.feature' ],
require: [ 'test/cucumber/**/*.steps.js' ],
},
};
import axios from 'axios';
/**
* Handle authentication and send/receive with the backbone
*/
class Adapter {
constructor(protocol, config) {
this.protocol = protocol;
this.config = config;
this.axios = axios;
}
/**
* Test parsing the message based on the provided protocol schema
*
* The message must be successfully json decoded into an object
* prior to validation
*
* At present this returns the validation result which is an
* object containing a boolean valid field as well as details
* of any errors.
* @param {object} message
* @returns {object}
*/
validate(message) {
return this.protocol.validate(message);
}
/**
* Ensure the token in this.credentials is still valid
* @returns {boolean}
*/
tokenValid() {
let valid = false;
if (this.credentials) {
const now = new Date().toISOString();
valid = this.credentials.expiry > now;
}
return valid;
}
/**
* Return an http headers object containing a bearer token authorisation header
* @returns {object}
*/
getAuthorizationHeader() {
if (!this.tokenValid())
return this.auth().then((response) => {
return {
Authorization: `Bearer ${response.data.token}`,
};
});
else {
return Promise.resolve({
Authorization: `Bearer ${this.credentials.token}`,
});
}
}
/**
* Do a client credentials grant and store in this.credentials
* @returns {object}
*/
auth() {
let adapterConfig = this.config;
return this.axios
.get(`${adapterConfig.api}/token`, {
params: {
client_id: adapterConfig.client_id,
secret: adapterConfig.secret,
},
})
.then((response) => {
this.credentials = response.data;
return response;
})
.catch((error) => {
return Promise.reject(error);
});
}
/**
* Call the GET /receive endpoint and process the messages with decode
*
* Returns the response
* @returns {object}
*/
poll() {
let adapterConfig = this.config;
return this.getAuthorizationHeader()
.then((headers) => {
return this.axios.get(`${adapterConfig.api}/receive`, {
headers,
});
})
.then((response) => {
response.data.forEach((message) => {
const parsed = JSON.parse(message.message);
const validation = this.validate(parsed);
if (validation.valid) {
const type = this.protocol.getType(parsed);
this.protocol.decode(type, parsed);
}
});
return response;
})
.catch((error) => {
return Promise.reject(error);
});
}
/**
* Publish a message to the backbone with the specified topic
*
* Messages should be passed through encode before sending
* @param {string} topic
* @param {string} body
* @returns
*/
publish(topic, body) {
let adapterConfig = this.config;
return this.getAuthorizationHeader()
.then((headers) => {
return this.axios.post(
`${adapterConfig.api}/send`,
{
topic,
body,
},
{
headers,
}
);
})
.then((response) => {
return response;
})
.catch((error) => {
return Promise.reject(error);
});
}
/**
* Broadcast the message on the backbone
*
* Broadcast messages bypass the normal publisher queues
* this means they can be used to deliver messages more
* quickly in an emergency scenario.
*
* Messages should be passed through encode before sending
* @param {*} body
* @returns
*/
broadcast(body) {
let adapterConfig = this.config;
return this.getAuthorizationHeader()
.then((headers) => {
return this.axios.post(
`${adapterConfig.api}/notify`,
{
body,
},
{
headers,
}
);
})
.then((response) => {
return response;
})
.catch((error) => {
return Promise.reject(error);
});
}
}
export { Adapter };
'use strict';
var axios = require('axios');
/**
* Handle authentication and send/receive with the backbone
*/
class Adapter {
constructor(protocol, config) {
this.protocol = protocol;
this.config = config;
this.axios = axios;
}
/**
* Test parsing the message based on the provided protocol schema
*
* The message must be successfully json decoded into an object
* prior to validation
*
* At present this returns the validation result which is an
* object containing a boolean valid field as well as details
* of any errors.
* @param {object} message
* @returns {object}
*/
validate(message) {
return this.protocol.validate(message);
}
/**
* Ensure the token in this.credentials is still valid
* @returns {boolean}
*/
tokenValid() {
let valid = false;
if (this.credentials) {
const now = new Date().toISOString();
valid = this.credentials.expiry > now;
}
return valid;
}
/**
* Return an http headers object containing a bearer token authorisation header
* @returns {object}
*/
getAuthorizationHeader() {
if (!this.tokenValid())
return this.auth().then((response) => {
return {
Authorization: `Bearer ${response.data.token}`,
};
});
else {
return Promise.resolve({
Authorization: `Bearer ${this.credentials.token}`,
});
}
}
/**
* Do a client credentials grant and store in this.credentials
* @returns {object}
*/
auth() {
let adapterConfig = this.config;
return this.axios
.get(`${adapterConfig.api}/token`, {
params: {
client_id: adapterConfig.client_id,
secret: adapterConfig.secret,
},
})
.then((response) => {
this.credentials = response.data;
return response;
})
.catch((error) => {
return Promise.reject(error);
});
}
/**
* Call the GET /receive endpoint and process the messages with decode
*
* Returns the response
* @returns {object}
*/
poll() {
let adapterConfig = this.config;
return this.getAuthorizationHeader()
.then((headers) => {
return this.axios.get(`${adapterConfig.api}/receive`, {
headers,
});
})
.then((response) => {
response.data.forEach((message) => {
const parsed = JSON.parse(message.message);
const validation = this.validate(parsed);
if (validation.valid) {
const type = this.protocol.getType(parsed);
this.protocol.decode(type, parsed);
}
});
return response;
})
.catch((error) => {
return Promise.reject(error);
});
}
/**
* Publish a message to the backbone with the specified topic
*
* Messages should be passed through encode before sending
* @param {string} topic
* @param {string} body
* @returns
*/
publish(topic, body) {
let adapterConfig = this.config;
return this.getAuthorizationHeader()
.then((headers) => {
return this.axios.post(
`${adapterConfig.api}/send`,
{
topic,
body,
},
{
headers,
}
);
})
.then((response) => {
return response;
})
.catch((error) => {
return Promise.reject(error);
});
}
/**
* Broadcast the message on the backbone
*
* Broadcast messages bypass the normal publisher queues
* this means they can be used to deliver messages more
* quickly in an emergency scenario.
*
* Messages should be passed through encode before sending
* @param {*} body
* @returns
*/
broadcast(body) {
let adapterConfig = this.config;
return this.getAuthorizationHeader()
.then((headers) => {
return this.axios.post(
`${adapterConfig.api}/notify`,
{
body,
},
{
headers,
}
);
})
.then((response) => {
return response;
})
.catch((error) => {
return Promise.reject(error);
});
}
}
exports.Adapter = Adapter;
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
*/
class GenericProtocol {
constructor(schema, services) {
this.schema = schema;
this.validator = new Validator(schema);
this.services = services;
}
/**
* 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,
true
);
}
/**
* 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;
}
/**
* 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;
}
}
export { GenericProtocol };
'use strict';
var Validator = require('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
*/
class GenericProtocol {
constructor(schema, services) {
this.schema = schema;
this.validator = new Validator(schema);
this.services = services;
}
/**
* 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,
true
);
}
/**
* 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;
}
/**
* 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;
}
}
exports.GenericProtocol = GenericProtocol;
Feature: Does the adapter authenticate?
When an adapter instance is created it authenticates and receives a token
Scenario: A token is granted with valid config
Given valid config
When the adapter instance is created
When the auth method is called
Then the adapter credentials are populated
Scenario: Auth fails with invalid config
Given invalid config
When the adapter instance is created
Then the adapter auth fails
\ No newline at end of file
Feature: Can the adapter broadcast 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 notify API response is configured to return success
When the broadcast 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 notify API response is configured to return an error
When the broadcast method is called
Then an error response is returned with status 403
\ No newline at end of file
Feature: Can the adapter create an authorization header?
The adapter getAuthorizationHeader method works as expected
Scenario: getAuthorizationHeader returns a bearer token
Given valid config
When the adapter instance is created
When the auth method is called
When the getAuthorizationHeader method is called
Then a headers object is returned containing a bearer token authorization header
Scenario: getAuthorizationHeader implicitly calls auth if required
Given valid config
When the adapter instance is created
When the getAuthorizationHeader method is called
Then a headers object is returned containing a bearer token authorization header
Scenario: getAuthorizationHeader implicitly calls auth if required
Given invalid config
When the adapter instance is created
When the getAuthorizationHeader method is called
Then an error response is returned with status 403
\ No newline at end of file
# When the queue contains x messages
# only mocks the API response
# Testing how the API behaves with a full queue are defined in the API
Feature: Can the adapter receive messages?
The adapter poll method works as expected
Scenario: No messages are received succecssfully if the queue is empty
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 0 messages
When the poll method is called
Then a successful response is returned with 0 messages
Scenario: 2 messages are received succecssfully if the queue contains 2 messages
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 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 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
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
Feature: Is the token valid?
The adapter tokenValid method works as expected
Scenario: If adapter has not authed token is invalid
Given valid config
When the adapter instance is created
Then tokenValid returns false
Scenario: If credentials.expiry is in the future token is valid
Given valid config
When the adapter instance is created
When the auth method is called
When the token expiry is in the future
Then tokenValid returns true
Scenario: If credentials.expiry is in the past token is invalid
Given valid config
When the adapter instance is created
When the auth method is called
When the token expiry is in the past
Then tokenValid returns false
Feature: Can the adapter validate messages?
The adapter validate method works as expected
Scenario: A valid message is successfully validated against the protocol schema
Given valid config
Given a valid message
When the adapter instance is created
When the validate method is called
Then the message is validated successfully
Scenario: An invalid message fails to validate against the protocol schema
Given valid config
Given an invalid message
When the adapter instance is created
When the validate method is called
Then the message fails to validate
\ No newline at end of file
# Decode and encode are provided as stubs which are intended to be overridden
# These can be used to translate the message or to invoke other functions
# to take action based on the type and content of messages
Feature: Decode stubs passthru message unchanged
The protocol decode method works as expected
Scenario: Decode passes the message through unaltered
Given a valid message
When the protocol.decode method is called
Then the message is returned unaltered
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