Blockchain Node: - Replace /blocks (pagination) with /blocks-range (height range query) - Add start/end height parameters with 1000-block max range validation - Return blocks in ascending height order instead of descending - Update metrics names (rpc_get_blocks_range_*) - Remove total count from response (return start/end/count instead) Coordinator API: - Add effective_url property to DatabaseConfig (SQLite/PostgreSQL defaults
471 lines
18 KiB
Python
471 lines
18 KiB
Python
"""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"]
|