walletpress/backend/tests/test_chain_vault.py
Rug Munch Media LLC 85d8ef5eac
refactor(routers): split chain_vault.py god router — WP-085..WP-087
Extracted admin endpoints from chain_vault.py (2,178 lines) into
wallet_admin.py (768 lines). chain_vault.py is now 1,469 lines.

What moved to wallet_admin.py (29 routes):
- API keys: /api-keys, /api-keys/revoke
- Alerts: /alerts, /alerts/delete
- Webhooks: /webhooks, /webhooks/{id}, /webhooks/{id}/test,
  /webhooks/{id}/retry, /webhooks/deliveries
- Audit trail: /audit-trail
- Bulk ops: /bulk/filter, /bulk/delete, /bulk/export, /export
- 2FA: /admin/2fa/{setup,verify-setup,disable,status}
- Config: /config
- Proof of Generation: /proof/{commit,provenance,verify,roots,stats}

What stayed in chain_vault.py (32 routes):
- Chain metadata: /chains, /stats, /healthz, /health-score
- RPC config: /rpc-chains
- Wallet gen: /generate, /generate/batch, /generate/all,
  /import, /from-mnemonic, /derive-address, /hd-wallet
- Vault CRUD: /vault, /vault/{id}, /vault/{id}/full, DELETE
- Wallet ops: /tree, /cluster, /rotate, /rotate-sweep, /rotations,
  /distribute, /sweep, /escrow, /escrow/release, /paper-wallet, /tx
- Validation: /validate/{chain}/{address}, /validate/all
- Balances: /balances, /balances/snapshot
- PDF: /paper-wallet/{id}/pdf, /wallet/{id}/birth-certificate

Helpers extracted to _persistent_store.py:
- _PersistentStore class (webhooks/alerts/payments SQLite store)
  P3-7 fix: removed dead _webhooks global references in test/retry
  endpoints — now uses _PersistentStore.all('webhooks')

Added to wallet_admin.py:
- _require_totp() helper (also kept in chain_vault.py for the
  /vault/{id}/full endpoint that needs it)
- WebhookCreateRequest, BulkFilterRequest, BulkDeleteRequest models
  (these were inlined in original chain_vault.py body — now in
  the request schemas section)

P3-10 — WP plugin supported_chains() rewritten
  Plugin used to advertise chains the backend doesn't support
  ('bitcoin' vs backend 'btc', 'ethereum' vs 'eth', etc.). Rewrote
  to use correct backend keys + added a 'backend' field for clarity.
  Now matches ADDRESS_GENERATION.md truth table.

main.py: updated import + include_router for wallet_admin.router.

Test results: 80 passed, 5 skipped (no regressions).

Refs: AUDIT.md P2-16, P3-7, P3-10, P3-17
2026-06-30 21:40:45 +07:00

372 lines
13 KiB
Python

