diff --git a/qiskit_ibm_runtime/accounts/account.py b/qiskit_ibm_runtime/accounts/account.py index 3f81ba9628..8f5e0bc65f 100644 --- a/qiskit_ibm_runtime/accounts/account.py +++ b/qiskit_ibm_runtime/accounts/account.py @@ -16,9 +16,10 @@ from typing import Optional from urllib.parse import urlparse -from typing_extensions import Literal from requests.auth import AuthBase - +from typing_extensions import Literal +from ..proxies import ProxyConfiguration +from ..utils.hgp import from_instance_format from ..api.auth import LegacyAuth, CloudAuth AccountType = Optional[Literal["cloud", "legacy"]] @@ -27,40 +28,6 @@ CLOUD_API_URL = "https://us-east.quantum-computing.cloud.ibm.com" -def _assert_valid_auth(auth: AccountType) -> None: - """Assert that the auth parameter is valid.""" - if not (auth in ["cloud", "legacy"]): - raise ValueError( - f"Inappropriate `auth` value. Expected one of ['cloud', 'legacy'], got '{auth}'." - ) - - -def _assert_valid_token(token: str) -> None: - """Assert that the token is valid.""" - if not (isinstance(token, str) and len(token) > 0): - raise ValueError( - f"Inappropriate `token` value. Expected a non-empty string, got '{token}'." - ) - - -def _assert_valid_url(url: str) -> None: - """Assert that the URL is valid.""" - try: - urlparse(url) - except: - raise ValueError(f"Inappropriate `url` value. Failed to parse '{url}' as URL.") - - -def _assert_valid_instance(auth: AccountType, instance: str) -> None: - """Assert that the instance name is valid for the given account type.""" - if auth == "cloud": - if not (isinstance(instance, str) and len(instance) > 0): - raise ValueError( - f"Inappropriate `instance` value. Expected a non-empty string." - ) - # TODO: add validation for legacy instance when adding test coverage - - class Account: """Class that represents an account.""" @@ -70,8 +37,7 @@ def __init__( token: str, url: Optional[str] = None, instance: Optional[str] = None, - # TODO: add validation for proxies input format - proxies: Optional[dict] = None, + proxies: Optional[ProxyConfiguration] = None, verify: Optional[bool] = True, ): """Account constructor. @@ -81,36 +47,35 @@ def __init__( token: Account token to use. url: Authentication URL. instance: Service instance to use. - proxies: Proxies to use. + proxies: Proxy configuration. verify: Whether to verify server's TLS certificate. """ - _assert_valid_auth(auth) - self.auth = auth + resolved_url = url or (LEGACY_API_URL if auth == "legacy" else CLOUD_API_URL) - _assert_valid_token(token) + self.auth = auth self.token = token - - resolved_url = url or (LEGACY_API_URL if auth == "legacy" else CLOUD_API_URL) - _assert_valid_url(resolved_url) self.url = resolved_url - self.instance = instance self.proxies = proxies self.verify = verify def to_saved_format(self) -> dict: """Returns a dictionary that represents how the account is saved on disk.""" - return {k: v for k, v in self.__dict__.items() if v is not None} + result = {k: v for k, v in self.__dict__.items() if v is not None} + if self.proxies: + result["proxies"] = self.proxies.to_dict() + return result @classmethod def from_saved_format(cls, data: dict) -> "Account": """Creates an account instance from data saved on disk.""" + proxies = data.get("proxies") return cls( auth=data.get("auth"), url=data.get("url"), token=data.get("token"), instance=data.get("instance"), - proxies=data.get("proxies"), + proxies=ProxyConfiguration(**proxies) if proxies else None, verify=data.get("verify", True), ) @@ -134,3 +99,64 @@ def __eq__(self, other: object) -> bool: self.verify == other.verify, ] ) + + def validate(self) -> "Account": + """Validates the account instance. + + Raises: + ValueError: if the account is invalid + """ + + self._assert_valid_auth(self.auth) + self._assert_valid_token(self.token) + self._assert_valid_url(self.url) + self._assert_valid_instance(self.auth, self.instance) + self._assert_valid_proxies(self.proxies) + return self + + @staticmethod + def _assert_valid_auth(auth: AccountType) -> None: + """Assert that the auth parameter is valid.""" + if not (auth in ["cloud", "legacy"]): + raise ValueError( + f"Invalid `auth` value. Expected one of ['cloud', 'legacy'], got '{auth}'." + ) + + @staticmethod + def _assert_valid_token(token: str) -> None: + """Assert that the token is valid.""" + if not (isinstance(token, str) and len(token) > 0): + raise ValueError( + f"Invalid `token` value. Expected a non-empty string, got '{token}'." + ) + + @staticmethod + def _assert_valid_url(url: str) -> None: + """Assert that the URL is valid.""" + try: + urlparse(url) + except: + raise ValueError(f"Invalid `url` value. Failed to parse '{url}' as URL.") + + @staticmethod + def _assert_valid_proxies(config: ProxyConfiguration) -> None: + """Assert that the proxy configuration is valid.""" + if config is not None: + config.validate() + + @staticmethod + def _assert_valid_instance(auth: AccountType, instance: str) -> None: + """Assert that the instance name is valid for the given account type.""" + if auth == "cloud": + if not (isinstance(instance, str) and len(instance) > 0): + raise ValueError( + f"Invalid `instance` value. Expected a non-empty string, got '{instance}'." + ) + if auth == "legacy": + if instance is not None: + try: + from_instance_format(instance) + except: + raise ValueError( + f"Invalid `instance` value. Expected hub/group/project format, got {instance}" + ) diff --git a/qiskit_ibm_runtime/accounts/management.py b/qiskit_ibm_runtime/accounts/management.py index d95d9778d3..ef149b9707 100644 --- a/qiskit_ibm_runtime/accounts/management.py +++ b/qiskit_ibm_runtime/accounts/management.py @@ -16,6 +16,7 @@ from typing import Optional, Dict from .account import Account, AccountType +from ..proxies import ProxyConfiguration from .storage import save_config, read_config, delete_config _DEFAULT_ACCOUNT_CONFIG_JSON_FILE = os.path.join( @@ -39,7 +40,7 @@ def save( instance: Optional[str] = None, auth: Optional[AccountType] = None, name: Optional[str] = _DEFAULT_ACCOUNT_NAME, - proxies: Optional[dict] = None, + proxies: Optional[ProxyConfiguration] = None, verify: Optional[bool] = None, ) -> None: """Save account on disk.""" @@ -55,7 +56,9 @@ def save( auth=auth, proxies=proxies, verify=verify, - ).to_saved_format(), + ) + # avoid storing invalid accounts + .validate().to_saved_format(), ) @staticmethod diff --git a/qiskit_ibm_runtime/api/client_parameters.py b/qiskit_ibm_runtime/api/client_parameters.py index 1ad34ab741..69544eb091 100644 --- a/qiskit_ibm_runtime/api/client_parameters.py +++ b/qiskit_ibm_runtime/api/client_parameters.py @@ -14,9 +14,8 @@ from typing import Dict, Optional, Any, Union -from requests_ntlm import HttpNtlmAuth - from ..api.auth import LegacyAuth, CloudAuth +from ..proxies import ProxyConfiguration TEMPLATE_IBM_HUBS = "{prefix}/Network/{hub}/Groups/{group}/Projects/{project}" """str: Template for creating an IBM Quantum URL with hub/group/project information.""" @@ -31,7 +30,7 @@ def __init__( token: str, url: str = None, instance: Optional[str] = None, - proxies: Optional[Dict] = None, + proxies: Optional[ProxyConfiguration] = None, verify: bool = True, ) -> None: """ClientParameters constructor. @@ -64,15 +63,9 @@ def connection_parameters(self) -> Dict[str, Any]: expected by ``requests``. The following keys can be present: ``proxies``, ``verify``, and ``auth``. """ - request_kwargs = {"verify": self.verify} + request_kwargs: Any = {"verify": self.verify} if self.proxies: - if "urls" in self.proxies: - request_kwargs["proxies"] = self.proxies["urls"] - - if "username_ntlm" in self.proxies and "password_ntlm" in self.proxies: - request_kwargs["auth"] = HttpNtlmAuth( - self.proxies["username_ntlm"], self.proxies["password_ntlm"] - ) + request_kwargs.update(self.proxies.to_request_params()) return request_kwargs diff --git a/qiskit_ibm_runtime/api/clients/base.py b/qiskit_ibm_runtime/api/clients/base.py index 2ada1850d6..010063cb25 100644 --- a/qiskit_ibm_runtime/api/clients/base.py +++ b/qiskit_ibm_runtime/api/clients/base.py @@ -26,7 +26,6 @@ from ..client_parameters import ClientParameters from ..exceptions import WebsocketError, WebsocketTimeoutError -from .utils import ws_proxy_params logger = logging.getLogger(__name__) @@ -68,8 +67,10 @@ def __init__( message_queue: Queue used to hold received messages. """ self._websocket_url = websocket_url.rstrip("/") - self._proxy_params = ws_proxy_params( - client_params=client_params, ws_url=self._websocket_url + self._proxy_params = ( + client_params.proxies.to_ws_params(self._websocket_url) + if client_params.proxies + else {} ) self._access_token = client_params.token self._job_id = job_id diff --git a/qiskit_ibm_runtime/api/clients/utils.py b/qiskit_ibm_runtime/api/clients/utils.py deleted file mode 100644 index b285c52ab7..0000000000 --- a/qiskit_ibm_runtime/api/clients/utils.py +++ /dev/null @@ -1,68 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Utilities for working with IBM Quantum API.""" - -from typing import Dict -from urllib.parse import urlparse - -from ..client_parameters import ClientParameters - - -def ws_proxy_params(client_params: ClientParameters, ws_url: str) -> Dict: - """Extract proxy information for websocket. - - Args: - client_params: Parameters used for server connection. - ws_url: Websocket URL. - - Returns: - Proxy information to be used by the websocket client. - """ - conn_data = client_params.connection_parameters() - out = {} - - if "proxies" in conn_data: - proxies = conn_data["proxies"] - url_parts = urlparse(ws_url) - proxy_keys = [ - ws_url, - "wss", - "https://" + url_parts.hostname, - "https", - "all://" + url_parts.hostname, - "all", - ] - for key in proxy_keys: - if key in proxies: - proxy_parts = urlparse(proxies[key], scheme="http") - out["http_proxy_host"] = proxy_parts.hostname - out["http_proxy_port"] = proxy_parts.port - out["proxy_type"] = ( - "http" - if proxy_parts.scheme.startswith("http") - else proxy_parts.scheme - ) - if proxy_parts.username and proxy_parts.password: - out["http_proxy_auth"] = ( - proxy_parts.username, - proxy_parts.password, - ) - break - - if "auth" in conn_data: - out["http_proxy_auth"] = ( - client_params.proxies["username_ntlm"], - client_params.proxies["password_ntlm"], - ) - - return out diff --git a/qiskit_ibm_runtime/ibm_runtime_service.py b/qiskit_ibm_runtime/ibm_runtime_service.py index 0300098a79..594c374c21 100644 --- a/qiskit_ibm_runtime/ibm_runtime_service.py +++ b/qiskit_ibm_runtime/ibm_runtime_service.py @@ -24,9 +24,10 @@ from qiskit.providers.exceptions import QiskitBackendNotFoundError from qiskit.providers.providerutils import filter_backends -from qiskit_ibm_runtime import ibm_backend # pylint: disable=unused-import +from qiskit_ibm_runtime import ibm_backend from .accounts import AccountManager, Account, AccountType from .accounts.exceptions import AccountsError +from .proxies import ProxyConfiguration from .api.clients import AuthClient, VersionClient from .api.clients.runtime import RuntimeClient from .api.exceptions import RequestsApiError @@ -143,7 +144,11 @@ def __init__( name: Name of the account to load. instance: The service instance to use. For cloud runtime, this is the Cloud Resource Name (CRN). For legacy runtime, this is the hub/group/project in that format. - proxies: Proxy configuration for the server. + proxies: Proxy configuration. Supported optional keys are + ``urls`` (a dictionary mapping protocol or protocol and host to the URL of the proxy, + documented at https://docs.python-requests.org/en/latest/api/#requests.Session.proxies), + ``username_ntlm``, ``password_ntlm`` (username and password to enable NTLM user + authentication) verify: Whether to verify the server's TLS certificate. Returns: @@ -160,13 +165,9 @@ def __init__( instance=instance, auth=auth, name=name, - proxies=proxies, + proxies=ProxyConfiguration(**proxies) if proxies else None, verify=verify, ) - if self._account.auth == "cloud" and not self._account.instance: - raise IBMInputValueError( - f"Cloud account must have a service instance (CRN)." - ) self._client_params = ClientParameters( auth_type=self._account.auth, @@ -211,7 +212,7 @@ def _discover_account( instance: Optional[str] = None, auth: Optional[AccountType] = None, name: Optional[str] = None, - proxies: Optional[dict] = None, + proxies: Optional[ProxyConfiguration] = None, verify: Optional[bool] = None, ) -> Account: """Discover account.""" @@ -235,7 +236,7 @@ def _discover_account( instance=instance, proxies=proxies, verify=verify_, - ) + ).validate() if url: logger.warning( "Loading default %s account. Input 'url' is ignored.", auth @@ -259,6 +260,9 @@ def _discover_account( if verify is not None: account.verify = verify + # ensure account is valid, fail early if not + account.validate() + return account def _discover_cloud_backends(self) -> Dict[str, "ibm_backend.IBMBackend"]: @@ -549,7 +553,11 @@ def save_account( instance: The CRN (cloud) or hub/group/project (legacy). auth: Authentication type. `cloud` or `legacy`. name: Name of the account to save. - proxies: Proxy configuration for the server. + proxies: Proxy configuration. Supported optional keys are + ``urls`` (a dictionary mapping protocol or protocol and host to the URL of the proxy, + documented at https://docs.python-requests.org/en/latest/api/#requests.Session.proxies), + ``username_ntlm``, ``password_ntlm`` (username and password to enable NTLM user + authentication) verify: Verify the server's TLS certificate. """ @@ -559,7 +567,7 @@ def save_account( instance=instance, auth=auth, name=name, - proxies=proxies, + proxies=ProxyConfiguration(**proxies) if proxies else None, verify=verify, ) diff --git a/qiskit_ibm_runtime/proxies/__init__.py b/qiskit_ibm_runtime/proxies/__init__.py new file mode 100644 index 0000000000..c0212bfb0d --- /dev/null +++ b/qiskit_ibm_runtime/proxies/__init__.py @@ -0,0 +1,17 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Proxy configuration. +""" + +from .configuration import ProxyConfiguration diff --git a/qiskit_ibm_runtime/proxies/configuration.py b/qiskit_ibm_runtime/proxies/configuration.py new file mode 100644 index 0000000000..2e0ea25eb2 --- /dev/null +++ b/qiskit_ibm_runtime/proxies/configuration.py @@ -0,0 +1,132 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Proxy related classes and functions.""" + + +from dataclasses import dataclass +from typing import Optional, Dict, Any +from urllib.parse import urlparse + +from requests_ntlm import HttpNtlmAuth + + +@dataclass +class ProxyConfiguration: + """Class for representing a proxy configuration. + + Args + urls: a dictionary mapping protocol or protocol and host to the URL of the proxy. Refer to + https://docs.python-requests.org/en/latest/api/#requests.Session.proxies for details. + username_ntlm: username used to enable NTLM user authentication. + password_ntlm: password used to enable NTLM user authentication. + """ + + urls: Optional[Dict[str, str]] = None + username_ntlm: Optional[str] = None + password_ntlm: Optional[str] = None + + def validate(self) -> None: + """Validate configuration. + + Raises: + ValueError: If configuration is invalid. + """ + if not any( + [ + isinstance(self.username_ntlm, str) + and isinstance(self.password_ntlm, str), + self.username_ntlm is None and self.password_ntlm is None, + ] + ): + raise ValueError( + f"Invalid proxy configuration for NTLM authentication. None or both of username and " + f"password must be provided. Got username_ntlm={self.username_ntlm}, " + f"password_ntlm={self.password_ntlm}." + ) + + if self.urls is not None and not isinstance(self.urls, dict): + raise ValueError( + f"Invalid proxy configuration. Expected `urls` to contain a dictionary mapping protocol " + f"or protocol and host to the URL of the proxy. Got {self.urls}" + ) + + def to_dict(self) -> dict: + """Transform configuration to dictionary.""" + + return {k: v for k, v in self.__dict__.items() if v is not None} + + def to_request_params(self) -> dict: + """Transform configuration to request parameters. + + Returns: + A dictionary with proxy configuration parameters in the format + expected by ``requests``. The following keys can be present: + ``proxies``and ``auth``. + """ + + request_kwargs = {} + if self.urls: + request_kwargs["proxies"] = self.urls + + if self.username_ntlm and self.password_ntlm: + request_kwargs["auth"] = HttpNtlmAuth( + self.username_ntlm, self.password_ntlm + ) + + return request_kwargs + + def to_ws_params(self, ws_url: str) -> dict: + """Extract proxy information for websocket. + + Args: + ws_url: Websocket URL. + + Returns: + A dictionary with proxy configuration parameters in the format expected by websocket. + The following keys can be present: ``http_proxy_host``and ``http_proxy_port``, + ``proxy_type``, ``http_proxy_auth``. + """ + out: Any = {} + + if self.urls: + proxies = self.urls + url_parts = urlparse(ws_url) + proxy_keys = [ + ws_url, + "wss", + "https://" + url_parts.hostname, + "https", + "all://" + url_parts.hostname, + "all", + ] + for key in proxy_keys: + if key in proxies: + proxy_parts = urlparse(proxies[key], scheme="http") + out["http_proxy_host"] = proxy_parts.hostname + out["http_proxy_port"] = proxy_parts.port + out["proxy_type"] = ( + "http" + if proxy_parts.scheme.startswith("http") + else proxy_parts.scheme + ) + if proxy_parts.username and proxy_parts.password: + out["http_proxy_auth"] = ( + proxy_parts.username, + proxy_parts.password, + ) + break + + if self.username_ntlm and self.password_ntlm: + out["http_proxy_auth"] = (self.username_ntlm, self.password_ntlm) + + return out diff --git a/test/mock/proxy_server.py b/test/mock/proxy_server.py index 3f95d5abd0..a50b8141b5 100644 --- a/test/mock/proxy_server.py +++ b/test/mock/proxy_server.py @@ -15,6 +15,8 @@ import subprocess from contextlib import contextmanager +from qiskit_ibm_runtime.proxies import ProxyConfiguration + class MockProxyServer: """Local proxy server for testing.""" @@ -55,7 +57,7 @@ def stop(self): def use_proxies(service, proxies): """Context manager to set and restore proxies setting.""" try: - service._client_params.proxies = {"urls": proxies} + service._client_params.proxies = ProxyConfiguration(urls=proxies) yield finally: service._client_params.proxies = None diff --git a/test/test_account.py b/test/test_account.py index 91affd44ba..cf36914587 100644 --- a/test/test_account.py +++ b/test/test_account.py @@ -13,15 +13,15 @@ """Tests for the account functions.""" import json -import uuid import logging import os +import uuid +from typing import Any from unittest import skipIf -from qiskit_ibm_runtime.accounts.account import CLOUD_API_URL, LEGACY_API_URL from qiskit_ibm_runtime.accounts import AccountManager, Account, management -from qiskit_ibm_runtime.exceptions import IBMInputValueError - +from qiskit_ibm_runtime.proxies import ProxyConfiguration +from qiskit_ibm_runtime.accounts.account import CLOUD_API_URL, LEGACY_API_URL from .ibm_test_case import IBMTestCase from .mock.fake_runtime_service import FakeRuntimeService from .utils.account import ( @@ -43,9 +43,95 @@ token="token-y", url="https://cloud.ibm.com", instance="crn:v1:bluemix:public:quantum-computing:us-east:a/...::", + proxies=ProxyConfiguration( + username_ntlm="bla", password_ntlm="blub", urls={"https": "127.0.0.1"} + ), ) +class TestAccount(IBMTestCase): + """Tests for Account class.""" + + dummy_token = "123" + dummy_cloud_url = "https://us-east.quantum-computing.cloud.ibm.com" + dummy_legacy_url = "https://auth.quantum-computing.ibm.com/api" + + def test_invalid_auth(self): + """Test invalid values for auth parameter.""" + + with self.assertRaises(ValueError) as err: + invalid_auth: Any = "phantom" + Account( + auth=invalid_auth, token=self.dummy_token, url=self.dummy_cloud_url + ).validate() + self.assertIn("Invalid `auth` value.", str(err.exception)) + + def test_invalid_token(self): + """Test invalid values for token parameter.""" + + invalid_tokens = [1, None, ""] + for token in invalid_tokens: + with self.subTest(token=token): + with self.assertRaises(ValueError) as err: + Account( + auth="cloud", token=token, url=self.dummy_cloud_url + ).validate() + self.assertIn("Invalid `token` value.", str(err.exception)) + + def test_invalid_url(self): + """Test invalid values for url parameter.""" + + subtests = [ + {"auth": "cloud", "url": 123}, + ] + for params in subtests: + with self.subTest(params=params): + with self.assertRaises(ValueError) as err: + Account(**params, token=self.dummy_token).validate() + self.assertIn("Invalid `url` value.", str(err.exception)) + + def test_invalid_instance(self): + """Test invalid values for instance parameter.""" + + subtests = [ + {"auth": "cloud", "instance": ""}, + {"auth": "cloud"}, + {"auth": "legacy", "instance": "no-hgp-format"}, + ] + for params in subtests: + with self.subTest(params=params): + with self.assertRaises(ValueError) as err: + Account( + **params, token=self.dummy_token, url=self.dummy_cloud_url + ).validate() + self.assertIn("Invalid `instance` value.", str(err.exception)) + + def test_invalid_proxy_config(self): + """Test invalid values for proxy configuration.""" + + subtests = [ + { + "proxies": ProxyConfiguration(**{"username_ntlm": "user-only"}), + }, + { + "proxies": ProxyConfiguration(**{"password_ntlm": "password-only"}), + }, + { + "proxies": ProxyConfiguration(**{"urls": ""}), + }, + ] + for params in subtests: + with self.subTest(params=params): + with self.assertRaises(ValueError) as err: + Account( + **params, + auth="legacy", + token=self.dummy_token, + url=self.dummy_cloud_url, + ).validate() + self.assertIn("Invalid proxy configuration", str(err.exception)) + + # NamedTemporaryFiles not supported in Windows @skipIf(os.name == "nt", "Test not supported in Windows") class TestAccountManager(IBMTestCase): @@ -122,10 +208,10 @@ def test_list(self): "key1": _TEST_CLOUD_ACCOUNT.to_saved_format(), "key2": _TEST_LEGACY_ACCOUNT.to_saved_format(), management._DEFAULT_ACCOUNT_NAME_CLOUD: Account( - "cloud", "token-legacy" + "cloud", "token-cloud", instance="crn:123" ).to_saved_format(), management._DEFAULT_ACCOUNT_NAME_LEGACY: Account( - "legacy", "token-cloud" + "legacy", "token-legacy" ).to_saved_format(), } ), self.subTest("filtered list of accounts"): @@ -176,6 +262,9 @@ def test_delete(self): self.assertTrue(len(AccountManager.list()) == 0) +MOCK_PROXY_CONFIG_DICT = { + "urls": {"https": "127.0.0.1", "username_ntlm": "", "password_ntlm": ""} +} # NamedTemporaryFiles not supported in Windows @skipIf(os.name == "nt", "Test not supported in Windows") class TestEnableAccount(IBMTestCase): @@ -243,7 +332,7 @@ def test_enable_cloud_account_by_auth_token_url(self): for url in urls: with self.subTest(url=url), no_envs(["QISKIT_IBM_API_TOKEN"]): token = uuid.uuid4().hex - with self.assertRaises(IBMInputValueError) as err: + with self.assertRaises(ValueError) as err: _ = FakeRuntimeService(auth="cloud", token=token, url=url) self.assertIn("instance", str(err.exception)) @@ -320,7 +409,7 @@ def test_enable_account_by_env_auth(self): envs = { "QISKIT_IBM_API_TOKEN": token, "QISKIT_IBM_API_URL": url, - "QISKIT_IBM_INSTANCE": "my_crn", + "QISKIT_IBM_INSTANCE": "h/g/p" if auth == "legacy" else "crn:123", } with custom_envs(envs): service = FakeRuntimeService(auth=auth) @@ -365,10 +454,10 @@ def test_enable_account_by_name_pref(self): """Test initializing account by name and preferences.""" name = "foo" subtests = [ - {"proxies": "foo"}, + {"proxies": MOCK_PROXY_CONFIG_DICT}, {"verify": False}, {"instance": "bar"}, - {"proxies": "foo", "verify": False, "instance": "bar"}, + {"proxies": MOCK_PROXY_CONFIG_DICT, "verify": False, "instance": "bar"}, ] for extra in subtests: with self.subTest(extra=extra): @@ -382,10 +471,10 @@ def test_enable_account_by_name_pref(self): def test_enable_account_by_auth_pref(self): """Test initializing account by auth and preferences.""" subtests = [ - {"proxies": "foo"}, + {"proxies": MOCK_PROXY_CONFIG_DICT}, {"verify": False}, - {"instance": "bar"}, - {"proxies": "foo", "verify": False, "instance": "bar"}, + {"instance": "h/g/p"}, + {"proxies": MOCK_PROXY_CONFIG_DICT, "verify": False, "instance": "h/g/p"}, ] for auth in ["cloud", "legacy"]: for extra in subtests: @@ -403,10 +492,10 @@ def test_enable_account_by_auth_pref(self): def test_enable_account_by_env_pref(self): """Test initializing account by environment variable and preferences.""" subtests = [ - {"proxies": "foo"}, + {"proxies": MOCK_PROXY_CONFIG_DICT}, {"verify": False}, {"instance": "bar"}, - {"proxies": "foo", "verify": False, "instance": "bar"}, + {"proxies": MOCK_PROXY_CONFIG_DICT, "verify": False, "instance": "bar"}, ] for extra in subtests: with self.subTest(extra=extra): @@ -427,7 +516,7 @@ def test_enable_account_by_name_input_instance(self): """Test initializing account by name and input instance.""" name = "foo" instance = uuid.uuid4().hex - with temporary_account_config_file(name=name, instance=""): + with temporary_account_config_file(name=name, instance="stored-instance"): service = FakeRuntimeService(name=name, instance=instance) self.assertTrue(service._account) self.assertEqual(service._account.instance, instance) @@ -435,7 +524,7 @@ def test_enable_account_by_name_input_instance(self): def test_enable_account_by_auth_input_instance(self): """Test initializing account by auth and input instance.""" instance = uuid.uuid4().hex - with temporary_account_config_file(auth="cloud", instance=""): + with temporary_account_config_file(auth="cloud", instance="bla"): service = FakeRuntimeService(auth="cloud", instance=instance) self.assertTrue(service._account) self.assertEqual(service._account.instance, instance) @@ -443,7 +532,11 @@ def test_enable_account_by_auth_input_instance(self): def test_enable_account_by_env_input_instance(self): """Test initializing account by env and input instance.""" instance = uuid.uuid4().hex - envs = {"QISKIT_IBM_API_TOKEN": "some_token", "QISKIT_IBM_API_URL": "some_url"} + envs = { + "QISKIT_IBM_API_TOKEN": "some_token", + "QISKIT_IBM_API_URL": "some_url", + "QISKIT_IBM_INSTANCE": "some_instance", + } with custom_envs(envs): service = FakeRuntimeService(auth="cloud", instance=instance) self.assertTrue(service._account) @@ -451,7 +544,7 @@ def test_enable_account_by_env_input_instance(self): def _verify_prefs(self, prefs, account): if "proxies" in prefs: - self.assertEqual(account.proxies, prefs["proxies"]) + self.assertEqual(account.proxies, ProxyConfiguration(**prefs["proxies"])) if "verify" in prefs: self.assertEqual(account.verify, prefs["verify"]) if "instance" in prefs: diff --git a/test/test_backend_retrieval.py b/test/test_backend_retrieval.py index 26e9282ce5..ba871bae46 100644 --- a/test/test_backend_retrieval.py +++ b/test/test_backend_retrieval.py @@ -157,7 +157,7 @@ def test_filter_by_hgp(self): legacy_service = FakeRuntimeService( auth="legacy", token="my_token", - instance="my_instance", + instance="h/g/p", test_options=test_options, ) backends = legacy_service.backends(instance="hub0/group0/project0") @@ -182,7 +182,7 @@ def _get_services(self, fake_backends): legacy_service = FakeRuntimeService( auth="legacy", token="my_token", - instance="my_instance", + instance="h/g/p", test_options=test_options, ) cloud_service = FakeRuntimeService( diff --git a/test/test_client_parameters.py b/test/test_client_parameters.py index 1b294e7a8e..3c83c67943 100644 --- a/test/test_client_parameters.py +++ b/test/test_client_parameters.py @@ -15,7 +15,7 @@ import uuid from requests_ntlm import HttpNtlmAuth - +from qiskit_ibm_runtime.proxies import ProxyConfiguration from qiskit_ibm_runtime.api.client_parameters import ClientParameters from qiskit_ibm_runtime.api.auth import CloudAuth, LegacyAuth @@ -43,7 +43,9 @@ def test_proxy_param(self) -> None: """Test using only proxy urls (no NTLM credentials).""" urls = {"http": "localhost:8080", "https": "localhost:8080"} proxies_only_expected_result = {"verify": True, "proxies": urls} - proxies_only_credentials = self._get_client_params(proxies={"urls": urls}) + proxies_only_credentials = self._get_client_params( + proxies=ProxyConfiguration(**{"urls": urls}) + ) result = proxies_only_credentials.connection_parameters() self.assertDictEqual(proxies_only_expected_result, result) @@ -61,7 +63,7 @@ def test_proxies_param_with_ntlm(self) -> None: "auth": HttpNtlmAuth("domain\\username", "password"), } proxies_with_ntlm_credentials = self._get_client_params( - proxies=proxies_with_ntlm_dict + proxies=ProxyConfiguration(**proxies_with_ntlm_dict) ) result = proxies_with_ntlm_credentials.connection_parameters() @@ -74,19 +76,6 @@ def test_proxies_param_with_ntlm(self) -> None: result.pop("auth") self.assertDictEqual(ntlm_expected_result, result) - def test_malformed_proxy_param(self) -> None: - """Test input with malformed nesting of the proxies dictionary.""" - urls = {"http": "localhost:8080", "https": "localhost:8080"} - malformed_nested_proxies_dict = {"proxies": urls} - malformed_nested_credentials = self._get_client_params( - proxies=malformed_nested_proxies_dict - ) - - # Malformed proxy entries should be ignored. - expected_result = {"verify": True} - result = malformed_nested_credentials.connection_parameters() - self.assertDictEqual(expected_result, result) - def test_malformed_ntlm_params(self) -> None: """Test input with malformed NTLM credentials.""" urls = {"http": "localhost:8080", "https": "localhost:8080"} diff --git a/test/test_proxies.py b/test/test_proxies.py index b98c2b615e..c2cc2dfa12 100644 --- a/test/test_proxies.py +++ b/test/test_proxies.py @@ -12,17 +12,17 @@ """Tests for the proxy support.""" -import urllib import subprocess +import urllib from requests.exceptions import ProxyError from qiskit_ibm_runtime import IBMRuntimeService -from qiskit_ibm_runtime.api.clients import AuthClient, VersionClient -from qiskit_ibm_runtime.api.exceptions import RequestsApiError from qiskit_ibm_runtime.api.client_parameters import ClientParameters +from qiskit_ibm_runtime.api.clients import AuthClient, VersionClient from qiskit_ibm_runtime.api.clients.runtime import RuntimeClient - +from qiskit_ibm_runtime.api.exceptions import RequestsApiError +from qiskit_ibm_runtime.proxies import ProxyConfiguration from .ibm_test_case import IBMTestCase from .utils.decorators import requires_qe_access, requires_cloud_service @@ -60,7 +60,7 @@ def test_proxies_cloud_runtime_client(self, service, instance): """Should reach the proxy using RuntimeClient.""" # pylint: disable=unused-argument params = service._client_params - params.proxies = {"urls": VALID_PROXIES} + params.proxies = ProxyConfiguration(urls=VALID_PROXIES) client = RuntimeClient(params) client.list_programs(limit=1) api_line = pproxy_desired_access_log_line(params.url) @@ -120,7 +120,7 @@ def test_proxies_authclient(self, qe_token, qe_url): auth_type="legacy", token=qe_token, url=qe_url, - proxies={"urls": VALID_PROXIES}, + proxies=ProxyConfiguration(urls=VALID_PROXIES), ) _ = AuthClient(params) @@ -153,7 +153,7 @@ def test_invalid_proxy_port_runtime_client(self, qe_token, qe_url): auth_type="legacy", token=qe_token, url=qe_url, - proxies={"urls": INVALID_PORT_PROXIES}, + proxies=ProxyConfiguration(urls=INVALID_PORT_PROXIES), ) with self.assertRaises(RequestsApiError) as context_manager: client = RuntimeClient(params) @@ -167,7 +167,7 @@ def test_invalid_proxy_port_authclient(self, qe_token, qe_url): auth_type="legacy", token=qe_token, url=qe_url, - proxies={"urls": INVALID_PORT_PROXIES}, + proxies=ProxyConfiguration(urls=INVALID_PORT_PROXIES), ) with self.assertRaises(RequestsApiError) as context_manager: _ = AuthClient(params) @@ -191,7 +191,7 @@ def test_invalid_proxy_address_runtime_client(self, qe_token, qe_url): auth_type="legacy", token=qe_token, url=qe_url, - proxies={"urls": INVALID_ADDRESS_PROXIES}, + proxies=ProxyConfiguration(urls=INVALID_ADDRESS_PROXIES), ) with self.assertRaises(RequestsApiError) as context_manager: client = RuntimeClient(params) @@ -206,7 +206,7 @@ def test_invalid_proxy_address_authclient(self, qe_token, qe_url): auth_type="legacy", token=qe_token, url=qe_url, - proxies={"urls": INVALID_ADDRESS_PROXIES}, + proxies=ProxyConfiguration(urls=INVALID_ADDRESS_PROXIES), ) with self.assertRaises(RequestsApiError) as context_manager: _ = AuthClient(params) @@ -237,7 +237,7 @@ def test_proxy_urls(self, qe_token, qe_url): auth_type="legacy", token=qe_token, url=qe_url, - proxies={"urls": {"https": proxy_url}}, + proxies=ProxyConfiguration(urls={"https": proxy_url}), ) version_finder = VersionClient( params.url, **params.connection_parameters() diff --git a/test/utils/decorators.py b/test/utils/decorators.py index 33e6cdfed5..7c65a2c170 100644 --- a/test/utils/decorators.py +++ b/test/utils/decorators.py @@ -272,10 +272,10 @@ def run_legacy_and_cloud_fake(func): @wraps(func) def _wrapper(self, *args, **kwargs): legacy_service = FakeRuntimeService( - auth="legacy", token="my_token", instance="my_instance" + auth="legacy", token="my_token", instance="h/g/p" ) cloud_service = FakeRuntimeService( - auth="cloud", token="my_token", instance="my_instance" + auth="cloud", token="my_token", instance="crn:123" ) for service in [legacy_service, cloud_service]: with self.subTest(service=service.auth):