diff --git a/setup.py b/setup.py index 8063172..48ffd1c 100644 --- a/setup.py +++ b/setup.py @@ -58,7 +58,7 @@ "dapr>=1.10.0", "aiohttp==3.8.4", "dapr-ext-grpc>=1.10.0", - "dapr-ext-fastapi>=1.10.0" + "dapr-ext-fastapi>=1.10.0", ], entry_points={ "console_scripts": [ diff --git a/src/functions_framework/_cli.py b/src/functions_framework/_cli.py index 0dfc379..4120098 100644 --- a/src/functions_framework/_cli.py +++ b/src/functions_framework/_cli.py @@ -26,7 +26,7 @@ @click.option("--dry-run", envvar="DRY_RUN", is_flag=True) def _cli(target, source, host, port, debug, dry_run): # fetch the context - context = _function_registry.get_openfunction_context('') + context = _function_registry.get_openfunction_context("") runner = Runner(context, target, source, host, port, debug, dry_run) runner.run() @@ -35,4 +35,4 @@ def _cli(target, source, host, port, debug, dry_run): def run_dry(target, host, port): click.echo("Function: {}".format(target)) click.echo("URL: http://{}:{}/".format(host, port)) - click.echo("Dry run successful, shutting down.") \ No newline at end of file + click.echo("Dry run successful, shutting down.") diff --git a/src/functions_framework/_function_registry.py b/src/functions_framework/_function_registry.py index 4759b78..e02b22a 100644 --- a/src/functions_framework/_function_registry.py +++ b/src/functions_framework/_function_registry.py @@ -71,8 +71,11 @@ def get_user_function(source, source_module, target): raise InvalidFunctionSignatureException( "The function defined in file {source} as {target} needs to be of " "function signature {signature}, but got {target_signature}".format( - source=source, target=target, signature=FUNCTION_SIGNATURE_RULE, - target_signature=inspect.signature(function)) + source=source, + target=target, + signature=FUNCTION_SIGNATURE_RULE, + target_signature=inspect.signature(function), + ) ) return function @@ -142,13 +145,10 @@ def get_func_signature_type(func_name: str, signature_type: str) -> str: def get_openfunction_context(func_context: str) -> FunctionContext: """Get openfunction context""" - context_str = ( - func_context - or os.environ.get(FUNC_CONTEXT) - ) + context_str = func_context or os.environ.get(FUNC_CONTEXT) if context_str: context = FunctionContext.from_json(json.loads(context_str)) return context - + return None diff --git a/src/functions_framework/context/function_context.py b/src/functions_framework/context/function_context.py index 687c960..21f192a 100644 --- a/src/functions_framework/context/function_context.py +++ b/src/functions_framework/context/function_context.py @@ -17,9 +17,20 @@ class FunctionContext(object): """OpenFunction's serving context.""" - def __init__(self, name="", version="", dapr_triggers=None, http_trigger=None, - inputs=None, outputs=None, states=None, - pre_hooks=None, post_hooks=None, tracing=None, port=0): + def __init__( + self, + name="", + version="", + dapr_triggers=None, + http_trigger=None, + inputs=None, + outputs=None, + states=None, + pre_hooks=None, + post_hooks=None, + tracing=None, + port=0, + ): self.name = name self.version = version self.dapr_triggers = dapr_triggers @@ -34,17 +45,17 @@ def __init__(self, name="", version="", dapr_triggers=None, http_trigger=None, @staticmethod def from_json(json_dct): - name = json_dct.get('name') - version = json_dct.get('version') - inputs_map = json_dct.get('inputs') - outputs_map = json_dct.get('outputs') - _dapr_triggers = json_dct.get('triggers', {}).get('dapr', []) - http_trigger = json_dct.get('triggers', {}).get('http', None) - states = json_dct.get('states', {}) - pre_hooks = json_dct.get('pre_hooks', []) - post_hooks = json_dct.get('post_hooks', []) - tracing = json_dct.get('tracing', {}) - port = json_dct.get('port', 0) + name = json_dct.get("name") + version = json_dct.get("version") + inputs_map = json_dct.get("inputs") + outputs_map = json_dct.get("outputs") + _dapr_triggers = json_dct.get("triggers", {}).get("dapr", []) + http_trigger = json_dct.get("triggers", {}).get("http", None) + states = json_dct.get("states", {}) + pre_hooks = json_dct.get("pre_hooks", []) + post_hooks = json_dct.get("post_hooks", []) + tracing = json_dct.get("tracing", {}) + port = json_dct.get("port", 0) inputs = None if inputs_map: @@ -67,14 +78,32 @@ def from_json(json_dct): if http_trigger: http_trigger = HTTPRoute.from_json(http_trigger) - return FunctionContext(name, version, dapr_triggers, http_trigger, - inputs, outputs, states, pre_hooks, post_hooks, tracing, port) + return FunctionContext( + name, + version, + dapr_triggers, + http_trigger, + inputs, + outputs, + states, + pre_hooks, + post_hooks, + tracing, + port, + ) class Component(object): """Components for inputs and outputs.""" - def __init__(self, component_name="", component_type="", topic="", metadata=None, operation=""): + def __init__( + self, + component_name="", + component_type="", + topic="", + metadata=None, + operation="", + ): self.topic = topic self.component_name = component_name self.component_type = component_type @@ -91,21 +120,24 @@ def get_type(self): return "" def __str__(self): - return "{component_name: %s, component_type: %s, topic: %s, metadata: %s, operation: %s}" % ( - self.component_name, - self.component_type, - self.topic, - self.metadata, - self.operation + return ( + "{component_name: %s, component_type: %s, topic: %s, metadata: %s, operation: %s}" + % ( + self.component_name, + self.component_type, + self.topic, + self.metadata, + self.operation, + ) ) @staticmethod def from_json(json_dct): - topic = json_dct.get('topic', '') - component_name = json_dct.get('componentName', '') - metadata = json_dct.get('metadata') - component_type = json_dct.get('componentType', '') - operation = json_dct.get('operation', '') + topic = json_dct.get("topic", "") + component_name = json_dct.get("componentName", "") + metadata = json_dct.get("metadata") + component_type = json_dct.get("componentType", "") + operation = json_dct.get("operation", "") return Component(component_name, component_type, topic, metadata, operation) @@ -116,18 +148,15 @@ def __init__(self, port=""): self.port = port def __str__(self): - return "{port: %s}" % ( - self.port - ) + return "{port: %s}" % (self.port) @staticmethod def from_json(json_dct): - port = json_dct.get('port', '') + port = json_dct.get("port", "") return HTTPRoute(port) class DaprTrigger(object): - def __init__(self, name, component_type, topic): self.name = name self.component_type = component_type @@ -137,12 +166,12 @@ def __str__(self): return "{name: %s, component_type: %s, topic: %s}" % ( self.name, self.component_type, - self.topic + self.topic, ) @staticmethod def from_json(json_dct): - name = json_dct.get('name', '') - component_type = json_dct.get('type', '') - topic = json_dct.get('topic') + name = json_dct.get("name", "") + component_type = json_dct.get("type", "") + topic = json_dct.get("topic") return DaprTrigger(name, component_type, topic) diff --git a/src/functions_framework/context/user_context.py b/src/functions_framework/context/user_context.py index 4d543d4..022501d 100644 --- a/src/functions_framework/context/user_context.py +++ b/src/functions_framework/context/user_context.py @@ -25,8 +25,14 @@ class UserContext(object): """Context for user.""" - def __init__(self, runtime_context: RuntimeContext = None, - binding_request=None, topic_event=None, http_request=None, logger=None): + def __init__( + self, + runtime_context: RuntimeContext = None, + binding_request=None, + topic_event=None, + http_request=None, + logger=None, + ): self.runtime_context = runtime_context self.logger = logger self.out = FunctionOut(0, None, "", {}) @@ -73,11 +79,17 @@ def send(self, output_name, data): target = outputs[output_name] if target.component_type.startswith(constants.DAPR_BINDING_TYPE): - resp = self.dapr_client.invoke_binding(target.component_name, target.operation, data, target.metadata) + resp = self.dapr_client.invoke_binding( + target.component_name, target.operation, data, target.metadata + ) elif target.component_type.startswith(constants.DAPR_PUBSUB_TYPE): data = json.dumps(data) resp = self.dapr_client.publish_event( - target.component_name, target.topic, data, - data_content_type=constants.DEFAULT_DATA_CONTENT_TYPE, publish_metadata=target.metadata) + target.component_name, + target.topic, + data, + data_content_type=constants.DEFAULT_DATA_CONTENT_TYPE, + publish_metadata=target.metadata, + ) return resp diff --git a/src/functions_framework/log.py b/src/functions_framework/log.py index 477e0a5..36ca743 100644 --- a/src/functions_framework/log.py +++ b/src/functions_framework/log.py @@ -31,7 +31,9 @@ def initialize_logger(name=None, level=logging.DEBUG): console_handler.setLevel(level) # create formatter - formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + ) # add formatter to handlers file_handler.setFormatter(formatter) diff --git a/src/functions_framework/runner.py b/src/functions_framework/runner.py index 6ad12ea..67d18a0 100644 --- a/src/functions_framework/runner.py +++ b/src/functions_framework/runner.py @@ -25,8 +25,16 @@ class Runner: - def __init__(self, context: FunctionContext, target=None, source=None, - host=None, port=None, debug=None, dry_run=None): + def __init__( + self, + context: FunctionContext, + target=None, + source=None, + host=None, + port=None, + debug=None, + dry_run=None, + ): self.target = target self.source = source self.context = context @@ -54,7 +62,9 @@ def load_user_function(self): source_module, spec = _function_registry.load_function_module(_source) spec.loader.exec_module(source_module) - self.user_function = _function_registry.get_user_function(_source, source_module, _target) + self.user_function = _function_registry.get_user_function( + _source, source_module, _target + ) def init_logger(self): level = logging.INFO @@ -68,10 +78,18 @@ def run(self): _trigger = runtime_context.get_http_trigger() if _trigger: - http_trigger = HTTPTriggerHandler(self.context.port, _trigger, self.source, self.target, self.user_function) + http_trigger = HTTPTriggerHandler( + self.context.port, + _trigger, + self.source, + self.target, + self.user_function, + ) http_trigger.start(runtime_context, logger=self.logger) _triggers = runtime_context.get_dapr_triggers() if _triggers: - dapr_trigger = DaprTriggerHandler(self.context.port, _triggers, self.user_function) + dapr_trigger = DaprTriggerHandler( + self.context.port, _triggers, self.user_function + ) dapr_trigger.start(runtime_context, logger=self.logger) diff --git a/src/functions_framework/triggers/dapr_trigger/dapr.py b/src/functions_framework/triggers/dapr_trigger/dapr.py index 01a8897..ad474f9 100644 --- a/src/functions_framework/triggers/dapr_trigger/dapr.py +++ b/src/functions_framework/triggers/dapr_trigger/dapr.py @@ -25,6 +25,7 @@ class DaprTriggerHandler(TriggerHandler): """Handle dapr trigger.""" + def __init__(self, port, triggers: [DaprTrigger] = None, user_function=None): self.port = port self.triggers = triggers @@ -39,17 +40,23 @@ def start(self, context: RuntimeContext, logger=None): for trigger in self.triggers: if trigger.component_type.startswith("bindings"): + @self.app.binding(trigger.name) def binding_handler(request: BindingRequest): rt_ctx = deepcopy(context) - user_ctx = UserContext(runtime_context=rt_ctx, binding_request=request, logger=logger) + user_ctx = UserContext( + runtime_context=rt_ctx, binding_request=request, logger=logger + ) self.user_function(user_ctx) if trigger.component_type.startswith("pubsub"): + @self.app.subscribe(pubsub_name=trigger.name, topic=trigger.topic) def topic_handler(event: v1.Event): rt_ctx = deepcopy(context) - user_ctx = UserContext(runtime_context=rt_ctx, topic_event=event, logger=logger) + user_ctx = UserContext( + runtime_context=rt_ctx, topic_event=event, logger=logger + ) self.user_function(user_ctx) self.app.run(self.port) diff --git a/src/functions_framework/triggers/http_trigger/__init__.py b/src/functions_framework/triggers/http_trigger/__init__.py index f60f269..8e9bb1e 100644 --- a/src/functions_framework/triggers/http_trigger/__init__.py +++ b/src/functions_framework/triggers/http_trigger/__init__.py @@ -89,7 +89,9 @@ def _http_view_func_wrapper(function, runtime_context: RuntimeContext, request, @functools.wraps(function) def view_func(path): rt_ctx = deepcopy(runtime_context) - user_ctx = UserContext(runtime_context=rt_ctx, http_request=request, logger=logger) + user_ctx = UserContext( + runtime_context=rt_ctx, http_request=request, logger=logger + ) return function(user_ctx) return view_func @@ -102,7 +104,9 @@ def _configure_app(wsgi_app, runtime_context: RuntimeContext, function, logger): wsgi_app.url_map.add(werkzeug.routing.Rule("/robots.txt", endpoint="error")) wsgi_app.url_map.add(werkzeug.routing.Rule("/favicon.ico", endpoint="error")) wsgi_app.url_map.add(werkzeug.routing.Rule("/", endpoint="run")) - wsgi_app.view_functions["run"] = _http_view_func_wrapper(function, runtime_context, flask.request, logger) + wsgi_app.view_functions["run"] = _http_view_func_wrapper( + function, runtime_context, flask.request, logger + ) wsgi_app.view_functions["error"] = lambda: flask.abort(404, description="Not Found") wsgi_app.after_request(read_request) @@ -127,7 +131,9 @@ def crash_handler(e): return str(e), 500, {_FUNCTION_STATUS_HEADER_FIELD: _CRASH} -def create_app(runtime_context: RuntimeContext = None, target=None, source=None, logger=None): +def create_app( + runtime_context: RuntimeContext = None, target=None, source=None, logger=None +): _target = _function_registry.get_function_target(target) _source = _function_registry.get_function_source(source) diff --git a/src/functions_framework/triggers/http_trigger/_http/__init__.py b/src/functions_framework/triggers/http_trigger/_http/__init__.py index 6f7694b..045429c 100644 --- a/src/functions_framework/triggers/http_trigger/_http/__init__.py +++ b/src/functions_framework/triggers/http_trigger/_http/__init__.py @@ -40,4 +40,4 @@ def run(self, host, port): def create_server(wsgi_app, debug, **options): - return HTTPServer(wsgi_app, debug, **options) \ No newline at end of file + return HTTPServer(wsgi_app, debug, **options) diff --git a/src/functions_framework/triggers/http_trigger/_http/flask.py b/src/functions_framework/triggers/http_trigger/_http/flask.py index 8cc5987..b2edf56 100644 --- a/src/functions_framework/triggers/http_trigger/_http/flask.py +++ b/src/functions_framework/triggers/http_trigger/_http/flask.py @@ -22,4 +22,4 @@ def __init__(self, app, host, port, debug, **options): self.options = options def run(self): - self.app.run(self.host, self.port, debug=self.debug, **self.options) \ No newline at end of file + self.app.run(self.host, self.port, debug=self.debug, **self.options) diff --git a/src/functions_framework/triggers/http_trigger/http.py b/src/functions_framework/triggers/http_trigger/http.py index cf92ecb..bc6dd63 100644 --- a/src/functions_framework/triggers/http_trigger/http.py +++ b/src/functions_framework/triggers/http_trigger/http.py @@ -23,7 +23,16 @@ class HTTPTriggerHandler(TriggerHandler): """Handle http trigger.""" - def __init__(self, port, trigger: HTTPRoute, source=None, target=None, user_function=None, debug=False): + + def __init__( + self, + port, + trigger: HTTPRoute, + source=None, + target=None, + user_function=None, + debug=False, + ): self.port = trigger.port if trigger.port else port self.source = source self.target = target diff --git a/src/functions_framework/triggers/trigger.py b/src/functions_framework/triggers/trigger.py index 8edf907..2c4e129 100644 --- a/src/functions_framework/triggers/trigger.py +++ b/src/functions_framework/triggers/trigger.py @@ -20,4 +20,3 @@ class TriggerHandler(ABC): @abstractmethod def start(self, context: RuntimeContext): pass - diff --git a/tests/conformance/main.py b/tests/conformance/main.py deleted file mode 100644 index e790f62..0000000 --- a/tests/conformance/main.py +++ /dev/null @@ -1,48 +0,0 @@ -import json - -from cloudevents.http import to_json - -import functions_framework - -filename = "function_output.json" - - -def _write_output(content): - with open(filename, "w") as f: - f.write(content) - - -def write_http(request): - _write_output(json.dumps(request.json)) - return "OK", 200 - - -def write_legacy_event(data, context): - _write_output( - json.dumps( - { - "data": data, - "context": { - "eventId": context.event_id, - "timestamp": context.timestamp, - "eventType": context.event_type, - "resource": context.resource, - }, - } - ) - ) - - -def write_cloud_event(cloud_event): - _write_output(to_json(cloud_event).decode()) - - -@functions_framework.http -def write_http_declarative(request): - _write_output(json.dumps(request.json)) - return "OK", 200 - - -@functions_framework.cloud_event -def write_cloud_event_declarative(cloud_event): - _write_output(to_json(cloud_event).decode()) diff --git a/tests/test_async.py b/tests/test_async.py deleted file mode 100644 index edd321e..0000000 --- a/tests/test_async.py +++ /dev/null @@ -1,183 +0,0 @@ -import random -import json -import subprocess -import threading -import time -import os -import re -import pytest - -from paho.mqtt import client as mqtt_client - -from functions_framework.openfunction import FunctionContext -from functions_framework.openfunction import AsyncApp - -TEST_PAYLOAD = {"data": "hello world"} -APP_ID="async.dapr" -TEST_CONTEXT = { - "name": "test-context", - "version": "1.0.0", - "runtime": "Async", - "port": "8080", - "inputs": { - "cron": { - "uri": "cron_input", - "componentName": "binding-cron", - "componentType": "bindings.cron" - }, - "mqtt_binding": { - "uri": "of-async-default", - "componentName": "binding-mqtt", - "componentType": "bindings.mqtt" - }, - "mqtt_sub": { - "uri": "of-async-default-sub", - "componentName": "pubsub-mqtt", - "componentType": "pubsub.mqtt" - } - }, - "outputs": { - "cron": { - "uri": 'cron_output', - "operation": 'delete', - "componentName": 'binding-cron', - "componentType": 'bindings.cron', - }, - "localfs": { - "operation": "create", - "componentName": "binding-localfs", - "componentType": "bindings.localstorage", - "metadata": { - "fileName": "output-file.txt" - } - }, - "localfs-delete": { - "operation": "delete", - "componentName": "binding-localfs", - "componentType": "bindings.localstorage", - "metadata": { - "fileName": "output-file.txt" - } - }, - "mqtt_pub": { - "uri": 'of-async-default-pub', - "componentName": 'pubsub-mqtt', - "componentType": 'pubsub.mqtt', - } - } -} - -CLIENT_ID = f'of-async-mqtt-{random.randint(0, 1000)}' -BROKER = 'broker.emqx.io' -MQTT_PORT = 1883 - - -@pytest.fixture(scope="module", autouse=True) -def hook(request): - subprocess.Popen( - "dapr run -G 50001 -d ./tests/test_data/components/async -a {} -p {} --app-protocol grpc".format(APP_ID, TEST_CONTEXT["port"]), - shell=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) - - yield request - - subprocess.Popen("dapr stop {}".format(APP_ID), shell=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) - time.sleep(10) - -@pytest.fixture -def client(): - return AsyncApp(FunctionContext.from_json(TEST_CONTEXT)) - - -@pytest.fixture -def mqtt_test_client(): - client = mqtt_client.Client(CLIENT_ID) - client.connect(BROKER, MQTT_PORT) - return client - - -def test_cron(client): - def cron(client): - def user_function(context, data): - assert context.runtime == TEST_CONTEXT["runtime"] - assert context.inputs["cron"].uri == TEST_CONTEXT["inputs"]["cron"]["uri"] - context.send("", "cron") - client.app.stop() - - return - return user_function - - client.bind(cron(client)) - client.app.run(TEST_CONTEXT["port"]) - - -def test_mqtt_binding(client, mqtt_test_client): - def binding(client): - def user_function(context, data): - context.send(data, "localfs") - filename = TEST_CONTEXT["outputs"]["localfs"]["metadata"]["fileName"] - exist = os.path.exists(filename) - assert exist - - context.send(data, "localfs-delete") - client.app.stop() - - return - return user_function - - client.bind(binding(client)) - - def loop(): - client.app.run(TEST_CONTEXT["port"]) - - t = threading.Thread(target=loop, name='LoopThread') - t.start() - - time.sleep(10) - mqtt_test_client.publish("of-async-default", payload=json.dumps(TEST_PAYLOAD).encode('utf-8')) - - t.join() - - -def test_mqtt_subscribe(client, mqtt_test_client): - def binding(client): - def user_function(context, data): - output = 'mqtt_pub' - # subscribe from mqtt_pub - def on_message(client, userdata, msg): - assert msg.payload == json.dumps(TEST_PAYLOAD).encode('utf-8') - - print(msg.payload.decode('utf-8')) - - mqtt_test_client.subscribe(TEST_CONTEXT["outputs"]["mqtt_pub"]["uri"]) - mqtt_test_client.on_message = on_message - mqtt_test_client.loop_start() - - context.send(json.dumps(TEST_PAYLOAD), output) - - time.sleep(5) - mqtt_test_client.loop_stop() - - client.app.stop() - time.sleep(5) - - return - return user_function - - client.bind(binding(client)) - - def loop(): - client.app.run(TEST_CONTEXT["port"]) - - t = threading.Thread(target=loop, name='LoopThread') - t.start() - - time.sleep(10) - - formatted = re.sub(r'"', '\\"', json.dumps(TEST_PAYLOAD)) - subprocess.Popen("dapr publish -i {} -p {} -t {} -d '{}'".format( - APP_ID, TEST_CONTEXT["inputs"]["mqtt_sub"]["componentName"], - TEST_CONTEXT["inputs"]["mqtt_sub"]["uri"], - formatted - ), shell=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) - - t.join() \ No newline at end of file diff --git a/tests/test_binding.py b/tests/test_binding.py deleted file mode 100644 index 4ae9f90..0000000 --- a/tests/test_binding.py +++ /dev/null @@ -1,76 +0,0 @@ -import pathlib -import os -import subprocess -import time - -import pytest - -from functions_framework import create_app -from functions_framework.openfunction import FunctionContext - -TEST_FUNCTIONS_DIR = pathlib.Path(__file__).resolve().parent / "test_functions" -TEST_RESPONSE = "Hello world!" -FILENAME = "test-binding.txt" -APP_ID="http.dapr" - - -@pytest.fixture(scope="module", autouse=True) -def dapr(request): - subprocess.Popen("dapr run -G 50001 -d ./tests/test_data/components/http -a {}".format(APP_ID), - shell=True) - time.sleep(3) - - yield request - - subprocess.Popen("dapr stop {}".format(APP_ID), shell=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) - time.sleep(10) - - -def create_func_context(param): - return { - "name": param["name"], - "version": "1.0.0", - "runtime": "Knative", - "outputs": { - "file": { - "componentName": "local", - "componentType": "bindings.localstorage", - "operation": param["operation"], - "metadata": { - "fileName": FILENAME - } - } - } - } - - -@pytest.fixture -def client(): - def return_client(param): - source = TEST_FUNCTIONS_DIR / "http_basic" / "main.py" - target = "hello" - - context = create_func_context(param) - - return create_app(target, source, "http", FunctionContext.from_json(context)).test_client() - - return return_client - - -test_data = [ - {"name": "Save data", "operation": "create", "listable": True}, - {"name": "Get data", "operation": "get", "listable": True}, - {"name": "Delete data", "operation": "delete", "listable": False}, -] - - -@pytest.mark.parametrize("test_data", test_data) -def test_http_binding(client, test_data): - resp = client(test_data).get("/") - - assert resp.status_code == 200 - assert TEST_RESPONSE == resp.get_data().decode() - - exist = os.path.exists(FILENAME) - - assert exist == test_data["listable"] \ No newline at end of file diff --git a/tests/test_cli.py b/tests/test_cli.py index 8332ba7..d53dc72 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,4 +1,4 @@ -# Copyright 2020 Google LLC +# Copyright 2023 The OpenFunction Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,107 +11,41 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import pathlib +import unittest -import pretend -import pytest +from unittest import mock -from click.testing import CliRunner +from functions_framework._cli import _cli -import functions_framework -from functions_framework._cli import _cli +class TestCLI(unittest.TestCase): + TEST_FUNCTIONS_DIR = pathlib.Path(__file__).resolve().parent / "test_data" + source = TEST_FUNCTIONS_DIR / "test_source.py" + + args = [ + "--target", "test_function", + "--source", source, + "--host", "localhost", + "--port", "8081", + "--debug" + ] + @mock.patch('sys.argv', ['functions-framework'] + args) + def test_main_with_function(self): + with self.assertRaises(SystemExit) as cm: + _cli() -def test_cli_no_arguments(): - runner = CliRunner() - result = runner.invoke(_cli) + self.assertEqual(cm.exception.code, 0) - assert result.exit_code == 2 - assert "Missing option '--target'" in result.output + @mock.patch('sys.argv', ['functions-framework']) + def test_main_without_function(self): + with self.assertRaises(SystemExit) as cm: + _cli() + self.assertEqual(cm.exception.code, 2) -@pytest.mark.parametrize( - "args, env, create_app_calls, run_calls", - [ - ( - ["--target", "foo"], - {}, - [pretend.call("foo", None, "http", None, False)], - [pretend.call("0.0.0.0", 8080)], - ), - ( - [], - {"FUNCTION_TARGET": "foo"}, - [pretend.call("foo", None, "http", None, False)], - [pretend.call("0.0.0.0", 8080)], - ), - ( - ["--target", "foo", "--source", "/path/to/source.py"], - {}, - [pretend.call("foo", "/path/to/source.py", "http", None, False)], - [pretend.call("0.0.0.0", 8080)], - ), - ( - [], - {"FUNCTION_TARGET": "foo", "FUNCTION_SOURCE": "/path/to/source.py"}, - [pretend.call("foo", "/path/to/source.py", "http", None, False)], - [pretend.call("0.0.0.0", 8080)], - ), - ( - ["--target", "foo", "--signature-type", "event"], - {}, - [pretend.call("foo", None, "event", None, False)], - [pretend.call("0.0.0.0", 8080)], - ), - ( - [], - {"FUNCTION_TARGET": "foo", "FUNCTION_SIGNATURE_TYPE": "event"}, - [pretend.call("foo", None, "event", None, False)], - [pretend.call("0.0.0.0", 8080)], - ), - ( - ["--target", "foo", "--dry-run"], - {}, - [pretend.call("foo", None, "http", None, False)], - [], - ), - ( - [], - {"FUNCTION_TARGET": "foo", "DRY_RUN": "True"}, - [pretend.call("foo", None, "http", None, False)], - [], - ), - ( - ["--target", "foo", "--host", "127.0.0.1"], - {}, - [pretend.call("foo", None, "http", None, False)], - [pretend.call("127.0.0.1", 8080)], - ), - ( - ["--target", "foo", "--debug"], - {}, - [pretend.call("foo", None, "http", None, True)], - [pretend.call("0.0.0.0", 8080)], - ), - ( - [], - {"FUNCTION_TARGET": "foo", "DEBUG": "True"}, - [pretend.call("foo", None, "http", None, True)], - [pretend.call("0.0.0.0", 8080)], - ), - ], -) -def test_cli(monkeypatch, args, env, create_app_calls, run_calls): - wsgi_server = pretend.stub(run=pretend.call_recorder(lambda *a, **kw: None)) - wsgi_app = pretend.stub(run=pretend.call_recorder(lambda *a, **kw: None)) - create_app = pretend.call_recorder(lambda *a, **kw: wsgi_app) - monkeypatch.setattr(functions_framework._cli, "create_app", create_app) - create_server = pretend.call_recorder(lambda *a, **kw: wsgi_server) - monkeypatch.setattr(functions_framework._cli, "create_server", create_server) - runner = CliRunner(env=env) - result = runner.invoke(_cli, args) +if __name__ == '__main__': + unittest.main() - assert result.exit_code == 0 - assert create_app.calls == create_app_calls - assert wsgi_server.run.calls == run_calls diff --git a/tests/test_cloud_event_functions.py b/tests/test_cloud_event_functions.py deleted file mode 100644 index a673c6e..0000000 --- a/tests/test_cloud_event_functions.py +++ /dev/null @@ -1,231 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import json -import pathlib - -import pytest - -from cloudevents.http import CloudEvent, to_binary, to_structured - -from functions_framework import create_app - -TEST_FUNCTIONS_DIR = pathlib.Path(__file__).resolve().parent / "test_functions" -TEST_DATA_DIR = pathlib.Path(__file__).resolve().parent / "test_data" - -# Python 3.5: ModuleNotFoundError does not exist -try: - _ModuleNotFoundError = ModuleNotFoundError -except: - _ModuleNotFoundError = ImportError - - -@pytest.fixture -def data_payload(): - return {"name": "john"} - - -@pytest.fixture -def cloud_event_1_0(): - attributes = { - "specversion": "1.0", - "id": "my-id", - "source": "from-galaxy-far-far-away", - "type": "cloud_event.greet.you", - "time": "2020-08-16T13:58:54.471765", - } - data = {"name": "john"} - return CloudEvent(attributes, data) - - -@pytest.fixture -def cloud_event_0_3(): - attributes = { - "id": "my-id", - "source": "from-galaxy-far-far-away", - "type": "cloud_event.greet.you", - "specversion": "0.3", - "time": "2020-08-16T13:58:54.471765", - } - data = {"name": "john"} - return CloudEvent(attributes, data) - - -@pytest.fixture -def create_headers_binary(): - return lambda specversion: { - "ce-id": "my-id", - "ce-source": "from-galaxy-far-far-away", - "ce-type": "cloud_event.greet.you", - "ce-specversion": specversion, - "time": "2020-08-16T13:58:54.471765", - } - - -@pytest.fixture -def create_structured_data(): - return lambda specversion: { - "id": "my-id", - "source": "from-galaxy-far-far-away", - "type": "cloud_event.greet.you", - "specversion": specversion, - "time": "2020-08-16T13:58:54.471765", - } - - -@pytest.fixture -def background_event(): - with open(TEST_DATA_DIR / "pubsub_text-legacy-input.json", "r") as f: - return json.load(f) - - -@pytest.fixture -def client(): - source = TEST_FUNCTIONS_DIR / "cloud_events" / "main.py" - target = "function" - return create_app(target, source, "cloudevent").test_client() - - -@pytest.fixture -def empty_client(): - source = TEST_FUNCTIONS_DIR / "cloud_events" / "empty_data.py" - target = "function" - return create_app(target, source, "cloudevent").test_client() - - -@pytest.fixture -def converted_background_event_client(): - source = TEST_FUNCTIONS_DIR / "cloud_events" / "converted_background_event.py" - target = "function" - return create_app(target, source, "cloudevent").test_client() - - -def test_event(client, cloud_event_1_0): - headers, data = to_structured(cloud_event_1_0) - resp = client.post("/", headers=headers, data=data) - - assert resp.status_code == 200 - assert resp.data == b"OK" - - -def test_binary_event(client, cloud_event_1_0): - headers, data = to_binary(cloud_event_1_0) - resp = client.post("/", headers=headers, data=data) - - assert resp.status_code == 200 - assert resp.data == b"OK" - - -def test_event_0_3(client, cloud_event_0_3): - headers, data = to_structured(cloud_event_0_3) - resp = client.post("/", headers=headers, data=data) - - assert resp.status_code == 200 - assert resp.data == b"OK" - - -def test_binary_event_0_3(client, cloud_event_0_3): - headers, data = to_binary(cloud_event_0_3) - resp = client.post("/", headers=headers, data=data) - - assert resp.status_code == 200 - assert resp.data == b"OK" - - -@pytest.mark.parametrize("specversion", ["0.3", "1.0"]) -def test_cloud_event_missing_required_binary_fields( - client, specversion, create_headers_binary, data_payload -): - headers = create_headers_binary(specversion) - - for remove_key in headers: - if remove_key == "time": - continue - - invalid_headers = {key: headers[key] for key in headers if key != remove_key} - resp = client.post("/", headers=invalid_headers, json=data_payload) - - assert resp.status_code == 400 - assert "MissingRequiredFields" in resp.get_data().decode() - - -@pytest.mark.parametrize("specversion", ["0.3", "1.0"]) -def test_cloud_event_missing_required_structured_fields( - client, specversion, create_structured_data -): - headers = {"Content-Type": "application/cloudevents+json"} - data = create_structured_data(specversion) - - for remove_key in data: - if remove_key == "time": - continue - - invalid_data = {key: data[key] for key in data if key != remove_key} - resp = client.post("/", headers=headers, json=invalid_data) - - assert resp.status_code == 400 - assert "MissingRequiredFields" in resp.data.decode() - - -def test_invalid_fields_binary(client, create_headers_binary, data_payload): - # Testing none specversion fails - headers = create_headers_binary("not a spec version") - resp = client.post("/", headers=headers, json=data_payload) - - assert resp.status_code == 400 - assert "InvalidRequiredFields" in resp.data.decode() - - -def test_unparsable_cloud_event(client): - resp = client.post("/", headers={}, data="") - - assert resp.status_code == 400 - assert "Bad Request" in resp.data.decode() - - -@pytest.mark.parametrize("specversion", ["0.3", "1.0"]) -def test_empty_data_binary(empty_client, create_headers_binary, specversion): - headers = create_headers_binary(specversion) - resp = empty_client.post("/", headers=headers, json="") - - assert resp.status_code == 200 - assert resp.get_data() == b"OK" - - -@pytest.mark.parametrize("specversion", ["0.3", "1.0"]) -def test_empty_data_structured(empty_client, specversion, create_structured_data): - headers = {"Content-Type": "application/cloudevents+json"} - - data = create_structured_data(specversion) - resp = empty_client.post("/", headers=headers, json=data) - - assert resp.status_code == 200 - assert resp.get_data() == b"OK" - - -@pytest.mark.parametrize("specversion", ["0.3", "1.0"]) -def test_no_mime_type_structured(empty_client, specversion, create_structured_data): - data = create_structured_data(specversion) - resp = empty_client.post("/", headers={}, json=data) - - assert resp.status_code == 200 - assert resp.get_data() == b"OK" - - -def test_background_event(converted_background_event_client, background_event): - resp = converted_background_event_client.post( - "/", headers={}, json=background_event - ) - - assert resp.status_code == 200 - assert resp.get_data() == b"OK" diff --git a/tests/test_convert.py b/tests/test_convert.py deleted file mode 100644 index 6128cd5..0000000 --- a/tests/test_convert.py +++ /dev/null @@ -1,612 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import json -import pathlib - -import flask -import pretend -import pytest - -from cloudevents.http import from_json, to_binary - -from functions_framework import event_conversion -from functions_framework.exceptions import EventConversionException -from google_origin.cloud.functions.context import Context - -TEST_DATA_DIR = pathlib.Path(__file__).resolve().parent / "test_data" - - -PUBSUB_BACKGROUND_EVENT = { - "context": { - "eventId": "1215011316659232", - "timestamp": "2020-05-18T12:13:19Z", - "eventType": "google_origin.pubsub.topic.publish", - "resource": { - "service": "pubsub.googleapis.com", - "name": "projects/sample-project/topics/gcf-test", - "type": "type.googleapis.com/google_origin.pubsub.v1.PubsubMessage", - }, - }, - "data": { - "data": "10", - }, -} - -PUBSUB_BACKGROUND_EVENT_WITHOUT_CONTEXT = { - "eventId": "1215011316659232", - "timestamp": "2020-05-18T12:13:19Z", - "eventType": "providers/cloud.pubsub/eventTypes/topic.publish", - "resource": "projects/sample-project/topics/gcf-test", - "data": { - "data": "10", - }, -} - -BACKGROUND_RESOURCE = { - "service": "storage.googleapis.com", - "name": "projects/_/buckets/some-bucket/objects/folder/Test.cs", - "type": "storage#object", -} - -BACKGROUND_RESOURCE_WITHOUT_SERVICE = { - "name": "projects/_/buckets/some-bucket/objects/folder/Test.cs", - "type": "storage#object", -} - -BACKGROUND_RESOURCE_STRING = "projects/_/buckets/some-bucket/objects/folder/Test.cs" - -PUBSUB_CLOUD_EVENT = { - "specversion": "1.0", - "id": "1215011316659232", - "source": "//pubsub.googleapis.com/projects/sample-project/topics/gcf-test", - "time": "2020-05-18T12:13:19Z", - "type": "google_origin.cloud.pubsub.topic.v1.messagePublished", - "datacontenttype": "application/json", - "data": { - "message": { - "data": "10", - "publishTime": "2020-05-18T12:13:19Z", - "messageId": "1215011316659232", - }, - }, -} - - -@pytest.fixture -def pubsub_cloud_event_output(): - return from_json(json.dumps(PUBSUB_CLOUD_EVENT)) - - -@pytest.fixture -def raw_pubsub_request(): - return { - "subscription": "projects/sample-project/subscriptions/gcf-test-sub", - "message": { - "data": "eyJmb28iOiJiYXIifQ==", - "messageId": "1215011316659232", - "attributes": {"test": "123"}, - }, - } - - -@pytest.fixture -def marshalled_pubsub_request(): - return { - "data": { - "@type": "type.googleapis.com/google_origin.pubsub.v1.PubsubMessage", - "data": "eyJmb28iOiJiYXIifQ==", - "attributes": {"test": "123"}, - }, - "context": { - "eventId": "1215011316659232", - "eventType": "google_origin.pubsub.topic.publish", - "resource": { - "name": "projects/sample-project/topics/gcf-test", - "service": "pubsub.googleapis.com", - "type": "type.googleapis.com/google_origin.pubsub.v1.PubsubMessage", - }, - "timestamp": "2021-04-17T07:21:18.249Z", - }, - } - - -@pytest.fixture -def raw_pubsub_cloud_event_output(marshalled_pubsub_request): - event = PUBSUB_CLOUD_EVENT.copy() - # the data payload is more complex for the raw pubsub request - data = marshalled_pubsub_request["data"] - data["messageId"] = event["id"] - data["publishTime"] = event["time"] - event["data"] = {"message": data} - return from_json(json.dumps(event)) - - -@pytest.fixture -def firebase_auth_background_input(): - with open(TEST_DATA_DIR / "firebase-auth-legacy-input.json", "r") as f: - return json.load(f) - - -@pytest.fixture -def firebase_auth_cloud_event_output(): - with open(TEST_DATA_DIR / "firebase-auth-cloud-event-output.json", "r") as f: - return from_json(f.read()) - - -@pytest.fixture -def firebase_db_background_input(): - with open(TEST_DATA_DIR / "firebase-db-legacy-input.json", "r") as f: - return json.load(f) - - -@pytest.fixture -def firebase_db_cloud_event_output(): - with open(TEST_DATA_DIR / "firebase-db-cloud-event-output.json", "r") as f: - return from_json(f.read()) - - -@pytest.fixture -def create_ce_headers(): - return lambda event_type, source: { - "ce-id": "my-id", - "ce-type": event_type, - "ce-source": source, - "ce-specversion": "1.0", - "ce-subject": "my/subject", - "ce-time": "2020-08-16T13:58:54.471765", - } - - -@pytest.mark.parametrize( - "event", [PUBSUB_BACKGROUND_EVENT, PUBSUB_BACKGROUND_EVENT_WITHOUT_CONTEXT] -) -def test_pubsub_event_to_cloud_event(event, pubsub_cloud_event_output): - req = flask.Request.from_values(json=event) - cloud_event = event_conversion.background_event_to_cloud_event(req) - assert cloud_event == pubsub_cloud_event_output - - -def test_firebase_auth_event_to_cloud_event( - firebase_auth_background_input, firebase_auth_cloud_event_output -): - req = flask.Request.from_values(json=firebase_auth_background_input) - cloud_event = event_conversion.background_event_to_cloud_event(req) - assert cloud_event == firebase_auth_cloud_event_output - - -def test_firebase_auth_event_to_cloud_event_no_metadata( - firebase_auth_background_input, firebase_auth_cloud_event_output -): - # Remove metadata from the events to verify conversion still works. - del firebase_auth_background_input["data"]["metadata"] - del firebase_auth_cloud_event_output.data["metadata"] - - req = flask.Request.from_values(json=firebase_auth_background_input) - cloud_event = event_conversion.background_event_to_cloud_event(req) - assert cloud_event == firebase_auth_cloud_event_output - - -def test_firebase_auth_event_to_cloud_event_no_metadata_timestamps( - firebase_auth_background_input, firebase_auth_cloud_event_output -): - # Remove metadata timestamps from the events to verify conversion still works. - del firebase_auth_background_input["data"]["metadata"]["createdAt"] - del firebase_auth_background_input["data"]["metadata"]["lastSignedInAt"] - del firebase_auth_cloud_event_output.data["metadata"]["createTime"] - del firebase_auth_cloud_event_output.data["metadata"]["lastSignInTime"] - - req = flask.Request.from_values(json=firebase_auth_background_input) - cloud_event = event_conversion.background_event_to_cloud_event(req) - assert cloud_event == firebase_auth_cloud_event_output - - -def test_firebase_auth_event_to_cloud_event_no_uid( - firebase_auth_background_input, firebase_auth_cloud_event_output -): - # Remove UIDs from the events to verify conversion still works. The UID is mapped - # to the subject in the CloudEvent so remove that from the expected CloudEvent. - del firebase_auth_background_input["data"]["uid"] - del firebase_auth_cloud_event_output.data["uid"] - del firebase_auth_cloud_event_output["subject"] - - req = flask.Request.from_values(json=firebase_auth_background_input) - cloud_event = event_conversion.background_event_to_cloud_event(req) - assert cloud_event == firebase_auth_cloud_event_output - - -def test_firebase_db_event_to_cloud_event_default_location( - firebase_db_background_input, firebase_db_cloud_event_output -): - req = flask.Request.from_values(json=firebase_db_background_input) - cloud_event = event_conversion.background_event_to_cloud_event(req) - assert cloud_event == firebase_db_cloud_event_output - - -def test_firebase_db_event_to_cloud_event_location_subdomain( - firebase_db_background_input, firebase_db_cloud_event_output -): - firebase_db_background_input["domain"] = "europe-west1.firebasedatabase.app" - firebase_db_cloud_event_output["source"] = firebase_db_cloud_event_output[ - "source" - ].replace("us-central1", "europe-west1") - - req = flask.Request.from_values(json=firebase_db_background_input) - cloud_event = event_conversion.background_event_to_cloud_event(req) - assert cloud_event == firebase_db_cloud_event_output - - -def test_firebase_db_event_to_cloud_event_missing_domain( - firebase_db_background_input, firebase_db_cloud_event_output -): - del firebase_db_background_input["domain"] - req = flask.Request.from_values(json=firebase_db_background_input) - - with pytest.raises(EventConversionException) as exc_info: - event_conversion.background_event_to_cloud_event(req) - - assert ( - "Invalid FirebaseDB event payload: missing 'domain'" in exc_info.value.args[0] - ) - - -def test_marshal_background_event_data_bad_request(): - req = pretend.stub(headers={}, get_json=lambda: None) - - with pytest.raises(EventConversionException): - event_conversion.background_event_to_cloud_event(req) - - -@pytest.mark.parametrize( - "background_resource", - [ - BACKGROUND_RESOURCE, - BACKGROUND_RESOURCE_WITHOUT_SERVICE, - BACKGROUND_RESOURCE_STRING, - ], -) -def test_split_resource(background_resource): - context = Context( - eventType="google_origin.storage.object.finalize", resource=background_resource - ) - service, resource, subject = event_conversion._split_resource(context) - assert service == "storage.googleapis.com" - assert resource == "projects/_/buckets/some-bucket" - assert subject == "objects/folder/Test.cs" - - -def test_split_resource_unknown_service_and_event_type(): - # With both an unknown service and an unknown event type, we won't attempt any - # event type mapping or resource/subject splitting. - background_resource = { - "service": "not_a_known_service", - "name": "projects/_/my/stuff/at/test.txt", - "type": "storage#object", - } - context = Context(eventType="not_a_known_event_type", resource=background_resource) - service, resource, subject = event_conversion._split_resource(context) - assert service == "not_a_known_service" - assert resource == "projects/_/my/stuff/at/test.txt" - assert subject == "" - - -def test_split_resource_without_service_unknown_event_type(): - background_resource = { - "name": "projects/_/buckets/some-bucket/objects/folder/Test.cs", - "type": "storage#object", - } - # This event type cannot be mapped to an equivalent CloudEvent type. - context = Context(eventType="not_a_known_event_type", resource=background_resource) - with pytest.raises(EventConversionException) as exc_info: - event_conversion._split_resource(context) - assert "Unable to find CloudEvent equivalent service" in exc_info.value.args[0] - - -def test_split_resource_no_resource_regex_match(): - background_resource = { - "service": "storage.googleapis.com", - # This name will not match the regex associated with the service. - "name": "foo/bar/baz", - "type": "storage#object", - } - context = Context( - eventType="google_origin.storage.object.finalize", resource=background_resource - ) - with pytest.raises(EventConversionException) as exc_info: - event_conversion._split_resource(context) - assert "Resource regex did not match" in exc_info.value.args[0] - - -def test_marshal_background_event_data_without_topic_in_path( - raw_pubsub_request, marshalled_pubsub_request -): - req = flask.Request.from_values(json=raw_pubsub_request, path="/myfunc/") - payload = event_conversion.marshal_background_event_data(req) - - # Remove timestamps as they get generates on the fly - del marshalled_pubsub_request["context"]["timestamp"] - del payload["context"]["timestamp"] - - # Resource name is set to empty string when it cannot be parsed from the request path - marshalled_pubsub_request["context"]["resource"]["name"] = "" - - assert payload == marshalled_pubsub_request - - -def test_marshal_background_event_data_with_topic_path( - raw_pubsub_request, marshalled_pubsub_request -): - req = flask.Request.from_values( - json=raw_pubsub_request, - path="x/projects/sample-project/topics/gcf-test?pubsub_trigger=true", - ) - payload = event_conversion.marshal_background_event_data(req) - - # Remove timestamps as they are generated on the fly. - del marshalled_pubsub_request["context"]["timestamp"] - del payload["context"]["timestamp"] - - assert payload == marshalled_pubsub_request - - -@pytest.mark.parametrize( - "request_fixture, overrides", - [ - ( - "raw_pubsub_request", - { - "request_path": "x/projects/sample-project/topics/gcf-test?pubsub_trigger=true", - }, - ), - ("raw_pubsub_request", {"source": "//pubsub.googleapis.com/"}), - ("marshalled_pubsub_request", {}), - ], -) -def test_pubsub_emulator_request_to_cloud_event( - raw_pubsub_cloud_event_output, request_fixture, overrides, request -): - request_path = overrides.get("request_path", "/") - payload = request.getfixturevalue(request_fixture) - req = flask.Request.from_values( - path=request_path, - json=payload, - ) - cloud_event = event_conversion.background_event_to_cloud_event(req) - - # Remove timestamps as they are generated on the fly. - del raw_pubsub_cloud_event_output["time"] - del raw_pubsub_cloud_event_output.data["message"]["publishTime"] - del cloud_event["time"] - del cloud_event.data["message"]["publishTime"] - - if "source" in overrides: - # Default to the service name, when the topic is not configured subscription's pushEndpoint. - raw_pubsub_cloud_event_output["source"] = overrides["source"] - - assert cloud_event == raw_pubsub_cloud_event_output - - -def test_pubsub_emulator_request_with_invalid_message( - raw_pubsub_request, raw_pubsub_cloud_event_output -): - # Create an invalid message payload - raw_pubsub_request["message"] = None - req = flask.Request.from_values(json=raw_pubsub_request, path="/") - - with pytest.raises(EventConversionException) as exc_info: - cloud_event = event_conversion.background_event_to_cloud_event(req) - assert "Failed to convert Pub/Sub payload to event" in exc_info.value.args[0] - - -@pytest.mark.parametrize( - "ce_event_type, ce_source, expected_type, expected_resource", - [ - ( - "google_origin.firebase.database.ref.v1.written", - "//firebasedatabase.googleapis.com/projects/_/instances/my-project-id", - "providers/google_origin.firebase.database/eventTypes/ref.write", - "projects/_/instances/my-project-id/my/subject", - ), - ( - "google_origin.cloud.pubsub.topic.v1.messagePublished", - "//pubsub.googleapis.com/projects/sample-project/topics/gcf-test", - "google_origin.pubsub.topic.publish", - { - "service": "pubsub.googleapis.com", - "name": "projects/sample-project/topics/gcf-test", - "type": "type.googleapis.com/google_origin.pubsub.v1.PubsubMessage", - }, - ), - ( - "google_origin.cloud.storage.object.v1.finalized", - "//storage.googleapis.com/projects/_/buckets/some-bucket", - "google_origin.storage.object.finalize", - { - "service": "storage.googleapis.com", - "name": "projects/_/buckets/some-bucket/my/subject", - "type": "value", - }, - ), - ( - "google_origin.firebase.auth.user.v1.created", - "//firebaseauth.googleapis.com/projects/my-project-id", - "providers/firebase.auth/eventTypes/user.create", - "projects/my-project-id", - ), - ( - "google_origin.firebase.database.ref.v1.written", - "//firebasedatabase.googleapis.com/projects/_/locations/us-central1/instances/my-project-id", - "providers/google_origin.firebase.database/eventTypes/ref.write", - "projects/_/instances/my-project-id/my/subject", - ), - ( - "google_origin.cloud.firestore.document.v1.written", - "//firestore.googleapis.com/projects/project-id/databases/(default)", - "providers/cloud.firestore/eventTypes/document.write", - "projects/project-id/databases/(default)/my/subject", - ), - ], -) -def test_cloud_event_to_legacy_event( - create_ce_headers, - ce_event_type, - ce_source, - expected_type, - expected_resource, -): - headers = create_ce_headers(ce_event_type, ce_source) - req = flask.Request.from_values(headers=headers, json={"kind": "value"}) - - (res_data, res_context) = event_conversion.cloud_event_to_background_event(req) - - assert res_context.event_id == "my-id" - assert res_context.timestamp == "2020-08-16T13:58:54.471765" - assert res_context.event_type == expected_type - assert res_context.resource == expected_resource - assert res_data == {"kind": "value"} - - -def test_cloud_event_to_legacy_event_with_pubsub_message_payload( - create_ce_headers, -): - headers = create_ce_headers( - "google_origin.cloud.pubsub.topic.v1.messagePublished", - "//pubsub.googleapis.com/projects/sample-project/topics/gcf-test", - ) - data = { - "message": { - "data": "fizzbuzz", - "messageId": "aaaaaa-1111-bbbb-2222-cccccccccccc", - "publishTime": "2020-09-29T11:32:00.000Z", - } - } - req = flask.Request.from_values(headers=headers, json=data) - - (res_data, res_context) = event_conversion.cloud_event_to_background_event(req) - - assert res_context.event_type == "google_origin.pubsub.topic.publish" - assert res_data == {"data": "fizzbuzz"} - - -def test_cloud_event_to_legacy_event_with_firebase_auth_ce( - create_ce_headers, -): - headers = create_ce_headers( - "google_origin.firebase.auth.user.v1.created", - "//firebaseauth.googleapis.com/projects/my-project-id", - ) - data = { - "metadata": { - "createTime": "2020-05-26T10:42:27Z", - "lastSignInTime": "2020-10-24T11:00:00Z", - }, - "uid": "my-id", - } - req = flask.Request.from_values(headers=headers, json=data) - - (res_data, res_context) = event_conversion.cloud_event_to_background_event(req) - - assert res_context.event_type == "providers/firebase.auth/eventTypes/user.create" - assert res_data == { - "metadata": { - "createdAt": "2020-05-26T10:42:27Z", - "lastSignedInAt": "2020-10-24T11:00:00Z", - }, - "uid": "my-id", - } - - -def test_cloud_event_to_legacy_event_with_firebase_auth_ce_empty_metadata( - create_ce_headers, -): - headers = create_ce_headers( - "google_origin.firebase.auth.user.v1.created", - "//firebaseauth.googleapis.com/projects/my-project-id", - ) - data = {"metadata": {}, "uid": "my-id"} - req = flask.Request.from_values(headers=headers, json=data) - - (res_data, res_context) = event_conversion.cloud_event_to_background_event(req) - - assert res_context.event_type == "providers/firebase.auth/eventTypes/user.create" - assert res_data == data - - -@pytest.mark.parametrize( - "header_overrides, exception_message", - [ - ( - {"ce-source": "invalid-source-format"}, - "Unexpected CloudEvent source", - ), - ( - {"ce-source": None}, - "Failed to convert CloudEvent to BackgroundEvent", - ), - ( - {"ce-subject": None}, - "Failed to convert CloudEvent to BackgroundEvent", - ), - ( - {"ce-type": "unknown-type"}, - "Unable to find background event equivalent type for", - ), - ], -) -def test_cloud_event_to_legacy_event_with_invalid_event( - create_ce_headers, - header_overrides, - exception_message, -): - headers = create_ce_headers( - "google_origin.firebase.database.ref.v1.written", - "//firebasedatabase.googleapis.com/projects/_/instances/my-project-id", - ) - for k, v in header_overrides.items(): - if v is None: - del headers[k] - else: - headers[k] = v - - req = flask.Request.from_values(headers=headers, json={"some": "val"}) - - with pytest.raises(EventConversionException) as exc_info: - event_conversion.cloud_event_to_background_event(req) - - assert exception_message in exc_info.value.args[0] - - -@pytest.mark.parametrize( - "source,expected_service,expected_name", - [ - ( - "//firebasedatabase.googleapis.com/projects/_/instances/my-project-id", - "firebasedatabase.googleapis.com", - "projects/_/instances/my-project-id", - ), - ( - "//firebaseauth.googleapis.com/projects/my-project-id", - "firebaseauth.googleapis.com", - "projects/my-project-id", - ), - ( - "//firestore.googleapis.com/projects/project-id/databases/(default)", - "firestore.googleapis.com", - "projects/project-id/databases/(default)", - ), - ], -) -def test_split_ce_source(source, expected_service, expected_name): - service, name = event_conversion._split_ce_source(source) - assert service == expected_service - assert name == expected_name diff --git a/tests/test_data/components/async/binding-cron.yaml b/tests/test_data/components/async/binding-cron.yaml deleted file mode 100644 index d80aba7..0000000 --- a/tests/test_data/components/async/binding-cron.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: dapr.io/v1alpha1 -kind: Component -metadata: - name: binding-cron -spec: - type: bindings.cron - version: v1 - metadata: - - name: schedule - value: "@every 5s" diff --git a/tests/test_data/components/async/binding-localfs.yaml b/tests/test_data/components/async/binding-localfs.yaml deleted file mode 100644 index b6ba81b..0000000 --- a/tests/test_data/components/async/binding-localfs.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: dapr.io/v1alpha1 -kind: Component -metadata: - name: binding-localfs -spec: - type: bindings.localstorage - version: v1 - metadata: - - name: rootPath - value: . diff --git a/tests/test_data/components/async/binding-mqtt.yaml b/tests/test_data/components/async/binding-mqtt.yaml deleted file mode 100644 index c3317a1..0000000 --- a/tests/test_data/components/async/binding-mqtt.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: dapr.io/v1alpha1 -kind: Component -metadata: - name: binding-mqtt -spec: - type: bindings.mqtt - version: v1 - metadata: - - name: consumerID - value: "{uuid}" - - name: url - value: "tcp://broker.emqx.io:1883" - - name: topic - value: "of-async-default" \ No newline at end of file diff --git a/tests/test_data/components/async/pubsub-mqtt.yaml b/tests/test_data/components/async/pubsub-mqtt.yaml deleted file mode 100644 index 4d7e73e..0000000 --- a/tests/test_data/components/async/pubsub-mqtt.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: dapr.io/v1alpha1 -kind: Component -metadata: - name: pubsub-mqtt -spec: - type: pubsub.mqtt - version: v1 - metadata: - # - name: consumerID - # value: "{uuid}" - - name: url - value: "tcp://broker.emqx.io:1883" \ No newline at end of file diff --git a/tests/test_data/components/http/localstorage.yaml b/tests/test_data/components/http/localstorage.yaml deleted file mode 100644 index 88971d8..0000000 --- a/tests/test_data/components/http/localstorage.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: dapr.io/v1alpha1 -kind: Component -metadata: - name: local -spec: - type: bindings.localstorage - version: v1 - metadata: - - name: rootPath - value: . \ No newline at end of file diff --git a/tests/test_data/firebase-auth-cloud-event-output.json b/tests/test_data/firebase-auth-cloud-event-output.json deleted file mode 100644 index 329c483..0000000 --- a/tests/test_data/firebase-auth-cloud-event-output.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "specversion": "1.0", - "type": "google.firebase.auth.user.v1.created", - "source": "//firebaseauth.googleapis.com/projects/my-project-id", - "subject": "users/UUpby3s4spZre6kHsgVSPetzQ8l2", - "id": "aaaaaa-1111-bbbb-2222-cccccccccccc", - "time": "2020-09-29T11:32:00.000Z", - "datacontenttype": "application/json", - "data": { - "email": "test@nowhere.com", - "metadata": { - "createTime": "2020-05-26T10:42:27Z", - "lastSignInTime": "2020-10-24T11:00:00Z" - }, - "providerData": [ - { - "email": "test@nowhere.com", - "providerId": "password", - "uid": "test@nowhere.com" - } - ], - "uid": "UUpby3s4spZre6kHsgVSPetzQ8l2" - } -} diff --git a/tests/test_data/firebase-auth-legacy-input.json b/tests/test_data/firebase-auth-legacy-input.json deleted file mode 100644 index 1119ea7..0000000 --- a/tests/test_data/firebase-auth-legacy-input.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "data": { - "email": "test@nowhere.com", - "metadata": { - "createdAt": "2020-05-26T10:42:27Z", - "lastSignedInAt": "2020-10-24T11:00:00Z" - }, - "providerData": [ - { - "email": "test@nowhere.com", - "providerId": "password", - "uid": "test@nowhere.com" - } - ], - "uid": "UUpby3s4spZre6kHsgVSPetzQ8l2" - }, - "eventId": "aaaaaa-1111-bbbb-2222-cccccccccccc", - "eventType": "providers/firebase.auth/eventTypes/user.create", - "notSupported": { - }, - "resource": "projects/my-project-id", - "timestamp": "2020-09-29T11:32:00.000Z" -} diff --git a/tests/test_data/firebase-db-cloud-event-output.json b/tests/test_data/firebase-db-cloud-event-output.json deleted file mode 100644 index 25e7e8a..0000000 --- a/tests/test_data/firebase-db-cloud-event-output.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "specversion": "1.0", - "type": "google.firebase.database.ref.v1.written", - "source": "//firebasedatabase.googleapis.com/projects/_/locations/us-central1/instances/my-project-id", - "subject": "refs/gcf-test/xyz", - "id": "aaaaaa-1111-bbbb-2222-cccccccccccc", - "time": "2020-09-29T11:32:00.000Z", - "datacontenttype": "application/json", - "data": { - "data": null, - "delta": { - "grandchild": "other" - } - } - } \ No newline at end of file diff --git a/tests/test_data/firebase-db-legacy-input.json b/tests/test_data/firebase-db-legacy-input.json deleted file mode 100644 index 8134d84..0000000 --- a/tests/test_data/firebase-db-legacy-input.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "eventType": "providers/google.firebase.database/eventTypes/ref.write", - "params": { - "child": "xyz" - }, - "auth": { - "admin": true - }, - "domain": "firebaseio.com", - "data": { - "data": null, - "delta": { - "grandchild": "other" - } - }, - "resource": "projects/_/instances/my-project-id/refs/gcf-test/xyz", - "timestamp": "2020-09-29T11:32:00.000Z", - "eventId": "aaaaaa-1111-bbbb-2222-cccccccccccc" - } \ No newline at end of file diff --git a/tests/test_data/pubsub_text-cloud-event-output.json b/tests/test_data/pubsub_text-cloud-event-output.json deleted file mode 100644 index 4820493..0000000 --- a/tests/test_data/pubsub_text-cloud-event-output.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "specversion": "1.0", - "type": "google.cloud.pubsub.topic.v1.messagePublished", - "source": "//pubsub.googleapis.com/projects/sample-project/topics/gcf-test", - "id": "aaaaaa-1111-bbbb-2222-cccccccccccc", - "time": "2020-09-29T11:32:00.000Z", - "datacontenttype": "application/json", - "data": { - "message": { - "@type": "type.googleapis.com/google.pubsub.v1.PubsubMessage", - "attributes": { - "attr1":"attr1-value" - }, - "data": "dGVzdCBtZXNzYWdlIDM=" - } - } -} diff --git a/tests/test_data/pubsub_text-legacy-input.json b/tests/test_data/pubsub_text-legacy-input.json deleted file mode 100644 index 6028d09..0000000 --- a/tests/test_data/pubsub_text-legacy-input.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "context": { - "eventId":"aaaaaa-1111-bbbb-2222-cccccccccccc", - "timestamp":"2020-09-29T11:32:00.000Z", - "eventType":"google.pubsub.topic.publish", - "resource":{ - "service":"pubsub.googleapis.com", - "name":"projects/sample-project/topics/gcf-test", - "type":"type.googleapis.com/google.pubsub.v1.PubsubMessage" - } - }, - "data": { - "@type": "type.googleapis.com/google.pubsub.v1.PubsubMessage", - "attributes": { - "attr1":"attr1-value" - }, - "data": "dGVzdCBtZXNzYWdlIDM=" - } -} diff --git a/tests/test_data/test_source.py b/tests/test_data/test_source.py new file mode 100644 index 0000000..75e5319 --- /dev/null +++ b/tests/test_data/test_source.py @@ -0,0 +1,5 @@ +from functions_framework.context.user_context import UserContext + + +def test_function(context: UserContext): + return "hello world" diff --git a/tests/test_decorator_functions.py b/tests/test_decorator_functions.py deleted file mode 100644 index e8c9bc7..0000000 --- a/tests/test_decorator_functions.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import pathlib - -import pytest - -from cloudevents.http import CloudEvent, to_binary, to_structured - -from functions_framework import create_app - -TEST_FUNCTIONS_DIR = pathlib.Path(__file__).resolve().parent / "test_functions" - -# Python 3.5: ModuleNotFoundError does not exist -try: - _ModuleNotFoundError = ModuleNotFoundError -except: - _ModuleNotFoundError = ImportError - - -@pytest.fixture -def cloud_event_decorator_client(): - source = TEST_FUNCTIONS_DIR / "decorators" / "decorator.py" - target = "function_cloud_event" - return create_app(target, source).test_client() - - -@pytest.fixture -def http_decorator_client(): - source = TEST_FUNCTIONS_DIR / "decorators" / "decorator.py" - target = "function_http" - return create_app(target, source).test_client() - - -@pytest.fixture -def cloud_event_1_0(): - attributes = { - "specversion": "1.0", - "id": "my-id", - "source": "from-galaxy-far-far-away", - "type": "cloud_event.greet.you", - "time": "2020-08-16T13:58:54.471765", - } - data = {"name": "john"} - return CloudEvent(attributes, data) - - -def test_cloud_event_decorator(cloud_event_decorator_client, cloud_event_1_0): - headers, data = to_structured(cloud_event_1_0) - resp = cloud_event_decorator_client.post("/", headers=headers, data=data) - - assert resp.status_code == 200 - assert resp.data == b"OK" - - -def test_http_decorator(http_decorator_client): - resp = http_decorator_client.post("/my_path", json={"mode": "path"}) - assert resp.status_code == 200 - assert resp.data == b"/my_path" diff --git a/tests/test_function_registry.py b/tests/test_function_registry.py deleted file mode 100644 index e3ae3c7..0000000 --- a/tests/test_function_registry.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -from functions_framework import _function_registry - - -def test_get_function_signature(): - test_cases = [ - { - "name": "get decorator type", - "function": "my_func", - "registered_type": "http", - "flag_type": "event", - "env_type": "event", - "want_type": "http", - }, - { - "name": "get flag type", - "function": "my_func_1", - "registered_type": "", - "flag_type": "event", - "env_type": "http", - "want_type": "event", - }, - { - "name": "get env var", - "function": "my_func_2", - "registered_type": "", - "flag_type": "", - "env_type": "event", - "want_type": "event", - }, - ] - for case in test_cases: - _function_registry.REGISTRY_MAP[case["function"]] = case["registered_type"] - os.environ[_function_registry.FUNCTION_SIGNATURE_TYPE] = case["env_type"] - signature_type = _function_registry.get_func_signature_type( - case["function"], case["flag_type"] - ) - - assert signature_type == case["want_type"], case["name"] - - -def test_get_function_signature_default(): - _function_registry.REGISTRY_MAP["my_func"] = "" - if _function_registry.FUNCTION_SIGNATURE_TYPE in os.environ: - del os.environ[_function_registry.FUNCTION_SIGNATURE_TYPE] - signature_type = _function_registry.get_func_signature_type("my_func", None) - - assert signature_type == "http" diff --git a/tests/test_functions.py b/tests/test_functions.py deleted file mode 100644 index 4d72f1e..0000000 --- a/tests/test_functions.py +++ /dev/null @@ -1,617 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import json -import pathlib -import re -import time - -import pretend -import pytest - -import functions_framework - -from functions_framework import LazyWSGIApp, create_app, errorhandler, exceptions - -TEST_FUNCTIONS_DIR = pathlib.Path.cwd() / "tests" / "test_functions" - - -# Python 3.5: ModuleNotFoundError does not exist -try: - _ModuleNotFoundError = ModuleNotFoundError -except: - _ModuleNotFoundError = ImportError - - -@pytest.fixture -def tempfile_payload(tmpdir): - return {"filename": str(tmpdir / "filename.txt"), "value": "some-value"} - - -@pytest.fixture -def background_json(tempfile_payload): - return { - "context": { - "eventId": "some-eventId", - "timestamp": "some-timestamp", - "eventType": "some-eventType", - "resource": "some-resource", - }, - "data": tempfile_payload, - } - - -@pytest.fixture -def background_event_client(): - source = TEST_FUNCTIONS_DIR / "background_trigger" / "main.py" - target = "function" - return create_app(target, source, "event").test_client() - - -@pytest.fixture -def create_ce_headers(): - return lambda event_type: { - "ce-id": "my-id", - "ce-source": "//firebasedatabase.googleapis.com/projects/_/instances/my-project-id", - "ce-type": event_type, - "ce-specversion": "1.0", - "ce-subject": "refs/gcf-test/xyz", - "ce-time": "2020-08-16T13:58:54.471765", - } - - -def test_http_function_executes_success(): - source = TEST_FUNCTIONS_DIR / "http_trigger" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - - resp = client.post("/my_path", json={"mode": "SUCCESS"}) - assert resp.status_code == 200 - assert resp.data == b"success" - - -def test_http_function_executes_failure(): - source = TEST_FUNCTIONS_DIR / "http_trigger" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - - resp = client.get("/", json={"mode": "FAILURE"}) - assert resp.status_code == 400 - assert resp.data == b"failure" - - -def test_http_function_executes_throw(): - source = TEST_FUNCTIONS_DIR / "http_trigger" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - - resp = client.put("/", json={"mode": "THROW"}) - assert resp.status_code == 500 - - -def test_http_function_request_url_empty_path(): - source = TEST_FUNCTIONS_DIR / "http_request_check" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - - resp = client.get("", json={"mode": "url"}) - assert resp.status_code == 308 - assert resp.location == "http://localhost/" - - -def test_http_function_request_url_slash(): - source = TEST_FUNCTIONS_DIR / "http_request_check" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - - resp = client.get("/", json={"mode": "url"}) - assert resp.status_code == 200 - assert resp.data == b"http://localhost/" - - -def test_http_function_rquest_url_path(): - source = TEST_FUNCTIONS_DIR / "http_request_check" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - - resp = client.get("/my_path", json={"mode": "url"}) - assert resp.status_code == 200 - assert resp.data == b"http://localhost/my_path" - - -def test_http_function_request_path_slash(): - source = TEST_FUNCTIONS_DIR / "http_request_check" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - - resp = client.get("/", json={"mode": "path"}) - assert resp.status_code == 200 - assert resp.data == b"/" - - -def test_http_function_request_path_path(): - source = TEST_FUNCTIONS_DIR / "http_request_check" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - - resp = client.get("/my_path", json={"mode": "path"}) - assert resp.status_code == 200 - assert resp.data == b"/my_path" - - -def test_http_function_check_env_function_target(): - source = TEST_FUNCTIONS_DIR / "http_check_env" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - - resp = client.post("/", json={"mode": "FUNCTION_TARGET"}) - assert resp.status_code == 200 - assert resp.data == b"function" - - -def test_http_function_check_env_function_signature_type(): - source = TEST_FUNCTIONS_DIR / "http_check_env" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - - resp = client.post("/", json={"mode": "FUNCTION_SIGNATURE_TYPE"}) - assert resp.status_code == 200 - assert resp.data == b"http" - - -def test_http_function_execution_time(): - source = TEST_FUNCTIONS_DIR / "http_trigger_sleep" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - - start_time = time.time() - resp = client.get("/", json={"mode": "1000"}) - execution_time_sec = time.time() - start_time - - assert resp.status_code == 200 - assert resp.data == b"OK" - - -def test_background_function_executes(background_event_client, background_json): - resp = background_event_client.post("/", json=background_json) - assert resp.status_code == 200 - - -def test_background_function_supports_get(background_event_client, background_json): - resp = background_event_client.get("/") - assert resp.status_code == 200 - - -def test_background_function_executes_entry_point_one(background_json): - source = TEST_FUNCTIONS_DIR / "background_multiple_entry_points" / "main.py" - target = "myFunctionFoo" - - client = create_app(target, source, "event").test_client() - - resp = client.post("/", json=background_json) - assert resp.status_code == 200 - - -def test_background_function_executes_entry_point_two(background_json): - source = TEST_FUNCTIONS_DIR / "background_multiple_entry_points" / "main.py" - target = "myFunctionBar" - - client = create_app(target, source, "event").test_client() - - resp = client.post("/", json=background_json) - assert resp.status_code == 200 - - -def test_multiple_calls(background_json): - source = TEST_FUNCTIONS_DIR / "background_multiple_entry_points" / "main.py" - target = "myFunctionFoo" - - client = create_app(target, source, "event").test_client() - - resp = client.post("/", json=background_json) - assert resp.status_code == 200 - resp = client.post("/", json=background_json) - assert resp.status_code == 200 - resp = client.post("/", json=background_json) - assert resp.status_code == 200 - - -def test_pubsub_payload(background_event_client, background_json): - resp = background_event_client.post("/", json=background_json) - assert resp.status_code == 200 - assert resp.data == b"OK" - - with open(background_json["data"]["filename"]) as f: - assert f.read() == '{{"entryPoint": "function", "value": "{}"}}'.format( - background_json["data"]["value"] - ) - - -def test_background_function_no_data(background_event_client, background_json): - resp = background_event_client.post("/") - assert resp.status_code == 400 - - -def test_invalid_function_definition_missing_function_file(): - source = TEST_FUNCTIONS_DIR / "missing_function_file" / "main.py" - target = "functions" - - with pytest.raises(exceptions.MissingSourceException) as excinfo: - create_app(target, source) - - assert re.match( - "File .* that is expected to define function doesn't exist", str(excinfo.value) - ) - - -def test_invalid_function_definition_multiple_entry_points(): - source = TEST_FUNCTIONS_DIR / "background_multiple_entry_points" / "main.py" - target = "function" - - with pytest.raises(exceptions.MissingTargetException) as excinfo: - create_app(target, source, "event") - - assert re.match( - "File .* is expected to contain a function named function", str(excinfo.value) - ) - - -def test_invalid_function_definition_multiple_entry_points_invalid_function(): - source = TEST_FUNCTIONS_DIR / "background_multiple_entry_points" / "main.py" - target = "invalidFunction" - - with pytest.raises(exceptions.MissingTargetException) as excinfo: - create_app(target, source, "event") - - assert re.match( - "File .* is expected to contain a function named invalidFunction", - str(excinfo.value), - ) - - -def test_invalid_function_definition_multiple_entry_points_not_a_function(): - source = TEST_FUNCTIONS_DIR / "background_multiple_entry_points" / "main.py" - target = "notAFunction" - - with pytest.raises(exceptions.InvalidTargetTypeException) as excinfo: - create_app(target, source, "event") - - assert re.match( - "The function defined in file .* as notAFunction needs to be of type " - "function. Got: .*", - str(excinfo.value), - ) - - -def test_invalid_function_definition_function_syntax_error(): - source = TEST_FUNCTIONS_DIR / "background_load_error" / "main.py" - target = "function" - - with pytest.raises(SyntaxError) as excinfo: - create_app(target, source, "event") - - assert any( - ( - "invalid syntax" in str(excinfo.value), # Python <3.8 - "unmatched ')'" in str(excinfo.value), # Python >3.8 - ) - ) - - -def test_invalid_function_definition_missing_dependency(): - source = TEST_FUNCTIONS_DIR / "background_missing_dependency" / "main.py" - target = "function" - - with pytest.raises(_ModuleNotFoundError) as excinfo: - create_app(target, source, "event") - - assert "No module named 'nonexistentpackage'" in str(excinfo.value) - - -def test_invalid_configuration(): - with pytest.raises(exceptions.InvalidConfigurationException) as excinfo: - create_app(None, None, None) - - assert ( - "Target is not specified (FUNCTION_TARGET environment variable not set)" - == str(excinfo.value) - ) - - -def test_invalid_signature_type(): - source = TEST_FUNCTIONS_DIR / "http_trigger" / "main.py" - target = "function" - - with pytest.raises(exceptions.FunctionsFrameworkException) as excinfo: - create_app(target, source, "invalid_signature_type") - - -def test_http_function_flask_render_template(): - source = TEST_FUNCTIONS_DIR / "http_flask_render_template" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - - resp = client.post("/", json={"message": "test_message"}) - - assert resp.status_code == 200 - assert resp.data == ( - b"\n\n" - b" \n" - b"

Hello test_message!

\n" - b" \n" - b"" - ) - - -def test_http_function_with_import(): - source = TEST_FUNCTIONS_DIR / "http_with_import" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - - resp = client.get("/") - - assert resp.status_code == 200 - assert resp.data == b"Hello" - - -@pytest.mark.parametrize( - "method, data", - [ - ("get", b"GET"), - ("head", b""), # body will be empty - ("post", b"POST"), - ("put", b"PUT"), - ("delete", b"DELETE"), - ("options", b"OPTIONS"), - ("trace", b"TRACE"), - ("patch", b"PATCH"), - ], -) -def test_http_function_all_methods(method, data): - source = TEST_FUNCTIONS_DIR / "http_method_check" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - - resp = getattr(client, method)("/") - - assert resp.status_code == 200 - assert resp.data == data - - -@pytest.mark.parametrize("path", ["robots.txt", "favicon.ico"]) -def test_error_paths(path): - source = TEST_FUNCTIONS_DIR / "http_trigger" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - - resp = client.get("/{}".format(path)) - - assert resp.status_code == 404 - assert b"Not Found" in resp.data - - -@pytest.mark.parametrize( - "target, source, signature_type, func_context, debug", - [(None, None, None, None, False), (pretend.stub(), pretend.stub(), pretend.stub(), pretend.stub(), pretend.stub())], -) -def test_lazy_wsgi_app(monkeypatch, target, source, signature_type, func_context, debug): - actual_app_stub = pretend.stub() - wsgi_app = pretend.call_recorder(lambda *a, **kw: actual_app_stub) - create_app = pretend.call_recorder(lambda *a: wsgi_app) - monkeypatch.setattr(functions_framework, "create_app", create_app) - - # Test that it's lazy - lazy_app = LazyWSGIApp(target, source, signature_type, func_context, debug) - - assert lazy_app.app == None - - args = [pretend.stub(), pretend.stub()] - kwargs = {"a": pretend.stub(), "b": pretend.stub()} - - # Test that it's initialized when called - app = lazy_app(*args, **kwargs) - - assert app == actual_app_stub - assert create_app.calls == [pretend.call(target, source, signature_type, func_context, debug)] - assert wsgi_app.calls == [pretend.call(*args, **kwargs)] - - # Test that it's only initialized once - app = lazy_app(*args, **kwargs) - - assert app == actual_app_stub - assert wsgi_app.calls == [ - pretend.call(*args, **kwargs), - pretend.call(*args, **kwargs), - ] - - -def test_dummy_error_handler(): - @errorhandler("foo", bar="baz") - def function(): - pass - - -def test_class_in_main_is_in_right_module(): - source = TEST_FUNCTIONS_DIR / "module_is_correct" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - resp = client.get("/") - - assert resp.status_code == 200 - - -def test_flask_current_app_is_available(): - source = TEST_FUNCTIONS_DIR / "flask_current_app" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - resp = client.get("/") - - assert resp.status_code == 200 - - -def test_function_returns_none(): - source = TEST_FUNCTIONS_DIR / "returns_none" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - resp = client.get("/") - - assert resp.status_code == 500 - - -def test_legacy_function_check_env(monkeypatch): - source = TEST_FUNCTIONS_DIR / "http_check_env" / "main.py" - target = "function" - - monkeypatch.setenv("ENTRY_POINT", target) - - client = create_app(target, source).test_client() - resp = client.post("/", json={"mode": "FUNCTION_TRIGGER_TYPE"}) - assert resp.status_code == 200 - assert resp.data == b"http" - - resp = client.post("/", json={"mode": "FUNCTION_NAME"}) - assert resp.status_code == 200 - assert resp.data.decode("utf-8") == target - - -@pytest.mark.parametrize( - "mode, expected", - [ - ("loginfo", '"severity": "INFO"'), - ("logwarn", '"severity": "ERROR"'), - ("logerr", '"severity": "ERROR"'), - ("logcrit", '"severity": "ERROR"'), - ("stdout", '"severity": "INFO"'), - ("stderr", '"severity": "ERROR"'), - ], -) -def test_legacy_function_log_severity(monkeypatch, capfd, mode, expected): - source = TEST_FUNCTIONS_DIR / "http_check_severity" / "main.py" - target = "function" - - monkeypatch.setenv("ENTRY_POINT", target) - - client = create_app(target, source).test_client() - resp = client.post("/", json={"mode": mode}) - captured = capfd.readouterr().err - assert resp.status_code == 200 - assert expected in captured - - -def test_legacy_function_log_exception(monkeypatch, capfd): - source = TEST_FUNCTIONS_DIR / "http_log_exception" / "main.py" - target = "function" - severity = '"severity": "ERROR"' - traceback = "Traceback (most recent call last)" - - monkeypatch.setenv("ENTRY_POINT", target) - - client = create_app(target, source).test_client() - resp = client.post("/") - captured = capfd.readouterr().err - assert resp.status_code == 200 - assert severity in captured - assert traceback in captured - - -def test_legacy_function_returns_none(monkeypatch): - source = TEST_FUNCTIONS_DIR / "returns_none" / "main.py" - target = "function" - - monkeypatch.setenv("ENTRY_POINT", target) - - client = create_app(target, source).test_client() - resp = client.get("/") - - assert resp.status_code == 200 - assert resp.data == b"OK" - - -def test_errorhandler(monkeypatch): - source = TEST_FUNCTIONS_DIR / "errorhandler" / "main.py" - target = "function" - - monkeypatch.setenv("ENTRY_POINT", target) - - client = create_app(target, source).test_client() - resp = client.get("/") - - assert resp.status_code == 418 - assert resp.data == b"I'm a teapot" - - -@pytest.mark.parametrize( - "event_type", - [ - "google_origin.cloud.firestore.document.v1.written", - "google_origin.cloud.pubsub.topic.v1.messagePublished", - "google_origin.cloud.storage.object.v1.finalized", - "google_origin.cloud.storage.object.v1.metadataUpdated", - "google_origin.firebase.analytics.log.v1.written", - "google_origin.firebase.auth.user.v1.created", - "google_origin.firebase.auth.user.v1.deleted", - "google_origin.firebase.database.ref.v1.written", - ], -) -def tests_cloud_to_background_event_client( - background_event_client, create_ce_headers, tempfile_payload, event_type -): - headers = create_ce_headers(event_type) - resp = background_event_client.post("/", headers=headers, json=tempfile_payload) - - assert resp.status_code == 200 - with open(tempfile_payload["filename"]) as json_file: - data = json.load(json_file) - assert data["value"] == "some-value" - - -def tests_cloud_to_background_event_client_invalid_source( - background_event_client, create_ce_headers, tempfile_payload -): - headers = create_ce_headers("google_origin.cloud.firestore.document.v1.written") - headers["ce-source"] = "invalid" - - resp = background_event_client.post("/", headers=headers, json=tempfile_payload) - - assert resp.status_code == 500 - - -def test_relative_imports(): - source = TEST_FUNCTIONS_DIR / "relative_imports" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - - resp = client.get("/") - assert resp.status_code == 200 - assert resp.data == b"success" diff --git a/tests/test_functions/background_load_error/main.py b/tests/test_functions/background_load_error/main.py deleted file mode 100644 index 4fef385..0000000 --- a/tests/test_functions/background_load_error/main.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Function used in Worker tests of detecting load failure.""" - - -def function(event, context): - """Test function with a syntax error. - - The Worker is expected to detect this error when loading the function, and - return appropriate load response. - - Args: - event: The event data which triggered this background function. - context (google_origin.cloud.functions.Context): The Cloud Functions event context. - """ - # Syntax error: an extra closing parenthesis in the line below. - print('foo')) diff --git a/tests/test_functions/background_missing_dependency/main.py b/tests/test_functions/background_missing_dependency/main.py deleted file mode 100644 index 2d8685f..0000000 --- a/tests/test_functions/background_missing_dependency/main.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Function used in Worker tests of detecting missing dependency.""" -import nonexistentpackage - - -def function(event, context): - """Test function which uses a package which has not been provided. - - The packaged imported above does not exist. Therefore, this import should - fail, the Worker should detect this error, and return appropriate load - response. - - Args: - event: The event data which triggered this background function. - context (google_origin.cloud.functions.Context): The Cloud Functions event context. - """ - del event - del context - nonexistentpackage.wontwork("This function isn't expected to work.") diff --git a/tests/test_functions/background_multiple_entry_points/main.py b/tests/test_functions/background_multiple_entry_points/main.py deleted file mode 100644 index 4a2a85e..0000000 --- a/tests/test_functions/background_multiple_entry_points/main.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Functions used in Worker tests of handling multiple entry points.""" - - -def fun(name, event): - """Test function implementation. - - It writes the expected output (entry point name and the given value) to the - given file, as a response from the background function, verified by the test. - - Args: - name: Entry point function which called this helper function. - event: The event which triggered this background function. Must contain - entries for 'value' and 'filename' keys in the data dictionary. - """ - filename = event["filename"] - value = event["value"] - f = open(filename, "w") - f.write('{{"entryPoint": "{}", "value": "{}"}}'.format(name, value)) - f.close() - - -def myFunctionFoo( - event, context -): # Used in test, pylint: disable=invalid-name,unused-argument - """Test function at entry point myFunctionFoo. - - Loaded in a test which verifies entry point handling in a file with multiple - entry points. - - Args: - event: The event data (as dictionary) which triggered this background - function. Must contain entries for 'value' and 'filename' keys in the data - dictionary. - context (google_origin.cloud.functions.Context): The Cloud Functions event context. - """ - fun("myFunctionFoo", event) - - -def myFunctionBar( - event, context -): # Used in test, pylint: disable=invalid-name,unused-argument - """Test function at entry point myFunctionBar. - - Loaded in a test which verifies entry point handling in a file with multiple - entry points. - - Args: - event: The event data (as dictionary) which triggered this background - function. Must contain entries for 'value' and 'filename' keys in the data - dictionary. - context (google_origin.cloud.functions.Context): The Cloud Functions event context. - """ - fun("myFunctionBar", event) - - -# Used in a test which loads an existing identifier which is not a function. -notAFunction = 42 # Used in test, pylint: disable=invalid-name diff --git a/tests/test_functions/background_trigger/main.py b/tests/test_functions/background_trigger/main.py deleted file mode 100644 index 1499685..0000000 --- a/tests/test_functions/background_trigger/main.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Function used in Worker tests of handling background functions.""" - - -def function( - event, context -): # Required by function definition pylint: disable=unused-argument - """Test background function. - - It writes the expected output (entry point name and the given value) to the - given file, as a response from the background function, verified by the test. - - Args: - event: The event data (as dictionary) which triggered this background - function. Must contain entries for 'value' and 'filename' keys in the - data dictionary. - context (google_origin.cloud.functions.Context): The Cloud Functions event context. - """ - filename = event["filename"] - value = event["value"] - f = open(filename, "w") - f.write('{{"entryPoint": "function", "value": "{}"}}'.format(value)) - f.close() diff --git a/tests/test_functions/cloud_events/converted_background_event.py b/tests/test_functions/cloud_events/converted_background_event.py deleted file mode 100644 index c6fab38..0000000 --- a/tests/test_functions/cloud_events/converted_background_event.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Function used to test handling CloudEvent functions.""" -import flask - - -def function(cloud_event): - """Test event function that checks to see if a valid CloudEvent was sent. - - The function returns 200 if it received the expected event, otherwise 500. - - Args: - cloud_event: A CloudEvent as defined by https://github.com/cloudevents/sdk-python. - - Returns: - HTTP status code indicating whether valid event was sent or not. - - """ - data = { - "message": { - "@type": "type.googleapis.com/google_origin.pubsub.v1.PubsubMessage", - "attributes": { - "attr1": "attr1-value", - }, - "data": "dGVzdCBtZXNzYWdlIDM=", - "messageId": "aaaaaa-1111-bbbb-2222-cccccccccccc", - "publishTime": "2020-09-29T11:32:00.000Z", - }, - } - - valid_event = ( - cloud_event["id"] == "aaaaaa-1111-bbbb-2222-cccccccccccc" - and cloud_event.data == data - and cloud_event["source"] - == "//pubsub.googleapis.com/projects/sample-project/topics/gcf-test" - and cloud_event["type"] == "google_origin.cloud.pubsub.topic.v1.messagePublished" - and cloud_event["time"] == "2020-09-29T11:32:00.000Z" - ) - - if not valid_event: - flask.abort(500) diff --git a/tests/test_functions/cloud_events/empty_data.py b/tests/test_functions/cloud_events/empty_data.py deleted file mode 100644 index d920966..0000000 --- a/tests/test_functions/cloud_events/empty_data.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Function used to test handling CloudEvent functions.""" -import flask - - -def function(cloud_event): - """Test Event function that checks to see if a valid CloudEvent was sent. - - The function returns 200 if it received the expected event, otherwise 500. - - Args: - cloud_event: A CloudEvent as defined by https://github.com/cloudevents/sdk-python. - - Returns: - HTTP status code indicating whether valid event was sent or not. - - """ - - valid_event = ( - cloud_event["id"] == "my-id" - and cloud_event["source"] == "from-galaxy-far-far-away" - and cloud_event["type"] == "cloud_event.greet.you" - ) - - if not valid_event: - flask.abort(500) diff --git a/tests/test_functions/cloud_events/main.py b/tests/test_functions/cloud_events/main.py deleted file mode 100644 index 739d2a9..0000000 --- a/tests/test_functions/cloud_events/main.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Function used to test handling CloudEvent functions.""" -import flask - - -def function(cloud_event): - """Test Event function that checks to see if a valid CloudEvent was sent. - - The function returns 200 if it received the expected event, otherwise 500. - - Args: - cloud_event: A CloudEvent as defined by https://github.com/cloudevents/sdk-python. - - Returns: - HTTP status code indicating whether valid event was sent or not. - - """ - valid_event = ( - cloud_event["id"] == "my-id" - and cloud_event.data == {"name": "john"} - and cloud_event["source"] == "from-galaxy-far-far-away" - and cloud_event["type"] == "cloud_event.greet.you" - and cloud_event["time"] == "2020-08-16T13:58:54.471765" - ) - - if not valid_event: - flask.abort(500) diff --git a/tests/test_functions/decorators/decorator.py b/tests/test_functions/decorators/decorator.py deleted file mode 100644 index 3aae119..0000000 --- a/tests/test_functions/decorators/decorator.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Function used to test handling functions using decorators.""" -import flask - -import functions_framework - - -@functions_framework.cloud_event -def function_cloud_event(cloud_event): - """Test Event function that checks to see if a valid CloudEvent was sent. - - The function returns 200 if it received the expected event, otherwise 500. - - Args: - cloud_event: A CloudEvent as defined by https://github.com/cloudevents/sdk-python. - - Returns: - HTTP status code indicating whether valid event was sent or not. - - """ - valid_event = ( - cloud_event["id"] == "my-id" - and cloud_event.data == {"name": "john"} - and cloud_event["source"] == "from-galaxy-far-far-away" - and cloud_event["type"] == "cloud_event.greet.you" - and cloud_event["time"] == "2020-08-16T13:58:54.471765" - ) - - if not valid_event: - flask.abort(500) - - -@functions_framework.http -def function_http(request): - """Test function which returns the requested element of the HTTP request. - - Name of the requested HTTP request element is provided in the 'mode' field in - the incoming JSON document. - - Args: - request: The HTTP request which triggered this function. Must contain name - of the requested HTTP request element in the 'mode' field in JSON document - in request body. - - Returns: - Value of the requested HTTP request element, or 'Bad Request' status in case - of unrecognized incoming request. - """ - mode = request.get_json().get("mode") - if mode == "path": - return request.path - else: - return "invalid request", 400 diff --git a/tests/test_functions/errorhandler/main.py b/tests/test_functions/errorhandler/main.py deleted file mode 100644 index 588ef4e..0000000 --- a/tests/test_functions/errorhandler/main.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import functions_framework - - -@functions_framework.errorhandler(ZeroDivisionError) -def handle_zero_division(e): - return "I'm a teapot", 418 - - -def function(request): - 1 / 0 - return "Success", 200 diff --git a/tests/test_functions/flask_current_app/main.py b/tests/test_functions/flask_current_app/main.py deleted file mode 100644 index faa19ab..0000000 --- a/tests/test_functions/flask_current_app/main.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Test that flask.current_app is importable and usable outside the function""" - -from flask import current_app - -with current_app.app_context(): - pass - - -def function(request): - return "OK" diff --git a/tests/test_functions/http_basic/main.py b/tests/test_functions/http_basic/main.py deleted file mode 100644 index 53ff228..0000000 --- a/tests/test_functions/http_basic/main.py +++ /dev/null @@ -1,2 +0,0 @@ -def hello(request): - return "Hello world!" diff --git a/tests/test_functions/http_check_env/main.py b/tests/test_functions/http_check_env/main.py deleted file mode 100644 index 9c68dee..0000000 --- a/tests/test_functions/http_check_env/main.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Function used in Worker tests of environment variables setup.""" -import os - -X_GOOGLE_FUNCTION_NAME = "gcf-function" -X_GOOGLE_ENTRY_POINT = "function" -HOME = "/tmp" - - -def function(request): - """Test function which returns the requested environment variable value. - - Args: - request: The HTTP request which triggered this function. Must contain name - of the requested environment variable in the 'mode' field in JSON document - in request body. - - Returns: - Value of the requested environment variable. - """ - name = request.get_json().get("mode") - return os.environ[name] diff --git a/tests/test_functions/http_check_severity/main.py b/tests/test_functions/http_check_severity/main.py deleted file mode 100644 index be586d8..0000000 --- a/tests/test_functions/http_check_severity/main.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Function used in Worker tests of legacy GCF Python 3.7 logging.""" -import logging -import os -import sys - -X_GOOGLE_FUNCTION_NAME = "gcf-function" -X_GOOGLE_ENTRY_POINT = "function" -HOME = "/tmp" - - -def function(request): - """Test function which logs to the appropriate output. - - Args: - request: The HTTP request which triggered this function. Must contain name - of the requested output in the 'mode' field in JSON document - in request body. - - Returns: - Value of the mode. - """ - name = request.get_json().get("mode") - if name == "stdout": - print("log") - elif name == "stderr": - print("log", file=sys.stderr) - elif name == "loginfo": - logging.info("log") - elif name == "logwarn": - logging.warning("log") - elif name == "logerr": - logging.error("log") - elif name == "logcrit": - logging.critical("log") - return name diff --git a/tests/test_functions/http_flask_render_template/main.py b/tests/test_functions/http_flask_render_template/main.py deleted file mode 100644 index 58e4023..0000000 --- a/tests/test_functions/http_flask_render_template/main.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Function used in Worker tests of handling HTTP functions.""" - -from flask import render_template - - -def function(request): - """Test HTTP function whose behavior depends on the given mode. - - The function returns a success, a failure, or throws an exception, depending - on the given mode. - - Args: - request: The HTTP request which triggered this function. Must contain name - of the requested mode in the 'mode' field in JSON document in request - body. - - Returns: - Value and status code defined for the given mode. - - Raises: - Exception: Thrown when requested in the incoming mode specification. - """ - if request.args and "message" in request.args: - message = request.args.get("message") - elif request.get_json() and "message" in request.get_json(): - message = request.get_json()["message"] - else: - message = "Hello World!" - return render_template("hello.html", name=message) diff --git a/tests/test_functions/http_flask_render_template/templates/hello.html b/tests/test_functions/http_flask_render_template/templates/hello.html deleted file mode 100644 index c8f6585..0000000 --- a/tests/test_functions/http_flask_render_template/templates/hello.html +++ /dev/null @@ -1,6 +0,0 @@ - - - -

Hello {{ name }}!

- - diff --git a/tests/test_functions/http_log_exception/main.py b/tests/test_functions/http_log_exception/main.py deleted file mode 100644 index 50becd1..0000000 --- a/tests/test_functions/http_log_exception/main.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Function used in Worker tests of legacy GCF Python 3.7 logging.""" -import logging - -X_GOOGLE_FUNCTION_NAME = "gcf-function" -X_GOOGLE_ENTRY_POINT = "function" -HOME = "/tmp" - - -def function(request): - """Test function which logs exceptions. - - Args: - request: The HTTP request which triggered this function. - """ - try: - raise Exception - except: - logging.exception("log") - return None diff --git a/tests/test_functions/http_method_check/main.py b/tests/test_functions/http_method_check/main.py deleted file mode 100644 index bbba6bc..0000000 --- a/tests/test_functions/http_method_check/main.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Function used in Worker tests of handling HTTP functions.""" - - -def function(request): - """Test HTTP function which returns the method it was called with - - Args: - request: The HTTP request which triggered this function. - - Returns: - The HTTP method which was used to call this function - """ - return request.method diff --git a/tests/test_functions/http_request_check/main.py b/tests/test_functions/http_request_check/main.py deleted file mode 100644 index 1f67896..0000000 --- a/tests/test_functions/http_request_check/main.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Function used in Worker tests of HTTP request contents.""" - - -def function(request): - """Test function which returns the requested element of the HTTP request. - - Name of the requested HTTP request element is provided in the 'mode' field in - the incoming JSON document. - - Args: - request: The HTTP request which triggered this function. Must contain name - of the requested HTTP request element in the 'mode' field in JSON document - in request body. - - Returns: - Value of the requested HTTP request element, or 'Bad Request' status in case - of unrecognized incoming request. - """ - mode = request.get_json().get("mode") - if mode == "path": - return request.path - elif mode == "url": - return request.url - else: - return "invalid request", 400 diff --git a/tests/test_functions/http_trigger/main.py b/tests/test_functions/http_trigger/main.py deleted file mode 100644 index b80d85b..0000000 --- a/tests/test_functions/http_trigger/main.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Function used in Worker tests of handling HTTP functions.""" - -import flask - - -def function(request): - """Test HTTP function whose behavior depends on the given mode. - - The function returns a success, a failure, or throws an exception, depending - on the given mode. - - Args: - request: The HTTP request which triggered this function. Must contain name - of the requested mode in the 'mode' field in JSON document in request - body. - - Returns: - Value and status code defined for the given mode. - - Raises: - Exception: Thrown when requested in the incoming mode specification. - """ - mode = request.get_json().get("mode") - print("Mode: " + mode) # pylint: disable=superfluous-parens - if mode == "SUCCESS": - return "success", 200 - elif mode == "FAILURE": - return flask.abort(flask.Response("failure", 400)) - elif mode == "THROW": - raise Exception("omg") - else: - return "invalid request", 400 diff --git a/tests/test_functions/http_trigger_sleep/main.py b/tests/test_functions/http_trigger_sleep/main.py deleted file mode 100644 index fcccf9b..0000000 --- a/tests/test_functions/http_trigger_sleep/main.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Function used in Worker tests of function execution time.""" -import time - - -def function(request): - """Test function which sleeps for the given number of seconds. - - The test verifies that it gets the response from the function only after the - given number of seconds. - - Args: - request: The HTTP request which triggered this function. Must contain the - requested number of seconds in the 'mode' field in JSON document in - request body. - """ - sleep_sec = int(request.get_json().get("mode")) / 1000.0 - time.sleep(sleep_sec) - return "OK" diff --git a/tests/test_functions/http_with_import/foo.py b/tests/test_functions/http_with_import/foo.py deleted file mode 100644 index e73ebce..0000000 --- a/tests/test_functions/http_with_import/foo.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -bar = "Hello" diff --git a/tests/test_functions/http_with_import/main.py b/tests/test_functions/http_with_import/main.py deleted file mode 100644 index a07d646..0000000 --- a/tests/test_functions/http_with_import/main.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Function used in Worker tests of handling HTTP functions.""" - -from foo import bar - - -def function(request): - """Test HTTP function which imports from another file - - Args: - request: The HTTP request which triggered this function. - - Returns: - The imported return value and status code defined for the given mode. - """ - return bar diff --git a/tests/test_functions/missing_function_file/dummy_file b/tests/test_functions/missing_function_file/dummy_file deleted file mode 100644 index 195ebe3..0000000 --- a/tests/test_functions/missing_function_file/dummy_file +++ /dev/null @@ -1 +0,0 @@ -This is not a file with user's function. diff --git a/tests/test_functions/module_is_correct/main.py b/tests/test_functions/module_is_correct/main.py deleted file mode 100644 index 06d2f97..0000000 --- a/tests/test_functions/module_is_correct/main.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os.path -import typing - - -class TestClass: - pass - - -def function(request): - # Ensure that the module for any object in this file is set correctly - _, filename = os.path.split(__file__) - name, _ = os.path.splitext(filename) - assert TestClass.__mro__[0].__module__ == name - - # Ensure that calling `get_type_hints` on an object in this file succeeds - assert typing.get_type_hints(TestClass) == {} - - return "OK" diff --git a/tests/test_functions/relative_imports/main.py b/tests/test_functions/relative_imports/main.py deleted file mode 100644 index 3c28ff1..0000000 --- a/tests/test_functions/relative_imports/main.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Function used in test for relative imports.""" - -from .test import foo - - -def function(request): - """Test HTTP function who returns a value from a relative import""" - return foo diff --git a/tests/test_functions/relative_imports/test.py b/tests/test_functions/relative_imports/test.py deleted file mode 100644 index 862c735..0000000 --- a/tests/test_functions/relative_imports/test.py +++ /dev/null @@ -1 +0,0 @@ -foo = "success" diff --git a/tests/test_functions/returns_none/main.py b/tests/test_functions/returns_none/main.py deleted file mode 100644 index 9bd68bd..0000000 --- a/tests/test_functions/returns_none/main.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -def function(request): - """Test HTTP function when using legacy GCF behavior. - - The function returns None, which should be a 200 response. - - Args: - request: The HTTP request which triggered this function. - - Returns: - None. - """ - return None diff --git a/tests/test_http.py b/tests/test_http.py deleted file mode 100644 index bd301c9..0000000 --- a/tests/test_http.py +++ /dev/null @@ -1,134 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import platform -import sys - -import pretend -import pytest - -import functions_framework._http - - -@pytest.mark.parametrize("debug", [True, False]) -def test_create_server(monkeypatch, debug): - server_stub = pretend.stub() - httpserver = pretend.call_recorder(lambda *a, **kw: server_stub) - monkeypatch.setattr(functions_framework._http, "HTTPServer", httpserver) - wsgi_app = pretend.stub() - options = {"a": pretend.stub(), "b": pretend.stub()} - - functions_framework._http.create_server(wsgi_app, debug, **options) - - assert httpserver.calls == [pretend.call(wsgi_app, debug, **options)] - - -@pytest.mark.parametrize( - "debug, gunicorn_missing, expected", - [ - (True, False, "flask"), - (False, False, "flask" if platform.system() == "Windows" else "gunicorn"), - (True, True, "flask"), - (False, True, "flask"), - ], -) -def test_httpserver(monkeypatch, debug, gunicorn_missing, expected): - app = pretend.stub() - http_server = pretend.stub(run=pretend.call_recorder(lambda: None)) - server_classes = { - "flask": pretend.call_recorder(lambda *a, **kw: http_server), - "gunicorn": pretend.call_recorder(lambda *a, **kw: http_server), - } - options = {"a": pretend.stub(), "b": pretend.stub()} - - monkeypatch.setattr( - functions_framework._http, "FlaskApplication", server_classes["flask"] - ) - if gunicorn_missing or platform.system() == "Windows": - monkeypatch.setitem(sys.modules, "functions_framework._http.gunicorn", None) - else: - from functions_framework._http import gunicorn - - monkeypatch.setattr(gunicorn, "GunicornApplication", server_classes["gunicorn"]) - - wrapper = functions_framework._http.HTTPServer(app, debug, **options) - - assert wrapper.app == app - assert wrapper.server_class == server_classes[expected] - assert wrapper.options == options - - host = pretend.stub() - port = pretend.stub() - - wrapper.run(host, port) - - assert wrapper.server_class.calls == [ - pretend.call(app, host, port, debug, **options) - ] - assert http_server.run.calls == [pretend.call()] - - -@pytest.mark.skipif("platform.system() == 'Windows'") -@pytest.mark.parametrize("debug", [True, False]) -def test_gunicorn_application(debug): - app = pretend.stub() - host = "1.2.3.4" - port = "1234" - options = {} - - import functions_framework._http.gunicorn - - gunicorn_app = functions_framework._http.gunicorn.GunicornApplication( - app, host, port, debug, **options - ) - - assert gunicorn_app.app == app - assert gunicorn_app.options == { - "bind": "%s:%s" % (host, port), - "workers": 1, - "threads": 8, - "timeout": 0, - "loglevel": "error", - "limit_request_line": 0, - } - - assert gunicorn_app.cfg.bind == ["1.2.3.4:1234"] - assert gunicorn_app.cfg.workers == 1 - assert gunicorn_app.cfg.threads == 8 - assert gunicorn_app.cfg.timeout == 0 - assert gunicorn_app.load() == app - - -@pytest.mark.parametrize("debug", [True, False]) -def test_flask_application(debug): - app = pretend.stub(run=pretend.call_recorder(lambda *a, **kw: None)) - host = pretend.stub() - port = pretend.stub() - options = {"a": pretend.stub(), "b": pretend.stub()} - - flask_app = functions_framework._http.flask.FlaskApplication( - app, host, port, debug, **options - ) - - assert flask_app.app == app - assert flask_app.host == host - assert flask_app.port == port - assert flask_app.debug == debug - assert flask_app.options == options - - flask_app.run() - - assert app.run.calls == [ - pretend.call(host, port, debug=debug, a=options["a"], b=options["b"]), - ] diff --git a/tests/test_main.py b/tests/test_main.py deleted file mode 100644 index 91df82c..0000000 --- a/tests/test_main.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pretend - -import functions_framework._cli - - -def test_main(monkeypatch): - _cli = pretend.call_recorder(lambda prog_name: None) - monkeypatch.setattr(functions_framework._cli, "_cli", _cli) - - from functions_framework import __main__ - - assert _cli.calls == [pretend.call(prog_name="python -m functions_framework")] diff --git a/tests/test_samples.py b/tests/test_samples.py deleted file mode 100644 index 65cee7d..0000000 --- a/tests/test_samples.py +++ /dev/null @@ -1,44 +0,0 @@ -import pathlib -import sys -import time - -import docker -import pytest -import requests - -EXAMPLES_DIR = pathlib.Path(__file__).resolve().parent.parent / "examples" - - -@pytest.mark.skipif( - sys.platform != "linux", reason="docker only works on linux in GH actions" -) -class TestSamples: - def stop_all_containers(self, docker_client): - containers = docker_client.containers.list() - for container in containers: - container.stop() - - @pytest.mark.slow_integration_test - def test_cloud_run_http(self): - client = docker.from_env() - self.stop_all_containers(client) - - TAG = "cloud_run_http" - client.images.build(path=str(EXAMPLES_DIR / "cloud_run_http"), tag={TAG}) - container = client.containers.run(image=TAG, detach=True, ports={8080: 8080}) - timeout = 10 - success = False - while success == False and timeout > 0: - try: - response = requests.get("http://localhost:8080/") - if response.text == "Hello world!": - success = True - except: - pass - - time.sleep(1) - timeout -= 1 - - container.stop() - - assert success diff --git a/tests/test_triggers.py b/tests/test_triggers.py new file mode 100644 index 0000000..0150dfa --- /dev/null +++ b/tests/test_triggers.py @@ -0,0 +1,71 @@ +# Copyright 2023 The OpenFunction Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import threading +import time +import unittest + +from unittest.mock import MagicMock, Mock + +from src.functions_framework.context.function_context import DaprTrigger +from src.functions_framework.context.runtime_context import RuntimeContext +from src.functions_framework.triggers.dapr_trigger.dapr import DaprTriggerHandler +from src.functions_framework.triggers.http_trigger import _http +from src.functions_framework.triggers.http_trigger.http import HTTPTriggerHandler + + +class TestHttpTrigger(unittest.TestCase): + def test_http_server_creation(self): + app = MagicMock() + server = _http.create_server(app, debug=True) + self.assertIsNotNone(server) + + def test_http_trigger_handler_start(self): + trigger = HTTPTriggerHandler(port=0, trigger=MagicMock(), user_function=MagicMock()) + context = MagicMock() + with self.assertRaises(Exception): + trigger.start(context) + + +class TestDaprTrigger(unittest.TestCase): + def test_dapr_trigger_handler(self): + # Create a mock RuntimeContext and logger + context = Mock(spec=RuntimeContext) + logger = Mock() + + # Create an example DaprTrigger + dapr_trigger = DaprTrigger( + name="example", + component_type="pubsub.redis", + topic="example-topic", + ) + + # Create an instance of DaprTriggerHandler and start it + dapr_handler = DaprTriggerHandler(port=50055, triggers=[dapr_trigger]) + + # Start the Dapr trigger handler in a separate thread + dapr_thread = threading.Thread(target=dapr_handler.start, args=(context, logger)) + dapr_thread.start() + + # Wait for 5 seconds + time.sleep(5) + + # Stop the Dapr trigger handler + dapr_handler.app.stop() + + # Wait for the Dapr trigger handler thread to finish + dapr_thread.join() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_view_functions.py b/tests/test_view_functions.py deleted file mode 100644 index 8de543d..0000000 --- a/tests/test_view_functions.py +++ /dev/null @@ -1,210 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import json - -import pretend -import pytest -import werkzeug - -from cloudevents.http import from_http - -import functions_framework - - -def test_http_view_func_wrapper(): - function = pretend.call_recorder(lambda request: "Hello") - request_object = pretend.stub() - local_proxy = pretend.stub(_get_current_object=lambda: request_object) - - view_func = functions_framework._http_view_func_wrapper(function, local_proxy) - view_func("/some/path") - - assert function.calls == [pretend.call(request_object)] - - -def test_event_view_func_wrapper(monkeypatch): - data = pretend.stub() - json = { - "context": { - "eventId": "some-eventId", - "timestamp": "some-timestamp", - "eventType": "some-eventType", - "resource": "some-resource", - }, - "data": data, - } - request = pretend.stub(headers={}, get_json=lambda: json) - - context_stub = pretend.stub() - context_class = pretend.call_recorder(lambda *a, **kw: context_stub) - monkeypatch.setattr(functions_framework, "Context", context_class) - function = pretend.call_recorder(lambda data, context: "Hello") - - view_func = functions_framework._event_view_func_wrapper(function, request) - view_func("/some/path") - - assert function.calls == [pretend.call(data, context_stub)] - assert context_class.calls == [ - pretend.call( - eventId="some-eventId", - timestamp="some-timestamp", - eventType="some-eventType", - resource="some-resource", - ) - ] - - -def test_event_view_func_wrapper_bad_request(monkeypatch): - request = pretend.stub(headers={}, get_json=lambda: None) - - context_stub = pretend.stub() - context_class = pretend.call_recorder(lambda *a, **kw: context_stub) - monkeypatch.setattr(functions_framework, "Context", context_class) - function = pretend.call_recorder(lambda data, context: "Hello") - - view_func = functions_framework._event_view_func_wrapper(function, request) - - with pytest.raises(werkzeug.exceptions.BadRequest): - view_func("/some/path") - - -def test_run_cloud_event(): - headers = {"Content-Type": "application/cloudevents+json"} - data = json.dumps( - { - "source": "from-galaxy-far-far-away", - "type": "cloud_event.greet.you", - "specversion": "1.0", - "id": "f6a65fcd-eed2-429d-9f71-ec0663d83025", - "time": "2020-08-13T02:12:14.946587+00:00", - "data": {"name": "john"}, - } - ) - request = pretend.stub(headers=headers, get_data=lambda: data) - - function = pretend.call_recorder(lambda cloud_event: "hello") - functions_framework._run_cloud_event(function, request) - expected_cloud_event = from_http(request.headers, request.get_data()) - - assert function.calls == [pretend.call(expected_cloud_event)] - - -def test_cloud_event_view_func_wrapper(): - headers = {"Content-Type": "application/cloudevents+json"} - data = json.dumps( - { - "source": "from-galaxy-far-far-away", - "type": "cloud_event.greet.you", - "specversion": "1.0", - "id": "f6a65fcd-eed2-429d-9f71-ec0663d83025", - "time": "2020-08-13T02:12:14.946587+00:00", - "data": {"name": "john"}, - } - ) - - request = pretend.stub(headers=headers, get_data=lambda: data) - event = from_http(request.headers, request.get_data()) - - function = pretend.call_recorder(lambda cloud_event: cloud_event) - - view_func = functions_framework._cloud_event_view_func_wrapper(function, request) - view_func("/some/path") - - assert function.calls == [pretend.call(event)] - - -def test_binary_cloud_event_view_func_wrapper(): - headers = { - "ce-specversion": "1.0", - "ce-source": "from-galaxy-far-far-away", - "ce-type": "cloud_event.greet.you", - "ce-id": "f6a65fcd-eed2-429d-9f71-ec0663d83025", - "ce-time": "2020-08-13T02:12:14.946587+00:00", - } - data = json.dumps({"name": "john"}) - - request = pretend.stub(headers=headers, get_data=lambda: data) - event = from_http(request.headers, request.get_data()) - - function = pretend.call_recorder(lambda cloud_event: cloud_event) - - view_func = functions_framework._cloud_event_view_func_wrapper(function, request) - view_func("/some/path") - - assert function.calls == [pretend.call(event)] - - -def test_binary_event_view_func_wrapper(monkeypatch): - data = pretend.stub() - request = pretend.stub( - headers={ - "ce-type": "something", - "ce-specversion": "something", - "ce-source": "something", - "ce-id": "something", - "ce-eventId": "some-eventId", - "ce-timestamp": "some-timestamp", - "ce-eventType": "some-eventType", - "ce-resource": "some-resource", - }, - get_data=lambda: data, - ) - - context_stub = pretend.stub() - context_class = pretend.call_recorder(lambda *a, **kw: context_stub) - monkeypatch.setattr(functions_framework, "Context", context_class) - function = pretend.call_recorder(lambda data, context: "Hello") - - view_func = functions_framework._event_view_func_wrapper(function, request) - view_func("/some/path") - - assert function.calls == [pretend.call(data, context_stub)] - assert context_class.calls == [ - pretend.call( - eventId="some-eventId", - timestamp="some-timestamp", - eventType="some-eventType", - resource="some-resource", - ) - ] - - -def test_legacy_event_view_func_wrapper(monkeypatch): - data = pretend.stub() - json = { - "eventId": "some-eventId", - "timestamp": "some-timestamp", - "eventType": "some-eventType", - "resource": "some-resource", - "data": data, - } - request = pretend.stub(headers={}, get_json=lambda: json) - - context_stub = pretend.stub() - context_class = pretend.call_recorder(lambda *a, **kw: context_stub) - monkeypatch.setattr(functions_framework, "Context", context_class) - function = pretend.call_recorder(lambda data, context: "Hello") - - view_func = functions_framework._event_view_func_wrapper(function, request) - view_func("/some/path") - - assert function.calls == [pretend.call(data, context_stub)] - assert context_class.calls == [ - pretend.call( - eventId="some-eventId", - timestamp="some-timestamp", - eventType="some-eventType", - resource="some-resource", - ) - ] diff --git a/tox.ini b/tox.ini index fb76a0e..bdd3692 100644 --- a/tox.ini +++ b/tox.ini @@ -9,8 +9,8 @@ deps = pytest-integration pretend setenv = - PYTESTARGS = --cov=functions_framework --cov-branch --cov-report term-missing --cov-fail-under=100 - windows-latest: PYTESTARGS = + PYTESTARGS = --cov=functions_framework --cov-branch --cov-report term-missing --cov-fail-under=50 --ignore=tests/test_data + windows-latest: PYTESTARGS = --cov=functions_framework --cov-branch --cov-report term-missing --cov-fail-under=50 --ignore=tests/test_data commands = pytest {env:PYTESTARGS} {posargs} [testenv:lint] @@ -20,7 +20,19 @@ deps = twine isort commands = - black --check src tests setup.py conftest.py --exclude tests/test_functions/background_load_error/main.py + black --check src tests setup.py conftest.py --exclude tests/ isort -c src tests setup.py conftest.py python setup.py --quiet sdist bdist_wheel twine check dist/* + +[testenv:triggers] +deps = + pytest-cov + pytest-integration +commands = pytest -s tests/test_triggers.py + +[testenv:cli] +deps = + pytest-cov + pytest-integration +commands = pytest -s tests/test_cli.py