From 6a154dc6935bd2aec48f536ba197adbb20cbbc01 Mon Sep 17 00:00:00 2001 From: Rohan Agarwal Date: Wed, 26 Feb 2025 15:36:20 -0700 Subject: [PATCH 1/7] WIP --- cdp/cdp.py | 15 +++++++++++---- cdp/errors.py | 13 +++++++++++++ tests/conftest.py | 15 +++++++++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/cdp/cdp.py b/cdp/cdp.py index f7a62d9..68b5354 100644 --- a/cdp/cdp.py +++ b/cdp/cdp.py @@ -5,7 +5,7 @@ from cdp.api_clients import ApiClients from cdp.cdp_api_client import CdpApiClient from cdp.constants import SDK_DEFAULT_SOURCE -from cdp.errors import InvalidConfigurationError +from cdp.errors import InvalidConfigurationError, UninitializedSDKError class Cdp: @@ -18,7 +18,7 @@ class Cdp: debugging (bool): Whether debugging is enabled. base_path (str): The base URL for the Platform API. max_network_retries (int): The maximum number of network retries. - api_clients (Optional[ApiClients]): The Platform API clients instance. + api_clients: The Platform API clients instance. """ @@ -30,7 +30,13 @@ class Cdp: debugging = False base_path = "https://api.cdp.coinbase.com/platform" max_network_retries = 3 - api_clients: ApiClients | None = None + + class ApiClientsWrapper: + """Wrapper that raises a helpful error when SDK is not initialized.""" + def __getattr__(self, _name): + raise UninitializedSDKError() + + api_clients = ApiClientsWrapper() def __new__(cls): """Create or return the singleton instance of the Cdp class. @@ -88,6 +94,7 @@ def configure( source, source_version, ) + # Replace the wrapper with the real api_clients instance cls.api_clients = ApiClients(cdp_client) @classmethod @@ -133,4 +140,4 @@ def configure_from_json( max_network_retries, source, source_version, - ) + ) \ No newline at end of file diff --git a/cdp/errors.py b/cdp/errors.py index 03670f0..b31f970 100644 --- a/cdp/errors.py +++ b/cdp/errors.py @@ -4,6 +4,19 @@ from cdp.client.exceptions import ApiException +class UninitializedSDKError(Exception): + """Exception raised when trying to access CDP API clients before SDK initialization.""" + + def __init__(self): + message = ( + "Coinbase SDK has not been initialized. Please initialize by calling either:\n\n" + + "- Cdp.configure(api_key_name='...', private_key='...')\n" + "- Cdp.configure_from_json(file_path='/path/to/api_keys.json')\n\n" + "If needed, register for API keys at https://portal.cdp.coinbase.com/ or view the docs at https://docs.cdp.coinbase.com/wallet-api/docs/welcome" + ) + super().__init__(message) + + class ApiError(Exception): """A wrapper for API exceptions to provide more context.""" diff --git a/tests/conftest.py b/tests/conftest.py index 627976e..321bb15 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,20 @@ import os +from unittest.mock import MagicMock +from cdp import Cdp +import pytest + +from cdp.api_clients import ApiClients + +@pytest.fixture(autouse=True) +def initialize_cdp(): + """Initialize the CDP SDK with mock API clients before each test.""" + mock_api_clients = MagicMock(spec=ApiClients) + Cdp.api_clients = mock_api_clients + + + + factory_modules = [ f[:-3] for f in os.listdir("./tests/factories") if f.endswith(".py") and f != "__init__.py" ] From c01515334686e425ad5ce303ac9f3d84498baf00 Mon Sep 17 00:00:00 2001 From: Rohan Agarwal Date: Wed, 26 Feb 2025 15:38:00 -0700 Subject: [PATCH 2/7] Format, lint --- cdp/cdp.py | 7 ++++--- cdp/errors.py | 8 ++++---- tests/conftest.py | 7 +++---- tests/factories/api_key_factory.py | 4 +++- tests/test_api_key_utils.py | 3 +++ tests/test_cdp.py | 17 +++++++++++++++++ 6 files changed, 34 insertions(+), 12 deletions(-) create mode 100644 tests/test_cdp.py diff --git a/cdp/cdp.py b/cdp/cdp.py index 68b5354..a46a660 100644 --- a/cdp/cdp.py +++ b/cdp/cdp.py @@ -30,12 +30,13 @@ class Cdp: debugging = False base_path = "https://api.cdp.coinbase.com/platform" max_network_retries = 3 - + class ApiClientsWrapper: """Wrapper that raises a helpful error when SDK is not initialized.""" + def __getattr__(self, _name): raise UninitializedSDKError() - + api_clients = ApiClientsWrapper() def __new__(cls): @@ -140,4 +141,4 @@ def configure_from_json( max_network_retries, source, source_version, - ) \ No newline at end of file + ) diff --git a/cdp/errors.py b/cdp/errors.py index b31f970..870644c 100644 --- a/cdp/errors.py +++ b/cdp/errors.py @@ -6,16 +6,16 @@ class UninitializedSDKError(Exception): """Exception raised when trying to access CDP API clients before SDK initialization.""" - + def __init__(self): message = ( - "Coinbase SDK has not been initialized. Please initialize by calling either:\n\n" + - "- Cdp.configure(api_key_name='...', private_key='...')\n" + "Coinbase SDK has not been initialized. Please initialize by calling either:\n\n" + + "- Cdp.configure(api_key_name='...', private_key='...')\n" "- Cdp.configure_from_json(file_path='/path/to/api_keys.json')\n\n" "If needed, register for API keys at https://portal.cdp.coinbase.com/ or view the docs at https://docs.cdp.coinbase.com/wallet-api/docs/welcome" ) super().__init__(message) - + class ApiError(Exception): """A wrapper for API exceptions to provide more context.""" diff --git a/tests/conftest.py b/tests/conftest.py index 321bb15..7ecc75f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,20 +1,19 @@ import os from unittest.mock import MagicMock -from cdp import Cdp import pytest +from cdp import Cdp from cdp.api_clients import ApiClients + @pytest.fixture(autouse=True) def initialize_cdp(): """Initialize the CDP SDK with mock API clients before each test.""" mock_api_clients = MagicMock(spec=ApiClients) Cdp.api_clients = mock_api_clients - - - + factory_modules = [ f[:-3] for f in os.listdir("./tests/factories") if f.endswith(".py") and f != "__init__.py" ] diff --git a/tests/factories/api_key_factory.py b/tests/factories/api_key_factory.py index 1d54679..af80376 100644 --- a/tests/factories/api_key_factory.py +++ b/tests/factories/api_key_factory.py @@ -12,6 +12,7 @@ def dummy_key_factory(): - "ed25519-32": Returns a base64-encoded 32-byte Ed25519 private key. - "ed25519-64": Returns a base64-encoded 64-byte dummy Ed25519 key (the first 32 bytes will be used). """ + def _create_dummy(key_type: str = "ecdsa") -> str: if key_type == "ecdsa": return ( @@ -25,9 +26,10 @@ def _create_dummy(key_type: str = "ecdsa") -> str: return "BXyKC+eFINc/6ztE/3neSaPGgeiU9aDRpaDnAbaA/vyTrUNgtuh/1oX6Vp+OEObV3SLWF+OkF2EQNPtpl0pbfA==" elif key_type == "ed25519-64": # Create a 64-byte dummy by concatenating a 32-byte sequence with itself. - dummy_32 = b'\x01' * 32 + dummy_32 = b"\x01" * 32 dummy_64 = dummy_32 + dummy_32 return base64.b64encode(dummy_64).decode("utf-8") else: raise ValueError("Unsupported key type for dummy key creation") + return _create_dummy diff --git a/tests/test_api_key_utils.py b/tests/test_api_key_utils.py index 30f347a..21b178a 100644 --- a/tests/test_api_key_utils.py +++ b/tests/test_api_key_utils.py @@ -10,18 +10,21 @@ def test_parse_private_key_pem_ec(dummy_key_factory): parsed_key = _parse_private_key(dummy_key) assert isinstance(parsed_key, ec.EllipticCurvePrivateKey) + def test_parse_private_key_ed25519_32(dummy_key_factory): """Test that a base64-encoded 32-byte Ed25519 key is parsed correctly using a dummy key from the factory.""" dummy_key = dummy_key_factory("ed25519-32") parsed_key = _parse_private_key(dummy_key) assert isinstance(parsed_key, ed25519.Ed25519PrivateKey) + def test_parse_private_key_ed25519_64(dummy_key_factory): """Test that a base64-encoded 64-byte input is parsed correctly by taking the first 32 bytes using a dummy key from the factory.""" dummy_key = dummy_key_factory("ed25519-64") parsed_key = _parse_private_key(dummy_key) assert isinstance(parsed_key, ed25519.Ed25519PrivateKey) + def test_parse_private_key_invalid(): """Test that an invalid key string raises a ValueError.""" with pytest.raises(ValueError, match="Could not parse the private key"): diff --git a/tests/test_cdp.py b/tests/test_cdp.py new file mode 100644 index 0000000..50b3b9e --- /dev/null +++ b/tests/test_cdp.py @@ -0,0 +1,17 @@ + +import pytest + +from cdp import Cdp +from cdp.errors import UninitializedSDKError + + +def test_uninitialized_error(): + """Test that direct access to API clients raises UninitializedSDKError.""" + Cdp.api_clients = Cdp.ApiClientsWrapper() + + with pytest.raises(UninitializedSDKError) as excinfo: + Cdp.api_clients.wallets + + assert "Coinbase SDK has not been initialized" in str(excinfo.value) + assert "Cdp.configure(api_key_name=" in str(excinfo.value) + assert "Cdp.configure_from_json(file_path=" in str(excinfo.value) From 3730c43198f0bb454990b114ea6789c92661a08e Mon Sep 17 00:00:00 2001 From: Rohan Agarwal Date: Wed, 26 Feb 2025 15:44:40 -0700 Subject: [PATCH 3/7] Fix --- cdp/cdp.py | 1 - tests/conftest.py | 3 +++ tests/test_cdp.py | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cdp/cdp.py b/cdp/cdp.py index a46a660..d80a332 100644 --- a/cdp/cdp.py +++ b/cdp/cdp.py @@ -95,7 +95,6 @@ def configure( source, source_version, ) - # Replace the wrapper with the real api_clients instance cls.api_clients = ApiClients(cdp_client) @classmethod diff --git a/tests/conftest.py b/tests/conftest.py index 7ecc75f..dbf95c3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,8 +10,11 @@ @pytest.fixture(autouse=True) def initialize_cdp(): """Initialize the CDP SDK with mock API clients before each test.""" + original_api_clients = Cdp.api_clients mock_api_clients = MagicMock(spec=ApiClients) Cdp.api_clients = mock_api_clients + yield + Cdp.api_clients = original_api_clients factory_modules = [ diff --git a/tests/test_cdp.py b/tests/test_cdp.py index 50b3b9e..c584213 100644 --- a/tests/test_cdp.py +++ b/tests/test_cdp.py @@ -1,4 +1,3 @@ - import pytest from cdp import Cdp From 93e0c663979f9b30c8e3702a7bf5d408cabdc97b Mon Sep 17 00:00:00 2001 From: Rohan Agarwal Date: Wed, 26 Feb 2025 15:51:45 -0700 Subject: [PATCH 4/7] Fix lint --- tests/test_cdp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cdp.py b/tests/test_cdp.py index c584213..a947ce4 100644 --- a/tests/test_cdp.py +++ b/tests/test_cdp.py @@ -9,7 +9,7 @@ def test_uninitialized_error(): Cdp.api_clients = Cdp.ApiClientsWrapper() with pytest.raises(UninitializedSDKError) as excinfo: - Cdp.api_clients.wallets + _ = Cdp.api_clients.wallets assert "Coinbase SDK has not been initialized" in str(excinfo.value) assert "Cdp.configure(api_key_name=" in str(excinfo.value) From 8f16da9b0826657c60bd6101f418b75289eec57a Mon Sep 17 00:00:00 2001 From: Rohan Agarwal Date: Wed, 26 Feb 2025 16:46:02 -0700 Subject: [PATCH 5/7] lint --- cdp/cdp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cdp/cdp.py b/cdp/cdp.py index d80a332..a18e7ab 100644 --- a/cdp/cdp.py +++ b/cdp/cdp.py @@ -35,6 +35,7 @@ class ApiClientsWrapper: """Wrapper that raises a helpful error when SDK is not initialized.""" def __getattr__(self, _name): + """Raise an error when accessing an attribute of the ApiClientsWrapper.""" raise UninitializedSDKError() api_clients = ApiClientsWrapper() From 4cfb50523cf619bd8ffe4a683dbbb5e903a1fc4b Mon Sep 17 00:00:00 2001 From: Rohan Agarwal Date: Wed, 26 Feb 2025 20:20:30 -0700 Subject: [PATCH 6/7] trying to fix e2e --- tests/conftest.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index dbf95c3..0190032 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,11 @@ @pytest.fixture(autouse=True) def initialize_cdp(): """Initialize the CDP SDK with mock API clients before each test.""" + # Skip this fixture for e2e tests + if request.node.get_closest_marker("e2e"): + yield + return + original_api_clients = Cdp.api_clients mock_api_clients = MagicMock(spec=ApiClients) Cdp.api_clients = mock_api_clients From 3bce6791bd7f51263977c724b7e7cfe28ce6266f Mon Sep 17 00:00:00 2001 From: Rohan Agarwal Date: Wed, 26 Feb 2025 20:27:41 -0700 Subject: [PATCH 7/7] Fix --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0190032..dab2a73 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,7 @@ @pytest.fixture(autouse=True) -def initialize_cdp(): +def initialize_cdp(request): """Initialize the CDP SDK with mock API clients before each test.""" # Skip this fixture for e2e tests if request.node.get_closest_marker("e2e"):