diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 906a3391..6c1626b6 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -17,12 +17,8 @@ jobs: # Fake a TRAVIS env so that the pre-existing test cases would behave like before TRAVIS: true LAB_APP_CLIENT_ID: ${{ secrets.LAB_APP_CLIENT_ID }} - LAB_APP_CLIENT_SECRET: ${{ secrets.LAB_APP_CLIENT_SECRET }} LAB_APP_CLIENT_CERT_BASE64: ${{ secrets.LAB_APP_CLIENT_CERT_BASE64 }} LAB_APP_CLIENT_CERT_PFX_PATH: lab_cert.pfx - LAB_OBO_CLIENT_SECRET: ${{ secrets.LAB_OBO_CLIENT_SECRET }} - LAB_OBO_CONFIDENTIAL_CLIENT_ID: ${{ secrets.LAB_OBO_CONFIDENTIAL_CLIENT_ID }} - LAB_OBO_PUBLIC_CLIENT_ID: ${{ secrets.LAB_OBO_PUBLIC_CLIENT_ID }} # Derived from https://docs.github.com/en/actions/guides/building-and-testing-python#starting-with-the-python-workflow-template runs-on: ubuntu-22.04 diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5bc556fa..0426b1eb 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -37,5 +37,23 @@ steps: - script: | pip install pytest pytest-azurepipelines - pytest - displayName: 'pytest' + mkdir -p test-results + set -o pipefail + pytest -vv --junitxml=test-results/junit.xml 2>&1 | tee test-results/pytest.log + displayName: 'pytest (verbose + junit + log)' + +- task: PublishTestResults@2 + displayName: 'Publish test results' + condition: succeededOrFailed() + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: 'test-results/junit.xml' + failTaskOnFailedTests: true + testRunTitle: 'Python $(python.version) pytest' + +- task: PublishPipelineArtifact@1 + displayName: 'Publish pytest log artifact' + condition: succeededOrFailed() + inputs: + targetPath: 'test-results' + artifact: 'pytest-logs-$(python.version)' diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..0b044a60 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,87 @@ +# Local test setup + +This document explains how to set up a local development environment to run tests in this repo, including E2E tests. + +## 1) Prerequisites + +- Windows, macOS, or Linux +- Python 3.9+ +- Access to the MSAL lab secrets (Key Vault) for E2E tests +- A registered lab app credential: i.e. certificate `.pfx` file path + +## 2) Create and activate a virtual environment + +From repo root: + +```powershell +python -m venv .venv +.\.venv\Scripts\Activate.ps1 +python -m pip install --upgrade pip +python -m pip install -r requirements.txt +``` + +## 3) Configure environment variables + +Create a local `.env` file in repo root (same folder as `setup.py`): + +```dotenv +LAB_APP_CLIENT_ID= +LAB_APP_CLIENT_CERT_PFX_PATH=C:/path/to/your/cert.pfx + +``` + +Notes: +- `tests/test_e2e.py` loads `.env` automatically when `python-dotenv` is installed. +- For certificate auth, `LAB_APP_CLIENT_CERT_PFX_PATH` should be an absolute path. + +## 4) Run unit/integration tests + +Run all non-E2E tests quickly: + +```powershell +python -m pytest -q tests -k "not e2e" +``` + +Run full E2E unattended suite: + +```powershell +python -m pytest -q tests/test_e2e.py +``` + +## 5) Manual-intervention E2E tests + +Manual tests (interactive browser/device-flow/POP manual scenarios) are separated into: + +- `tests/test_e2e_manual.py` + +By default they are skipped. To enable: + +```powershell +$env:RUN_MANUAL_E2E = "1" +python -m pytest -q tests/test_e2e_manual.py +``` + +To disable again in the current shell: + +```powershell +Remove-Item Env:RUN_MANUAL_E2E +``` + +## 6) Common troubleshooting + +### AADSTS700027 / invalid_client for certificate flow + +If you see errors indicating SNI/x5c is required, your app registration may only accept certificate auth with x5c chain. In this repo, that path is covered by SNI-oriented cert tests. + +### Key Vault access failures + +Verify: +- `LAB_APP_CLIENT_ID` is correct +- `LAB_APP_CLIENT_CERT_PFX_PATH` points to a valid `.pfx` file +- Your principal has access to: + - `https://msidlabs.vault.azure.net` + - `https://id4skeyvault.vault.azure.net` + +### Interactive tests unexpectedly skipped + +Interactive/manual tests are intentionally gated. Set `RUN_MANUAL_E2E=1` and run `tests/test_e2e_manual.py`. diff --git a/tests/lab_config.py b/tests/lab_config.py index 181ac035..f99b1683 100644 --- a/tests/lab_config.py +++ b/tests/lab_config.py @@ -21,8 +21,7 @@ Environment Variables: LAB_APP_CLIENT_ID: Client ID for Key Vault authentication (required) - LAB_APP_CLIENT_CERT_PFX_PATH: Path to .pfx certificate file (preferred) - LAB_APP_CLIENT_SECRET: Client secret (alternative to certificate) + LAB_APP_CLIENT_CERT_PFX_PATH: Path to .pfx certificate file (required) """ import json @@ -31,7 +30,7 @@ from dataclasses import dataclass from typing import Dict, Optional -from azure.identity import CertificateCredential, ClientSecretCredential +from azure.identity import CertificateCredential from azure.keyvault.secrets import SecretClient logger = logging.getLogger(__name__) @@ -169,9 +168,8 @@ def _get_credential(): """ Create an Azure credential for Key Vault access. - Reads authentication details from environment variables. Prefers - certificate-based authentication if LAB_APP_CLIENT_CERT_PFX_PATH is set, - otherwise falls back to client secret. + Reads authentication details from environment variables and uses + certificate-based authentication via LAB_APP_CLIENT_CERT_PFX_PATH. Returns: A credential object suitable for Azure SDK clients. @@ -180,7 +178,6 @@ def _get_credential(): EnvironmentError: If required environment variables are not set. """ client_id = os.getenv("LAB_APP_CLIENT_ID") - client_secret = os.getenv("LAB_APP_CLIENT_SECRET") cert_path = os.getenv("LAB_APP_CLIENT_CERT_PFX_PATH") tenant_id = "72f988bf-86f1-41af-91ab-2d7cd011db47" # Microsoft tenant @@ -196,16 +193,9 @@ def _get_credential(): certificate_path=cert_path, send_certificate_chain=True, ) - elif client_secret: - logger.debug("Using client secret credential for Key Vault access") - return ClientSecretCredential( - tenant_id=tenant_id, - client_id=client_id, - client_secret=client_secret, - ) else: raise EnvironmentError( - "Either LAB_APP_CLIENT_SECRET or LAB_APP_CLIENT_CERT_PFX_PATH is required") + "LAB_APP_CLIENT_CERT_PFX_PATH is required") def _get_msid_lab_client() -> SecretClient: diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 77af940b..daab80f3 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -1,9 +1,6 @@ """If the following ENV VAR were available, many end-to-end test cases would run. -LAB_OBO_CLIENT_SECRET=... LAB_APP_CLIENT_ID=... LAB_APP_CLIENT_CERT_PFX_PATH=... -LAB_OBO_PUBLIC_CLIENT_ID=... -LAB_OBO_CONFIDENTIAL_CLIENT_ID=... """ try: from dotenv import load_dotenv # Use this only in local dev machine @@ -47,6 +44,7 @@ _PYMSALRUNTIME_INSTALLED = is_pymsalruntime_installed() _AZURE_CLI = "04b07795-8ddb-461a-bbee-02f9e1bf7b46" +_SKIP_UNATTENDED_E2E_TESTS = os.getenv("TRAVIS") or not os.getenv("CI") def _get_app_and_auth_code( client_id, @@ -230,7 +228,7 @@ def _test_username_password(self, return result @unittest.skipIf( - os.getenv("TRAVIS"), # It is set when running on TravisCI or Github Actions + _SKIP_UNATTENDED_E2E_TESTS, "Although it is doable, we still choose to skip device flow to save time") def _test_device_flow( self, @@ -262,7 +260,7 @@ def _test_device_flow( logger.info( "%s obtained tokens: %s", self.id(), json.dumps(result, indent=4)) - @unittest.skipIf(os.getenv("TRAVIS"), "Browser automation is not yet implemented") + @unittest.skipIf(_SKIP_UNATTENDED_E2E_TESTS, "Browser automation is not yet implemented") def _test_acquire_token_interactive( self, *, client_id=None, authority=None, scope=None, port=None, oidc_authority=None, @@ -331,17 +329,23 @@ def test_access_token_should_be_obtained_for_a_supported_scope(self): self.assertIsNotNone(result.get("access_token")) -THIS_FOLDER = os.path.dirname(__file__) -CONFIG = os.path.join(THIS_FOLDER, "config.json") -@unittest.skipUnless(os.path.exists(CONFIG), "Optional %s not found" % CONFIG) -class FileBasedTestCase(E2eTestCase): - # This covers scenarios that are not currently available for test automation. - # So they mean to be run on maintainer's machine for semi-automated tests. +@unittest.skipIf(os.getenv("TF_BUILD"), "Skip PublicCloud scenarios on Azure DevOps") +class PublicCloudScenariosTestCase(E2eTestCase): + # Historically this class was driven by tests/config.json for semi-automated runs. + # It now uses lab config + env vars so it can run automatically without local files. @classmethod def setUpClass(cls): - with open(CONFIG) as f: - cls.config = json.load(f) + pca_app = get_app_config(AppSecrets.PCA_CLIENT) + user = get_user_config(UserSecrets.PUBLIC_CLOUD) + cls.config = { + "client_id": pca_app.app_id, + "authority": user.authority, + "username": user.upn, + "password": get_user_password(user), + "scope": ["https://graph.microsoft.com/.default"], + "listen_port": 44331, + } def skipUnlessWithConfig(self, fields): for field in fields: @@ -376,13 +380,19 @@ def _test_auth_code(self, auth_kwargs, token_kwargs): error_description=result.get("error_description"))) self.assertCacheWorksForUser(result, self.config["scope"], username=None) - def test_auth_code(self): + def manual_test_auth_code(self): + if _SKIP_UNATTENDED_E2E_TESTS: + self.skipTest("Browser automation is not yet implemented") self._test_auth_code({}, {}) - def test_auth_code_with_matching_nonce(self): + def manual_test_auth_code_with_matching_nonce(self): + if _SKIP_UNATTENDED_E2E_TESTS: + self.skipTest("Browser automation is not yet implemented") self._test_auth_code({"nonce": "foo"}, {"nonce": "foo"}) - def test_auth_code_with_mismatching_nonce(self): + def manual_test_auth_code_with_mismatching_nonce(self): + if _SKIP_UNATTENDED_E2E_TESTS: + self.skipTest("Browser automation is not yet implemented") self.skipUnlessWithConfig(["client_id", "scope"]) (self.app, ac, redirect_uri) = self._get_app_and_auth_code(nonce="foo") with self.assertRaises(ValueError): @@ -390,80 +400,54 @@ def test_auth_code_with_mismatching_nonce(self): ac, self.config["scope"], redirect_uri=redirect_uri, nonce="bar") def test_client_secret(self): - self.skipUnlessWithConfig(["client_id", "client_secret"]) - self.app = msal.ConfidentialClientApplication( - self.config["client_id"], - client_credential=self.config.get("client_secret"), - authority=self.config.get("authority"), - http_client=MinimalHttpClient()) - scope = self.config.get("scope", []) - result = self.app.acquire_token_for_client(scope) - self.assertIn('access_token', result) - self.assertCacheWorksForApp(result, scope) - - def test_client_certificate(self): - self.skipUnlessWithConfig(["client_id", "client_certificate"]) - client_cert = self.config["client_certificate"] - assert "private_key_path" in client_cert and "thumbprint" in client_cert - with open(os.path.join(THIS_FOLDER, client_cert['private_key_path'])) as f: - private_key = f.read() # Should be in PEM format + app = get_app_config(AppSecrets.S2S_CLIENT) + secret_name = app.secret_name or app.client_secret + if not (app.app_id and secret_name): + self.skipTest("S2S app configuration is incomplete") self.app = msal.ConfidentialClientApplication( - self.config['client_id'], - {"private_key": private_key, "thumbprint": client_cert["thumbprint"]}, + app.app_id, + client_credential=get_secret(secret_name, vault="msal_team"), + authority=app.authority, http_client=MinimalHttpClient()) - scope = self.config.get("scope", []) + scope = ["https://graph.microsoft.com/.default"] result = self.app.acquire_token_for_client(scope) self.assertIn('access_token', result) self.assertCacheWorksForApp(result, scope) def test_subject_name_issuer_authentication(self): - self.skipUnlessWithConfig(["client_id", "client_certificate"]) - client_cert = self.config["client_certificate"] - assert "private_key_path" in client_cert and "thumbprint" in client_cert - if not "public_certificate" in client_cert: - self.skipTest("Skipping SNI test due to lack of public_certificate") - with open(os.path.join(THIS_FOLDER, client_cert['private_key_path'])) as f: - private_key = f.read() # Should be in PEM format - with open(os.path.join(THIS_FOLDER, client_cert['public_certificate'])) as f: - public_certificate = f.read() - self.app = msal.ConfidentialClientApplication( - self.config['client_id'], authority=self.config["authority"], - client_credential={ - "private_key": private_key, - "thumbprint": self.config["thumbprint"], - "public_certificate": public_certificate, - }, - http_client=MinimalHttpClient()) - scope = self.config.get("scope", []) - result = self.app.acquire_token_for_client(scope) - self.assertIn('access_token', result) - self.assertCacheWorksForApp(result, scope) + from tests.lab_config import get_client_certificate + + client_id = os.getenv("LAB_APP_CLIENT_ID") + if not client_id: + self.skipTest("LAB_APP_CLIENT_ID environment variable is required") - def test_client_assertion(self): - self.skipUnlessWithConfig(["client_id", "client_assertion"]) self.app = msal.ConfidentialClientApplication( - self.config['client_id'], authority=self.config["authority"], - client_credential={"client_assertion": self.config["client_assertion"]}, + client_id, + authority="https://login.microsoftonline.com/microsoft.onmicrosoft.com", + client_credential=get_client_certificate(), http_client=MinimalHttpClient()) - scope = self.config.get("scope", []) + scope = ["https://graph.microsoft.com/.default"] result = self.app.acquire_token_for_client(scope) self.assertIn('access_token', result) self.assertCacheWorksForApp(result, scope) -@unittest.skipUnless(os.path.exists(CONFIG), "Optional %s not found" % CONFIG) class DeviceFlowTestCase(E2eTestCase): # A leaf class so it will be run only once @classmethod def setUpClass(cls): - with open(CONFIG) as f: - cls.config = json.load(f) + app = get_app_config(AppSecrets.PCA_CLIENT) + user = get_user_config(UserSecrets.PUBLIC_CLOUD) + cls.config = { + "client_id": app.app_id, + "authority": user.authority, + "scope": ["https://graph.microsoft.com/.default"], + } - def test_device_flow(self): + def manual_test_device_flow(self): self._test_device_flow(**self.config) def get_lab_app( env_client_id="LAB_APP_CLIENT_ID", - env_name2="LAB_APP_CLIENT_SECRET", # A var name that hopefully avoids false alarm env_client_cert_path="LAB_APP_CLIENT_CERT_PFX_PATH", authority="https://login.microsoftonline.com/" "72f988bf-86f1-41af-91ab-2d7cd011db47", # Microsoft tenant ID @@ -476,7 +460,7 @@ def get_lab_app( logger.info( "Reading ENV variables %s and %s for lab app defined at " "https://docs.msidlab.com/accounts/confidentialclient.html", - env_client_id, env_name2) + env_client_id, env_client_cert_path) if os.getenv(env_client_id) and os.getenv(env_client_cert_path): # id came from https://docs.msidlab.com/accounts/confidentialclient.html client_id = os.getenv(env_client_id) @@ -486,11 +470,6 @@ def get_lab_app( os.getenv(env_client_cert_path), "public_certificate": True, # Opt in for SNI } - elif os.getenv(env_client_id) and os.getenv(env_name2): - # Data came from here - # https://docs.msidlab.com/accounts/confidentialclient.html - client_id = os.getenv(env_client_id) - client_credential = os.getenv(env_name2) else: logger.info("ENV variables are not defined. Fall back to MSI.") # See also https://microsoft.sharepoint-df.com/teams/MSIDLABSExtended/SitePages/Programmatically-accessing-LAB-API's.aspx @@ -537,7 +516,7 @@ def setUpClass(cls): def tearDownClass(cls): cls.session.close() - @unittest.skipIf(os.getenv("TRAVIS"), "Browser automation is not yet implemented") + @unittest.skipIf(_SKIP_UNATTENDED_E2E_TESTS, "Browser automation is not yet implemented") def _test_acquire_token_by_auth_code( self, client_id=None, authority=None, port=None, scope=None, **ignored): @@ -560,7 +539,7 @@ def _test_acquire_token_by_auth_code( error_description=result.get("error_description"))) self.assertCacheWorksForUser(result, scope, username=None) - @unittest.skipIf(os.getenv("TRAVIS"), "Browser automation is not yet implemented") + @unittest.skipIf(_SKIP_UNATTENDED_E2E_TESTS, "Browser automation is not yet implemented") def _test_acquire_token_by_auth_code_flow( self, client_id=None, authority=None, port=None, scope=None, username=None, lab_name=None, @@ -881,7 +860,7 @@ def test_adfs2022_fed_user(self): scope=["https://graph.microsoft.com/.default"], ) - def test_cloud_acquire_token_interactive(self): + def manual_test_cloud_acquire_token_interactive(self): """Test interactive flow for a cloud user.""" app = get_app_config(AppSecrets.PCA_CLIENT) user = get_user_config(UserSecrets.PUBLIC_CLOUD) @@ -893,7 +872,7 @@ def test_cloud_acquire_token_interactive(self): scope=["https://graph.microsoft.com/.default"], ) - def test_msa_pt_app_signin_via_organizations_authority_without_login_hint(self): + def manual_test_msa_pt_app_signin_via_organizations_authority_without_login_hint(self): """There is/was an upstream bug. See test case full docstring for the details. When a MSAL-PT flow that account control is launched, user has 2+ AAD accounts in WAM, @@ -1007,7 +986,7 @@ def _build_b2c_authority(self, policy): base = "https://msidlabb2c.b2clogin.com/msidlabb2c.onmicrosoft.com" return base + "/" + policy # We do not support base + "?p=" + policy - def test_b2c_acquire_token_by_auth_code(self): + def manual_test_b2c_acquire_token_by_auth_code(self): """Test auth code flow for B2C.""" # TODO: Update with B2C app config from Key Vault when available app = get_app_config(AppSecrets.B2C_CLIENT) # TODO: Verify secret name @@ -1018,7 +997,7 @@ def test_b2c_acquire_token_by_auth_code(self): scope=app.defaultscopes, ) - def test_b2c_acquire_token_by_auth_code_flow(self): + def manual_test_b2c_acquire_token_by_auth_code_flow(self): """Test auth code flow (with PKCE) for B2C.""" user = get_user_config(UserSecrets.B2C) # TODO: Verify secret name app = get_app_config(AppSecrets.B2C_CLIENT) # TODO: Verify secret name @@ -1078,7 +1057,7 @@ def setUpClass(cls): # CIAM authority format: https://.ciamlogin.com/ cls.ciam_authority = f"https://{cls.user_config.lab_name}.ciamlogin.com/" - def test_ciam_acquire_token_interactive(self): + def manual_test_ciam_acquire_token_interactive(self): """Test interactive flow for CIAM.""" self._test_acquire_token_interactive( authority=self.ciam_authority, @@ -1113,7 +1092,7 @@ def test_ciam_acquire_token_by_ropc(self): @unittest.skip("""As of Aug 2024, in both ciam2 and ciam6, sign-in fails with AADSTS500208: The domain is not a valid login domain for the account type.""") - def test_ciam_device_flow(self): + def manual_test_ciam_device_flow(self): self._test_device_flow( authority=self.ciam_authority, client_id=self.app_config.app_id, @@ -1136,7 +1115,7 @@ def setUpClass(cls): cls.ciam_authority = None # Clear standard authority cls.oidc_authority = cls.CIAM_CUD_OIDC_AUTHORITY - def test_ciam_acquire_token_interactive(self): + def manual_test_ciam_acquire_token_interactive(self): """Test interactive flow for CIAM CUD with OIDC authority.""" self._test_acquire_token_interactive( oidc_authority=self.oidc_authority, @@ -1336,7 +1315,7 @@ def test_acquire_token_by_ropc(self): scope=["https://graph.microsoft.us/.default"], ) - def test_acquire_token_device_flow(self): + def manual_test_acquire_token_device_flow(self): """Test device code flow for Arlington cloud.""" self._test_device_flow( authority=self.arlington_authority, @@ -1374,7 +1353,7 @@ class PopTestCase(LabBasedTestCase): These tests require pymsalruntime to be installed. """ - def test_at_pop_should_contain_pop_scheme_content(self): + def manual_test_at_pop_should_contain_pop_scheme_content(self): auth_scheme = msal.PopAuthScheme( http_method=msal.PopAuthScheme.HTTP_GET, url="https://www.Contoso.com/Path1/Path2?queryParam1=a&queryParam2=b", @@ -1397,7 +1376,7 @@ def test_at_pop_should_contain_pop_scheme_content(self): self.assertEqual(payload["nonce"], auth_scheme._nonce) # TODO: Remove this, as ROPC support is removed by Broker-on-Win - def test_at_pop_via_testingsts_service(self): + def manual_test_at_pop_via_testingsts_service(self): """Test AT POP via testingsts.azurewebsites.net nonce validation service.""" self.skipTest("ROPC support is removed by Broker-on-Win") auth_scheme = msal.PopAuthScheme( @@ -1435,7 +1414,7 @@ def test_at_pop_via_testingsts_service(self): ) self.assertEqual(validation.status_code, 200) - def test_at_pop_calling_pattern(self): + def manual_test_at_pop_calling_pattern(self): """Test the POP calling pattern with Key Vault config. The calling pattern was described here: diff --git a/tests/test_e2e_manual.py b/tests/test_e2e_manual.py new file mode 100644 index 00000000..43cb83f8 --- /dev/null +++ b/tests/test_e2e_manual.py @@ -0,0 +1,84 @@ +import os +import unittest + +from tests.test_e2e import ( + E2eTestCase, + LabBasedTestCase, + PublicCloudScenariosTestCase, + DeviceFlowTestCase, + WorldWideTestCase, + CiamTestCase, + CiamCudTestCase, + ArlingtonCloudTestCase, + PopTestCase, +) + + +_MANUAL_E2E_ENABLED = bool(os.getenv("RUN_MANUAL_E2E")) +_MANUAL_SKIP_REASON = "Manual E2E tests. Set RUN_MANUAL_E2E=1 to run." + + +@unittest.skipUnless(_MANUAL_E2E_ENABLED, _MANUAL_SKIP_REASON) +class PublicCloudScenariosManualTestCase(PublicCloudScenariosTestCase): + def test_auth_code(self): + self.manual_test_auth_code() + + def test_auth_code_with_matching_nonce(self): + self.manual_test_auth_code_with_matching_nonce() + + def test_auth_code_with_mismatching_nonce(self): + self.manual_test_auth_code_with_mismatching_nonce() + + +@unittest.skipUnless(_MANUAL_E2E_ENABLED, _MANUAL_SKIP_REASON) +class DeviceFlowManualTestCase(DeviceFlowTestCase): + def test_device_flow(self): + self.manual_test_device_flow() + + +@unittest.skipUnless(_MANUAL_E2E_ENABLED, _MANUAL_SKIP_REASON) +class WorldWideManualTestCase(WorldWideTestCase): + def test_cloud_acquire_token_interactive(self): + self.manual_test_cloud_acquire_token_interactive() + + def test_msa_pt_app_signin_via_organizations_authority_without_login_hint(self): + self.manual_test_msa_pt_app_signin_via_organizations_authority_without_login_hint() + + def test_b2c_acquire_token_by_auth_code(self): + self.manual_test_b2c_acquire_token_by_auth_code() + + def test_b2c_acquire_token_by_auth_code_flow(self): + self.manual_test_b2c_acquire_token_by_auth_code_flow() + + +@unittest.skipUnless(_MANUAL_E2E_ENABLED, _MANUAL_SKIP_REASON) +class CiamManualTestCase(CiamTestCase): + def test_ciam_acquire_token_interactive(self): + self.manual_test_ciam_acquire_token_interactive() + + def test_ciam_device_flow(self): + self.manual_test_ciam_device_flow() + + +@unittest.skipUnless(_MANUAL_E2E_ENABLED, _MANUAL_SKIP_REASON) +class CiamCudManualTestCase(CiamCudTestCase): + def test_ciam_acquire_token_interactive(self): + self.manual_test_ciam_acquire_token_interactive() + + +@unittest.skipUnless(_MANUAL_E2E_ENABLED, _MANUAL_SKIP_REASON) +class ArlingtonManualTestCase(ArlingtonCloudTestCase): + def test_acquire_token_device_flow(self): + self.manual_test_acquire_token_device_flow() + + +@unittest.skipUnless(_MANUAL_E2E_ENABLED, _MANUAL_SKIP_REASON) +class PopManualTestCase(PopTestCase): + def test_at_pop_should_contain_pop_scheme_content(self): + self.manual_test_at_pop_should_contain_pop_scheme_content() + + def test_at_pop_via_testingsts_service(self): + self.manual_test_at_pop_via_testingsts_service() + + def test_at_pop_calling_pattern(self): + self.manual_test_at_pop_calling_pattern()