Skip to content
Merged
2 changes: 1 addition & 1 deletion samcli/lib/providers/api_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def dedupe_function_routes(routes: List[Route]) -> List[Route]:
grouped_routes: Dict[str, Route] = {}

for route in routes:
key = "{}-{}-{}".format(route.stack_path, route.function_name, route.path)
key = "{}-{}-{}-{}".format(route.stack_path, route.function_name, route.path, route.operation_name or "")
config = grouped_routes.get(key, None)
methods = route.methods
if config:
Expand Down
9 changes: 8 additions & 1 deletion samcli/local/apigw/local_apigw_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,13 +324,20 @@ def _request_handler(self, **kwargs):
route_key,
)
else:

# The OperationName is only sent to the Lambda Function from API Gateway V1(Rest API).
# For Http Apis (v2), API Gateway never sends the OperationName.
if route.event_type == Route.API:
operation_name = route.operation_name
else:
operation_name = None
event = self._construct_v_1_0_event(
request,
self.port,
self.api.binary_media_types,
self.api.stage_name,
self.api.stage_variables,
route.operation_name,
operation_name,
)
except UnicodeDecodeError:
return ServiceErrorResponses.lambda_failure_response()
Expand Down
55 changes: 55 additions & 0 deletions tests/integration/local/start_api/test_start_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,30 @@ def test_patch_call_with_path_setup_with_any_swagger(self):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"hello": "world"})

@pytest.mark.flaky(reruns=3)
@pytest.mark.timeout(timeout=600, method="thread")
def test_http_api_payload_v1_should_not_have_operation_id(self):
response = requests.get(self.url + "/httpapi-operation-id-v1", timeout=300)
self.assertEqual(response.status_code, 200)

response_data = response.json()
self.assertEqual(response_data.get("version", {}), "1.0")
# operationName or operationId shouldn't be processed by Httpapi swaggers
self.assertIsNone(response_data.get("requestContext", {}).get("operationName"))
self.assertIsNone(response_data.get("requestContext", {}).get("operationId"))

@pytest.mark.flaky(reruns=3)
@pytest.mark.timeout(timeout=600, method="thread")
def test_http_api_payload_v2_should_not_have_operation_id(self):
response = requests.get(self.url + "/httpapi-operation-id-v2", timeout=300)
self.assertEqual(response.status_code, 200)

response_data = response.json()
self.assertEqual(response_data.get("version", {}), "2.0")
# operationName or operationId shouldn't be processed by Httpapi swaggers
self.assertIsNone(response_data.get("requestContext", {}).get("operationName"))
self.assertIsNone(response_data.get("requestContext", {}).get("operationId"))


class TestStartApiWithSwaggerRestApis(StartApiIntegBaseClass):
template_path = "/testdata/start_api/swagger-rest-api-template.yaml"
Expand Down Expand Up @@ -792,6 +816,16 @@ def test_binary_response(self):
self.assertEqual(response.headers.get("Content-Type"), "image/gif")
self.assertEqual(response.content, expected)

@pytest.mark.flaky(reruns=3)
@pytest.mark.timeout(timeout=600, method="thread")
def test_rest_api_operation_id(self):
"""
Binary data is returned correctly
"""
response = requests.get(self.url + "/printeventwithoperationidfunction", timeout=300)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json().get("requestContext", {}).get("operationName"), "MyOperationName")


