Ejemplo n.º 1
0
def validate_against_openapi_schema(
    content: Dict[str, Any],
    path: str,
    method: str,
    status_code: str,
    display_brief_error: bool = False,
) -> bool:
    """Compare a "content" dict with the defined schema for a specific method
    in an endpoint. Return true if validated and false if skipped.
    """

    # This first set of checks are primarily training wheels that we
    # hope to eliminate over time as we improve our API documentation.

    # No 500 responses have been documented, so skip them
    if status_code.startswith("5"):
        return False
    if path not in openapi_spec.openapi()["paths"].keys():
        endpoint = find_openapi_endpoint(path)
        # If it doesn't match it hasn't been documented yet.
        if endpoint is None:
            return False
    else:
        endpoint = path
    # Excluded endpoint/methods
    if (endpoint, method) in EXCLUDE_UNDOCUMENTED_ENDPOINTS:
        return False
    # Return true for endpoints with only response documentation remaining
    if (endpoint, method) in EXCLUDE_DOCUMENTED_ENDPOINTS:
        return True
    # Check if the response matches its code
    if status_code.startswith("2") and (content.get(
            "result", "success").lower() != "success"):
        raise SchemaError(
            "Response is not 200 but is validating against 200 schema")
    # Code is not declared but appears in various 400 responses. If
    # common, it can be added to 400 response schema
    if status_code.startswith("4"):
        # This return statement should ideally be not here. But since
        # we have not defined 400 responses for various paths this has
        # been added as all 400 have the same schema.  When all 400
        # response have been defined this should be removed.
        return True
    # The actual work of validating that the response matches the
    # schema is done via the third-party OAS30Validator.
    schema = get_schema(endpoint, method, status_code)
    if endpoint == "/events" and method == "get":
        # This a temporary function for checking only documented events
        # as all events haven't been documented yet.
        # TODO: Remove this after all events have been documented.
        fix_events(content)

    validator = OAS30Validator(schema)
    try:
        validator.validate(content)
    except JsonSchemaValidationError as error:
        if not display_brief_error:
            raise error

        # display_brief_error is designed to avoid printing 1000 lines
        # of output when the schema to validate is extremely large
        # (E.g. the several dozen format variants for individual
        # events returned by GET /events) and instead just display the
        # specific variant we expect to match the response.
        brief_error_display_schema = {"nullable": False, "oneOf": list()}
        brief_error_display_schema_oneOf = []
        brief_error_validator_value = []

        for validator_value in error.validator_value:
            if validator_value["example"]["type"] == error.instance["type"]:
                brief_error_validator_value.append(validator_value)

        for i_schema in error.schema["oneOf"]:
            if i_schema["example"]["type"] == error.instance["type"]:
                brief_error_display_schema_oneOf.append(i_schema)
        brief_error_display_schema["oneOf"] = brief_error_display_schema_oneOf

        # Field list from https://python-jsonschema.readthedocs.io/en/stable/errors/
        raise JsonSchemaValidationError(
            message=error.message,
            validator=error.validator,
            path=error.path,
            instance=error.instance,
            schema_path=error.schema_path,
            schema=brief_error_display_schema,
            validator_value=brief_error_validator_value,
            cause=error.cause,
        )

    return True
Ejemplo n.º 2
0
def validate_against_openapi_schema(
    content: Dict[str, Any],
    path: str,
    method: str,
    status_code: str,
    display_brief_error: bool = False,
) -> bool:
    """Compare a "content" dict with the defined schema for a specific method
    in an endpoint. Return true if validated and false if skipped.
    """

    # This first set of checks are primarily training wheels that we
    # hope to eliminate over time as we improve our API documentation.

    # No 500 responses have been documented, so skip them
    if status_code.startswith("5"):
        return False
    if path not in openapi_spec.openapi()["paths"].keys():
        endpoint = find_openapi_endpoint(path)
        # If it doesn't match it hasn't been documented yet.
        if endpoint is None:
            return False
    else:
        endpoint = path
    # Excluded endpoint/methods
    if (endpoint, method) in EXCLUDE_UNDOCUMENTED_ENDPOINTS:
        return False
    # Return true for endpoints with only response documentation remaining
    if (endpoint, method) in EXCLUDE_DOCUMENTED_ENDPOINTS:
        return True
    # Check if the response matches its code
    if status_code.startswith("2") and (content.get(
            "result", "success").lower() != "success"):
        raise SchemaError(
            "Response is not 200 but is validating against 200 schema")
    # Code is not declared but appears in various 400 responses. If
    # common, it can be added to 400 response schema
    if status_code.startswith("4"):
        # This return statement should ideally be not here. But since
        # we have not defined 400 responses for various paths this has
        # been added as all 400 have the same schema.  When all 400
        # response have been defined this should be removed.
        return True

    if endpoint == "/events" and method == "get":
        # This a temporary function for checking only documented events
        # as all events haven't been documented yet.
        # TODO: Remove this after all events have been documented.
        fix_events(content)

    mock_request = MockRequest("http://localhost:9991/", method,
                               "/api/v1" + path)
    mock_response = MockResponse(
        # TODO: Use original response content instead of re-serializing it.
        orjson.dumps(content),
        status_code=status_code,
    )
    result = openapi_spec.response_validator().validate(
        mock_request, mock_response)
    try:
        result.raise_for_errors()
    except InvalidSchemaValue as isv:
        message = f"{len(isv.schema_errors)} response validation error(s) at {method} /api/v1{path} ({status_code}):"
        for error in isv.schema_errors:
            if display_brief_error:
                # display_brief_error is designed to avoid printing 1000 lines
                # of output when the schema to validate is extremely large
                # (E.g. the several dozen format variants for individual
                # events returned by GET /events) and instead just display the
                # specific variant we expect to match the response.
                brief_error_validator_value = [
                    validator_value
                    for validator_value in error.validator_value
                    if not prune_schema_by_type(validator_value,
                                                error.instance["type"])
                ]
                brief_error_display_schema = error.schema.copy()
                if "oneOf" in brief_error_display_schema:
                    brief_error_display_schema["oneOf"] = [
                        i_schema for i_schema in error.schema["oneOf"]
                        if not prune_schema_by_type(i_schema,
                                                    error.instance["type"])
                    ]

                # Field list from https://python-jsonschema.readthedocs.io/en/stable/errors/
                error = JsonSchemaValidationError(
                    message=error.message,
                    validator=error.validator,
                    path=error.path,
                    instance=error.instance,
                    schema_path=error.schema_path,
                    schema=brief_error_display_schema,
                    validator_value=brief_error_validator_value,
                    cause=error.cause,
                )
            message += f"\n\n{type(error).__name__}: {error}"
        raise SchemaError(message) from None

    return True