diff --git a/README.md b/README.md index 119be49..47716d6 100644 --- a/README.md +++ b/README.md @@ -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? diff --git a/sedr/edreq12.py b/sedr/edreq12.py index a6d7aef..9c8dd1a 100644 --- a/sedr/edreq12.py +++ b/sedr/edreq12.py @@ -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( @@ -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.", ) diff --git a/sedr/rodeoprofile10.py b/sedr/rodeoprofile10.py index 8b0f67f..0988053 100644 --- a/sedr/rodeoprofile10.py +++ b/sedr/rodeoprofile10.py @@ -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, "" @@ -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 = "" @@ -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 " - ". 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.", ) @@ -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 @@ -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: @@ -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 ( @@ -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 ( diff --git a/sedr/schemat.py b/sedr/schemat.py index 842b175..ec6b697 100644 --- a/sedr/schemat.py +++ b/sedr/schemat.py @@ -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]}" ) @@ -100,7 +100,7 @@ def test_edr_collections(case): also require /collections to exist, in accordance with Requirement A.2.2 A.9 """ - 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" diff --git a/sedr/util.py b/sedr/util.py index abe7aec..26d3e29 100644 --- a/sedr/util.py +++ b/sedr/util.py @@ -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 @@ -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", @@ -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: diff --git a/tox.ini b/tox.ini index 0646eef..48fb06e 100644 --- a/tox.ini +++ b/tox.ini @@ -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] @@ -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