class TestServiceResponses(StartApiIntegBaseClass):
"""
Expand Down Expand Up @@ -1691,6 +1725,17 @@ def test_http_api_is_reachable(self):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"hello": "world"})

@pytest.mark.flaky(reruns=3)
@pytest.mark.timeout(timeout=600, method="thread")
def test_http_api_with_operation_name_is_reachable(self):
response = requests.get(self.url + "/http-api-with-operation-name", timeout=300)

self.assertEqual(response.status_code, 200)
response_data = response.json()
# operationName or operationId shouldn't be processed by Httpapi
self.assertIsNone(response_data.get("requestContext", {}).get("operationName"))
self.assertIsNone(response_data.get("requestContext", {}).get("operationId"))

@pytest.mark.flaky(reruns=3)
@pytest.mark.timeout(timeout=600, method="thread")
def test_rest_api_is_reachable(self):
Expand All @@ -1699,6 +1744,13 @@ def test_rest_api_is_reachable(self):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"hello": "world"})

@pytest.mark.flaky(reruns=3)
@pytest.mark.timeout(timeout=600, method="thread")
def test_rest_api_with_operation_name_is_reachable(self):
response = requests.get(self.url + "/rest-api-with-operation-name", timeout=300)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"operation_name": "MyOperationName"})


class TestCFNTemplateHttpApiWithSwaggerBody(StartApiIntegBaseClass):
template_path = "/testdata/start_api/cfn-http-api-with-swagger-body.yaml"
Expand All @@ -1716,6 +1768,9 @@ def test_swagger_got_parsed_and_api_is_reachable_and_payload_version_is_2(self):
self.assertEqual(response_data.get("version", {}), "2.0")
self.assertIsNone(response_data.get("multiValueHeaders"))
self.assertIsNotNone(response_data.get("cookies"))
# operationName or operationId shouldn't be processed by Httpapi swaggers
self.assertIsNone(response_data.get("requestContext", {}).get("operationName"))
self.assertIsNone(response_data.get("requestContext", {}).get("operationId"))


class TestWarmContainersBaseClass(StartApiIntegBaseClass):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ Resources:
Value: SAM
Timeout: 3
Type: AWS::Lambda::Function
HelloWorldFunctionWithOperationName:
Properties:
Handler: main.operation_name_handler
Code: '.'
Role:
Fn::GetAtt:
- HelloWorldFunctionRole
- Arn
Runtime: python3.6
Tags:
- Key: lambda:createdBy
Value: SAM
Timeout: 3
Type: AWS::Lambda::Function
HelloWorldFunctionRole:
Properties:
AssumeRolePolicyDocument:
Expand Down Expand Up @@ -81,6 +95,15 @@ Resources:
IntegrationMethod: POST
IntegrationUri:
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorldFunction.Arn}/invocations
MyIntegrationWithOperationName:
Type: 'AWS::ApiGatewayV2::Integration'
Properties:
ApiId: !Ref HTTPAPIGateway
PayloadFormatVersion: "1.0"
IntegrationType: AWS_PROXY
IntegrationMethod: POST
IntegrationUri:
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorldFunctionWithOperationName.Arn}/invocations
MyRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
Expand All @@ -90,6 +113,16 @@ Resources:
- /
- - integrations
- !Ref MyIntegration
MyRouteWithOperationName:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref HTTPAPIGateway
OperationName: 'MyOperationName'
RouteKey: 'GET /http-api-with-operation-name'
Target: !Join
- /
- - integrations
- !Ref MyIntegrationWithOperationName
RestApiGateway:
Type: AWS::ApiGateway::RestApi
Properties:
Expand Down Expand Up @@ -141,4 +174,39 @@ Resources:
- Fn::GetAtt:
- HelloWorldFunction
- Arn
- /invocations
RestApiGatewayWithOperationNameResource:
Type: AWS::ApiGateway::Resource
Properties:
ParentId:
Fn::GetAtt:
- RestApiGateway
- RootResourceId
PathPart: "rest-api-with-operation-name"
RestApiId:
Ref: RestApiGateway
RestApiGatewayWithOperationNameMethod:
Type: AWS::ApiGateway::Method
Properties:
HttpMethod: GET
OperationName: 'MyOperationName'
ResourceId:
Ref: RestApiGatewayWithOperationNameResource
RestApiId:
Ref: RestApiGateway
AuthorizationType: NONE
Integration:
IntegrationHttpMethod: POST
Type: AWS_PROXY
Uri:
Fn::Join:
- ""
- - "arn:"
- Ref: AWS::Partition
- ":apigateway:"
- Ref: AWS::Region
- :lambda:path/2015-03-31/functions/
- Fn::GetAtt:
- HelloWorldFunctionWithOperationName
- Arn
- /invocations
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Resources:
/echoeventbody:
get:
responses: {}
operationId: 'postOperationIdShouldNotBeInHttpApi'
x-amazon-apigateway-integration:
httpMethod: POST
payloadFormatVersion: '2.0'
Expand Down
3 changes: 3 additions & 0 deletions tests/integration/testdata/start_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
def handler(event, context):
return {"statusCode": 200, "body": json.dumps({"hello": "world"})}

def operation_name_handler(event, context):
return {"statusCode": 200, "body": json.dumps({"operation_name": event["requestContext"].get("operationName", "")})}


def echo_event_handler(event, context):
return {"statusCode": 200, "body": json.dumps(event)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ Resources:
type: aws_proxy
uri:
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyNonServerlessLambdaFunction.Arn}/invocations
"/printeventwithoperationidfunction":
get:
operationId: 'MyOperationName'
x-amazon-apigateway-integration:
httpMethod: POST
type: aws_proxy
uri:
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${EchoEventBodyFunction.Arn}/invocations
swagger: '2.0'
x-amazon-apigateway-binary-media-types:
- image/gif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,26 @@ Resources:
type: aws_proxy
uri:
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${EchoBase64EventBodyFunction.Arn}/invocations
"/httpapi-operation-id-v1":
get:
responses: {}
operationId: 'OperationNameShouldNotAppear'
x-amazon-apigateway-integration:
httpMethod: GET
type: aws_proxy
payloadFormatVersion: '1.0'
uri:
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${EchoEventHandlerHttpApiFunction.Arn}/invocations
"/httpapi-operation-id-v2":
get:
responses: {}
operationId: 'OperationNameShouldNotAppear'
x-amazon-apigateway-integration:
httpMethod: GET
type: aws_proxy
payloadFormatVersion: '2.0'
uri:
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${EchoEventHandlerHttpApiFunction.Arn}/invocations

MyHttpApiLambdaFunction:
Type: AWS::Serverless::Function
Expand Down
12 changes: 9 additions & 3 deletions tests/unit/local/apigw/test_local_apigw_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@
class TestApiGatewayService(TestCase):
def setUp(self):
self.function_name = Mock()
self.api_gateway_route = Route(methods=["GET"], function_name=self.function_name, path="/")
self.api_gateway_route = Route(
methods=["GET"],
function_name=self.function_name,
path="/",
operation_name="getRestApi",
)
self.http_gateway_route = Route(
methods=["GET"], function_name=self.function_name, path="/", event_type=Route.HTTP
)
Expand Down Expand Up @@ -77,6 +82,7 @@ def test_api_request_must_invoke_lambda(self, request_mock):

self.api_service.service_response = make_response_mock
self.api_service._get_current_route = MagicMock()
self.api_service._get_current_route.return_value = self.api_gateway_route
self.api_service._get_current_route.methods = []
self.api_service._get_current_route.return_value.payload_format_version = "2.0"
self.api_service._construct_v_1_0_event = Mock()
Expand All @@ -95,7 +101,7 @@ def test_api_request_must_invoke_lambda(self, request_mock):

self.assertEqual(result, make_response_mock)
self.lambda_runner.invoke.assert_called_with(ANY, ANY, stdout=ANY, stderr=self.stderr)
self.api_service._construct_v_1_0_event.assert_called_with(ANY, ANY, ANY, ANY, ANY, ANY)
self.api_service._construct_v_1_0_event.assert_called_with(ANY, ANY, ANY, ANY, ANY, "getRestApi")

@patch.object(LocalApigwService, "get_request_methods_endpoints")
def test_http_request_must_invoke_lambda(self, request_mock):
Expand Down Expand Up @@ -151,7 +157,7 @@ def test_http_v1_payload_request_must_invoke_lambda(self, request_mock):

self.assertEqual(result, make_response_mock)
self.lambda_runner.invoke.assert_called_with(ANY, ANY, stdout=ANY, stderr=self.stderr)
self.http_service._construct_v_1_0_event.assert_called_with(ANY, ANY, ANY, ANY, ANY, "getV1")
self.http_service._construct_v_1_0_event.assert_called_with(ANY, ANY, ANY, ANY, ANY, None)

@patch.object(LocalApigwService, "get_request_methods_endpoints")
def test_http_v2_payload_request_must_invoke_lambda(self, request_mock):
Expand Down