Skip to content

Commit

Permalink
Merge pull request #31 from metno/30-accept-only-openapi31-or-higher
Browse files Browse the repository at this point in the history
30 accept only openapi31 or higher
  • Loading branch information
ways authored Nov 26, 2024
2 parents 3d2ed55 + 41b6189 commit d32603c
Show file tree
Hide file tree
Showing 6 changed files with 50 additions and 39 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## What is sedr?

An experimental validator for OGC EDR APIs using schemathesis.
An experimental validator for OGC EDR APIs using schemathesis. Main focus will be on the Rodeo Profile, which is a subset of the OGC EDR API.

## Who is responsible?

Expand Down
20 changes: 11 additions & 9 deletions sedr/edreq12.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ def requirementA2_2_A5(jsondata: dict, siteurl="") -> tuple[bool, str]:
if "conformsTo" not in jsondata:
return (
False,
f"Conformance page <{siteurl}conformance> does not contain a conformsTo attribute. See <{spec_url}> for more info.",
f"Conformance page <{siteurl}conformance> does not contain a "
f"conformsTo attribute. See <{spec_url}> for more info.",
)
for url in conformance_urls:
if url not in jsondata["conformsTo"]:
return (
False,
f"Conformance page <{siteurl}conformance> does not contain the core edr class {url}. See <{spec_url}> for more info.",
f"Conformance page <{siteurl}conformance> does not contain "
f"the core edr class {url}. See <{spec_url}> for more info.",
)

