From 44abceeb3a93f3b167f547c77b582f8916bf05a2 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 4 Jul 2021 11:28:35 -0700 Subject: [PATCH 01/10] feat(api-gateway): add debug mode --- .../event_handler/api_gateway.py | 30 +++++++++++++- aws_lambda_powertools/shared/constants.py | 2 + .../event_handler/test_api_gateway.py | 41 +++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 2b1e1fc0900..93f7823606c 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -1,11 +1,15 @@ import base64 import json import logging +import os import re +import traceback import zlib from enum import Enum from typing import Any, Callable, Dict, List, Optional, Set, Union +from aws_lambda_powertools.shared import constants +from aws_lambda_powertools.shared.functions import resolve_truthy_env_var_choice from aws_lambda_powertools.shared.json_encoder import Encoder from aws_lambda_powertools.utilities.data_classes import ALBEvent, APIGatewayProxyEvent, APIGatewayProxyEventV2 from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent @@ -237,7 +241,12 @@ def lambda_handler(event, context): current_event: BaseProxyEvent lambda_context: LambdaContext - def __init__(self, proxy_type: Enum = ProxyEventType.APIGatewayProxyEvent, cors: CORSConfig = None): + def __init__( + self, + proxy_type: Enum = ProxyEventType.APIGatewayProxyEvent, + cors: CORSConfig = None, + debug: Optional[bool] = None, + ): """ Parameters ---------- @@ -245,12 +254,15 @@ def __init__(self, proxy_type: Enum = ProxyEventType.APIGatewayProxyEvent, cors: Proxy request type, defaults to API Gateway V1 cors: CORSConfig Optionally configure and enabled CORS. Not each route will need to have to cors=True + debug: Optional[bool] + Enables debug mode, by default False, can be enabled by an environement variable """ self._proxy_type = proxy_type self._routes: List[Route] = [] self._cors = cors self._cors_enabled: bool = cors is not None self._cors_methods: Set[str] = {"OPTIONS"} + self.debug = resolve_truthy_env_var_choice(choice=debug, env=os.getenv(constants.API_DEBUG_ENV, "false")) def get(self, rule: str, cors: bool = None, compress: bool = False, cache_control: str = None): """Get route decorator with GET `method` @@ -475,7 +487,21 @@ def _not_found(self, method: str) -> ResponseBuilder: def _call_route(self, route: Route, args: Dict[str, str]) -> ResponseBuilder: """Actually call the matching route with any provided keyword arguments.""" - return ResponseBuilder(self._to_response(route.func(**args)), route) + try: + return ResponseBuilder(self._to_response(route.func(**args)), route) + except Exception: + if self.debug: + # If the user has turned on debug mode, + # we'll let the original exception propagate so + # they get more information about what went wrong. + return ResponseBuilder( + Response( + status_code=500, + content_type="text/plain", + body="".join(traceback.format_exc()), + ) + ) + raise @staticmethod def _to_response(result: Union[Dict, Response]) -> Response: diff --git a/aws_lambda_powertools/shared/constants.py b/aws_lambda_powertools/shared/constants.py index eaad5640dfd..a9008d86db9 100644 --- a/aws_lambda_powertools/shared/constants.py +++ b/aws_lambda_powertools/shared/constants.py @@ -18,3 +18,5 @@ XRAY_SDK_MODULE = "aws_xray_sdk" XRAY_SDK_CORE_MODULE = "aws_xray_sdk.core" + +API_DEBUG_ENV: str = "POWERTOOLS_API_DEBUG" diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index caaaeb1b97b..6205a925eaf 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -5,6 +5,8 @@ from pathlib import Path from typing import Dict +import pytest + from aws_lambda_powertools.event_handler.api_gateway import ( ApiGatewayResolver, CORSConfig, @@ -490,3 +492,42 @@ def custom_method(): assert headers["Content-Type"] == TEXT_HTML assert "Access-Control-Allow-Origin" in result["headers"] assert headers["Access-Control-Allow-Methods"] == "CUSTOM" + + +def test_unhandled_exceptions_debug_on(): + # GIVEN debug is enabled + # AND an unhandlable exception is raised + app = ApiGatewayResolver(debug=True) + + @app.get("/raises-error") + def raises_error(): + raise RuntimeError("Foo") + + # WHEN calling the handler + result = app({"path": "/raises-error", "httpMethod": "GET"}, None) + + # THEN return a 500 + # AND Content-Type is set to text/plain + # AND include the exception traceback in the response + assert result["statusCode"] == 500 + assert "Traceback (most recent call last)" in result["body"] + headers = result["headers"] + assert headers["Content-Type"] == "text/plain" + + +def test_unhandled_exceptions_debug_off(): + # GIVEN debug is disabled + # AND an unhandlable exception is raised + app = ApiGatewayResolver(debug=False) + + @app.get("/raises-error") + def raises_error(): + raise RuntimeError("Foo") + + # WHEN calling the handler + # THEN raise the original exception + with pytest.raises(RuntimeError) as e: + app({"path": "/raises-error", "httpMethod": "GET"}, None) + + # AND include the original error + assert e.value.args == ("Foo",) From 7d73c042289cbd3184caddbc9c22924f1cc21468 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 4 Jul 2021 17:20:40 -0700 Subject: [PATCH 02/10] test(api-gateway): test for debug environment variable --- tests/functional/event_handler/test_api_gateway.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 6205a925eaf..aeabdf72a3f 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -14,6 +14,7 @@ Response, ResponseBuilder, ) +from aws_lambda_powertools.shared import constants from aws_lambda_powertools.shared.json_encoder import Encoder from aws_lambda_powertools.utilities.data_classes import ALBEvent, APIGatewayProxyEvent, APIGatewayProxyEventV2 from tests.functional.utils import load_event @@ -498,6 +499,7 @@ def test_unhandled_exceptions_debug_on(): # GIVEN debug is enabled # AND an unhandlable exception is raised app = ApiGatewayResolver(debug=True) + assert app.debug @app.get("/raises-error") def raises_error(): @@ -519,6 +521,7 @@ def test_unhandled_exceptions_debug_off(): # GIVEN debug is disabled # AND an unhandlable exception is raised app = ApiGatewayResolver(debug=False) + assert not app.debug @app.get("/raises-error") def raises_error(): @@ -531,3 +534,13 @@ def raises_error(): # AND include the original error assert e.value.args == ("Foo",) + + +def test_debug_mode_environment_variable(monkeypatch): + # GIVEN a debug mode environment variable is set + monkeypatch.setenv(constants.API_DEBUG_ENV, "true") + app = ApiGatewayResolver() + + # WHEN calling app.debug + # THEN the debug mode is enabled + assert app.debug From 3f994e6a2a3a62a446af83c9f7850ccac7275762 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 4 Jul 2021 17:28:26 -0700 Subject: [PATCH 03/10] refactor(api-gateway): rename to _debug and fix spelling --- aws_lambda_powertools/event_handler/api_gateway.py | 6 +++--- tests/functional/event_handler/test_api_gateway.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 93f7823606c..456a852770e 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -255,14 +255,14 @@ def __init__( cors: CORSConfig Optionally configure and enabled CORS. Not each route will need to have to cors=True debug: Optional[bool] - Enables debug mode, by default False, can be enabled by an environement variable + Enables debug mode, by default False. Can be enabled by "POWERTOOLS_API_DEBUG" environment variable """ self._proxy_type = proxy_type self._routes: List[Route] = [] self._cors = cors self._cors_enabled: bool = cors is not None self._cors_methods: Set[str] = {"OPTIONS"} - self.debug = resolve_truthy_env_var_choice(choice=debug, env=os.getenv(constants.API_DEBUG_ENV, "false")) + self._debug = resolve_truthy_env_var_choice(choice=debug, env=os.getenv(constants.API_DEBUG_ENV, "false")) def get(self, rule: str, cors: bool = None, compress: bool = False, cache_control: str = None): """Get route decorator with GET `method` @@ -490,7 +490,7 @@ def _call_route(self, route: Route, args: Dict[str, str]) -> ResponseBuilder: try: return ResponseBuilder(self._to_response(route.func(**args)), route) except Exception: - if self.debug: + if self._debug: # If the user has turned on debug mode, # we'll let the original exception propagate so # they get more information about what went wrong. diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index aeabdf72a3f..925751bfc36 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -497,9 +497,9 @@ def custom_method(): def test_unhandled_exceptions_debug_on(): # GIVEN debug is enabled - # AND an unhandlable exception is raised + # AND an unhandled exception is raised app = ApiGatewayResolver(debug=True) - assert app.debug + assert app._debug @app.get("/raises-error") def raises_error(): @@ -519,9 +519,9 @@ def raises_error(): def test_unhandled_exceptions_debug_off(): # GIVEN debug is disabled - # AND an unhandlable exception is raised + # AND an unhandled exception is raised app = ApiGatewayResolver(debug=False) - assert not app.debug + assert not app._debug @app.get("/raises-error") def raises_error(): @@ -541,6 +541,6 @@ def test_debug_mode_environment_variable(monkeypatch): monkeypatch.setenv(constants.API_DEBUG_ENV, "true") app = ApiGatewayResolver() - # WHEN calling app.debug + # WHEN calling app._debug # THEN the debug mode is enabled - assert app.debug + assert app._debug From a6760f7bbebf8faf31b6328f44ed113433a3ac94 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 4 Jul 2021 21:54:55 -0700 Subject: [PATCH 04/10] feat(api-gateway): pretty print json in debug mode --- .../event_handler/api_gateway.py | 14 ++++++++++---- .../functional/event_handler/test_api_gateway.py | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 456a852770e..6ce7a201a94 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -481,7 +481,7 @@ def _not_found(self, method: str) -> ResponseBuilder: status_code=404, content_type="application/json", headers=headers, - body=json.dumps({"message": "Not found"}), + body=self._json_dump({"message": "Not found"}), ) ) @@ -503,8 +503,7 @@ def _call_route(self, route: Route, args: Dict[str, str]) -> ResponseBuilder: ) raise - @staticmethod - def _to_response(result: Union[Dict, Response]) -> Response: + def _to_response(self, result: Union[Dict, Response]) -> Response: """Convert the route's result to a Response 2 main result types are supported: @@ -520,5 +519,12 @@ def _to_response(result: Union[Dict, Response]) -> Response: return Response( status_code=200, content_type="application/json", - body=json.dumps(result, separators=(",", ":"), cls=Encoder), + body=self._json_dump(result), ) + + def _json_dump(self, obj: Any) -> str: + """Does a concise json serialization""" + if self._debug: + return json.dumps(obj, indent=4, cls=Encoder) + else: + return json.dumps(obj, separators=(",", ":"), cls=Encoder) diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 925751bfc36..9278ecc8196 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -544,3 +544,19 @@ def test_debug_mode_environment_variable(monkeypatch): # WHEN calling app._debug # THEN the debug mode is enabled assert app._debug + + +def test_debug_json_formatting(): + # GIVEN debug is True + app = ApiGatewayResolver(debug=True) + response = {"message": "Foo"} + + @app.get("/foo") + def foo(): + return response + + # WHEN calling the handler + result = app({"path": "/foo", "httpMethod": "GET"}, None) + + # THEN return a pretty print json in the body + assert result["body"] == json.dumps(response, indent=4) From 1e65399bd776fc41a283359ddabe0fcfc8b18ec7 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Mon, 5 Jul 2021 12:07:43 -0700 Subject: [PATCH 05/10] refactor(data-classes): rename to POWERTOOLS_EVENT_HANDLER_DEBUG --- aws_lambda_powertools/event_handler/api_gateway.py | 7 +++++-- aws_lambda_powertools/shared/constants.py | 2 +- tests/functional/event_handler/test_api_gateway.py | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 6ce7a201a94..2f779d47a52 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -255,14 +255,17 @@ def __init__( cors: CORSConfig Optionally configure and enabled CORS. Not each route will need to have to cors=True debug: Optional[bool] - Enables debug mode, by default False. Can be enabled by "POWERTOOLS_API_DEBUG" environment variable + Enables debug mode, by default False. Can be also be enabled by "POWERTOOLS_EVENT_HANDLER_DEBUG" + environment variable """ self._proxy_type = proxy_type self._routes: List[Route] = [] self._cors = cors self._cors_enabled: bool = cors is not None self._cors_methods: Set[str] = {"OPTIONS"} - self._debug = resolve_truthy_env_var_choice(choice=debug, env=os.getenv(constants.API_DEBUG_ENV, "false")) + self._debug = resolve_truthy_env_var_choice( + choice=debug, env=os.getenv(constants.EVENT_HANDLER_DEBUG_ENV, "false") + ) def get(self, rule: str, cors: bool = None, compress: bool = False, cache_control: str = None): """Get route decorator with GET `method` diff --git a/aws_lambda_powertools/shared/constants.py b/aws_lambda_powertools/shared/constants.py index a9008d86db9..05eac6cd8c4 100644 --- a/aws_lambda_powertools/shared/constants.py +++ b/aws_lambda_powertools/shared/constants.py @@ -19,4 +19,4 @@ XRAY_SDK_MODULE = "aws_xray_sdk" XRAY_SDK_CORE_MODULE = "aws_xray_sdk.core" -API_DEBUG_ENV: str = "POWERTOOLS_API_DEBUG" +EVENT_HANDLER_DEBUG_ENV: str = "POWERTOOLS_EVENT_HANDLER_DEBUG" diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 9278ecc8196..e9d6933cf2d 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -538,7 +538,7 @@ def raises_error(): def test_debug_mode_environment_variable(monkeypatch): # GIVEN a debug mode environment variable is set - monkeypatch.setenv(constants.API_DEBUG_ENV, "true") + monkeypatch.setenv(constants.EVENT_HANDLER_DEBUG_ENV, "true") app = ApiGatewayResolver() # WHEN calling app._debug From fe76aa404f573361b170c0023a9aa02c8df8770e Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Tue, 6 Jul 2021 11:14:59 -0700 Subject: [PATCH 06/10] chore: add text/html content types --- .../event_handler/api_gateway.py | 55 ++++++++++--------- .../event_handler/content_types.py | 3 +- .../event_handler/test_api_gateway.py | 39 +++++++------ 3 files changed, 50 insertions(+), 47 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 534ff94a1a1..35457b1e05a 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -32,43 +32,46 @@ class ProxyEventType(Enum): class CORSConfig(object): """CORS Config - Examples -------- Simple cors example using the default permissive cors, not this should only be used during early prototyping - from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver + ```python + from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver - app = ApiGatewayResolver() + app = ApiGatewayResolver() - @app.get("/my/path", cors=True) - def with_cors(): - return {"message": "Foo"} + @app.get("/my/path", cors=True) + def with_cors(): + return {"message": "Foo"} + ``` Using a custom CORSConfig where `with_cors` used the custom provided CORSConfig and `without_cors` do not include any cors headers. - from aws_lambda_powertools.event_handler.api_gateway import ( - ApiGatewayResolver, CORSConfig - ) - - cors_config = CORSConfig( - allow_origin="https://wwww.example.com/", - expose_headers=["x-exposed-response-header"], - allow_headers=["x-custom-request-header"], - max_age=100, - allow_credentials=True, - ) - app = ApiGatewayResolver(cors=cors_config) - - @app.get("/my/path") - def with_cors(): - return {"message": "Foo"} + ```python + from aws_lambda_powertools.event_handler.api_gateway import ( + ApiGatewayResolver, CORSConfig + ) + + cors_config = CORSConfig( + allow_origin="https://wwww.example.com/", + expose_headers=["x-exposed-response-header"], + allow_headers=["x-custom-request-header"], + max_age=100, + allow_credentials=True, + ) + app = ApiGatewayResolver(cors=cors_config) + + @app.get("/my/path") + def with_cors(): + return {"message": "Foo"} - @app.get("/another-one", cors=False) - def without_cors(): - return {"message": "Foo"} + @app.get("/another-one", cors=False) + def without_cors(): + return {"message": "Foo"} + ``` """ _REQUIRED_HEADERS = ["Authorization", "Content-Type", "X-Amz-Date", "X-Api-Key", "X-Amz-Security-Token"] @@ -512,7 +515,7 @@ def _call_route(self, route: Route, args: Dict[str, str]) -> ResponseBuilder: return ResponseBuilder( Response( status_code=500, - content_type="text/plain", + content_type=content_types.TEXT_PLAIN, body="".join(traceback.format_exc()), ) ) diff --git a/aws_lambda_powertools/event_handler/content_types.py b/aws_lambda_powertools/event_handler/content_types.py index 00ec3db168e..0f55b1088ad 100644 --- a/aws_lambda_powertools/event_handler/content_types.py +++ b/aws_lambda_powertools/event_handler/content_types.py @@ -1,4 +1,5 @@ # use mimetypes library to be certain, e.g., mimetypes.types_map[".json"] APPLICATION_JSON = "application/json" -PLAIN_TEXT = "text/plain" +TEXT_PLAIN = "text/plain" +TEXT_HTML = "text/html" diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index ae0a69c66eb..738ebd4794f 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -34,7 +34,6 @@ def read_media(file_name: str) -> bytes: LOAD_GW_EVENT = load_event("apiGatewayProxyEvent.json") -TEXT_HTML = "text/html" def test_alb_event(): @@ -45,7 +44,7 @@ def test_alb_event(): def foo(): assert isinstance(app.current_event, ALBEvent) assert app.lambda_context == {} - return Response(200, TEXT_HTML, "foo") + return Response(200, content_types.TEXT_HTML, "foo") # WHEN calling the event handler result = app(load_event("albEvent.json"), {}) @@ -53,7 +52,7 @@ def foo(): # THEN process event correctly # AND set the current_event type as ALBEvent assert result["statusCode"] == 200 - assert result["headers"]["Content-Type"] == TEXT_HTML + assert result["headers"]["Content-Type"] == content_types.TEXT_HTML assert result["body"] == "foo" @@ -83,7 +82,7 @@ def test_api_gateway(): @app.get("/my/path") def get_lambda() -> Response: assert isinstance(app.current_event, APIGatewayProxyEvent) - return Response(200, TEXT_HTML, "foo") + return Response(200, content_types.TEXT_HTML, "foo") # WHEN calling the event handler result = app(LOAD_GW_EVENT, {}) @@ -91,7 +90,7 @@ def get_lambda() -> Response: # THEN process event correctly # AND set the current_event type as APIGatewayProxyEvent assert result["statusCode"] == 200 - assert result["headers"]["Content-Type"] == TEXT_HTML + assert result["headers"]["Content-Type"] == content_types.TEXT_HTML assert result["body"] == "foo" @@ -103,7 +102,7 @@ def test_api_gateway_v2(): def my_path() -> Response: assert isinstance(app.current_event, APIGatewayProxyEventV2) post_data = app.current_event.json_body - return Response(200, content_types.PLAIN_TEXT, post_data["username"]) + return Response(200, content_types.TEXT_PLAIN, post_data["username"]) # WHEN calling the event handler result = app(load_event("apiGatewayProxyV2Event.json"), {}) @@ -111,7 +110,7 @@ def my_path() -> Response: # THEN process event correctly # AND set the current_event type as APIGatewayProxyEventV2 assert result["statusCode"] == 200 - assert result["headers"]["Content-Type"] == content_types.PLAIN_TEXT + assert result["headers"]["Content-Type"] == content_types.TEXT_PLAIN assert result["body"] == "tom" @@ -122,14 +121,14 @@ def test_include_rule_matching(): @app.get("//") def get_lambda(my_id: str, name: str) -> Response: assert name == "my" - return Response(200, TEXT_HTML, my_id) + return Response(200, content_types.TEXT_HTML, my_id) # WHEN calling the event handler result = app(LOAD_GW_EVENT, {}) # THEN assert result["statusCode"] == 200 - assert result["headers"]["Content-Type"] == TEXT_HTML + assert result["headers"]["Content-Type"] == content_types.TEXT_HTML assert result["body"] == "path" @@ -190,11 +189,11 @@ def test_cors(): @app.get("/my/path", cors=True) def with_cors() -> Response: - return Response(200, TEXT_HTML, "test") + return Response(200, content_types.TEXT_HTML, "test") @app.get("/without-cors") def without_cors() -> Response: - return Response(200, TEXT_HTML, "test") + return Response(200, content_types.TEXT_HTML, "test") def handler(event, context): return app.resolve(event, context) @@ -205,7 +204,7 @@ def handler(event, context): # THEN the headers should include cors headers assert "headers" in result headers = result["headers"] - assert headers["Content-Type"] == TEXT_HTML + assert headers["Content-Type"] == content_types.TEXT_HTML assert headers["Access-Control-Allow-Origin"] == "*" assert "Access-Control-Allow-Credentials" not in headers assert headers["Access-Control-Allow-Headers"] == ",".join(sorted(CORSConfig._REQUIRED_HEADERS)) @@ -271,7 +270,7 @@ def test_compress_no_accept_encoding(): @app.get("/my/path", compress=True) def return_text() -> Response: - return Response(200, content_types.PLAIN_TEXT, expected_value) + return Response(200, content_types.TEXT_PLAIN, expected_value) # WHEN calling the event handler result = app({"path": "/my/path", "httpMethod": "GET", "headers": {}}, None) @@ -287,7 +286,7 @@ def test_cache_control_200(): @app.get("/success", cache_control="max-age=600") def with_cache_control() -> Response: - return Response(200, TEXT_HTML, "has 200 response") + return Response(200, content_types.TEXT_HTML, "has 200 response") def handler(event, context): return app.resolve(event, context) @@ -298,7 +297,7 @@ def handler(event, context): # THEN return the set Cache-Control headers = result["headers"] - assert headers["Content-Type"] == TEXT_HTML + assert headers["Content-Type"] == content_types.TEXT_HTML assert headers["Cache-Control"] == "max-age=600" @@ -308,7 +307,7 @@ def test_cache_control_non_200(): @app.delete("/fails", cache_control="max-age=600") def with_cache_control_has_500() -> Response: - return Response(503, TEXT_HTML, "has 503 response") + return Response(503, content_types.TEXT_HTML, "has 503 response") def handler(event, context): return app.resolve(event, context) @@ -319,7 +318,7 @@ def handler(event, context): # THEN return a Cache-Control of "no-cache" headers = result["headers"] - assert headers["Content-Type"] == TEXT_HTML + assert headers["Content-Type"] == content_types.TEXT_HTML assert headers["Cache-Control"] == "no-cache" @@ -482,7 +481,7 @@ def test_custom_preflight_response(): def custom_preflight(): return Response( status_code=200, - content_type=TEXT_HTML, + content_type=content_types.TEXT_HTML, body="Foo", headers={"Access-Control-Allow-Methods": "CUSTOM"}, ) @@ -498,7 +497,7 @@ def custom_method(): assert result["statusCode"] == 200 assert result["body"] == "Foo" headers = result["headers"] - assert headers["Content-Type"] == TEXT_HTML + assert headers["Content-Type"] == content_types.TEXT_HTML assert "Access-Control-Allow-Origin" in result["headers"] assert headers["Access-Control-Allow-Methods"] == "CUSTOM" @@ -522,7 +521,7 @@ def raises_error(): assert result["statusCode"] == 500 assert "Traceback (most recent call last)" in result["body"] headers = result["headers"] - assert headers["Content-Type"] == "text/plain" + assert headers["Content-Type"] == content_types.TEXT_PLAIN def test_unhandled_exceptions_debug_off(): From 4beaaaa324887293ed81159dbac24de9af7e1bc9 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Tue, 6 Jul 2021 12:46:51 -0700 Subject: [PATCH 07/10] feat(api-gateway): print json event when debug is on --- .../event_handler/api_gateway.py | 2 + .../event_handler/test_api_gateway.py | 151 ++++++++++-------- tests/functional/py.typed | 0 3 files changed, 86 insertions(+), 67 deletions(-) create mode 100644 tests/functional/py.typed diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 35457b1e05a..b6e9cd4698b 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -434,6 +434,8 @@ def resolve(self, event, context) -> Dict[str, Any]: dict Returns the dict response """ + if self._debug: + print(self._json_dump(event)) self.current_event = self._to_proxy_event(event) self.lambda_context = context return self._resolve().build(self.current_event, self._cors) diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 738ebd4794f..3d77aa7521c 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -1,9 +1,11 @@ import base64 +import builtins import json import zlib from decimal import Decimal from pathlib import Path from typing import Dict +from unittest.mock import MagicMock import pytest @@ -502,73 +504,6 @@ def custom_method(): assert headers["Access-Control-Allow-Methods"] == "CUSTOM" -def test_unhandled_exceptions_debug_on(): - # GIVEN debug is enabled - # AND an unhandled exception is raised - app = ApiGatewayResolver(debug=True) - assert app._debug - - @app.get("/raises-error") - def raises_error(): - raise RuntimeError("Foo") - - # WHEN calling the handler - result = app({"path": "/raises-error", "httpMethod": "GET"}, None) - - # THEN return a 500 - # AND Content-Type is set to text/plain - # AND include the exception traceback in the response - assert result["statusCode"] == 500 - assert "Traceback (most recent call last)" in result["body"] - headers = result["headers"] - assert headers["Content-Type"] == content_types.TEXT_PLAIN - - -def test_unhandled_exceptions_debug_off(): - # GIVEN debug is disabled - # AND an unhandled exception is raised - app = ApiGatewayResolver(debug=False) - assert not app._debug - - @app.get("/raises-error") - def raises_error(): - raise RuntimeError("Foo") - - # WHEN calling the handler - # THEN raise the original exception - with pytest.raises(RuntimeError) as e: - app({"path": "/raises-error", "httpMethod": "GET"}, None) - - # AND include the original error - assert e.value.args == ("Foo",) - - -def test_debug_mode_environment_variable(monkeypatch): - # GIVEN a debug mode environment variable is set - monkeypatch.setenv(constants.EVENT_HANDLER_DEBUG_ENV, "true") - app = ApiGatewayResolver() - - # WHEN calling app._debug - # THEN the debug mode is enabled - assert app._debug - - -def test_debug_json_formatting(): - # GIVEN debug is True - app = ApiGatewayResolver(debug=True) - response = {"message": "Foo"} - - @app.get("/foo") - def foo(): - return response - - # WHEN calling the handler - result = app({"path": "/foo", "httpMethod": "GET"}, None) - - # THEN return a pretty print json in the body - assert result["body"] == json.dumps(response, indent=4) - - def test_service_error_responses(): # SCENARIO handling different kind of service errors being raised app = ApiGatewayResolver(cors=CORSConfig()) @@ -651,3 +586,85 @@ def service_error(): assert "Access-Control-Allow-Origin" in result["headers"] expected = {"statusCode": 502, "message": "Something went wrong!"} assert result["body"] == json_dump(expected) + + +def test_debug_unhandled_exceptions_debug_on(): + # GIVEN debug is enabled + # AND an unhandled exception is raised + app = ApiGatewayResolver(debug=True) + assert app._debug + + @app.get("/raises-error") + def raises_error(): + raise RuntimeError("Foo") + + # WHEN calling the handler + result = app({"path": "/raises-error", "httpMethod": "GET"}, None) + + # THEN return a 500 + # AND Content-Type is set to text/plain + # AND include the exception traceback in the response + assert result["statusCode"] == 500 + assert "Traceback (most recent call last)" in result["body"] + headers = result["headers"] + assert headers["Content-Type"] == content_types.TEXT_PLAIN + + +def test_debug_unhandled_exceptions_debug_off(): + # GIVEN debug is disabled + # AND an unhandled exception is raised + app = ApiGatewayResolver(debug=False) + assert not app._debug + + @app.get("/raises-error") + def raises_error(): + raise RuntimeError("Foo") + + # WHEN calling the handler + # THEN raise the original exception + with pytest.raises(RuntimeError) as e: + app({"path": "/raises-error", "httpMethod": "GET"}, None) + + # AND include the original error + assert e.value.args == ("Foo",) + + +def test_debug_mode_environment_variable(monkeypatch): + # GIVEN a debug mode environment variable is set + monkeypatch.setenv(constants.EVENT_HANDLER_DEBUG_ENV, "true") + app = ApiGatewayResolver() + + # WHEN calling app._debug + # THEN the debug mode is enabled + assert app._debug + + +def test_debug_json_formatting(): + # GIVEN debug is True + app = ApiGatewayResolver(debug=True) + response = {"message": "Foo"} + + @app.get("/foo") + def foo(): + return response + + # WHEN calling the handler + result = app({"path": "/foo", "httpMethod": "GET"}, None) + + # THEN return a pretty print json in the body + assert result["body"] == json.dumps(response, indent=4) + + +def test_debug_print_event(monkeypatch): + # GIVE debug is True + app = ApiGatewayResolver(debug=True) + mocked_print = MagicMock() + monkeypatch.setattr(builtins, "print", mocked_print) + + # WHEN calling resolve + event = {"path": "/foo", "httpMethod": "GET"} + app(event, None) + + # THEN print the event + # NOTE: other calls might have happened outside of this mock + mocked_print.assert_any_call(json.dumps(event, indent=4)) diff --git a/tests/functional/py.typed b/tests/functional/py.typed new file mode 100644 index 00000000000..e69de29bb2d From efa025f067838a1a65f90aa2efb6b74f01d93713 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Fri, 9 Jul 2021 01:16:22 -0700 Subject: [PATCH 08/10] refactor(constants): some cleanup --- aws_lambda_powertools/shared/constants.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/aws_lambda_powertools/shared/constants.py b/aws_lambda_powertools/shared/constants.py index 05eac6cd8c4..8388eded654 100644 --- a/aws_lambda_powertools/shared/constants.py +++ b/aws_lambda_powertools/shared/constants.py @@ -10,13 +10,12 @@ METRICS_NAMESPACE_ENV: str = "POWERTOOLS_METRICS_NAMESPACE" +EVENT_HANDLER_DEBUG_ENV: str = "POWERTOOLS_EVENT_HANDLER_DEBUG" + SAM_LOCAL_ENV: str = "AWS_SAM_LOCAL" CHALICE_LOCAL_ENV: str = "AWS_CHALICE_CLI_MODE" SERVICE_NAME_ENV: str = "POWERTOOLS_SERVICE_NAME" XRAY_TRACE_ID_ENV: str = "_X_AMZN_TRACE_ID" - -XRAY_SDK_MODULE = "aws_xray_sdk" -XRAY_SDK_CORE_MODULE = "aws_xray_sdk.core" - -EVENT_HANDLER_DEBUG_ENV: str = "POWERTOOLS_EVENT_HANDLER_DEBUG" +XRAY_SDK_MODULE: str = "aws_xray_sdk" +XRAY_SDK_CORE_MODULE: str = "aws_xray_sdk.core" From e7df7d523030baaa02ad10d7e5cd18359e4a5858 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Fri, 9 Jul 2021 01:24:11 -0700 Subject: [PATCH 09/10] tests: use capsys Co-authored-by: Heitor Lessa --- tests/functional/event_handler/test_api_gateway.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 3d77aa7521c..b2ecf14f441 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -655,16 +655,14 @@ def foo(): assert result["body"] == json.dumps(response, indent=4) -def test_debug_print_event(monkeypatch): +def test_debug_print_event(capsys): # GIVE debug is True app = ApiGatewayResolver(debug=True) - mocked_print = MagicMock() - monkeypatch.setattr(builtins, "print", mocked_print) # WHEN calling resolve event = {"path": "/foo", "httpMethod": "GET"} app(event, None) # THEN print the event - # NOTE: other calls might have happened outside of this mock - mocked_print.assert_any_call(json.dumps(event, indent=4)) + out, err = capsys.readouterr() + assert json.loads(out) == event From f1e6faa5345332a765a06e6b46badb11f7f62002 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Fri, 9 Jul 2021 01:27:49 -0700 Subject: [PATCH 10/10] chore: remove unused imports --- tests/functional/event_handler/test_api_gateway.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index b2ecf14f441..b39dccc6084 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -1,11 +1,9 @@ import base64 -import builtins import json import zlib from decimal import Decimal from pathlib import Path from typing import Dict -from unittest.mock import MagicMock import pytest