Skip to content

Commit 60d4080

Browse files
committed
Merge fix-e2e-tests into PyPI-ADO-PackagePublish
2 parents e3b627d + 91b7c2b commit 60d4080

File tree

3 files changed

+66
-49
lines changed

3 files changed

+66
-49
lines changed

.Pipelines/template-pipeline-stages.yml

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ stages:
3131
- task: NodeTool@0
3232
displayName: 'Install Node.js (includes npm)'
3333
inputs:
34-
versionSpec: 'lts/*'
34+
versionSpec: '20.x'
3535

3636
- task: securedevelopmentteam.vss-secure-development-tools.build-task-policheck.PoliCheck@2
3737
displayName: 'Run PoliCheck'
@@ -80,11 +80,9 @@ stages:
8080
python.version: '3.14'
8181
steps:
8282
# Retrieve the MSID Lab certificate from Key Vault (via AuthSdkResourceManager SC).
83-
# Gated on LAB_APP_CLIENT_ID being non-empty — if e2e tests are not enabled (the default),
84-
# both steps are skipped and the pipeline has no Key Vault dependency.
83+
# Matches the pattern used by MSAL.js (install-keyvault-secrets.yml) and MSAL Java.
8584
- task: AzureKeyVault@2
8685
displayName: 'Retrieve lab certificate from Key Vault'
87-
condition: and(succeeded(), ne(variables['LAB_APP_CLIENT_ID'], ''))
8886
inputs:
8987
azureSubscription: 'AuthSdkResourceManager'
9088
KeyVaultName: 'msidlabs'
@@ -98,7 +96,6 @@ stages:
9896
echo "##vso[task.setvariable variable=LAB_APP_CLIENT_CERT_PFX_PATH]$CERT_PATH"
9997
echo "Lab cert written to: $CERT_PATH ($(wc -c < "$CERT_PATH") bytes)"
10098
displayName: 'Write lab certificate to disk'
101-
condition: and(succeeded(), ne(variables['LAB_APP_CLIENT_ID'], ''))
10299
103100
- task: UsePythonVersion@0
104101
inputs:
@@ -120,13 +117,6 @@ stages:
120117
pytest -vv --junitxml=test-results/junit.xml 2>&1 | tee test-results/pytest.log
121118
displayName: 'Run tests'
122119
env:
123-
# LAB_APP_CLIENT_ID is intentionally omitted to match the PR gate build
124-
# behaviour (azure-pipelines.yml). Without it, _get_credential() in
125-
# lab_config.py raises EnvironmentError and all e2e tests skip or error
126-
# gracefully — identical to the PR build result.
127-
# Uncomment and set this variable to enable full e2e runs on a
128-
# lab-capable agent pool (requires CA-exempt network / internal agent).
129-
# LAB_APP_CLIENT_ID: $(LAB_APP_CLIENT_ID)
130120
LAB_APP_CLIENT_CERT_PFX_PATH: $(LAB_APP_CLIENT_CERT_PFX_PATH)
131121
132122
- task: PublishTestResults@2
@@ -141,7 +131,6 @@ stages:
141131
- bash: rm -f "$(Agent.TempDirectory)/lab-auth.pfx"
142132
displayName: 'Clean up lab certificate'
143133
condition: and(always(), ne(variables['LAB_APP_CLIENT_ID'], ''))
144-
- stage: Build
145134
displayName: 'Build package'
146135
dependsOn: CI
147136
condition: and(eq(dependencies.CI.result, 'Succeeded'), eq(${{ parameters.runPublish }}, true))

