diff --git a/eng/ci/integration-tests.yml b/eng/ci/integration-tests.yml index d26898e2..5dfb6c7d 100644 --- a/eng/ci/integration-tests.yml +++ b/eng/ci/integration-tests.yml @@ -47,8 +47,7 @@ extends: dependsOn: [] jobs: - template: /eng/templates/official/jobs/ci-e2e-tests.yml@self -# Skipping consumption tests till pipeline is fixed -# - stage: RunLCTests -# dependsOn: [] -# jobs: -# - template: /eng/templates/official/jobs/ci-lc-tests.yml@self + - stage: RunLCTests + dependsOn: [] + jobs: + - template: /eng/templates/official/jobs/ci-lc-tests.yml@self diff --git a/eng/templates/official/jobs/ci-lc-tests.yml b/eng/templates/official/jobs/ci-lc-tests.yml index 14605f51..85cdc065 100644 --- a/eng/templates/official/jobs/ci-lc-tests.yml +++ b/eng/templates/official/jobs/ci-lc-tests.yml @@ -12,21 +12,34 @@ jobs: strategy: matrix: - Python37: - PYTHON_VERSION: '3.7' - Python38: - PYTHON_VERSION: '3.8' - Python39: - PYTHON_VERSION: '3.9' - Python310: - PYTHON_VERSION: '3.10' - Python311: - PYTHON_VERSION: '3.11' - + Python39: + PYTHON_VERSION: '3.9' + Python310: + PYTHON_VERSION: '3.10' + Python311: + PYTHON_VERSION: '3.11' + Python312: + PYTHON_VERSION: '3.12' steps: - task: UsePythonVersion@0 inputs: versionSpec: $(PYTHON_VERSION) + + - bash: | + # Start Azurite storage emulator in the background + docker run -d -p 10000:10000 -p 10001:10001 -p 10002:10002 \ + --name azurite-storage \ + mcr.microsoft.com/azure-storage/azurite:latest \ + azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 + + # Wait for Azurite to be ready + sleep 5 + + # Verify Azurite is running + docker ps | grep azurite-storage + displayName: 'Start Azurite Storage Emulator' + condition: and(eq(variables.isSdkRelease, false), eq(variables.isExtensionsRelease, false), eq(variables['USETESTPYTHONSDK'], false), eq(variables['USETESTPYTHONEXTENSIONS'], false)) + - bash: | python -m pip install --upgrade pip python -m pip install -U -e ${{ parameters.PROJECT_DIRECTORY }}/[dev] @@ -36,11 +49,69 @@ jobs: displayName: 'Install dependencies and the worker' # Skip the installation stage for SDK and Extensions release branches. This stage will fail because pyproject.toml contains the updated (and unreleased) library version condition: and(eq(variables.isSdkRelease, false), eq(variables.isExtensionsRelease, false), eq(variables['USETESTPYTHONSDK'], false), eq(variables['USETESTPYTHONEXTENSIONS'], false)) + + - bash: | + # Install Azure CLI (if not already present) + curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + + # Create the apps container in Azurite + az storage container create \ + --name apps \ + --connection-string "$(AZURE_STORAGE_CONNECTION_STRING)" + + # Upload all function app packages to the container + FUNCTION_APPS_DIR="$(Build.SourcesDirectory)/${{ parameters.PROJECT_DIRECTORY }}/tests/consumption_tests/function_app_zips" + for zipfile in "$FUNCTION_APPS_DIR"/*.zip; do + filename=$(basename "$zipfile") + echo "Uploading $filename..." + az storage blob upload \ + --container-name apps \ + --name "$filename" \ + --file "$zipfile" \ + --connection-string "$(AZURE_STORAGE_CONNECTION_STRING)" \ + --overwrite + done + + # Generate a container-level SAS token valid for 1 day + SAS_TOKEN=$(az storage container generate-sas \ + --name apps \ + --permissions r \ + --expiry $(date -u -d '+1 day' +%Y-%m-%dT%H:%M:%SZ) \ + --connection-string "$(AZURE_STORAGE_CONNECTION_STRING)" \ + --output tsv) + + echo "##vso[task.setvariable variable=CONTAINER_SAS_TOKEN]$SAS_TOKEN" + + # List blobs in the container to verify uploads + echo "Verifying uploaded blobs in 'apps' container..." + az storage blob list \ + --container-name apps \ + --connection-string "$(AZURE_STORAGE_CONNECTION_STRING)" \ + --output table + + env: + AZURE_STORAGE_CONNECTION_STRING: $(AZURE_STORAGE_CONNECTION_STRING) + + displayName: 'Setup Function App Packages in Azurite' + condition: and(eq(variables.isSdkRelease, false), eq(variables.isExtensionsRelease, false), eq(variables['USETESTPYTHONSDK'], false), eq(variables['USETESTPYTHONEXTENSIONS'], false)) + + - powershell: | + Write-Host "CONTAINER_SAS_TOKEN: $(CONTAINER_SAS_TOKEN)" + displayName: 'Display CONTAINER_SAS_TOKEN variable' + - bash: | - python -m pytest -n auto --dist loadfile -vv --reruns 4 --instafail tests/consumption_tests + python -m pytest -n auto --dist loadfile -vv --instafail tests/consumption_tests env: - AzureWebJobsStorage: $(LinuxStorageConnectionString312) + AzureWebJobsStorage: $(AZURE_STORAGE_CONNECTION_STRING) _DUMMY_CONT_KEY: $(_DUMMY_CONT_KEY) + CONTAINER_SAS_TOKEN: $(CONTAINER_SAS_TOKEN) displayName: "Running $(PYTHON_VERSION) Linux Consumption tests" workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PROJECT_DIRECTORY }} - condition: and(eq(variables.isSdkRelease, false), eq(variables.isExtensionsRelease, false), eq(variables['USETESTPYTHONSDK'], false), eq(variables['USETESTPYTHONEXTENSIONS'], false)) \ No newline at end of file + condition: and(eq(variables.isSdkRelease, false), eq(variables.isExtensionsRelease, false), eq(variables['USETESTPYTHONSDK'], false), eq(variables['USETESTPYTHONEXTENSIONS'], false)) + + - bash: | + # Cleanup: Stop and remove Azurite container + docker stop azurite-storage || true + docker rm azurite-storage || true + displayName: 'Cleanup Azurite Storage Emulator' + condition: always() \ No newline at end of file diff --git a/workers/tests/consumption_tests/function_app_zips/CommonLibraries.zip b/workers/tests/consumption_tests/function_app_zips/CommonLibraries.zip new file mode 100644 index 00000000..f288136e Binary files /dev/null and b/workers/tests/consumption_tests/function_app_zips/CommonLibraries.zip differ diff --git a/workers/tests/consumption_tests/function_app_zips/EnableDebugLogging.zip b/workers/tests/consumption_tests/function_app_zips/EnableDebugLogging.zip new file mode 100644 index 00000000..52d52ff8 Binary files /dev/null and b/workers/tests/consumption_tests/function_app_zips/EnableDebugLogging.zip differ diff --git a/workers/tests/consumption_tests/function_app_zips/HttpNoAuth.zip b/workers/tests/consumption_tests/function_app_zips/HttpNoAuth.zip new file mode 100644 index 00000000..3849553b Binary files /dev/null and b/workers/tests/consumption_tests/function_app_zips/HttpNoAuth.zip differ diff --git a/workers/tests/consumption_tests/function_app_zips/HttpV2FastApiStreaming.zip b/workers/tests/consumption_tests/function_app_zips/HttpV2FastApiStreaming.zip new file mode 100644 index 00000000..1538b722 Binary files /dev/null and b/workers/tests/consumption_tests/function_app_zips/HttpV2FastApiStreaming.zip differ diff --git a/workers/tests/consumption_tests/function_app_zips/OOMError.zip b/workers/tests/consumption_tests/function_app_zips/OOMError.zip new file mode 100644 index 00000000..74b28d6c Binary files /dev/null and b/workers/tests/consumption_tests/function_app_zips/OOMError.zip differ diff --git a/workers/tests/consumption_tests/function_app_zips/Opencensus.zip b/workers/tests/consumption_tests/function_app_zips/Opencensus.zip new file mode 100644 index 00000000..fbfb8ac9 Binary files /dev/null and b/workers/tests/consumption_tests/function_app_zips/Opencensus.zip differ diff --git a/workers/tests/consumption_tests/function_app_zips/PinningFunctions.zip b/workers/tests/consumption_tests/function_app_zips/PinningFunctions.zip new file mode 100644 index 00000000..8cf08d50 Binary files /dev/null and b/workers/tests/consumption_tests/function_app_zips/PinningFunctions.zip differ diff --git a/workers/tests/consumption_tests/test_linux_consumption.py b/workers/tests/consumption_tests/test_linux_consumption.py index 09f10fd9..109a6806 100644 --- a/workers/tests/consumption_tests/test_linux_consumption.py +++ b/workers/tests/consumption_tests/test_linux_consumption.py @@ -3,10 +3,7 @@ import os import sys from time import sleep -from unittest import TestCase, skipIf - -from requests import Request -from tests.utils.testutils_lc import LinuxConsumptionWebHostController +from unittest import TestCase, skip, skipIf from azure_functions_worker.constants import ( PYTHON_ENABLE_DEBUG_LOGGING, @@ -14,22 +11,13 @@ PYTHON_ENABLE_WORKER_EXTENSIONS, PYTHON_ISOLATE_WORKER_DEPENDENCIES, ) +from requests import Request +from tests.utils.testutils_lc import LinuxConsumptionWebHostController _DEFAULT_HOST_VERSION = "4" class TestLinuxConsumption(TestCase): - """Test worker behaviors on specific scenarios. - - SCM_RUN_FROM_PACKAGE: built function apps are acquired from - -> "Simple Batch" Subscription - -> "AzureFunctionsPythonWorkerCILinuxDevOps" Resource Group - -> "pythonworkersa" Storage Account - -> "python-worker-lc-apps" Blob Container - - For a list of scenario names: - https://pythonworker39sa.blob.core.windows.net/python-worker-lc-apps?restype=container&comp=list - """ @classmethod def setUpClass(cls): @@ -65,6 +53,8 @@ def test_http_no_auth(self): resp = ctrl.send_request(req) self.assertEqual(resp.status_code, 200) + @skipIf(sys.version_info.minor != 11, + "Uploaded common libraries are only supported for Python 3.11") def test_common_libraries(self): """A function app with the following requirements.txt: @@ -95,66 +85,6 @@ def test_common_libraries(self): self.assertIn('pyodbc', content) self.assertIn('requests', content) - @skipIf(sys.version_info.minor in (10, 11), - "Protobuf pinning fails during remote build") - def test_new_protobuf(self): - """A function app with the following requirements.txt: - - azure-functions==1.7.0 - protobuf==3.15.8 - grpcio==1.33.2 - - should return 200 after importing all libraries. - """ - with LinuxConsumptionWebHostController(_DEFAULT_HOST_VERSION, - self._py_version) as ctrl: - ctrl.assign_container(env={ - "AzureWebJobsStorage": self._storage, - "SCM_RUN_FROM_PACKAGE": self._get_blob_url("NewProtobuf"), - PYTHON_ISOLATE_WORKER_DEPENDENCIES: "1" - }) - req = Request('GET', f'{ctrl.url}/api/HttpTrigger') - resp = ctrl.send_request(req) - self.assertEqual(resp.status_code, 200) - - content = resp.json() - - # Worker always picks up the SDK version bundled with the image - # Version of the packages are inconsistent due to isolation's bug - self.assertEqual(content['azure.functions'], '1.7.0') - self.assertEqual(content['google.protobuf'], '3.15.8') - self.assertEqual(content['grpc'], '1.33.2') - - @skipIf(sys.version_info.minor in (10, 11), - "Protobuf pinning fails during remote build") - def test_old_protobuf(self): - """A function app with the following requirements.txt: - - azure-functions==1.5.0 - protobuf==3.8.0 - grpcio==1.27.1 - - should return 200 after importing all libraries. - """ - with LinuxConsumptionWebHostController(_DEFAULT_HOST_VERSION, - self._py_version) as ctrl: - ctrl.assign_container(env={ - "AzureWebJobsStorage": self._storage, - "SCM_RUN_FROM_PACKAGE": self._get_blob_url("OldProtobuf"), - PYTHON_ISOLATE_WORKER_DEPENDENCIES: "1" - }) - req = Request('GET', f'{ctrl.url}/api/HttpTrigger') - resp = ctrl.send_request(req) - self.assertEqual(resp.status_code, 200) - - content = resp.json() - - # Worker always picks up the SDK version bundled with the image - # Version of the packages are inconsistent due to isolation's bug - self.assertIn(content['azure.functions'], '1.5.0') - self.assertIn(content['google.protobuf'], '3.8.0') - self.assertIn(content['grpc'], '1.27.1') - def test_debug_logging_disabled(self): """An HttpTrigger function app with 'azure-functions' library should return 200 and by default customer debug logging should be @@ -230,8 +160,6 @@ def test_pinning_functions_to_older_version(self): self.assertEqual(resp.status_code, 200) self.assertIn("Func Version: 1.11.1", resp.text) - @skipIf(sys.version_info.minor != 10, - "This is testing only for python310") def test_opencensus_with_extensions_enabled(self): """A function app with extensions enabled containing the following libraries: @@ -251,8 +179,6 @@ def test_opencensus_with_extensions_enabled(self): resp = ctrl.send_request(req) self.assertEqual(resp.status_code, 200) - @skipIf(sys.version_info.minor != 10, - "This is testing only for python310") def test_opencensus_with_extensions_enabled_init_indexing(self): """ A function app with init indexing enabled @@ -269,43 +195,6 @@ def test_opencensus_with_extensions_enabled_init_indexing(self): resp = ctrl.send_request(req) self.assertEqual(resp.status_code, 200) - @skipIf(sys.version_info.minor != 9, - "This is testing only for python39 where extensions" - "enabled by default") - def test_reload_variables_after_timeout_error(self): - """ - A function app with HTTPtrigger which has a function timeout of - 20s. The app as a sleep of 30s which should trigger a timeout - """ - with LinuxConsumptionWebHostController(_DEFAULT_HOST_VERSION, - self._py_version) as ctrl: - ctrl.assign_container(env={ - "AzureWebJobsStorage": self._storage, - "SCM_RUN_FROM_PACKAGE": self._get_blob_url( - "TimeoutError"), - PYTHON_ISOLATE_WORKER_DEPENDENCIES: "1" - }) - req = Request('GET', f'{ctrl.url}/api/hello') - resp = ctrl.send_request(req) - self.assertEqual(resp.status_code, 500) - - sleep(2) - logs = ctrl.get_container_logs() - self.assertRegex( - logs, - r"Applying prioritize_customer_dependencies: " - r"worker_dependencies_path: \/azure-functions-host\/" - r"workers\/python\/.*?\/LINUX\/X64," - r" customer_dependencies_path: \/home\/site\/wwwroot\/" - r"\.python_packages\/lib\/site-packages, working_directory:" - r" \/home\/site\/wwwroot, Linux Consumption: True," - r" Placeholder: False") - self.assertNotIn("Failure Exception: ModuleNotFoundError", - logs) - - @skipIf(sys.version_info.minor != 9, - "This is testing only for python39 where extensions" - "enabled by default") def test_reload_variables_after_oom_error(self): """ A function app with HTTPtrigger mocking error code 137 @@ -337,8 +226,7 @@ def test_reload_variables_after_oom_error(self): self.assertNotIn("Failure Exception: ModuleNotFoundError", logs) - @skipIf(sys.version_info.minor != 10, - "This is testing only for python310") + @skip("Flaky test.") def test_http_v2_fastapi_streaming_upload_download(self): """ A function app using http v2 fastapi extension with streaming upload and @@ -377,7 +265,10 @@ def generate_random_bytes_stream(): streamed_data, b'streamingtestingresponseisreturned') def _get_blob_url(self, scenario_name: str) -> str: - return ( - f'https://pythonworker{self._py_shortform}sa.blob.core.windows.net/' - f'python-worker-lc-apps/{scenario_name}{self._py_shortform}.zip' - ) + base_url = "http://172.17.0.1:10000/devstoreaccount1/apps" + + container_sas_token = os.getenv('CONTAINER_SAS_TOKEN') + if not container_sas_token: + raise RuntimeError('Environment variable CONTAINER_SAS_TOKEN is ' + 'required before running Linux Consumption test') + return f"{base_url}/{scenario_name}.zip?{container_sas_token}" diff --git a/workers/tests/utils/testutils_lc.py b/workers/tests/utils/testutils_lc.py index 43665a65..94979adb 100644 --- a/workers/tests/utils/testutils_lc.py +++ b/workers/tests/utils/testutils_lc.py @@ -62,7 +62,7 @@ def url(self) -> str: def assign_container(self, env: Dict[str, str] = {}): """Make a POST request to /admin/instance/assign to specialize the - container + container. """ url = f'http://localhost:{self._ports[self._uuid]}' @@ -73,6 +73,28 @@ def assign_container(self, env: Dict[str, str] = {}): env["WEBSITE_SITE_NAME"] = self._uuid env["WEBSITE_HOSTNAME"] = f"{self._uuid}.azurewebsites.com" + # Debug: Print SCM_RUN_FROM_PACKAGE value + scm_package = env.get("SCM_RUN_FROM_PACKAGE", "NOT_SET") + print(f"πŸ” DEBUG: SCM_RUN_FROM_PACKAGE in env: {scm_package}") + + # Wait for the container to be ready + max_retries = 60 + for i in range(max_retries): + try: + ping_req = requests.Request(method="GET", url=f"{url}/admin/host/ping") + ping_response = self.send_request(ping_req) + if ping_response.ok: + print(f"πŸ” DEBUG: Container ready after {i + 1} attempts") + break + else: + print("πŸ” DEBUG: Ping attempt {i+1}/60 failed with status " + f"{ping_response.status_code}") + except Exception as e: + print(f"πŸ” DEBUG: Ping attempt {i + 1}/60 failed with exception: {e}") + time.sleep(1) + else: + raise RuntimeError(f'Container {self._uuid} did not become ready in time') + # Send the specialization context via a POST request req = requests.Request( method="POST", @@ -102,48 +124,76 @@ def send_request( prepped = session.prepare_request(req) prepped.headers['Content-Type'] = 'application/json' - prepped.headers['x-ms-site-restricted-token'] = ( - self._get_site_restricted_token() - ) + + # Try to generate a proper JWT token first + try: + jwt_token = self._generate_jwt_token() + # Use JWT token for newer Azure Functions host versions + prepped.headers['Authorization'] = f'Bearer {jwt_token}' + except ImportError: + # Fall back to the old SWT token format if jwt library is not available + swt_token = self._get_site_restricted_token() + prepped.headers['x-ms-site-restricted-token'] = swt_token + prepped.headers['Authorization'] = f'Bearer {swt_token}' + + # Add additional headers required by Azure Functions host prepped.headers['x-site-deployment-id'] = self._uuid + prepped.headers['x-ms-client-request-id'] = str(uuid.uuid4()) + prepped.headers['x-ms-request-id'] = str(uuid.uuid4()) resp = session.send(prepped) return resp @classmethod - def _find_latest_mesh_image(cls, - host_major: str, - python_version: str) -> str: - """Find the latest image in https://mcr.microsoft.com/v2/ - azure-functions/mesh/tags/list. Match either (3.1.3, or 3.1.3-python3.x) + def _find_latest_mesh_image( + cls, + host_major: str, + python_version: str, + ) -> str: + """ + Return the tag for the most recent mesh image that starts with + and ends with β€œ-python”. + + Example tag: 4.1040.100-0-python3.13 """ if host_major in cls._mesh_images: return cls._mesh_images[host_major] - # match 3.1.3 - regex = re.compile(host_major + r'.\d+.\d+-python' + python_version) + # might already contain dots, so escape it + tag_pattern = re.compile( + rf'{re.escape(host_major)}\.\d+\.\d+-\d+-python{python_version}$' + ) - response = requests.get(_MESH_IMAGE_URL, allow_redirects=True) + response = requests.get(_MESH_IMAGE_URL, timeout=10) if not response.ok: - raise RuntimeError(f'Failed to query latest image for v{host_major}' - f' Python {python_version}.' - f' Status {response.status_code}') - - tag_list = response.json().get('tags', []) - # Removing images with a -upgrade. Upgrade images were temporary - # images used to onboard customers from a previous version. These - # images are no longer used. - tag_list = [x.strip("-upgrade") for x in tag_list] - - # Listing all the versions from the tags with suffix -python - python_versions = list(filter(regex.match, tag_list)) - - # sorting all the python versions based on the runtime version and - # getting the latest released runtime version for python. - latest_version = sorted(python_versions, key=lambda x: float( - x.split(host_major + '.')[-1].split("-python")[0]))[-1] - - image_tag = f'{_MESH_IMAGE_REPO}:{latest_version}' + raise RuntimeError( + f'Failed to query latest image for v{host_major} ' + f'Python {python_version}. ' + f'Status {response.status_code}' + ) + + # Strip β€œ-upgrade” from any temporary tags + tags = [t.removesuffix("-upgrade") for t in response.json().get("tags", [])] + + # Keep only tags that match host_major and python_version + candidates = [t for t in tags if tag_pattern.match(t)] + if not candidates: + raise RuntimeError( + f'No mesh image found for v{host_major} Python {python_version}.' + ) + + def _numeric_key(tag: str) -> tuple[int, ...]: + """ + Convert the part before β€œ-python” to a tuple of integers so that + numeric comparison works as expected. + + For β€œ4.1040.100-0” we get (4, 1040, 100, 0). + """ + numeric_part = tag.split("-python")[0].replace("-", ".") + return tuple(int(piece) for piece in numeric_part.split(".")) + + latest_tag = max(candidates, key=_numeric_key) + image_tag = f"{_MESH_IMAGE_REPO}:{latest_tag}" cls._mesh_images[host_major] = image_tag return image_tag @@ -201,6 +251,9 @@ def spawn_container(self, run_cmd.extend(["-e", f"CONTAINER_ENCRYPTION_KEY={os.getenv('_DUMMY_CONT_KEY')}"]) run_cmd.extend(["-e", "WEBSITE_PLACEHOLDER_MODE=1"]) + # Add required environment variables for JWT issuer validation + run_cmd.extend(["-e", f"WEBSITE_SITE_NAME={self._uuid}"]) + run_cmd.extend(["-e", "WEBSITE_SKU=Dynamic"]) run_cmd.extend(["-v", f'{worker_path}:{container_worker_path}']) run_cmd.extend(["-v", f'{base_ext_local_path}:{base_ext_container_path}']) @@ -266,27 +319,69 @@ def _get_site_restricted_token(cls) -> str: """Get the header value which can be used by x-ms-site-restricted-token which expires in one day. """ - exp_ns = int(time.time() + 24 * 60 * 60) * 1000000000 - return cls._encrypt_context(os.getenv('_DUMMY_CONT_KEY'), f'exp={exp_ns}') + # For compatibility with older Azure Functions host versions, + # try the old SWT format first + exp_ns = int((time.time() + 24 * 60 * 60) * 1000000000) + token = cls._encrypt_context(os.getenv('_DUMMY_CONT_KEY'), f'exp={exp_ns}') + return token + + def _generate_jwt_token(self) -> str: + """Generate a proper JWT token for newer Azure Functions host versions.""" + try: + import jwt + except ImportError: + # Fall back to SWT format if JWT library not available + return self._get_site_restricted_token() + + # JWT payload matching Azure Functions host expectations + exp_time = int(time.time()) + (24 * 60 * 60) # 24 hours from now + + # Use the site name consistently for issuer and audience validation + site_name = self._uuid + container_name = self._uuid + + # According to Azure Functions host analysis, use site-specific issuer format + # This matches the ValidIssuers array in ScriptJwtBearerExtensions.cs + issuer = f"https://{site_name}.azurewebsites.net" + + payload = { + 'exp': exp_time, + 'iat': int(time.time()), + # Use site-specific issuer format that matches ValidIssuers in the host + 'iss': issuer, + # For Linux Consumption in placeholder mode, audience is the container name + 'aud': container_name + } + + # Use the same encryption key for JWT signing + key = base64.b64decode(os.getenv('_DUMMY_CONT_KEY').encode()) + + # Generate JWT token using HMAC SHA256 (matches Azure Functions host) + jwt_token = jwt.encode(payload, key, algorithm='HS256') + return jwt_token @classmethod def _get_site_encrypted_context(cls, site_name: str, env: Dict[str, str]) -> str: """Get the encrypted context for placeholder mode specialization""" + # Ensure WEBSITE_SITE_NAME is set to simulate production mode + env["WEBSITE_SITE_NAME"] = site_name + ctx = { "SiteId": 1, "SiteName": site_name, "Environment": env } - # Ensure WEBSITE_SITE_NAME is set to simulate production mode - ctx["Environment"]["WEBSITE_SITE_NAME"] = site_name - return cls._encrypt_context(os.getenv('_DUMMY_CONT_KEY'), json.dumps(ctx)) + json_ctx = json.dumps(ctx) + + encrypted = cls._encrypt_context(os.getenv('_DUMMY_CONT_KEY'), json_ctx) + return encrypted @classmethod def _encrypt_context(cls, encryption_key: str, plain_text: str) -> str: - """Encrypt plain text context into a encrypted message which can + """Encrypt plain text context into an encrypted message which can be accepted by the host """ # Decode the encryption key