Parcourir la source

Add Python wallet test suite with CI integration (#1362)

* Add Python wallet test suite with CI integration

Adds comprehensive Python test suite for CDK FFI wallet database operations
with full CI/CD integration.

Test Infrastructure:
- Add 6 wallet operation tests covering core functionality:
  - Wallet creation with SQLite backend
  - Mint management (add/query/retrieve)
  - Keyset management (add/query by ID or mint)
  - Keyset counter operations (increment/read)
  - Quote operations (mint/melt queries)
  - Proof retrieval by Y values
- Automatic path resolution and library loading from target/ directories
- Isolated test execution with temporary SQLite databases
- Proper cleanup with try/finally blocks

Build Integration:
- Add `just ffi-test` command for convenient test execution
- Automatically generates Python bindings before running tests
- Clear output with test progress and results summary

CI Integration:
- Add `ffi-tests` job to GitHub Actions workflow
- Runs after pre-commit-checks with 30-minute timeout
- Ubuntu runner with Python 3.11
- Nix development environment with caching
- Rust cache for faster builds
C il y a 1 mois
Parent
commit
6e4ec9116f
5 fichiers modifiés avec 442 ajouts et 52 suppressions
  1. 41 52
      .github/workflows/ci.yml
  2. 112 0
      crates/cdk-ffi/tests/README.md
  3. 280 0
      crates/cdk-ffi/tests/test_transactions.py
  4. 1 0
      flake.nix
  5. 8 0
      justfile

+ 41 - 52
.github/workflows/ci.yml

@@ -6,7 +6,7 @@ on:
   pull_request:
     branches:
       - main
-      - 'v[0-9]*.[0-9]*.x'  # Match version branches like v0.13.x, v1.0.x, etc.
+      - "v[0-9]*.[0-9]*.x" # Match version branches like v0.13.x, v1.0.x, etc.
   release:
     types: [created]
 
@@ -48,14 +48,7 @@ jobs:
     strategy:
       fail-fast: true
       matrix:
-        build-args:
-          [
-            mint-token,
-            melt-token,
-            p2pk,
-            proof-selection,
-            wallet
-          ]
+        build-args: [mint-token, melt-token, p2pk, proof-selection, wallet]
     steps:
       - name: checkout
         uses: actions/checkout@v4
@@ -84,8 +77,7 @@ jobs:
     strategy:
       fail-fast: true
       matrix:
-        build-args:
-          [
+        build-args: [
             # Core crate testing
             -p cashu,
             -p cashu --no-default-features,
@@ -105,18 +97,18 @@ jobs:
             -p cdk-sql-common,
             -p cdk-sql-common --no-default-features --features wallet,
             -p cdk-sql-common --no-default-features --features mint,
-            
+
             # Database and infrastructure crates
             -p cdk-redb,
             -p cdk-sqlite,
             -p cdk-sqlite --features sqlcipher,
-            
+
             # HTTP/API layer - consolidated
             -p cdk-axum,
             -p cdk-axum --no-default-features,
             -p cdk-axum --no-default-features --features redis,
             -p cdk-axum --no-default-features --features "redis swagger",
-            
+
             # Lightning backends
             -p cdk-cln,
             -p cdk-lnd,
@@ -124,12 +116,12 @@ jobs:
             -p cdk-fake-wallet,
             -p cdk-payment-processor,
             -p cdk-ldk-node,
-            
+
             -p cdk-signatory,
             -p cdk-mint-rpc,
 
             -p cdk-prometheus,
-            
+
             # FFI bindings
             -p cdk-ffi,
             -p cdk-ffi --no-default-features,
@@ -187,15 +179,8 @@ jobs:
     strategy:
       fail-fast: true
       matrix:
-        build-args:
-          [
-            -p cdk-integration-tests,
-          ]
-        database:
-          [
-            SQLITE,
-            POSTGRES
-          ]
+        build-args: [-p cdk-integration-tests]
+        database: [SQLITE, POSTGRES]
     steps:
       - name: checkout
         uses: actions/checkout@v4
@@ -234,14 +219,8 @@ jobs:
     strategy:
       fail-fast: true
       matrix:
-        build-args:
-          [
-            -p cdk-integration-tests,
-          ]
-        database:
-          [
-          SQLITE,
-          ]
+        build-args: [-p cdk-integration-tests]
+        database: [SQLITE]
     steps:
       - name: checkout
         uses: actions/checkout@v4
@@ -282,12 +261,7 @@ jobs:
     strategy:
       fail-fast: true
       matrix:
-        database:
-          [
-          memory,
-          sqlite,
-          redb
-          ]
+        database: [memory, sqlite, redb]
     steps:
       - name: checkout
         uses: actions/checkout@v4
@@ -322,7 +296,6 @@ jobs:
       - name: Test mint
         run: nix develop -i -L .#stable --command just test
 
-
   payment-processor-itests:
     name: "Payment processor tests"
     runs-on: ubuntu-latest
@@ -331,12 +304,7 @@ jobs:
     strategy:
       fail-fast: true
       matrix:
-        ln:
-          [
-          FAKEWALLET,
-          CLN,
-          LND
-          ]
+        ln: [FAKEWALLET, CLN, LND]
     steps:
       - name: checkout
         uses: actions/checkout@v4
@@ -375,8 +343,7 @@ jobs:
     strategy:
       fail-fast: true
       matrix:
-        build-args:
-          [
+        build-args: [
             # Core library - all features EXCEPT swagger (which breaks MSRV)
             '-p cdk --features "mint,wallet,auth,nostr,bip353,tor,prometheus"',
 
@@ -456,10 +423,7 @@ jobs:
     strategy:
       fail-fast: true
       matrix:
-        database:
-          [
-          SQLITE,
-          ]
+        database: [SQLITE]
     steps:
       - name: checkout
         uses: actions/checkout@v4
@@ -537,3 +501,28 @@ jobs:
         run: nix develop -i -L .#stable --command cargo test --doc
       - name: Check docs with strict warnings
         run: nix develop -i -L .#stable --command just docs-strict
+
+  ffi-tests:
+    name: "FFI Python tests"
+    runs-on: ubuntu-latest
+    timeout-minutes: 30
+    needs: pre-commit-checks
+    steps:
+      - name: checkout
+        uses: actions/checkout@v4
+      - name: Get flake hash
+        id: flake-hash
+        run: echo "hash=$(sha256sum flake.lock | cut -d' ' -f1 | cut -c1-8)" >> $GITHUB_OUTPUT
+      - name: Install Nix
+        uses: DeterminateSystems/nix-installer-action@v17
+      - name: Nix Cache
+        uses: DeterminateSystems/magic-nix-cache-action@main
+        with:
+          diagnostic-endpoint: ""
+          use-flakehub: false
+      - name: Rust Cache
+        uses: Swatinem/rust-cache@v2
+        with:
+          shared-key: "stable-${{ steps.flake-hash.outputs.hash }}"
+      - name: Run FFI tests
+        run: nix develop -i -L .#integration --command just ffi-test

+ 112 - 0
crates/cdk-ffi/tests/README.md

@@ -0,0 +1,112 @@
+# CDK FFI Python Tests
+
+This directory contains Python tests for the CDK FFI (Foreign Function Interface) bindings, focusing on wallet database operations.
+
+## Running the Tests
+
+### Quick Start
+
+The easiest way to run all tests:
+
+```bash
+# From the repository root
+just ffi-test
+```
+
+This command will automatically:
+1. Build the FFI bindings (if needed)
+2. Run all Python tests
+3. Report results
+
+Or run directly (assumes bindings are already built):
+
+```bash
+# From the repository root
+python3 crates/cdk-ffi/tests/test_transactions.py
+```
+
+### Prerequisites
+
+**Python 3.7+** is required. The `just ffi-test` command handles everything else automatically.
+
+## How It Works
+
+The test script automatically:
+1. Locates the bindings in `target/bindings/python/`
+2. Copies the shared library from `target/release/` to the bindings directory
+3. Runs all wallet tests
+
+**No manual file copying required!**
+
+## Test Suite
+
+### Wallet Tests (test_transactions.py)
+
+Comprehensive tests for wallet database operations:
+
+1. **Wallet Creation** - Tests creating a wallet with SQLite backend
+2. **Wallet Mint Management** - Tests adding and querying mints
+3. **Wallet Keyset Management** - Tests adding and querying keysets
+4. **Wallet Keyset Counter** - Tests keyset counter increment operations
+5. **Wallet Quote Operations** - Tests querying mint and melt quotes
+6. **Wallet Get Proofs by Y Values** - Tests retrieving proofs by Y values
+
+### Key Features Tested
+
+- ✅ **Wallet creation** - SQLite backend initialization
+- ✅ **Mint management** - Add, query, and retrieve mint URLs
+- ✅ **Keyset operations** - Add keysets and query by ID or mint
+- ✅ **Counter operations** - Keyset counter increment/read
+- ✅ **Quote queries** - Retrieve mint and melt quotes
+- ✅ **Proof retrieval** - Get proofs by Y values
+- ✅ **Foreign key constraints** - Proper referential integrity
+
+## Test Output
+
+Expected output for successful run:
+
+```
+Starting CDK FFI Wallet Tests
+==================================================
+... (test execution) ...
+==================================================
+Test Results: 6 passed, 0 failed
+==================================================
+```
+
+## Troubleshooting
+
+### Import Errors
+
+If you see `ModuleNotFoundError: No module named 'cdk_ffi'`:
+- Ensure FFI bindings are generated: `just ffi-generate python`
+- Check that `target/bindings/python/cdk_ffi.py` exists
+
+### Library Not Found
+
+If you see errors about missing `.dylib` or `.so` files:
+- Build the release version: `cargo build --release -p cdk-ffi`
+- Check that the library exists in `target/release/`
+
+### Test Failures
+
+If tests fail:
+- Ensure you're running from the repository root
+- Check that the FFI bindings match the current code version
+- Try rebuilding: `just ffi-generate python && cargo build --release -p cdk-ffi`
+
+## Development
+
+When adding new tests:
+
+1. Add test function with `async def test_*()` signature
+2. Add test to the `tests` list in `main()`
+3. Use temporary databases for isolation
+4. Follow existing patterns for setup/teardown
+
+## Implementation Notes
+
+- All tests use temporary SQLite databases
+- Each test is fully isolated with its own database
+- Tests clean up automatically via `finally` blocks
+- The script handles path resolution and library loading automatically

+ 280 - 0
crates/cdk-ffi/tests/test_transactions.py

@@ -0,0 +1,280 @@
+#!/usr/bin/env python3
+"""
+Test suite for CDK FFI wallet operations
+"""
+
+import asyncio
+import os
+import sys
+import tempfile
+from pathlib import Path
+
+# Setup paths before importing cdk_ffi
+repo_root = Path(__file__).parent.parent.parent.parent
+bindings_path = repo_root / "target" / "bindings" / "python"
+lib_path = repo_root / "target" / "release"
+
+# Copy the library to the bindings directory so Python can find it
+import shutil
+lib_file = "libcdk_ffi.dylib" if sys.platform == "darwin" else "libcdk_ffi.so"
+src_lib = lib_path / lib_file
+dst_lib = bindings_path / lib_file
+
+if src_lib.exists() and not dst_lib.exists():
+    shutil.copy2(src_lib, dst_lib)
+
+# Add target/bindings/python to path to load cdk_ffi module
+sys.path.insert(0, str(bindings_path))
+
+import cdk_ffi
+
+
+async def test_wallet_creation():
+    """Test creating a wallet with SQLite backend"""
+    print("\n=== Test: Wallet Creation ===")
+
+    with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
+        db_path = tmp.name
+
+    try:
+        backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
+        db = cdk_ffi.create_wallet_db(backend)
+        print("✓ Wallet database created")
+
+        # Verify database is accessible by querying quotes
+        mint_quotes = await db.get_mint_quotes()
+        assert isinstance(mint_quotes, list), "get_mint_quotes should return a list"
+        print("✓ Wallet database accessible")
+
+        print("✓ Test passed: Wallet creation works")
+
+    finally:
+        if os.path.exists(db_path):
+            os.unlink(db_path)
+
+
+async def test_wallet_mint_management():
+    """Test adding and querying mints"""
+    print("\n=== Test: Wallet Mint Management ===")
+
+    with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
+        db_path = tmp.name
+
+    try:
+        backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
+        db = cdk_ffi.create_wallet_db(backend)
+
+        mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com")
+
+        # Add mint
+        await db.add_mint(mint_url, None)
+        print("✓ Added mint to wallet")
+
+        # Get specific mint (verifies it was added)
+        await db.get_mint(mint_url)
+        print("✓ Retrieved mint from database")
+
+        # Remove mint
+        await db.remove_mint(mint_url)
+        print("✓ Removed mint from wallet")
+
+        # Verify removal
+        mint_info_after = await db.get_mint(mint_url)
+        assert mint_info_after is None, "Mint should be removed"
+        print("✓ Verified mint removal")
+
+        print("✓ Test passed: Mint management works")
+
+    finally:
+        if os.path.exists(db_path):
+            os.unlink(db_path)
+
+
+async def test_wallet_keyset_management():
+    """Test adding and querying keysets"""
+    print("\n=== Test: Wallet Keyset Management ===")
+
+    with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
+        db_path = tmp.name
+
+    try:
+        backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
+        db = cdk_ffi.create_wallet_db(backend)
+
+        mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com")
+        keyset_id = cdk_ffi.Id(hex="004146bdf4a9afab")
+
+        # Add mint first (foreign key requirement)
+        await db.add_mint(mint_url, None)
+        print("✓ Added mint")
+
+        # Add keyset
+        keyset_info = cdk_ffi.KeySetInfo(
+            id=keyset_id.hex,
+            unit=cdk_ffi.CurrencyUnit.SAT(),
+            active=True,
+            input_fee_ppk=0
+        )
+        await db.add_mint_keysets(mint_url, [keyset_info])
+        print("✓ Added keyset")
+
+        # Query keyset by ID
+        keyset = await db.get_keyset_by_id(keyset_id)
+        assert keyset is not None, "Keyset should exist"
+        assert keyset.id == keyset_id.hex, "Keyset ID should match"
+        print(f"✓ Retrieved keyset: {keyset.id}")
+
+        # Query keysets for mint
+        keysets = await db.get_mint_keysets(mint_url)
+        assert keysets is not None and len(keysets) > 0, "Should have keysets for mint"
+        print(f"✓ Retrieved {len(keysets)} keyset(s) for mint")
+
+        print("✓ Test passed: Keyset management works")
+
+    finally:
+        if os.path.exists(db_path):
+            os.unlink(db_path)
+
+
+async def test_wallet_keyset_counter():
+    """Test keyset counter operations"""
+    print("\n=== Test: Wallet Keyset Counter ===")
+
+    with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
+        db_path = tmp.name
+
+    try:
+        backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
+        db = cdk_ffi.create_wallet_db(backend)
+
+        mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com")
+        keyset_id = cdk_ffi.Id(hex="004146bdf4a9afab")
+
+        # Setup mint and keyset
+        await db.add_mint(mint_url, None)
+        keyset_info = cdk_ffi.KeySetInfo(
+            id=keyset_id.hex,
+            unit=cdk_ffi.CurrencyUnit.SAT(),
+            active=True,
+            input_fee_ppk=0
+        )
+        await db.add_mint_keysets(mint_url, [keyset_info])
+        print("✓ Setup complete")
+
+        # Increment counter
+        counter1 = await db.increment_keyset_counter(keyset_id, 1)
+        print(f"✓ Counter after +1: {counter1}")
+        assert counter1 == 1, f"Expected counter 1, got {counter1}"
+
+        # Increment again
+        counter2 = await db.increment_keyset_counter(keyset_id, 5)
+        print(f"✓ Counter after +5: {counter2}")
+        assert counter2 == 6, f"Expected counter 6, got {counter2}"
+
+        # Read current value (increment by 0)
+        counter3 = await db.increment_keyset_counter(keyset_id, 0)
+        print(f"✓ Current counter: {counter3}")
+        assert counter3 == 6, f"Expected counter 6, got {counter3}"
+
+        print("✓ Test passed: Keyset counter works")
+
+    finally:
+        if os.path.exists(db_path):
+            os.unlink(db_path)
+
+
+async def test_wallet_quotes():
+    """Test mint and melt quote operations"""
+    print("\n=== Test: Wallet Quote Operations ===")
+
+    with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
+        db_path = tmp.name
+
+    try:
+        backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
+        db = cdk_ffi.create_wallet_db(backend)
+
+        mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com")
+
+        # Add mint first
+        await db.add_mint(mint_url, None)
+        print("✓ Added mint")
+
+        # Query mint quotes (should be empty initially)
+        mint_quotes = await db.get_mint_quotes()
+        assert isinstance(mint_quotes, list), "get_mint_quotes should return a list"
+        print(f"✓ Retrieved {len(mint_quotes)} mint quote(s)")
+
+        # Query melt quotes (should be empty initially)
+        melt_quotes = await db.get_melt_quotes()
+        assert isinstance(melt_quotes, list), "get_melt_quotes should return a list"
+        print(f"✓ Retrieved {len(melt_quotes)} melt quote(s)")
+
+        print("✓ Test passed: Quote operations work")
+
+    finally:
+        if os.path.exists(db_path):
+            os.unlink(db_path)
+
+
+async def test_wallet_proofs_by_ys():
+    """Test retrieving proofs by Y values"""
+    print("\n=== Test: Wallet Get Proofs by Y Values ===")
+
+    with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
+        db_path = tmp.name
+
+    try:
+        backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
+        db = cdk_ffi.create_wallet_db(backend)
+
+        # Test with empty list
+        proofs = await db.get_proofs_by_ys([])
+        assert len(proofs) == 0, f"Expected 0 proofs, got {len(proofs)}"
+        print("✓ get_proofs_by_ys returns empty for empty input")
+
+        print("✓ Test passed: get_proofs_by_ys works")
+
+    finally:
+        if os.path.exists(db_path):
+            os.unlink(db_path)
+
+
+async def main():
+    """Run all tests"""
+    print("Starting CDK FFI Wallet Tests")
+    print("=" * 50)
+
+    tests = [
+        ("Wallet Creation", test_wallet_creation),
+        ("Wallet Mint Management", test_wallet_mint_management),
+        ("Wallet Keyset Management", test_wallet_keyset_management),
+        ("Wallet Keyset Counter", test_wallet_keyset_counter),
+        ("Wallet Quote Operations", test_wallet_quotes),
+        ("Wallet Get Proofs by Y Values", test_wallet_proofs_by_ys),
+    ]
+
+    passed = 0
+    failed = 0
+
+    for test_name, test_func in tests:
+        try:
+            await test_func()
+            passed += 1
+        except Exception as e:
+            failed += 1
+            print(f"\n✗ Test failed: {test_name}")
+            print(f"Error: {e}")
+            import traceback
+            traceback.print_exc()
+
+    print("\n" + "=" * 50)
+    print(f"Test Results: {passed} passed, {failed} failed")
+    print("=" * 50)
+
+    return 0 if failed == 0 else 1
+
+
+if __name__ == "__main__":
+    exit_code = asyncio.run(main())
+    sys.exit(exit_code)

+ 1 - 0
flake.nix

@@ -236,6 +236,7 @@
                 buildInputs = buildInputs ++ [
                   stable_toolchain
                   pkgs.docker-client
+                  pkgs.python311
                 ];
                 inherit nativeBuildInputs;
               }

+ 8 - 0
justfile

@@ -577,6 +577,14 @@ ffi-generate-all *ARGS="--release": ffi-build
   just ffi-generate kotlin {{ARGS}}
   @echo "✅ All bindings generated successfully!"
 
+# Run Python FFI tests
+ffi-test: ffi-generate-python
+  #!/usr/bin/env bash
+  set -euo pipefail
+  echo "🧪 Running Python FFI tests..."
+  python3 crates/cdk-ffi/tests/test_transactions.py
+  echo "✅ Tests completed!"
+
 # Build debug version and generate Python bindings quickly (for development)
 ffi-dev-python:
   #!/usr/bin/env bash