From 7ecac0269346c11a32fb73c693c6c144da4bc57f Mon Sep 17 00:00:00 2001 From: Tom Kralidis Date: Tue, 29 Jun 2021 08:48:22 -0400 Subject: [PATCH] add support for OpenAPI validation (#717) (#718) * add support for OpenAPI validation (#717) * update copyright years * fix ES ref * update CLI --- MANIFEST.in | 2 +- debian/control | 1 + docker/entrypoint.sh | 2 +- docs/source/administration.rst | 20 +- docs/source/installation.rst | 2 +- pygeoapi/__init__.py | 6 +- pygeoapi/openapi.py | 68 +- pygeoapi/provider/elasticsearch_.py | 8 +- pygeoapi/schemas/openapi/openapi-3.0.x.json | 1654 +++++++++++++++++++ requirements.txt | 1 + tests/__init__.py | 28 + tests/cite/ogcapi-features/README.md | 2 +- tests/pygeoapi-test-openapi.yml | 54 +- tests/test_api.py | 40 +- tests/test_config.py | 11 +- tests/test_csv__provider.py | 14 +- tests/test_l10n.py | 14 +- tests/test_openapi.py | 38 +- tests/test_rasterio_provider.py | 13 +- tests/test_tinydb_catalogue_provider.py | 12 +- tests/test_util.py | 12 +- tests/test_xarray_netcdf_provider.py | 13 +- tests/test_xarray_zarr_provider.py | 13 +- tests/util.py | 71 + 24 files changed, 1921 insertions(+), 178 deletions(-) create mode 100644 pygeoapi/schemas/openapi/openapi-3.0.x.json create mode 100644 tests/__init__.py create mode 100644 tests/util.py diff --git a/MANIFEST.in b/MANIFEST.in index fc2e56e..92505fd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ include README.md LICENSE.md requirements.txt -recursive-include pygeoapi *.html +recursive-include pygeoapi *.html *.json recursive-include pygeoapi/static * diff --git a/debian/control b/debian/control index 6bb97f5..3020962 100644 --- a/debian/control +++ b/debian/control @@ -18,6 +18,7 @@ Depends: ${python3:Depends}, python3-click, python3-dateutil, python3-flask, + python3-jsonschema, python3-tz, python3-unicodecsv, python3-yaml, diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index c284710..a0c2109 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -60,7 +60,7 @@ function error() { cd ${PYGEOAPI_HOME} echo "Trying to generate openapi.yml" -pygeoapi generate-openapi-document -c ${PYGEOAPI_CONFIG} > ${PYGEOAPI_OPENAPI} +pygeoapi openapi generate -c ${PYGEOAPI_CONFIG} > ${PYGEOAPI_OPENAPI} [[ $? -ne 0 ]] && error "openapi.yml could not be generated ERROR" diff --git a/docs/source/administration.rst b/docs/source/administration.rst index f6724a8..5df5f52 100644 --- a/docs/source/administration.rst +++ b/docs/source/administration.rst @@ -7,31 +7,32 @@ Now that you have pygeoapi installed and a basic configuration setup, it's time the administrative steps required before starting up the server. The remaining steps are: - create OpenAPI document +- validate OpenAPI document - set system environment variables Creating the OpenAPI document ----------------------------- -The OpenAPI document ia a YAML configuration which is generated from the pygeoapi configuration, +The OpenAPI document is a YAML configuration which is generated from the pygeoapi configuration, and describes the server information, endpoints, and parameters. To generate the OpenAPI document, run the following: .. code-block:: bash - pygeoapi generate-openapi-document -c /path/to/my-pygeoapi-config.yml + pygeoapi openapi generate -c /path/to/my-pygeoapi-config.yml This will dump the OpenAPI document as YAML to your system's ``stdout``. To save to a file on disk, run: .. code-block:: bash - pygeoapi generate-openapi-document -c /path/to/my-pygeoapi-config.yml > /path/to/my-pygeoapi-openapi.yml + pygeoapi openapi generate -c /path/to/my-pygeoapi-config.yml > /path/to/my-pygeoapi-openapi.yml To generate the OpenAPI document as JSON, run: .. code-block:: bash - pygeoapi generate-openapi-document -c /path/to/my-pygeoapi-config.yml -f json > /path/to/my-pygeoapi-openapi.json + pygeoapi openapi generate -c /path/to/my-pygeoapi-config.yml -f json > /path/to/my-pygeoapi-openapi.json .. note:: Generate as YAML or JSON? If your OpenAPI YAML definition is slow to render as JSON, @@ -47,6 +48,17 @@ To generate the OpenAPI document as JSON, run: :ref:`openapi` for more information on pygeoapi's OpenAPI support +Validating the OpenAPI document +------------------------------- + +To ensure your OpenAPI document is valid, pygeoapi provides a validation +utility that can be run as follows: + +.. code-block:: bash + + pygeoapi validate-openapi-document -o /path/to/my-pygeoapi-openapi.yml + + Verifying configuration files ----------------------------- diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 0a4b61e..0cd54d0 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -31,7 +31,7 @@ For developers and the truly impatient vi example-config.yml export PYGEOAPI_CONFIG=example-config.yml export PYGEOAPI_OPENAPI=example-openapi.yml - pygeoapi generate-openapi-document -c $PYGEOAPI_CONFIG > $PYGEOAPI_OPENAPI + pygeoapi openapi generate -c $PYGEOAPI_CONFIG > $PYGEOAPI_OPENAPI pygeoapi serve curl http://localhost:5000 diff --git a/pygeoapi/__init__.py b/pygeoapi/__init__.py index 0396238..ce3a65f 100644 --- a/pygeoapi/__init__.py +++ b/pygeoapi/__init__.py @@ -2,7 +2,7 @@ # # Authors: Tom Kralidis # -# Copyright (c) 2018 Tom Kralidis +# Copyright (c) 2021 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -30,7 +30,7 @@ __version__ = '0.11.dev0' import click -from pygeoapi.openapi import generate_openapi_document +from pygeoapi.openapi import openapi @click.group() @@ -58,4 +58,4 @@ def serve(ctx, server): raise click.ClickException('--flask/--starlette is required') -cli.add_command(generate_openapi_document) +cli.add_command(openapi) diff --git a/pygeoapi/openapi.py b/pygeoapi/openapi.py index abab0ee..1f7a4ea 100644 --- a/pygeoapi/openapi.py +++ b/pygeoapi/openapi.py @@ -28,10 +28,12 @@ # ================================================================= from copy import deepcopy +import json import logging import os import click +from jsonschema import validate as jsonschema_validate import yaml from pygeoapi import __version__ @@ -54,6 +56,8 @@ OPENAPI_YAML = { 'oat': 'https://raw.githubusercontent.com/opengeospatial/ogcapi-tiles/master/openapi/swaggerHubUnresolved/ogc-api-tiles.yaml', # noqa } +THISDIR = os.path.dirname(os.path.realpath(__file__)) + def get_ogc_schemas_location(server_config): @@ -417,7 +421,7 @@ def get_oas_30(cfg): 'get': { 'summary': 'Get {} metadata'.format(title), 'description': desc, - 'tags': name, + 'tags': [name], 'operationId': 'describe{}Collection'.format(name.capitalize()), # noqa 'parameters': [ {'$ref': '#/components/parameters/f'}, @@ -508,7 +512,7 @@ def get_oas_30(cfg): paths[items_path]['get']['parameters'].append( {'$ref': '{}#/components/parameters/datetime'.format(OPENAPI_YAML['oapif'])}) # noqa - for field, type in p.fields.items(): + for field, type_ in p.fields.items(): if p.properties and field not in p.properties: LOGGER.debug('Provider specified not to advertise property') # noqa @@ -518,25 +522,23 @@ def get_oas_30(cfg): LOGGER.debug('q parameter already declared, skipping') continue - if type == 'date': + if type_ == 'date': schema = { 'type': 'string', 'format': 'date' } - elif type == 'float': + elif type_ == 'float': schema = { 'type': 'number', 'format': 'float' } - elif type == 'long': + elif type_ == 'long': schema = { 'type': 'integer', 'format': 'int64' } else: - schema = { - 'type': type - } + schema = type_ path_ = '{}/items'.format(collection_name_path) paths['{}'.format(path_)]['get']['parameters'].append({ @@ -756,6 +758,7 @@ def get_oas_30(cfg): } mimetype = tile_extension['format']['mimetype'] paths[tiles_data_path]['get']['responses']['200'] = { + 'description': 'successful operation', 'content': { mimetype: { 'schema': { @@ -1027,12 +1030,37 @@ def get_oas(cfg, version='3.0'): raise RuntimeError('OpenAPI version not supported') -@click.command('generate-openapi-document') +def validate_openapi_document(instance_dict): + """ + Validate an OpenAPI document against the OpenAPI schema + + :param instance_dict: dict of OpenAPI instance + + :returns: `bool` of validation + """ + + schema_file = os.path.join(THISDIR, 'schemas', 'openapi', + 'openapi-3.0.x.json') + + with open(schema_file) as fh2: + schema_dict = json.load(fh2) + jsonschema_validate(instance_dict, schema_dict) + + return True + + +@click.group() +def openapi(): + """OpenAPI management""" + pass + + +@click.command() @click.pass_context @click.option('--config', '-c', 'config_file', help='configuration file') @click.option('--format', '-f', 'format_', type=click.Choice(['json', 'yaml']), default='yaml', help='output format (json|yaml)') -def generate_openapi_document(ctx, config_file, format_='yaml'): +def generate(ctx, config_file, format_='yaml'): """Generate OpenAPI Document""" if config_file is None: @@ -1044,3 +1072,23 @@ def generate_openapi_document(ctx, config_file, format_='yaml'): click.echo(yaml.safe_dump(get_oas(s), default_flow_style=False)) else: click.echo(to_json(get_oas(s), pretty=pretty_print)) + + +@click.command() +@click.pass_context +@click.option('--openapi', '-o', 'openapi_file', help='OpenAPI document') +def validate(ctx, openapi_file): + """Validate OpenAPI Document""" + + if openapi_file is None: + raise click.ClickException('--openapi/-o required') + + with open(openapi_file) as ff: + click.echo('Validating {}'.format(openapi_file)) + instance = yaml_load(ff) + validate_openapi_document(instance) + click.echo('Valid OpenAPI document') + + +openapi.add_command(generate) +openapi.add_command(validate) diff --git a/pygeoapi/provider/elasticsearch_.py b/pygeoapi/provider/elasticsearch_.py index 85913cc..c58f3a7 100644 --- a/pygeoapi/provider/elasticsearch_.py +++ b/pygeoapi/provider/elasticsearch_.py @@ -134,11 +134,11 @@ class ElasticsearchProvider(BaseProvider): for k, v in p['properties'].items(): if 'type' in v: if v['type'] == 'text': - type_ = 'string' + fields_[k] = {'type': 'string'} + elif v['type'] == 'date': + fields_[k] = {'type': 'string', 'format': 'date'} else: - type_ = v['type'] - - fields_[k] = {'type': type_} + fields_[k] = {'type': v['type']} return fields_ diff --git a/pygeoapi/schemas/openapi/openapi-3.0.x.json b/pygeoapi/schemas/openapi/openapi-3.0.x.json new file mode 100644 index 0000000..99cdf5d --- /dev/null +++ b/pygeoapi/schemas/openapi/openapi-3.0.x.json @@ -0,0 +1,1654 @@ +{ + "id": "https://spec.openapis.org/oas/3.0/schema/2019-04-02", + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Validation schema for OpenAPI Specification 3.0.X.", + "type": "object", + "required": [ + "openapi", + "info", + "paths" + ], + "properties": { + "openapi": { + "type": "string", + "pattern": "^3\\.0\\.\\d(-.+)?$" + }, + "info": { + "$ref": "#/definitions/Info" + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/definitions/Server" + } + }, + "security": { + "type": "array", + "items": { + "$ref": "#/definitions/SecurityRequirement" + } + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/Tag" + }, + "uniqueItems": true + }, + "paths": { + "$ref": "#/definitions/Paths" + }, + "components": { + "$ref": "#/definitions/Components" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "definitions": { + "Reference": { + "type": "object", + "required": [ + "$ref" + ], + "patternProperties": { + "^\\$ref$": { + "type": "string", + "format": "uri-reference" + } + } + }, + "Info": { + "type": "object", + "required": [ + "title", + "version" + ], + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "termsOfService": { + "type": "string", + "format": "uri-reference" + }, + "contact": { + "$ref": "#/definitions/Contact" + }, + "license": { + "$ref": "#/definitions/License" + }, + "version": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Contact": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + }, + "email": { + "type": "string", + "format": "email" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "License": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Server": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "variables": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/ServerVariable" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "ServerVariable": { + "type": "object", + "required": [ + "default" + ], + "properties": { + "enum": { + "type": "array", + "items": { + "type": "string" + } + }, + "default": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Components": { + "type": "object", + "properties": { + "schemas": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "responses": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Response" + } + ] + } + } + }, + "parameters": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Parameter" + } + ] + } + } + }, + "examples": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Example" + } + ] + } + } + }, + "requestBodies": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/RequestBody" + } + ] + } + } + }, + "headers": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Header" + } + ] + } + } + }, + "securitySchemes": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/SecurityScheme" + } + ] + } + } + }, + "links": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Link" + } + ] + } + } + }, + "callbacks": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Callback" + } + ] + } + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "multipleOf": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "boolean", + "default": false + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "boolean", + "default": false + }, + "maxLength": { + "type": "integer", + "minimum": 0 + }, + "minLength": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "pattern": { + "type": "string", + "format": "regex" + }, + "maxItems": { + "type": "integer", + "minimum": 0 + }, + "minItems": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxProperties": { + "type": "integer", + "minimum": 0 + }, + "minProperties": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "required": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, + "enum": { + "type": "array", + "items": { + }, + "minItems": 1, + "uniqueItems": false + }, + "type": { + "type": "string", + "enum": [ + "array", + "boolean", + "integer", + "number", + "object", + "string" + ] + }, + "not": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "allOf": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "oneOf": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "anyOf": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "properties": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + }, + { + "type": "boolean" + } + ], + "default": true + }, + "description": { + "type": "string" + }, + "format": { + "type": "string" + }, + "default": { + }, + "nullable": { + "type": "boolean", + "default": false + }, + "discriminator": { + "$ref": "#/definitions/Discriminator" + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "example": { + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "xml": { + "$ref": "#/definitions/XML" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Discriminator": { + "type": "object", + "required": [ + "propertyName" + ], + "properties": { + "propertyName": { + "type": "string" + }, + "mapping": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "XML": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string", + "format": "uri" + }, + "prefix": { + "type": "string" + }, + "attribute": { + "type": "boolean", + "default": false + }, + "wrapped": { + "type": "boolean", + "default": false + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Response": { + "type": "object", + "required": [ + "description" + ], + "properties": { + "description": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Header" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + } + }, + "links": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Link" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "MediaType": { + "type": "object", + "properties": { + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "example": { + }, + "examples": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Example" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "encoding": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Encoding" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "allOf": [ + { + "$ref": "#/definitions/ExampleXORExamples" + } + ] + }, + "Example": { + "type": "object", + "properties": { + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "value": { + }, + "externalValue": { + "type": "string", + "format": "uri-reference" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Header": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "required": { + "type": "boolean", + "default": false + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "allowEmptyValue": { + "type": "boolean", + "default": false + }, + "style": { + "type": "string", + "enum": [ + "simple" + ], + "default": "simple" + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean", + "default": false + }, + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + }, + "minProperties": 1, + "maxProperties": 1 + }, + "example": { + }, + "examples": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Example" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "allOf": [ + { + "$ref": "#/definitions/ExampleXORExamples" + }, + { + "$ref": "#/definitions/SchemaXORContent" + } + ] + }, + "Paths": { + "type": "object", + "patternProperties": { + "^\\/": { + "$ref": "#/definitions/PathItem" + }, + "^x-": { + } + }, + "additionalProperties": false + }, + "PathItem": { + "type": "object", + "properties": { + "$ref": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/definitions/Server" + } + }, + "parameters": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Parameter" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "uniqueItems": true + } + }, + "patternProperties": { + "^(get|put|post|delete|options|head|patch|trace)$": { + "$ref": "#/definitions/Operation" + }, + "^x-": { + } + }, + "additionalProperties": false + }, + "Operation": { + "type": "object", + "required": [ + "responses" + ], + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + }, + "operationId": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Parameter" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "uniqueItems": true + }, + "requestBody": { + "oneOf": [ + { + "$ref": "#/definitions/RequestBody" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "responses": { + "$ref": "#/definitions/Responses" + }, + "callbacks": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Callback" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "security": { + "type": "array", + "items": { + "$ref": "#/definitions/SecurityRequirement" + } + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/definitions/Server" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Responses": { + "type": "object", + "properties": { + "default": { + "oneOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "patternProperties": { + "^[1-5](?:\\d{2}|XX)$": { + "oneOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "^x-": { + } + }, + "minProperties": 1, + "additionalProperties": false + }, + "SecurityRequirement": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "Tag": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "ExternalDocumentation": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "ExampleXORExamples": { + "description": "Example and examples are mutually exclusive", + "not": { + "required": [ + "example", + "examples" + ] + } + }, + "SchemaXORContent": { + "description": "Schema and content are mutually exclusive, at least one is required", + "not": { + "required": [ + "schema", + "content" + ] + }, + "oneOf": [ + { + "required": [ + "schema" + ] + }, + { + "required": [ + "content" + ], + "description": "Some properties are not allowed if content is present", + "allOf": [ + { + "not": { + "required": [ + "style" + ] + } + }, + { + "not": { + "required": [ + "explode" + ] + } + }, + { + "not": { + "required": [ + "allowReserved" + ] + } + }, + { + "not": { + "required": [ + "example" + ] + } + }, + { + "not": { + "required": [ + "examples" + ] + } + } + ] + } + ] + }, + "Parameter": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "in": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean", + "default": false + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "allowEmptyValue": { + "type": "boolean", + "default": false + }, + "style": { + "type": "string" + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean", + "default": false + }, + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + }, + "minProperties": 1, + "maxProperties": 1 + }, + "example": { + }, + "examples": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Example" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "required": [ + "name", + "in" + ], + "allOf": [ + { + "$ref": "#/definitions/ExampleXORExamples" + }, + { + "$ref": "#/definitions/SchemaXORContent" + }, + { + "$ref": "#/definitions/ParameterLocation" + } + ] + }, + "ParameterLocation": { + "description": "Parameter location", + "oneOf": [ + { + "description": "Parameter in path", + "required": [ + "required" + ], + "properties": { + "in": { + "enum": [ + "path" + ] + }, + "style": { + "enum": [ + "matrix", + "label", + "simple" + ], + "default": "simple" + }, + "required": { + "enum": [ + true + ] + } + } + }, + { + "description": "Parameter in query", + "properties": { + "in": { + "enum": [ + "query" + ] + }, + "style": { + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ], + "default": "form" + } + } + }, + { + "description": "Parameter in header", + "properties": { + "in": { + "enum": [ + "header" + ] + }, + "style": { + "enum": [ + "simple" + ], + "default": "simple" + } + } + }, + { + "description": "Parameter in cookie", + "properties": { + "in": { + "enum": [ + "cookie" + ] + }, + "style": { + "enum": [ + "form" + ], + "default": "form" + } + } + } + ] + }, + "RequestBody": { + "type": "object", + "required": [ + "content" + ], + "properties": { + "description": { + "type": "string" + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + } + }, + "required": { + "type": "boolean", + "default": false + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "SecurityScheme": { + "oneOf": [ + { + "$ref": "#/definitions/APIKeySecurityScheme" + }, + { + "$ref": "#/definitions/HTTPSecurityScheme" + }, + { + "$ref": "#/definitions/OAuth2SecurityScheme" + }, + { + "$ref": "#/definitions/OpenIdConnectSecurityScheme" + } + ] + }, + "APIKeySecurityScheme": { + "type": "object", + "required": [ + "type", + "name", + "in" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "apiKey" + ] + }, + "name": { + "type": "string" + }, + "in": { + "type": "string", + "enum": [ + "header", + "query", + "cookie" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "HTTPSecurityScheme": { + "type": "object", + "required": [ + "scheme", + "type" + ], + "properties": { + "scheme": { + "type": "string" + }, + "bearerFormat": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "http" + ] + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "oneOf": [ + { + "description": "Bearer", + "properties": { + "scheme": { + "enum": [ + "bearer" + ] + } + } + }, + { + "description": "Non Bearer", + "not": { + "required": [ + "bearerFormat" + ] + }, + "properties": { + "scheme": { + "not": { + "enum": [ + "bearer" + ] + } + } + } + } + ] + }, + "OAuth2SecurityScheme": { + "type": "object", + "required": [ + "type", + "flows" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flows": { + "$ref": "#/definitions/OAuthFlows" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "OpenIdConnectSecurityScheme": { + "type": "object", + "required": [ + "type", + "openIdConnectUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "openIdConnect" + ] + }, + "openIdConnectUrl": { + "type": "string", + "format": "uri-reference" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "OAuthFlows": { + "type": "object", + "properties": { + "implicit": { + "$ref": "#/definitions/ImplicitOAuthFlow" + }, + "password": { + "$ref": "#/definitions/PasswordOAuthFlow" + }, + "clientCredentials": { + "$ref": "#/definitions/ClientCredentialsFlow" + }, + "authorizationCode": { + "$ref": "#/definitions/AuthorizationCodeOAuthFlow" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "ImplicitOAuthFlow": { + "type": "object", + "required": [ + "authorizationUrl", + "scopes" + ], + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "PasswordOAuthFlow": { + "type": "object", + "required": [ + "tokenUrl" + ], + "properties": { + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "ClientCredentialsFlow": { + "type": "object", + "required": [ + "tokenUrl" + ], + "properties": { + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "AuthorizationCodeOAuthFlow": { + "type": "object", + "required": [ + "authorizationUrl", + "tokenUrl" + ], + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri-reference" + }, + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Link": { + "type": "object", + "properties": { + "operationId": { + "type": "string" + }, + "operationRef": { + "type": "string", + "format": "uri-reference" + }, + "parameters": { + "type": "object", + "additionalProperties": { + } + }, + "requestBody": { + }, + "description": { + "type": "string" + }, + "server": { + "$ref": "#/definitions/Server" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "not": { + "description": "Operation Id and Operation Ref are mutually exclusive", + "required": [ + "operationId", + "operationRef" + ] + } + }, + "Callback": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/PathItem" + }, + "patternProperties": { + "^x-": { + } + } + }, + "Encoding": { + "type": "object", + "properties": { + "contentType": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Header" + } + }, + "style": { + "type": "string", + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ] + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + } + } +} diff --git a/requirements.txt b/requirements.txt index fb66494..34579bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ Babel click<8 Flask +jsonschema pyproj python-dateutil pytz diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..52d71ea --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,28 @@ +# ================================================================= +# +# Authors: Tom Kralidis +# +# Copyright (c) 2021 Tom Kralidis +# +# 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. +# +# ================================================================= diff --git a/tests/cite/ogcapi-features/README.md b/tests/cite/ogcapi-features/README.md index d3525ec..cdb55bd 100644 --- a/tests/cite/ogcapi-features/README.md +++ b/tests/cite/ogcapi-features/README.md @@ -14,6 +14,6 @@ pip install gunicorn cd tests/cite/ogcapi-features . cite.env python ../../load_es_data.py ./canada-hydat-daily-mean-02hc003.geojson IDENTIFIER -pygeoapi generate-openapi-document -c $PYGEOAPI_CONFIG > $PYGEOAPI_OPENAPI +pygeoapi openapi generate -c $PYGEOAPI_CONFIG > $PYGEOAPI_OPENAPI gunicorn pygeoapi.flask_app:APP -b 0.0.0.0:5001 --access-logfile '-' ``` diff --git a/tests/pygeoapi-test-openapi.yml b/tests/pygeoapi-test-openapi.yml index e29e79e..ca04377 100644 --- a/tests/pygeoapi-test-openapi.yml +++ b/tests/pygeoapi-test-openapi.yml @@ -102,11 +102,11 @@ paths: parameters: - $ref: '#/components/parameters/f' responses: - 200: + '200': $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/LandingPage - 400: + '400': $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - 500: + '500': $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError summary: Landing page tags: @@ -117,9 +117,9 @@ paths: parameters: - $ref: '#/components/parameters/f' responses: - 200: + '200': $ref: '#/components/responses/200' - 400: + '400': $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter default: $ref: '#/components/responses/default' @@ -132,11 +132,11 @@ paths: parameters: - $ref: '#/components/parameters/f' responses: - 200: + '200': $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Collections - 400: + '400': $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - 500: + '500': $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError summary: Feature Collections tags: @@ -147,13 +147,13 @@ paths: parameters: - $ref: '#/components/parameters/f' responses: - 200: + '200': $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Collection - 400: + '400': $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - 404: + '404': $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound - 500: + '500': $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError summary: Get feature collection metadata tags: @@ -182,13 +182,13 @@ paths: - $ref: '#/components/parameters/sortby' - $ref: '#/components/parameters/startindex' responses: - 200: + '200': $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Features - 400: + '400': $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - 404: + '404': $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound - 500: + '500': $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError summary: Get Observations features tags: @@ -200,13 +200,13 @@ paths: - $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/featureId - $ref: '#/components/parameters/f' responses: - 200: + '200': $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Feature - 400: + '400': $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - 404: + '404': $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound - 500: + '500': $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError summary: Get Observations feature by id tags: @@ -217,11 +217,11 @@ paths: parameters: - $ref: '#/components/parameters/f' responses: - 200: + '200': $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ConformanceDeclaration - 400: + '400': $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - 500: + '500': $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError summary: API conformance definition tags: @@ -232,7 +232,7 @@ paths: parameters: - $ref: '#/components/parameters/f' responses: - 200: + '200': $ref: '#/components/responses/200' default: $ref: '#/components/responses/default' @@ -245,7 +245,7 @@ paths: parameters: - $ref: '#/components/parameters/f' responses: - 200: + '200': $ref: '#/components/responses/200' default: $ref: '#/components/responses/default' @@ -256,7 +256,7 @@ paths: get: description: Hello World process responses: - 200: + '200': $ref: '#/components/responses/200' default: $ref: '#/components/responses/default' @@ -279,7 +279,7 @@ paths: description: Mandatory execute request JSON required: true responses: - 200: + '200': $ref: '#/components/responses/200' default: $ref: '#/components/responses/default' diff --git a/tests/test_api.py b/tests/test_api.py index 29f761f..5938a94 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -28,58 +28,22 @@ # ================================================================= import json -import os import logging import time from pyld import jsonld import pytest -from werkzeug.test import create_environ -from werkzeug.wrappers import Request -from werkzeug.datastructures import ImmutableMultiDict from pygeoapi.api import ( API, APIRequest, FORMAT_TYPES, validate_bbox, validate_datetime, F_HTML, F_JSON, F_JSONLD ) from pygeoapi.util import yaml_load +from .util import get_test_file_path, mock_request + LOGGER = logging.getLogger(__name__) -def get_test_file_path(filename): - """helper function to open test file safely""" - - if os.path.isfile(filename): - return filename - else: - return 'tests/{}'.format(filename) - - -def mock_request(params: dict = None, data=None, **headers) -> Request: - """ - Mocks a Request object so the @pre_process decorator can inject it - as an APIRequest. - - :param params: Optional query parameter dict for the request. - Will be set to {} if omitted. - :param data: Optional data/body to send with the request. - Can be text/bytes or a JSON dictionary. - :param headers: Optional request HTTP headers to set. - :returns: A Werkzeug Request instance. - """ - params = params or {} - # TODO: We are not setting a path in the create_environ() call. - # This is fine as long as an API test does not need the URL path. - if isinstance(data, dict): - environ = create_environ(base_url='http://localhost:5000/', json=data) - else: - environ = create_environ(base_url='http://localhost:5000/', data=data) - environ.update(headers) - request = Request(environ) - request.args = ImmutableMultiDict(params.items()) - return request - - @pytest.fixture() def config(): with open(get_test_file_path('pygeoapi-test-config.yml')) as fh: diff --git a/tests/test_config.py b/tests/test_config.py index 7d2b455..73dc49c 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -2,7 +2,7 @@ # # Authors: Tom Kralidis # -# Copyright (c) 2019 Tom Kralidis +# Copyright (c) 2021 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -33,14 +33,7 @@ import pytest from pygeoapi.util import yaml_load - -def get_test_file_path(filename): - """helper function to open test file safely""" - - if os.path.isfile(filename): - return filename - else: - return 'tests/{}'.format(filename) +from .util import get_test_file_path def test_config_envvars(): diff --git a/tests/test_csv__provider.py b/tests/test_csv__provider.py index 3550832..5ad4112 100644 --- a/tests/test_csv__provider.py +++ b/tests/test_csv__provider.py @@ -2,7 +2,7 @@ # # Authors: Tom Kralidis # -# Copyright (c) 2018 Tom Kralidis +# Copyright (c) 2021 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -27,22 +27,12 @@ # # ================================================================= -import os - import pytest from pygeoapi.provider.base import ProviderItemNotFoundError from pygeoapi.provider.csv_ import CSVProvider - -def get_test_file_path(filename): - """helper function to open test file safely""" - - if os.path.isfile(filename): - return filename - else: - return 'tests/{}'.format(filename) - +from .util import get_test_file_path path = get_test_file_path('data/obs.csv') diff --git a/tests/test_l10n.py b/tests/test_l10n.py index 04025a2..d195907 100644 --- a/tests/test_l10n.py +++ b/tests/test_l10n.py @@ -26,13 +26,14 @@ # OTHER DEALINGS IN THE SOFTWARE. # # ================================================================= -import os from babel import Locale +import pytest + from pygeoapi import l10n from pygeoapi.util import yaml_load -import pytest +from .util import get_test_file_path def test_str2locale(): @@ -223,15 +224,6 @@ def test_setresponselanguage(): assert headers['Content-Language'] == 'en' -def get_test_file_path(filename): - """helper function to open test file safely""" - - if os.path.isfile(filename): - return filename - else: - return 'tests/{}'.format(filename) - - @pytest.fixture() def config(): with open(get_test_file_path('pygeoapi-test-config.yml')) as fh: diff --git a/tests/test_openapi.py b/tests/test_openapi.py index 7098be6..0c210c6 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -2,7 +2,7 @@ # # Authors: Tom Kralidis # -# Copyright (c) 2020 Tom Kralidis +# Copyright (c) 2021 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -27,7 +27,25 @@ # # ================================================================= -from pygeoapi.openapi import get_ogc_schemas_location +import pytest + +from pygeoapi.openapi import (get_oas, get_ogc_schemas_location, + validate_openapi_document) +from pygeoapi.util import yaml_load + +from .util import get_test_file_path + + +@pytest.fixture() +def config(): + with open(get_test_file_path('pygeoapi-test-config.yml')) as fh: + return yaml_load(fh) + + +@pytest.fixture() +def openapi(): + with open(get_test_file_path('pygeoapi-test-openapi.yml')) as fh: + return yaml_load(fh) def test_str2bool(): @@ -45,3 +63,19 @@ def test_str2bool(): default['ogc_schemas_location'] = '/opt/schemas.opengis.net' osl = get_ogc_schemas_location(default) + + +def test_get_oas(config, openapi): + openapi_doc = get_oas(config) + + assert isinstance(openapi_doc, dict) + + is_valid = validate_openapi_document(openapi_doc) + + assert is_valid is True + + +def test_validate_openapi_document(openapi): + is_valid = validate_openapi_document(openapi) + + assert is_valid is True diff --git a/tests/test_rasterio_provider.py b/tests/test_rasterio_provider.py index 01c49ce..f1b4471 100644 --- a/tests/test_rasterio_provider.py +++ b/tests/test_rasterio_provider.py @@ -2,7 +2,7 @@ # # Authors: Tom Kralidis # -# Copyright (c) 2020 Tom Kralidis +# Copyright (c) 2021 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -27,20 +27,11 @@ # # ================================================================= -import os import pytest from pygeoapi.provider.rasterio_ import RasterioProvider - -def get_test_file_path(filename): - """helper function to open test file safely""" - - if os.path.isfile(filename): - return filename - else: - return 'tests/{}'.format(filename) - +from .util import get_test_file_path path = get_test_file_path( 'tests/data/CMC_glb_TMP_TGL_2_latlon.15x.15_2020081000_P000.grib2') diff --git a/tests/test_tinydb_catalogue_provider.py b/tests/test_tinydb_catalogue_provider.py index 55e7539..ebbd0cf 100644 --- a/tests/test_tinydb_catalogue_provider.py +++ b/tests/test_tinydb_catalogue_provider.py @@ -27,22 +27,12 @@ # # ================================================================= -import os - import pytest from pygeoapi.provider.base import ProviderItemNotFoundError from pygeoapi.provider.tinydb_ import TinyDBCatalogueProvider - -def get_test_file_path(filename): - """helper function to open test file safely""" - - if os.path.isfile(filename): - return filename - else: - return 'tests/{}'.format(filename) - +from .util import get_test_file_path path = get_test_file_path('tests/data/open.canada.ca/sample-records.tinydb') diff --git a/tests/test_util.py b/tests/test_util.py index 46ee613..e7e0d3f 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -2,7 +2,7 @@ # # Authors: Tom Kralidis # -# Copyright (c) 2020 Tom Kralidis +# Copyright (c) 2021 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -29,21 +29,13 @@ from datetime import datetime, date, time from decimal import Decimal -import os import pytest from pygeoapi import util from pygeoapi.provider.base import ProviderTypeError - -def get_test_file_path(filename): - """helper function to open test file safely""" - - if os.path.isfile(filename): - return filename - else: - return 'tests/{}'.format(filename) +from .util import get_test_file_path def test_get_typed_value(): diff --git a/tests/test_xarray_netcdf_provider.py b/tests/test_xarray_netcdf_provider.py index 6e97799..f45b2df 100644 --- a/tests/test_xarray_netcdf_provider.py +++ b/tests/test_xarray_netcdf_provider.py @@ -2,7 +2,7 @@ # # Authors: Gregory Petrochenkov # -# Copyright (c) 2020 Gregory Petrochenkov +# Copyright (c) 2021 Gregory Petrochenkov # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -27,21 +27,12 @@ # # ================================================================= -import os import pytest from pygeoapi.provider.base import ProviderQueryError from pygeoapi.provider.xarray_ import XarrayProvider - -def get_test_file_path(filename): - """helper function to open test file safely""" - - if os.path.isfile(filename): - return filename - else: - return 'tests/{}'.format(filename) - +from .util import get_test_file_path path = get_test_file_path('tests/data/coads_sst.nc') diff --git a/tests/test_xarray_zarr_provider.py b/tests/test_xarray_zarr_provider.py index 9ef7752..539f3d2 100644 --- a/tests/test_xarray_zarr_provider.py +++ b/tests/test_xarray_zarr_provider.py @@ -2,7 +2,7 @@ # # Authors: Gregory Petrochenkov # -# Copyright (c) 2020 Gregory Petrochenkov +# Copyright (c) 2021 Gregory Petrochenkov # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -27,20 +27,11 @@ # # ================================================================= -import os import pytest from pygeoapi.provider.xarray_ import XarrayProvider - -def get_test_file_path(filename): - """helper function to open test file safely""" - - if os.path.isfile(filename): - return filename - else: - return 'tests/{}'.format(filename) - +from .util import get_test_file_path path = get_test_file_path( 'data/analysed_sst.zarr') diff --git a/tests/util.py b/tests/util.py new file mode 100644 index 0000000..3375dd5 --- /dev/null +++ b/tests/util.py @@ -0,0 +1,71 @@ +# ================================================================= +# +# Authors: Tom Kralidis +# +# Copyright (c) 2021 Tom Kralidis +# +# 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. +# +# ================================================================= + +import os +import logging + +from werkzeug.test import create_environ +from werkzeug.wrappers import Request +from werkzeug.datastructures import ImmutableMultiDict + +LOGGER = logging.getLogger(__name__) + + +def get_test_file_path(filename): + """helper function to open test file safely""" + + if os.path.isfile(filename): + return filename + else: + return 'tests/{}'.format(filename) + + +def mock_request(params: dict = None, data=None, **headers) -> Request: + """ + Mocks a Request object so the @pre_process decorator can inject it + as an APIRequest. + + :param params: Optional query parameter dict for the request. + Will be set to {} if omitted. + :param data: Optional data/body to send with the request. + Can be text/bytes or a JSON dictionary. + :param headers: Optional request HTTP headers to set. + :returns: A Werkzeug Request instance. + """ + params = params or {} + # TODO: We are not setting a path in the create_environ() call. + # This is fine as long as an API test does not need the URL path. + if isinstance(data, dict): + environ = create_environ(base_url='http://localhost:5000/', json=data) + else: + environ = create_environ(base_url='http://localhost:5000/', data=data) + environ.update(headers) + request = Request(environ) + request.args = ImmutableMultiDict(params.items()) + return request