util.logger.debug(
Expand Down Expand Up @@ -66,25 +68,25 @@ def requirementA11_1(jsondata: dict) -> tuple[bool, str]:
Version: 1.1
Requirement A11.1
Check if the conformance page contains openapi classes, and that they match our version."""
Check if the conformance page contains openapi classes,
and that they match our version."""
spec_url = f"{edr_root_url}#_requirements_class_openapi_3_0"

for url in jsondata["conformsTo"]:
if url in openapi_conformance_urls:
if (
util.args.openapi_version == "3.1"
and "oas31" in url
or util.args.openapi_version == "3.0"
and "oas30" in url
"oas31" in url or "oas30" in url # TODO: oas30 should be removed
):
util.logger.debug("requirementA11_1 Found openapi class <%s>", url)
return True, url
return (
False,
f"OpenAPI version {util.args.openapi_version} and version in conformance {url} doesn't match. See <{spec_url}> for more info.",
f"OpenAPI version {util.args.openapi_version} and version in "
f"conformance {url} doesn't match. See <{spec_url}> for more info.",
)

return (
False,
f"Conformance page /conformance does not contain an openapi class. See <{spec_url}> for more info.",
f"Conformance page /conformance does not contain an openapi class. "
f"See <{spec_url}> for more info.",
)
35 changes: 21 additions & 14 deletions sedr/rodeoprofile10.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ def requirement7_1(jsondata: dict) -> tuple[bool, str]:
if conformance_url not in jsondata["conformsTo"]:
return (
False,
f"Conformance page /conformance does not contain the profile class {conformance_url}. See <{spec_url}> for more info.",
f"Conformance page /conformance does not contain the profile "
f"class {conformance_url}. See <{spec_url}> for more info.",
)
util.logger.debug("Rodeoprofile Requirement 7.1 OK")
return True, ""
Expand All @@ -36,7 +37,7 @@ def requirement7_2(jsondata: dict, timeout: int) -> tuple[bool, str]:
jsondata should be a valid landing page json dict.
"""
spec_url = f"{spec_base_url}#_openapi"
openapi_type = "application/vnd.oai.openapi+json;version=" # 3.0"
openapi_type = "application/vnd.oai.openapi+json;version=3.1"
servicedoc_type = "text/html"

service_desc_link = ""
Expand All @@ -58,9 +59,8 @@ def requirement7_2(jsondata: dict, timeout: int) -> tuple[bool, str]:
return (
False,
f"OpenAPI link service-desc should identify the content as "
"openAPI and include version. Example "
"<application/vnd.oai.openapi+json;version=3.0>. Found: "
f"<{service_desc_type}> See <{spec_url}> and <{spec_base_url}"
f"openAPI and include version. Example <{openapi_type}>. Found: "
f"<{service_desc_type}>. See <{spec_url}> and <{spec_base_url}"
"#_openapi_2> for more info.",
)

Expand All @@ -79,8 +79,8 @@ def requirement7_2(jsondata: dict, timeout: int) -> tuple[bool, str]:
except (json.JSONDecodeError, TypeError) as err:
return (
False,
f"OpenAPI link service-desc <{service_desc_link}> does not contain valid JSON.\n"
f"Error: {err}",
f"OpenAPI link service-desc <{service_desc_link}> does not "
f"contain valid JSON.\nError: {err}",
)

# D API documentation
Expand All @@ -92,7 +92,8 @@ def requirement7_2(jsondata: dict, timeout: int) -> tuple[bool, str]:
if servicedoc_type not in link["type"]:
return (
False,
f"Service-doc should have type <{servicedoc_type}>. Found <{link['type']}> See <{spec_url}> for more info.",
f"Service-doc should have type <{servicedoc_type}>. Found "
f"<{link['type']}> See <{spec_url}> for more info.",
)
break
else:
Expand Down Expand Up @@ -159,13 +160,15 @@ def requirement7_4(jsondata: dict) -> tuple[bool, str]:
if len(jsondata["title"]) > 50:
return (
False,
f"Collection title should not exceed 50 chars. See <{spec_url}> for more info.",
f"Collection title should not exceed 50 chars. See "
f"<{spec_url}> for more info.",
)
except (json.JSONDecodeError, KeyError):
# A
return (
False,
f"Collection must have a title, but it seems to be missing. See <{spec_url}> and {spec_base_url}#_collection_title_2 for more info.",
f"Collection must have a title, but it seems to be missing. See "
f"<{spec_url}> and {spec_base_url}#_collection_title_2 for more info.",
)
util.logger.debug("Rodeoprofile Requirement 7.4 OK")
return (
Expand All @@ -177,19 +180,23 @@ def requirement7_4(jsondata: dict) -> tuple[bool, str]:
def requirement7_5(jsondata: dict) -> tuple[bool, str]:
"""Check collection license. Can't test D."""
spec_url = f"{spec_base_url}#_collection_license"
wanted_type = "text/html"
wanted_rel = "license"
# A, B
for link in jsondata["links"]:
if link["rel"] == "license":
if not link["type"] == "text/html":
if link["rel"] == wanted_rel:
if not link["type"] == wanted_type:
return (
False,
f"Collection <{jsondata['id']}> license link should have type='text/html'. See <{spec_url}> C for more info.",
f"Collection <{jsondata['id']}> license link should have "
f"type='{wanted_type}'. See <{spec_url}> C for more info.",
)
break
else:
return (
False,
f"Collection <{jsondata['id']}> is missing a license link with rel='license'. See <{spec_url}> A, B for more info.",
f"Collection <{jsondata['id']}> is missing a license link with "
f"rel='{wanted_rel}'. See <{spec_url}> A, B for more info.",
)
util.logger.debug("Rodeoprofile Requirement 7.5 OK")
return (
Expand Down
6 changes: 3 additions & 3 deletions sedr/schemat.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,11 @@ def test_openapi(case):


@schemathesis.hook
def after_call(context, case, response):
def after_call(context, case, response): # noqa: pylint: disable=unused-argument
"""Hook runs after any call to the API, used for logging."""
if response.request:
# Log calls with status
util.logger.debug(
util.logger.debug( # noqa: pylint: disable=logging-not-lazy
f"after_call {'OK' if response.ok else 'ERR'} "
+ f"{response.request.path_url} {response.text[0:150]}"
)
Expand All @@ -100,7 +100,7 @@ def test_edr_collections(case):
also require /collections to exist, in accordance with Requirement A.2.2 A.9
<https://docs.ogc.org/is/19-086r6/19-086r6.html#_26b5ceeb-1127-4dc1-b88e-89a32d73ade9>
"""
global collection_ids, extents
global collection_ids, extents # noqa: pylint: disable=global-variable-not-assigned

response = case.call()
spec_ref = "https://docs.ogc.org/is/19-086r6/19-086r6.html#_ed0b4d0d-f90a-4a7d-a123-17a1d7849b2d"
Expand Down
21 changes: 12 additions & 9 deletions sedr/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,21 @@ def set_up_logging(args, logfile=None, version: str = "") -> logging.Logger:
pass # Touch file
except PermissionError as err:
print(
f"Could not write to logfile {logfile}: {err}\nIf you're running this as a docker "
+ "container, make sure you mount the log dir (docker run -v host-dir:container-dir) "
+ "and give log option to sedr using the container-dir (--log-file /container-dir/debug.log)."
f"Could not write to logfile {logfile}: {err}\nIf you're "
f"running this as a docker container, make sure you mount "
f"the log dir (docker run -v host-dir:container-dir) and give "
f"log option to sedr using the container-dir "
f"(--log-file /container-dir/debug.log)."
)
sys.exit(1)

fh = logging.FileHandler(mode="a", filename=logfile)
fh.setLevel(logging.DEBUG)
logger.addHandler(fh)
logger.debug(
logger.debug( # noqa: pylint: disable=logging-not-lazy
f"SEDR version {version} on python {sys.version}, schemathesis "
+ f"{schemathesis.__version__} \nTesting url <{args.url}>, openapi url <{args.openapi}>, "
+ f"openapi-version {args.openapi_version}.\n\n"
f"{schemathesis.__version__} \nTesting url <{args.url}>, openapi "
f"url <{args.openapi}>, openapi-version {args.openapi_version}.\n"
)

# Console
Expand Down Expand Up @@ -135,6 +137,7 @@ def parse_locations(jsondata) -> None:
def test_conformance_links(jsondata: dict, timeout: int) -> tuple[bool, str]:
"""Test that all conformance links are valid and resolves."""
msg = ""
valid = True
for link in jsondata["conformsTo"]:
if link in [
"http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/conformance",
Expand All @@ -148,12 +151,12 @@ def test_conformance_links(jsondata: dict, timeout: int) -> tuple[bool, str]:
try:
response = requests.head(url=link, timeout=timeout)
except requests.exceptions.MissingSchema as error:
valid = False
msg += f"test_conformance_links Link <{link}> from /conformance is malformed: {error}). "
if not response.status_code < 400:
valid = False
msg += f"test_conformance_links Link {link} from /conformance is broken (gives status code {response.status_code}). "
if msg:
return False, msg
return True, ""
return valid, msg


def locate_openapi_url(url: str, timeout: int) -> str:
Expand Down
5 changes: 2 additions & 3 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ commands = python -m unittest discover -v -s ./sedr -p "test_*.py"

[testenv:prospector]
description = Run static analysis using prospector, but dont fail on errors
ignore_outcome = true
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/requirements-dev.txt
commands = prospector --no-autodetect \
--test-warnings \
--die-on-tool-error \
--zero-exit \
{toxinidir}/sedr/

[testenv:format]
Expand All @@ -40,12 +40,11 @@ deps =
commands = ruff format {toxinidir}/sedr

[testenv:mypy]
ignore_outcome = true
description = Check typing TODO
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/requirements-dev.txt
commands = mypy --follow-imports skip {toxinidir}/sedr
commands = mypy --ignore-missing-imports {toxinidir}/sedr

[testenv:bandit]
description = Check for security issues
Expand Down

0 comments on commit d32603c

Please sign in to comment.