diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml new file mode 100644 index 0000000..b8f8ce5 --- /dev/null +++ b/.github/workflows/e2e_tests.yml @@ -0,0 +1,44 @@ +name: Run E2E Tests + +on: [pull_request] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: latest + virtualenvs-create: true + virtualenvs-in-project: true + + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v3 + with: + path: ./.venv + key: venv-${{ runner.os }}-${{ matrix.python }}-${{ hashFiles('poetry.lock') }} + + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --with dev + + - name: Run tests + env: + CDP_API_KEY_NAME: ${{ secrets.CDP_API_KEY_NAME }} + CDP_API_KEY_PRIVATE_KEY: ${{ secrets.CDP_API_KEY_PRIVATE_KEY }} + NETWORK_ID: ${{ secrets.NETWORK_ID}} + WALLET_DATA: ${{ secrets.WALLET_DATA }} + run: make e2e diff --git a/Makefile b/Makefile index c42dead..19af360 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ test: .PHONY: e2e e2e: - poetry run python -m tests.e2e + poetry run pytest -m "e2e" .PHONY: repl repl: diff --git a/poetry.lock b/poetry.lock index f763b41..8f9a8c4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -984,7 +984,6 @@ files = [ {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, @@ -995,7 +994,6 @@ files = [ {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, @@ -2483,6 +2481,20 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "python-dotenv" +version = "1.0.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + [[package]] name = "python-lsp-jsonrpc" version = "1.1.2" @@ -3569,4 +3581,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "d5b1697efb06bb79e0473cc479171cff56a76fac98140e2369476770ebe81529" +content-hash = "93bda329adce112ff0b1bcb35f83acae6ccd44a29cde239a39545973ba807ecc" diff --git a/pyproject.toml b/pyproject.toml index 6502b50..612d566 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ sphinx-autodoc-typehints = "^2.4.4" myst-parser = "^4.0.0" ruff-lsp = "^0.0.58" python-lsp-server = "^1.12.0" +python-dotenv = "^1.0.1" [build-system] requires = ["poetry-core"] @@ -65,10 +66,13 @@ known-first-party = ["cdp"] [tool.pytest.ini_options] minversion = "6.0" -addopts = "-ra -q --cov=cdp --cov-report=term-missing" +addopts = "-ra -q --cov=cdp --cov-report=term-missing -m 'not e2e'" testpaths = [ "tests", ] +markers = [ + "e2e: e2e tests, requiring env, deselect with '-m \"not e2e\"'", +] [tool.coverage.run] omit = ["cdp/client/*"] diff --git a/tests/test_e2e.py b/tests/test_e2e.py new file mode 100644 index 0000000..634e3b9 --- /dev/null +++ b/tests/test_e2e.py @@ -0,0 +1,193 @@ +import json +import os +import time +from decimal import Decimal + +import pytest +from dotenv import load_dotenv + +from cdp import Cdp +from cdp.wallet import Wallet +from cdp.wallet_data import WalletData + +load_dotenv() + + +@pytest.fixture(scope="module", autouse=True) +def configure_cdp(): + """Configure CDP once for all tests.""" + Cdp.configure( + api_key_name=os.environ["CDP_API_KEY_NAME"], + private_key=os.environ["CDP_API_KEY_PRIVATE_KEY"].replace("\\n", "\n"), + ) + + +@pytest.fixture(scope="module") +def wallet_data(): + """Load wallet data once for all tests.""" + wallet_data_str = os.environ.get("WALLET_DATA") + return json.loads(wallet_data_str) + + +@pytest.fixture(scope="module") +def imported_wallet(wallet_data): + """Create imported wallet once for all tests.""" + return Wallet.import_data(WalletData.from_dict(wallet_data)) + + +@pytest.mark.e2e +def test_wallet_data(wallet_data): + """Test wallet data format and required values.""" + expected = { + "wallet_id": wallet_data["wallet_id"], + "network_id": wallet_data["network_id"], + "seed": wallet_data["seed"], + "default_address_id": wallet_data["default_address_id"] + } + + for key, value in expected.items(): + assert key in wallet_data + assert value is not None + + +@pytest.mark.e2e +def test_wallet_import(wallet_data): + """Test wallet import functionality.""" + wallet_id = wallet_data["wallet_id"] + network_id = wallet_data["network_id"] + default_address_id = wallet_data["default_address_id"] + + imported_wallet = Wallet.import_data(WalletData.from_dict(wallet_data)) + + assert imported_wallet is not None + assert imported_wallet.id == wallet_id + assert imported_wallet.network_id == network_id + assert imported_wallet.default_address is not None + assert imported_wallet.default_address.address_id == default_address_id + + +@pytest.mark.e2e +def test_wallet_faucet(imported_wallet): + """Test wallet faucet with ETH.""" + initial_balances = imported_wallet.balances() + initial_eth_balance = Decimal(str(initial_balances.get("eth", 0))) + + imported_wallet.faucet().wait() + time.sleep(1) + + final_balances = imported_wallet.balances() + final_eth_balance = Decimal(str(final_balances.get("eth", 0))) + assert final_eth_balance > initial_eth_balance + + +@pytest.mark.e2e +def test_wallet_faucet_usdc(imported_wallet): + """Test wallet faucet with USDC.""" + initial_balances = imported_wallet.balances() + initial_usdc_balance = Decimal(str(initial_balances.get("usdc", 0))) + + imported_wallet.faucet(asset_id="usdc").wait() + time.sleep(1) + + final_balances = imported_wallet.balances() + final_usdc_balance = Decimal(str(final_balances.get("usdc", 0))) + assert final_usdc_balance > initial_usdc_balance + + +@pytest.mark.e2e +def test_wallet_transfer(imported_wallet): + """Test wallet transfer.""" + destination_wallet = Wallet.create() + + initial_source_balance = Decimal(str(imported_wallet.balances().get("eth", 0))) + initial_dest_balance = Decimal(str(destination_wallet.balances().get("eth", 0))) + + transfer = imported_wallet.transfer( + amount=Decimal("0.000000001"), + asset_id="eth", + destination=destination_wallet + ) + + transfer.wait() + + assert transfer is not None + assert transfer.status.value == "complete" + + final_source_balance = Decimal(str(imported_wallet.balances().get("eth", 0))) + final_dest_balance = Decimal(str(destination_wallet.balances().get("eth", 0))) + + assert final_source_balance < initial_source_balance + assert final_dest_balance > initial_dest_balance + + +@pytest.mark.e2e +def test_transaction_history(imported_wallet): + """Test transaction history retrieval.""" + # create a transaction + destination_wallet = Wallet.create() + transfer = imported_wallet.transfer( + amount=Decimal("0.000000001"), + asset_id="eth", + destination=destination_wallet + ).wait() + + time.sleep(10) + + transactions = imported_wallet.default_address.transactions() + matching_tx = None + + for tx in transactions: + if tx.transaction_hash == transfer.transaction_hash: + matching_tx = tx + break + + assert matching_tx is not None + assert matching_tx.status.value == "complete" + + +@pytest.mark.e2e +def test_wallet_export(imported_wallet): + """Test wallet export.""" + exported_wallet = imported_wallet.export_data() + assert exported_wallet.wallet_id is not None + assert exported_wallet.seed is not None + assert len(exported_wallet.seed) == 128 + + imported_wallet.save_seed_to_file("test_seed.json") + assert os.path.exists("test_seed.json") + + with open("test_seed.json") as f: + saved_seed = json.loads(f.read()) + + assert saved_seed[exported_wallet.wallet_id] == { + "seed": exported_wallet.seed, + "encrypted": False, + "auth_tag": "", + "iv": "", + "network_id": exported_wallet.network_id + } + + os.unlink("test_seed.json") + + +@pytest.mark.e2e +def test_wallet_addresses(imported_wallet): + """Test wallet addresses retrieval.""" + addresses = imported_wallet.addresses + assert addresses + assert imported_wallet.default_address in addresses + + +@pytest.mark.e2e +def test_wallet_balances(imported_wallet): + """Test wallet balances retrieval.""" + balances = imported_wallet.balances() + assert balances.get("eth") > 0 + + +@pytest.mark.e2e +def test_historical_balances(imported_wallet): + """Test historical balance retrieval.""" + balances = list(imported_wallet.default_address.historical_balances("eth")) + assert balances + assert all(balance.amount > 0 for balance in balances)