Skip to content

feat(event_handler): add custom response validation in OpenAPI utility #6189

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
cc27ba7
feat(openapi-validation): Add response validation flag and distinct e…
Feb 28, 2025
bc69d18
feat(api-gateway-resolver): Add option for custom response validation…
Feb 28, 2025
6ddfdc0
feat(docs): Added doc for custom response validation error responses.
Feb 28, 2025
a9be196
refactor(docs): Make exception handler function name better.
Feb 28, 2025
276d7cd
feat(unit-test): Add tests for custom response validation error.
Feb 28, 2025
fb49e9b
fix: Formatting.
Feb 28, 2025
df105dc
fix(docs): Fix grammar in response validation docs
Feb 28, 2025
63fd201
fix(unit-test): fix failed CI.
Feb 28, 2025
1c33611
bugfix(lint): Ignore lint error FA102, irrelevant for python >=3.9
Feb 28, 2025
f8ead84
refactor: make response_validation_error_http_status accept more type…
Feb 28, 2025
eb2430b
feat(unit-test): add tests for incorrect types and invalid configs
Feb 28, 2025
d6b7638
Merge branch 'develop' into feature/response-validation
leandrodamascena Mar 1, 2025
0090692
Merge branch 'develop' into feature/response-validation
leandrodamascena Mar 3, 2025
13b7380
Merge branch 'develop' into feature/response-validation
leandrodamascena Mar 4, 2025
218c666
Merge branch 'develop' into feature/response-validation
leandrodamascena Mar 4, 2025
82918c7
Merge branch 'develop' into feature/response-validation
leandrodamascena Mar 9, 2025
2a4d57f
refactor: rename response_validation_error_http_status to response_va…
amin-farjadi Mar 7, 2025
fece0e8
refactor(api_gateway): add method for validating response_validation_…
amin-farjadi Mar 7, 2025
f85c749
fix(api_gateway): fix type and docstring for response_validation_erro…
amin-farjadi Mar 7, 2025
c4f0819
fix(api_gateway): remove unncessary check of response_validation_erro…
amin-farjadi Mar 7, 2025
8fe4edc
fix(openapi-validation): docstring for has_response_validation_error …
amin-farjadi Mar 7, 2025
f89b598
refactor(tests): move unit tests into openapi_validation functional t…
amin-farjadi Mar 7, 2025
f60b812
Merge branch 'develop' into feature/response-validation
leandrodamascena Mar 10, 2025
aaa0086
fix(tests): skipping validation for falsy response
Mar 12, 2025
6d5f913
Merge branch 'develop' into feature/response-validation
amin-farjadi Mar 12, 2025
bd93ee6
Making Ruff happy
leandrodamascena Mar 17, 2025
558c03c
Refactoring documentation
leandrodamascena Mar 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor(tests): move unit tests into openapi_validation functional t…
…est file
  • Loading branch information
amin-farjadi committed Mar 10, 2025
commit f89b59883a52d63f8acda0465a070de4d9fca594
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
VPCLatticeResolver,
VPCLatticeV2Resolver,
)
from aws_lambda_powertools.event_handler.openapi.exceptions import ResponseValidationError
from aws_lambda_powertools.event_handler.openapi.params import Body, Header, Query


Expand Down Expand Up @@ -1128,3 +1129,249 @@ def handler(user_id: int = 123):
# THEN the handler should be invoked and return 200
result = app(minimal_event, {})
assert result["statusCode"] == 200


def test_validation_error_none_returned_non_optional_type(gw_event):
# GIVEN an APIGatewayRestResolver with validation enabled
app = APIGatewayRestResolver(enable_validation=True)

class Model(BaseModel):
name: str
age: int

@app.get("/none_not_allowed")
def handler_none_not_allowed() -> Model:
return None # type: ignore

# WHEN returning None for a non-Optional type
gw_event["path"] = "/none_not_allowed"
result = app(gw_event, {})

# THEN it should return a validation error
assert result["statusCode"] == 422
body = json.loads(result["body"])
assert body["detail"][0]["type"] == "model_attributes_type"
assert body["detail"][0]["loc"] == ["response"]


def test_validation_error_incomplete_model_returned_non_optional_type(gw_event):
# GIVEN an APIGatewayRestResolver with validation enabled
app = APIGatewayRestResolver(enable_validation=True)

class Model(BaseModel):
name: str
age: int

@app.get("/incomplete_model_not_allowed")
def handler_incomplete_model_not_allowed() -> Model:
return {"age": 18} # type: ignore

# WHEN returning incomplete model for a non-Optional type
gw_event["path"] = "/incomplete_model_not_allowed"
result = app(gw_event, {})

# THEN it should return a validation error
assert result["statusCode"] == 422
body = json.loads(result["body"])
assert "missing" in body["detail"][0]["type"]
assert "name" in body["detail"][0]["loc"]


def test_none_returned_for_optional_type(gw_event):
# GIVEN an APIGatewayRestResolver with validation enabled
app = APIGatewayRestResolver(enable_validation=True)

class Model(BaseModel):
name: str
age: int

@app.get("/none_allowed")
def handler_none_allowed() -> Optional[Model]:
return None

# WHEN returning None for an Optional type
gw_event["path"] = "/none_allowed"
result = app(gw_event, {})

