From d2afc9b12122d3f79f1feb1b475c0ad62f165f13 Mon Sep 17 00:00:00 2001 From: Jacob Fuss Date: Tue, 26 Jun 2018 12:11:15 -0700 Subject: [PATCH 1/6] Initial implementation to start up Local Lambda Service --- samcli/local/apigw/service.py | 2 +- samcli/local/lambda_service/__init__.py | 0 samcli/local/lambda_service/service.py | 103 ++++++++++++++++++ .../local/lambda_service/__init__.py | 0 .../lambda_service/test_lambda_service.py | 84 ++++++++++++++ tests/unit/local/lambda_service/__init__.py | 0 .../unit/local/lambda_service/test_service.py | 89 +++++++++++++++ 7 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 samcli/local/lambda_service/__init__.py create mode 100644 samcli/local/lambda_service/service.py create mode 100644 tests/functional/local/lambda_service/__init__.py create mode 100644 tests/functional/local/lambda_service/test_lambda_service.py create mode 100644 tests/unit/local/lambda_service/__init__.py create mode 100644 tests/unit/local/lambda_service/test_service.py diff --git a/samcli/local/apigw/service.py b/samcli/local/apigw/service.py index 6441708367..4e5eda750c 100644 --- a/samcli/local/apigw/service.py +++ b/samcli/local/apigw/service.py @@ -65,7 +65,7 @@ def __init__(self, routing_list, lambda_runner, static_dir=None, port=None, host :param int port: Optional. port for the service to start listening on Defaults to 3000 :param str host: Optional. host to start the service on - Defaults to '0.0.0.0' + Defaults to '127.0.0.1 :param io.BaseIO stderr: Optional stream where the stderr from Docker container should be written to """ self.routing_list = routing_list diff --git a/samcli/local/lambda_service/__init__.py b/samcli/local/lambda_service/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/samcli/local/lambda_service/service.py b/samcli/local/lambda_service/service.py new file mode 100644 index 0000000000..392c1ab0fd --- /dev/null +++ b/samcli/local/lambda_service/service.py @@ -0,0 +1,103 @@ +"""Local Lambda Service""" + +import logging +import os + +from flask import Flask, jsonify + +LOG = logging.getLogger(__name__) + + +class LocalLambdaService(object): + + _DEFAULT_PORT = 3001 + _DEFAULT_HOST = '127.0.0.1' + + def __init__(self, function_name_list, lambda_runner, port=None, host=None, stderr=None): + """ + Creates a Local Lambda Service + + Parameters + ---------- + function_name_list list of str + List of the Function Logical Ids + lambda_runner samcli.commands.local.lib.local_lambda.LocalLambdaRunner + The Lambda runner class capable of invoking the function + port int + Optional. port for the service to start listening on. Defaults to 3001 + host str + Optional. host to start the service on Defaults to '127.0.0.1' + stderr io.BaseIO + Optional stream where the stderr from Docker container should be written to + """ + self.function_name_list = function_name_list + self.lambda_runner = lambda_runner + self.port = port or self._DEFAULT_PORT + self.host = host or self._DEFAULT_HOST + self._app = None + self.stderr = stderr + + def create(self): + """ + Creates a Flask Application that can be started. + """ + self._app = Flask(__name__) + + for function_name in self.function_name_list: + path = '/2015-03-31/functions/{}/invocations'.format(function_name) + self._app.add_url_rule(path, + endpoint=path, + view_func=self._invoke_request_handler, + methods=['POST'], + provide_automatic_options=False) + + self._construct_error_handling() + + def run(self): + """ + This starts up the (threaded) Local Server. + Note: This is a **blocking call** + + Raises + ------ + RuntimeError + if the service was not created + """ + if not self._app: + raise RuntimeError("The application must be created before running") + + # Flask can operate as a single threaded server (which is default) and a multi-threaded server which is + # more for development. When the Lambda container is going to be debugged, then it does not make sense + # to turn on multi-threading because customers can realistically attach only one container at a time to + # the debugger. Keeping this single threaded also enables the Lambda Runner to handle Ctrl+C in order to + # kill the container gracefully (Ctrl+C can be handled only by the main thread) + multi_threaded = not self.lambda_runner.is_debugging() + + LOG.debug("Local Lambda Server starting up. Multi-threading = %s", multi_threaded) + + # This environ signifies we are running a main function for Flask. This is true, since we are using it within + # our cli and not on a production server. + os.environ['WERKZEUG_RUN_MAIN'] = 'true' + + self._app.run(threaded=multi_threaded, host=self.host, port=self.port) + + def _construct_error_handling(self): + """ + Updates the Flask app with Error Handlers for different Error Codes + + """ + pass + + def _invoke_request_handler(self): + """ + Request Handler for the Local Lambda Invoke path. This method is responsible for understanding the incoming + request and invoking the Local Lambda Function + + Returns + ------- + A Flask Response response object as if it was returned from Lambda + + """ + response = jsonify({"lambda": "mock response"}) + response.status_code = 200 + return response diff --git a/tests/functional/local/lambda_service/__init__.py b/tests/functional/local/lambda_service/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/functional/local/lambda_service/test_lambda_service.py b/tests/functional/local/lambda_service/test_lambda_service.py new file mode 100644 index 0000000000..6ab05c9fcb --- /dev/null +++ b/tests/functional/local/lambda_service/test_lambda_service.py @@ -0,0 +1,84 @@ +import threading +import shutil +import random +from mock import Mock +import time +from unittest import TestCase +import os + +import requests + +from samcli.local.lambda_service.service import LocalLambdaService +from tests.functional.function_code import nodejs_lambda, API_GATEWAY_ECHO_EVENT +from samcli.commands.local.lib import provider +from samcli.local.lambdafn.runtime import LambdaRuntime +from samcli.commands.local.lib.local_lambda import LocalLambdaRunner +from samcli.local.docker.manager import ContainerManager + + +class TestService_FlaskDefaultOptionsDisabled(TestCase): + @classmethod + def setUpClass(cls): + cls.code_abs_path = nodejs_lambda(API_GATEWAY_ECHO_EVENT) + + # Let's convert this absolute path to relative path. Let the parent be the CWD, and codeuri be the folder + cls.cwd = os.path.dirname(cls.code_abs_path) + cls.code_uri = os.path.relpath(cls.code_abs_path, cls.cwd) # Get relative path with respect to CWD + + cls.function_name = "HelloWorld" + + cls.function = provider.Function(name=cls.function_name, runtime="nodejs4.3", memory=256, timeout=5, + handler="index.handler", codeuri=cls.code_uri, environment=None, + rolearn=None) + + cls.base64_response_function = provider.Function(name=cls.function_name, runtime="nodejs4.3", memory=256, timeout=5, + handler="index.handler", codeuri=cls.code_uri, environment=None, + rolearn=None) + + cls.mock_function_provider = Mock() + cls.mock_function_provider.get.return_value = cls.function + + list_of_function_names = ['HelloWorld'] + + cls.service, cls.port, cls.url, cls.scheme = make_service(list_of_function_names, cls.mock_function_provider, cls.cwd) + cls.service.create() + t = threading.Thread(name='thread', target=cls.service.run, args=()) + t.setDaemon(True) + t.start() + time.sleep(1) + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.code_abs_path) + + def setUp(self): + # Print full diff when comparing large dictionaries + self.maxDiff = None + + def test_flask_default_options_is_disabled(self): + expected = {"lambda": "mock response"} + + response = requests.post(self.url + '/2015-03-31/functions/HelloWorld/invocations') + + actual = response.json() + + self.assertEquals(actual, expected) + self.assertEquals(response.status_code, 200) + + +def make_service(list_of_function_names, function_provider, cwd): + port = random_port() + manager = ContainerManager() + local_runtime = LambdaRuntime(manager) + lambda_runner = LocalLambdaRunner(local_runtime=local_runtime, + function_provider=function_provider, + cwd=cwd) + + service = LocalLambdaService(list_of_function_names, lambda_runner, port=port) + + scheme = "http" + url = '{}://0.0.0.0:{}'.format(scheme, port) + return service, port, url, scheme + +def random_port(): + return random.randint(30000, 40000) \ No newline at end of file diff --git a/tests/unit/local/lambda_service/__init__.py b/tests/unit/local/lambda_service/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/local/lambda_service/test_service.py b/tests/unit/local/lambda_service/test_service.py new file mode 100644 index 0000000000..c04930cff9 --- /dev/null +++ b/tests/unit/local/lambda_service/test_service.py @@ -0,0 +1,89 @@ +from unittest import TestCase +from mock import Mock, patch + +from samcli.local.lambda_service.service import LocalLambdaService + + +class TestLocalLambdaService(TestCase): + + def test_initalize_creates_default_values(self): + lambda_runner_mock = Mock() + service = LocalLambdaService(function_name_list=['HelloWorld'], lambda_runner=lambda_runner_mock) + self.assertEquals(service.port, 3001) + self.assertEquals(service.host, '127.0.0.1') + self.assertEquals(service.function_name_list, ['HelloWorld']) + self.assertEquals(service.lambda_runner, lambda_runner_mock) + self.assertIsNone(service.stderr) + + def test_initalize_with_values(self): + lambda_runner_mock = Mock() + stderr_mock = Mock() + local_service = LocalLambdaService([], lambda_runner_mock, port=5000, host='129.0.0.0', stderr=stderr_mock) + self.assertEquals(local_service.port, 5000) + self.assertEquals(local_service.host, '129.0.0.0') + self.assertEquals(local_service.function_name_list, []) + self.assertEquals(local_service.stderr, stderr_mock) + self.assertEquals(local_service.lambda_runner, lambda_runner_mock) + + @patch('samcli.local.lambda_service.service.LocalLambdaService._construct_error_handling') + @patch('samcli.local.lambda_service.service.Flask') + def test_create_service_endpoints(self, flask_mock, error_handling_mock): + app_mock = Mock() + flask_mock.return_value = app_mock + + error_handling_mock.return_value = Mock() + + lambda_runner_mock = Mock() + service = LocalLambdaService(function_name_list=['HelloWorld'], lambda_runner=lambda_runner_mock) + + service.create() + + app_mock.add_url_rule.assert_called_once_with('/2015-03-31/functions/HelloWorld/invocations', + endpoint='/2015-03-31/functions/HelloWorld/invocations', + view_func=service._invoke_request_handler, + methods=['POST'], + provide_automatic_options=False) + + def test_runtime_error_raised_when_app_not_created(self): + lambda_runner_mock = Mock() + service = LocalLambdaService(function_name_list=['HelloWorld'], lambda_runner=lambda_runner_mock) + + with self.assertRaises(RuntimeError): + service.run() + + def test_run_starts_service_multithreaded(self): + lambda_runner_mock = Mock() + service = LocalLambdaService(function_name_list=['HelloWorld'], lambda_runner=lambda_runner_mock) + + service._app = Mock() + app_run_mock = Mock() + service._app.run = app_run_mock + + lambda_runner_mock.is_debugging.return_value = False # multithreaded + service.run() + + app_run_mock.assert_called_once_with(threaded=True, host='127.0.0.1', port=3001) + + def test_run_starts_service_singlethreaded(self): + lambda_runner_mock = Mock() + service = LocalLambdaService(function_name_list=['HelloWorld'], lambda_runner=lambda_runner_mock) + + service._app = Mock() + app_run_mock = Mock() + service._app.run = app_run_mock + + lambda_runner_mock.is_debugging.return_value = True # single threaded + service.run() + + app_run_mock.assert_called_once_with(threaded=False, host='127.0.0.1', port=3001) + + @patch('samcli.local.lambda_service.service.jsonify') + def test_invoke_request_handler(self, jsonify_mock): + lambda_runner_mock = Mock() + service = LocalLambdaService(function_name_list=['HelloWorld'], lambda_runner=lambda_runner_mock) + + response = service._invoke_request_handler() + + self.assertEquals(response.status_code, 200) + + jsonify_mock.assert_called_once_with({"lambda": "mock response"}) From a689482c128dd4463e3b6252314eb17c27c011e2 Mon Sep 17 00:00:00 2001 From: Jacob Fuss Date: Wed, 27 Jun 2018 08:05:52 -0700 Subject: [PATCH 2/6] fix naming of Functional tests --- .../local/lambda_service/test_lambda_service.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/functional/local/lambda_service/test_lambda_service.py b/tests/functional/local/lambda_service/test_lambda_service.py index 6ab05c9fcb..08431eb49c 100644 --- a/tests/functional/local/lambda_service/test_lambda_service.py +++ b/tests/functional/local/lambda_service/test_lambda_service.py @@ -16,7 +16,7 @@ from samcli.local.docker.manager import ContainerManager -class TestService_FlaskDefaultOptionsDisabled(TestCase): +class TestLocalLambda(TestCase): @classmethod def setUpClass(cls): cls.code_abs_path = nodejs_lambda(API_GATEWAY_ECHO_EVENT) @@ -31,10 +31,6 @@ def setUpClass(cls): handler="index.handler", codeuri=cls.code_uri, environment=None, rolearn=None) - cls.base64_response_function = provider.Function(name=cls.function_name, runtime="nodejs4.3", memory=256, timeout=5, - handler="index.handler", codeuri=cls.code_uri, environment=None, - rolearn=None) - cls.mock_function_provider = Mock() cls.mock_function_provider.get.return_value = cls.function @@ -55,7 +51,7 @@ def setUp(self): # Print full diff when comparing large dictionaries self.maxDiff = None - def test_flask_default_options_is_disabled(self): + def test_mock_response_is_returned(self): expected = {"lambda": "mock response"} response = requests.post(self.url + '/2015-03-31/functions/HelloWorld/invocations') From 41c31940079ca0fcdff1ef873fa3794c2c900bcc Mon Sep 17 00:00:00 2001 From: Jacob Fuss Date: Wed, 27 Jun 2018 08:55:01 -0700 Subject: [PATCH 3/6] refactored shared code for the services and invoke Local Lambda on request Details: * Moved shared code between APIGW and Lambda service to a Base Class * Updated the Invoke function on the LocalLambdaService to invoke the Local Lambda based on the request * Updated Functional tests for invoking the Local Lambda --- samcli/local/apigw/service.py | 64 +----------------- samcli/local/lambda_service/service.py | 47 ++++++++++--- samcli/local/services/__init__.py | 0 samcli/local/services/base_service.py | 66 +++++++++++++++++++ .../lambda_service/test_lambda_service.py | 8 +-- 5 files changed, 110 insertions(+), 75 deletions(-) create mode 100644 samcli/local/services/__init__.py create mode 100644 samcli/local/services/base_service.py diff --git a/samcli/local/apigw/service.py b/samcli/local/apigw/service.py index 4e5eda750c..efa6abaf31 100644 --- a/samcli/local/apigw/service.py +++ b/samcli/local/apigw/service.py @@ -7,6 +7,7 @@ from flask import Flask, request, Response +from samcli.local.services.base_service import BaseService from samcli.local.lambdafn.exceptions import FunctionNotFound from samcli.local.events.api_event import ContextIdentity, RequestContext, ApiGatewayLambdaEvent from .service_error_responses import ServiceErrorResponses @@ -48,7 +49,7 @@ def __init__(self, methods, function_name, path, binary_types=None): self.binary_types = binary_types or [] -class Service(object): +class Service(BaseService): _DEFAULT_PORT = 3000 _DEFAULT_HOST = '127.0.0.1' @@ -69,13 +70,9 @@ def __init__(self, routing_list, lambda_runner, static_dir=None, port=None, host :param io.BaseIO stderr: Optional stream where the stderr from Docker container should be written to """ self.routing_list = routing_list - self.lambda_runner = lambda_runner self.static_dir = static_dir - self.port = port or self._DEFAULT_PORT - self.host = host or self._DEFAULT_HOST self._dict_of_routes = {} - self._app = None - self.stderr = stderr + super(Service, self).__init__(lambda_runner, port=port, host=host, stderr=stderr) def create(self): """ @@ -203,21 +200,6 @@ def _request_handler(self, **kwargs): return self._service_response(body, headers, status_code) - @staticmethod - def _service_response(body, headers, status_code): - """ - Constructs a Flask Response from the body, headers, and status_code. - - :param str body: Response body as a string - :param dict headers: headers for the response - :param int status_code: status_code for response - :return: Flask Response - """ - response = Response(body) - response.headers = headers - response.status_code = status_code - return response - def _get_current_route(self, flask_request): """ Get the route (Route) based on the current request @@ -239,46 +221,6 @@ def _get_current_route(self, flask_request): return route - @staticmethod - def _get_lambda_output(stdout_stream): - """ - This method will extract read the given stream and return the response from Lambda function separated out - from any log statements it might have outputted. Logs end up in the stdout stream if the Lambda function - wrote directly to stdout using System.out.println or equivalents. - - Parameters - ---------- - stdout_stream : io.BaseIO - Stream to fetch data from - - Returns - ------- - str - String data containing response from Lambda function - str - String data containng logs statements, if any. - """ - # We only want the last line of stdout, because it's possible that - # the function may have written directly to stdout using - # System.out.println or similar, before docker-lambda output the result - stdout_data = stdout_stream.getvalue().rstrip(b'\n') - - # Usually the output is just one line and contains response as JSON string, but if the Lambda function - # wrote anything directly to stdout, there will be additional lines. So just extract the last line as - # response and everything else as log output. - lambda_response = stdout_data - lambda_logs = None - - last_line_position = stdout_data.rfind(b'\n') - if last_line_position > 0: - # So there are multiple lines. Separate them out. - # Everything but the last line are logs - lambda_logs = stdout_data[:last_line_position] - # Last line is Lambda response. Make sure to strip() so we get rid of extra whitespaces & newlines around - lambda_response = stdout_data[last_line_position:].strip() - - return lambda_response, lambda_logs - # Consider moving this out to its own class. Logic is started to get dense and looks messy @jfuss @staticmethod def _parse_lambda_output(lambda_output, binary_types, flask_request): diff --git a/samcli/local/lambda_service/service.py b/samcli/local/lambda_service/service.py index 392c1ab0fd..b767179252 100644 --- a/samcli/local/lambda_service/service.py +++ b/samcli/local/lambda_service/service.py @@ -1,14 +1,20 @@ """Local Lambda Service""" import logging +import re +import io import os -from flask import Flask, jsonify +from flask import Flask, jsonify, request, Response + + +from samcli.local.services.base_service import BaseService +from samcli.local.lambdafn.exceptions import FunctionNotFound LOG = logging.getLogger(__name__) -class LocalLambdaService(object): +class LocalLambdaService(BaseService): _DEFAULT_PORT = 3001 _DEFAULT_HOST = '127.0.0.1' @@ -31,11 +37,7 @@ def __init__(self, function_name_list, lambda_runner, port=None, host=None, stde Optional stream where the stderr from Docker container should be written to """ self.function_name_list = function_name_list - self.lambda_runner = lambda_runner - self.port = port or self._DEFAULT_PORT - self.host = host or self._DEFAULT_HOST - self._app = None - self.stderr = stderr + super(LocalLambdaService, self).__init__(lambda_runner, port=port, host=host, stderr=stderr) def create(self): """ @@ -98,6 +100,31 @@ def _invoke_request_handler(self): A Flask Response response object as if it was returned from Lambda """ - response = jsonify({"lambda": "mock response"}) - response.status_code = 200 - return response + flask_request = request + + function_name_regex = re.compile(r'/2015-03-31/functions/(.*)/invocations') + + regex_match = function_name_regex.match(request.path) + + function_name = "" + + if regex_match: + function_name = regex_match.group(1) + + stdout_stream = io.BytesIO() + + try: + self.lambda_runner.invoke(function_name, {}, stdout=stdout_stream, stderr=self.stderr) + except FunctionNotFound: + # TODO Change this + raise Exception('Change this later') + + lambda_response, lambda_logs = self._get_lambda_output(stdout_stream) + + # import pdb; pdb.set_trace() + + if self.stderr and lambda_logs: + # Write the logs to stderr if available. + self.stderr.write(lambda_logs) + + return self._service_response(lambda_response, {'Content-Type': 'application/json'}, 200) diff --git a/samcli/local/services/__init__.py b/samcli/local/services/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/samcli/local/services/base_service.py b/samcli/local/services/base_service.py new file mode 100644 index 0000000000..f5b649c1b8 --- /dev/null +++ b/samcli/local/services/base_service.py @@ -0,0 +1,66 @@ +from flask import Response + + +class BaseService(object): + + def __init__(self, lambda_runner, port=None, host=None, stderr=None): + self.lambda_runner = lambda_runner + self.port = port or self._DEFAULT_PORT + self.host = host or self._DEFAULT_HOST + self._app = None + self.stderr = stderr + + @staticmethod + def _get_lambda_output(stdout_stream): + """ + This method will extract read the given stream and return the response from Lambda function separated out + from any log statements it might have outputted. Logs end up in the stdout stream if the Lambda function + wrote directly to stdout using System.out.println or equivalents. + + Parameters + ---------- + stdout_stream : io.BaseIO + Stream to fetch data from + + Returns + ------- + str + String data containing response from Lambda function + str + String data containng logs statements, if any. + """ + # We only want the last line of stdout, because it's possible that + # the function may have written directly to stdout using + # System.out.println or similar, before docker-lambda output the result + stdout_data = stdout_stream.getvalue().rstrip(b'\n') + + # Usually the output is just one line and contains response as JSON string, but if the Lambda function + # wrote anything directly to stdout, there will be additional lines. So just extract the last line as + # response and everything else as log output. + lambda_response = stdout_data + lambda_logs = None + + last_line_position = stdout_data.rfind(b'\n') + if last_line_position > 0: + # So there are multiple lines. Separate them out. + # Everything but the last line are logs + lambda_logs = stdout_data[:last_line_position] + # Last line is Lambda response. Make sure to strip() so we get rid of extra whitespaces & newlines around + lambda_response = stdout_data[last_line_position:].strip() + + return lambda_response, lambda_logs + + @staticmethod + def _service_response(body, headers, status_code): + """ + Constructs a Flask Response from the body, headers, and status_code. + + :param str body: Response body as a string + :param dict headers: headers for the response + :param int status_code: status_code for response + :return: Flask Response + """ + response = Response(body) + response.headers = headers + response.status_code = status_code + return response \ No newline at end of file diff --git a/tests/functional/local/lambda_service/test_lambda_service.py b/tests/functional/local/lambda_service/test_lambda_service.py index 08431eb49c..25d0664bca 100644 --- a/tests/functional/local/lambda_service/test_lambda_service.py +++ b/tests/functional/local/lambda_service/test_lambda_service.py @@ -9,17 +9,17 @@ import requests from samcli.local.lambda_service.service import LocalLambdaService -from tests.functional.function_code import nodejs_lambda, API_GATEWAY_ECHO_EVENT +from tests.functional.function_code import nodejs_lambda, HELLO_FROM_LAMBDA from samcli.commands.local.lib import provider from samcli.local.lambdafn.runtime import LambdaRuntime from samcli.commands.local.lib.local_lambda import LocalLambdaRunner from samcli.local.docker.manager import ContainerManager -class TestLocalLambda(TestCase): +class TestLocalLambdaService(TestCase): @classmethod def setUpClass(cls): - cls.code_abs_path = nodejs_lambda(API_GATEWAY_ECHO_EVENT) + cls.code_abs_path = nodejs_lambda(HELLO_FROM_LAMBDA) # Let's convert this absolute path to relative path. Let the parent be the CWD, and codeuri be the folder cls.cwd = os.path.dirname(cls.code_abs_path) @@ -52,7 +52,7 @@ def setUp(self): self.maxDiff = None def test_mock_response_is_returned(self): - expected = {"lambda": "mock response"} + expected = 'Hello from Lambda' response = requests.post(self.url + '/2015-03-31/functions/HelloWorld/invocations') From 29e117ceaa137764c5630fe47f17aafdb0e993c6 Mon Sep 17 00:00:00 2001 From: Jacob Fuss Date: Wed, 27 Jun 2018 10:59:33 -0700 Subject: [PATCH 4/6] Update unit tests based on refactor of Lambda and APIGW Service --- samcli/local/apigw/service.py | 2 +- samcli/local/lambda_service/service.py | 8 +-- samcli/local/services/base_service.py | 21 +++++- tests/functional/local/apigw/test_service.py | 2 +- tests/unit/local/apigw/test_service.py | 64 +---------------- .../unit/local/lambda_service/test_service.py | 59 +++++++++++++-- tests/unit/local/services/__init__.py | 0 .../unit/local/services/test_base_service.py | 72 +++++++++++++++++++ 8 files changed, 152 insertions(+), 76 deletions(-) create mode 100644 tests/unit/local/services/__init__.py create mode 100644 tests/unit/local/services/test_base_service.py diff --git a/samcli/local/apigw/service.py b/samcli/local/apigw/service.py index efa6abaf31..b70d9ad43f 100644 --- a/samcli/local/apigw/service.py +++ b/samcli/local/apigw/service.py @@ -5,7 +5,7 @@ import os import base64 -from flask import Flask, request, Response +from flask import Flask, request from samcli.local.services.base_service import BaseService from samcli.local.lambdafn.exceptions import FunctionNotFound diff --git a/samcli/local/lambda_service/service.py b/samcli/local/lambda_service/service.py index b767179252..1a06cb0f93 100644 --- a/samcli/local/lambda_service/service.py +++ b/samcli/local/lambda_service/service.py @@ -5,7 +5,7 @@ import io import os -from flask import Flask, jsonify, request, Response +from flask import Flask, request from samcli.local.services.base_service import BaseService @@ -104,12 +104,12 @@ def _invoke_request_handler(self): function_name_regex = re.compile(r'/2015-03-31/functions/(.*)/invocations') - regex_match = function_name_regex.match(request.path) + regex_match = function_name_regex.match(flask_request.path) function_name = "" if regex_match: - function_name = regex_match.group(1) + function_name = regex_match.group(1) stdout_stream = io.BytesIO() @@ -121,8 +121,6 @@ def _invoke_request_handler(self): lambda_response, lambda_logs = self._get_lambda_output(stdout_stream) - # import pdb; pdb.set_trace() - if self.stderr and lambda_logs: # Write the logs to stderr if available. self.stderr.write(lambda_logs) diff --git a/samcli/local/services/base_service.py b/samcli/local/services/base_service.py index f5b649c1b8..c3cf28eefc 100644 --- a/samcli/local/services/base_service.py +++ b/samcli/local/services/base_service.py @@ -1,9 +1,28 @@ +"""Base class for all Services that interact with Local Lambda""" + from flask import Response class BaseService(object): + _DEFAULT_PORT = 3000 + _DEFAULT_HOST = '127.0.0.1' + def __init__(self, lambda_runner, port=None, host=None, stderr=None): + """ + Creates a BaseService class + + Parameters + ---------- + lambda_runner samcli.commands.local.lib.local_lambda.LocalLambdaRunner + The Lambda runner class capable of invoking the function + port int + Optional. port for the service to start listening on Defaults to 3000 + host str + Optional. host to start the service on Defaults to '127.0.0.1 + stderr io.BaseIO + Optional stream where the stderr from Docker container should be written to + """ self.lambda_runner = lambda_runner self.port = port or self._DEFAULT_PORT self.host = host or self._DEFAULT_HOST @@ -63,4 +82,4 @@ def _service_response(body, headers, status_code): response = Response(body) response.headers = headers response.status_code = status_code - return response \ No newline at end of file + return response diff --git a/tests/functional/local/apigw/test_service.py b/tests/functional/local/apigw/test_service.py index 8cf6b851d6..ff0cb57edb 100644 --- a/tests/functional/local/apigw/test_service.py +++ b/tests/functional/local/apigw/test_service.py @@ -4,7 +4,6 @@ import shutil import json import time -import logging import requests import random @@ -550,6 +549,7 @@ def test_post_binary_with_incorrect_content_type(self): self.assertEquals(response.status_code, 502) self.assertEquals(response.headers.get('Content-Type'), "application/json") + class TestService_FlaskDefaultOptionsDisabled(TestCase): @classmethod def setUpClass(cls): diff --git a/tests/unit/local/apigw/test_service.py b/tests/unit/local/apigw/test_service.py index 8fd5b16aa3..44362d0cf4 100644 --- a/tests/unit/local/apigw/test_service.py +++ b/tests/unit/local/apigw/test_service.py @@ -1,5 +1,5 @@ from unittest import TestCase -from mock import Mock, patch, MagicMock, ANY +from mock import Mock, patch, ANY import json import base64 @@ -265,48 +265,6 @@ def test_get_current_route_keyerror(self, request_patch): 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) - class TestApiGatewayModel(TestCase): @@ -513,26 +471,6 @@ def test_query_string_params_with_param_value_being_non_empty_list(self): self.assertEquals(actual_query_string, {"param": "b"}) -class TestService_service_response(TestCase): - - @patch('samcli.local.apigw.service.Response') - def test_service_response(self, flask_response_patch): - flask_response_mock = MagicMock() - - flask_response_patch.return_value = flask_response_mock - - body = "this is the body" - status_code = 200 - headers = {"Content-Type": "application/json"} - - actual_response = Service._service_response(body, headers, status_code) - - flask_response_patch.assert_called_once_with("this is the body") - - self.assertEquals(actual_response.status_code, 200) - self.assertEquals(actual_response.headers, {"Content-Type": "application/json"}) - - class TestService_should_base64_encode(TestCase): @parameterized.expand([ diff --git a/tests/unit/local/lambda_service/test_service.py b/tests/unit/local/lambda_service/test_service.py index c04930cff9..10aa47d88c 100644 --- a/tests/unit/local/lambda_service/test_service.py +++ b/tests/unit/local/lambda_service/test_service.py @@ -1,7 +1,8 @@ from unittest import TestCase -from mock import Mock, patch +from mock import Mock, patch, ANY from samcli.local.lambda_service.service import LocalLambdaService +from samcli.local.lambdafn.exceptions import FunctionNotFound class TestLocalLambdaService(TestCase): @@ -77,13 +78,61 @@ def test_run_starts_service_singlethreaded(self): app_run_mock.assert_called_once_with(threaded=False, host='127.0.0.1', port=3001) - @patch('samcli.local.lambda_service.service.jsonify') - def test_invoke_request_handler(self, jsonify_mock): + @patch('samcli.local.lambda_service.service.LocalLambdaService._service_response') + @patch('samcli.local.lambda_service.service.LocalLambdaService._get_lambda_output') + @patch('samcli.local.lambda_service.service.request') + def test_invoke_request_handler(self, request_mock, get_lambda_output_mock, service_response_mock): + request_mock.path = '/2015-03-31/functions/HelloWorld/invocations' + get_lambda_output_mock.return_value = 'hello world', None + service_response_mock.return_value = 'request response' + lambda_runner_mock = Mock() service = LocalLambdaService(function_name_list=['HelloWorld'], lambda_runner=lambda_runner_mock) response = service._invoke_request_handler() - self.assertEquals(response.status_code, 200) + self.assertEquals(response, 'request response') + + lambda_runner_mock.invoke.assert_called_once_with('HelloWorld', {}, stdout=ANY, stderr=None) + service_response_mock.assert_called_once_with('hello world', {'Content-Type': 'application/json'}, 200) + + @patch('samcli.local.lambda_service.service.request') + def test_invoke_request_handler_on_incorrect_path(self, request_mock): + # This will not match the regex + request_mock.path = '/2015-03-31/functions/' + + lambda_runner_mock = Mock() + lambda_runner_mock.invoke.side_effect = FunctionNotFound + service = LocalLambdaService(function_name_list=['HelloWorld'], lambda_runner=lambda_runner_mock) + + with self.assertRaises(Exception): + service._invoke_request_handler() + + lambda_runner_mock.invoke.assert_called_once_with('', {}, stdout=ANY, stderr=None) + + @patch('samcli.local.lambda_service.service.LocalLambdaService._service_response') + @patch('samcli.local.lambda_service.service.LocalLambdaService._get_lambda_output') + @patch('samcli.local.lambda_service.service.request') + def test_request_handler_returns_process_stdout_when_making_response(self, request_mock, get_lambda_output_mock, + service_response_mock): + request_mock.path = '/2015-03-31/functions/HelloWorld/invocations' + + lambda_logs = "logs" + lambda_response = "response" + get_lambda_output_mock.return_value = lambda_response, lambda_logs + + service_response_mock.return_value = 'request response' + + lambda_runner_mock = Mock() + stderr_mock = Mock() + service = LocalLambdaService(function_name_list=['HelloWorld'], + lambda_runner=lambda_runner_mock, + stderr=stderr_mock) + + result = service._invoke_request_handler() + + self.assertEquals(result, 'request response') + service._get_lambda_output.assert_called_with(ANY) - jsonify_mock.assert_called_once_with({"lambda": "mock response"}) + # Make sure the logs are written to stderr + stderr_mock.write.assert_called_with(lambda_logs) diff --git a/tests/unit/local/services/__init__.py b/tests/unit/local/services/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/local/services/test_base_service.py b/tests/unit/local/services/test_base_service.py new file mode 100644 index 0000000000..d4e577277b --- /dev/null +++ b/tests/unit/local/services/test_base_service.py @@ -0,0 +1,72 @@ +from unittest import TestCase +from mock import Mock, patch + +from parameterized import parameterized, param + +from samcli.local.services.base_service import BaseService + + +class TestBaseService(TestCase): + + @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): + lambda_runner_mock = Mock() + + service = BaseService(lambda_runner_mock, port=None, host=None, stderr=None) + + stdout = Mock() + stdout.getvalue.return_value = stdout_data + + response, logs = service._get_lambda_output(stdout) + self.assertEquals(logs, expected_logs) + self.assertEquals(response, expected_response) + + @patch('samcli.local.services.base_service.Response') + def test_service_response(self, flask_response_patch): + flask_response_mock = Mock() + + flask_response_patch.return_value = flask_response_mock + + body = "this is the body" + status_code = 200 + headers = {"Content-Type": "application/json"} + + actual_response = BaseService._service_response(body, headers, status_code) + + flask_response_patch.assert_called_once_with("this is the body") + + self.assertEquals(actual_response.status_code, 200) + self.assertEquals(actual_response.headers, {"Content-Type": "application/json"}) From d8e5c6852d6fae9d49f7dda7ca523dd692bd9c93 Mon Sep 17 00:00:00 2001 From: Jacob Fuss Date: Fri, 29 Jun 2018 11:38:51 -0700 Subject: [PATCH 5/6] Updated based on feedback from PR Details: * Updated some names * Forwarded request body to the event of the invoke * Added functional tests for echoing the event sent to lambda * More refactoring around shared code --- samcli/local/apigw/service.py | 33 +---- samcli/local/lambda_service/local_invoke.py | 88 +++++++++++ samcli/local/lambda_service/service.py | 128 ---------------- .../{base_service.py => localhost_runner.py} | 83 ++++++++--- ...service.py => test_local_lambda_invoke.py} | 54 ++++++- tests/unit/local/apigw/test_service.py | 35 +---- .../local/lambda_service/test_local_invoke.py | 99 +++++++++++++ .../unit/local/lambda_service/test_service.py | 138 ------------------ ...se_service.py => test_localhost_runner.py} | 87 ++++++++--- 9 files changed, 370 insertions(+), 375 deletions(-) create mode 100644 samcli/local/lambda_service/local_invoke.py delete mode 100644 samcli/local/lambda_service/service.py rename samcli/local/services/{base_service.py => localhost_runner.py} (62%) rename tests/functional/local/lambda_service/{test_lambda_service.py => test_local_lambda_invoke.py} (57%) create mode 100644 tests/unit/local/lambda_service/test_local_invoke.py delete mode 100644 tests/unit/local/lambda_service/test_service.py rename tests/unit/local/services/{test_base_service.py => test_localhost_runner.py} (51%) diff --git a/samcli/local/apigw/service.py b/samcli/local/apigw/service.py index b70d9ad43f..6ee71fd7dc 100644 --- a/samcli/local/apigw/service.py +++ b/samcli/local/apigw/service.py @@ -2,12 +2,11 @@ import io import json import logging -import os import base64 from flask import Flask, request -from samcli.local.services.base_service import BaseService +from samcli.local.services.localhost_runner import LocalhostRunner, LambdaOutputParser from samcli.local.lambdafn.exceptions import FunctionNotFound from samcli.local.events.api_event import ContextIdentity, RequestContext, ApiGatewayLambdaEvent from .service_error_responses import ServiceErrorResponses @@ -49,7 +48,7 @@ def __init__(self, methods, function_name, path, binary_types=None): self.binary_types = binary_types or [] -class Service(BaseService): +class Service(LocalhostRunner): _DEFAULT_PORT = 3000 _DEFAULT_HOST = '127.0.0.1' @@ -125,32 +124,6 @@ def _construct_error_handling(self): # Something went wrong self._app.register_error_handler(500, ServiceErrorResponses.lambda_failure_response) - def run(self): - """ - This starts up the (threaded) Local Server. - Note: This is a **blocking call** - - :raise RuntimeError: If the service was not created - """ - - if not self._app: - raise RuntimeError("The application must be created before running") - - # Flask can operate as a single threaded server (which is default) and a multi-threaded server which is - # more for development. When the Lambda container is going to be debugged, then it does not make sense - # to turn on multi-threading because customers can realistically attach only one container at a time to - # the debugger. Keeping this single threaded also enables the Lambda Runner to handle Ctrl+C in order to - # kill the container gracefully (Ctrl+C can be handled only by the main thread) - multi_threaded = not self.lambda_runner.is_debugging() - - LOG.debug("Local API Server starting up. Multi-threading = %s", multi_threaded) - - # This environ signifies we are running a main function for Flask. This is true, since we are using it within - # our cli and not on a production server. - os.environ['WERKZEUG_RUN_MAIN'] = 'true' - - self._app.run(threaded=multi_threaded, host=self.host, port=self.port) - def _request_handler(self, **kwargs): """ We handle all requests to the host:port. The general flow of handling a request is as follows @@ -183,7 +156,7 @@ def _request_handler(self, **kwargs): except FunctionNotFound: return ServiceErrorResponses.lambda_not_found_response() - lambda_response, lambda_logs = self._get_lambda_output(stdout_stream) + lambda_response, lambda_logs = LambdaOutputParser.get_lambda_output(stdout_stream) if self.stderr and lambda_logs: # Write the logs to stderr if available. diff --git a/samcli/local/lambda_service/local_invoke.py b/samcli/local/lambda_service/local_invoke.py new file mode 100644 index 0000000000..6a0ec0d506 --- /dev/null +++ b/samcli/local/lambda_service/local_invoke.py @@ -0,0 +1,88 @@ +"""Local Lambda Service that only invokes a function""" + +import logging +import io + +from flask import Flask, request + + +from samcli.local.services.localhost_runner import LocalhostRunner, LambdaOutputParser +from samcli.local.lambdafn.exceptions import FunctionNotFound + +LOG = logging.getLogger(__name__) + + +class LocalInvoke(LocalhostRunner): + + def __init__(self, lambda_runner, port, host, stderr=None): + """ + Creates a Local Lambda Service that will only response to invoking a function + + Parameters + ---------- + lambda_runner samcli.commands.local.lib.local_lambda.LocalLambdaRunner + The Lambda runner class capable of invoking the function + port int + Optional. port for the service to start listening on + host str + Optional. host to start the service on + stderr io.BaseIO + Optional stream where the stderr from Docker container should be written to + """ + super(LocalInvoke, self).__init__(lambda_runner, port=port, host=host, stderr=stderr) + + def create(self): + """ + Creates a Flask Application that can be started. + """ + self._app = Flask(__name__) + + path = '/2015-03-31/functions//invocations' + self._app.add_url_rule(path, + endpoint=path, + view_func=self._invoke_request_handler, + methods=['POST'], + provide_automatic_options=False) + + self._construct_error_handling() + + def _construct_error_handling(self): + """ + Updates the Flask app with Error Handlers for different Error Codes + + """ + pass + + def _invoke_request_handler(self, function_name): + """ + Request Handler for the Local Lambda Invoke path. This method is responsible for understanding the incoming + request and invoking the Local Lambda Function + + Parameters + ---------- + function_name str + Name of the function to invoke + + Returns + ------- + A Flask Response response object as if it was returned from Lambda + + """ + flask_request = request + request_data = flask_request.get_data().decode('utf-8') + + stdout_stream = io.BytesIO() + + try: + self.lambda_runner.invoke(function_name, request_data, stdout=stdout_stream, stderr=self.stderr) + except FunctionNotFound: + # TODO Change this + raise Exception('Change this later') + + lambda_response, lambda_logs = LambdaOutputParser.get_lambda_output(stdout_stream) + + if self.stderr and lambda_logs: + # Write the logs to stderr if available. + self.stderr.write(lambda_logs) + + return self._service_response(lambda_response, {'Content-Type': 'application/json'}, 200) diff --git a/samcli/local/lambda_service/service.py b/samcli/local/lambda_service/service.py deleted file mode 100644 index 1a06cb0f93..0000000000 --- a/samcli/local/lambda_service/service.py +++ /dev/null @@ -1,128 +0,0 @@ -"""Local Lambda Service""" - -import logging -import re -import io -import os - -from flask import Flask, request - - -from samcli.local.services.base_service import BaseService -from samcli.local.lambdafn.exceptions import FunctionNotFound - -LOG = logging.getLogger(__name__) - - -class LocalLambdaService(BaseService): - - _DEFAULT_PORT = 3001 - _DEFAULT_HOST = '127.0.0.1' - - def __init__(self, function_name_list, lambda_runner, port=None, host=None, stderr=None): - """ - Creates a Local Lambda Service - - Parameters - ---------- - function_name_list list of str - List of the Function Logical Ids - lambda_runner samcli.commands.local.lib.local_lambda.LocalLambdaRunner - The Lambda runner class capable of invoking the function - port int - Optional. port for the service to start listening on. Defaults to 3001 - host str - Optional. host to start the service on Defaults to '127.0.0.1' - stderr io.BaseIO - Optional stream where the stderr from Docker container should be written to - """ - self.function_name_list = function_name_list - super(LocalLambdaService, self).__init__(lambda_runner, port=port, host=host, stderr=stderr) - - def create(self): - """ - Creates a Flask Application that can be started. - """ - self._app = Flask(__name__) - - for function_name in self.function_name_list: - path = '/2015-03-31/functions/{}/invocations'.format(function_name) - self._app.add_url_rule(path, - endpoint=path, - view_func=self._invoke_request_handler, - methods=['POST'], - provide_automatic_options=False) - - self._construct_error_handling() - - def run(self): - """ - This starts up the (threaded) Local Server. - Note: This is a **blocking call** - - Raises - ------ - RuntimeError - if the service was not created - """ - if not self._app: - raise RuntimeError("The application must be created before running") - - # Flask can operate as a single threaded server (which is default) and a multi-threaded server which is - # more for development. When the Lambda container is going to be debugged, then it does not make sense - # to turn on multi-threading because customers can realistically attach only one container at a time to - # the debugger. Keeping this single threaded also enables the Lambda Runner to handle Ctrl+C in order to - # kill the container gracefully (Ctrl+C can be handled only by the main thread) - multi_threaded = not self.lambda_runner.is_debugging() - - LOG.debug("Local Lambda Server starting up. Multi-threading = %s", multi_threaded) - - # This environ signifies we are running a main function for Flask. This is true, since we are using it within - # our cli and not on a production server. - os.environ['WERKZEUG_RUN_MAIN'] = 'true' - - self._app.run(threaded=multi_threaded, host=self.host, port=self.port) - - def _construct_error_handling(self): - """ - Updates the Flask app with Error Handlers for different Error Codes - - """ - pass - - def _invoke_request_handler(self): - """ - Request Handler for the Local Lambda Invoke path. This method is responsible for understanding the incoming - request and invoking the Local Lambda Function - - Returns - ------- - A Flask Response response object as if it was returned from Lambda - - """ - flask_request = request - - function_name_regex = re.compile(r'/2015-03-31/functions/(.*)/invocations') - - regex_match = function_name_regex.match(flask_request.path) - - function_name = "" - - if regex_match: - function_name = regex_match.group(1) - - stdout_stream = io.BytesIO() - - try: - self.lambda_runner.invoke(function_name, {}, stdout=stdout_stream, stderr=self.stderr) - except FunctionNotFound: - # TODO Change this - raise Exception('Change this later') - - lambda_response, lambda_logs = self._get_lambda_output(stdout_stream) - - if self.stderr and lambda_logs: - # Write the logs to stderr if available. - self.stderr.write(lambda_logs) - - return self._service_response(lambda_response, {'Content-Type': 'application/json'}, 200) diff --git a/samcli/local/services/base_service.py b/samcli/local/services/localhost_runner.py similarity index 62% rename from samcli/local/services/base_service.py rename to samcli/local/services/localhost_runner.py index c3cf28eefc..02c9e5b9d2 100644 --- a/samcli/local/services/base_service.py +++ b/samcli/local/services/localhost_runner.py @@ -1,14 +1,16 @@ """Base class for all Services that interact with Local Lambda""" +import logging +import os + from flask import Response +LOG = logging.getLogger(__name__) -class BaseService(object): - _DEFAULT_PORT = 3000 - _DEFAULT_HOST = '127.0.0.1' +class LocalhostRunner(object): - def __init__(self, lambda_runner, port=None, host=None, stderr=None): + def __init__(self, lambda_runner, port, host, stderr=None): """ Creates a BaseService class @@ -24,13 +26,65 @@ def __init__(self, lambda_runner, port=None, host=None, stderr=None): Optional stream where the stderr from Docker container should be written to """ self.lambda_runner = lambda_runner - self.port = port or self._DEFAULT_PORT - self.host = host or self._DEFAULT_HOST + self.port = port + self.host = host self._app = None self.stderr = stderr + def create(self): + """ + Creates a Flask Application that can be started. + """ + raise NotImplementedError("Required method to implement") + + def run(self): + """ + This starts up the (threaded) Local Server. + Note: This is a **blocking call** + + Raises + ------ + RuntimeError + if the service was not created + """ + if not self._app: + raise RuntimeError("The application must be created before running") + + # Flask can operate as a single threaded server (which is default) and a multi-threaded server which is + # more for development. When the Lambda container is going to be debugged, then it does not make sense + # to turn on multi-threading because customers can realistically attach only one container at a time to + # the debugger. Keeping this single threaded also enables the Lambda Runner to handle Ctrl+C in order to + # kill the container gracefully (Ctrl+C can be handled only by the main thread) + multi_threaded = not self.lambda_runner.is_debugging() + + LOG.debug("Localhost server is starting up. Multi-threading = %s", multi_threaded) + + # This environ signifies we are running a main function for Flask. This is true, since we are using it within + # our cli and not on a production server. + os.environ['WERKZEUG_RUN_MAIN'] = 'true' + + self._app.run(threaded=multi_threaded, host=self.host, port=self.port) + + @staticmethod + def _service_response(body, headers, status_code): + """ + Constructs a Flask Response from the body, headers, and status_code. + + :param str body: Response body as a string + :param dict headers: headers for the response + :param int status_code: status_code for response + :return: Flask Response + """ + response = Response(body) + response.headers = headers + response.status_code = status_code + return response + + +class LambdaOutputParser(object): + @staticmethod - def _get_lambda_output(stdout_stream): + def get_lambda_output(stdout_stream): """ This method will extract read the given stream and return the response from Lambda function separated out from any log statements it might have outputted. Logs end up in the stdout stream if the Lambda function @@ -68,18 +122,3 @@ def _get_lambda_output(stdout_stream): lambda_response = stdout_data[last_line_position:].strip() return lambda_response, lambda_logs - - @staticmethod - def _service_response(body, headers, status_code): - """ - Constructs a Flask Response from the body, headers, and status_code. - - :param str body: Response body as a string - :param dict headers: headers for the response - :param int status_code: status_code for response - :return: Flask Response - """ - response = Response(body) - response.headers = headers - response.status_code = status_code - return response diff --git a/tests/functional/local/lambda_service/test_lambda_service.py b/tests/functional/local/lambda_service/test_local_lambda_invoke.py similarity index 57% rename from tests/functional/local/lambda_service/test_lambda_service.py rename to tests/functional/local/lambda_service/test_local_lambda_invoke.py index 25d0664bca..a339b877d5 100644 --- a/tests/functional/local/lambda_service/test_lambda_service.py +++ b/tests/functional/local/lambda_service/test_local_lambda_invoke.py @@ -8,8 +8,8 @@ import requests -from samcli.local.lambda_service.service import LocalLambdaService -from tests.functional.function_code import nodejs_lambda, HELLO_FROM_LAMBDA +from samcli.local.lambda_service.local_invoke import LocalInvoke +from tests.functional.function_code import nodejs_lambda, HELLO_FROM_LAMBDA, ECHO_CODE from samcli.commands.local.lib import provider from samcli.local.lambdafn.runtime import LambdaRuntime from samcli.commands.local.lib.local_lambda import LocalLambdaRunner @@ -62,6 +62,52 @@ def test_mock_response_is_returned(self): self.assertEquals(response.status_code, 200) +class TestLocalEchoLambdaService(TestCase): + @classmethod + def setUpClass(cls): + cls.code_abs_path = nodejs_lambda(ECHO_CODE) + + # Let's convert this absolute path to relative path. Let the parent be the CWD, and codeuri be the folder + cls.cwd = os.path.dirname(cls.code_abs_path) + cls.code_uri = os.path.relpath(cls.code_abs_path, cls.cwd) # Get relative path with respect to CWD + + cls.function_name = "HelloWorld" + + cls.function = provider.Function(name=cls.function_name, runtime="nodejs4.3", memory=256, timeout=5, + handler="index.handler", codeuri=cls.code_uri, environment=None, + rolearn=None) + + cls.mock_function_provider = Mock() + cls.mock_function_provider.get.return_value = cls.function + + list_of_function_names = ['HelloWorld'] + + cls.service, cls.port, cls.url, cls.scheme = make_service(list_of_function_names, cls.mock_function_provider, cls.cwd) + cls.service.create() + t = threading.Thread(name='thread', target=cls.service.run, args=()) + t.setDaemon(True) + t.start() + time.sleep(1) + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.code_abs_path) + + def setUp(self): + # Print full diff when comparing large dictionaries + self.maxDiff = None + + def test_mock_response_is_returned(self): + expected = {"key1": "value1"} + + response = requests.post(self.url + '/2015-03-31/functions/HelloWorld/invocations', json={"key1": "value1"}) + + actual = response.json() + + self.assertEquals(actual, expected) + self.assertEquals(response.status_code, 200) + + def make_service(list_of_function_names, function_provider, cwd): port = random_port() manager = ContainerManager() @@ -70,10 +116,10 @@ def make_service(list_of_function_names, function_provider, cwd): function_provider=function_provider, cwd=cwd) - service = LocalLambdaService(list_of_function_names, lambda_runner, port=port) + service = LocalInvoke(lambda_runner, port=port, host='127.0.0.1') scheme = "http" - url = '{}://0.0.0.0:{}'.format(scheme, port) + url = '{}://127.0.0.1:{}'.format(scheme, port) return service, port, url, scheme def random_port(): diff --git a/tests/unit/local/apigw/test_service.py b/tests/unit/local/apigw/test_service.py index 44362d0cf4..af2ec569bd 100644 --- a/tests/unit/local/apigw/test_service.py +++ b/tests/unit/local/apigw/test_service.py @@ -19,7 +19,7 @@ def setUp(self): self.lambda_runner = Mock() self.stderr = Mock() - self.service = Service(self.list_of_routes, self.lambda_runner, stderr=self.stderr) + self.service = Service(self.list_of_routes, self.lambda_runner, port=3000, host='127.0.0.1', stderr=self.stderr) def test_request_must_invoke_lambda(self): make_response_mock = Mock() @@ -44,7 +44,9 @@ def test_request_must_invoke_lambda(self): stdout=ANY, stderr=self.stderr) - def test_request_handler_returns_process_stdout_when_making_response(self): + @patch('samcli.local.apigw.service.LambdaOutputParser') + def test_request_handler_returns_process_stdout_when_making_response(self, lambda_output_parser_mock): + make_response_mock = Mock() self.service._service_response = make_response_mock @@ -57,8 +59,7 @@ def test_request_handler_returns_process_stdout_when_making_response(self): lambda_logs = "logs" lambda_response = "response" - self.service._get_lambda_output = Mock() - self.service._get_lambda_output.return_value = lambda_response, lambda_logs + lambda_output_parser_mock.get_lambda_output.return_value = lambda_response, lambda_logs service_response_mock = Mock() service_response_mock.return_value = make_response_mock @@ -67,7 +68,7 @@ def test_request_handler_returns_process_stdout_when_making_response(self): result = self.service._request_handler() self.assertEquals(result, make_response_mock) - self.service._get_lambda_output.assert_called_with(ANY) + lambda_output_parser_mock.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) @@ -93,30 +94,6 @@ def test_request_handler_returns_make_response(self): 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() diff --git a/tests/unit/local/lambda_service/test_local_invoke.py b/tests/unit/local/lambda_service/test_local_invoke.py new file mode 100644 index 0000000000..12e2ed1c39 --- /dev/null +++ b/tests/unit/local/lambda_service/test_local_invoke.py @@ -0,0 +1,99 @@ +from unittest import TestCase +from mock import Mock, patch, ANY + +from samcli.local.lambda_service.local_invoke import LocalInvoke +from samcli.local.lambdafn.exceptions import FunctionNotFound + + +class TestLocalLambdaService(TestCase): + + def test_initalize_creates_default_values(self): + lambda_runner_mock = Mock() + service = LocalInvoke(lambda_runner=lambda_runner_mock, port=3001, host='127.0.0.1') + self.assertEquals(service.port, 3001) + self.assertEquals(service.host, '127.0.0.1') + self.assertEquals(service.lambda_runner, lambda_runner_mock) + self.assertIsNone(service.stderr) + + def test_initalize_with_values(self): + lambda_runner_mock = Mock() + stderr_mock = Mock() + local_service = LocalInvoke(lambda_runner_mock, port=5000, host='129.0.0.0', stderr=stderr_mock) + self.assertEquals(local_service.port, 5000) + self.assertEquals(local_service.host, '129.0.0.0') + self.assertEquals(local_service.stderr, stderr_mock) + self.assertEquals(local_service.lambda_runner, lambda_runner_mock) + + @patch('samcli.local.lambda_service.local_invoke.LocalInvoke._construct_error_handling') + @patch('samcli.local.lambda_service.local_invoke.Flask') + def test_create_service_endpoints(self, flask_mock, error_handling_mock): + app_mock = Mock() + flask_mock.return_value = app_mock + + error_handling_mock.return_value = Mock() + + lambda_runner_mock = Mock() + service = LocalInvoke(lambda_runner=lambda_runner_mock, port=3000, host='localhost') + + service.create() + + app_mock.add_url_rule.assert_called_once_with('/2015-03-31/functions//invocations', + endpoint='/2015-03-31/functions//invocations', + view_func=service._invoke_request_handler, + methods=['POST'], + provide_automatic_options=False) + + @patch('samcli.local.lambda_service.local_invoke.LocalInvoke._service_response') + @patch('samcli.local.lambda_service.local_invoke.LambdaOutputParser') + @patch('samcli.local.lambda_service.local_invoke.request') + def test_invoke_request_handler(self, request_mock, lambda_output_parser_mock, service_response_mock): + lambda_output_parser_mock.get_lambda_output.return_value = 'hello world', None + service_response_mock.return_value = 'request response' + request_mock.get_data.return_value = b'{}' + + lambda_runner_mock = Mock() + service = LocalInvoke(lambda_runner=lambda_runner_mock, port=3000, host='localhost') + + response = service._invoke_request_handler(function_name='HelloWorld') + + self.assertEquals(response, 'request response') + + lambda_runner_mock.invoke.assert_called_once_with('HelloWorld', '{}', stdout=ANY, stderr=None) + service_response_mock.assert_called_once_with('hello world', {'Content-Type': 'application/json'}, 200) + + @patch('samcli.local.lambda_service.local_invoke.request') + def test_invoke_request_handler_on_incorrect_path(self, request_mock): + request_mock.get_data.return_value = b'{}' + lambda_runner_mock = Mock() + lambda_runner_mock.invoke.side_effect = FunctionNotFound + service = LocalInvoke(lambda_runner=lambda_runner_mock, port=3000, host='localhost') + + with self.assertRaises(Exception): + service._invoke_request_handler(function_name='NotFound') + + lambda_runner_mock.invoke.assert_called_once_with('NotFound', '{}', stdout=ANY, stderr=None) + + @patch('samcli.local.lambda_service.local_invoke.LocalInvoke._service_response') + @patch('samcli.local.lambda_service.local_invoke.LambdaOutputParser') + @patch('samcli.local.lambda_service.local_invoke.request') + def test_request_handler_returns_process_stdout_when_making_response(self, request_mock, lambda_output_parser_mock, + service_response_mock): + request_mock.get_data.return_value = b'{}' + + lambda_logs = "logs" + lambda_response = "response" + lambda_output_parser_mock.get_lambda_output.return_value = lambda_response, lambda_logs + + service_response_mock.return_value = 'request response' + + lambda_runner_mock = Mock() + stderr_mock = Mock() + service = LocalInvoke(lambda_runner=lambda_runner_mock, port=3000, host='localhost', stderr=stderr_mock) + + result = service._invoke_request_handler(function_name='HelloWorld') + + self.assertEquals(result, 'request response') + lambda_output_parser_mock.get_lambda_output.assert_called_with(ANY) + + # Make sure the logs are written to stderr + stderr_mock.write.assert_called_with(lambda_logs) diff --git a/tests/unit/local/lambda_service/test_service.py b/tests/unit/local/lambda_service/test_service.py deleted file mode 100644 index 10aa47d88c..0000000000 --- a/tests/unit/local/lambda_service/test_service.py +++ /dev/null @@ -1,138 +0,0 @@ -from unittest import TestCase -from mock import Mock, patch, ANY - -from samcli.local.lambda_service.service import LocalLambdaService -from samcli.local.lambdafn.exceptions import FunctionNotFound - - -class TestLocalLambdaService(TestCase): - - def test_initalize_creates_default_values(self): - lambda_runner_mock = Mock() - service = LocalLambdaService(function_name_list=['HelloWorld'], lambda_runner=lambda_runner_mock) - self.assertEquals(service.port, 3001) - self.assertEquals(service.host, '127.0.0.1') - self.assertEquals(service.function_name_list, ['HelloWorld']) - self.assertEquals(service.lambda_runner, lambda_runner_mock) - self.assertIsNone(service.stderr) - - def test_initalize_with_values(self): - lambda_runner_mock = Mock() - stderr_mock = Mock() - local_service = LocalLambdaService([], lambda_runner_mock, port=5000, host='129.0.0.0', stderr=stderr_mock) - self.assertEquals(local_service.port, 5000) - self.assertEquals(local_service.host, '129.0.0.0') - self.assertEquals(local_service.function_name_list, []) - self.assertEquals(local_service.stderr, stderr_mock) - self.assertEquals(local_service.lambda_runner, lambda_runner_mock) - - @patch('samcli.local.lambda_service.service.LocalLambdaService._construct_error_handling') - @patch('samcli.local.lambda_service.service.Flask') - def test_create_service_endpoints(self, flask_mock, error_handling_mock): - app_mock = Mock() - flask_mock.return_value = app_mock - - error_handling_mock.return_value = Mock() - - lambda_runner_mock = Mock() - service = LocalLambdaService(function_name_list=['HelloWorld'], lambda_runner=lambda_runner_mock) - - service.create() - - app_mock.add_url_rule.assert_called_once_with('/2015-03-31/functions/HelloWorld/invocations', - endpoint='/2015-03-31/functions/HelloWorld/invocations', - view_func=service._invoke_request_handler, - methods=['POST'], - provide_automatic_options=False) - - def test_runtime_error_raised_when_app_not_created(self): - lambda_runner_mock = Mock() - service = LocalLambdaService(function_name_list=['HelloWorld'], lambda_runner=lambda_runner_mock) - - with self.assertRaises(RuntimeError): - service.run() - - def test_run_starts_service_multithreaded(self): - lambda_runner_mock = Mock() - service = LocalLambdaService(function_name_list=['HelloWorld'], lambda_runner=lambda_runner_mock) - - service._app = Mock() - app_run_mock = Mock() - service._app.run = app_run_mock - - lambda_runner_mock.is_debugging.return_value = False # multithreaded - service.run() - - app_run_mock.assert_called_once_with(threaded=True, host='127.0.0.1', port=3001) - - def test_run_starts_service_singlethreaded(self): - lambda_runner_mock = Mock() - service = LocalLambdaService(function_name_list=['HelloWorld'], lambda_runner=lambda_runner_mock) - - service._app = Mock() - app_run_mock = Mock() - service._app.run = app_run_mock - - lambda_runner_mock.is_debugging.return_value = True # single threaded - service.run() - - app_run_mock.assert_called_once_with(threaded=False, host='127.0.0.1', port=3001) - - @patch('samcli.local.lambda_service.service.LocalLambdaService._service_response') - @patch('samcli.local.lambda_service.service.LocalLambdaService._get_lambda_output') - @patch('samcli.local.lambda_service.service.request') - def test_invoke_request_handler(self, request_mock, get_lambda_output_mock, service_response_mock): - request_mock.path = '/2015-03-31/functions/HelloWorld/invocations' - get_lambda_output_mock.return_value = 'hello world', None - service_response_mock.return_value = 'request response' - - lambda_runner_mock = Mock() - service = LocalLambdaService(function_name_list=['HelloWorld'], lambda_runner=lambda_runner_mock) - - response = service._invoke_request_handler() - - self.assertEquals(response, 'request response') - - lambda_runner_mock.invoke.assert_called_once_with('HelloWorld', {}, stdout=ANY, stderr=None) - service_response_mock.assert_called_once_with('hello world', {'Content-Type': 'application/json'}, 200) - - @patch('samcli.local.lambda_service.service.request') - def test_invoke_request_handler_on_incorrect_path(self, request_mock): - # This will not match the regex - request_mock.path = '/2015-03-31/functions/' - - lambda_runner_mock = Mock() - lambda_runner_mock.invoke.side_effect = FunctionNotFound - service = LocalLambdaService(function_name_list=['HelloWorld'], lambda_runner=lambda_runner_mock) - - with self.assertRaises(Exception): - service._invoke_request_handler() - - lambda_runner_mock.invoke.assert_called_once_with('', {}, stdout=ANY, stderr=None) - - @patch('samcli.local.lambda_service.service.LocalLambdaService._service_response') - @patch('samcli.local.lambda_service.service.LocalLambdaService._get_lambda_output') - @patch('samcli.local.lambda_service.service.request') - def test_request_handler_returns_process_stdout_when_making_response(self, request_mock, get_lambda_output_mock, - service_response_mock): - request_mock.path = '/2015-03-31/functions/HelloWorld/invocations' - - lambda_logs = "logs" - lambda_response = "response" - get_lambda_output_mock.return_value = lambda_response, lambda_logs - - service_response_mock.return_value = 'request response' - - lambda_runner_mock = Mock() - stderr_mock = Mock() - service = LocalLambdaService(function_name_list=['HelloWorld'], - lambda_runner=lambda_runner_mock, - stderr=stderr_mock) - - result = service._invoke_request_handler() - - self.assertEquals(result, 'request response') - service._get_lambda_output.assert_called_with(ANY) - - # Make sure the logs are written to stderr - stderr_mock.write.assert_called_with(lambda_logs) diff --git a/tests/unit/local/services/test_base_service.py b/tests/unit/local/services/test_localhost_runner.py similarity index 51% rename from tests/unit/local/services/test_base_service.py rename to tests/unit/local/services/test_localhost_runner.py index d4e577277b..c8ecc5384a 100644 --- a/tests/unit/local/services/test_base_service.py +++ b/tests/unit/local/services/test_localhost_runner.py @@ -3,10 +3,70 @@ from parameterized import parameterized, param -from samcli.local.services.base_service import BaseService +from samcli.local.services.localhost_runner import LocalhostRunner, LambdaOutputParser -class TestBaseService(TestCase): +class TestLocalHostRunner(TestCase): + + def test_runtime_error_raised_when_app_not_created(self): + lambda_runner_mock = Mock() + service = LocalhostRunner(lambda_runner=lambda_runner_mock, port=3000, host='127.0.0.1') + + with self.assertRaises(RuntimeError): + service.run() + + def test_run_starts_service_multithreaded(self): + lambda_runner_mock = Mock() + service = LocalhostRunner(lambda_runner=lambda_runner_mock, port=3000, host='127.0.0.1') + + service._app = Mock() + app_run_mock = Mock() + service._app.run = app_run_mock + + lambda_runner_mock.is_debugging.return_value = False # multithreaded + 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): + lambda_runner_mock = Mock() + service = LocalhostRunner(lambda_runner=lambda_runner_mock, port=3000, host='127.0.0.1') + + service._app = Mock() + app_run_mock = Mock() + service._app.run = app_run_mock + + lambda_runner_mock.is_debugging.return_value = True # single threaded + service.run() + + app_run_mock.assert_called_once_with(threaded=False, host='127.0.0.1', port=3000) + + @patch('samcli.local.services.localhost_runner.Response') + def test_service_response(self, flask_response_patch): + flask_response_mock = Mock() + + flask_response_patch.return_value = flask_response_mock + + body = "this is the body" + status_code = 200 + headers = {"Content-Type": "application/json"} + + actual_response = LocalhostRunner._service_response(body, headers, status_code) + + flask_response_patch.assert_called_once_with("this is the body") + + self.assertEquals(actual_response.status_code, 200) + self.assertEquals(actual_response.headers, {"Content-Type": "application/json"}) + + def test_create_returns_not_implemented(self): + lambda_runner_mock = Mock() + service = LocalhostRunner(lambda_runner=lambda_runner_mock, port=3000, host='127.0.0.1') + + with self.assertRaises(NotImplementedError): + service.create() + + +class TestLambdaOutputParser(TestCase): @parameterized.expand([ param( @@ -43,30 +103,9 @@ class TestBaseService(TestCase): ) ]) def test_get_lambda_output_extracts_response(self, test_case_name, stdout_data, expected_logs, expected_response): - lambda_runner_mock = Mock() - - service = BaseService(lambda_runner_mock, port=None, host=None, stderr=None) - stdout = Mock() stdout.getvalue.return_value = stdout_data - response, logs = service._get_lambda_output(stdout) + response, logs = LambdaOutputParser.get_lambda_output(stdout) self.assertEquals(logs, expected_logs) self.assertEquals(response, expected_response) - - @patch('samcli.local.services.base_service.Response') - def test_service_response(self, flask_response_patch): - flask_response_mock = Mock() - - flask_response_patch.return_value = flask_response_mock - - body = "this is the body" - status_code = 200 - headers = {"Content-Type": "application/json"} - - actual_response = BaseService._service_response(body, headers, status_code) - - flask_response_patch.assert_called_once_with("this is the body") - - self.assertEquals(actual_response.status_code, 200) - self.assertEquals(actual_response.headers, {"Content-Type": "application/json"}) From 628a5061238428262e7132cabec4309a9d883170 Mon Sep 17 00:00:00 2001 From: Jacob Fuss Date: Mon, 2 Jul 2018 09:31:57 -0700 Subject: [PATCH 6/6] More naming updates based on feedback --- .../commands/local/lib/local_api_service.py | 14 +-- .../{service.py => local_apigw_service.py} | 14 +-- ...voke.py => local_lambda_invoke_service.py} | 8 +- ...alhost_runner.py => base_local_service.py} | 17 ++-- ...service.py => test_local_apigw_service.py} | 4 +- .../test_local_lambda_invoke.py | 4 +- .../local/lib/test_local_api_service.py | 6 +- ...service.py => test_local_apigw_service.py} | 99 ++++++++++--------- ...py => test_local_lambda_invoke_service.py} | 35 ++++--- ...t_runner.py => test_base_local_service.py} | 24 +++-- 10 files changed, 116 insertions(+), 109 deletions(-) rename samcli/local/apigw/{service.py => local_apigw_service.py} (96%) rename samcli/local/lambda_service/{local_invoke.py => local_lambda_invoke_service.py} (89%) rename samcli/local/services/{localhost_runner.py => base_local_service.py} (88%) rename tests/functional/local/apigw/{test_service.py => test_local_apigw_service.py} (99%) rename tests/unit/local/apigw/{test_service.py => test_local_apigw_service.py} (82%) rename tests/unit/local/lambda_service/{test_local_invoke.py => test_local_lambda_invoke_service.py} (67%) rename tests/unit/local/services/{test_localhost_runner.py => test_base_local_service.py} (75%) diff --git a/samcli/commands/local/lib/local_api_service.py b/samcli/commands/local/lib/local_api_service.py index e8761798de..e0243192c8 100644 --- a/samcli/commands/local/lib/local_api_service.py +++ b/samcli/commands/local/lib/local_api_service.py @@ -5,7 +5,7 @@ import os import logging -from samcli.local.apigw.service import Service, Route +from samcli.local.apigw.local_apigw_service import LocalApigwService, Route from samcli.commands.local.lib.sam_api_provider import SamApiProvider from samcli.commands.local.lib.exceptions import NoApisDefined @@ -62,12 +62,12 @@ def start(self): # 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 = LocalApigwService(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() diff --git a/samcli/local/apigw/service.py b/samcli/local/apigw/local_apigw_service.py similarity index 96% rename from samcli/local/apigw/service.py rename to samcli/local/apigw/local_apigw_service.py index 6ee71fd7dc..ec4a01c942 100644 --- a/samcli/local/apigw/service.py +++ b/samcli/local/apigw/local_apigw_service.py @@ -6,7 +6,7 @@ from flask import Flask, request -from samcli.local.services.localhost_runner import LocalhostRunner, LambdaOutputParser +from samcli.local.services.base_local_service import BaseLocalService, LambdaOutputParser from samcli.local.lambdafn.exceptions import FunctionNotFound from samcli.local.events.api_event import ContextIdentity, RequestContext, ApiGatewayLambdaEvent from .service_error_responses import ServiceErrorResponses @@ -48,7 +48,7 @@ def __init__(self, methods, function_name, path, binary_types=None): self.binary_types = binary_types or [] -class Service(LocalhostRunner): +class LocalApigwService(BaseLocalService): _DEFAULT_PORT = 3000 _DEFAULT_HOST = '127.0.0.1' @@ -68,10 +68,12 @@ def __init__(self, routing_list, lambda_runner, static_dir=None, port=None, host Defaults to '127.0.0.1 :param io.BaseIO stderr: Optional stream where the stderr from Docker container should be written to """ + super(LocalApigwService, self).__init__(lambda_runner.is_debugging(), port=port, host=host) self.routing_list = routing_list + self.lambda_runner = lambda_runner self.static_dir = static_dir self._dict_of_routes = {} - super(Service, self).__init__(lambda_runner, port=port, host=host, stderr=stderr) + self.stderr = stderr def create(self): """ @@ -223,7 +225,7 @@ def _parse_lambda_output(lambda_output, binary_types, flask_request): LOG.info("No Content-Type given. Defaulting to 'application/json'.") headers["Content-Type"] = "application/json" - if Service._should_base64_decode_body(binary_types, flask_request, headers, is_base_64_encoded): + if LocalApigwService._should_base64_decode_body(binary_types, flask_request, headers, is_base_64_encoded): body = base64.b64decode(body) return status_code, headers, body @@ -272,7 +274,7 @@ def _construct_event(flask_request, port, binary_types): request_mimetype = flask_request.mimetype - is_base_64 = Service._should_base64_encode(binary_types, request_mimetype) + is_base_64 = LocalApigwService._should_base64_encode(binary_types, request_mimetype) if is_base_64: LOG.debug("Incoming Request seems to be binary. Base64 encoding the request data before sending to Lambda.") @@ -295,7 +297,7 @@ def _construct_event(flask_request, port, binary_types): # APIGW does not support duplicate query parameters. Flask gives query params as a list so # we need to convert only grab the first item unless many were given, were we grab the last to be consistent # with APIGW - query_string_dict = Service._query_string_params(flask_request) + query_string_dict = LocalApigwService._query_string_params(flask_request) event = ApiGatewayLambdaEvent(http_method=method, body=request_data, diff --git a/samcli/local/lambda_service/local_invoke.py b/samcli/local/lambda_service/local_lambda_invoke_service.py similarity index 89% rename from samcli/local/lambda_service/local_invoke.py rename to samcli/local/lambda_service/local_lambda_invoke_service.py index 6a0ec0d506..4b65ec60e7 100644 --- a/samcli/local/lambda_service/local_invoke.py +++ b/samcli/local/lambda_service/local_lambda_invoke_service.py @@ -6,13 +6,13 @@ from flask import Flask, request -from samcli.local.services.localhost_runner import LocalhostRunner, LambdaOutputParser +from samcli.local.services.base_local_service import BaseLocalService, LambdaOutputParser from samcli.local.lambdafn.exceptions import FunctionNotFound LOG = logging.getLogger(__name__) -class LocalInvoke(LocalhostRunner): +class LocalLambdaInvokeService(BaseLocalService): def __init__(self, lambda_runner, port, host, stderr=None): """ @@ -29,7 +29,9 @@ def __init__(self, lambda_runner, port, host, stderr=None): stderr io.BaseIO Optional stream where the stderr from Docker container should be written to """ - super(LocalInvoke, self).__init__(lambda_runner, port=port, host=host, stderr=stderr) + super(LocalLambdaInvokeService, self).__init__(lambda_runner.is_debugging(), port=port, host=host) + self.lambda_runner = lambda_runner + self.stderr = stderr def create(self): """ diff --git a/samcli/local/services/localhost_runner.py b/samcli/local/services/base_local_service.py similarity index 88% rename from samcli/local/services/localhost_runner.py rename to samcli/local/services/base_local_service.py index 02c9e5b9d2..9a2874bc7f 100644 --- a/samcli/local/services/localhost_runner.py +++ b/samcli/local/services/base_local_service.py @@ -8,28 +8,25 @@ LOG = logging.getLogger(__name__) -class LocalhostRunner(object): +class BaseLocalService(object): - def __init__(self, lambda_runner, port, host, stderr=None): + def __init__(self, is_debugging, port, host): """ - Creates a BaseService class + Creates a BaseLocalService class Parameters ---------- - lambda_runner samcli.commands.local.lib.local_lambda.LocalLambdaRunner - The Lambda runner class capable of invoking the function + is_debugging bool + Flag to run in debug mode or not port int Optional. port for the service to start listening on Defaults to 3000 host str Optional. host to start the service on Defaults to '127.0.0.1 - stderr io.BaseIO - Optional stream where the stderr from Docker container should be written to """ - self.lambda_runner = lambda_runner + self.is_debugging = is_debugging self.port = port self.host = host self._app = None - self.stderr = stderr def create(self): """ @@ -55,7 +52,7 @@ def run(self): # to turn on multi-threading because customers can realistically attach only one container at a time to # the debugger. Keeping this single threaded also enables the Lambda Runner to handle Ctrl+C in order to # kill the container gracefully (Ctrl+C can be handled only by the main thread) - multi_threaded = not self.lambda_runner.is_debugging() + multi_threaded = not self.is_debugging LOG.debug("Localhost server is starting up. Multi-threading = %s", multi_threaded) diff --git a/tests/functional/local/apigw/test_service.py b/tests/functional/local/apigw/test_local_apigw_service.py similarity index 99% rename from tests/functional/local/apigw/test_service.py rename to tests/functional/local/apigw/test_local_apigw_service.py index ff0cb57edb..7ae8fa8939 100644 --- a/tests/functional/local/apigw/test_service.py +++ b/tests/functional/local/apigw/test_local_apigw_service.py @@ -9,7 +9,7 @@ import random from mock import Mock -from samcli.local.apigw.service import Route, Service +from samcli.local.apigw.local_apigw_service import Route, LocalApigwService from tests.functional.function_code import nodejs_lambda, API_GATEWAY_ECHO_EVENT, API_GATEWAY_BAD_PROXY_RESPONSE, API_GATEWAY_ECHO_BASE64_EVENT, API_GATEWAY_CONTENT_TYPE_LOWER from samcli.commands.local.lib import provider from samcli.local.lambdafn.runtime import LambdaRuntime @@ -617,7 +617,7 @@ def make_service(list_of_routes, function_provider, cwd): function_provider=function_provider, cwd=cwd) - service = Service(list_of_routes, lambda_runner, port=port) + service = LocalApigwService(list_of_routes, lambda_runner, port=port) scheme = "http" url = '{}://0.0.0.0:{}'.format(scheme, port) diff --git a/tests/functional/local/lambda_service/test_local_lambda_invoke.py b/tests/functional/local/lambda_service/test_local_lambda_invoke.py index a339b877d5..e6dc0e8277 100644 --- a/tests/functional/local/lambda_service/test_local_lambda_invoke.py +++ b/tests/functional/local/lambda_service/test_local_lambda_invoke.py @@ -8,7 +8,7 @@ import requests -from samcli.local.lambda_service.local_invoke import LocalInvoke +from samcli.local.lambda_service.local_lambda_invoke_service import LocalLambdaInvokeService from tests.functional.function_code import nodejs_lambda, HELLO_FROM_LAMBDA, ECHO_CODE from samcli.commands.local.lib import provider from samcli.local.lambdafn.runtime import LambdaRuntime @@ -116,7 +116,7 @@ def make_service(list_of_function_names, function_provider, cwd): function_provider=function_provider, cwd=cwd) - service = LocalInvoke(lambda_runner, port=port, host='127.0.0.1') + service = LocalLambdaInvokeService(lambda_runner, port=port, host='127.0.0.1') scheme = "http" url = '{}://127.0.0.1:{}'.format(scheme, port) diff --git a/tests/unit/commands/local/lib/test_local_api_service.py b/tests/unit/commands/local/lib/test_local_api_service.py index aee6f7f4fa..d018ad0525 100644 --- a/tests/unit/commands/local/lib/test_local_api_service.py +++ b/tests/unit/commands/local/lib/test_local_api_service.py @@ -8,7 +8,7 @@ from samcli.commands.local.lib.local_api_service import LocalApiService from samcli.commands.local.lib.exceptions import NoApisDefined from samcli.commands.local.lib.provider import Api -from samcli.local.apigw.service import Route +from samcli.local.apigw.local_apigw_service import Route class TestLocalApiService_start(TestCase): @@ -32,7 +32,7 @@ def setUp(self): self.lambda_invoke_context_mock.get_cwd.return_value = self.cwd self.lambda_invoke_context_mock.stderr = self.stderr_mock - @patch("samcli.commands.local.lib.local_api_service.Service") + @patch("samcli.commands.local.lib.local_api_service.LocalApigwService") @patch("samcli.commands.local.lib.local_api_service.SamApiProvider") @patch.object(LocalApiService, "_make_static_dir_path") @patch.object(LocalApiService, "_print_routes") @@ -73,7 +73,7 @@ def test_must_start_service(self, self.apigw_service.create.assert_called_with() self.apigw_service.run.assert_called_with() - @patch("samcli.commands.local.lib.local_api_service.Service") + @patch("samcli.commands.local.lib.local_api_service.LocalApigwService") @patch("samcli.commands.local.lib.local_api_service.SamApiProvider") @patch.object(LocalApiService, "_make_static_dir_path") @patch.object(LocalApiService, "_print_routes") diff --git a/tests/unit/local/apigw/test_service.py b/tests/unit/local/apigw/test_local_apigw_service.py similarity index 82% rename from tests/unit/local/apigw/test_service.py rename to tests/unit/local/apigw/test_local_apigw_service.py index af2ec569bd..3e741e21dc 100644 --- a/tests/unit/local/apigw/test_service.py +++ b/tests/unit/local/apigw/test_local_apigw_service.py @@ -5,7 +5,7 @@ from parameterized import parameterized, param -from samcli.local.apigw.service import Service, Route, CaseInsensitiveDict +from samcli.local.apigw.local_apigw_service import LocalApigwService, Route, CaseInsensitiveDict from samcli.local.lambdafn.exceptions import FunctionNotFound @@ -17,9 +17,14 @@ def setUp(self): 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 = Service(self.list_of_routes, self.lambda_runner, port=3000, host='127.0.0.1', stderr=self.stderr) + self.service = LocalApigwService(self.list_of_routes, + self.lambda_runner, + port=3000, + host='127.0.0.1', + stderr=self.stderr) def test_request_must_invoke_lambda(self): make_response_mock = Mock() @@ -44,7 +49,7 @@ def test_request_must_invoke_lambda(self): stdout=ANY, stderr=self.stderr) - @patch('samcli.local.apigw.service.LambdaOutputParser') + @patch('samcli.local.apigw.local_apigw_service.LambdaOutputParser') def test_request_handler_returns_process_stdout_when_making_response(self, lambda_output_parser_mock): make_response_mock = Mock() @@ -104,7 +109,7 @@ def test_create_creates_dict_of_routes(self): lambda_runner = Mock() - service = Service(list_of_routes, lambda_runner) + service = LocalApigwService(list_of_routes, lambda_runner) service.create() @@ -112,7 +117,7 @@ def test_create_creates_dict_of_routes(self): '/:POST': api_gateway_route_2 }) - @patch('samcli.local.apigw.service.Flask') + @patch('samcli.local.apigw.local_apigw_service.Flask') def test_create_creates_flask_app_with_url_rules(self, flask): app_mock = Mock() flask.return_value = app_mock @@ -136,14 +141,14 @@ def test_initalize_creates_default_values(self): 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') + local_service = LocalApigwService([], 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') + @patch('samcli.local.apigw.local_apigw_service.ServiceErrorResponses') def test_request_handles_error_when_invoke_cant_find_function(self, service_error_responses_patch): not_found_response_mock = Mock() @@ -166,7 +171,7 @@ def test_request_throws_when_invoke_fails(self): with self.assertRaises(Exception): self.service._request_handler() - @patch('samcli.local.apigw.service.ServiceErrorResponses') + @patch('samcli.local.apigw.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() @@ -183,7 +188,7 @@ def test_request_handler_errors_when_parse_lambda_output_raises_keyerror(self, s self.assertEquals(result, failure_response_mock) - @patch('samcli.local.apigw.service.ServiceErrorResponses') + @patch('samcli.local.apigw.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() @@ -192,7 +197,7 @@ def test_request_handler_errors_when_get_current_route_fails(self, service_error with self.assertRaises(KeyError): self.service._request_handler() - @patch('samcli.local.apigw.service.ServiceErrorResponses') + @patch('samcli.local.apigw.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") @@ -205,7 +210,7 @@ def test_request_handler_errors_when_unable_to_read_binary_data(self, service_er result = self.service._request_handler() self.assertEquals(result, failure_mock) - @patch('samcli.local.apigw.service.request') + @patch('samcli.local.apigw.local_apigw_service.request') def test_get_current_route(self, request_patch): request_mock = Mock() @@ -221,7 +226,7 @@ def test_get_current_route(self, request_patch): self.assertEquals(self.service._get_current_route(request_mock), "function") - @patch('samcli.local.apigw.service.request') + @patch('samcli.local.apigw.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, @@ -261,7 +266,7 @@ def test_default_content_type_header_added_with_no_headers(self): lambda_output = '{"statusCode": 200, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' \ '"isBase64Encoded": false}' - (_, headers, _) = Service._parse_lambda_output(lambda_output, binary_types=[], flask_request=Mock()) + (_, headers, _) = LocalApigwService._parse_lambda_output(lambda_output, binary_types=[], flask_request=Mock()) self.assertIn("Content-Type", headers) self.assertEquals(headers["Content-Type"], "application/json") @@ -270,7 +275,7 @@ def test_default_content_type_header_added_with_empty_headers(self): lambda_output = '{"statusCode": 200, "headers":{}, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' \ '"isBase64Encoded": false}' - (_, headers, _) = Service._parse_lambda_output(lambda_output, binary_types=[], flask_request=Mock()) + (_, headers, _) = LocalApigwService._parse_lambda_output(lambda_output, binary_types=[], flask_request=Mock()) self.assertIn("Content-Type", headers) self.assertEquals(headers["Content-Type"], "application/json") @@ -279,7 +284,7 @@ def test_custom_content_type_header_is_not_modified(self): lambda_output = '{"statusCode": 200, "headers":{"Content-Type": "text/xml"}, "body": "{}", ' \ '"isBase64Encoded": false}' - (_, headers, _) = Service._parse_lambda_output(lambda_output, binary_types=[], flask_request=Mock()) + (_, headers, _) = LocalApigwService._parse_lambda_output(lambda_output, binary_types=[], flask_request=Mock()) self.assertIn("Content-Type", headers) self.assertEquals(headers["Content-Type"], "text/xml") @@ -288,9 +293,9 @@ def test_extra_values_ignored(self): lambda_output = '{"statusCode": 200, "headers": {}, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' \ '"isBase64Encoded": false, "another_key": "some value"}' - (status_code, headers, body) = Service._parse_lambda_output(lambda_output, - binary_types=[], - flask_request=Mock()) + (status_code, headers, body) = LocalApigwService._parse_lambda_output(lambda_output, + binary_types=[], + flask_request=Mock()) self.assertEquals(status_code, 200) self.assertEquals(headers, {"Content-Type": "application/json"}) @@ -300,15 +305,15 @@ def test_parse_returns_correct_tuple(self): lambda_output = '{"statusCode": 200, "headers": {}, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' \ '"isBase64Encoded": false}' - (status_code, headers, body) = Service._parse_lambda_output(lambda_output, - binary_types=[], - flask_request=Mock()) + (status_code, headers, body) = LocalApigwService._parse_lambda_output(lambda_output, + binary_types=[], + flask_request=Mock()) self.assertEquals(status_code, 200) self.assertEquals(headers, {"Content-Type": "application/json"}) self.assertEquals(body, '{"message":"Hello from Lambda"}') - @patch('samcli.local.apigw.service.Service._should_base64_decode_body') + @patch('samcli.local.apigw.local_apigw_service.LocalApigwService._should_base64_decode_body') def test_parse_returns_decodes_base64_to_binary(self, should_decode_body_patch): should_decode_body_patch.return_value = True @@ -319,9 +324,9 @@ def test_parse_returns_decodes_base64_to_binary(self, should_decode_body_patch): "body": base64_body, "isBase64Encoded": False} - (status_code, headers, body) = Service._parse_lambda_output(json.dumps(lambda_output), - binary_types=['*/*'], - flask_request=Mock()) + (status_code, headers, body) = LocalApigwService._parse_lambda_output(json.dumps(lambda_output), + binary_types=['*/*'], + flask_request=Mock()) self.assertEquals(status_code, 200) self.assertEquals(headers, {"Content-Type": "application/octet-stream"}) @@ -332,40 +337,40 @@ def test_status_code_not_int(self): '"isBase64Encoded": false}' with self.assertRaises(TypeError): - Service._parse_lambda_output(lambda_output, - binary_types=[], - flask_request=Mock()) + LocalApigwService._parse_lambda_output(lambda_output, + binary_types=[], + flask_request=Mock()) def test_status_code_negative_int(self): lambda_output = '{"statusCode": -1, "headers": {}, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' \ '"isBase64Encoded": false}' with self.assertRaises(TypeError): - Service._parse_lambda_output(lambda_output, - binary_types=[], - flask_request=Mock()) + LocalApigwService._parse_lambda_output(lambda_output, + binary_types=[], + flask_request=Mock()) def test_lambda_output_list_not_dict(self): lambda_output = '[]' with self.assertRaises(TypeError): - Service._parse_lambda_output(lambda_output, - binary_types=[], - flask_request=Mock()) + LocalApigwService._parse_lambda_output(lambda_output, + binary_types=[], + flask_request=Mock()) def test_lambda_output_not_json_serializable(self): lambda_output = 'some str' with self.assertRaises(ValueError): - Service._parse_lambda_output(lambda_output, binary_types=[], flask_request=Mock()) + LocalApigwService._parse_lambda_output(lambda_output, binary_types=[], flask_request=Mock()) def test_properties_are_null(self): lambda_output = '{"statusCode": 0, "headers": null, "body": null, ' \ '"isBase64Encoded": null}' - (status_code, headers, body) = Service._parse_lambda_output(lambda_output, - binary_types=[], - flask_request=Mock()) + (status_code, headers, body) = LocalApigwService._parse_lambda_output(lambda_output, + binary_types=[], + flask_request=Mock()) self.assertEquals(status_code, 200) self.assertEquals(headers, {"Content-Type": "application/json"}) @@ -402,17 +407,17 @@ def setUp(self): self.expected_dict = json.loads(expected) def test_construct_event_with_data(self): - actual_event_str = Service._construct_event(self.request_mock, 3000, binary_types=[]) + actual_event_str = LocalApigwService._construct_event(self.request_mock, 3000, binary_types=[]) self.assertEquals(json.loads(actual_event_str), self.expected_dict) def test_construct_event_no_data(self): self.request_mock.get_data.return_value = None self.expected_dict["body"] = None - actual_event_str = Service._construct_event(self.request_mock, 3000, binary_types=[]) + actual_event_str = LocalApigwService._construct_event(self.request_mock, 3000, binary_types=[]) self.assertEquals(json.loads(actual_event_str), self.expected_dict) - @patch('samcli.local.apigw.service.Service._should_base64_encode') + @patch('samcli.local.apigw.local_apigw_service.LocalApigwService._should_base64_encode') def test_construct_event_with_binary_data(self, should_base64_encode_patch): should_base64_encode_patch.return_value = True @@ -423,28 +428,28 @@ def test_construct_event_with_binary_data(self, should_base64_encode_patch): self.expected_dict["body"] = base64_body self.expected_dict["isBase64Encoded"] = True - actual_event_str = Service._construct_event(self.request_mock, 3000, binary_types=[]) + actual_event_str = LocalApigwService._construct_event(self.request_mock, 3000, binary_types=[]) self.assertEquals(json.loads(actual_event_str), self.expected_dict) def test_query_string_params_with_empty_params(self): request_mock = Mock() request_mock.args = {} - actual_query_string = Service._query_string_params(request_mock) + actual_query_string = LocalApigwService._query_string_params(request_mock) self.assertEquals(actual_query_string, {}) def test_query_string_params_with_param_value_being_empty_list(self): request_mock = Mock() request_mock.args = {"param": []} - actual_query_string = Service._query_string_params(request_mock) + actual_query_string = LocalApigwService._query_string_params(request_mock) self.assertEquals(actual_query_string, {"param": ""}) def test_query_string_params_with_param_value_being_non_empty_list(self): request_mock = Mock() request_mock.args = {"param": ["a", "b"]} - actual_query_string = Service._query_string_params(request_mock) + actual_query_string = LocalApigwService._query_string_params(request_mock) self.assertEquals(actual_query_string, {"param": "b"}) @@ -456,13 +461,13 @@ class TestService_should_base64_encode(TestCase): param("*/* is in binary types with no mimetype defined", ['*/*'], None) ]) def test_should_base64_encode_returns_true(self, test_case_name, binary_types, mimetype): - self.assertTrue(Service._should_base64_encode(binary_types, mimetype)) + self.assertTrue(LocalApigwService._should_base64_encode(binary_types, mimetype)) @parameterized.expand([ param("Mimetype is not in binary types", ['image/gif'], "application/octet-stream") ]) def test_should_base64_encode_returns_false(self, test_case_name, binary_types, mimetype): - self.assertFalse(Service._should_base64_encode(binary_types, mimetype)) + self.assertFalse(LocalApigwService._should_base64_encode(binary_types, mimetype)) class TestService_CaseInsensiveDict(TestCase): diff --git a/tests/unit/local/lambda_service/test_local_invoke.py b/tests/unit/local/lambda_service/test_local_lambda_invoke_service.py similarity index 67% rename from tests/unit/local/lambda_service/test_local_invoke.py rename to tests/unit/local/lambda_service/test_local_lambda_invoke_service.py index 12e2ed1c39..dca78c5c25 100644 --- a/tests/unit/local/lambda_service/test_local_invoke.py +++ b/tests/unit/local/lambda_service/test_local_lambda_invoke_service.py @@ -1,7 +1,7 @@ from unittest import TestCase from mock import Mock, patch, ANY -from samcli.local.lambda_service.local_invoke import LocalInvoke +from samcli.local.lambda_service.local_lambda_invoke_service import LocalLambdaInvokeService from samcli.local.lambdafn.exceptions import FunctionNotFound @@ -9,7 +9,7 @@ class TestLocalLambdaService(TestCase): def test_initalize_creates_default_values(self): lambda_runner_mock = Mock() - service = LocalInvoke(lambda_runner=lambda_runner_mock, port=3001, host='127.0.0.1') + service = LocalLambdaInvokeService(lambda_runner=lambda_runner_mock, port=3001, host='127.0.0.1') self.assertEquals(service.port, 3001) self.assertEquals(service.host, '127.0.0.1') self.assertEquals(service.lambda_runner, lambda_runner_mock) @@ -18,14 +18,14 @@ def test_initalize_creates_default_values(self): def test_initalize_with_values(self): lambda_runner_mock = Mock() stderr_mock = Mock() - local_service = LocalInvoke(lambda_runner_mock, port=5000, host='129.0.0.0', stderr=stderr_mock) + local_service = LocalLambdaInvokeService(lambda_runner_mock, port=5000, host='129.0.0.0', stderr=stderr_mock) self.assertEquals(local_service.port, 5000) self.assertEquals(local_service.host, '129.0.0.0') self.assertEquals(local_service.stderr, stderr_mock) self.assertEquals(local_service.lambda_runner, lambda_runner_mock) - @patch('samcli.local.lambda_service.local_invoke.LocalInvoke._construct_error_handling') - @patch('samcli.local.lambda_service.local_invoke.Flask') + @patch('samcli.local.lambda_service.local_lambda_invoke_service.LocalLambdaInvokeService._construct_error_handling') + @patch('samcli.local.lambda_service.local_lambda_invoke_service.Flask') def test_create_service_endpoints(self, flask_mock, error_handling_mock): app_mock = Mock() flask_mock.return_value = app_mock @@ -33,7 +33,7 @@ def test_create_service_endpoints(self, flask_mock, error_handling_mock): error_handling_mock.return_value = Mock() lambda_runner_mock = Mock() - service = LocalInvoke(lambda_runner=lambda_runner_mock, port=3000, host='localhost') + service = LocalLambdaInvokeService(lambda_runner=lambda_runner_mock, port=3000, host='localhost') service.create() @@ -43,16 +43,16 @@ def test_create_service_endpoints(self, flask_mock, error_handling_mock): methods=['POST'], provide_automatic_options=False) - @patch('samcli.local.lambda_service.local_invoke.LocalInvoke._service_response') - @patch('samcli.local.lambda_service.local_invoke.LambdaOutputParser') - @patch('samcli.local.lambda_service.local_invoke.request') + @patch('samcli.local.lambda_service.local_lambda_invoke_service.LocalLambdaInvokeService._service_response') + @patch('samcli.local.lambda_service.local_lambda_invoke_service.LambdaOutputParser') + @patch('samcli.local.lambda_service.local_lambda_invoke_service.request') def test_invoke_request_handler(self, request_mock, lambda_output_parser_mock, service_response_mock): lambda_output_parser_mock.get_lambda_output.return_value = 'hello world', None service_response_mock.return_value = 'request response' request_mock.get_data.return_value = b'{}' lambda_runner_mock = Mock() - service = LocalInvoke(lambda_runner=lambda_runner_mock, port=3000, host='localhost') + service = LocalLambdaInvokeService(lambda_runner=lambda_runner_mock, port=3000, host='localhost') response = service._invoke_request_handler(function_name='HelloWorld') @@ -61,21 +61,21 @@ def test_invoke_request_handler(self, request_mock, lambda_output_parser_mock, s lambda_runner_mock.invoke.assert_called_once_with('HelloWorld', '{}', stdout=ANY, stderr=None) service_response_mock.assert_called_once_with('hello world', {'Content-Type': 'application/json'}, 200) - @patch('samcli.local.lambda_service.local_invoke.request') + @patch('samcli.local.lambda_service.local_lambda_invoke_service.request') def test_invoke_request_handler_on_incorrect_path(self, request_mock): request_mock.get_data.return_value = b'{}' lambda_runner_mock = Mock() lambda_runner_mock.invoke.side_effect = FunctionNotFound - service = LocalInvoke(lambda_runner=lambda_runner_mock, port=3000, host='localhost') + service = LocalLambdaInvokeService(lambda_runner=lambda_runner_mock, port=3000, host='localhost') with self.assertRaises(Exception): service._invoke_request_handler(function_name='NotFound') lambda_runner_mock.invoke.assert_called_once_with('NotFound', '{}', stdout=ANY, stderr=None) - @patch('samcli.local.lambda_service.local_invoke.LocalInvoke._service_response') - @patch('samcli.local.lambda_service.local_invoke.LambdaOutputParser') - @patch('samcli.local.lambda_service.local_invoke.request') + @patch('samcli.local.lambda_service.local_lambda_invoke_service.LocalLambdaInvokeService._service_response') + @patch('samcli.local.lambda_service.local_lambda_invoke_service.LambdaOutputParser') + @patch('samcli.local.lambda_service.local_lambda_invoke_service.request') def test_request_handler_returns_process_stdout_when_making_response(self, request_mock, lambda_output_parser_mock, service_response_mock): request_mock.get_data.return_value = b'{}' @@ -88,7 +88,10 @@ def test_request_handler_returns_process_stdout_when_making_response(self, reque lambda_runner_mock = Mock() stderr_mock = Mock() - service = LocalInvoke(lambda_runner=lambda_runner_mock, port=3000, host='localhost', stderr=stderr_mock) + service = LocalLambdaInvokeService(lambda_runner=lambda_runner_mock, + port=3000, + host='localhost', + stderr=stderr_mock) result = service._invoke_request_handler(function_name='HelloWorld') diff --git a/tests/unit/local/services/test_localhost_runner.py b/tests/unit/local/services/test_base_local_service.py similarity index 75% rename from tests/unit/local/services/test_localhost_runner.py rename to tests/unit/local/services/test_base_local_service.py index c8ecc5384a..428b3f3f27 100644 --- a/tests/unit/local/services/test_localhost_runner.py +++ b/tests/unit/local/services/test_base_local_service.py @@ -3,45 +3,43 @@ from parameterized import parameterized, param -from samcli.local.services.localhost_runner import LocalhostRunner, LambdaOutputParser +from samcli.local.services.base_local_service import BaseLocalService, LambdaOutputParser class TestLocalHostRunner(TestCase): def test_runtime_error_raised_when_app_not_created(self): - lambda_runner_mock = Mock() - service = LocalhostRunner(lambda_runner=lambda_runner_mock, port=3000, host='127.0.0.1') + is_debugging = False + service = BaseLocalService(is_debugging=is_debugging, port=3000, host='127.0.0.1') with self.assertRaises(RuntimeError): service.run() def test_run_starts_service_multithreaded(self): - lambda_runner_mock = Mock() - service = LocalhostRunner(lambda_runner=lambda_runner_mock, port=3000, host='127.0.0.1') + is_debugging = False # multithreaded + service = BaseLocalService(is_debugging=is_debugging, port=3000, host='127.0.0.1') service._app = Mock() app_run_mock = Mock() service._app.run = app_run_mock - lambda_runner_mock.is_debugging.return_value = False # multithreaded 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): - lambda_runner_mock = Mock() - service = LocalhostRunner(lambda_runner=lambda_runner_mock, port=3000, host='127.0.0.1') + is_debugging = True # singlethreaded + service = BaseLocalService(is_debugging=is_debugging, port=3000, host='127.0.0.1') service._app = Mock() app_run_mock = Mock() service._app.run = app_run_mock - lambda_runner_mock.is_debugging.return_value = True # single threaded service.run() app_run_mock.assert_called_once_with(threaded=False, host='127.0.0.1', port=3000) - @patch('samcli.local.services.localhost_runner.Response') + @patch('samcli.local.services.base_local_service.Response') def test_service_response(self, flask_response_patch): flask_response_mock = Mock() @@ -51,7 +49,7 @@ def test_service_response(self, flask_response_patch): status_code = 200 headers = {"Content-Type": "application/json"} - actual_response = LocalhostRunner._service_response(body, headers, status_code) + actual_response = BaseLocalService._service_response(body, headers, status_code) flask_response_patch.assert_called_once_with("this is the body") @@ -59,8 +57,8 @@ def test_service_response(self, flask_response_patch): self.assertEquals(actual_response.headers, {"Content-Type": "application/json"}) def test_create_returns_not_implemented(self): - lambda_runner_mock = Mock() - service = LocalhostRunner(lambda_runner=lambda_runner_mock, port=3000, host='127.0.0.1') + is_debugging = False + service = BaseLocalService(is_debugging=is_debugging, port=3000, host='127.0.0.1') with self.assertRaises(NotImplementedError): service.create()