tests/lab_config.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
app = get_app_config(AppSecrets.PCA_CLIENT)
2121
2222
Environment Variables:
23-
LAB_APP_CLIENT_ID: Client ID for Key Vault authentication (required)
2423
LAB_APP_CLIENT_CERT_PFX_PATH: Path to .pfx certificate file (required)
2524
"""
2625

@@ -43,6 +42,7 @@
4342
"UserConfig",
4443
"AppConfig",
4544
# Functions
45+
"LAB_APP_CLIENT_ID",
4646
"get_secret",
4747
"get_user_config",
4848
"get_app_config",
@@ -57,6 +57,12 @@
5757
_MSID_LAB_VAULT = "https://msidlabs.vault.azure.net"
5858
_MSAL_TEAM_VAULT = "https://id4skeyvault.vault.azure.net"
5959

60+
# Client ID for the RequestMSIDLAB app used to authenticate against the lab
61+
# Key Vaults. Hardcoded here following the same pattern as MSAL.NET
62+
# (see build/template-install-keyvault-secrets.yaml in that repo).
63+
# See https://docs.msidlab.com/accounts/confidentialclient.html
64+
LAB_APP_CLIENT_ID = "f62c5ae3-bf3a-4af5-afa8-a68b800396e9"
65+
6066
# =============================================================================
6167
# Secret Name Constants
6268
# =============================================================================
@@ -164,6 +170,21 @@ class AppConfig:
164170
_msal_team_client: Optional[SecretClient] = None
165171

166172

173+
def _clean_env(name: str) -> Optional[str]:
174+
"""Return the env var value, or None if unset or it contains an unexpanded
175+
ADO pipeline variable literal such as ``$(VAR_NAME)``.
176+
177+
Azure DevOps injects the literal string ``$(VAR_NAME)`` when a ``$(...)``
178+
reference in a step ``env:`` block refers to a variable that has not been
179+
defined at runtime. That literal is truthy, so a plain ``os.getenv()``
180+
check would incorrectly proceed as if the variable were set.
181+
"""
182+
value = os.getenv(name)
183+
if value and value.startswith("$("):
184+
return None
185+
return value or None
186+
187+
167188
def _get_credential():
168189
"""
169190
Create an Azure credential for Key Vault access.
@@ -177,19 +198,14 @@ def _get_credential():
177198
Raises:
178199
EnvironmentError: If required environment variables are not set.
179200
"""
180-
client_id = os.getenv("LAB_APP_CLIENT_ID")
181-
cert_path = os.getenv("LAB_APP_CLIENT_CERT_PFX_PATH")
201+
cert_path = _clean_env("LAB_APP_CLIENT_CERT_PFX_PATH")
182202
tenant_id = "72f988bf-86f1-41af-91ab-2d7cd011db47" # Microsoft tenant
183-
184-
if not client_id:
185-
raise EnvironmentError(
186-
"LAB_APP_CLIENT_ID environment variable is required for Key Vault access")
187-
203+
188204
if cert_path:
189205
logger.debug("Using certificate credential for Key Vault access")
190206
return CertificateCredential(
191207
tenant_id=tenant_id,
192-
client_id=client_id,
208+
client_id=LAB_APP_CLIENT_ID,
193209
certificate_path=cert_path,
194210
send_certificate_chain=True,
195211
)
@@ -396,7 +412,7 @@ def get_client_certificate() -> Dict[str, object]:
396412
Raises:
397413
EnvironmentError: If LAB_APP_CLIENT_CERT_PFX_PATH is not set.
398414
"""
399-
cert_path = os.getenv("LAB_APP_CLIENT_CERT_PFX_PATH")
415+
cert_path = _clean_env("LAB_APP_CLIENT_CERT_PFX_PATH")
400416
if not cert_path:
401417
raise EnvironmentError(
402418
"LAB_APP_CLIENT_CERT_PFX_PATH environment variable is required "

tests/test_e2e.py

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
"""If the following ENV VAR were available, many end-to-end test cases would run.
2-
LAB_APP_CLIENT_ID=...
32
LAB_APP_CLIENT_CERT_PFX_PATH=...
43
"""
54
try:
@@ -29,7 +28,7 @@
2928
from tests.broker_util import is_pymsalruntime_installed
3029
from tests.lab_config import (
3130
get_user_config, get_app_config, get_user_password, get_secret,
32-
UserSecrets, AppSecrets,
31+
UserSecrets, AppSecrets, LAB_APP_CLIENT_ID,
3332
)
3433

3534

@@ -44,7 +43,23 @@
4443

4544
_PYMSALRUNTIME_INSTALLED = is_pymsalruntime_installed()
4645
_AZURE_CLI = "04b07795-8ddb-461a-bbee-02f9e1bf7b46"
47-
_SKIP_UNATTENDED_E2E_TESTS = os.getenv("TRAVIS") or not os.getenv("CI")
46+
# Skip interactive / browser-dependent tests when:
47+
# - on Travis CI (TRAVIS), or
48+
# - on Azure DevOps (TF_BUILD) where there is no display/browser on the agent, or
49+
# - not running in any CI environment at all (not CI).
50+
# Service-principal and ROPC tests are NOT gated on this flag; only tests that
51+
# call acquire_token_interactive() or acquire_token_by_device_flow() are.
52+
_SKIP_UNATTENDED_E2E_TESTS = (
53+
os.getenv("TRAVIS") or os.getenv("TF_BUILD") or not os.getenv("CI")
54+
)
55+
56+
57+
def _clean_env(name):
58+
"""Return the env var value, or None if unset or it contains an unexpanded
59+
ADO pipeline variable literal such as ``$(VAR_NAME)``."""
60+
value = os.getenv(name)
61+
return None if (not value or value.startswith("$(")) else value
62+
4863

4964
def _get_app_and_auth_code(
5065
client_id,
@@ -329,13 +344,16 @@ def test_access_token_should_be_obtained_for_a_supported_scope(self):
329344
self.assertIsNotNone(result.get("access_token"))
330345

331346

332-
@unittest.skipIf(os.getenv("TF_BUILD"), "Skip PublicCloud scenarios on Azure DevOps")
333347
class PublicCloudScenariosTestCase(E2eTestCase):
334348
# Historically this class was driven by tests/config.json for semi-automated runs.
335-
# It now uses lab config + env vars so it can run automatically without local files.
349+
# It now uses lab config + env vars so it can run automatically on any CI
350+
# (including Azure DevOps) as long as LAB_APP_CLIENT_CERT_PFX_PATH is set.
336351

337352
@classmethod
338353
def setUpClass(cls):
354+
if not _clean_env("LAB_APP_CLIENT_CERT_PFX_PATH"):
355+
raise unittest.SkipTest(
356+
"LAB_APP_CLIENT_CERT_PFX_PATH not set; skipping PublicCloud e2e tests")
339357
pca_app = get_app_config(AppSecrets.PCA_CLIENT)
340358
user = get_user_config(UserSecrets.PUBLIC_CLOUD)
341359
cls.config = {
@@ -416,13 +434,11 @@ def test_client_secret(self):
416434

417435
def test_subject_name_issuer_authentication(self):
418436
from tests.lab_config import get_client_certificate
419-
420-
client_id = os.getenv("LAB_APP_CLIENT_ID")
421-
if not client_id:
422-
self.skipTest("LAB_APP_CLIENT_ID environment variable is required")
437+
if not _clean_env("LAB_APP_CLIENT_CERT_PFX_PATH"):
438+
self.skipTest("LAB_APP_CLIENT_CERT_PFX_PATH not set")
423439

424440
self.app = msal.ConfidentialClientApplication(
425-
client_id,
441+
LAB_APP_CLIENT_ID,
426442
authority="https://login.microsoftonline.com/microsoft.onmicrosoft.com",
427443
client_credential=get_client_certificate(),
428444
http_client=MinimalHttpClient())
@@ -447,35 +463,35 @@ def manual_test_device_flow(self):
447463

448464

449465
def get_lab_app(
450-
env_client_id="LAB_APP_CLIENT_ID",
451466
env_client_cert_path="LAB_APP_CLIENT_CERT_PFX_PATH",
452467
authority="https://login.microsoftonline.com/"
453468
"72f988bf-86f1-41af-91ab-2d7cd011db47", # Microsoft tenant ID
454469
timeout=None,
455470
**kwargs):
456471
"""Returns the lab app as an MSAL confidential client.
457472
458-
Get it from environment variables if defined, otherwise fall back to use MSI.
473+
Uses the hardcoded lab app client ID (RequestMSIDLAB) and a certificate
474+
from the LAB_APP_CLIENT_CERT_PFX_PATH env var.
459475
"""
460476
logger.info(
461-
"Reading ENV variables %s and %s for lab app defined at "
477+
"Reading ENV variable %s for lab app defined at "
462478
"https://docs.msidlab.com/accounts/confidentialclient.html",
463-
env_client_id, env_client_cert_path)
464-
if os.getenv(env_client_id) and os.getenv(env_client_cert_path):
479+
env_client_cert_path)
480+
cert_path = _clean_env(env_client_cert_path)
481+
if cert_path:
465482
# id came from https://docs.msidlab.com/accounts/confidentialclient.html
466-
client_id = os.getenv(env_client_id)
467483
client_credential = {
468484
"private_key_pfx_path":
469485
# Cert came from https://ms.portal.azure.com/#@microsoft.onmicrosoft.com/asset/Microsoft_Azure_KeyVault/Certificate/https://msidlabs.vault.azure.net/certificates/LabAuth
470-
os.getenv(env_client_cert_path),
486+
cert_path,
471487
"public_certificate": True, # Opt in for SNI
472488
}
473489
else:
474490
logger.info("ENV variables are not defined. Fall back to MSI.")
475491
# See also https://microsoft.sharepoint-df.com/teams/MSIDLABSExtended/SitePages/Programmatically-accessing-LAB-API's.aspx
476492
raise unittest.SkipTest("MSI-based mechanism has not been implemented yet")
477493
return msal.ConfidentialClientApplication(
478-
client_id,
494+
LAB_APP_CLIENT_ID,
479495
client_credential=client_credential,
480496
authority=authority,
481497
http_client=MinimalHttpClient(timeout=timeout),
@@ -831,7 +847,6 @@ def test_user_account(self):
831847

832848

833849
class WorldWideTestCase(LabBasedTestCase):
834-
_ADFS_LABS_UNAVAILABLE = "ADFS labs were temporarily down since July 2025 until further notice"
835850

836851
def test_aad_managed_user(self): # Pure cloud
837852
"""Test username/password flow for a managed AAD user."""
@@ -846,7 +861,6 @@ def test_aad_managed_user(self): # Pure cloud
846861
scope=["https://graph.microsoft.com/.default"],
847862
)
848863

849-
@unittest.skip(_ADFS_LABS_UNAVAILABLE)
850864
def test_adfs2022_fed_user(self):
851865
"""Test username/password flow for a federated user via ADFS 2022."""
852866
app = get_app_config(AppSecrets.PCA_CLIENT)
@@ -1162,15 +1176,13 @@ def _test_acquire_token_for_client(self, configured_region, expected_region):
11621176
import os
11631177
from tests.lab_config import get_client_certificate
11641178

1165-
# Get client ID from environment and certificate from lab_config
1166-
client_id = os.getenv("LAB_APP_CLIENT_ID")
1167-
if not client_id:
1168-
self.skipTest("LAB_APP_CLIENT_ID environment variable is required")
1169-
1179+
# Get client ID from lab_config constant and certificate from lab_config
1180+
if not _clean_env("LAB_APP_CLIENT_CERT_PFX_PATH"):
1181+
self.skipTest("LAB_APP_CLIENT_CERT_PFX_PATH is required")
11701182
client_credential = get_client_certificate()
11711183

11721184
self.app = msal.ConfidentialClientApplication(
1173-
client_id,
1185+
LAB_APP_CLIENT_ID,
11741186
client_credential=client_credential,
11751187
authority="https://login.microsoftonline.com/microsoft.onmicrosoft.com",
11761188
azure_region=configured_region,

0 commit comments

Comments
 (0)