"""Integration tests: chain-vault API endpoints."""
import pytest
def test_health(client):
resp = client.get("/health")
assert resp.status_code == 200
data = resp.json()
assert data["status"] == "ok"
assert data["service"] == "walletpress"
def test_healthz(client):
resp = client.get("/api/v1/chain-vault/healthz")
assert resp.status_code == 200
data = resp.json()
assert data["status"] == "ok"
assert "timestamp" in data
def test_root(client):
resp = client.get("/")
assert resp.status_code == 200
data = resp.json()
assert data["service"] == "WalletPress API"
def test_list_chains(client):
resp = client.get("/api/v1/chain-vault/chains")
assert resp.status_code == 200
data = resp.json()
chains = data if isinstance(data, dict) and "chains" in data else data
assert len(chains) > 0
def test_get_stats(client, admin_headers):
resp = client.get("/api/v1/chain-vault/stats", headers=admin_headers)
assert resp.status_code == 200
data = resp.json()
assert "vault" in data
assert "encryption_enabled" in data
@pytest.mark.parametrize("chain", ["eth", "sol", "btc", "trx", "base", "polygon", "bsc"])
def test_generate_wallet(client, admin_headers, chain):
resp = client.post("/api/v1/chain-vault/generate", json={
"chain": chain, "count": 1,
}, headers=admin_headers)
if resp.status_code in (400, 402):
pytest.skip(f"{chain}: {resp.json().get('detail', resp.json())}")
assert resp.status_code == 200, f"{chain}: {resp.json()}"
data = resp.json()
assert data["generated"] >= 1
assert data["wallets"][0]["address"]
def test_generate_batch(client, admin_headers):
resp = client.post("/api/v1/chain-vault/generate", json={
"chain": "eth", "count": 3,
}, headers=admin_headers)
assert resp.status_code == 200
data = resp.json()
assert data["generated"] == 3
assert len(data["wallets"]) == 3
def test_generate_all(client, admin_headers):
resp = client.post("/api/v1/chain-vault/generate", json={
"chain": "all", "count": 1,
}, headers=admin_headers)
if resp.status_code == 402:
pytest.skip("License tier blocks 'all' chains")
assert resp.status_code == 200
data = resp.json()
assert data["generated"] >= 1
def test_generate_with_qr(client, admin_headers):
resp = client.post("/api/v1/chain-vault/generate", json={
"chain": "eth", "count": 1, "include_qr": True,
}, headers=admin_headers)
assert resp.status_code == 200
def test_from_mnemonic(client, admin_headers):
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
resp = client.post("/api/v1/chain-vault/derive-address", json={
"mnemonic": mnemonic, "chain": "eth",
}, headers=admin_headers)
assert resp.status_code == 200
data = resp.json()
assert data["chain"] == "eth"
assert data["mnemonic"].startswith("abandon")
assert data["address"].startswith("0x")
def test_vault_list(client, admin_headers):
client.post("/api/v1/chain-vault/generate", json={
"chain": "eth", "count": 1,
}, headers=admin_headers)
resp = client.get("/api/v1/chain-vault/vault", headers=admin_headers)
assert resp.status_code == 200
data = resp.json()
assert "wallets" in data
assert data["total"] >= 1
def test_vault_get(client, admin_headers):
gen = client.post("/api/v1/chain-vault/generate", json={
"chain": "eth", "count": 1,
}, headers=admin_headers)
wallet_id = gen.json()["wallets"][0]["id"]
resp = client.get(f"/api/v1/chain-vault/vault/{wallet_id}", headers=admin_headers)
assert resp.status_code == 200
data = resp.json()
assert data["id"] == wallet_id
assert data["chain"] == "eth"
assert data["address"].startswith("0x")
def test_vault_get_full(client, admin_headers):
gen = client.post("/api/v1/chain-vault/generate", json={
"chain": "eth", "count": 1,
}, headers=admin_headers)
wallet_id = gen.json()["wallets"][0]["id"]
resp = client.get(f"/api/v1/chain-vault/vault/{wallet_id}/full", headers=admin_headers)
assert resp.status_code == 200
data = resp.json()
assert data["id"] == wallet_id
assert "private_key_hex" in data
def test_validate_address(client, admin_headers):
resp = client.get("/api/v1/chain-vault/validate/eth/0xd8da6bf26964af9d7eed9e03e53415d37aa96045", headers=admin_headers)
assert resp.status_code == 200
data = resp.json()
assert data["valid"] is True
assert data["chain"] == "eth"
assert data["address"] == "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"
def test_validate_all(client, admin_headers):
resp = client.get("/api/v1/chain-vault/validate/all?address=0xd8da6bf26964af9d7eed9e03e53415d37aa96045", headers=admin_headers)
assert resp.status_code == 200
data = resp.json()
assert "possible_chains" in data or "results" in data or isinstance(data.get("address"), str)
def test_wallet_tree(client, admin_headers):
client.post("/api/v1/chain-vault/generate", json={
"chain": "eth", "count": 1,
}, headers=admin_headers)
resp = client.get("/api/v1/chain-vault/tree", headers=admin_headers)
assert resp.status_code == 200
data = resp.json()
assert "tree" in data
assert "total_families" in data
assert data["total_wallets"] >= 1
def test_derive_address(client, admin_headers):
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
resp = client.post("/api/v1/chain-vault/derive-address", json={
"mnemonic": mnemonic, "chain": "eth",
}, headers=admin_headers)
assert resp.status_code == 200
data = resp.json()
assert data["chain"] == "eth"
assert data["address"].startswith("0x")
assert data["derivation_path"] != ""
def test_api_keys_crud(client, admin_headers):
create = client.post("/api/v1/chain-vault/api-keys", json={
"label": "test-key", "scopes": ["wallet.read"],
}, headers=admin_headers)
assert create.status_code == 200
key_data = create.json()
assert "api_key_id" in key_data
assert "api_key" in key_data
list_resp = client.get("/api/v1/chain-vault/api-keys", headers=admin_headers)
assert list_resp.status_code == 200
list_data = list_resp.json()
keys = list_data.get("api_keys", list_data.get("keys", []))
assert len(keys) >= 1
def test_team_keys_crud(client, admin_headers):
create = client.post("/api/v1/team/keys?label=test-team-key&role=operator", headers=admin_headers)
assert create.status_code == 200
key_data = create.json()
assert "key_id" in key_data
assert "api_key" in key_data
list_resp = client.get("/api/v1/team/keys", headers=admin_headers)
assert list_resp.status_code == 200
list_data = list_resp.json()
assert "keys" in list_data
def test_rotate_wallet(client, admin_headers):
gen = client.post("/api/v1/chain-vault/generate", json={
"chain": "eth", "count": 1,
}, headers=admin_headers)
wallet_id = gen.json()["wallets"][0]["id"]
resp = client.post("/api/v1/chain-vault/rotate", json={
"wallet_id": wallet_id, "reason": "test",
}, headers=admin_headers)
assert resp.status_code == 200
data = resp.json()
assert data["rotated"] is True
assert data["old_wallet_id"] == wallet_id
def test_temporal_wallet(client, admin_headers):
create = client.post("/api/v1/chain-vault/temporal/create", json={
"chain": "eth", "ttl_seconds": 3600,
}, headers=admin_headers)
assert create.status_code == 200
data = create.json()
assert "temporal_id" in data
assert data["chain"] == "eth"
def test_2fa_flow(client, admin_headers):
resp = client.get("/api/v1/chain-vault/admin/2fa/status", headers=admin_headers)
assert resp.status_code == 200
data = resp.json()
assert "enabled" in data
def test_proof_of_generation(client, admin_headers):
gen = client.post("/api/v1/chain-vault/generate", json={
"chain": "eth", "count": 1,
}, headers=admin_headers)
wallet_id = gen.json()["wallets"][0]["id"]
prov = client.get(f"/api/v1/chain-vault/proof/provenance/{wallet_id}", headers=admin_headers)
assert prov.status_code == 200
data = prov.json()
assert "attested" in data or "wallet_id" in data
def test_config_endpoint(client, admin_headers):
resp = client.get("/api/v1/chain-vault/config", headers=admin_headers)
assert resp.status_code == 200
data = resp.json()
assert "version" in data
assert "features" in data
def test_delete_wallet(client, admin_headers):
gen = client.post("/api/v1/chain-vault/generate", json={
"chain": "eth", "count": 1,
}, headers=admin_headers)
wallet_id = gen.json()["wallets"][0]["id"]
resp = client.delete(f"/api/v1/chain-vault/vault/{wallet_id}", headers=admin_headers)
assert resp.status_code == 200
data = resp.json()
assert data["deleted"] is True
assert data["wallet_id"] == wallet_id
def test_auth_required_for_mutations(client):
resp = client.post("/api/v1/chain-vault/generate", json={"chain": "eth", "count": 1})
assert resp.status_code == 401, f"Expected 401, got {resp.status_code}: {resp.text[:200]}"
def test_invalid_api_key_rejected(client):
bad_headers = {"X-API-Key": "this-key-is-wrong"}
resp = client.post("/api/v1/chain-vault/generate", json={
"chain": "eth", "count": 1,
}, headers=bad_headers)
assert resp.status_code in (401, 403)
def test_unsupported_chain_returns_error(client, admin_headers):
resp = client.post("/api/v1/chain-vault/generate", json={
"chain": "nonexistent", "count": 1,
}, headers=admin_headers)
# License check may return 402 before chain validation, or 400 if chain is invalid
assert resp.status_code in (400, 402)
def test_empty_label_generates_default(client, admin_headers):
resp = client.post("/api/v1/chain-vault/generate", json={
"chain": "eth", "count": 1,
}, headers=admin_headers)
assert resp.status_code == 200
data = resp.json()
assert data["wallets"][0]["label"] != ""
def test_generate_multiple_wallets(client, admin_headers):
resp = client.post("/api/v1/chain-vault/generate", json={
"chain": "eth", "count": 5,
}, headers=admin_headers)
assert resp.status_code == 200
data = resp.json()
assert data["generated"] == 5
assert len(data["wallets"]) == 5
# each wallet must have a unique address
addresses = [w["address"] for w in data["wallets"]]
assert len(set(addresses)) == 5
def test_retention_portfolio(client, admin_headers):
client.post("/api/v1/chain-vault/generate", json={
"chain": "eth", "count": 1,
}, headers=admin_headers)
resp = client.get("/api/v1/portfolio", headers=admin_headers)
assert resp.status_code == 200
data = resp.json()
assert "total_wallets" in data
def test_retention_groups(client, admin_headers):
resp = client.get("/api/v1/vault/groups", headers=admin_headers)
assert resp.status_code == 200
data = resp.json()
assert "groups" in data
def test_wallet_memory_stubs(client, admin_headers):
resp = client.get("/api/v1/wallet-memory/cluster", headers=admin_headers)
assert resp.status_code in (200, 405, 404)
def test_webhook_crud(client, admin_headers):
create = client.post("/api/v1/chain-vault/webhooks", json={
"url": "https://example.com/webhook",
"events": ["wallet.generated"],
}, headers=admin_headers)
assert create.status_code == 200
data = create.json()
assert "webhook_id" in data
list_resp = client.get("/api/v1/chain-vault/webhooks", headers=admin_headers)
assert list_resp.status_code == 200
list_data = list_resp.json()
webhooks = list_data.get("webhooks", [])
assert len(webhooks) >= 1
wh_id = data["webhook_id"]
delete = client.delete(f"/api/v1/chain-vault/webhooks/{wh_id}", headers=admin_headers)
assert delete.status_code == 200
assert delete.json()["deleted"] is True
def test_bulk_filter(client, admin_headers):
client.post("/api/v1/chain-vault/generate", json={
"chain": "eth", "count": 1,
}, headers=admin_headers)
resp = client.post("/api/v1/chain-vault/bulk/filter", json={
"filters": {"chain": "eth"},
}, headers=admin_headers)
assert resp.status_code == 200
data = resp.json()
assert "filtered" in data or "wallets" in data
def test_metrics(client, admin_headers):
resp = client.get("/metrics", headers=admin_headers)
assert resp.status_code == 200
def test_test_vectors(client, admin_headers):
resp = client.get("/api/v1/test-vectors", headers=admin_headers)
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, dict) or isinstance(data, list)