# THEN it should succeed
assert result["statusCode"] == 200
assert result["body"] == "null"


@pytest.mark.parametrize(
"path, body",
[
("/empty_dict", {}),
("/empty_list", []),
("/none", "null"),
("/empty_string", ""),
],
ids=["empty_dict", "empty_list", "none", "empty_string"],
)
def test_none_returned_for_falsy_return(gw_event, path, body):
# GIVEN an APIGatewayRestResolver with validation enabled
app = APIGatewayRestResolver(enable_validation=True)

class Model(BaseModel):
name: str
age: int

@app.get(path)
def handler_none_allowed() -> Model:
return body

# WHEN returning None for an Optional type
gw_event["path"] = path
result = app(gw_event, {})

# THEN it should succeed
assert result["statusCode"] == 422


def test_custom_response_validation_error_http_code_valid_response(gw_event):
# GIVEN an APIGatewayRestResolver with custom response validation enabled
app = APIGatewayRestResolver(enable_validation=True, response_validation_error_http_code=422)

class Model(BaseModel):
name: str
age: int

@app.get("/valid_response")
def handler_valid_response() -> Model:
return {
"name": "Joe",
"age": 18,
} # type: ignore

# WHEN returning the expected type
gw_event["path"] = "/valid_response"
result = app(gw_event, {})

# THEN it should return a 200 OK
assert result["statusCode"] == 200
body = json.loads(result["body"])
assert body == {"name": "Joe", "age": 18}


@pytest.mark.parametrize(
"http_code",
(422, 500, 510),
)
def test_custom_response_validation_error_http_code_invalid_response_none(
http_code,
gw_event,
):
# GIVEN an APIGatewayRestResolver with custom response validation enabled
app = APIGatewayRestResolver(enable_validation=True, response_validation_error_http_code=http_code)

class Model(BaseModel):
name: str
age: int

@app.get("/none_not_allowed")
def handler_none_not_allowed() -> Model:
return None # type: ignore

# WHEN returning None for a non-Optional type
gw_event["path"] = "/none_not_allowed"
result = app(gw_event, {})

# THEN it should return a validation error with the custom status code provided
assert result["statusCode"] == http_code
body = json.loads(result["body"])
assert body["detail"][0]["type"] == "model_attributes_type"
assert body["detail"][0]["loc"] == ["response"]


@pytest.mark.parametrize(
"http_code",
(422, 500, 510),
)
def test_custom_response_validation_error_http_code_invalid_response_incomplete_model(
http_code,
gw_event,
):
# GIVEN an APIGatewayRestResolver with custom response validation enabled
app = APIGatewayRestResolver(enable_validation=True, response_validation_error_http_code=http_code)

class Model(BaseModel):
name: str
age: int

@app.get("/incomplete_model_not_allowed")
def handler_incomplete_model_not_allowed() -> Model:
return {"age": 18} # type: ignore

# WHEN returning incomplete model for a non-Optional type
gw_event["path"] = "/incomplete_model_not_allowed"
result = app(gw_event, {})

# THEN it should return a validation error with the custom status code provided
assert result["statusCode"] == http_code
body = json.loads(result["body"])
assert body["detail"][0]["type"] == "missing"
assert body["detail"][0]["loc"] == ["response", "name"]


@pytest.mark.parametrize(
"http_code",
(422, 500, 510),
)
def test_custom_response_validation_error_sanitized_response(
http_code,
gw_event,
):
# GIVEN an APIGatewayRestResolver with custom response validation enabled
# with a sanitized response validation error response
app = APIGatewayRestResolver(enable_validation=True, response_validation_error_http_code=http_code)

class Model(BaseModel):
name: str
age: int

@app.get("/incomplete_model_not_allowed")
def handler_incomplete_model_not_allowed() -> Model:
return {"age": 18} # type: ignore

@app.exception_handler(ResponseValidationError)
def handle_response_validation_error(ex: ResponseValidationError):
return Response(
status_code=500,
body="Unexpected response.",
)

# WHEN returning incomplete model for a non-Optional type
gw_event["path"] = "/incomplete_model_not_allowed"
result = app(gw_event, {})

# THEN it should return the sanitized response
assert result["statusCode"] == 500
assert result["body"] == "Unexpected response."


def test_custom_response_validation_error_no_validation():
# GIVEN an APIGatewayRestResolver with validation not enabled
# setting a custom http status code for response validation must raise a ValueError
with pytest.raises(ValueError) as exception_info:
APIGatewayRestResolver(response_validation_error_http_code=500)

assert (
str(exception_info.value)
== "'response_validation_error_http_code' cannot be set when enable_validation is False."
)


@pytest.mark.parametrize("response_validation_error_http_code", [(20), ("hi"), (1.21)])
def test_custom_response_validation_error_bad_http_code(response_validation_error_http_code):
# GIVEN an APIGatewayRestResolver with validation enabled
# setting custom status code for response validation that is not a valid HTTP code must raise a ValueError
with pytest.raises(ValueError) as exception_info:
APIGatewayRestResolver(
enable_validation=True,
response_validation_error_http_code=response_validation_error_http_code,
)

assert (
str(exception_info.value)
== f"'{response_validation_error_http_code}' must be an integer representing an HTTP status code."
)
Loading
Loading