diff --git a/.github/ISSUE_TEMPLATE/deferred_bindings_bug_report.yml b/.github/ISSUE_TEMPLATE/deferred_bindings_bug_report.yml deleted file mode 100644 index fecce8046..000000000 --- a/.github/ISSUE_TEMPLATE/deferred_bindings_bug_report.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: Python Worker Deferred Bindings Bug Report -description: File a Deferred Bindings bug report -title: "[Bug] Bug Title Here" -labels: ["python", "bug", "deferred-bindings"] -body: - - type: markdown - attributes: - value: | - This form will help you to fill in a bug report for the Azure Functions Python Worker Deferred Bindings feature. - - - type: textarea - id: expected-behavior - attributes: - label: Expected Behavior - description: A clear and concise description of what you expected to happen. - placeholder: What should have occurred? - - - type: textarea - id: actual-behavior - attributes: - label: Actual Behavior - description: A clear and concise description of what actually happened. - placeholder: What went wrong? - - - type: textarea - id: reproduction-steps - attributes: - label: Steps to Reproduce - description: Please provide detailed step-by-step instructions on how to reproduce the bug. - placeholder: | - 1. Go to the [specific page or section] in the application. - 2. Click on [specific button or link]. - 3. Scroll down to [specific location]. - 4. Observe [describe what you see, e.g., an error message or unexpected behavior]. - 5. Include any additional steps or details that may be relevant. - - - type: textarea - id: code-snippet - attributes: - label: Relevant code being tried - description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. - render: shell - - - type: textarea - id: logs - attributes: - label: Relevant log output - description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. - render: shell - - - type: textarea - id: requirements - attributes: - label: requirements.txt file - description: Please copy and paste your requirements.txt file. This will be automatically formatted into code, so no need for backticks. - render: shell - - - type: dropdown - id: environment - attributes: - label: Where are you facing this problem? - default: 0 - options: - - Local - Core Tools - - Production Environment (explain below) - - - type: textarea - id: additional-info - attributes: - label: Additional Information - description: Add any other information about the problem here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/deferred_bindings_feature_request.yml b/.github/ISSUE_TEMPLATE/deferred_bindings_feature_request.yml deleted file mode 100644 index 809771438..000000000 --- a/.github/ISSUE_TEMPLATE/deferred_bindings_feature_request.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Python Worker Deferred Bindings Feature Request -description: File a Deferred Bindings Feature report -title: "Request a feature" -labels: ["python", "feature", "deferred-bindings"] -body: - - type: markdown - attributes: - value: | - This form will help you to fill in a feature request for the Azure Functions Python Worker Deferred Bindings feature. - - - type: textarea - id: binding-type - attributes: - label: Binding Type - description: Add information about the binding type. - placeholder: Is this on an existing binding or new binding? - - - type: textarea - id: expected-behavior - attributes: - label: Expected Behavior - description: A clear and concise description of what you expected to happen. - placeholder: What should have occurred? - - - type: textarea - id: code-snippet - attributes: - label: Relevant sample code snipped - description: Please copy and paste any relevant code snippet of how you want the feature to be used. (This will be automatically formatted into code, so no need for backticks) - render: shell - - - type: textarea - id: additional-info - attributes: - label: Additional Information - description: Add any other information about the problem here. \ No newline at end of file diff --git a/eng/ci/emulator-tests.yml b/eng/ci/emulator-tests.yml index 1e2bbbdc5..c7792ea94 100644 --- a/eng/ci/emulator-tests.yml +++ b/eng/ci/emulator-tests.yml @@ -34,6 +34,7 @@ variables: - template: /ci/variables/build.yml@eng - template: /ci/variables/cfs.yml@eng - template: /eng/templates/utils/variables.yml@self + - template: /eng/templates/utils/emulator-variables.yml@self extends: template: v1/1ES.Unofficial.PipelineTemplate.yml@1es diff --git a/eng/ci/public-build.yml b/eng/ci/public-build.yml index 1b0e5fe4c..69ae8576f 100644 --- a/eng/ci/public-build.yml +++ b/eng/ci/public-build.yml @@ -28,6 +28,7 @@ resources: variables: - template: /eng/templates/utils/variables.yml@self + - template: /eng/templates/utils/emulator-variables.yml@self extends: template: v1/1ES.Unofficial.PipelineTemplate.yml@1es diff --git a/eng/templates/jobs/ci-emulator-tests.yml b/eng/templates/jobs/ci-emulator-tests.yml index 8b784e4f7..ee4874557 100644 --- a/eng/templates/jobs/ci-emulator-tests.yml +++ b/eng/templates/jobs/ci-emulator-tests.yml @@ -90,7 +90,8 @@ jobs: - bash: | python -m pytest -q --dist loadfile --reruns 4 --ignore=tests/emulator_tests/test_servicebus_functions.py tests/emulator_tests env: - AzureWebJobsStorage: "UseDevelopmentStorage=true" + AzureWebJobsStorage: $(AzureWebJobsStorage) + AZURE_STORAGE_CONNECTION_STRING: $(AZURE_STORAGE_CONNECTION_STRING) AzureWebJobsEventHubConnectionString: $(EmulatorEventHubConnectionString) AzureWebJobsCosmosDBConnectionString: $(EmulatorCosmosDBConnectionString) CosmosDBEmulatorUrl: $(CosmosDBEmulatorUrl) @@ -109,7 +110,8 @@ jobs: - bash: | python -m pytest -q --dist loadfile --reruns 4 tests/emulator_tests/test_servicebus_functions.py env: - AzureWebJobsStorage: "UseDevelopmentStorage=true" + AzureWebJobsStorage: $(AzureWebJobsStorage) AzureWebJobsServiceBusConnectionString: $(EmulatorServiceBusConnectionString) + AzureWebJobsServiceBusSDKConnectionString: $(AzureWebJobsServiceBusSDKConnectionString) workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PROJECT_DIRECTORY }} displayName: "Running $(PYTHON_VERSION) Python ServiceBus Linux Emulator Tests" diff --git a/eng/templates/jobs/ci-unit-tests.yml b/eng/templates/jobs/ci-unit-tests.yml index 78ecc3f3c..cb44ef65a 100644 --- a/eng/templates/jobs/ci-unit-tests.yml +++ b/eng/templates/jobs/ci-unit-tests.yml @@ -63,5 +63,6 @@ jobs: condition: and(eq(variables.isSdkRelease, false), eq(variables.isExtensionsRelease, false), eq(variables['USETESTPYTHONSDK'], false), eq(variables['USETESTPYTHONEXTENSIONS'], false)) env: PYTHON_VERSION: $(PYTHON_VERSION) + AZURE_STORAGE_CONNECTION_STRING: $(AZURE_STORAGE_CONNECTION_STRING) workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PROJECT_DIRECTORY }} \ No newline at end of file diff --git a/eng/templates/official/jobs/ci-e2e-tests.yml b/eng/templates/official/jobs/ci-e2e-tests.yml index cac7bbd21..681d5aa01 100644 --- a/eng/templates/official/jobs/ci-e2e-tests.yml +++ b/eng/templates/official/jobs/ci-e2e-tests.yml @@ -153,7 +153,7 @@ jobs: Write-Host "skipTest: $(skipTest)" displayName: 'Display skipTest variable' - bash: | - python -m pytest -q --dist loadfile --reruns 4 --cov=./azure_functions_worker --cov-report xml --cov-branch --cov-append tests/endtoend tests/extension_tests/deferred_bindings_tests tests/extension_tests/http_v2_tests + python -m pytest -q --dist loadfile --reruns 4 --cov=./azure_functions_worker --cov-report xml --cov-branch --cov-append tests/endtoend env: AzureWebJobsStorage: $(STORAGE_CONNECTION) AzureWebJobsCosmosDBConnectionString: $(COSMOSDB_CONNECTION) diff --git a/eng/templates/utils/emulator-variables.yml b/eng/templates/utils/emulator-variables.yml new file mode 100644 index 000000000..0f4ce54fb --- /dev/null +++ b/eng/templates/utils/emulator-variables.yml @@ -0,0 +1,2 @@ +variables: + - group: python-emulator-resources \ No newline at end of file diff --git a/workers/pyproject.toml b/workers/pyproject.toml index 6e9b04221..5d855dc4a 100644 --- a/workers/pyproject.toml +++ b/workers/pyproject.toml @@ -82,6 +82,7 @@ dev = [ "pre-commit", "invoke", "cryptography", + "jsonpickle", "orjson" ] test-http-v2 = [ @@ -90,7 +91,8 @@ test-http-v2 = [ ] test-deferred-bindings = [ "azurefunctions-extensions-bindings-blob==1.1.1", - "azurefunctions-extensions-bindings-eventhub==1.0.0b1" + "azurefunctions-extensions-bindings-eventhub==1.0.0b1", + "azurefunctions-extensions-bindings-servicebus==1.0.0b1" ] [build-system] diff --git a/workers/tests/extension_tests/deferred_bindings_tests/deferred_bindings_blob_functions/function_app.py b/workers/tests/emulator_tests/blob_functions/blob_functions_sdk/function_app.py similarity index 85% rename from workers/tests/extension_tests/deferred_bindings_tests/deferred_bindings_blob_functions/function_app.py rename to workers/tests/emulator_tests/blob_functions/blob_functions_sdk/function_app.py index 087df4be9..b8adc3f33 100644 --- a/workers/tests/extension_tests/deferred_bindings_tests/deferred_bindings_blob_functions/function_app.py +++ b/workers/tests/emulator_tests/blob_functions/blob_functions_sdk/function_app.py @@ -11,7 +11,7 @@ @app.function_name(name="put_bc_trigger") @app.blob_output(arg_name="file", path="python-worker-tests/test-blobclient-trigger.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.route(route="put_bc_trigger") def put_bc_trigger(req: func.HttpRequest, file: func.Out[str]) -> str: file.set(req.get_body()) @@ -21,10 +21,10 @@ def put_bc_trigger(req: func.HttpRequest, file: func.Out[str]) -> str: @app.function_name(name="bc_blob_trigger") @app.blob_trigger(arg_name="client", path="python-worker-tests/test-blobclient-trigger.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.blob_output(arg_name="$return", path="python-worker-tests/test-blobclient-triggered.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def bc_blob_trigger(client: blob.BlobClient) -> str: blob_properties = client.get_blob_properties() file = client.download_blob(encoding='utf-8').readall() @@ -38,7 +38,7 @@ def bc_blob_trigger(client: blob.BlobClient) -> str: @app.function_name(name="get_bc_blob_triggered") @app.blob_input(arg_name="client", path="python-worker-tests/test-blobclient-triggered.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.route(route="get_bc_blob_triggered") def get_bc_blob_triggered(req: func.HttpRequest, client: blob.BlobClient) -> str: @@ -48,7 +48,7 @@ def get_bc_blob_triggered(req: func.HttpRequest, @app.function_name(name="put_cc_trigger") @app.blob_output(arg_name="file", path="python-worker-tests/test-containerclient-trigger.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.route(route="put_cc_trigger") def put_cc_trigger(req: func.HttpRequest, file: func.Out[str]) -> str: file.set(req.get_body()) @@ -58,10 +58,10 @@ def put_cc_trigger(req: func.HttpRequest, file: func.Out[str]) -> str: @app.function_name(name="cc_blob_trigger") @app.blob_trigger(arg_name="client", path="python-worker-tests/test-containerclient-trigger.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.blob_output(arg_name="$return", path="python-worker-tests/test-containerclient-triggered.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def cc_blob_trigger(client: blob.ContainerClient) -> str: container_properties = client.get_container_properties() file = client.download_blob("test-containerclient-trigger.txt", @@ -75,7 +75,7 @@ def cc_blob_trigger(client: blob.ContainerClient) -> str: @app.function_name(name="get_cc_blob_triggered") @app.blob_input(arg_name="client", path="python-worker-tests/test-containerclient-triggered.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.route(route="get_cc_blob_triggered") def get_cc_blob_triggered(req: func.HttpRequest, client: blob.ContainerClient) -> str: @@ -86,7 +86,7 @@ def get_cc_blob_triggered(req: func.HttpRequest, @app.function_name(name="put_ssd_trigger") @app.blob_output(arg_name="file", path="python-worker-tests/test-ssd-trigger.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.route(route="put_ssd_trigger") def put_ssd_trigger(req: func.HttpRequest, file: func.Out[str]) -> str: file.set(req.get_body()) @@ -96,10 +96,10 @@ def put_ssd_trigger(req: func.HttpRequest, file: func.Out[str]) -> str: @app.function_name(name="ssd_blob_trigger") @app.blob_trigger(arg_name="stream", path="python-worker-tests/test-ssd-trigger.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.blob_output(arg_name="$return", path="python-worker-tests/test-ssd-triggered.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def ssd_blob_trigger(stream: blob.StorageStreamDownloader) -> str: # testing chunking file = "" @@ -113,7 +113,7 @@ def ssd_blob_trigger(stream: blob.StorageStreamDownloader) -> str: @app.function_name(name="get_ssd_blob_triggered") @app.blob_input(arg_name="stream", path="python-worker-tests/test-ssd-triggered.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.route(route="get_ssd_blob_triggered") def get_ssd_blob_triggered(req: func.HttpRequest, stream: blob.StorageStreamDownloader) -> str: @@ -124,7 +124,7 @@ def get_ssd_blob_triggered(req: func.HttpRequest, @app.route(route="get_bc_bytes") @app.blob_input(arg_name="client", path="python-worker-tests/test-blob-extension-bytes.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def get_bc_bytes(req: func.HttpRequest, client: blob.BlobClient) -> str: return client.download_blob(encoding='utf-8').readall() @@ -133,7 +133,7 @@ def get_bc_bytes(req: func.HttpRequest, client: blob.BlobClient) -> str: @app.route(route="get_cc_bytes") @app.blob_input(arg_name="client", path="python-worker-tests/test-blob-extension-bytes.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def get_cc_bytes(req: func.HttpRequest, client: blob.ContainerClient) -> str: return client.download_blob("test-blob-extension-bytes.txt", @@ -144,7 +144,7 @@ def get_cc_bytes(req: func.HttpRequest, @app.route(route="get_ssd_bytes") @app.blob_input(arg_name="stream", path="python-worker-tests/test-blob-extension-bytes.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def get_ssd_bytes(req: func.HttpRequest, stream: blob.StorageStreamDownloader) -> str: return stream.readall().decode('utf-8') @@ -154,7 +154,7 @@ def get_ssd_bytes(req: func.HttpRequest, @app.route(route="get_bc_str") @app.blob_input(arg_name="client", path="python-worker-tests/test-blob-extension-str.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def get_bc_str(req: func.HttpRequest, client: blob.BlobClient) -> str: return client.download_blob(encoding='utf-8').readall() @@ -163,7 +163,7 @@ def get_bc_str(req: func.HttpRequest, client: blob.BlobClient) -> str: @app.route(route="get_cc_str") @app.blob_input(arg_name="client", path="python-worker-tests", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def get_cc_str(req: func.HttpRequest, client: blob.ContainerClient) -> str: return client.download_blob("test-blob-extension-str.txt", encoding='utf-8').readall() @@ -173,7 +173,7 @@ def get_cc_str(req: func.HttpRequest, client: blob.ContainerClient) -> str: @app.route(route="get_ssd_str") @app.blob_input(arg_name="stream", path="python-worker-tests/test-blob-extension-str.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def get_ssd_str(req: func.HttpRequest, stream: blob.StorageStreamDownloader) -> str: return stream.readall().decode('utf-8') @@ -183,11 +183,11 @@ def get_ssd_str(req: func.HttpRequest, stream: blob.StorageStreamDownloader) -> @app.blob_input(arg_name="client", path="python-worker-tests/test-blob-extension-str.txt", data_type="STRING", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.blob_input(arg_name="blob", path="python-worker-tests/test-blob-extension-str.txt", data_type="STRING", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def bc_and_inputstream_input(req: func.HttpRequest, client: blob.BlobClient, blob: func.InputStream) -> str: output_msg = "" @@ -202,11 +202,11 @@ def bc_and_inputstream_input(req: func.HttpRequest, client: blob.BlobClient, @app.blob_input(arg_name="blob", path="python-worker-tests/test-blob-extension-str.txt", data_type="STRING", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.blob_input(arg_name="client", path="python-worker-tests/test-blob-extension-str.txt", data_type="STRING", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def inputstream_and_bc_input(req: func.HttpRequest, blob: func.InputStream, client: blob.BlobClient) -> str: output_msg = "" @@ -221,7 +221,7 @@ def inputstream_and_bc_input(req: func.HttpRequest, blob: func.InputStream, @app.blob_input(arg_name="file", path="python-worker-tests/test-blob-extension-str.txt", data_type="STRING", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def type_undefined(req: func.HttpRequest, file) -> str: assert not isinstance(file, blob.BlobClient) assert not isinstance(file, blob.ContainerClient) @@ -232,7 +232,7 @@ def type_undefined(req: func.HttpRequest, file) -> str: @app.function_name(name="put_blob_str") @app.blob_output(arg_name="file", path="python-worker-tests/test-blob-extension-str.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.route(route="put_blob_str") def put_blob_str(req: func.HttpRequest, file: func.Out[str]) -> str: file.set(req.get_body()) @@ -242,7 +242,7 @@ def put_blob_str(req: func.HttpRequest, file: func.Out[str]) -> str: @app.function_name(name="put_blob_bytes") @app.blob_output(arg_name="file", path="python-worker-tests/test-blob-extension-bytes.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.route(route="put_blob_bytes") def put_blob_bytes(req: func.HttpRequest, file: func.Out[bytes]) -> str: file.set(req.get_body()) diff --git a/workers/tests/emulator_tests/servicebus_functions/servicebus_functions_sdk/function_app.py b/workers/tests/emulator_tests/servicebus_functions/servicebus_functions_sdk/function_app.py new file mode 100644 index 000000000..aec143953 --- /dev/null +++ b/workers/tests/emulator_tests/servicebus_functions/servicebus_functions_sdk/function_app.py @@ -0,0 +1,51 @@ +import json +import jsonpickle + +import azure.functions as func +import azurefunctions.extensions.bindings.servicebus as sb + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + + +@app.route(route="put_message_sdk") +@app.service_bus_queue_output( + arg_name="msg", + connection="AzureWebJobsServiceBusSDKConnectionString", + queue_name="testqueue") +def put_message_sdk(req: func.HttpRequest, msg: func.Out[str]): + msg.set(req.get_body().decode('utf-8')) + return 'OK' + + +@app.route(route="get_servicebus_triggered_sdk") +@app.blob_input(arg_name="file", + path="python-worker-tests/test-servicebus-sdk-triggered.txt", + connection="AzureWebJobsStorage") +def get_servicebus_triggered_sdk(req: func.HttpRequest, + file: func.InputStream) -> str: + return func.HttpResponse( + file.read().decode('utf-8'), mimetype='application/json') + + +@app.service_bus_queue_trigger( + arg_name="msg", + connection="AzureWebJobsServiceBusSDKConnectionString", + queue_name="testqueue") +@app.blob_output(arg_name="$return", + path="python-worker-tests/test-servicebus-sdk-triggered.txt", + connection="AzureWebJobsStorage") +def servicebus_trigger_sdk(msg: sb.ServiceBusReceivedMessage) -> str: + msg_json = jsonpickle.encode(msg) + body_json = jsonpickle.encode(msg.body) + enqueued_time_json = jsonpickle.encode(msg.enqueued_time_utc) + lock_token_json = jsonpickle.encode(msg.lock_token) + result = json.dumps({ + 'message': msg_json, + 'body': body_json, + 'enqueued_time_utc': enqueued_time_json, + 'lock_token': lock_token_json, + 'message_id': msg.message_id, + 'sequence_number': msg.sequence_number + }) + + return result diff --git a/workers/tests/extension_tests/deferred_bindings_tests/test_deferred_bindings_blob_functions.py b/workers/tests/emulator_tests/test_deferred_bindings_blob_functions.py similarity index 98% rename from workers/tests/extension_tests/deferred_bindings_tests/test_deferred_bindings_blob_functions.py rename to workers/tests/emulator_tests/test_deferred_bindings_blob_functions.py index b9e4bd2a1..2dd124f48 100644 --- a/workers/tests/extension_tests/deferred_bindings_tests/test_deferred_bindings_blob_functions.py +++ b/workers/tests/emulator_tests/test_deferred_bindings_blob_functions.py @@ -13,8 +13,8 @@ class TestDeferredBindingsBlobFunctions(testutils.WebHostTestCase): @classmethod def get_script_dir(cls): - return testutils.EXTENSION_TESTS_FOLDER / 'deferred_bindings_tests' / \ - 'deferred_bindings_blob_functions' + return testutils.EMULATOR_TESTS_FOLDER / 'blob_functions' / \ + 'blob_functions_sdk' @classmethod def get_libraries_to_install(cls): diff --git a/workers/tests/emulator_tests/test_servicebus_functions.py b/workers/tests/emulator_tests/test_servicebus_functions.py index 2e6bd7310..cbd2962b3 100644 --- a/workers/tests/emulator_tests/test_servicebus_functions.py +++ b/workers/tests/emulator_tests/test_servicebus_functions.py @@ -1,7 +1,9 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. import json +import sys import time +import unittest from tests.utils import testutils @@ -63,3 +65,40 @@ class TestServiceBusFunctionsSteinGeneric(TestServiceBusFunctions): def get_script_dir(cls): return testutils.EMULATOR_TESTS_FOLDER / 'servicebus_functions' / \ 'servicebus_functions_stein' / 'generic' + + +@unittest.skipIf(sys.version_info.minor <= 8, "The servicebus extension" + "is only supported for 3.9+.") +class TestServiceBusSDKFunctions(testutils.WebHostTestCase): + + @classmethod + def get_script_dir(cls): + return testutils.EMULATOR_TESTS_FOLDER / 'servicebus_functions' / \ + 'servicebus_functions_sdk' + + @testutils.retryable_test(3, 5) + def test_servicebus_basic_sdk(self): + data = str(round(time.time())) + r = self.webhost.request('POST', 'put_message_sdk', + data=data) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, 'OK') + + max_retries = 10 + + for try_no in range(max_retries): + # wait for trigger to process the queue item + time.sleep(1) + + try: + r = self.webhost.request('GET', 'get_servicebus_triggered_sdk') + self.assertEqual(r.status_code, 200) + msg = r.json() + for attr in {'message', 'body', 'enqueued_time_utc', 'lock_token', + 'message_id', 'sequence_number'}: + self.assertIn(attr, msg) + except (AssertionError, json.JSONDecodeError): + if try_no == max_retries - 1: + raise + else: + break diff --git a/workers/tests/extension_tests/http_v2_tests/http_functions_v2/fastapi/function_app.py b/workers/tests/endtoend/http_functions_v2/fastapi/function_app.py similarity index 100% rename from workers/tests/extension_tests/http_v2_tests/http_functions_v2/fastapi/function_app.py rename to workers/tests/endtoend/http_functions_v2/fastapi/function_app.py diff --git a/workers/tests/endtoend/snake_case_functions/function_app.py b/workers/tests/endtoend/snake_case_functions/function_app.py new file mode 100644 index 000000000..904dffa33 --- /dev/null +++ b/workers/tests/endtoend/snake_case_functions/function_app.py @@ -0,0 +1,95 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import azure.functions as func + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + + +@app.route(route="classic_snake_case", trigger_arg_name="req_snake_snake_snake_snake") +def classic_snake_case(req_snake_snake_snake_snake: func.HttpRequest)\ + -> func.HttpResponse: + name = req_snake_snake_snake_snake.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="single_underscore", trigger_arg_name="_") +def single_underscore(_: func.HttpRequest) -> func.HttpResponse: + name = _.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="underscore_prefix", trigger_arg_name="_req") +def underscore_prefix(_req: func.HttpRequest) -> func.HttpResponse: + name = _req.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="underscore_prefix_snake", trigger_arg_name="_req_snake") +def underscore_prefix_snake(_req_snake: func.HttpRequest) -> func.HttpResponse: + name = _req_snake.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="underscore_suffix", trigger_arg_name="req_") +def underscore_suffix(req_: func.HttpRequest) -> func.HttpResponse: + name = req_.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="underscore_suffix_snake", trigger_arg_name="req_snake_") +def underscore_suffix_snake(req_snake_: func.HttpRequest) -> func.HttpResponse: + name = req_snake_.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="ultimate_combo", trigger_arg_name="_req_snake_snake_snake_snake_") +def ultimate_combo(_req_snake_snake_snake_snake_: func.HttpRequest)\ + -> func.HttpResponse: + name = _req_snake_snake_snake_snake_.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="sandwich", trigger_arg_name="_req_") +def sandwich(_req_: func.HttpRequest)\ + -> func.HttpResponse: + name = _req_.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="double_underscore", trigger_arg_name="req__snake") +def double_underscore(req__snake: func.HttpRequest) -> func.HttpResponse: + name = req__snake.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="double_underscore_prefix", trigger_arg_name="__req") +def classic_double_underscore(__req: func.HttpRequest) -> func.HttpResponse: + name = __req.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="double_underscore_suffix", trigger_arg_name="req__") +def double_underscore_suffix(req__: func.HttpRequest) -> func.HttpResponse: + name = req__.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="just_double_underscore", trigger_arg_name="__") +def just_double_underscore(__: func.HttpRequest) -> func.HttpResponse: + name = __.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="python_main_keyword", trigger_arg_name="__main__") +def python_main_keyword(__main__: func.HttpRequest) -> func.HttpResponse: + name = __main__.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="ultimate_combo2", + trigger_arg_name="__9req__snake__sna_ke________snake__sn0ke_") +def ultimate_combo2( + __9req__snake__sna_ke________snake__sn0ke_: func.HttpRequest)\ + -> func.HttpResponse: + name = __9req__snake__sna_ke________snake__sn0ke_.params.get('name') + return func.HttpResponse(f"Hello, {name}.") diff --git a/workers/tests/extension_tests/http_v2_tests/test_http_v2.py b/workers/tests/endtoend/test_http_v2.py similarity index 97% rename from workers/tests/extension_tests/http_v2_tests/test_http_v2.py rename to workers/tests/endtoend/test_http_v2.py index a41c840e7..da11f3f19 100644 --- a/workers/tests/extension_tests/http_v2_tests/test_http_v2.py +++ b/workers/tests/endtoend/test_http_v2.py @@ -44,9 +44,7 @@ def get_environment_variables(cls): @classmethod def get_script_dir(cls): - return testutils.EXTENSION_TESTS_FOLDER / 'http_v2_tests' / \ - 'http_functions_v2' / \ - 'fastapi' + return testutils.E2E_TESTS_FOLDER / 'http_functions_v2' / 'fastapi' @classmethod def get_libraries_to_install(cls): @@ -224,7 +222,7 @@ def get_environment_variables(cls): @classmethod def get_script_dir(cls): - return testutils.EXTENSION_TESTS_FOLDER / 'http_v2_tests' / \ + return testutils.E2E_TESTS_FOLDER / \ 'http_functions_v2' / \ 'fastapi' diff --git a/workers/tests/endtoend/test_snake_case_functions.py b/workers/tests/endtoend/test_snake_case_functions.py new file mode 100644 index 000000000..92aaa916f --- /dev/null +++ b/workers/tests/endtoend/test_snake_case_functions.py @@ -0,0 +1,166 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import os +from unittest.mock import patch + +from tests.utils import testutils + +REQUEST_TIMEOUT_SEC = 5 + + +class TestValidSnakeCaseFunctions(testutils.WebHostTestCase): + def setUp(self): + self._patch_environ = patch.dict('os.environ', os.environ.copy()) + self._patch_environ.start() + super().setUp() + + def tearDown(self): + super().tearDown() + self._patch_environ.stop() + + @classmethod + def get_script_dir(cls): + return testutils.E2E_TESTS_FOLDER / 'snake_case_functions' + + @testutils.retryable_test(3, 5) + def test_classic_snake_case(self): + r = self.webhost.request('GET', 'classic_snake_case', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_single_underscore(self): + r = self.webhost.request('GET', 'single_underscore', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_underscore_prefix(self): + r = self.webhost.request('GET', 'underscore_prefix', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_underscore_suffix(self): + r = self.webhost.request('GET', 'underscore_suffix', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_ultimate_combo(self): + r = self.webhost.request('GET', 'ultimate_combo', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_underscore_prefix_snake(self): + r = self.webhost.request('GET', 'underscore_prefix_snake', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_underscore_suffix_snake(self): + r = self.webhost.request('GET', 'underscore_suffix_snake', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_double_underscore(self): + r = self.webhost.request('GET', 'double_underscore', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_double_underscore_prefix(self): + r = self.webhost.request('GET', 'double_underscore_prefix', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_double_underscore_suffix(self): + r = self.webhost.request('GET', 'double_underscore_suffix', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_just_double_underscore(self): + r = self.webhost.request('GET', 'just_double_underscore', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_python_main_keyword(self): + r = self.webhost.request('GET', 'python_main_keyword', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_ultimate_combo2(self): + r = self.webhost.request('GET', 'ultimate_combo2', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) diff --git a/workers/tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_disabled/function_app.py b/workers/tests/unittests/deferred_bindings_functions/deferred_bindings_disabled/function_app.py similarity index 100% rename from workers/tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_disabled/function_app.py rename to workers/tests/unittests/deferred_bindings_functions/deferred_bindings_disabled/function_app.py diff --git a/workers/tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_enabled/function_app.py b/workers/tests/unittests/deferred_bindings_functions/deferred_bindings_enabled/function_app.py similarity index 100% rename from workers/tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_enabled/function_app.py rename to workers/tests/unittests/deferred_bindings_functions/deferred_bindings_enabled/function_app.py diff --git a/workers/tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_enabled_dual/function_app.py b/workers/tests/unittests/deferred_bindings_functions/deferred_bindings_enabled_dual/function_app.py similarity index 100% rename from workers/tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_enabled_dual/function_app.py rename to workers/tests/unittests/deferred_bindings_functions/deferred_bindings_enabled_dual/function_app.py diff --git a/workers/tests/extension_tests/deferred_bindings_tests/test_deferred_bindings.py b/workers/tests/unittests/test_deferred_bindings.py similarity index 91% rename from workers/tests/extension_tests/deferred_bindings_tests/test_deferred_bindings.py rename to workers/tests/unittests/test_deferred_bindings.py index b8ced2834..1e06750c0 100644 --- a/workers/tests/extension_tests/deferred_bindings_tests/test_deferred_bindings.py +++ b/workers/tests/unittests/test_deferred_bindings.py @@ -18,16 +18,13 @@ ContainerClient, StorageStreamDownloader) -DEFERRED_BINDINGS_ENABLED_DIR = testutils.EXTENSION_TESTS_FOLDER / \ - 'deferred_bindings_tests' / \ +DEFERRED_BINDINGS_ENABLED_DIR = testutils.UNIT_TESTS_FOLDER / \ 'deferred_bindings_functions' / \ 'deferred_bindings_enabled' -DEFERRED_BINDINGS_DISABLED_DIR = testutils.EXTENSION_TESTS_FOLDER / \ - 'deferred_bindings_tests' / \ +DEFERRED_BINDINGS_DISABLED_DIR = testutils.UNIT_TESTS_FOLDER / \ 'deferred_bindings_functions' / \ 'deferred_bindings_disabled' -DEFERRED_BINDINGS_ENABLED_DUAL_DIR = testutils.EXTENSION_TESTS_FOLDER / \ - 'deferred_bindings_tests' / \ +DEFERRED_BINDINGS_ENABLED_DUAL_DIR = testutils.UNIT_TESTS_FOLDER / \ 'deferred_bindings_functions' / \ 'deferred_bindings_enabled_dual' @@ -155,14 +152,15 @@ def test_mbd_deferred_bindings_enabled_decode(self): pb = protos.ParameterBinding(name='test', data=protos.TypedData( string='test')) - sample_mbd = MockMBD(version="1.0", - source="AzureStorageBlobs", - content_type="application/json", - content="{\"Connection\":\"AzureWebJobsStorage\"," - "\"ContainerName\":" - "\"python-worker-tests\"," - "\"BlobName\":" - "\"test-blobclient-trigger.txt\"}") + sample_mbd = MockMBD( + version="1.0", + source="AzureStorageBlobs", + content_type="application/json", + content="{\"Connection\":\"AZURE_STORAGE_CONNECTION_STRING\"," + "\"ContainerName\":" + "\"python-worker-tests\"," + "\"BlobName\":" + "\"test-blobclient-trigger.txt\"}") datum = datumdef.Datum(value=sample_mbd, type='model_binding_data') obj = meta.deferred_bindings_decode(binding=binding, pb=pb,