Files
aitbc/apps/coordinator-api/tests/test_exchange.py
oib 15427c96c0 chore: update file permissions to executable across repository
- Change file mode from 644 to 755 for all project files
- Add chain_id parameter to get_balance RPC endpoint with default "ait-devnet"
- Rename Miner.extra_meta_data to extra_metadata for consistency
2026-03-06 22:17:54 +01:00

471 lines
18 KiB
Python
Executable File

"""Tests for exchange API endpoints"""
import pytest
import time
import uuid
from fastapi.testclient import TestClient
from sqlmodel import Session, delete
from app.config import settings
from app.main import create_app
from app.routers.exchange import payments, BITCOIN_CONFIG
@pytest.fixture(scope="module", autouse=True)
def _init_db(tmp_path_factory):
db_file = tmp_path_factory.mktemp("data") / "exchange.db"
settings.database_url = f"sqlite:///{db_file}"
# Initialize database if needed
yield
@pytest.fixture()
def session():
# For this test, we'll use in-memory storage
yield None
@pytest.fixture()
def client():
app = create_app()
return TestClient(app)
@pytest.fixture(autouse=True)
def clear_payments():
"""Clear payments before each test"""
payments.clear()
yield
payments.clear()
class TestExchangeRatesEndpoint:
"""Test exchange rates endpoint"""
def test_get_exchange_rates_success(self, client: TestClient):
"""Test successful exchange rates retrieval"""
response = client.get("/v1/exchange/rates")
assert response.status_code == 200
data = response.json()
assert "btc_to_aitbc" in data
assert "aitbc_to_btc" in data
assert "fee_percent" in data
# Verify rates are reasonable
assert data["btc_to_aitbc"] > 0
assert data["aitbc_to_btc"] > 0
assert data["fee_percent"] >= 0
# Verify mathematical relationship
expected_aitbc_to_btc = 1.0 / data["btc_to_aitbc"]
assert abs(data["aitbc_to_btc"] - expected_aitbc_to_btc) < 0.00000001
class TestExchangeCreatePaymentEndpoint:
"""Test exchange create-payment endpoint"""
def test_create_payment_success(self, client: TestClient):
"""Test successful payment creation"""
payload = {
"user_id": "test_user_123",
"aitbc_amount": 1000,
"btc_amount": 0.01
}
response = client.post("/v1/exchange/create-payment", json=payload)
assert response.status_code == 200
data = response.json()
# Verify response structure
assert "payment_id" in data
assert data["user_id"] == payload["user_id"]
assert data["aitbc_amount"] == payload["aitbc_amount"]
assert data["btc_amount"] == payload["btc_amount"]
assert data["payment_address"] == BITCOIN_CONFIG['main_address']
assert data["status"] == "pending"
assert "created_at" in data
assert "expires_at" in data
# Verify payment was stored
assert data["payment_id"] in payments
stored_payment = payments[data["payment_id"]]
assert stored_payment["user_id"] == payload["user_id"]
assert stored_payment["aitbc_amount"] == payload["aitbc_amount"]
assert stored_payment["btc_amount"] == payload["btc_amount"]
def test_create_payment_invalid_amounts(self, client: TestClient):
"""Test payment creation with invalid amounts"""
# Test zero AITBC amount
payload1 = {
"user_id": "test_user",
"aitbc_amount": 0,
"btc_amount": 0.01
}
response1 = client.post("/v1/exchange/create-payment", json=payload1)
assert response1.status_code == 400
assert "Invalid amount" in response1.json()["detail"]
# Test negative BTC amount
payload2 = {
"user_id": "test_user",
"aitbc_amount": 1000,
"btc_amount": -0.01
}
response2 = client.post("/v1/exchange/create-payment", json=payload2)
assert response2.status_code == 400
assert "Invalid amount" in response2.json()["detail"]
def test_create_payment_amount_mismatch(self, client: TestClient):
"""Test payment creation with amount mismatch"""
payload = {
"user_id": "test_user",
"aitbc_amount": 1000, # Should be 0.01 BTC at 100000 rate
"btc_amount": 0.02 # This is double the expected amount
}
response = client.post("/v1/exchange/create-payment", json=payload)
assert response.status_code == 400
assert "Amount mismatch" in response.json()["detail"]
def test_create_payment_rounding_tolerance(self, client: TestClient):
"""Test payment creation with small rounding differences"""
payload = {
"user_id": "test_user",
"aitbc_amount": 1000,
"btc_amount": 0.01000000001 # Very small difference should be allowed
}
response = client.post("/v1/exchange/create-payment", json=payload)
assert response.status_code == 200
class TestExchangePaymentStatusEndpoint:
"""Test exchange payment-status endpoint"""
def test_get_payment_status_success(self, client: TestClient):
"""Test successful payment status retrieval"""
# First create a payment
create_payload = {
"user_id": "test_user",
"aitbc_amount": 500,
"btc_amount": 0.005
}
create_response = client.post("/v1/exchange/create-payment", json=create_payload)
payment_id = create_response.json()["payment_id"]
# Get payment status
response = client.get(f"/v1/exchange/payment-status/{payment_id}")
assert response.status_code == 200
data = response.json()
assert data["payment_id"] == payment_id
assert data["user_id"] == create_payload["user_id"]
assert data["aitbc_amount"] == create_payload["aitbc_amount"]
assert data["btc_amount"] == create_payload["btc_amount"]
assert data["status"] == "pending"
assert data["confirmations"] == 0
assert data["tx_hash"] is None
def test_get_payment_status_not_found(self, client: TestClient):
"""Test payment status for non-existent payment"""
fake_payment_id = "nonexistent_payment_id"
response = client.get(f"/v1/exchange/payment-status/{fake_payment_id}")
assert response.status_code == 404
assert "Payment not found" in response.json()["detail"]
def test_get_payment_status_expired(self, client: TestClient):
"""Test payment status for expired payment"""
# Create a payment with expired timestamp
payment_id = str(uuid.uuid4())
expired_payment = {
'payment_id': payment_id,
'user_id': 'test_user',
'aitbc_amount': 1000,
'btc_amount': 0.01,
'payment_address': BITCOIN_CONFIG['main_address'],
'status': 'pending',
'created_at': int(time.time()) - 7200, # 2 hours ago
'expires_at': int(time.time()) - 3600, # 1 hour ago (expired)
'confirmations': 0,
'tx_hash': None
}
payments[payment_id] = expired_payment
# Get payment status
response = client.get(f"/v1/exchange/payment-status/{payment_id}")
assert response.status_code == 200
data = response.json()
assert data["status"] == "expired"
class TestExchangeConfirmPaymentEndpoint:
"""Test exchange confirm-payment endpoint"""
def test_confirm_payment_success(self, client: TestClient):
"""Test successful payment confirmation"""
# First create a payment
create_payload = {
"user_id": "test_user",
"aitbc_amount": 1000,
"btc_amount": 0.01
}
create_response = client.post("/v1/exchange/create-payment", json=create_payload)
payment_id = create_response.json()["payment_id"]
# Confirm payment
confirm_payload = {"tx_hash": "test_transaction_hash_123"}
response = client.post(f"/v1/exchange/confirm-payment/{payment_id}",
json=confirm_payload)
assert response.status_code == 200
data = response.json()
assert data["status"] == "ok"
assert data["payment_id"] == payment_id
assert data["aitbc_amount"] == create_payload["aitbc_amount"]
# Verify payment was updated
payment = payments[payment_id]
assert payment["status"] == "confirmed"
assert payment["tx_hash"] == confirm_payload["tx_hash"]
assert "confirmed_at" in payment
def test_confirm_payment_not_found(self, client: TestClient):
"""Test payment confirmation for non-existent payment"""
fake_payment_id = "nonexistent_payment_id"
confirm_payload = {"tx_hash": "test_tx_hash"}
response = client.post(f"/v1/exchange/confirm-payment/{fake_payment_id}",
json=confirm_payload)
assert response.status_code == 404
assert "Payment not found" in response.json()["detail"]
def test_confirm_payment_not_pending(self, client: TestClient):
"""Test payment confirmation for non-pending payment"""
# Create and confirm a payment
create_payload = {
"user_id": "test_user",
"aitbc_amount": 1000,
"btc_amount": 0.01
}
create_response = client.post("/v1/exchange/create-payment", json=create_payload)
payment_id = create_response.json()["payment_id"]
# First confirmation
confirm_payload = {"tx_hash": "test_tx_hash_1"}
client.post(f"/v1/exchange/confirm-payment/{payment_id}", json=confirm_payload)
# Try to confirm again
confirm_payload2 = {"tx_hash": "test_tx_hash_2"}
response = client.post(f"/v1/exchange/confirm-payment/{payment_id}",
json=confirm_payload2)
assert response.status_code == 400
assert "Payment not in pending state" in response.json()["detail"]
class TestExchangeMarketStatsEndpoint:
"""Test exchange market-stats endpoint"""
def test_get_market_stats_empty(self, client: TestClient):
"""Test market stats with no payments"""
response = client.get("/v1/exchange/market-stats")
assert response.status_code == 200
data = response.json()
assert "price" in data
assert "price_change_24h" in data
assert "daily_volume" in data
assert "daily_volume_btc" in data
assert "total_payments" in data
assert "pending_payments" in data
# With no payments, these should be 0
assert data["daily_volume"] == 0
assert data["daily_volume_btc"] == 0
assert data["total_payments"] == 0
assert data["pending_payments"] == 0
def test_get_market_stats_with_payments(self, client: TestClient):
"""Test market stats with payments"""
current_time = int(time.time())
# Create some confirmed payments (within 24h)
for i in range(3):
payment_id = str(uuid.uuid4())
payment = {
'payment_id': payment_id,
'user_id': f'user_{i}',
'aitbc_amount': 1000 * (i + 1),
'btc_amount': 0.01 * (i + 1),
'payment_address': BITCOIN_CONFIG['main_address'],
'status': 'confirmed',
'created_at': current_time - 3600, # 1 hour ago
'expires_at': current_time + 3600,
'confirmations': 1,
'tx_hash': f'tx_hash_{i}',
'confirmed_at': current_time - 1800 # 30 minutes ago
}
payments[payment_id] = payment
# Create some pending payments
for i in range(2):
payment_id = str(uuid.uuid4())
payment = {
'payment_id': payment_id,
'user_id': f'pending_user_{i}',
'aitbc_amount': 500 * (i + 1),
'btc_amount': 0.005 * (i + 1),
'payment_address': BITCOIN_CONFIG['main_address'],
'status': 'pending',
'created_at': current_time - 1800, # 30 minutes ago
'expires_at': current_time + 1800,
'confirmations': 0,
'tx_hash': None
}
payments[payment_id] = payment
# Create an old confirmed payment (older than 24h)
old_payment_id = str(uuid.uuid4())
old_payment = {
'payment_id': old_payment_id,
'user_id': 'old_user',
'aitbc_amount': 2000,
'btc_amount': 0.02,
'payment_address': BITCOIN_CONFIG['main_address'],
'status': 'confirmed',
'created_at': current_time - 86400 - 3600, # 25 hours ago
'expires_at': current_time - 86400 + 3600,
'confirmations': 1,
'tx_hash': 'old_tx_hash',
'confirmed_at': current_time - 86400 # 24 hours ago
}
payments[old_payment_id] = old_payment
response = client.get("/v1/exchange/market-stats")
assert response.status_code == 200
data = response.json()
# Verify calculations
# Confirmed payments: 1000 + 2000 + 3000 = 6000 AITBC
# Pending payments: 500 + 1000 = 1500 AITBC
# Daily volume should only include recent confirmed payments
expected_daily_volume = 1000 + 2000 + 3000 # 6000 AITBC
expected_daily_volume_btc = expected_daily_volume / BITCOIN_CONFIG['exchange_rate']
assert data["total_payments"] == 3 # Only confirmed payments
assert data["pending_payments"] == 2
assert data["daily_volume"] == expected_daily_volume
assert abs(data["daily_volume_btc"] - expected_daily_volume_btc) < 0.00000001
class TestExchangeWalletEndpoints:
"""Test exchange wallet endpoints"""
def test_wallet_balance_endpoint(self, client: TestClient):
"""Test wallet balance endpoint"""
# This test may fail if bitcoin_wallet service is not available
# We'll test the endpoint structure and error handling
response = client.get("/v1/exchange/wallet/balance")
# The endpoint should exist, but may return 500 if service is unavailable
assert response.status_code in [200, 500]
if response.status_code == 200:
data = response.json()
# Verify response structure if successful
expected_fields = ["address", "balance", "unconfirmed_balance",
"total_received", "total_sent"]
for field in expected_fields:
assert field in data
def test_wallet_info_endpoint(self, client: TestClient):
"""Test wallet info endpoint"""
response = client.get("/v1/exchange/wallet/info")
# The endpoint should exist, but may return 500 if service is unavailable
assert response.status_code in [200, 500]
if response.status_code == 200:
data = response.json()
# Verify response structure if successful
expected_fields = ["address", "balance", "unconfirmed_balance",
"total_received", "total_sent", "transactions",
"network", "block_height"]
for field in expected_fields:
assert field in data
class TestExchangeIntegration:
"""Test exchange integration scenarios"""
def test_complete_payment_lifecycle(self, client: TestClient):
"""Test complete payment lifecycle: create → check status → confirm"""
# Step 1: Create payment
create_payload = {
"user_id": "integration_user",
"aitbc_amount": 1500,
"btc_amount": 0.015
}
create_response = client.post("/v1/exchange/create-payment", json=create_payload)
assert create_response.status_code == 200
payment_id = create_response.json()["payment_id"]
# Step 2: Check initial status
status_response = client.get(f"/v1/exchange/payment-status/{payment_id}")
assert status_response.status_code == 200
status_data = status_response.json()
assert status_data["status"] == "pending"
assert status_data["confirmations"] == 0
# Step 3: Confirm payment
confirm_payload = {"tx_hash": "integration_tx_hash"}
confirm_response = client.post(f"/v1/exchange/confirm-payment/{payment_id}",
json=confirm_payload)
assert confirm_response.status_code == 200
# Step 4: Check final status
final_status_response = client.get(f"/v1/exchange/payment-status/{payment_id}")
assert final_status_response.status_code == 200
final_status_data = final_status_response.json()
assert final_status_data["status"] == "confirmed"
assert final_status_data["tx_hash"] == "integration_tx_hash"
assert "confirmed_at" in final_status_data
def test_market_stats_update_after_payment(self, client: TestClient):
"""Test that market stats update after payment confirmation"""
# Get initial stats
initial_stats_response = client.get("/v1/exchange/market-stats")
assert initial_stats_response.status_code == 200
initial_stats = initial_stats_response.json()
initial_total = initial_stats["total_payments"]
# Create and confirm payment
create_payload = {
"user_id": "stats_user",
"aitbc_amount": 2000,
"btc_amount": 0.02
}
create_response = client.post("/v1/exchange/create-payment", json=create_payload)
payment_id = create_response.json()["payment_id"]
confirm_payload = {"tx_hash": "stats_tx_hash"}
client.post(f"/v1/exchange/confirm-payment/{payment_id}", json=confirm_payload)
# Check updated stats
updated_stats_response = client.get("/v1/exchange/market-stats")
assert updated_stats_response.status_code == 200
updated_stats = updated_stats_response.json()
# Total payments should have increased
assert updated_stats["total_payments"] == initial_total + 1
assert updated_stats["daily_volume"] >= initial_stats["daily_volume"]