Skip to content

Commit 17ffd54

Browse files
committed
Fix comma seperated parsing with test
1 parent 629f444 commit 17ffd54

3 files changed

Lines changed: 80 additions & 17 deletions

File tree

workflow-notifier/src/workflow_notifier/config/settings.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
1-
from typing import Annotated, Optional
1+
from typing import Optional
22

33
from dotenv import load_dotenv
4-
from pydantic import BeforeValidator, Field
4+
from pydantic import Field
55
from pydantic_settings import BaseSettings
66

7-
8-
def _parse_comma_separated(value: object) -> list[str]:
9-
"""Accept a comma-separated string **or** an existing list and return a list of stripped, non-empty strings."""
10-
if isinstance(value, str):
11-
return [item.strip() for item in value.split(",") if item.strip()]
12-
return value # type: ignore[return-value]
13-
14-
157
load_dotenv()
168

179

@@ -21,14 +13,18 @@ class Settings(BaseSettings):
2113
NOTIFIER_CLIENT_ID: str
2214
SARA_APP_REG_SCOPE: str
2315

24-
# Optional client secret for local development.
16+
# Optional client secret for local development. In cloud (AKS with Azure
17+
# Workload Identity) this is not provided and the federated token file is
18+
# used instead. Include "ClientSecret" in ALLOWED_AUTH_METHODS to enable
19+
# the ClientSecretCredential path.
2520
NOTIFIER_CLIENT_SECRET: Optional[str] = Field(default=None)
2621

27-
# In environment variables or ConfigMaps, supply a comma-separated string
22+
# Ordered, comma-separated list of credential types that may be used to
23+
# acquire an Azure AD access token. Allowed values: "WorkloadIdentity",
24+
# "ClientSecret". When more than one method is configured, the order
25+
# determines the priority inside the resulting ChainedTokenCredential.
2826
# (e.g. ALLOWED_AUTH_METHODS=WorkloadIdentity,ClientSecret).
29-
ALLOWED_AUTH_METHODS: Annotated[
30-
list[str], BeforeValidator(_parse_comma_separated)
31-
] = Field(default_factory=lambda: ["WorkloadIdentity"])
27+
ALLOWED_AUTH_METHODS: str = Field(default="WorkloadIdentity")
3228

3329
OTEL_SERVICE_NAME: str = Field(default="workflow-notifier")
3430
OTEL_EXPORTER_OTLP_ENDPOINT: str = Field(default="http://localhost:4317")
@@ -44,6 +40,11 @@ def authority(self) -> str:
4440
def scopes(self) -> list[str]:
4541
return [self.SARA_APP_REG_SCOPE]
4642

43+
@property
44+
def allowed_auth_methods(self) -> list[str]:
45+
"""Parse the comma-separated ALLOWED_AUTH_METHODS string into a list."""
46+
return [m.strip() for m in self.ALLOWED_AUTH_METHODS.split(",") if m.strip()]
47+
4748
@property
4849
def workflow_notification_url(self) -> str:
4950
return f"{self.SARA_SERVER_URL}/workflow-notification"

workflow-notifier/src/workflow_notifier/notifier.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def _get_credential() -> TokenCredential:
3535
Build a TokenCredential.
3636
3737
The set of credential types to try is configured via
38-
``settings.ALLOWED_AUTH_METHODS``, an ordered list whose entries may be
38+
``settings.allowed_auth_methods``, an ordered list whose entries may be
3939
``"WorkloadIdentity"`` and/or ``"ClientSecret"`` (case-insensitive). When
4040
more than one method is configured, the order determines the order inside
4141
the resulting ``ChainedTokenCredential``.
@@ -57,7 +57,7 @@ def _get_credential() -> TokenCredential:
5757
credentials: list[TokenCredential] = []
5858
activated: list[str] = []
5959

60-
allowed_methods = settings.ALLOWED_AUTH_METHODS or ["WorkloadIdentity"]
60+
allowed_methods = settings.allowed_auth_methods or ["WorkloadIdentity"]
6161

6262
for method in allowed_methods:
6363
normalized = method.strip().lower()
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from workflow_notifier.config.settings import Settings
2+
3+
# Required env vars that Settings always needs.
4+
_REQUIRED = {
5+
"SARA_SERVER_URL": "http://localhost:8100/api",
6+
"TENANT_ID": "00000000-0000-0000-0000-000000000000",
7+
"NOTIFIER_CLIENT_ID": "dummy-client-id",
8+
"SARA_APP_REG_SCOPE": "api://dummy/.default",
9+
}
10+
11+
12+
class TestSettingsAllowedAuthMethods:
13+
"""Ensure ALLOWED_AUTH_METHODS is correctly parsed from env vars."""
14+
15+
def test_default_value(self, monkeypatch):
16+
for k, v in _REQUIRED.items():
17+
monkeypatch.setenv(k, v)
18+
monkeypatch.delenv("ALLOWED_AUTH_METHODS", raising=False)
19+
20+
s = Settings()
21+
assert s.allowed_auth_methods == ["WorkloadIdentity"]
22+
23+
def test_single_value_from_env(self, monkeypatch):
24+
for k, v in _REQUIRED.items():
25+
monkeypatch.setenv(k, v)
26+
monkeypatch.setenv("ALLOWED_AUTH_METHODS", "ClientSecret")
27+
28+
s = Settings()
29+
assert s.allowed_auth_methods == ["ClientSecret"]
30+
31+
def test_comma_separated_from_env(self, monkeypatch):
32+
for k, v in _REQUIRED.items():
33+
monkeypatch.setenv(k, v)
34+
monkeypatch.setenv("ALLOWED_AUTH_METHODS", "WorkloadIdentity,ClientSecret")
35+
36+
s = Settings()
37+
assert s.allowed_auth_methods == ["WorkloadIdentity", "ClientSecret"]
38+
39+
def test_whitespace_is_stripped(self, monkeypatch):
40+
for k, v in _REQUIRED.items():
41+
monkeypatch.setenv(k, v)
42+
monkeypatch.setenv("ALLOWED_AUTH_METHODS", " WorkloadIdentity , ClientSecret ")
43+
44+
s = Settings()
45+
assert s.allowed_auth_methods == ["WorkloadIdentity", "ClientSecret"]
46+
47+
def test_empty_segments_ignored(self, monkeypatch):
48+
for k, v in _REQUIRED.items():
49+
monkeypatch.setenv(k, v)
50+
monkeypatch.setenv("ALLOWED_AUTH_METHODS", ",WorkloadIdentity,,ClientSecret,")
51+
52+
s = Settings()
53+
assert s.allowed_auth_methods == ["WorkloadIdentity", "ClientSecret"]
54+
55+
def test_raw_field_is_str(self, monkeypatch):
56+
for k, v in _REQUIRED.items():
57+
monkeypatch.setenv(k, v)
58+
monkeypatch.setenv("ALLOWED_AUTH_METHODS", "WorkloadIdentity")
59+
60+
s = Settings()
61+
assert isinstance(s.ALLOWED_AUTH_METHODS, str)
62+
assert s.ALLOWED_AUTH_METHODS == "WorkloadIdentity"

0 commit comments

Comments
 (0)