diff --git a/noxfile.py b/noxfile.py index b47b7e26254..2fd0d14fa38 100644 --- a/noxfile.py +++ b/noxfile.py @@ -54,6 +54,7 @@ def test_with_only_required_packages(session: nox.Session): # Metrics - Base provider # Middleware factory without tracer # Typing + # Event Handler without OpenAPI build_and_run_test( session, folders=[ @@ -61,6 +62,7 @@ def test_with_only_required_packages(session: nox.Session): f"{PREFIX_TESTS_FUNCTIONAL}/metrics/required_dependencies/", f"{PREFIX_TESTS_FUNCTIONAL}/middleware_factory/required_dependencies/", f"{PREFIX_TESTS_FUNCTIONAL}/typing/required_dependencies/", + f"{PREFIX_TESTS_FUNCTIONAL}/event_handler/required_dependencies/", ], ) @@ -134,3 +136,19 @@ def test_with_aws_encryption_sdk_as_required_package(session: nox.Session): ], extras="datamasking", ) + + +@nox.session() +@nox.parametrize("pydantic", ["1.10", "2.0"]) +def test_with_pydantic_required_package(session: nox.Session, pydantic: str): + """Tests that only depends for required libraries""" + # Event Handler OpenAPI + + session.install(f"pydantic>={pydantic}") + + build_and_run_test( + session, + folders=[ + f"{PREFIX_TESTS_FUNCTIONAL}/event_handler/_pydantic/", + ], + ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/event_handler/_pydantic/__init__.py b/tests/functional/event_handler/_pydantic/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/event_handler/conftest.py b/tests/functional/event_handler/_pydantic/conftest.py similarity index 100% rename from tests/functional/event_handler/conftest.py rename to tests/functional/event_handler/_pydantic/conftest.py diff --git a/tests/functional/event_handler/_pydantic/test_api_gateway.py b/tests/functional/event_handler/_pydantic/test_api_gateway.py new file mode 100644 index 00000000000..dcd05c4f1f7 --- /dev/null +++ b/tests/functional/event_handler/_pydantic/test_api_gateway.py @@ -0,0 +1,80 @@ +from pydantic import BaseModel + +from aws_lambda_powertools.event_handler import content_types +from aws_lambda_powertools.event_handler.api_gateway import ( + ApiGatewayResolver, + Response, +) +from aws_lambda_powertools.event_handler.openapi.exceptions import RequestValidationError +from tests.functional.utils import load_event + +LOAD_GW_EVENT = load_event("apiGatewayProxyEvent.json") + + +def test_exception_handler_with_data_validation(): + # GIVEN a resolver with an exception handler defined for RequestValidationError + app = ApiGatewayResolver(enable_validation=True) + + @app.exception_handler(RequestValidationError) + def handle_validation_error(ex: RequestValidationError): + return Response( + status_code=422, + content_type=content_types.TEXT_PLAIN, + body=f"Invalid data. Number of errors: {len(ex.errors())}", + ) + + @app.get("/my/path") + def get_lambda(param: int): ... + + # WHEN calling the event handler + # AND a RequestValidationError is raised + result = app(LOAD_GW_EVENT, {}) + + # THEN call the exception_handler + assert result["statusCode"] == 422 + assert result["multiValueHeaders"]["Content-Type"] == [content_types.TEXT_PLAIN] + assert result["body"] == "Invalid data. Number of errors: 1" + + +def test_exception_handler_with_data_validation_pydantic_response(): + # GIVEN a resolver with an exception handler defined for RequestValidationError + app = ApiGatewayResolver(enable_validation=True) + + class Err(BaseModel): + msg: str + + @app.exception_handler(RequestValidationError) + def handle_validation_error(ex: RequestValidationError): + return Response( + status_code=422, + content_type=content_types.APPLICATION_JSON, + body=Err(msg=f"Invalid data. Number of errors: {len(ex.errors())}"), + ) + + @app.get("/my/path") + def get_lambda(param: int): ... + + # WHEN calling the event handler + # AND a RequestValidationError is raised + result = app(LOAD_GW_EVENT, {}) + + # THEN exception handler's pydantic response should be serialized correctly + assert result["statusCode"] == 422 + assert result["body"] == '{"msg":"Invalid data. Number of errors: 1"}' + + +def test_data_validation_error(): + # GIVEN a resolver without an exception handler + app = ApiGatewayResolver(enable_validation=True) + + @app.get("/my/path") + def get_lambda(param: int): ... + + # WHEN calling the event handler + # AND a RequestValidationError is raised + result = app(LOAD_GW_EVENT, {}) + + # THEN call the exception_handler + assert result["statusCode"] == 422 + assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] + assert "missing" in result["body"] diff --git a/tests/functional/event_handler/test_bedrock_agent.py b/tests/functional/event_handler/_pydantic/test_bedrock_agent.py similarity index 100% rename from tests/functional/event_handler/test_bedrock_agent.py rename to tests/functional/event_handler/_pydantic/test_bedrock_agent.py diff --git a/tests/functional/event_handler/test_openapi_encoders.py b/tests/functional/event_handler/_pydantic/test_openapi_encoders.py similarity index 100% rename from tests/functional/event_handler/test_openapi_encoders.py rename to tests/functional/event_handler/_pydantic/test_openapi_encoders.py diff --git a/tests/functional/event_handler/test_openapi_params.py b/tests/functional/event_handler/_pydantic/test_openapi_params.py similarity index 100% rename from tests/functional/event_handler/test_openapi_params.py rename to tests/functional/event_handler/_pydantic/test_openapi_params.py diff --git a/tests/functional/event_handler/test_openapi_responses.py b/tests/functional/event_handler/_pydantic/test_openapi_responses.py similarity index 100% rename from tests/functional/event_handler/test_openapi_responses.py rename to tests/functional/event_handler/_pydantic/test_openapi_responses.py diff --git a/tests/functional/event_handler/test_openapi_schema_pydantic_v1.py b/tests/functional/event_handler/_pydantic/test_openapi_schema_pydantic_v1.py similarity index 100% rename from tests/functional/event_handler/test_openapi_schema_pydantic_v1.py rename to tests/functional/event_handler/_pydantic/test_openapi_schema_pydantic_v1.py diff --git a/tests/functional/event_handler/test_openapi_schema_pydantic_v2.py b/tests/functional/event_handler/_pydantic/test_openapi_schema_pydantic_v2.py similarity index 100% rename from tests/functional/event_handler/test_openapi_schema_pydantic_v2.py rename to tests/functional/event_handler/_pydantic/test_openapi_schema_pydantic_v2.py diff --git a/tests/functional/event_handler/test_openapi_security.py b/tests/functional/event_handler/_pydantic/test_openapi_security.py similarity index 100% rename from tests/functional/event_handler/test_openapi_security.py rename to tests/functional/event_handler/_pydantic/test_openapi_security.py diff --git a/tests/functional/event_handler/test_openapi_security_schemes.py b/tests/functional/event_handler/_pydantic/test_openapi_security_schemes.py similarity index 100% rename from tests/functional/event_handler/test_openapi_security_schemes.py rename to tests/functional/event_handler/_pydantic/test_openapi_security_schemes.py diff --git a/tests/functional/event_handler/test_openapi_serialization.py b/tests/functional/event_handler/_pydantic/test_openapi_serialization.py similarity index 100% rename from tests/functional/event_handler/test_openapi_serialization.py rename to tests/functional/event_handler/_pydantic/test_openapi_serialization.py diff --git a/tests/functional/event_handler/test_openapi_servers.py b/tests/functional/event_handler/_pydantic/test_openapi_servers.py similarity index 100% rename from tests/functional/event_handler/test_openapi_servers.py rename to tests/functional/event_handler/_pydantic/test_openapi_servers.py diff --git a/tests/functional/event_handler/test_openapi_swagger.py b/tests/functional/event_handler/_pydantic/test_openapi_swagger.py similarity index 100% rename from tests/functional/event_handler/test_openapi_swagger.py rename to tests/functional/event_handler/_pydantic/test_openapi_swagger.py diff --git a/tests/functional/event_handler/test_openapi_tags.py b/tests/functional/event_handler/_pydantic/test_openapi_tags.py similarity index 100% rename from tests/functional/event_handler/test_openapi_tags.py rename to tests/functional/event_handler/_pydantic/test_openapi_tags.py diff --git a/tests/functional/event_handler/test_openapi_validation_middleware.py b/tests/functional/event_handler/_pydantic/test_openapi_validation_middleware.py similarity index 100% rename from tests/functional/event_handler/test_openapi_validation_middleware.py rename to tests/functional/event_handler/_pydantic/test_openapi_validation_middleware.py diff --git a/tests/functional/event_handler/required_dependencies/__init__.py b/tests/functional/event_handler/required_dependencies/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/event_handler/required_dependencies/conftest.py b/tests/functional/event_handler/required_dependencies/conftest.py new file mode 100644 index 00000000000..5c2bdb7729a --- /dev/null +++ b/tests/functional/event_handler/required_dependencies/conftest.py @@ -0,0 +1,73 @@ +import json + +import pytest + +from tests.functional.utils import load_event + + +@pytest.fixture +def json_dump(): + # our serializers reduce length to save on costs; fixture to replicate separators + return lambda obj: json.dumps(obj, separators=(",", ":")) + + +@pytest.fixture +def validation_schema(): + return { + "$schema": "https://json-schema.org/draft-07/schema", + "$id": "https://example.com/example.json", + "type": "object", + "title": "Sample schema", + "description": "The root schema comprises the entire JSON document.", + "examples": [{"message": "hello world", "username": "lessa"}], + "required": ["message", "username"], + "properties": { + "message": { + "$id": "#/properties/message", + "type": "string", + "title": "The message", + "examples": ["hello world"], + }, + "username": { + "$id": "#/properties/username", + "type": "string", + "title": "The username", + "examples": ["lessa"], + }, + }, + } + + +@pytest.fixture +def raw_event(): + return {"message": "hello hello", "username": "blah blah"} + + +@pytest.fixture +def gw_event(): + return load_event("apiGatewayProxyEvent.json") + + +@pytest.fixture +def gw_event_http(): + return load_event("apiGatewayProxyV2Event.json") + + +@pytest.fixture +def gw_event_alb(): + return load_event("albMultiValueQueryStringEvent.json") + + +@pytest.fixture +def gw_event_lambda_url(): + return load_event("lambdaFunctionUrlEventWithHeaders.json") + + +@pytest.fixture +def gw_event_vpc_lattice(): + return load_event("vpcLatticeV2EventWithHeaders.json") + + +@pytest.fixture +def gw_event_vpc_lattice_v1(): + return load_event("vpcLatticeEvent.json") diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/required_dependencies/test_api_gateway.py similarity index 95% rename from tests/functional/event_handler/test_api_gateway.py rename to tests/functional/event_handler/required_dependencies/test_api_gateway.py index 14af1ad175e..efd7edf0e5e 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/required_dependencies/test_api_gateway.py @@ -10,7 +10,6 @@ from typing import Dict import pytest -from pydantic import BaseModel from aws_lambda_powertools.event_handler import content_types from aws_lambda_powertools.event_handler.api_gateway import ( @@ -31,7 +30,6 @@ ServiceError, UnauthorizedError, ) -from aws_lambda_powertools.event_handler.openapi.exceptions import RequestValidationError from aws_lambda_powertools.shared import constants from aws_lambda_powertools.shared.cookies import Cookie from aws_lambda_powertools.shared.json_encoder import Encoder @@ -45,7 +43,7 @@ def read_media(file_name: str) -> bytes: - path = Path(str(Path(__file__).parent.parent.parent.parent) + "/docs/media/" + file_name) + path = Path(str(Path(__file__).parent.parent.parent.parent) + "/../docs/media/" + file_name) return path.read_bytes() @@ -1458,58 +1456,6 @@ def get_lambda() -> Response: assert result["body"] == "Foo!" -def test_exception_handler_with_data_validation(): - # GIVEN a resolver with an exception handler defined for RequestValidationError - app = ApiGatewayResolver(enable_validation=True) - - @app.exception_handler(RequestValidationError) - def handle_validation_error(ex: RequestValidationError): - return Response( - status_code=422, - content_type=content_types.TEXT_PLAIN, - body=f"Invalid data. Number of errors: {len(ex.errors())}", - ) - - @app.get("/my/path") - def get_lambda(param: int): ... - - # WHEN calling the event handler - # AND a RequestValidationError is raised - result = app(LOAD_GW_EVENT, {}) - - # THEN call the exception_handler - assert result["statusCode"] == 422 - assert result["multiValueHeaders"]["Content-Type"] == [content_types.TEXT_PLAIN] - assert result["body"] == "Invalid data. Number of errors: 1" - - -def test_exception_handler_with_data_validation_pydantic_response(): - # GIVEN a resolver with an exception handler defined for RequestValidationError - app = ApiGatewayResolver(enable_validation=True) - - class Err(BaseModel): - msg: str - - @app.exception_handler(RequestValidationError) - def handle_validation_error(ex: RequestValidationError): - return Response( - status_code=422, - content_type=content_types.APPLICATION_JSON, - body=Err(msg=f"Invalid data. Number of errors: {len(ex.errors())}"), - ) - - @app.get("/my/path") - def get_lambda(param: int): ... - - # WHEN calling the event handler - # AND a RequestValidationError is raised - result = app(LOAD_GW_EVENT, {}) - - # THEN exception handler's pydantic response should be serialized correctly - assert result["statusCode"] == 422 - assert result["body"] == '{"msg":"Invalid data. Number of errors: 1"}' - - def test_exception_handler_with_route(): app = ApiGatewayResolver() # GIVEN a Router object with an exception handler defined for ValueError @@ -1540,23 +1486,6 @@ def get_lambda() -> Response: assert result["body"] == "Foo!" -def test_data_validation_error(): - # GIVEN a resolver without an exception handler - app = ApiGatewayResolver(enable_validation=True) - - @app.get("/my/path") - def get_lambda(param: int): ... - - # WHEN calling the event handler - # AND a RequestValidationError is raised - result = app(LOAD_GW_EVENT, {}) - - # THEN call the exception_handler - assert result["statusCode"] == 422 - assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] - assert "missing" in result["body"] - - def test_exception_handler_service_error(): # GIVEN app = ApiGatewayResolver() diff --git a/tests/functional/event_handler/test_api_middlewares.py b/tests/functional/event_handler/required_dependencies/test_api_middlewares.py similarity index 100% rename from tests/functional/event_handler/test_api_middlewares.py rename to tests/functional/event_handler/required_dependencies/test_api_middlewares.py diff --git a/tests/functional/event_handler/test_appsync.py b/tests/functional/event_handler/required_dependencies/test_appsync.py similarity index 100% rename from tests/functional/event_handler/test_appsync.py rename to tests/functional/event_handler/required_dependencies/test_appsync.py diff --git a/tests/functional/event_handler/test_base_path.py b/tests/functional/event_handler/required_dependencies/test_base_path.py similarity index 86% rename from tests/functional/event_handler/test_base_path.py rename to tests/functional/event_handler/required_dependencies/test_base_path.py index 479a46bda55..7fc5a0eced7 100644 --- a/tests/functional/event_handler/test_base_path.py +++ b/tests/functional/event_handler/required_dependencies/test_base_path.py @@ -10,7 +10,7 @@ def test_base_path_api_gateway_rest(): - app = APIGatewayRestResolver(enable_validation=True) + app = APIGatewayRestResolver() @app.get("/") def handle(): @@ -25,7 +25,7 @@ def handle(): def test_base_path_api_gateway_http(): - app = APIGatewayHttpResolver(enable_validation=True) + app = APIGatewayHttpResolver() @app.get("/") def handle(): @@ -42,7 +42,7 @@ def handle(): def test_base_path_alb(): - app = ALBResolver(enable_validation=True) + app = ALBResolver() @app.get("/") def handle(): @@ -57,7 +57,7 @@ def handle(): def test_base_path_lambda_function_url(): - app = LambdaFunctionUrlResolver(enable_validation=True) + app = LambdaFunctionUrlResolver() @app.get("/") def handle(): @@ -74,7 +74,7 @@ def handle(): def test_vpc_lattice(): - app = VPCLatticeResolver(enable_validation=True) + app = VPCLatticeResolver() @app.get("/") def handle(): @@ -89,7 +89,7 @@ def handle(): def test_vpc_latticev2(): - app = VPCLatticeV2Resolver(enable_validation=True) + app = VPCLatticeV2Resolver() @app.get("/") def handle(): diff --git a/tests/functional/event_handler/test_lambda_function_url.py b/tests/functional/event_handler/required_dependencies/test_lambda_function_url.py similarity index 100% rename from tests/functional/event_handler/test_lambda_function_url.py rename to tests/functional/event_handler/required_dependencies/test_lambda_function_url.py diff --git a/tests/functional/event_handler/test_router.py b/tests/functional/event_handler/required_dependencies/test_router.py similarity index 100% rename from tests/functional/event_handler/test_router.py rename to tests/functional/event_handler/required_dependencies/test_router.py diff --git a/tests/functional/event_handler/test_vpc_lattice.py b/tests/functional/event_handler/required_dependencies/test_vpc_lattice.py similarity index 100% rename from tests/functional/event_handler/test_vpc_lattice.py rename to tests/functional/event_handler/required_dependencies/test_vpc_lattice.py diff --git a/tests/functional/event_handler/test_vpc_latticev2.py b/tests/functional/event_handler/required_dependencies/test_vpc_latticev2.py similarity index 100% rename from tests/functional/event_handler/test_vpc_latticev2.py rename to tests/functional/event_handler/required_dependencies/test_vpc_latticev2.py diff --git a/tests/functional/idempotency/test_idempotency.py b/tests/functional/idempotency/test_idempotency.py index 83ee16f328b..d056bd4e95a 100644 --- a/tests/functional/idempotency/test_idempotency.py +++ b/tests/functional/idempotency/test_idempotency.py @@ -61,7 +61,7 @@ from tests.functional.utils import json_serialize, load_event TABLE_NAME = "TEST_TABLE" -TESTS_MODULE_PREFIX = "test-func.functional.idempotency.test_idempotency" +TESTS_MODULE_PREFIX = "test-func.tests.functional.idempotency.test_idempotency" def get_dataclasses_lib(): diff --git a/tests/functional/idempotency/utils.py b/tests/functional/idempotency/utils.py index c396e40957c..bba8f320438 100644 --- a/tests/functional/idempotency/utils.py +++ b/tests/functional/idempotency/utils.py @@ -19,7 +19,7 @@ def build_idempotency_put_item_stub( data: Dict, function_name: str = "test-func", function_qualified_name: str = "test_idempotent_lambda_first_execution_event_mutation.", - module_name: str = "functional.idempotency.test_idempotency", + module_name: str = "tests.functional.idempotency.test_idempotency", handler_name: str = "lambda_handler", ) -> Dict: idempotency_key_hash = ( @@ -57,7 +57,7 @@ def build_idempotency_update_item_stub( handler_response: Dict, function_name: str = "test-func", function_qualified_name: str = "test_idempotent_lambda_first_execution_event_mutation.", - module_name: str = "functional.idempotency.test_idempotency", + module_name: str = "tests.functional.idempotency.test_idempotency", handler_name: str = "lambda_handler", ) -> Dict: idempotency_key_hash = ( diff --git a/tests/unit/test_tracing.py b/tests/unit/test_tracing.py index 0d12afa629b..2c52d8e9dc0 100644 --- a/tests/unit/test_tracing.py +++ b/tests/unit/test_tracing.py @@ -9,7 +9,7 @@ # Maintenance: This should move to Functional tests and use Fake over mocks. -MODULE_PREFIX = "unit.test_tracing" +MODULE_PREFIX = "tests.unit.test_tracing" @pytest.fixture