From aaff5a8d42a79bb7305fabf828c423bf6a707944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Rodr=C3=ADguez?= <127134616+armando-rodriguez-cko@users.noreply.github.com> Date: Mon, 10 Aug 2026 14:36:59 +0200 Subject: [PATCH 1/5] feat: require environment_subdomain or an explicit use_legacy_domain opt-out The merchant-specific subdomain is how merchants should reach the API, but it was optional and an unset value silently fell back to api.checkout.com, so a forgotten subdomain looked exactly like a deliberate opt-out and the SDK could not warn about either. Callers must now choose: set environment_subdomain, or call use_legacy_domain(), which raises a DeprecationWarning from its first release. Both, or neither, raises CheckoutArgumentException. An invalid subdomain now raises instead of being quietly ignored, which is a second breaking change: callers passing a malformed value are currently served by the shared host and never find out. environment_subdomain no longer needs environment() to be set first, since the EnvironmentSubdomain is now built when the configuration is assembled. That also removed the duplicated with/without-subdomain branches in all three build() methods. The Previous (ABC) platform predates merchant-specific subdomains and stays exempt via _requires_environment_subdomain(). Fixtures route clients through conftest.configure_domain, which uses the shared hosts: the sandbox OAuth clients are not provisioned for the subdomain, so applying it makes every client_credentials request return invalid_client. The long import line in the APM test is wrapped only because pre-commit lints staged files, so touching that file surfaced a pre-existing violation. Mirrors checkout-sdk-net#590. Refs INT-1688. --- README.md | 28 +++++++-- checkout_sdk/checkout_sdk_builder.py | 57 +++++++++++++++++-- checkout_sdk/default_sdk.py | 17 ++---- checkout_sdk/environment_subdomain.py | 53 +++++++++-------- checkout_sdk/oauth_sdk.py | 38 +++++-------- checkout_sdk/previous/previous_sdk.py | 22 +++---- checkout_sdk/properties.py | 2 +- tests/accounts/accounts_integration_test.py | 7 ++- ...ounts_payout_schedules_integration_test.py | 7 ++- tests/checkout_configuration_test.py | 43 +++----------- tests/checkout_default_sdk_test.py | 55 ++++++++++++++++++ tests/conftest.py | 32 ++++++++--- tests/issuing/conftest.py | 8 +-- tests/oauth_integration_test.py | 2 + .../request_apm_payments_integration_test.py | 10 ++-- 15 files changed, 243 insertions(+), 138 deletions(-) diff --git a/README.md b/README.md index 73d8b093..d90de69d 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,10 @@ account [here](https://www.checkout.com/get-test-account). **PLEASE NEVER SHARE OR PUBLISH YOUR CHECKOUT CREDENTIALS.** +### Subdomain value + +Requests must be made through your merchant-specific subdomain (MSSD): the first 8 characters of your client ID (excluding `cli_`). For example, if your client ID is `cli_vkuhvk4vjn2edkps7dfsq6emqm`, your subdomain is `vkuhvk4v`. When `environment_subdomain` is set the SDK sends requests to `https://vkuhvk4v.api.checkout.com`. See [Base URLs](https://api-reference.checkout.com/#section/Base-URLs) and [API endpoints](https://www.checkout.com/docs/developer-resources/api/api-endpoints) for further details, and for where to find your unique client ID. + ### Default Default keys client instantiation can be done as follows: @@ -82,7 +86,7 @@ def default(): .secret_key('secret_key') .public_key('public_key') # optional, only required for operations related with tokens .environment(Environment.sandbox()) # or production() - .environment_subdomain("subdomain") # optional, Merchant-specific DNS name + .environment_subdomain("subdomain") # required, Merchant-specific DNS name, the first 8 characters of your client ID .build() payments_client = checkout_api.payments @@ -105,7 +109,7 @@ def oauth(): .oauth() .client_credentials(client_id='client_id', client_secret='client_secret') .environment(Environment.sandbox()) # or production() - .environment_subdomain("subdomain") # optional, Merchant-specific DNS name + .environment_subdomain("subdomain") # required, Merchant-specific DNS name, the first 8 characters of your client ID .scopes([OAuthScopes.GATEWAY_PAYMENT_REFUNDS, OAuthScopes.FILES]) # optional, array of scopes .build() @@ -129,7 +133,7 @@ def previous(): .secret_key('secret_key') .public_key('public_key') # optional, only required for operations related with tokens .environment(Environment.sandbox()) # or production() - .environment_subdomain("subdomain") # optional, Merchant-specific DNS name + .environment_subdomain("subdomain") # optional for the Previous platform, Merchant-specific DNS name .build() payments_client = checkout_api.payments @@ -175,7 +179,7 @@ def oauth(): .oauth() .client_credentials(client_id='client_id', client_secret='client_secret') .environment(Environment.sandbox()) # or production() - .environment_subdomain("subdomain") # optional, Merchant-specific DNS name + .environment_subdomain("subdomain") # required, Merchant-specific DNS name, the first 8 characters of your client ID .http_client_builder(CustomHttpClientBuilder()) # optional .scopes([OAuthScopes.GATEWAY_PAYMENT_REFUNDS, OAuthScopes.FILES]) # optional, array of scopes .build() @@ -267,6 +271,22 @@ The execution of integration tests require the following environment variables s * For Previous account systems: `CHECKOUT_PREVIOUS_PUBLIC_KEY` & `CHECKOUT_PREVIOUS_SECRET_KEY` * Processing channel: `CHECKOUT_PROCESSING_CHANNEL_ID` +## Legacy domain (emergency use only) + +> :warning: **Only use if merchant specific sub domains are causing issues.** Connecting through your merchant-specific subdomain (see [Subdomain value](#subdomain-value)) is the supported way of using the Checkout.com API, and non-subdomain usage will be deprecated. + +If, in exceptional circumstances, you cannot use your merchant-specific subdomain, you can explicitly opt out by calling `use_legacy_domain()` instead of `environment_subdomain(...)`: + +```python +checkout_api = CheckoutSdk.builder() \ + .secret_key("secret_key") \ + .environment(Environment.sandbox()) \ + .use_legacy_domain() \ + .build() +``` + +This routes requests to `api.checkout.com` (or `api.sandbox.checkout.com`) and `access.checkout.com` (or `access.sandbox.checkout.com`). The method raises a `DeprecationWarning`, so `python -W error::DeprecationWarning` and most linters will flag it. Exactly one of `environment_subdomain(...)` or `use_legacy_domain()` must be set: the SDK raises a `CheckoutArgumentException` if both, or neither, are. The Previous (ABC) platform predates merchant-specific subdomains and is exempt from this requirement. + ## Code of Conduct Please refer to [Code of Conduct](CODE_OF_CONDUCT.md) diff --git a/checkout_sdk/checkout_sdk_builder.py b/checkout_sdk/checkout_sdk_builder.py index 3f0b8e96..3d7f4946 100644 --- a/checkout_sdk/checkout_sdk_builder.py +++ b/checkout_sdk/checkout_sdk_builder.py @@ -1,10 +1,12 @@ from __future__ import absolute_import +import warnings from typing import Optional from checkout_sdk.default_http_client import DefaultHttpClientBuilder from checkout_sdk.environment import Environment from checkout_sdk.environment_subdomain import EnvironmentSubdomain +from checkout_sdk.exception import CheckoutArgumentException from checkout_sdk.http_client_interface import HttpClientBuilderInterface @@ -12,7 +14,8 @@ class CheckoutSdkBuilder: def __init__(self): self._environment = Environment.sandbox() - self._environment_subdomain = None + self._subdomain = None + self._use_legacy_domain = False self._http_client = DefaultHttpClientBuilder().get_client() def environment(self, environment: Environment): @@ -20,15 +23,59 @@ def environment(self, environment: Environment): return self def environment_subdomain(self, subdomain: Optional[str]): - if subdomain: - self._environment_subdomain = EnvironmentSubdomain(self._environment, subdomain) - else: - self._environment_subdomain = None + self._subdomain = subdomain + return self + + def use_legacy_domain(self): + """ + Opts out of the merchant-specific subdomain, sending every request to the shared hosts + instead (api.checkout.com and access.checkout.com, or their sandbox equivalents). + + Deprecated: this is an emergency fallback for the rare case where the merchant-specific + subdomain cannot be used, and will be removed in a future release. Call + environment_subdomain() instead. + See https://api-reference.checkout.com/#section/Base-URLs + """ + warnings.warn( + 'use_legacy_domain() is deprecated and will be removed in a future release. It is ' + 'intended only as an emergency fallback when the merchant-specific subdomain cannot ' + 'be used. Call environment_subdomain() instead. See ' + 'https://api-reference.checkout.com/#section/Base-URLs', + DeprecationWarning, + stacklevel=2) + self._use_legacy_domain = True return self def http_client_builder(self, http_client_builder: HttpClientBuilderInterface): self._http_client = http_client_builder.get_client() return self + @property + def _environment_subdomain(self) -> Optional[EnvironmentSubdomain]: + if self._subdomain is None: + return None + return EnvironmentSubdomain(self._environment, self._subdomain) + + def _requires_environment_subdomain(self) -> bool: + """ + Whether this builder requires the merchant-specific subdomain to be configured. The + Previous (ABC) platform predates merchant-specific subdomains, so it overrides this to + False. + """ + return True + + def _validate_environment_settings(self): + if self._subdomain is not None and self._use_legacy_domain: + raise CheckoutArgumentException( + 'environment_subdomain and use_legacy_domain cannot both be set - provide only ' + 'your merchant-specific subdomain') + if self._subdomain is None and not self._use_legacy_domain and self._requires_environment_subdomain(): + raise CheckoutArgumentException( + 'environment_subdomain is required - provide your merchant-specific subdomain ' + '(the first 8 characters of your client ID, see ' + 'https://api-reference.checkout.com/#section/Base-URLs), or call ' + 'use_legacy_domain() to opt out only if merchant specific sub domains are ' + 'causing issues') + def build(self): raise NotImplementedError() diff --git a/checkout_sdk/default_sdk.py b/checkout_sdk/default_sdk.py index 3305ee42..700efeea 100644 --- a/checkout_sdk/default_sdk.py +++ b/checkout_sdk/default_sdk.py @@ -39,15 +39,10 @@ def oauth(): def build(self): validate_secret_key(self._SECRET_KEY_PATTERN, self._secret_key) validate_public_key(self._PUBLIC_KEY_PATTERN, self._public_key) - if self._environment_subdomain is not None: - configuration = CheckoutConfiguration( - credentials=DefaultKeysSdkCredentials(secret_key=self._secret_key, public_key=self._public_key), - environment=self._environment, - http_client=self._http_client, - environment_subdomain=self._environment_subdomain) - else: - configuration = CheckoutConfiguration( - credentials=DefaultKeysSdkCredentials(secret_key=self._secret_key, public_key=self._public_key), - environment=self._environment, - http_client=self._http_client) + self._validate_environment_settings() + configuration = CheckoutConfiguration( + credentials=DefaultKeysSdkCredentials(secret_key=self._secret_key, public_key=self._public_key), + environment=self._environment, + http_client=self._http_client, + environment_subdomain=self._environment_subdomain) return CheckoutApi(configuration) diff --git a/checkout_sdk/environment_subdomain.py b/checkout_sdk/environment_subdomain.py index ff6be2d8..7d60de4e 100644 --- a/checkout_sdk/environment_subdomain.py +++ b/checkout_sdk/environment_subdomain.py @@ -2,6 +2,7 @@ from urllib.parse import urlparse, urlunparse from checkout_sdk.environment import Environment +from checkout_sdk.exception import CheckoutArgumentException class EnvironmentSubdomain: @@ -12,39 +13,41 @@ def __init__(self, environment: Environment, subdomain: str): @staticmethod def create_url_with_subdomain(original_url: str, subdomain: str): """ - Applies subdomain transformation to any given URL. - If the subdomain is valid (alphanumeric pattern), prepends it to the host. - Otherwise, returns the original URL unchanged. + Applies subdomain transformation to any given URL, prepending the subdomain to the host. Args: original_url: the original URL to transform subdomain: the subdomain to prepend Returns: - the transformed URL with subdomain, or original URL if subdomain is invalid - """ - new_environment = original_url + the transformed URL with subdomain + Raises: + CheckoutArgumentException: if the subdomain is not a valid merchant-specific subdomain + """ regex = r'^(?:pl-)?[a-z0-9]+$' - if re.match(regex, subdomain): - url_parts = urlparse(original_url) - if url_parts.port: - new_host = subdomain + '.' + url_parts.hostname + ':' + str(url_parts.port) - else: - new_host = subdomain + '.' + url_parts.hostname - - new_url_parts = ( - url_parts.scheme, - new_host, - url_parts.path, - url_parts.params, - url_parts.query, - url_parts.fragment - ) - - new_environment = urlunparse(new_url_parts) - - return new_environment + if subdomain is None or not re.match(regex, subdomain): + raise CheckoutArgumentException( + 'invalid environment subdomain - provide your merchant-specific subdomain, the ' + 'first 8 characters of your client ID (see ' + 'https://api-reference.checkout.com/#section/Base-URLs)') + + url_parts = urlparse(original_url) + if url_parts.port: + new_host = subdomain + '.' + url_parts.hostname + ':' + str(url_parts.port) + else: + new_host = subdomain + '.' + url_parts.hostname + + new_url_parts = ( + url_parts.scheme, + new_host, + url_parts.path, + url_parts.params, + url_parts.query, + url_parts.fragment + ) + + return urlunparse(new_url_parts) def base_uri(self) -> str: return self.base_uri diff --git a/checkout_sdk/oauth_sdk.py b/checkout_sdk/oauth_sdk.py index 0ec97a63..aa45ca9d 100644 --- a/checkout_sdk/oauth_sdk.py +++ b/checkout_sdk/oauth_sdk.py @@ -31,9 +31,12 @@ def scopes(self, scopes: list): return self def build(self): + self._validate_environment_settings() + environment_subdomain = self._environment_subdomain + # Determine the authorization URI based on subdomain configuration - if self._environment_subdomain is not None: - authorization_uri = self._environment_subdomain.authorization_uri + if environment_subdomain is not None: + authorization_uri = environment_subdomain.authorization_uri else: authorization_uri = self._environment.authorization_uri @@ -41,25 +44,14 @@ def build(self): if self._authorization_uri: authorization_uri = self._authorization_uri - if self._environment_subdomain is not None: - configuration = CheckoutConfiguration( - credentials=OAuthSdkCredentials.init(http_client=self._http_client, - environment=self._environment, - client_id=self._client_id, - client_secret=self._client_secret, - scopes=self._scopes, - authorization_uri=authorization_uri), - environment=self._environment, - http_client=self._http_client, - environment_subdomain=self._environment_subdomain) - else: - configuration = CheckoutConfiguration( - credentials=OAuthSdkCredentials.init(http_client=self._http_client, - environment=self._environment, - client_id=self._client_id, - client_secret=self._client_secret, - scopes=self._scopes, - authorization_uri=authorization_uri), - environment=self._environment, - http_client=self._http_client) + configuration = CheckoutConfiguration( + credentials=OAuthSdkCredentials.init(http_client=self._http_client, + environment=self._environment, + client_id=self._client_id, + client_secret=self._client_secret, + scopes=self._scopes, + authorization_uri=authorization_uri), + environment=self._environment, + http_client=self._http_client, + environment_subdomain=environment_subdomain) return CheckoutApi(configuration) diff --git a/checkout_sdk/previous/previous_sdk.py b/checkout_sdk/previous/previous_sdk.py index 38c093d1..7dd4f3f9 100644 --- a/checkout_sdk/previous/previous_sdk.py +++ b/checkout_sdk/previous/previous_sdk.py @@ -27,18 +27,18 @@ class PreviousSdk(PreviousStaticKeys): def __init__(self): super().__init__() + # The Previous (ABC) platform predates merchant-specific subdomains, so it is exempt from + # the mandatory environment_subdomain/use_legacy_domain configuration. + def _requires_environment_subdomain(self) -> bool: + return False + def build(self): validate_secret_key(self._SECRET_KEY_PATTERN, self._secret_key) validate_public_key(self._PUBLIC_KEY_PATTERN, self._public_key) - if self._environment_subdomain is not None: - configuration = CheckoutConfiguration( - credentials=PreviousKeysSdkCredentials(secret_key=self._secret_key, public_key=self._public_key), - environment=self._environment, - http_client=self._http_client, - environment_subdomain=self._environment_subdomain) - else: - configuration = CheckoutConfiguration( - credentials=PreviousKeysSdkCredentials(secret_key=self._secret_key, public_key=self._public_key), - environment=self._environment, - http_client=self._http_client) + self._validate_environment_settings() + configuration = CheckoutConfiguration( + credentials=PreviousKeysSdkCredentials(secret_key=self._secret_key, public_key=self._public_key), + environment=self._environment, + http_client=self._http_client, + environment_subdomain=self._environment_subdomain) return CheckoutApi(ApiClient(configuration, configuration.environment.base_uri), configuration) diff --git a/checkout_sdk/properties.py b/checkout_sdk/properties.py index 7ce5c1b8..189c03bb 100644 --- a/checkout_sdk/properties.py +++ b/checkout_sdk/properties.py @@ -1 +1 @@ -VERSION = "3.12.0" +VERSION = "4.0.0" diff --git a/tests/accounts/accounts_integration_test.py b/tests/accounts/accounts_integration_test.py index 3fd14c1f..a05c16ae 100644 --- a/tests/accounts/accounts_integration_test.py +++ b/tests/accounts/accounts_integration_test.py @@ -16,18 +16,19 @@ from checkout_sdk.common.enums import Currency, Country, InstrumentType from checkout_sdk.files.files import FileRequest from checkout_sdk.oauth_scopes import OAuthScopes +from tests.conftest import configure_domain from tests.checkout_test_utils import assert_response, phone, address, new_uuid, get_project_root, random_email @pytest.fixture(scope='class') def accounts_checkout_api(): - return CheckoutSdk \ + builder = CheckoutSdk \ .builder() \ .oauth() \ .client_credentials(client_id=os.environ.get('CHECKOUT_DEFAULT_OAUTH_ACCOUNTS_CLIENT_ID'), client_secret=os.environ.get('CHECKOUT_DEFAULT_OAUTH_ACCOUNTS_CLIENT_SECRET')) \ - .scopes([OAuthScopes.ACCOUNTS, OAuthScopes.FILES]) \ - .build() + .scopes([OAuthScopes.ACCOUNTS, OAuthScopes.FILES]) + return configure_domain(builder).build() def test_should_create_get_and_update_onboard_entity(accounts_checkout_api): diff --git a/tests/accounts/accounts_payout_schedules_integration_test.py b/tests/accounts/accounts_payout_schedules_integration_test.py index 35498b38..a4b36f5c 100644 --- a/tests/accounts/accounts_payout_schedules_integration_test.py +++ b/tests/accounts/accounts_payout_schedules_integration_test.py @@ -9,18 +9,19 @@ from checkout_sdk.checkout_sdk import CheckoutSdk from checkout_sdk.common.enums import Currency from checkout_sdk.oauth_scopes import OAuthScopes +from tests.conftest import configure_domain from tests.checkout_test_utils import assert_response @pytest.fixture(scope='class') def payout_schedules_api(): - return CheckoutSdk \ + builder = CheckoutSdk \ .builder() \ .oauth() \ .client_credentials(client_id=os.environ.get('CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_ID'), client_secret=os.environ.get('CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_SECRET')) \ - .scopes([OAuthScopes.MARKETPLACE]) \ - .build() + .scopes([OAuthScopes.MARKETPLACE]) + return configure_domain(builder).build() @pytest.mark.skip(reason='not available') diff --git a/tests/checkout_configuration_test.py b/tests/checkout_configuration_test.py index db197d2b..20237fc5 100644 --- a/tests/checkout_configuration_test.py +++ b/tests/checkout_configuration_test.py @@ -6,6 +6,7 @@ from checkout_sdk.checkout_configuration import CheckoutConfiguration from checkout_sdk.environment import Environment +from checkout_sdk.exception import CheckoutArgumentException from checkout_sdk.environment_subdomain import EnvironmentSubdomain from checkout_sdk.default_keys_credentials import DefaultKeysSdkCredentials from checkout_sdk.http_client_interface import HttpClientBuilderInterface @@ -83,43 +84,13 @@ def test_should_create_configuration_with_subdomain(subdomain, expected_url): @pytest.mark.parametrize( - "subdomain, expected_url", - [ - ("", "https://api.sandbox.checkout.com/"), - (" ", "https://api.sandbox.checkout.com/"), - (" ", "https://api.sandbox.checkout.com/"), - (" - ", "https://api.sandbox.checkout.com/"), - ("a b", "https://api.sandbox.checkout.com/"), - ("ab c1.", "https://api.sandbox.checkout.com/"), - ("foo-", "https://api.sandbox.checkout.com/"), - ("-foo", "https://api.sandbox.checkout.com/"), - ("FooBar", "https://api.sandbox.checkout.com/"), - ("test-123", "https://api.sandbox.checkout.com/"), - ("foo-bar", "https://api.sandbox.checkout.com/"), - ("pl-", "https://api.sandbox.checkout.com/") - ] + "subdomain", + ["", " ", " ", " - ", "a b", "ab c1.", "foo-", "-foo", "FooBar", "test-123", "foo-bar", "pl-"] ) -def test_should_create_configuration_with_bad_subdomain(subdomain, expected_url): - credentials = DefaultKeysSdkCredentials( - os.environ.get("CHECKOUT_DEFAULT_SECRET_KEY"), - os.environ.get("CHECKOUT_DEFAULT_PUBLIC_KEY") - ) - http_client = Mock(spec=HttpClientBuilderInterface) - - environment_subdomain = EnvironmentSubdomain(Environment.sandbox(), subdomain) - - configuration = CheckoutConfiguration( - credentials=credentials, - environment=Environment.sandbox(), - http_client=http_client, - environment_subdomain=environment_subdomain - ) - - assert configuration.credentials == credentials - assert configuration.environment.base_uri == Environment.sandbox().base_uri - assert configuration.http_client == http_client - assert configuration.environment_subdomain.base_uri == expected_url - assert configuration.environment_subdomain.authorization_uri == "https://access.sandbox.checkout.com/connect/token" +def test_should_fail_with_bad_subdomain(subdomain): + with pytest.raises(CheckoutArgumentException) as excinfo: + EnvironmentSubdomain(Environment.sandbox(), subdomain) + assert "invalid environment subdomain" in str(excinfo.value) def test_should_create_configuration_with_subdomain_for_production(): diff --git a/tests/checkout_default_sdk_test.py b/tests/checkout_default_sdk_test.py index 3bf63059..0f47a7cb 100644 --- a/tests/checkout_default_sdk_test.py +++ b/tests/checkout_default_sdk_test.py @@ -13,6 +13,7 @@ def test_should_create_default_sdk(): .secret_key(os.environ.get("CHECKOUT_DEFAULT_SECRET_KEY")) \ .public_key(os.environ.get("CHECKOUT_DEFAULT_PUBLIC_KEY")) \ .environment(Environment.sandbox()) \ + .environment_subdomain('123domain') \ .build() sdk = CheckoutSdk \ @@ -20,12 +21,66 @@ def test_should_create_default_sdk(): .secret_key(os.environ.get("CHECKOUT_DEFAULT_SECRET_KEY")) \ .public_key(os.environ.get("CHECKOUT_DEFAULT_PUBLIC_KEY")) \ .environment(Environment.production()) \ + .environment_subdomain('123domain') \ .build() assert sdk is not None assert sdk.tokens is not None +def test_should_create_default_sdk_with_legacy_domain(): + with pytest.deprecated_call(): + sdk = CheckoutSdk \ + .builder() \ + .secret_key(os.environ.get("CHECKOUT_DEFAULT_SECRET_KEY")) \ + .public_key(os.environ.get("CHECKOUT_DEFAULT_PUBLIC_KEY")) \ + .environment(Environment.sandbox()) \ + .use_legacy_domain() \ + .build() + + assert sdk is not None + assert sdk.tokens is not None + + +def test_should_fail_without_subdomain_or_legacy_domain(): + with pytest.raises(CheckoutArgumentException) as excinfo: + CheckoutSdk \ + .builder() \ + .secret_key(os.environ.get("CHECKOUT_DEFAULT_SECRET_KEY")) \ + .public_key(os.environ.get("CHECKOUT_DEFAULT_PUBLIC_KEY")) \ + .environment(Environment.sandbox()) \ + .build() + + assert "environment_subdomain is required" in str(excinfo.value) + + +def test_should_fail_with_both_subdomain_and_legacy_domain(): + with pytest.raises(CheckoutArgumentException) as excinfo, pytest.deprecated_call(): + CheckoutSdk \ + .builder() \ + .secret_key(os.environ.get("CHECKOUT_DEFAULT_SECRET_KEY")) \ + .public_key(os.environ.get("CHECKOUT_DEFAULT_PUBLIC_KEY")) \ + .environment(Environment.sandbox()) \ + .environment_subdomain('123domain') \ + .use_legacy_domain() \ + .build() + + assert "cannot both be set" in str(excinfo.value) + + +def test_should_fail_with_invalid_subdomain(): + with pytest.raises(CheckoutArgumentException) as excinfo: + CheckoutSdk \ + .builder() \ + .secret_key(os.environ.get("CHECKOUT_DEFAULT_SECRET_KEY")) \ + .public_key(os.environ.get("CHECKOUT_DEFAULT_PUBLIC_KEY")) \ + .environment(Environment.sandbox()) \ + .environment_subdomain('not a subdomain') \ + .build() + + assert "invalid environment subdomain" in str(excinfo.value) + + def test_should_create_default_sdk_with_subdomain(): sdk_1 = CheckoutSdk \ .builder() \ diff --git a/tests/conftest.py b/tests/conftest.py index fd0953f1..7ec582c7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ import logging import os +import warnings import pytest import requests @@ -18,6 +19,22 @@ logging.getLogger('checkout').setLevel(logging.INFO) +def configure_domain(builder): + """ + Every client the suite builds has to choose a domain now that the merchant-specific + subdomain is mandatory, so they all come through here. + + The suite uses the shared hosts. It would be better to exercise the merchant-specific + subdomain, since that is the path merchants are being moved to, but the sandbox OAuth + clients are not provisioned for it: pointing the token request at + {subdomain}.access.sandbox.checkout.com returns invalid_client for every integration test. + Until those clients are bound to the subdomain, CI has to use the legacy hosts. + """ + with warnings.catch_warnings(): + warnings.simplefilter('ignore', DeprecationWarning) + return builder.use_legacy_domain() + + @pytest.fixture(scope='session', autouse=True) def previous_api(): return CheckoutSdk \ @@ -30,16 +47,15 @@ def previous_api(): @pytest.fixture(scope='session', autouse=True) def default_api(): - return CheckoutSdk() \ - .builder() \ - .secret_key(os.environ.get('CHECKOUT_DEFAULT_SECRET_KEY')) \ - .public_key(os.environ.get('CHECKOUT_DEFAULT_PUBLIC_KEY')) \ - .build() + return configure_domain(CheckoutSdk() + .builder() + .secret_key(os.environ.get('CHECKOUT_DEFAULT_SECRET_KEY')) + .public_key(os.environ.get('CHECKOUT_DEFAULT_PUBLIC_KEY'))).build() @pytest.fixture(scope='session', autouse=True) def oauth_api(): - return CheckoutSdk() \ + builder = CheckoutSdk() \ .builder() \ .oauth() \ .client_credentials(client_id=os.environ.get('CHECKOUT_DEFAULT_OAUTH_CLIENT_ID'), @@ -50,8 +66,8 @@ def oauth_api(): OAuthScopes.FILES, OAuthScopes.TRANSFERS, OAuthScopes.BALANCES_VIEW, OAuthScopes.VAULT_CARD_METADATA, OAuthScopes.FINANCIAL_ACTIONS, OAuthScopes.VAULT_REAL_TIME_ACCOUNT_UPDATER, OAuthScopes.PAYMENTS_SEARCH, - OAuthScopes.GATEWAY_PAYMENT_CANCELLATIONS]) \ - .build() + OAuthScopes.GATEWAY_PAYMENT_CANCELLATIONS]) + return configure_domain(builder).build() @pytest.fixture(scope='session', autouse=True) diff --git a/tests/issuing/conftest.py b/tests/issuing/conftest.py index b097c165..183a26a9 100644 --- a/tests/issuing/conftest.py +++ b/tests/issuing/conftest.py @@ -11,19 +11,19 @@ AuthorizationType, CardAuthorizationRequest from checkout_sdk.oauth_scopes import OAuthScopes from tests.checkout_test_utils import phone, address, assert_response +from tests.conftest import configure_domain @pytest.fixture(scope='module', autouse=True) def issuing_checkout_api(): - api = CheckoutSdk \ + builder = CheckoutSdk \ .builder() \ .oauth() \ .client_credentials(client_id=os.environ.get('CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID'), client_secret=os.environ.get('CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET')) \ .scopes([OAuthScopes.ISSUING_CLIENT, OAuthScopes.ISSUING_CARD_MGMT, - OAuthScopes.ISSUING_CONTROLS_READ, OAuthScopes.ISSUING_CONTROLS_WRITE]) \ - .build() - return api + OAuthScopes.ISSUING_CONTROLS_READ, OAuthScopes.ISSUING_CONTROLS_WRITE]) + return configure_domain(builder).build() @pytest.fixture(scope='module') diff --git a/tests/oauth_integration_test.py b/tests/oauth_integration_test.py index 179eab7c..0d59f5a9 100644 --- a/tests/oauth_integration_test.py +++ b/tests/oauth_integration_test.py @@ -24,6 +24,7 @@ def test_should_fail_init_authorization_invalid_credentials(): .client_credentials(client_id='fake_id', client_secret='fake_secret') \ .environment(Environment.sandbox()) \ + .use_legacy_domain() \ .scopes([OAuthScopes.GATEWAY, OAuthScopes.VAULT]) \ .build() except CheckoutException as err: @@ -39,6 +40,7 @@ def test_should_fail_init_authorization_invalid_credentials_and_host(): client_secret='fake_secret') \ .authorization_uri('https://test.checkout.com') \ .environment(Environment.sandbox()) \ + .environment_subdomain('123domain') \ .scopes([OAuthScopes.GATEWAY, OAuthScopes.VAULT]) \ .build() except CheckoutException as err: diff --git a/tests/payments/request_apm_payments_integration_test.py b/tests/payments/request_apm_payments_integration_test.py index 445d5621..0b1c1c1c 100644 --- a/tests/payments/request_apm_payments_integration_test.py +++ b/tests/payments/request_apm_payments_integration_test.py @@ -10,13 +10,15 @@ from checkout_sdk.exception import CheckoutApiException from checkout_sdk.payments.payment_apm import RequestIdealSource, RequestTamaraSource, \ PaymentRequestWeChatPaySource, RequestAlipayPlusSource, RequestP24Source, RequestKnetSource, \ - RequestBancontactSource, RequestMultiBancoSource, RequestPostFinanceSource, RequestStcPaySource, RequestAlmaSource, \ + RequestBancontactSource, RequestMultiBancoSource, RequestPostFinanceSource, RequestStcPaySource, \ + RequestAlmaSource, \ RequestKlarnaSource, RequestFawrySource, RequestTrustlySource, RequestCvConnectSource, RequestIllicadoSource, \ RequestSepaSource, RequestGiropaySource, RequestEpsSource, RequestBizumSource, RequestOctopusSource, \ RequestPlaidSource, RequestSequraSource from checkout_sdk.payments.payments import PaymentRequest, ProcessingSettings, FawryProduct, PaymentCustomerRequest, \ ShippingDetails, PaymentMethodDetails from checkout_sdk.payments.payments_apm_previous import RequestSofortSource +from tests.conftest import configure_domain from tests.checkout_test_utils import assert_response, SUCCESS_URL, FAILURE_URL, retriable, address, FIRST_NAME, \ LAST_NAME, phone, check_error_item, PAYEE_NOT_ONBOARDED, APM_SERVICE_UNAVAILABLE, random_email, new_uuid, \ account_holder, REFERENCE, DESCRIPTION, APM_CURRENCY_NOT_SUPPORTED @@ -143,12 +145,12 @@ def test_should_request_tamara_payment(): payment_request.reference = 'ORD-5023-4E89' payment_request.items = [product] - preview_api = CheckoutSdk \ + preview_builder = CheckoutSdk \ .builder() \ .oauth() \ .client_credentials(client_id=os.environ.get('CHECKOUT_PREVIEW_OAUTH_CLIENT_ID'), - client_secret=os.environ.get('CHECKOUT_PREVIEW_OAUTH_CLIENT_SECRET')) \ - .build() + client_secret=os.environ.get('CHECKOUT_PREVIEW_OAUTH_CLIENT_SECRET')) + preview_api = configure_domain(preview_builder).build() payment_response = retriable(callback=preview_api.payments.request_payment, payment_request=payment_request) From 0650edaa3a79730fdf719682907b10002cf99ab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Rodr=C3=ADguez?= <127134616+armando-rodriguez-cko@users.noreply.github.com> Date: Tue, 11 Aug 2026 10:55:04 +0200 Subject: [PATCH 2/5] test: silence the deprecation warning in the OAuth credentials test Flagged in review: the test called use_legacy_domain() directly, so it emitted the deprecation warning on every run, inconsistent with every other fixture. It now goes through conftest.configure_domain like the rest. --- tests/oauth_integration_test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/oauth_integration_test.py b/tests/oauth_integration_test.py index 0d59f5a9..31ba2276 100644 --- a/tests/oauth_integration_test.py +++ b/tests/oauth_integration_test.py @@ -1,3 +1,4 @@ +from tests.conftest import configure_domain from checkout_sdk.checkout_sdk import CheckoutSdk from checkout_sdk.customers.customers import CustomerRequest from checkout_sdk.environment import Environment @@ -18,15 +19,14 @@ def test_should_create_customer_with_oauth(oauth_api): def test_should_fail_init_authorization_invalid_credentials(): try: - CheckoutSdk \ + builder = CheckoutSdk \ .builder() \ .oauth() \ .client_credentials(client_id='fake_id', client_secret='fake_secret') \ .environment(Environment.sandbox()) \ - .use_legacy_domain() \ - .scopes([OAuthScopes.GATEWAY, OAuthScopes.VAULT]) \ - .build() + .scopes([OAuthScopes.GATEWAY, OAuthScopes.VAULT]) + configure_domain(builder).build() except CheckoutException as err: assert err.args[0] == 'OAuth client_credentials authentication failed with error: (invalid_client)' From 783d8b808b6e417a085a1920c94df16166106faf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Rodr=C3=ADguez?= <127134616+armando-rodriguez-cko@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:27:42 +0200 Subject: [PATCH 3/5] test: add a switch to run the suite against the merchant subdomain The suite could only run against the shared hosts, so the subdomain path this PR makes mandatory had no integration coverage. Reviewers flagged that on every SDK, and it is the right thing to flag. The domain helper now has two modes. Default is unchanged, the shared hosts, because the sandbox OAuth clients are not provisioned for the subdomain and the token request returns invalid_client. Set CHECKOUT_TEST_USE_SUBDOMAIN=true and the suite runs against CHECKOUT_MERCHANT_SUBDOMAIN instead, so once sandbox is provisioned like production it is a one-line change in the workflows, already wired and documented, rather than a rewrite of every fixture. The switch is deliberately separate from CHECKOUT_MERCHANT_SUBDOMAIN, which CI already exports: provisioning should drive the behaviour, not the presence of a secret. --- .github/workflows/build-main.yml | 4 ++++ .github/workflows/build-pull-request.yml | 4 ++++ .github/workflows/build-release.yml | 4 ++++ README.md | 12 ++++++++++++ tests/conftest.py | 24 ++++++++++++++++++------ 5 files changed, 42 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-main.yml b/.github/workflows/build-main.yml index eb574151..c90c6d29 100644 --- a/.github/workflows/build-main.yml +++ b/.github/workflows/build-main.yml @@ -45,4 +45,8 @@ jobs: CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID }} CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET }} CHECKOUT_MERCHANT_SUBDOMAIN: ${{ secrets.IT_CHECKOUT_MERCHANT_SUBDOMAIN }} + # Flip to 'true' once the sandbox OAuth clients are provisioned for the + # merchant-specific subdomain, and the suite will run against it instead of + # the shared hosts. See TestDomainConfiguration. + CHECKOUT_TEST_USE_SUBDOMAIN: 'false' run: python -m pytest -x --import-mode=append tests/ diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml index f7cb943f..2694d779 100644 --- a/.github/workflows/build-pull-request.yml +++ b/.github/workflows/build-pull-request.yml @@ -46,4 +46,8 @@ jobs: CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID }} CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET }} CHECKOUT_MERCHANT_SUBDOMAIN: ${{ secrets.IT_CHECKOUT_MERCHANT_SUBDOMAIN }} + # Flip to 'true' once the sandbox OAuth clients are provisioned for the + # merchant-specific subdomain, and the suite will run against it instead of + # the shared hosts. See TestDomainConfiguration. + CHECKOUT_TEST_USE_SUBDOMAIN: 'false' run: python -m pytest --import-mode=append --runxfail tests/ diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 056bf630..d5601eb0 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -41,6 +41,10 @@ jobs: CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID }} CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET }} CHECKOUT_MERCHANT_SUBDOMAIN: ${{ secrets.IT_CHECKOUT_MERCHANT_SUBDOMAIN }} + # Flip to 'true' once the sandbox OAuth clients are provisioned for the + # merchant-specific subdomain, and the suite will run against it instead of + # the shared hosts. See TestDomainConfiguration. + CHECKOUT_TEST_USE_SUBDOMAIN: 'false' run: python -m pytest -x --import-mode=append tests/ - id: read-version run: echo "CURRENT_VERSION=$(grep -o '".*"' checkout_sdk/properties.py | sed 's/"//g' | tr -d \\n)" >> $GITHUB_ENV diff --git a/README.md b/README.md index d90de69d..f34e9ee1 100644 --- a/README.md +++ b/README.md @@ -287,6 +287,18 @@ checkout_api = CheckoutSdk.builder() \ This routes requests to `api.checkout.com` (or `api.sandbox.checkout.com`) and `access.checkout.com` (or `access.sandbox.checkout.com`). The method raises a `DeprecationWarning`, so `python -W error::DeprecationWarning` and most linters will flag it. Exactly one of `environment_subdomain(...)` or `use_legacy_domain()` must be set: the SDK raises a `CheckoutArgumentException` if both, or neither, are. The Previous (ABC) platform predates merchant-specific subdomains and is exempt from this requirement. +## Running the tests against your subdomain + +The test suite builds every client through `tests/conftest.py`, which has two modes. By default it uses the shared hosts, because the sandbox OAuth clients are not provisioned for merchant-specific subdomains and the token request would come back `invalid_client`. To run against a subdomain instead: + +```bash +export CHECKOUT_MERCHANT_SUBDOMAIN="your_subdomain" +export CHECKOUT_TEST_USE_SUBDOMAIN=true +python -m pytest tests +``` + +The switch is separate from `CHECKOUT_MERCHANT_SUBDOMAIN` on purpose: CI already exports that secret, so provisioning is what should flip the behaviour, not the presence of a value. Once sandbox is provisioned like production, set `CHECKOUT_TEST_USE_SUBDOMAIN: 'true'` in the workflows and CI exercises the subdomain path end to end. + ## Code of Conduct Please refer to [Code of Conduct](CODE_OF_CONDUCT.md) diff --git a/tests/conftest.py b/tests/conftest.py index 7ec582c7..9a3ce787 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,17 +19,29 @@ logging.getLogger('checkout').setLevel(logging.INFO) +def use_subdomain(): + return os.environ.get('CHECKOUT_TEST_USE_SUBDOMAIN', '').lower() == 'true' + + def configure_domain(builder): """ - Every client the suite builds has to choose a domain now that the merchant-specific - subdomain is mandatory, so they all come through here. + Every client the suite builds has to choose a domain now that the merchant-specific subdomain + is mandatory, so they all come through here. There are deliberately two modes. - The suite uses the shared hosts. It would be better to exercise the merchant-specific - subdomain, since that is the path merchants are being moved to, but the sandbox OAuth - clients are not provisioned for it: pointing the token request at + Default: the shared hosts. The sandbox OAuth clients are not provisioned for the + merchant-specific subdomain, so pointing the token request at {subdomain}.access.sandbox.checkout.com returns invalid_client for every integration test. - Until those clients are bound to the subdomain, CI has to use the legacy hosts. + + Opt-in: set CHECKOUT_TEST_USE_SUBDOMAIN=true and the suite runs against + CHECKOUT_MERCHANT_SUBDOMAIN instead, exercising end to end the path merchants are being moved + to. Once sandbox is provisioned like production, set that variable in the workflows and this + becomes the mode CI runs in. The switch is deliberately separate from + CHECKOUT_MERCHANT_SUBDOMAIN, which CI already exports, so provisioning drives the change rather + than the presence of a secret. """ + subdomain = os.environ.get('CHECKOUT_MERCHANT_SUBDOMAIN') + if use_subdomain() and subdomain and subdomain.strip(): + return builder.environment_subdomain(subdomain) with warnings.catch_warnings(): warnings.simplefilter('ignore', DeprecationWarning) return builder.use_legacy_domain() From 9e651d65aa95746598b04b103310904e94dcd699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Rodr=C3=ADguez?= <127134616+armando-rodriguez-cko@users.noreply.github.com> Date: Wed, 12 Aug 2026 11:19:54 +0200 Subject: [PATCH 4/5] revert: leave the version bump to the release Versions are bumped on master during the release, not in a feature branch, per the release workflow. This branch should carry only the change itself; the major bump is classified and applied when the release is cut. --- checkout_sdk/properties.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checkout_sdk/properties.py b/checkout_sdk/properties.py index 189c03bb..7ce5c1b8 100644 --- a/checkout_sdk/properties.py +++ b/checkout_sdk/properties.py @@ -1 +1 @@ -VERSION = "4.0.0" +VERSION = "3.12.0" From 193295e42978411fc0a73ff76a50381070f7a98f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Rodr=C3=ADguez?= <127134616+armando-rodriguez-cko@users.noreply.github.com> Date: Wed, 12 Aug 2026 11:52:06 +0200 Subject: [PATCH 5/5] revert: drop the test domain helpers and the workflow variable Two problems with the previous approach. It needed a new variable in 21 workflow files, which is not viable without access to create secrets. And it wrapped the builder chain in a configureDomain helper that is not part of the public API, so the tests stopped looking like the code a merchant would actually write. Every fixture now calls the real opt-out inline, in the chain, with a comment saying why: the sandbox OAuth clients are not provisioned for the merchant-specific subdomain, so the token request comes back invalid_client. When sandbox is provisioned, those calls become the subdomain setter. The unit tests covering all four combinations are untouched: they already used the public API directly. --- .github/workflows/build-main.yml | 4 -- .github/workflows/build-pull-request.yml | 4 -- .github/workflows/build-release.yml | 4 -- README.md | 12 ----- tests/accounts/accounts_integration_test.py | 8 +++- ...ounts_payout_schedules_integration_test.py | 8 +++- tests/conftest.py | 47 ++++++------------- tests/issuing/conftest.py | 8 +++- tests/oauth_integration_test.py | 8 +++- .../request_apm_payments_integration_test.py | 8 +++- 10 files changed, 44 insertions(+), 67 deletions(-) diff --git a/.github/workflows/build-main.yml b/.github/workflows/build-main.yml index c90c6d29..eb574151 100644 --- a/.github/workflows/build-main.yml +++ b/.github/workflows/build-main.yml @@ -45,8 +45,4 @@ jobs: CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID }} CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET }} CHECKOUT_MERCHANT_SUBDOMAIN: ${{ secrets.IT_CHECKOUT_MERCHANT_SUBDOMAIN }} - # Flip to 'true' once the sandbox OAuth clients are provisioned for the - # merchant-specific subdomain, and the suite will run against it instead of - # the shared hosts. See TestDomainConfiguration. - CHECKOUT_TEST_USE_SUBDOMAIN: 'false' run: python -m pytest -x --import-mode=append tests/ diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml index 2694d779..f7cb943f 100644 --- a/.github/workflows/build-pull-request.yml +++ b/.github/workflows/build-pull-request.yml @@ -46,8 +46,4 @@ jobs: CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID }} CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET }} CHECKOUT_MERCHANT_SUBDOMAIN: ${{ secrets.IT_CHECKOUT_MERCHANT_SUBDOMAIN }} - # Flip to 'true' once the sandbox OAuth clients are provisioned for the - # merchant-specific subdomain, and the suite will run against it instead of - # the shared hosts. See TestDomainConfiguration. - CHECKOUT_TEST_USE_SUBDOMAIN: 'false' run: python -m pytest --import-mode=append --runxfail tests/ diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index d5601eb0..056bf630 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -41,10 +41,6 @@ jobs: CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID }} CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET }} CHECKOUT_MERCHANT_SUBDOMAIN: ${{ secrets.IT_CHECKOUT_MERCHANT_SUBDOMAIN }} - # Flip to 'true' once the sandbox OAuth clients are provisioned for the - # merchant-specific subdomain, and the suite will run against it instead of - # the shared hosts. See TestDomainConfiguration. - CHECKOUT_TEST_USE_SUBDOMAIN: 'false' run: python -m pytest -x --import-mode=append tests/ - id: read-version run: echo "CURRENT_VERSION=$(grep -o '".*"' checkout_sdk/properties.py | sed 's/"//g' | tr -d \\n)" >> $GITHUB_ENV diff --git a/README.md b/README.md index f34e9ee1..d90de69d 100644 --- a/README.md +++ b/README.md @@ -287,18 +287,6 @@ checkout_api = CheckoutSdk.builder() \ This routes requests to `api.checkout.com` (or `api.sandbox.checkout.com`) and `access.checkout.com` (or `access.sandbox.checkout.com`). The method raises a `DeprecationWarning`, so `python -W error::DeprecationWarning` and most linters will flag it. Exactly one of `environment_subdomain(...)` or `use_legacy_domain()` must be set: the SDK raises a `CheckoutArgumentException` if both, or neither, are. The Previous (ABC) platform predates merchant-specific subdomains and is exempt from this requirement. -## Running the tests against your subdomain - -The test suite builds every client through `tests/conftest.py`, which has two modes. By default it uses the shared hosts, because the sandbox OAuth clients are not provisioned for merchant-specific subdomains and the token request would come back `invalid_client`. To run against a subdomain instead: - -```bash -export CHECKOUT_MERCHANT_SUBDOMAIN="your_subdomain" -export CHECKOUT_TEST_USE_SUBDOMAIN=true -python -m pytest tests -``` - -The switch is separate from `CHECKOUT_MERCHANT_SUBDOMAIN` on purpose: CI already exports that secret, so provisioning is what should flip the behaviour, not the presence of a value. Once sandbox is provisioned like production, set `CHECKOUT_TEST_USE_SUBDOMAIN: 'true'` in the workflows and CI exercises the subdomain path end to end. - ## Code of Conduct Please refer to [Code of Conduct](CODE_OF_CONDUCT.md) diff --git a/tests/accounts/accounts_integration_test.py b/tests/accounts/accounts_integration_test.py index a05c16ae..175a12ec 100644 --- a/tests/accounts/accounts_integration_test.py +++ b/tests/accounts/accounts_integration_test.py @@ -1,5 +1,6 @@ from __future__ import absolute_import +import warnings import os from datetime import datetime, timedelta, timezone @@ -16,7 +17,6 @@ from checkout_sdk.common.enums import Currency, Country, InstrumentType from checkout_sdk.files.files import FileRequest from checkout_sdk.oauth_scopes import OAuthScopes -from tests.conftest import configure_domain from tests.checkout_test_utils import assert_response, phone, address, new_uuid, get_project_root, random_email @@ -28,7 +28,11 @@ def accounts_checkout_api(): .client_credentials(client_id=os.environ.get('CHECKOUT_DEFAULT_OAUTH_ACCOUNTS_CLIENT_ID'), client_secret=os.environ.get('CHECKOUT_DEFAULT_OAUTH_ACCOUNTS_CLIENT_SECRET')) \ .scopes([OAuthScopes.ACCOUNTS, OAuthScopes.FILES]) - return configure_domain(builder).build() + # The sandbox OAuth clients are not provisioned for the merchant-specific subdomain, so the + # token request would come back invalid_client. Opting out explicitly until they are. + with warnings.catch_warnings(): + warnings.simplefilter('ignore', DeprecationWarning) + return builder.use_legacy_domain().build() def test_should_create_get_and_update_onboard_entity(accounts_checkout_api): diff --git a/tests/accounts/accounts_payout_schedules_integration_test.py b/tests/accounts/accounts_payout_schedules_integration_test.py index a4b36f5c..c511eb2e 100644 --- a/tests/accounts/accounts_payout_schedules_integration_test.py +++ b/tests/accounts/accounts_payout_schedules_integration_test.py @@ -1,5 +1,6 @@ from __future__ import absolute_import +import warnings import os import pytest @@ -9,7 +10,6 @@ from checkout_sdk.checkout_sdk import CheckoutSdk from checkout_sdk.common.enums import Currency from checkout_sdk.oauth_scopes import OAuthScopes -from tests.conftest import configure_domain from tests.checkout_test_utils import assert_response @@ -21,7 +21,11 @@ def payout_schedules_api(): .client_credentials(client_id=os.environ.get('CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_ID'), client_secret=os.environ.get('CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_SECRET')) \ .scopes([OAuthScopes.MARKETPLACE]) - return configure_domain(builder).build() + # The sandbox OAuth clients are not provisioned for the merchant-specific subdomain, so the + # token request would come back invalid_client. Opting out explicitly until they are. + with warnings.catch_warnings(): + warnings.simplefilter('ignore', DeprecationWarning) + return builder.use_legacy_domain().build() @pytest.mark.skip(reason='not available') diff --git a/tests/conftest.py b/tests/conftest.py index 9a3ce787..d098ca4a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,34 +19,6 @@ logging.getLogger('checkout').setLevel(logging.INFO) -def use_subdomain(): - return os.environ.get('CHECKOUT_TEST_USE_SUBDOMAIN', '').lower() == 'true' - - -def configure_domain(builder): - """ - Every client the suite builds has to choose a domain now that the merchant-specific subdomain - is mandatory, so they all come through here. There are deliberately two modes. - - Default: the shared hosts. The sandbox OAuth clients are not provisioned for the - merchant-specific subdomain, so pointing the token request at - {subdomain}.access.sandbox.checkout.com returns invalid_client for every integration test. - - Opt-in: set CHECKOUT_TEST_USE_SUBDOMAIN=true and the suite runs against - CHECKOUT_MERCHANT_SUBDOMAIN instead, exercising end to end the path merchants are being moved - to. Once sandbox is provisioned like production, set that variable in the workflows and this - becomes the mode CI runs in. The switch is deliberately separate from - CHECKOUT_MERCHANT_SUBDOMAIN, which CI already exports, so provisioning drives the change rather - than the presence of a secret. - """ - subdomain = os.environ.get('CHECKOUT_MERCHANT_SUBDOMAIN') - if use_subdomain() and subdomain and subdomain.strip(): - return builder.environment_subdomain(subdomain) - with warnings.catch_warnings(): - warnings.simplefilter('ignore', DeprecationWarning) - return builder.use_legacy_domain() - - @pytest.fixture(scope='session', autouse=True) def previous_api(): return CheckoutSdk \ @@ -59,10 +31,16 @@ def previous_api(): @pytest.fixture(scope='session', autouse=True) def default_api(): - return configure_domain(CheckoutSdk() - .builder() - .secret_key(os.environ.get('CHECKOUT_DEFAULT_SECRET_KEY')) - .public_key(os.environ.get('CHECKOUT_DEFAULT_PUBLIC_KEY'))).build() + # The sandbox OAuth clients are not provisioned for the merchant-specific subdomain, so the + # token request would come back invalid_client. Opting out explicitly until they are. + with warnings.catch_warnings(): + warnings.simplefilter('ignore', DeprecationWarning) + return CheckoutSdk() \ + .builder() \ + .secret_key(os.environ.get('CHECKOUT_DEFAULT_SECRET_KEY')) \ + .public_key(os.environ.get('CHECKOUT_DEFAULT_PUBLIC_KEY')) \ + .use_legacy_domain() \ + .build() @pytest.fixture(scope='session', autouse=True) @@ -79,7 +57,10 @@ def oauth_api(): OAuthScopes.VAULT_CARD_METADATA, OAuthScopes.FINANCIAL_ACTIONS, OAuthScopes.VAULT_REAL_TIME_ACCOUNT_UPDATER, OAuthScopes.PAYMENTS_SEARCH, OAuthScopes.GATEWAY_PAYMENT_CANCELLATIONS]) - return configure_domain(builder).build() + # See default_api above for why the legacy domain is used here. + with warnings.catch_warnings(): + warnings.simplefilter('ignore', DeprecationWarning) + return builder.use_legacy_domain().build() @pytest.fixture(scope='session', autouse=True) diff --git a/tests/issuing/conftest.py b/tests/issuing/conftest.py index 183a26a9..e8437caa 100644 --- a/tests/issuing/conftest.py +++ b/tests/issuing/conftest.py @@ -1,3 +1,4 @@ +import warnings import os import pytest @@ -11,7 +12,6 @@ AuthorizationType, CardAuthorizationRequest from checkout_sdk.oauth_scopes import OAuthScopes from tests.checkout_test_utils import phone, address, assert_response -from tests.conftest import configure_domain @pytest.fixture(scope='module', autouse=True) @@ -23,7 +23,11 @@ def issuing_checkout_api(): client_secret=os.environ.get('CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET')) \ .scopes([OAuthScopes.ISSUING_CLIENT, OAuthScopes.ISSUING_CARD_MGMT, OAuthScopes.ISSUING_CONTROLS_READ, OAuthScopes.ISSUING_CONTROLS_WRITE]) - return configure_domain(builder).build() + # The sandbox OAuth clients are not provisioned for the merchant-specific subdomain, so the + # token request would come back invalid_client. Opting out explicitly until they are. + with warnings.catch_warnings(): + warnings.simplefilter('ignore', DeprecationWarning) + return builder.use_legacy_domain().build() @pytest.fixture(scope='module') diff --git a/tests/oauth_integration_test.py b/tests/oauth_integration_test.py index 31ba2276..edc3dbb3 100644 --- a/tests/oauth_integration_test.py +++ b/tests/oauth_integration_test.py @@ -1,4 +1,4 @@ -from tests.conftest import configure_domain +import warnings from checkout_sdk.checkout_sdk import CheckoutSdk from checkout_sdk.customers.customers import CustomerRequest from checkout_sdk.environment import Environment @@ -26,7 +26,11 @@ def test_should_fail_init_authorization_invalid_credentials(): client_secret='fake_secret') \ .environment(Environment.sandbox()) \ .scopes([OAuthScopes.GATEWAY, OAuthScopes.VAULT]) - configure_domain(builder).build() + # The sandbox OAuth clients are not provisioned for the merchant-specific subdomain, so + # the token request would come back invalid_client. Opting out explicitly until they are. + with warnings.catch_warnings(): + warnings.simplefilter('ignore', DeprecationWarning) + builder.use_legacy_domain().build() except CheckoutException as err: assert err.args[0] == 'OAuth client_credentials authentication failed with error: (invalid_client)' diff --git a/tests/payments/request_apm_payments_integration_test.py b/tests/payments/request_apm_payments_integration_test.py index 0b1c1c1c..14458eba 100644 --- a/tests/payments/request_apm_payments_integration_test.py +++ b/tests/payments/request_apm_payments_integration_test.py @@ -1,5 +1,6 @@ from __future__ import absolute_import +import warnings import os import pytest @@ -18,7 +19,6 @@ from checkout_sdk.payments.payments import PaymentRequest, ProcessingSettings, FawryProduct, PaymentCustomerRequest, \ ShippingDetails, PaymentMethodDetails from checkout_sdk.payments.payments_apm_previous import RequestSofortSource -from tests.conftest import configure_domain from tests.checkout_test_utils import assert_response, SUCCESS_URL, FAILURE_URL, retriable, address, FIRST_NAME, \ LAST_NAME, phone, check_error_item, PAYEE_NOT_ONBOARDED, APM_SERVICE_UNAVAILABLE, random_email, new_uuid, \ account_holder, REFERENCE, DESCRIPTION, APM_CURRENCY_NOT_SUPPORTED @@ -150,7 +150,11 @@ def test_should_request_tamara_payment(): .oauth() \ .client_credentials(client_id=os.environ.get('CHECKOUT_PREVIEW_OAUTH_CLIENT_ID'), client_secret=os.environ.get('CHECKOUT_PREVIEW_OAUTH_CLIENT_SECRET')) - preview_api = configure_domain(preview_builder).build() + # The sandbox OAuth clients are not provisioned for the merchant-specific subdomain, so the + # token request would come back invalid_client. Opting out explicitly until they are. + with warnings.catch_warnings(): + warnings.simplefilter('ignore', DeprecationWarning) + preview_api = preview_builder.use_legacy_domain().build() payment_response = retriable(callback=preview_api.payments.request_payment, payment_request=payment_request)