def test_create_creates_dict_of_routes(self): function_name_1 = Mock() function_name_2 = Mock() api_gateway_route_1 = Route(methods=["GET"], function_name=function_name_1, path="/") api_gateway_route_2 = Route(methods=["POST"], function_name=function_name_2, path="/") list_of_routes = [api_gateway_route_1, api_gateway_route_2] lambda_runner = Mock() api = Api(routes=list_of_routes) service = LocalApigwService(api, lambda_runner) service.create() self.assertEqual(service._dict_of_routes, {"/:GET": api_gateway_route_1, "/:POST": api_gateway_route_2})
def test_resource_with_method_correct_routes(self): template = { "Resources": { "TestApi": {"Type": "AWS::ApiGateway::Resource", "Properties": {"StageName": "Prod"}}, "BetaApiResource": { "Type": "AWS::ApiGateway::Resource", "Properties": {"PathPart": "beta", "ResourceId": "TestApi"}, }, "BetaAlphaApiMethod": { "Type": "AWS::ApiGateway::Method", "Properties": {"HttpMethod": "ANY", "RestApiId": "TestApi", "ResourceId": "BetaApiResource"}, }, } } provider = ApiProvider(template) assertCountEqual( self, provider.routes, [ Route( path="/beta", methods=["POST", "GET", "DELETE", "HEAD", "OPTIONS", "PATCH", "PUT"], function_name=None, ) ], )
def test_provider_parse_stage_name(self): template = { "Resources": { "Stage": {"Type": "AWS::ApiGateway::Stage", "Properties": {"StageName": "dev", "RestApiId": "TestApi"}}, "TestApi": { "Type": "AWS::ApiGateway::RestApi", "Properties": { "Body": { "paths": { "/path": { "get": { "x-amazon-apigateway-integration": { "httpMethod": "POST", "type": "aws_proxy", "uri": { "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" "/functions/${NoApiEventFunction.Arn}/invocations" }, "responses": {}, } } } } } }, }, } } provider = ApiProvider(template) route1 = Route(path="/path", methods=["GET"], function_name="NoApiEventFunction") self.assertIn(route1, provider.routes) self.assertEqual(provider.api.stage_name, "dev") self.assertEqual(provider.api.stage_variables, None)
def _convert_event_route(lambda_logical_id, event_properties): """ Converts a AWS::Serverless::Function's Event Property to an Route configuration usable by the provider. :param str lambda_logical_id: Logical Id of the AWS::Serverless::Function :param dict event_properties: Dictionary of the Event's Property :return tuple: tuple of route resource name and route """ path = event_properties.get(SamApiProvider._EVENT_PATH) method = event_properties.get(SamApiProvider._EVENT_METHOD) # An API Event, can have RestApiId property which designates the resource that owns this API. If omitted, # the API is owned by Implicit API resource. This could either be a direct resource logical ID or a # "Ref" of the logicalID api_resource_id = event_properties.get( "RestApiId", SamApiProvider.IMPLICIT_API_RESOURCE_ID) if isinstance(api_resource_id, dict) and "Ref" in api_resource_id: api_resource_id = api_resource_id["Ref"] # This is still a dictionary. Something wrong with the template if isinstance(api_resource_id, dict): LOG.debug("Invalid RestApiId property of event %s", event_properties) raise InvalidSamDocumentException( "RestApiId property of resource with logicalId '{}' is invalid. " "It should either be a LogicalId string or a Ref of a Logical Id string" .format(lambda_logical_id)) return api_resource_id, Route(path=path, methods=[method], function_name=lambda_logical_id)
def dedupe_function_routes(routes: List[Route]) -> List[Route]: """ Remove duplicate routes that have the same function_name and method route: list(Route) List of Routes Return ------- A list of routes without duplicate routes with the same stack_path, function_name and method """ grouped_routes: Dict[str, Route] = {} for route in routes: key = "{}-{}-{}".format(route.stack_path, route.function_name, route.path) config = grouped_routes.get(key, None) methods = route.methods if config: methods += config.methods sorted_methods = sorted(methods) grouped_routes[key] = Route( function_name=route.function_name, path=route.path, methods=sorted_methods, event_type=route.event_type, payload_format_version=route.payload_format_version, stack_path=route.stack_path, ) return list(grouped_routes.values())
def test_with_one_path_method(self): function_name = "myfunction" swagger = { "paths": { "/path1": { "get": { "x-amazon-apigateway-integration": { "type": "aws_proxy", "uri": "someuri" } } } } } parser = SwaggerParser(swagger) parser._get_integration_function_name = Mock() parser._get_integration_function_name.return_value = function_name expected = [ Route(path="/path1", methods=["get"], function_name=function_name) ] result = parser.get_routes() self.assertEqual(expected, result) parser._get_integration_function_name.assert_called_with({ "x-amazon-apigateway-integration": { "type": "aws_proxy", "uri": "someuri" } })
def _extract_cfn_gateway_v2_route(self, stack_path: str, resources, logical_id, route_resource, collector): """ Extract APIs from AWS::ApiGatewayV2::Route, and link it with the integration resource to get the lambda function. Parameters ---------- stack_path : str Path of the stack the resource is located resources: dict All Resource definition, including its properties logical_id : str Logical ID of the resource route_resource : dict Resource definition, including its properties collector : ApiCollector Instance of the API collector that where we will save the API information """ properties = route_resource.get("Properties", {}) api_id = properties.get("ApiId") route_key = properties.get("RouteKey") integration_target = properties.get("Target") if integration_target: function_name, payload_format_version = self._get_route_function_name( resources, integration_target) else: LOG.debug( "Skipping The AWS::ApiGatewayV2::Route '%s', as it does not contain an integration for a Lambda " "Function", logical_id, ) return method, path = self._parse_route_key(route_key) if not route_key or not method or not path: LOG.debug( "The AWS::ApiGatewayV2::Route '%s' does not have a correct route key '%s'", logical_id, route_key) raise InvalidSamTemplateException( "The AWS::ApiGatewayV2::Route {} does not have a correct route key {}" .format(logical_id, route_key)) routes = Route( methods=[method], path=path, function_name=function_name, event_type=Route.HTTP, payload_format_version=payload_format_version, stack_path=stack_path, ) collector.add_routes(api_id, [routes])
def test_create_creates_dict_of_routes(self): function_name_1 = Mock() function_name_2 = Mock() api_gateway_route_1 = Route(['GET'], function_name_1, '/') api_gateway_route_2 = Route(['POST'], function_name_2, '/') list_of_routes = [api_gateway_route_1, api_gateway_route_2] lambda_runner = Mock() service = LocalApigwService(list_of_routes, lambda_runner) service.create() self.assertEquals(service._dict_of_routes, {'/:GET': api_gateway_route_1, '/:POST': api_gateway_route_2 })
def test_swagger_with_any_method(self): routes = [Route(path="/path", methods=["any"], function_name="SamFunc1")] expected_routes = [ Route( path="/path", methods=["GET", "DELETE", "PUT", "POST", "HEAD", "OPTIONS", "PATCH"], function_name="SamFunc1", ) ] template = { "Resources": {"Api1": {"Type": "AWS::ApiGateway::RestApi", "Properties": {"Body": make_swagger(routes)}}} } provider = ApiProvider(template) self.assertCountEqual(expected_routes, provider.routes)
def setUp(self): self.function_name = "name" self.stage_name = "Dev" self.stage_variables = {"test": "sample"} self.api_gateway = Route(['POST'], self.function_name, '/', stage_name=self.stage_name, stage_variables=self.stage_variables)
def test_with_combination_of_paths_methods(self): function_name = "myfunction" swagger = { "paths": { "/path1": { "get": { "x-amazon-apigateway-integration": { "type": "aws_proxy", "uri": "someuri" } }, "delete": { "x-amazon-apigateway-integration": { "type": "aws_proxy", "uri": "someuri" } } }, "/path2": { "post": { "x-amazon-apigateway-integration": { "type": "aws_proxy", "uri": "someuri" } } } } } parser = SwaggerParser(swagger) parser._get_integration_function_name = Mock() parser._get_integration_function_name.return_value = function_name expected = { Route(path="/path1", methods=["get"], function_name=function_name), Route(path="/path1", methods=["delete"], function_name=function_name), Route(path="/path2", methods=["post"], function_name=function_name), } result = parser.get_routes() self.assertEquals(expected, set(result))
def _extract_cfn_gateway_v2_api( self, stack_path: str, logical_id: str, api_resource: Dict, collector: ApiCollector, cwd: Optional[str] = None, ) -> None: """ Extract APIs from AWS::ApiGatewayV2::Api resource by reading and parsing Swagger documents. The result is added to the collector. If the Swagger documents is not available, it can add a catch-all route based on the target function. Parameters ---------- stack_path : str Path of the stack the resource is located logical_id : str Logical ID of the resource api_resource : dict Resource definition, including its properties collector : ApiCollector Instance of the API collector that where we will save the API information cwd : Optional[str] An optional string to override the current working directory """ properties = api_resource.get("Properties", {}) body = properties.get("Body") body_s3_location = properties.get("BodyS3Location") cors = self.extract_cors_http(properties.get("CorsConfiguration")) target = properties.get("Target") route_key = properties.get("RouteKey") protocol_type = properties.get("ProtocolType") if not body and not body_s3_location: LOG.debug( "Swagger document not found in Body and BodyS3Location for resource '%s'.", logical_id) if cors: collector.cors = cors if target and protocol_type == CfnApiProvider.HTTP_API_PROTOCOL_TYPE: method, path = self._parse_route_key(route_key) routes = Route( methods=[method], path=path, function_name=LambdaUri.get_function_name(target), event_type=Route.HTTP, stack_path=stack_path, ) collector.add_routes(logical_id, [routes]) return CfnBaseApiProvider.extract_swagger_route(stack_path, logical_id, body, body_s3_location, None, collector, cwd, Route.HTTP)
def _extract_cloud_formation_method(self, stack_path: str, resources, logical_id, method_resource, collector): """ Extract APIs from AWS::ApiGateway::Method and work backwards up the tree to resolve and find the true path. Parameters ---------- stack_path : str Path of the stack the resource is located resources: dict All Resource definition, including its properties logical_id : str Logical ID of the resource method_resource : dict Resource definition, including its properties collector : ApiCollector Instance of the API collector that where we will save the API information """ properties = method_resource.get("Properties", {}) resource_id = properties.get("ResourceId") rest_api_id = properties.get("RestApiId") method = properties.get("HttpMethod") resource_path = "/" if isinstance(resource_id, str): # If the resource_id resolves to a string resource = resources.get(resource_id) if resource: resource_path = self.resolve_resource_path( resources, resource, "") else: # This is the case that a raw ref resolves to a string { "Fn::GetAtt": ["MyRestApi", "RootResourceId"] } resource_path = resource_id integration = properties.get("Integration", {}) content_type = integration.get("ContentType") content_handling = integration.get("ContentHandling") if content_handling == CfnApiProvider.METHOD_BINARY_TYPE and content_type: collector.add_binary_media_types(logical_id, [content_type]) routes = Route( methods=[method], function_name=self._get_integration_function_name(integration), path=resource_path, stack_path=stack_path, ) collector.add_routes(rest_api_id, [routes])
def test_must_print_routes(self): host = "host" port = 123 apis = [ Route(path="/1", methods=["GET"], function_name="name1"), Route(path="/1", methods=["POST"], function_name="name1"), Route(path="/1", methods=["DELETE"], function_name="othername1"), Route(path="/2", methods=["GET2"], function_name="name2"), Route(path="/3", methods=["GET3"], function_name="name3"), ] apis = ApiCollector.dedupe_function_routes(apis) expected = {"Mounting name1 at http://host:123/1 [GET, POST]", "Mounting othername1 at http://host:123/1 [DELETE]", "Mounting name2 at http://host:123/2 [GET2]", "Mounting name3 at http://host:123/3 [GET3]"} actual = LocalApiService._print_routes(apis, host, port) self.assertEquals(expected, set(actual))
def test_must_return_routing_list_from_apis(self): api_provider = Mock() apis = [ Api(path="/1", method="GET1", function_name="name1", cors="CORS1"), Api(path="/2", method="GET2", function_name="name2", cors="CORS2"), Api(path="/3", method="GET3", function_name="name3", cors="CORS3"), ] expected = [ Route(path="/1", methods=["GET1"], function_name="name1"), Route(path="/2", methods=["GET2"], function_name="name2"), Route(path="/3", methods=["GET3"], function_name="name3") ] api_provider.get_all.return_value = apis result = LocalApiService._make_routing_list(api_provider) self.assertEquals(len(result), len(expected)) for index, r in enumerate(result): self.assertEquals(r.__dict__, expected[index].__dict__)
def test_payload_format_version(self): function_name = "myfunction" swagger = { "paths": { "/path1": { "get": { "x-amazon-apigateway-integration": { "type": "aws_proxy", "uri": "someuri", "payloadFormatVersion": "1.0", } } }, "/path2": { "get": { "x-amazon-apigateway-integration": { "type": "aws_proxy", "uri": "someuri", "payloadFormatVersion": "2.0", } } }, } } parser = SwaggerParser(swagger) parser._get_integration_function_name = Mock() parser._get_integration_function_name.return_value = function_name expected = [ Route(path="/path1", methods=["get"], function_name=function_name, payload_format_version="1.0"), Route(path="/path2", methods=["get"], function_name=function_name, payload_format_version="2.0"), ] result = parser.get_routes() self.assertEqual(expected, result)
def test_route_object_equals(self): route1 = Route(function_name="test", path="/test", methods=["POST", "GET"]) route2 = type('obj', (object, ), { 'function_name': 'test', "path": "/test", "methods": ["GET", "POST"] }) self.assertNotEqual(route1, route2)
def get_routes(self): """ Parses a swagger document and returns a list of APIs configured in the document. Swagger documents have the following structure { "/path1": { # path "get": { # method "x-amazon-apigateway-integration": { # integration "type": "aws_proxy", # URI contains the Lambda function ARN that needs to be parsed to get Function Name "uri": { "Fn::Sub": "arn:aws:apigateway:aws:lambda:path/2015-03-31/functions/${LambdaFunction.Arn}/..." } } }, "post": { }, }, "/path2": { ... } } Returns ------- list of list of samcli.commands.local.apigw.local_apigw_service.Route List of APIs that are configured in the Swagger document """ result = [] paths_dict = self.swagger.get("paths", {}) for full_path, path_config in paths_dict.items(): for method, method_config in path_config.items(): function_name = self._get_integration_function_name( method_config) if not function_name: LOG.debug( "Lambda function integration not found in Swagger document at path='%s' method='%s'", full_path, method, ) continue if method.lower() == self._ANY_METHOD_EXTENSION_KEY: # Convert to a more commonly used method notation method = self._ANY_METHOD route = Route(function_name, full_path, methods=[method]) result.append(route) return result
def test_with_binary_media_types(self): template = { "Resources": { "Api1": { "Type": "AWS::ApiGateway::RestApi", "Properties": {"Body": make_swagger(self.input_routes, binary_media_types=self.binary_types)}, } } } expected_binary_types = sorted(self.binary_types) expected_apis = [ Route(path="/path1", methods=["GET", "POST"], function_name="SamFunc1"), Route(path="/path2", methods=["PUT", "GET"], function_name="SamFunc1"), Route(path="/path3", methods=["DELETE"], function_name="SamFunc1"), ] provider = ApiProvider(template) self.assertCountEqual(expected_apis, provider.routes) self.assertCountEqual(provider.api.binary_media_types, expected_binary_types)
def setUp(self): self.function_name = Mock() self.api_gateway_route = Route(methods=["GET"], function_name=self.function_name, path="/") self.list_of_routes = [self.api_gateway_route] self.lambda_runner = Mock() self.lambda_runner.is_debugging.return_value = False self.stderr = Mock() self.api = Api(routes=self.list_of_routes) self.service = LocalApigwService(self.api, self.lambda_runner, port=3000, host="127.0.0.1", stderr=self.stderr)
def _convert_event_route(stack_path: str, lambda_logical_id, event_properties, event_type): """ Converts a AWS::Serverless::Function's Event Property to an Route configuration usable by the provider. :param str stack_path: Path of the stack the resource is located :param str lambda_logical_id: Logical Id of the AWS::Serverless::Function :param dict event_properties: Dictionary of the Event's Property :param event_type: The event type, 'Api' or 'HttpApi', see samcli/local/apigw/local_apigw_service.py:35 :return tuple: tuple of route resource name and route """ path = event_properties.get(SamApiProvider._EVENT_PATH) method = event_properties.get(SamApiProvider._EVENT_METHOD) # An RESTAPI (HTTPAPI) Event, can have RestApiId (ApiId) property which designates the resource that owns this # API. If omitted, the API is owned by Implicit API resource. This could either be a direct resource logical ID # or a "Ref" of the logicalID api_resource_id = None payload_format_version = None if event_type == SamApiProvider._EVENT_TYPE_API: api_resource_id = event_properties.get( "RestApiId", SamApiProvider.IMPLICIT_API_RESOURCE_ID) else: api_resource_id = event_properties.get( "ApiId", SamApiProvider.IMPLICIT_HTTP_API_RESOURCE_ID) payload_format_version = event_properties.get( "PayloadFormatVersion") if isinstance(api_resource_id, dict) and "Ref" in api_resource_id: api_resource_id = api_resource_id["Ref"] # This is still a dictionary. Something wrong with the template if isinstance(api_resource_id, dict): LOG.debug("Invalid RestApiId property of event %s", event_properties) raise InvalidSamDocumentException( "RestApiId property of resource with logicalId '{}' is invalid. " "It should either be a LogicalId string or a Ref of a Logical Id string" .format(lambda_logical_id)) return ( api_resource_id, Route( path=path, methods=[method], function_name=lambda_logical_id, event_type=event_type, payload_format_version=payload_format_version, stack_path=stack_path, ), )
def test_resolve_correct_multi_parent_resource_path(self): template = { "Resources": { "TestApi": {"Type": "AWS::ApiGateway::Resource", "Properties": {"StageName": "Prod"}}, "RootApiResource": { "Type": "AWS::ApiGateway::Resource", "Properties": {"PathPart": "root", "ResourceId": "TestApi"}, }, "V1ApiResource": { "Type": "AWS::ApiGateway::Resource", "Properties": {"PathPart": "v1", "ResourceId": "TestApi", "ParentId": "RootApiResource"}, }, "AlphaApiResource": { "Type": "AWS::ApiGateway::Resource", "Properties": {"PathPart": "alpha", "ResourceId": "TestApi", "ParentId": "V1ApiResource"}, }, "BetaApiResource": { "Type": "AWS::ApiGateway::Resource", "Properties": {"PathPart": "beta", "ResourceId": "TestApi", "ParentId": "V1ApiResource"}, }, "AlphaApiMethod": { "Type": "AWS::ApiGateway::Method", "Properties": {"HttpMethod": "GET", "RestApiId": "TestApi", "ResourceId": "AlphaApiResource"}, }, "BetaAlphaApiMethod": { "Type": "AWS::ApiGateway::Method", "Properties": {"HttpMethod": "POST", "RestApiId": "TestApi", "ResourceId": "BetaApiResource"}, }, } } provider = ApiProvider(template) assertCountEqual( self, provider.routes, [ Route(path="/root/v1/beta", methods=["POST"], function_name=None), Route(path="/root/v1/alpha", methods=["GET"], function_name=None), ], )
def test_with_binary_media_types_in_swagger_and_on_resource(self): input_routes = [Route(path="/path", methods=["OPTIONS"], function_name="SamFunc1")] extra_binary_types = ["text/html"] template = { "Resources": { "Api1": { "Type": "AWS::ApiGateway::RestApi", "Properties": { "BinaryMediaTypes": extra_binary_types, "Body": make_swagger(input_routes, binary_media_types=self.binary_types), }, } } } expected_binary_types = sorted(self.binary_types + extra_binary_types) expected_routes = [Route(path="/path", methods=["OPTIONS"], function_name="SamFunc1")] provider = ApiProvider(template) self.assertCountEqual(expected_routes, provider.routes) self.assertCountEqual(provider.api.binary_media_types, expected_binary_types)
def test_route_different_path_hash(self): route1 = Route(function_name="test", path="/test1", methods=["GET", "POST"]) route2 = Route(function_name="test", path="/test2", methods=["GET", "POST"]) self.assertNotEqual(route1.__hash__(), route2.__hash__())
def test_route_method_order_hash(self): route1 = Route(function_name="test", path="/test", methods=["POST", "GET"]) route2 = Route(function_name="test", path="/test", methods=["GET", "POST"]) self.assertEquals(route1.__hash__(), route2.__hash__())
def setUp(self): self.function_name = Mock() self.api_gateway_route = Route(['GET'], self.function_name, '/') self.list_of_routes = [self.api_gateway_route] self.lambda_runner = Mock() self.lambda_runner.is_debugging.return_value = False self.stderr = Mock() self.service = LocalApigwService(self.list_of_routes, self.lambda_runner, port=3000, host='127.0.0.1', stderr=self.stderr)
def test_basic_rest_api_resource_method(self): template = { "Resources": { "TestApi": {"Type": "AWS::ApiGateway::RestApi", "Properties": {"StageName": "Prod"}}, "ApiResource": {"Properties": {"PathPart": "{proxy+}", "RestApiId": "TestApi"}}, "ApiMethod": { "Type": "AWS::ApiGateway::Method", "Properties": {"HttpMethod": "POST", "RestApiId": "TestApi", "ResourceId": "ApiResource"}, }, } } provider = ApiProvider(template) self.assertEqual(provider.routes, [Route(function_name=None, path="/{proxy+}", methods=["POST"])])
def test_with_any_method(self): function_name = "myfunction" swagger = { "paths": { "/path1": { "x-amazon-apigateway-any-method": { "x-amazon-apigateway-integration": {"type": "aws_proxy", "uri": "someuri"} } } } } parser = SwaggerParser(self.stack_path, swagger) parser._get_integration_function_name = Mock() parser._get_integration_function_name.return_value = function_name expected = [Route(methods=["ANY"], path="/path1", function_name=function_name, stack_path=self.stack_path)] result = parser.get_routes() self.assertEqual(expected, result)
def _make_routing_list(api_provider): """ Returns a list of routes to configure the Local API Service based on the APIs configured in the template. Parameters ---------- api_provider : samcli.commands.local.lib.sam_api_provider.SamApiProvider Returns ------- list(samcli.local.apigw.service.Route) List of Routes to pass to the service """ routes = [] for api in api_provider.get_all(): route = Route(methods=[api.method], function_name=api.function_name, path=api.path, binary_types=api.binary_media_types) routes.append(route) return routes
def dedupe_function_routes(routes): """ Remove duplicate routes that have the same function_name and method route: list(Route) List of Routes Return ------- A list of routes without duplicate routes with the same function_name and method """ grouped_routes = {} for route in routes: key = "{}-{}".format(route.function_name, route.path) config = grouped_routes.get(key, None) methods = route.methods if config: methods += config.methods sorted_methods = sorted(methods) grouped_routes[key] = Route(function_name=route.function_name, path=route.path, methods=sorted_methods) return list(grouped_routes.values())