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 = Service(list_of_routes, lambda_runner) service.create() self.assertEquals(service._dict_of_routes, { '/:GET': api_gateway_route_1, '/:POST': api_gateway_route_2 })
def start(self): """ Creates and starts the local API Gateway service. This method will block until the service is stopped manually using an interrupt. After the service is started, callers can make HTTP requests to the endpoint to invoke the Lambda function and receive a response. NOTE: This is a blocking call that will not return until the thread is interrupted with SIGINT/SIGTERM """ routing_list = self._make_routing_list(self.api_provider) if not routing_list: raise NoApisDefined("No APIs available in SAM template") static_dir_path = self._make_static_dir_path(self.cwd, self.static_dir) # We care about passing only stderr to the Service and not stdout because stdout from Docker container # contains the response to the API which is sent out as HTTP response. Only stderr needs to be printed # to the console or a log file. stderr from Docker container contains runtime logs and output of print # statements from the Lambda function service = Service(routing_list=routing_list, lambda_runner=self.lambda_runner, static_dir=static_dir_path, port=self.port, host=self.host, stderr=self.stderr_stream) service.create() # Print out the list of routes that will be mounted self._print_routes(self.api_provider, self.host, self.port) LOG.info( "You can now browse to the above endpoints to invoke your functions. " "You do not need to restart/reload SAM CLI while working on your functions " "changes will be reflected instantly/automatically. You only need to restart " "SAM CLI if you update your AWS SAM template") service.run()
class TestApiGatewayService(TestCase): 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.stderr = Mock() self.service = Service(self.list_of_routes, self.lambda_runner, stderr=self.stderr) def test_request_must_invoke_lambda(self): make_response_mock = Mock() self.service._service_response = make_response_mock self.service._get_current_route = Mock() self.service._construct_event = Mock() parse_output_mock = Mock() parse_output_mock.return_value = ("status_code", "headers", "body") self.service._parse_lambda_output = parse_output_mock service_response_mock = Mock() service_response_mock.return_value = make_response_mock self.service._service_response = service_response_mock result = self.service._request_handler() self.assertEquals(result, make_response_mock) self.lambda_runner.invoke.assert_called_with(ANY, ANY, stdout=ANY, stderr=self.stderr) def test_request_handler_returns_process_stdout_when_making_response(self): make_response_mock = Mock() self.service._service_response = make_response_mock self.service._get_current_route = Mock() self.service._construct_event = Mock() parse_output_mock = Mock() parse_output_mock.return_value = ("status_code", "headers", "body") self.service._parse_lambda_output = parse_output_mock lambda_logs = "logs" lambda_response = "response" self.service._get_lambda_output = Mock() self.service._get_lambda_output.return_value = lambda_response, lambda_logs service_response_mock = Mock() service_response_mock.return_value = make_response_mock self.service._service_response = service_response_mock result = self.service._request_handler() self.assertEquals(result, make_response_mock) self.service._get_lambda_output.assert_called_with(ANY) # Make sure the parse method is called only on the returned response and not on the raw data from stdout parse_output_mock.assert_called_with(lambda_response, ANY, ANY) # Make sure the logs are written to stderr self.stderr.write.assert_called_with(lambda_logs) def test_request_handler_returns_make_response(self): make_response_mock = Mock() self.service._service_response = make_response_mock self.service._get_current_route = Mock() self.service._construct_event = Mock() parse_output_mock = Mock() parse_output_mock.return_value = ("status_code", "headers", "body") self.service._parse_lambda_output = parse_output_mock service_response_mock = Mock() service_response_mock.return_value = make_response_mock self.service._service_response = service_response_mock result = self.service._request_handler() self.assertEquals(result, make_response_mock) def test_runtime_error_raised_when_app_not_created(self): with self.assertRaises(RuntimeError): self.service.run() def test_run_starts_service_multithreaded(self): self.service._app = Mock() app_run_mock = Mock() self.service._app.run = app_run_mock self.lambda_runner.is_debugging.return_value = False # multithreaded self.service.run() app_run_mock.assert_called_once_with(threaded=True, host='127.0.0.1', port=3000) def test_run_starts_service_singlethreaded(self): self.service._app = Mock() app_run_mock = Mock() self.service._app.run = app_run_mock self.lambda_runner.is_debugging.return_value = True # single threaded self.service.run() app_run_mock.assert_called_once_with(threaded=False, host='127.0.0.1', port=3000) 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 = Service(list_of_routes, lambda_runner) service.create() self.assertEquals(service._dict_of_routes, { '/:GET': api_gateway_route_1, '/:POST': api_gateway_route_2 }) @patch('samcli.local.apigw.service.Flask') def test_create_creates_flask_app_with_url_rules(self, flask): app_mock = Mock() flask.return_value = app_mock self.service._construct_error_handling = Mock() self.service.create() app_mock.add_url_rule.assert_called_once_with( '/', endpoint='/', view_func=self.service._request_handler, methods=['GET'], provide_automatic_options=False) def test_initalize_creates_default_values(self): self.assertEquals(self.service.port, 3000) self.assertEquals(self.service.host, '127.0.0.1') self.assertEquals(self.service.routing_list, self.list_of_routes) self.assertIsNone(self.service.static_dir) self.assertEquals(self.service.lambda_runner, self.lambda_runner) def test_initalize_with_values(self): lambda_runner = Mock() local_service = Service([], lambda_runner, static_dir='dir/static', port=5000, host='129.0.0.0') self.assertEquals(local_service.port, 5000) self.assertEquals(local_service.host, '129.0.0.0') self.assertEquals(local_service.routing_list, []) self.assertEquals(local_service.static_dir, 'dir/static') self.assertEquals(local_service.lambda_runner, lambda_runner) @patch('samcli.local.apigw.service.ServiceErrorResponses') def test_request_handles_error_when_invoke_cant_find_function( self, service_error_responses_patch): not_found_response_mock = Mock() self.service._construct_event = Mock() self.service._get_current_route = Mock() service_error_responses_patch.lambda_not_found_response.return_value = not_found_response_mock self.lambda_runner.invoke.side_effect = FunctionNotFound() response = self.service._request_handler() self.assertEquals(response, not_found_response_mock) def test_request_throws_when_invoke_fails(self): self.lambda_runner.invoke.side_effect = Exception() self.service._construct_event = Mock() self.service._get_current_route = Mock() with self.assertRaises(Exception): self.service._request_handler() @patch('samcli.local.apigw.service.ServiceErrorResponses') def test_request_handler_errors_when_parse_lambda_output_raises_keyerror( self, service_error_responses_patch): parse_output_mock = Mock() parse_output_mock.side_effect = KeyError() self.service._parse_lambda_output = parse_output_mock failure_response_mock = Mock() service_error_responses_patch.lambda_failure_response.return_value = failure_response_mock self.service._construct_event = Mock() self.service._get_current_route = Mock() result = self.service._request_handler() self.assertEquals(result, failure_response_mock) @patch('samcli.local.apigw.service.ServiceErrorResponses') def test_request_handler_errors_when_get_current_route_fails( self, service_error_responses_patch): get_current_route = Mock() get_current_route.side_effect = KeyError() self.service._get_current_route = get_current_route with self.assertRaises(KeyError): self.service._request_handler() @patch('samcli.local.apigw.service.ServiceErrorResponses') def test_request_handler_errors_when_unable_to_read_binary_data( self, service_error_responses_patch): _construct_event = Mock() _construct_event.side_effect = UnicodeDecodeError( "utf8", b"obj", 1, 2, "reason") self.service._get_current_route = Mock() self.service._construct_event = _construct_event failure_mock = Mock() service_error_responses_patch.lambda_failure_response.return_value = failure_mock result = self.service._request_handler() self.assertEquals(result, failure_mock) @patch('samcli.local.apigw.service.request') def test_get_current_route(self, request_patch): request_mock = Mock() request_mock.endpoint = "path" request_mock.method = "method" request_patch.return_value = request_mock route_key_method_mock = Mock() route_key_method_mock.return_value = "method:path" self.service._route_key = route_key_method_mock self.service._dict_of_routes = {"method:path": "function"} self.assertEquals(self.service._get_current_route(request_mock), "function") @patch('samcli.local.apigw.service.request') def test_get_current_route_keyerror(self, request_patch): """ When the a HTTP request for given method+path combination is allowed by Flask but not in the list of routes, something is messed up. Flask should be configured only from the list of routes. """ request_mock = Mock() request_mock.endpoint = "path" request_mock.method = "method" request_patch.return_value = request_mock route_key_method_mock = Mock() route_key_method_mock.return_value = "method:path" self.service._route_key = route_key_method_mock self.service._dict_of_routes = {"a": "b"} with self.assertRaises(KeyError): self.service._get_current_route(request_mock) @parameterized.expand([ param("with both logs and response", b'this\nis\nlog\ndata\n{"a": "b"}', b'this\nis\nlog\ndata', b'{"a": "b"}'), param("with response as string", b"logs\nresponse", b"logs", b"response"), param("with response only", b'{"a": "b"}', None, b'{"a": "b"}'), param("with response only as string", b'this is the response line', None, b'this is the response line'), param("with whitespaces", b'log\ndata\n{"a": "b"} \n\n\n', b"log\ndata", b'{"a": "b"}'), param("with empty data", b'', None, b''), param("with just new lines", b'\n\n', None, b''), param( "with no data but with whitespaces", b'\n \n \n', b'\n ', b'' # Log data with whitespaces will be in the output unchanged ) ]) def test_get_lambda_output_extracts_response(self, test_case_name, stdout_data, expected_logs, expected_response): stdout = Mock() stdout.getvalue.return_value = stdout_data response, logs = self.service._get_lambda_output(stdout) self.assertEquals(logs, expected_logs) self.assertEquals(response, expected_response)