From 51bcc28d017443ff093199b1ed978e4f8bd68330 Mon Sep 17 00:00:00 2001 From: Nicole Gomes Date: Mon, 24 Aug 2026 19:52:44 -0300 Subject: [PATCH 1/4] feat: add method to validate feature toggles --- pyproject.toml | 2 +- .../core/runtime_context/__init__.py | 4 ++ .../core/runtime_context/_context.py | 13 ++++- .../core/runtime_context/_keys.py | 3 +- .../core/runtime_context/providers/_dwc.py | 7 ++- .../runtime_context/test_runtime_context.py | 47 +++++++++++++++++++ 6 files changed, 72 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fd556aea..620a8f30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sap-cloud-sdk" -version = "0.45.3" +version = "0.46.0" description = "SAP Cloud SDK for Python" readme = "README.md" license = "Apache-2.0" diff --git a/src/sap_cloud_sdk/core/runtime_context/__init__.py b/src/sap_cloud_sdk/core/runtime_context/__init__.py index 397da80c..16d8ba29 100644 --- a/src/sap_cloud_sdk/core/runtime_context/__init__.py +++ b/src/sap_cloud_sdk/core/runtime_context/__init__.py @@ -21,12 +21,14 @@ from sap_cloud_sdk.core.runtime_context._context import ( RuntimeContext, get_context, + is_feature_enabled, ) from sap_cloud_sdk.core.runtime_context._envelope import RequestEnvelope from sap_cloud_sdk.core.runtime_context._keys import ( ContextKey, DWC_SUBDOMAIN, DWC_TENANT, + FEATURE_TOGGLES, TRIGGER_TYPE, ) from sap_cloud_sdk.core.runtime_context._protocol import ContextProvider @@ -50,6 +52,7 @@ "DWC_SUBDOMAIN", "DWC_TENANT", "DWCContextProvider", + "FEATURE_TOGGLES", "FrameworkAdapter", "GLOBAL_TENANT_ID", "IASContextProvider", @@ -59,5 +62,6 @@ "TRIGGER_TYPE", "USER_ID", "get_context", + "is_feature_enabled", "register", ] diff --git a/src/sap_cloud_sdk/core/runtime_context/_context.py b/src/sap_cloud_sdk/core/runtime_context/_context.py index 11102ed7..709ffb28 100644 --- a/src/sap_cloud_sdk/core/runtime_context/_context.py +++ b/src/sap_cloud_sdk/core/runtime_context/_context.py @@ -4,7 +4,7 @@ from contextvars import ContextVar from typing import Any, AsyncGenerator, Dict, Generator, Optional, TypeVar -from sap_cloud_sdk.core.runtime_context._keys import ContextKey +from sap_cloud_sdk.core.runtime_context._keys import ContextKey, FEATURE_TOGGLES T = TypeVar("T") @@ -64,6 +64,17 @@ def get_context() -> RuntimeContext: return _context_var.get() +def is_feature_enabled(name: str) -> bool: + """Return ``True`` if *name* is in the active feature toggles for the current request. + + Feature toggles are populated from the ``dwc-feature-toggles`` DWC request + header by :class:`~sap_cloud_sdk.core.runtime_context.DWCContextProvider`. + Returns ``False`` when no toggles header was present or the toggle is absent. + """ + toggles = get_context().get(FEATURE_TOGGLES) + return name in toggles if toggles is not None else False + + @contextmanager def sdk_context(ctx: RuntimeContext) -> Generator[RuntimeContext, None, None]: """Sync context manager that sets *ctx* for the duration of the block.""" diff --git a/src/sap_cloud_sdk/core/runtime_context/_keys.py b/src/sap_cloud_sdk/core/runtime_context/_keys.py index 83e599c7..25e5a2bd 100644 --- a/src/sap_cloud_sdk/core/runtime_context/_keys.py +++ b/src/sap_cloud_sdk/core/runtime_context/_keys.py @@ -1,6 +1,6 @@ """Typed context key for RuntimeContext.""" -from typing import Generic, TypeVar +from typing import Generic, List, TypeVar T = TypeVar("T") @@ -37,3 +37,4 @@ def __repr__(self) -> str: TRIGGER_TYPE = ContextKey[str]("trigger_type") DWC_SUBDOMAIN = ContextKey[str]("dwc_subdomain") DWC_TENANT = ContextKey[str]("dwc_tenant") +FEATURE_TOGGLES = ContextKey[List[str]]("dwc_feature_toggles") diff --git a/src/sap_cloud_sdk/core/runtime_context/providers/_dwc.py b/src/sap_cloud_sdk/core/runtime_context/providers/_dwc.py index d59c719c..614fb103 100644 --- a/src/sap_cloud_sdk/core/runtime_context/providers/_dwc.py +++ b/src/sap_cloud_sdk/core/runtime_context/providers/_dwc.py @@ -2,9 +2,11 @@ from sap_cloud_sdk.core.runtime_context._context import RuntimeContext from sap_cloud_sdk.core.runtime_context._envelope import RequestEnvelope -from sap_cloud_sdk.core.runtime_context._keys import DWC_SUBDOMAIN, DWC_TENANT +from sap_cloud_sdk.core.runtime_context._keys import DWC_SUBDOMAIN, DWC_TENANT, FEATURE_TOGGLES from sap_cloud_sdk.core.runtime_context._protocol import ContextProvider +_FEATURE_TOGGLES_HEADER = "dwc-feature-toggles" + class DWCContextProvider(ContextProvider): """Extracts DWC tenant context from SAP DWC request headers. @@ -13,6 +15,7 @@ class DWCContextProvider(ContextProvider): - :data:`~sap_cloud_sdk.core.runtime_context.DWC_SUBDOMAIN` from ``dwc-subdomain`` - :data:`~sap_cloud_sdk.core.runtime_context.DWC_TENANT` from ``dwc-tenant`` + - :data:`~sap_cloud_sdk.core.runtime_context.FEATURE_TOGGLES` from ``dwc-feature-toggles`` """ def extract(self, envelope: RequestEnvelope) -> RuntimeContext: @@ -21,4 +24,6 @@ def extract(self, envelope: RequestEnvelope) -> RuntimeContext: values[DWC_SUBDOMAIN] = subdomain if tenant := envelope.headers.get("dwc-tenant"): values[DWC_TENANT] = tenant + if raw := envelope.headers.get(_FEATURE_TOGGLES_HEADER): + values[FEATURE_TOGGLES] = [t.strip() for t in raw.split(",") if t.strip()] return RuntimeContext(values) diff --git a/tests/core/unit/runtime_context/test_runtime_context.py b/tests/core/unit/runtime_context/test_runtime_context.py index 1a38e119..a9425d1f 100644 --- a/tests/core/unit/runtime_context/test_runtime_context.py +++ b/tests/core/unit/runtime_context/test_runtime_context.py @@ -9,12 +9,14 @@ DWC_SUBDOMAIN, DWC_TENANT, DWCContextProvider, + FEATURE_TOGGLES, IASContextProvider, RuntimeContext, RequestEnvelope, SAPTriggerContextProvider, TRIGGER_TYPE, get_context, + is_feature_enabled, ) from sap_cloud_sdk.core.runtime_context._context import ( async_sdk_context, @@ -332,6 +334,51 @@ def test_returns_empty_when_no_headers(self): def test_satisfies_context_provider_protocol(self): assert isinstance(DWCContextProvider(), ContextProvider) + def test_extracts_feature_toggles(self): + envelope = _make_envelope({"dwc-feature-toggles": "toggle-a,toggle-b"}) + ctx = DWCContextProvider().extract(envelope) + assert ctx.get(FEATURE_TOGGLES) == ["toggle-a", "toggle-b"] + + def test_feature_toggles_strips_whitespace(self): + envelope = _make_envelope({"dwc-feature-toggles": " toggle-a , toggle-b "}) + ctx = DWCContextProvider().extract(envelope) + assert ctx.get(FEATURE_TOGGLES) == ["toggle-a", "toggle-b"] + + def test_feature_toggles_empty_when_header_absent(self): + envelope = _make_envelope({}) + ctx = DWCContextProvider().extract(envelope) + assert ctx.get(FEATURE_TOGGLES) is None + + +# --------------------------------------------------------------------------- +# is_feature_enabled +# --------------------------------------------------------------------------- + + +class TestIsFeatureEnabled: + def test_returns_true_when_toggle_is_active(self): + ctx = RuntimeContext({FEATURE_TOGGLES: ["my-feature", "other"]}) + with sdk_context(ctx): + assert is_feature_enabled("my-feature") is True + + def test_returns_false_when_toggle_is_not_in_list(self): + ctx = RuntimeContext({FEATURE_TOGGLES: ["other-feature"]}) + with sdk_context(ctx): + assert is_feature_enabled("my-feature") is False + + def test_returns_false_when_toggle_list_is_empty(self): + ctx = RuntimeContext({FEATURE_TOGGLES: []}) + with sdk_context(ctx): + assert is_feature_enabled("my-feature") is False + + def test_returns_false_when_no_toggles_header(self): + ctx = RuntimeContext() + with sdk_context(ctx): + assert is_feature_enabled("my-feature") is False + + def teardown_method(self): + set_context(RuntimeContext()) + # --------------------------------------------------------------------------- # _merge From abd0356cffdf300575594e419567fb5f46bad231 Mon Sep 17 00:00:00 2001 From: Nicole Gomes Date: Tue, 25 Aug 2026 13:57:17 -0300 Subject: [PATCH 2/4] rename header to dwc-product-configuration --- src/sap_cloud_sdk/core/runtime_context/_context.py | 2 +- src/sap_cloud_sdk/core/runtime_context/providers/_dwc.py | 4 ++-- tests/core/unit/runtime_context/test_runtime_context.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sap_cloud_sdk/core/runtime_context/_context.py b/src/sap_cloud_sdk/core/runtime_context/_context.py index 709ffb28..a47e76b3 100644 --- a/src/sap_cloud_sdk/core/runtime_context/_context.py +++ b/src/sap_cloud_sdk/core/runtime_context/_context.py @@ -67,7 +67,7 @@ def get_context() -> RuntimeContext: def is_feature_enabled(name: str) -> bool: """Return ``True`` if *name* is in the active feature toggles for the current request. - Feature toggles are populated from the ``dwc-feature-toggles`` DWC request + Feature toggles are populated from the ``dwc-product-configuration`` DWC request header by :class:`~sap_cloud_sdk.core.runtime_context.DWCContextProvider`. Returns ``False`` when no toggles header was present or the toggle is absent. """ diff --git a/src/sap_cloud_sdk/core/runtime_context/providers/_dwc.py b/src/sap_cloud_sdk/core/runtime_context/providers/_dwc.py index 614fb103..8e0ef6a2 100644 --- a/src/sap_cloud_sdk/core/runtime_context/providers/_dwc.py +++ b/src/sap_cloud_sdk/core/runtime_context/providers/_dwc.py @@ -5,7 +5,7 @@ from sap_cloud_sdk.core.runtime_context._keys import DWC_SUBDOMAIN, DWC_TENANT, FEATURE_TOGGLES from sap_cloud_sdk.core.runtime_context._protocol import ContextProvider -_FEATURE_TOGGLES_HEADER = "dwc-feature-toggles" +_FEATURE_TOGGLES_HEADER = "dwc-product-configuration" class DWCContextProvider(ContextProvider): @@ -15,7 +15,7 @@ class DWCContextProvider(ContextProvider): - :data:`~sap_cloud_sdk.core.runtime_context.DWC_SUBDOMAIN` from ``dwc-subdomain`` - :data:`~sap_cloud_sdk.core.runtime_context.DWC_TENANT` from ``dwc-tenant`` - - :data:`~sap_cloud_sdk.core.runtime_context.FEATURE_TOGGLES` from ``dwc-feature-toggles`` + - :data:`~sap_cloud_sdk.core.runtime_context.FEATURE_TOGGLES` from ``dwc-product-configuration`` """ def extract(self, envelope: RequestEnvelope) -> RuntimeContext: diff --git a/tests/core/unit/runtime_context/test_runtime_context.py b/tests/core/unit/runtime_context/test_runtime_context.py index a9425d1f..2572f9fe 100644 --- a/tests/core/unit/runtime_context/test_runtime_context.py +++ b/tests/core/unit/runtime_context/test_runtime_context.py @@ -335,12 +335,12 @@ def test_satisfies_context_provider_protocol(self): assert isinstance(DWCContextProvider(), ContextProvider) def test_extracts_feature_toggles(self): - envelope = _make_envelope({"dwc-feature-toggles": "toggle-a,toggle-b"}) + envelope = _make_envelope({"dwc-product-configuration": "toggle-a,toggle-b"}) ctx = DWCContextProvider().extract(envelope) assert ctx.get(FEATURE_TOGGLES) == ["toggle-a", "toggle-b"] def test_feature_toggles_strips_whitespace(self): - envelope = _make_envelope({"dwc-feature-toggles": " toggle-a , toggle-b "}) + envelope = _make_envelope({"dwc-product-configuration": " toggle-a , toggle-b "}) ctx = DWCContextProvider().extract(envelope) assert ctx.get(FEATURE_TOGGLES) == ["toggle-a", "toggle-b"] From ebda1e81fe84fddaf15ea3e2de6a87c6804680c0 Mon Sep 17 00:00:00 2001 From: Nicole Gomes Date: Tue, 25 Aug 2026 14:07:09 -0300 Subject: [PATCH 3/4] update user-guide --- .../core/runtime_context/user-guide.md | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/sap_cloud_sdk/core/runtime_context/user-guide.md b/src/sap_cloud_sdk/core/runtime_context/user-guide.md index 2508021f..e84ed0a1 100644 --- a/src/sap_cloud_sdk/core/runtime_context/user-guide.md +++ b/src/sap_cloud_sdk/core/runtime_context/user-guide.md @@ -33,7 +33,7 @@ bootstrap(app) By default `bootstrap` registers `IASContextProvider` (reads IAS JWT), `SAPTriggerContextProvider` (reads `x-sap-origin`), and `DWCContextProvider` -(reads `dwc-subdomain` and `dwc-tenant`). +(reads `dwc-subdomain`, `dwc-tenant`, and `dwc-product-configuration`). ### 2. Read context anywhere @@ -95,7 +95,31 @@ metadata or a message queue envelope — as long as the adapter populates |---|---|---| | `IASContextProvider` | `Authorization: Bearer ` | `TENANT_ID`, `USER_ID`, `GLOBAL_TENANT_ID` | | `SAPTriggerContextProvider` | `x-sap-origin` | `TRIGGER_TYPE` | -| `DWCContextProvider` | `dwc-subdomain`, `dwc-tenant` | `DWC_SUBDOMAIN`, `DWC_TENANT` | +| `DWCContextProvider` | `dwc-subdomain`, `dwc-tenant`, `dwc-product-configuration` | `DWC_SUBDOMAIN`, `DWC_TENANT`, `FEATURE_TOGGLES` | + +### Feature toggles + +`DWCContextProvider` reads the `dwc-product-configuration` header and stores the +active toggle names as `FEATURE_TOGGLES` (`ContextKey[List[str]]`). Use +`is_feature_enabled(name)` to check a toggle for the current request: + +```python +from sap_cloud_sdk.core.runtime_context import is_feature_enabled + +@app.route("/") +async def handler(request): + if is_feature_enabled("my-feature"): + ... +``` + +`is_feature_enabled` returns `False` when the header is absent or the toggle +name is not in the active list. For direct access to the full list: + +```python +from sap_cloud_sdk.core.runtime_context import FEATURE_TOGGLES, get_context + +toggles = get_context().get(FEATURE_TOGGLES) # List[str] | None +``` ### Custom providers From 2315e46e85e552856a7d6f853ba66f6effc43a29 Mon Sep 17 00:00:00 2001 From: Nicole Gomes Date: Wed, 26 Aug 2026 11:33:44 -0300 Subject: [PATCH 4/4] fix: header is dwc-stage-configuration --- .../core/runtime_context/_context.py | 2 +- .../core/runtime_context/providers/_dwc.py | 24 +++++++++-- .../core/runtime_context/user-guide.md | 21 +++++++--- .../runtime_context/test_runtime_context.py | 42 ++++++++++++++----- 4 files changed, 69 insertions(+), 20 deletions(-) diff --git a/src/sap_cloud_sdk/core/runtime_context/_context.py b/src/sap_cloud_sdk/core/runtime_context/_context.py index a47e76b3..ebbda154 100644 --- a/src/sap_cloud_sdk/core/runtime_context/_context.py +++ b/src/sap_cloud_sdk/core/runtime_context/_context.py @@ -67,7 +67,7 @@ def get_context() -> RuntimeContext: def is_feature_enabled(name: str) -> bool: """Return ``True`` if *name* is in the active feature toggles for the current request. - Feature toggles are populated from the ``dwc-product-configuration`` DWC request + Feature toggles are populated from the ``dwc-stage-configuration`` DWC request header by :class:`~sap_cloud_sdk.core.runtime_context.DWCContextProvider`. Returns ``False`` when no toggles header was present or the toggle is absent. """ diff --git a/src/sap_cloud_sdk/core/runtime_context/providers/_dwc.py b/src/sap_cloud_sdk/core/runtime_context/providers/_dwc.py index 8e0ef6a2..a999f7a3 100644 --- a/src/sap_cloud_sdk/core/runtime_context/providers/_dwc.py +++ b/src/sap_cloud_sdk/core/runtime_context/providers/_dwc.py @@ -1,11 +1,17 @@ """DWC context provider.""" +import base64 +import json +import logging + from sap_cloud_sdk.core.runtime_context._context import RuntimeContext from sap_cloud_sdk.core.runtime_context._envelope import RequestEnvelope from sap_cloud_sdk.core.runtime_context._keys import DWC_SUBDOMAIN, DWC_TENANT, FEATURE_TOGGLES from sap_cloud_sdk.core.runtime_context._protocol import ContextProvider -_FEATURE_TOGGLES_HEADER = "dwc-product-configuration" +logger = logging.getLogger(__name__) + +_FEATURE_TOGGLES_HEADER = "dwc-stage-configuration" class DWCContextProvider(ContextProvider): @@ -15,7 +21,7 @@ class DWCContextProvider(ContextProvider): - :data:`~sap_cloud_sdk.core.runtime_context.DWC_SUBDOMAIN` from ``dwc-subdomain`` - :data:`~sap_cloud_sdk.core.runtime_context.DWC_TENANT` from ``dwc-tenant`` - - :data:`~sap_cloud_sdk.core.runtime_context.FEATURE_TOGGLES` from ``dwc-product-configuration`` + - :data:`~sap_cloud_sdk.core.runtime_context.FEATURE_TOGGLES` from ``dwc-stage-configuration`` """ def extract(self, envelope: RequestEnvelope) -> RuntimeContext: @@ -25,5 +31,17 @@ def extract(self, envelope: RequestEnvelope) -> RuntimeContext: if tenant := envelope.headers.get("dwc-tenant"): values[DWC_TENANT] = tenant if raw := envelope.headers.get(_FEATURE_TOGGLES_HEADER): - values[FEATURE_TOGGLES] = [t.strip() for t in raw.split(",") if t.strip()] + toggles = _parse_feature_toggles(raw) + if toggles is not None: + values[FEATURE_TOGGLES] = toggles return RuntimeContext(values) + + +def _parse_feature_toggles(raw: str) -> list[str] | None: + try: + decoded = base64.b64decode(raw).decode() + data = json.loads(decoded) + return [f["name"] for f in data.get("features", []) if f.get("enabled")] + except Exception as e: + logger.debug("Failed to parse dwc-stage-configuration header: %s", e) + return None diff --git a/src/sap_cloud_sdk/core/runtime_context/user-guide.md b/src/sap_cloud_sdk/core/runtime_context/user-guide.md index e84ed0a1..e1bd1a67 100644 --- a/src/sap_cloud_sdk/core/runtime_context/user-guide.md +++ b/src/sap_cloud_sdk/core/runtime_context/user-guide.md @@ -33,7 +33,7 @@ bootstrap(app) By default `bootstrap` registers `IASContextProvider` (reads IAS JWT), `SAPTriggerContextProvider` (reads `x-sap-origin`), and `DWCContextProvider` -(reads `dwc-subdomain`, `dwc-tenant`, and `dwc-product-configuration`). +(reads `dwc-subdomain`, `dwc-tenant`, and `dwc-stage-configuration`). ### 2. Read context anywhere @@ -95,13 +95,24 @@ metadata or a message queue envelope — as long as the adapter populates |---|---|---| | `IASContextProvider` | `Authorization: Bearer ` | `TENANT_ID`, `USER_ID`, `GLOBAL_TENANT_ID` | | `SAPTriggerContextProvider` | `x-sap-origin` | `TRIGGER_TYPE` | -| `DWCContextProvider` | `dwc-subdomain`, `dwc-tenant`, `dwc-product-configuration` | `DWC_SUBDOMAIN`, `DWC_TENANT`, `FEATURE_TOGGLES` | +| `DWCContextProvider` | `dwc-subdomain`, `dwc-tenant`, `dwc-stage-configuration` | `DWC_SUBDOMAIN`, `DWC_TENANT`, `FEATURE_TOGGLES` | ### Feature toggles -`DWCContextProvider` reads the `dwc-product-configuration` header and stores the -active toggle names as `FEATURE_TOGGLES` (`ContextKey[List[str]]`). Use -`is_feature_enabled(name)` to check a toggle for the current request: +`DWCContextProvider` reads the `dwc-stage-configuration` header. The value is +base64-encoded JSON with the shape: + +```json +{ + "features": [ + {"name": "MY_FEATURE", "enabled": true}, + {"name": "OTHER_FEATURE", "enabled": false} + ] +} +``` + +Only features with `"enabled": true` are included. Use `is_feature_enabled(name)` +to check a toggle for the current request: ```python from sap_cloud_sdk.core.runtime_context import is_feature_enabled diff --git a/tests/core/unit/runtime_context/test_runtime_context.py b/tests/core/unit/runtime_context/test_runtime_context.py index 2572f9fe..ae417b1c 100644 --- a/tests/core/unit/runtime_context/test_runtime_context.py +++ b/tests/core/unit/runtime_context/test_runtime_context.py @@ -1,5 +1,7 @@ """Tests for sap_cloud_sdk.core.runtime_context.""" +import base64 +import json import pytest from unittest.mock import MagicMock, patch @@ -314,6 +316,10 @@ def test_satisfies_context_provider_protocol(self): # --------------------------------------------------------------------------- +def _make_stage_config(features: list[dict]) -> str: + return base64.b64encode(json.dumps({"features": features}).encode()).decode() + + class TestDWCContextProvider: def test_extracts_dwc_subdomain(self): envelope = _make_envelope({"dwc-subdomain": "my-subdomain"}) @@ -334,19 +340,33 @@ def test_returns_empty_when_no_headers(self): def test_satisfies_context_provider_protocol(self): assert isinstance(DWCContextProvider(), ContextProvider) - def test_extracts_feature_toggles(self): - envelope = _make_envelope({"dwc-product-configuration": "toggle-a,toggle-b"}) - ctx = DWCContextProvider().extract(envelope) - assert ctx.get(FEATURE_TOGGLES) == ["toggle-a", "toggle-b"] + def test_extracts_enabled_feature_toggles(self): + header = _make_stage_config([ + {"name": "FEATURE_A", "enabled": True}, + {"name": "FEATURE_B", "enabled": True}, + ]) + ctx = DWCContextProvider().extract(_make_envelope({"dwc-stage-configuration": header})) + assert ctx.get(FEATURE_TOGGLES) == ["FEATURE_A", "FEATURE_B"] + + def test_excludes_disabled_feature_toggles(self): + header = _make_stage_config([ + {"name": "FEATURE_A", "enabled": True}, + {"name": "FEATURE_B", "enabled": False}, + ]) + ctx = DWCContextProvider().extract(_make_envelope({"dwc-stage-configuration": header})) + assert ctx.get(FEATURE_TOGGLES) == ["FEATURE_A"] + + def test_feature_toggles_none_when_header_absent(self): + ctx = DWCContextProvider().extract(_make_envelope({})) + assert ctx.get(FEATURE_TOGGLES) is None - def test_feature_toggles_strips_whitespace(self): - envelope = _make_envelope({"dwc-product-configuration": " toggle-a , toggle-b "}) - ctx = DWCContextProvider().extract(envelope) - assert ctx.get(FEATURE_TOGGLES) == ["toggle-a", "toggle-b"] + def test_feature_toggles_none_on_invalid_base64(self): + ctx = DWCContextProvider().extract(_make_envelope({"dwc-stage-configuration": "!!!"})) + assert ctx.get(FEATURE_TOGGLES) is None - def test_feature_toggles_empty_when_header_absent(self): - envelope = _make_envelope({}) - ctx = DWCContextProvider().extract(envelope) + def test_feature_toggles_none_on_invalid_json(self): + bad = base64.b64encode(b"not-json").decode() + ctx = DWCContextProvider().extract(_make_envelope({"dwc-stage-configuration": bad})) assert ctx.get(FEATURE_TOGGLES) is None