rmi-backend/tests/unit/test_bridge_health.py
opencode c762564d40 style(rmi-backend): complete lint cleanup — 1175→0 ruff errors
- Fix 71 invalid-syntax files (class-body newline-broken assignments)
- Add from/None chain to 307 B904 raise-without-from sites
- Add B008 ignore to ruff.toml (already in pyproject.toml)
- Noqa F401 on __init__.py re-exports (137 sites)
- Noqa E402 on deferred imports (63 sites)
- Bulk-add stdlib/FastAPI/project imports for F821 (127 sites)
- Replace ×→x, –→-, …→... in docstrings (4093 chars)
- Manual refactor of 5 SIM103/SIM116 patterns

Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py)
Co-authored-by: opencode <opencode@rugmunch.io>
2026-07-06 15:43:20 +02:00

229 lines
9 KiB
Python

"""
Tests: Bridge Health Monitor
=============================
Tests the x402 bridge health endpoint and the core BridgeHealthMonitor module.
"""
from unittest.mock import AsyncMock
import pytest
# ── Core Module Tests ────────────────────────────────────────────
class TestBridgeRegistry:
"""Verify bridge registry has expected bridges and their configurations."""
def test_registry_has_all_bridges(self):
from app.bridge_health_monitor import BRIDGE_REGISTRY
expected = {
"layerzero",
"stargate",
"across",
"wormhole",
"hop",
"synapse",
"axelar",
"celer",
"debridge",
"chainlink_ccip",
"connext",
"orbiter",
}
assert set(BRIDGE_REGISTRY.keys()) == expected, f"Missing bridges: {expected - set(BRIDGE_REGISTRY.keys())}"
assert len(BRIDGE_REGISTRY) == 12, "Expected exactly 12 bridges"
def test_each_bridge_has_required_fields(self):
from app.bridge_health_monitor import BRIDGE_REGISTRY
required = {"name", "trust_model", "chains", "tvl_source", "audit_recency_days"}
for key, bridge in BRIDGE_REGISTRY.items():
for field in required:
assert field in bridge, f"Bridge {key} missing field: {field}"
assert isinstance(bridge["chains"], list), f"Bridge {key}: chains must be a list"
assert len(bridge["chains"]) >= 1, f"Bridge {key}: must have at least 1 chain"
class TestSecurityScore:
"""Test the security scoring logic."""
@pytest.mark.asyncio
async def test_compute_security_score_high_tvl(self):
from app.bridge_health_monitor import BRIDGE_REGISTRY, BridgeHealthMonitor
monitor = BridgeHealthMonitor()
monitor._fetch_current_tvl = AsyncMock(return_value=2_000_000_000) # $2B
score = await monitor._compute_security_score("layerzero", BRIDGE_REGISTRY["layerzero"])
assert score.overall_score >= 70, f"Score too low: {score.overall_score}"
assert score.tvl_depth_score >= 15, "TVL depth score too low for $2B TVL"
assert "High TVL" in " ".join(score.strengths), "Expected TVL strength"
@pytest.mark.asyncio
async def test_compute_security_score_low_tvl(self):
from app.bridge_health_monitor import BRIDGE_REGISTRY, BridgeHealthMonitor
monitor = BridgeHealthMonitor()
monitor._fetch_current_tvl = AsyncMock(return_value=100_000) # $100K
score = await monitor._compute_security_score("stargate", BRIDGE_REGISTRY["stargate"])
assert score.overall_score <= 70, f"Score too high for low TVL: {score.overall_score}"
assert score.tvl_depth_score <= 10, "TVL depth score should be low"
@pytest.mark.asyncio
async def test_wormhole_exploit_penalty(self):
"""Wormhole had a $326M exploit - should be reflected in scoring."""
from app.bridge_health_monitor import BRIDGE_REGISTRY, BridgeHealthMonitor
monitor = BridgeHealthMonitor()
monitor._fetch_current_tvl = AsyncMock(return_value=500_000_000)
score = await monitor._compute_security_score("wormhole", BRIDGE_REGISTRY["wormhole"])
assert score.exploit_history_score == 0, "Wormhole should have 0 exploit score ($326M loss)"
assert any("exploit" in v.lower() for v in score.vulnerabilities), "Expected exploit mention in vulnerabilities"
@pytest.mark.asyncio
async def test_chainlink_ccip_immutability_bonus(self):
"""CCIP is non-upgradeable - should get a bonus."""
from app.bridge_health_monitor import BRIDGE_REGISTRY, BridgeHealthMonitor
monitor = BridgeHealthMonitor()
monitor._fetch_current_tvl = AsyncMock(return_value=800_000_000)
score = await monitor._compute_security_score("chainlink_ccip", BRIDGE_REGISTRY["chainlink_ccip"])
assert score.upgrade_risk_score >= 15, "CCIP non-upgradeable should score high"
assert any("immutable" in s.lower() for s in score.strengths), "Expected immutability strength"
class TestExploitDetection:
"""Test exploit signal detection logic."""
@pytest.mark.asyncio
async def test_critical_tvl_drop_detection(self):
from app.bridge_health_monitor import (
BRIDGE_REGISTRY,
BridgeHealthMonitor,
BridgeTVLSnapshot,
)
monitor = BridgeHealthMonitor()
# Simulate a -50% TVL drop (exceeds critical threshold of -40%)
snapshot = BridgeTVLSnapshot(
bridge_key="stargate",
bridge_name="Stargate",
tvl_usd=50_000_000,
tvl_change_24h_pct=-50.0,
tvl_change_7d_pct=-55.0,
tvl_change_30d_pct=-60.0,
)
signals = await monitor._detect_exploit_signals("stargate", BRIDGE_REGISTRY["stargate"], snapshot)
assert len(signals) >= 1, "Expected exploit signal for critical TVL drop"
assert any(s.severity == "critical" for s in signals), "Expected critical severity"
# ── API Endpoint Tests ──────────────────────────────────────────
class TestBridgeHealthEndpoint:
"""Test the x402 bridge health API endpoint."""
@pytest.mark.asyncio
async def test_endpoint_registration(self):
"""Verify the router is properly configured."""
from app.routers.x402_bridge_health import router
assert router.prefix == "/api/v1/x402-tools"
paths = [str(getattr(r, "path", "")) for r in router.routes]
assert any("/bridge_health" in p for p in paths), "bridge_health endpoint not registered"
@pytest.mark.asyncio
async def test_response_model(self):
"""Verify response model structure."""
from app.routers.x402_bridge_health import BridgeHealthResponse
resp = BridgeHealthResponse(
timestamp="2026-06-15T02:00:00",
total_bridges=12,
bridges_healthy=10,
bridges_watch=1,
bridges_danger=1,
bridges_critical=0,
total_tvl_usd=10_000_000_000,
tvl_change_24h_pct=-2.5,
bridges=[],
exploit_signals=[],
contagion_risk=[],
remaining_trials=2,
pricing={"price_usd": 0.10, "is_trial": True, "trial_free": 2, "trials_remaining": 2},
)
assert resp.tool == "Cross-Chain Bridge Health & Exploit Monitor"
assert resp.version == "1.0"
assert resp.total_bridges == 12
assert resp.total_tvl_usd == 10_000_000_000
@pytest.mark.asyncio
async def test_price_in_canonical(self):
"""Verify bridge_health is registered in canonical_tools with correct pricing."""
from app.canonical_tools import CANONICAL_TOOL_PRICES
assert "bridge_health" in CANONICAL_TOOL_PRICES
pricing = CANONICAL_TOOL_PRICES["bridge_health"]
assert pricing["price_usd"] == 0.10
assert pricing["category"] == "security"
assert pricing["trial_free"] == 2
def test_valid_bridge_passes_validation(self):
"""Valid bridge keys should pass validation."""
from app.routers.x402_bridge_health import BridgeHealthRequest
req = BridgeHealthRequest(bridge="stargate", wallet=None)
assert req.bridge == "stargate"
req2 = BridgeHealthRequest(bridge="wormhole", wallet="0x1234567890abcdef1234567890abcdef12345678")
assert req2.bridge == "wormhole"
def test_invalid_bridge_rejected(self):
"""Invalid bridge key should raise validation error."""
from pydantic import ValidationError
from app.routers.x402_bridge_health import BridgeHealthRequest
with pytest.raises(ValidationError) as exc:
BridgeHealthRequest(bridge="nonexistent_bridge")
assert "Invalid bridge" in str(exc.value)
def test_valid_evm_wallet_passes(self):
"""Valid EVM address should pass validation."""
from app.routers.x402_bridge_health import BridgeHealthRequest
req = BridgeHealthRequest(wallet="0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18")
assert req.wallet is not None
def test_invalid_evm_wallet_rejected(self):
"""Invalid EVM address should be rejected."""
from pydantic import ValidationError
from app.routers.x402_bridge_health import BridgeHealthRequest
with pytest.raises(ValidationError):
BridgeHealthRequest(wallet="0x1234") # Too short
@pytest.mark.asyncio
async def test_valid_bridges_set(self):
"""Verify the valid bridges set matches the registry."""
from app.bridge_health_monitor import BRIDGE_REGISTRY
from app.routers.x402_bridge_health import VALID_BRIDGES
assert set(BRIDGE_REGISTRY.keys()) == VALID_BRIDGES, (
f"VALID_BRIDGES mismatch! Extra in set: {VALID_BRIDGES - set(BRIDGE_REGISTRY.keys())}. "
f"Missing: {set(BRIDGE_REGISTRY.keys()) - VALID_BRIDGES}"
)