From ff23365290322959e6d15bf97c2774e5e3de1596 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Fri, 27 Dec 2024 16:41:43 +0000 Subject: [PATCH] Fixing an error in AppSync resolver event --- .../data_classes/appsync_resolver_event.py | 3 +- .../utilities/parser/parser.py | 6 +- tests/events/appSyncCustomResolverEvent.json | 69 +++++++++++++++++++ .../appsync/test_appsync_single_resolvers.py | 34 +++++++++ tests/functional/parser/test_parser.py | 13 ++-- 5 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 tests/events/appSyncCustomResolverEvent.json diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py index 9d2223d2b3e..83d266b119f 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py @@ -158,7 +158,8 @@ def __init__(self, data: dict): info: dict | None = data.get("info") if not info: - info = {"fieldName": self.get("fieldName"), "parentTypeName": self.get("typeName")} + parent_type_name = self.get("parentTypeName") or self.get("typeName") + info = {"fieldName": self.get("fieldName"), "parentTypeName": parent_type_name} self._info = AppSyncResolverEventInfo(info) diff --git a/aws_lambda_powertools/utilities/parser/parser.py b/aws_lambda_powertools/utilities/parser/parser.py index 7aa7531edb9..4f6115255a8 100644 --- a/aws_lambda_powertools/utilities/parser/parser.py +++ b/aws_lambda_powertools/utilities/parser/parser.py @@ -110,7 +110,7 @@ def handler(event: Order, context: LambdaContext): logger.debug(f"Calling handler {handler.__name__}") return handler(parsed_event, context, **kwargs) - + @overload def parse(event: dict[str, Any], model: type[T]) -> T: ... # pragma: no cover @@ -189,7 +189,7 @@ def handler(event: Order, context: LambdaContext): adapter = _retrieve_or_set_model_from_cache(model=model) logger.debug("Parsing and validating event model; no envelope used") - + return _parse_and_validate_event(data=event, adapter=adapter) # Pydantic raises PydanticSchemaGenerationError when the model is not a Pydantic model @@ -202,4 +202,4 @@ def handler(event: Order, context: LambdaContext): f"Error: {str(exc)}. Please ensure the Input model inherits from BaseModel,\n" "and your payload adheres to the specified Input model structure.\n" f"Model={model}", - ) from exc \ No newline at end of file + ) from exc diff --git a/tests/events/appSyncCustomResolverEvent.json b/tests/events/appSyncCustomResolverEvent.json new file mode 100644 index 00000000000..0751f794d51 --- /dev/null +++ b/tests/events/appSyncCustomResolverEvent.json @@ -0,0 +1,69 @@ +{ + "parentTypeName": "Merchant", + "fieldName": "locations", + "arguments": { + "page": 2 + }, + "identity": { + "claims": { + "sub": "07920713-4526-4642-9c88-2953512de441", + "iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_POOL_ID", + "aud": "58rc9bf5kkti90ctmvioppukm9", + "event_id": "7f4c9383-abf6-48b7-b821-91643968b755", + "token_use": "id", + "auth_time": 1615366261, + "name": "Michael Brewer", + "exp": 1615369861, + "iat": 1615366261 + }, + "defaultAuthStrategy": "ALLOW", + "groups": null, + "issuer": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_POOL_ID", + "sourceIp": [ + "11.215.2.22" + ], + "sub": "07920713-4526-4642-9c88-2953512de441", + "username": "mike" + }, + "source": { + "name": "Value", + "nested": { + "name": "value", + "list": [] + } + }, + "request": { + "headers": { + "x-forwarded-for": "11.215.2.22, 64.44.173.11", + "cloudfront-viewer-country": "US", + "cloudfront-is-tablet-viewer": "false", + "via": "2.0 SOMETHING.cloudfront.net (CloudFront)", + "cloudfront-forwarded-proto": "https", + "origin": "https://console.aws.amazon.com", + "content-length": "156", + "accept-language": "en-US,en;q=0.9", + "host": "SOMETHING.appsync-api.us-east-1.amazonaws.com", + "x-forwarded-proto": "https", + "sec-gpc": "1", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) etc.", + "accept": "*/*", + "cloudfront-is-mobile-viewer": "false", + "cloudfront-is-smarttv-viewer": "false", + "accept-encoding": "gzip, deflate, br", + "referer": "https://console.aws.amazon.com/", + "content-type": "application/json", + "sec-fetch-mode": "cors", + "x-amz-cf-id": "Fo5VIuvP6V6anIEt62WzFDCK45mzM4yEdpt5BYxOl9OFqafd-WR0cA==", + "x-amzn-trace-id": "Root=1-60488877-0b0c4e6727ab2a1c545babd0", + "authorization": "AUTH-HEADER", + "sec-fetch-dest": "empty", + "x-amz-user-agent": "AWS-Console-AppSync/", + "cloudfront-is-desktop-viewer": "true", + "sec-fetch-site": "cross-site", + "x-forwarded-port": "443" + } + }, + "prev": { + "result": {} + } +} diff --git a/tests/functional/event_handler/required_dependencies/appsync/test_appsync_single_resolvers.py b/tests/functional/event_handler/required_dependencies/appsync/test_appsync_single_resolvers.py index d58c966e67b..3ac91337002 100644 --- a/tests/functional/event_handler/required_dependencies/appsync/test_appsync_single_resolvers.py +++ b/tests/functional/event_handler/required_dependencies/appsync/test_appsync_single_resolvers.py @@ -26,6 +26,40 @@ def create_something(id: str): # noqa AA03 VNE003 assert result == "my identifier" +def test_direct_resolver_with_parent_name(): + # Check whether we can handle an example appsync direct resolver + mock_event = load_event("appSyncDirectResolver.json") + + app = AppSyncResolver() + + @app.resolver(field_name="createSomething", type_name="Mutation") + def create_something(id: str): # noqa AA03 VNE003 + assert app.lambda_context == {} + return id + + # Call the implicit handler + result = app(mock_event, {}) + + assert result == "my identifier" + + +def test_custom_resolver_with_fields(): + # Check whether we can handle an example appsync with custom resolver + mock_event = load_event("appSyncCustomResolverEvent.json") + + app = AppSyncResolver() + + @app.resolver(field_name="locations", type_name="Merchant") + def create_something(page: int): # noqa AA03 VNE003 + assert app.lambda_context == {} + return page + + # Call the implicit handler + result = app(mock_event, {}) + + assert result == 2 + + def test_amplify_resolver(): # Check whether we can handle an example appsync resolver mock_event = load_event("appSyncResolverEvent.json") diff --git a/tests/functional/parser/test_parser.py b/tests/functional/parser/test_parser.py index 1b4a5770b28..23052aa63f4 100644 --- a/tests/functional/parser/test_parser.py +++ b/tests/functional/parser/test_parser.py @@ -4,8 +4,8 @@ import pydantic import pytest +from pydantic import BaseModel, ValidationError from typing_extensions import Annotated -from pydantic import ValidationError, BaseModel from aws_lambda_powertools.utilities.parser import event_parser, exceptions, parse from aws_lambda_powertools.utilities.parser.envelopes.sqs import SqsEnvelope @@ -130,6 +130,7 @@ def handler(event, _): with pytest.raises(ValidationError): handler({"project": "powertools"}, LambdaContext()) + def test_parser_validation_error(): class StrictModel(pydantic.BaseModel): age: int @@ -143,9 +144,10 @@ def handle_validation(event: Dict, _: LambdaContext): with pytest.raises(ValidationError) as exc_info: handle_validation(event=invalid_event, context=LambdaContext()) - + assert "age" in str(exc_info.value) # Verify the error mentions the invalid field + def test_parser_type_value_errors(): class CustomModel(pydantic.BaseModel): timestamp: datetime @@ -158,7 +160,7 @@ def handle_type_validation(event: Dict, _: LambdaContext): # Test both TypeError and ValueError scenarios invalid_events = [ {"timestamp": "invalid-date", "status": "SUCCESS"}, # Will raise ValueError for invalid date - {"timestamp": datetime.now(), "status": "INVALID"} # Will raise ValueError for invalid literal + {"timestamp": datetime.now(), "status": "INVALID"}, # Will raise ValueError for invalid literal ] for invalid_event in invalid_events: @@ -168,10 +170,11 @@ def handle_type_validation(event: Dict, _: LambdaContext): def test_event_parser_no_model(): with pytest.raises(exceptions.InvalidModelTypeError): + @event_parser def handler(event, _): return event - + handler({}, None) @@ -179,6 +182,7 @@ class Shopping(BaseModel): id: int description: str + def test_event_parser_invalid_event(): event = {"id": "forgot-the-id", "description": "really nice blouse"} # 'id' is invalid @@ -228,6 +232,7 @@ def handler(event, _: Any) -> str: ret = handler(test_input, None) assert ret == expected + @pytest.mark.parametrize( "test_input,expected", [