Source
...
Target
Commits (15)
__pycache__/
*.pyc
tests/test_swagger.json
\ No newline at end of file
......@@ -2,6 +2,7 @@ include:
- project: communications-backbone-system/backbone-infrastructure-config
ref: master
file: gitlab/all.yml
- local: /gitlab/test-js.yml
variables:
DOCKER_IMAGE_NAME: backbone-message-format
......
......@@ -10,11 +10,20 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Changed
- Updated README to run tests against JS dependencies
- Use discover to find all unit tests
- Run tests against current format definitions
- Test that formats match saved schema
- Run python and javascript tests in CI
- Refactor schema script to remove invalid definitions object automatically
- Refactor generate_schema_config script to output file on -f flag
### Added
## [v0.2.0] - 2024-02-06
- Emergency flag in mission plan schemas
### Added
- New alert message definition
- Emergency flag in mission plan schemas
- Added additional battery and fuel fields to platform status for SPINE project
## [v0.1.0] - 2023-03-24
......@@ -32,5 +41,6 @@ JSON schema definitions for the SoAR project
Example messages matching the schema for each partner
[unreleased]: https://git.noc.ac.uk/communications-backbone-system/backbone-message-format/compare/v0.2.0...dev
[v0.2.0]: https://git.noc.ac.uk/communications-backbone-system/backbone-message-format/compare/v0.1.0...v0.2.0
[v0.1.0]: https://git.noc.ac.uk/communications-backbone-system/backbone-message-format/compare/9e6ce245...v0.1.0
[unreleased]: https://git.noc.ac.uk/communications-backbone-system/backbone-message-format/compare/v0.1.0...dev
......@@ -72,22 +72,28 @@ eg
## Run Docs & Save Schema When Updating Message Formats
1. Run the command below and go to `http://127.0.0.1:5000`
```
python3 generate_schema_config.py
python3 generate_schema_config.py -f
```
2. Copy the schema generated `http://127.0.0.1:5000/soar_protocol.json` into the `backbone-message-format/project/<project_name>/swagger.json`
3. In the json copied, remove the key `"definitions"` from the schema and save this.
2. Commit any changes to the `backbone-message-format/project/<project_name>/swagger.json`
## Run Tests
We have found slightly different behaviour between the OpenAPI validators in python and javascript.
By running tests in both languages we should protect against messages that pass validation in
one client but fail in another.
Run both tests below:
1. Test 1
1. Test 1 - Use python validators
```
python3 -m unittest tests/test_schemas.py
python3 -m unittest discover
```
2. Test 2
2. Test 2 - Use javascript validators
```
cd tests-js/docker; docker compose up --build
# Compile schema and run javascript validation tests in docker
python3 test-js.py
```
## Quick Links
......
......@@ -12,4 +12,4 @@ services:
- PYTHONDONTWRITEBYTECODE=1
volumes:
- ../:/app
command: "python3 -m unittest tests/test_schemas.py"
\ No newline at end of file
command: "python3 -m unittest discover"
......@@ -14,11 +14,19 @@ from formats.alert import alert_schema
from flasgger import Swagger
from flask import Flask
import argparse
import json
import os
app = Flask(__name__)
url_prefix = os.getenv("URL_PREFIX", "")
# Enable running on domain sub-path
URL_PREFIX = os.getenv("URL_PREFIX", "")
# Allow env override of default host
FLASK_HOST = os.getenv("FLASK_HOST", "localhost")
# Allow env override of default port
FLASK_PORT = os.getenv("FLASK_PORT", 5000)
# Switch on debug mode if env var is truthy
FLASK_DEBUG = os.getenv("FLASK_DEBUG", "False").lower() in ("true", "1", "t")
swagger_config = {
"openapi": "3.0.2",
......@@ -35,7 +43,7 @@ swagger_config = {
"route": "/soar_protocol.json",
}
],
"url_prefix": url_prefix,
"url_prefix": URL_PREFIX,
"paths": {},
"components": {
"schemas": {
......@@ -103,11 +111,125 @@ swagger_config = {
},
}
swag = Swagger(app, config=swagger_config, merge=True)
flask_host = os.getenv(
"FLASK_HOST", "localhost"
) # Sets to whatever FLASK_HOST is, or defaults to localhost
def configure_flask(swagger_config):
"""
Setup a flask app, load flasgger
and then patch to remove invalid
definitions:{} object
"""
app = Flask(__name__)
Swagger(app, config=swagger_config, merge=True)
# Replace schema route to remove invalid
# definitions: {}
# Should be fixed if Flassger 0.9.7 is released
#
# The last release of flasgger was Aug 2020
# This bug was fixed in Nov 2021
# There is a pre-release from May 2023
# Until the fix gets released we have to
# remove the invalid definitions object
# from the spec
@app.after_request
def after_request_decorator(response):
"""
I didn't want to mess with flasgger so
this blunt workaround that runs on every
route and then checks whether it's required
"""
is_response = type(response).__name__ == "Response"
is_json = is_response and response.content_type == "application/json"
if is_json:
parsed = response.json
if "definitions" in parsed:
del parsed["definitions"]
response.data = json.dumps(parsed)
return response
return app
def serve(swagger_config):
"""
Run as local flask app on FLASK_PORT|5000
"""
app = configure_flask(swagger_config)
app.run(debug=FLASK_DEBUG, host=FLASK_HOST, port=FLASK_PORT)
def compile_schema(swagger_config):
"""Extract the output schema from flasgger
The only way I have found to do this is to
use a test client to make the GET request
for the page
The function that returns the definition
can't be called outside the flask app context
"""
app = configure_flask(swagger_config)
route = swagger_config["specs"][0]["route"]
client = app.test_client()
response = client.get(route)
spec = response.json
return spec
def write_schema(swagger_config, file_path):
"""
Dump schema to specified file
"""
spec = compile_schema(swagger_config)
json_schema = json.dumps(spec, indent=2)
with open(file_path, "w") as f:
f.write(json_schema)
def get_options():
"""
Parse script arguments
"""
parser = argparse.ArgumentParser(
description="Generate the schema",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"-s",
"--serve",
dest="run_flask",
action="store_true",
help="Run flask app",
default=False,
)
parser.add_argument(
"-f",
"--file",
dest="output_file",
action="store_true",
help="Save output to schema file",
default=False,
)
parser.add_argument("filename", nargs="?", default="project/soar/swagger.json")
args = parser.parse_args()
config = vars(args)
# If no flag is specified default to running the flask server
if all(v is False for v in config.values()):
config["run_flask"] = True
return config
if __name__ == "__main__":
app.run(debug=False, host=flask_host)
# Parse script args
config = get_options()
# Output compiled schema
if config.get("output_file"):
write_schema(swagger_config, config.get("filename"))
# Run flask app
if config.get("run_flask"):
serve(swagger_config)
test-js:
stage: test
script:
- cd tests-js/docker
- docker compose up --build
tags:
- shell
rules:
- if: '$CI_SKIP_TESTS == "1"'
when: never
- if: '$CI_IGNORE_TESTS == "1"'
allow_failure: true
- when: on_success
......@@ -1150,10 +1150,10 @@
}
}
},
"info": {
"description": "SoAR message protocol in schemas",
"title": "SoAR Backbone Message Formats",
"version": "1.0"
"info":{
"description":"SoAR message protocol in schemas",
"title":"SoAR Backbone Message Formats",
"version":"0.2"
},
"openapi": "3.0.2",
"paths": {}
......
......@@ -2,7 +2,7 @@ from setuptools import setup
setup(
version="0.1.0",
version="0.2.0",
name="backbone_message_format",
python_requires=">=3.8",
packages=["backbone_formats", "soar_schema"],
......
FROM node:18.7.0-alpine
FROM python:3.9.16-alpine
# Add node for running JS validator tests
RUN apk add --update nodejs npm
WORKDIR /app/tests-js
COPY tests-js/package.json /app/tests-js/package.json
RUN npm install -g yarn
RUN yarn install
WORKDIR /app
COPY . /app
# compile test schema from message
RUN pip install -r requirements.txt
RUN python generate_schema_config.py -f tests/test_swagger.json
WORKDIR /app/tests-js
CMD [ 'yarn', 'test' ]
......@@ -3,7 +3,7 @@ const Validator = require('swagger-model-validator');
const OpenAPISchemaValidator = require('openapi-schema-validator').default;
const getSchema = () => {
const schema = require(`${__dirname}/../project/soar/swagger.json`);
const schema = require(`${__dirname}/../tests/test_swagger.json`);
return schema;
};
......
This diff is collapsed.
......@@ -5,12 +5,28 @@ from jsonschema.validators import RefResolver
import unittest
import json
import os
from generate_schema_config import write_schema, swagger_config
SCHEMA_DIR = "project/soar/swagger.json"
MOCK_DATA_DIR = "examples/"
class SchemaTestCase(unittest.TestCase):
def load_schema(self, file_path=None):
if file_path:
schema_path = file_path
else:
schema_path = os.getenv("SCHEMA_PATH")
schema, _ = read_from_filename(schema_path)
return schema
@classmethod
def setUpClass(cls):
test_schema_path = "tests/test_swagger.json"
os.environ["SCHEMA_PATH"] = test_schema_path
write_schema(swagger_config, test_schema_path)
class SchemaError(Exception):
"""
Test config specs of swagger.json for projects
......@@ -19,26 +35,37 @@ class SchemaError(Exception):
def __init__(self, exception_type, message):
self.type = exception_type
self.message = message
super().__init__(self.message)
super().__init__(message)
class TestSpecs(unittest.TestCase):
class TestCompiledSoarSpec(SchemaTestCase):
"""
Test the saved version of the compiled schema has been updated
"""
def test_compiled_spec_matches_definitions(self):
print("TEST: definitions match project/soar/swagger.json")
derived_schema = self.load_schema()
saved_schema = self.load_schema("project/soar/swagger.json")
self.assertEqual(saved_schema, derived_schema)
class TestSpecs(SchemaTestCase):
def test_swagger_specs(self):
"""
Test specs (swagger.json generated from generate_schema_config.py)
"""
schema, spec_url = read_from_filename("tests/fixtures/swagger.json")
print("TEST: compiled schema matches openapi v3 spec")
schema = self.load_schema()
self.assertIsNone(openapi_v30_spec_validator.validate(schema))
class TestAllMessageExamples(unittest.TestCase):
class TestAllMessageExamples(SchemaTestCase):
def test_schema_specs(self):
"""
Test specs (swagger.json generated from generate_schema_config.py)
"""
schema_ref = open(SCHEMA_DIR)
schema = json.load(schema_ref)
schema_ref.close()
schema = self.load_schema()
partner_dir_list = os.listdir(MOCK_DATA_DIR)
......@@ -70,8 +97,6 @@ class TestAllMessageExamples(unittest.TestCase):
f.close()
print("Done.")
schema_ref.close()
if __name__ == "__main__":
unittest.main()