Skip to content

Commit

Permalink
feature: Properly handle response data validation errors.
Browse files Browse the repository at this point in the history
Previously validation error on response resulted in 500 Server Error
responses, this commit fixes it and retursn 422 Unprocessable Entity
responses with data similar to Request Validation Error.

Issue: #14
  • Loading branch information
playpauseandstop committed Jan 27, 2020
1 parent 9c06295 commit 61e631f
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 30 deletions.
27 changes: 12 additions & 15 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions rororo/openapi/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,20 @@ def from_request_errors( # type: ignore
errors=parameters + body,
)

@classmethod
def from_response_errors( # type: ignore
cls, errors: List[OpenAPIMappingError]
) -> "ValidationError":
result = []

for err in errors:
if isinstance(err, OpenAPIMediaTypeError):
details = get_media_type_error_details(["response"], err)
if details:
result.append(details)

return cls(message="Response data validation error", errors=result)


def ensure_loc(loc: List[PathItem]) -> List[PathItem]:
return [item for item in loc if item != ""]
Expand Down
4 changes: 3 additions & 1 deletion rororo/openapi/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,7 @@ def validate_response_data(
validator = ResponseValidator(spec, custom_formatters=CUSTOM_FORMATTERS)
result = validator.validate(core_request, core_response)

result.raise_for_errors()
if result.errors:
raise ValidationError.from_response_errors(result.errors)

return result.data
17 changes: 17 additions & 0 deletions tests/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,23 @@
}
}
},
"/invalid-response": {
"get": {
"operationId": "retrieve_invalid_response",
"responses": {
"200": {
"description": "Expected response.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NestedObject"
}
}
}
}
}
}
},
"/nested-object": {
"post": {
"operationId": "retrieve_nested_object_from_request_body",
Expand Down
11 changes: 11 additions & 0 deletions tests/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,17 @@ paths:
"204":
description: "Empty response."

"/invalid-response":
get:
operationId: "retrieve_invalid_response"
responses:
"200":
description: "Expected response."
content:
application/json:
schema:
$ref: "#/components/schemas/NestedObject"

"/nested-object":
post:
operationId: "retrieve_nested_object_from_request_body"
Expand Down
48 changes: 48 additions & 0 deletions tests/test_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ async def retrieve_empty(request: web.Request) -> web.Response:
return web.Response(status=204)


@operations.register
async def retrieve_invalid_response(request: web.Request) -> web.Response:
return web.json_response({})


@operations.register
async def retrieve_nested_object_from_request_body(
request: web.Request,
Expand Down Expand Up @@ -477,3 +482,46 @@ async def test_validate_empty_response(aiohttp_client, schema_path):
client = await aiohttp_client(app)
response = await client.get("/api/empty")
assert response.status == 204


@pytest.mark.parametrize(
"schema_path, is_validate_response, expected_status",
(
(OPENAPI_JSON_PATH, False, 200),
(OPENAPI_JSON_PATH, True, 422),
(OPENAPI_YAML_PATH, False, 200),
(OPENAPI_JSON_PATH, True, 422),
),
)
async def test_validate_response(
aiohttp_client, schema_path, is_validate_response, expected_status
):
app = setup_openapi(
web.Application(),
schema_path,
operations,
server_url="/api",
is_validate_response=is_validate_response,
)

client = await aiohttp_client(app)
response = await client.get("/api/invalid-response")
assert response.status == expected_status


@pytest.mark.parametrize("schema_path", (OPENAPI_JSON_PATH, OPENAPI_YAML_PATH))
async def test_validate_response_error(aiohttp_client, schema_path):
app = setup_openapi(
web.Application(),
schema_path,
operations,
server_url="/api",
is_validate_response=True,
)

client = await aiohttp_client(app)
response = await client.get("/api/invalid-response")
assert response.status == 422
assert await response.json() == {
"detail": [{"loc": ["response", "uid"], "message": "Field required"}]
}
27 changes: 13 additions & 14 deletions tests/test_openapi_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,19 +92,18 @@ def test_validation_error_from_dict_value_error():
)


def test_validation_error_from_dummy_mapping_error():
err = ValidationError.from_request_errors([OpenAPIMappingError()])
assert err.errors == []
assert err.data is None


def test_validation_error_from_dummy_media_type_error():
err = ValidationError.from_request_errors([OpenAPIMediaTypeError()])
assert err.errors == []
assert err.data is None


def test_validation_error_from_dummy_operation_error():
err = ValidationError.from_request_errors([OpenAPIParameterError()])
@pytest.mark.parametrize(
"method, error",
(
(ValidationError.from_request_errors, OpenAPIMappingError()),
(ValidationError.from_response_errors, OpenAPIMappingError()),
(ValidationError.from_request_errors, OpenAPIMediaTypeError()),
(ValidationError.from_response_errors, OpenAPIMediaTypeError()),
(ValidationError.from_request_errors, OpenAPIParameterError()),
(ValidationError.from_response_errors, OpenAPIParameterError()),
),
)
def test_validation_error_from_dummy_error(method, error):
err = method([error])
assert err.errors == []
assert err.data is None

0 comments on commit 61e631f

Please sign in to comment.