Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
"""
|
|
|
|
|
Exchange API Tests
|
|
|
|
|
|
|
|
|
|
Tests for the Bitcoin trading exchange endpoints.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from datetime import UTC, date, datetime, timedelta
|
|
|
|
|
from unittest.mock import patch
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
2025-12-23 14:34:22 +01:00
|
|
|
from models import (
|
|
|
|
|
BitcoinTransferMethod,
|
|
|
|
|
Exchange,
|
|
|
|
|
ExchangeStatus,
|
|
|
|
|
PriceHistory,
|
|
|
|
|
TradeDirection,
|
|
|
|
|
)
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
from price_fetcher import PAIR_BTC_EUR, SOURCE_BITFINEX
|
2025-12-27 12:52:43 +01:00
|
|
|
from shared_constants import EUR_TRADE_INCREMENT
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def tomorrow() -> date:
|
|
|
|
|
return date.today() + timedelta(days=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def in_days(n: int) -> date:
|
|
|
|
|
return date.today() + timedelta(days=n)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# Fixtures
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def create_price_in_db(db, price: float = 20000.0, minutes_ago: int = 0):
|
|
|
|
|
"""Create a price record in the database."""
|
|
|
|
|
timestamp = datetime.now(UTC) - timedelta(minutes=minutes_ago)
|
|
|
|
|
price_record = PriceHistory(
|
|
|
|
|
source=SOURCE_BITFINEX,
|
|
|
|
|
pair=PAIR_BTC_EUR,
|
|
|
|
|
price=price,
|
|
|
|
|
timestamp=timestamp,
|
|
|
|
|
)
|
|
|
|
|
db.add(price_record)
|
|
|
|
|
await db.commit()
|
|
|
|
|
await db.refresh(price_record)
|
|
|
|
|
return price_record
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def mock_price_fetcher(price: float = 20000.0):
|
|
|
|
|
"""Return a mock that returns the given price."""
|
|
|
|
|
|
|
|
|
|
async def mock_fetch():
|
|
|
|
|
return (price, datetime.now(UTC))
|
|
|
|
|
|
|
|
|
|
return patch("routes.exchange.fetch_btc_eur_price", mock_fetch)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def setup_availability_and_price(client_factory, admin_user, target_date=None):
|
2025-12-26 20:20:23 +01:00
|
|
|
"""Helper to set up availability, price, and pricing config for tests."""
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
if target_date is None:
|
|
|
|
|
target_date = tomorrow()
|
|
|
|
|
|
|
|
|
|
# Admin sets availability
|
|
|
|
|
async with client_factory.create(cookies=admin_user["cookies"]) as admin_client:
|
|
|
|
|
await admin_client.put(
|
|
|
|
|
"/api/admin/availability",
|
|
|
|
|
json={
|
|
|
|
|
"date": str(target_date),
|
|
|
|
|
"slots": [{"start_time": "09:00:00", "end_time": "17:00:00"}],
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
2025-12-26 20:20:23 +01:00
|
|
|
# Create fresh price in DB and pricing config
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
async with client_factory.get_db_session() as db:
|
|
|
|
|
await create_price_in_db(db, price=20000.0, minutes_ago=0)
|
2025-12-26 20:20:23 +01:00
|
|
|
from repositories.pricing import PricingRepository
|
|
|
|
|
|
|
|
|
|
repo = PricingRepository(db)
|
|
|
|
|
await repo.create_or_update(
|
|
|
|
|
premium_buy=5,
|
|
|
|
|
premium_sell=5,
|
|
|
|
|
small_trade_threshold_eur=20000,
|
|
|
|
|
small_trade_extra_premium=2,
|
|
|
|
|
eur_min_buy=10000,
|
|
|
|
|
eur_max_buy=300000,
|
|
|
|
|
eur_min_sell=10000,
|
|
|
|
|
eur_max_sell=300000,
|
|
|
|
|
)
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
return target_date
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# Price Endpoint Tests
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestExchangePriceEndpoint:
|
|
|
|
|
"""Test the /api/exchange/price endpoint."""
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_regular_user_can_get_price(self, client_factory, regular_user):
|
|
|
|
|
"""Regular user can access price endpoint."""
|
2025-12-26 20:20:23 +01:00
|
|
|
# Create fresh price in DB and pricing config
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
async with client_factory.get_db_session() as db:
|
|
|
|
|
await create_price_in_db(db, price=20000.0, minutes_ago=0)
|
2025-12-26 20:20:23 +01:00
|
|
|
from repositories.pricing import PricingRepository
|
|
|
|
|
|
|
|
|
|
repo = PricingRepository(db)
|
|
|
|
|
await repo.create_or_update(
|
|
|
|
|
premium_buy=5,
|
|
|
|
|
premium_sell=5,
|
|
|
|
|
small_trade_threshold_eur=20000,
|
|
|
|
|
small_trade_extra_premium=2,
|
|
|
|
|
eur_min_buy=10000,
|
|
|
|
|
eur_max_buy=300000,
|
|
|
|
|
eur_min_sell=10000,
|
|
|
|
|
eur_max_sell=300000,
|
|
|
|
|
)
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
# Mock the price fetcher to prevent real API calls
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
response = await client.get("/api/exchange/price")
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert "price" in data
|
|
|
|
|
assert "config" in data
|
|
|
|
|
assert data["price"]["market_price"] == 20000.0
|
2025-12-26 20:20:23 +01:00
|
|
|
# Note: premium is now in config, agreed_price is calculated on frontend
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_admin_cannot_get_price(self, client_factory, admin_user):
|
|
|
|
|
"""Admin cannot access price endpoint."""
|
|
|
|
|
async with client_factory.create(cookies=admin_user["cookies"]) as client:
|
|
|
|
|
response = await client.get("/api/exchange/price")
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 403
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_unauthenticated_cannot_get_price(self, client):
|
|
|
|
|
"""Unauthenticated user cannot access price endpoint."""
|
|
|
|
|
response = await client.get("/api/exchange/price")
|
|
|
|
|
assert response.status_code == 401
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_stale_price_triggers_fetch(self, client_factory, regular_user):
|
|
|
|
|
"""When cached price is stale, a fresh price is fetched."""
|
2025-12-26 20:20:23 +01:00
|
|
|
# Create stale price (6 minutes old, threshold is 5 minutes) and pricing config
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
async with client_factory.get_db_session() as db:
|
|
|
|
|
await create_price_in_db(db, price=19000.0, minutes_ago=6)
|
2025-12-26 20:20:23 +01:00
|
|
|
from repositories.pricing import PricingRepository
|
|
|
|
|
|
|
|
|
|
repo = PricingRepository(db)
|
|
|
|
|
await repo.create_or_update(
|
|
|
|
|
premium_buy=5,
|
|
|
|
|
premium_sell=5,
|
|
|
|
|
small_trade_threshold_eur=20000,
|
|
|
|
|
small_trade_extra_premium=2,
|
|
|
|
|
eur_min_buy=10000,
|
|
|
|
|
eur_max_buy=300000,
|
|
|
|
|
eur_min_sell=10000,
|
|
|
|
|
eur_max_sell=300000,
|
|
|
|
|
)
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
# Mock fetcher returns new price
|
|
|
|
|
with mock_price_fetcher(21000.0):
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
response = await client.get("/api/exchange/price")
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
# Should have the fresh price, not the stale one
|
|
|
|
|
assert data["price"]["market_price"] == 21000.0
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_config_contains_expected_fields(self, client_factory, regular_user):
|
|
|
|
|
"""Config section contains all required fields."""
|
|
|
|
|
async with client_factory.get_db_session() as db:
|
|
|
|
|
await create_price_in_db(db)
|
2025-12-26 20:20:23 +01:00
|
|
|
from repositories.pricing import PricingRepository
|
|
|
|
|
|
|
|
|
|
repo = PricingRepository(db)
|
|
|
|
|
await repo.create_or_update(
|
|
|
|
|
premium_buy=5,
|
|
|
|
|
premium_sell=6,
|
|
|
|
|
small_trade_threshold_eur=20000,
|
|
|
|
|
small_trade_extra_premium=2,
|
|
|
|
|
eur_min_buy=10000,
|
|
|
|
|
eur_max_buy=300000,
|
|
|
|
|
eur_min_sell=12000,
|
|
|
|
|
eur_max_sell=320000,
|
|
|
|
|
)
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
with mock_price_fetcher():
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
response = await client.get("/api/exchange/price")
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
config = response.json()["config"]
|
2025-12-26 20:20:23 +01:00
|
|
|
assert "eur_min_buy" in config
|
|
|
|
|
assert "eur_max_buy" in config
|
|
|
|
|
assert "eur_min_sell" in config
|
|
|
|
|
assert "eur_max_sell" in config
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
assert "eur_increment" in config
|
2025-12-26 20:20:23 +01:00
|
|
|
assert "premium_buy" in config
|
|
|
|
|
assert "premium_sell" in config
|
|
|
|
|
assert "small_trade_threshold_eur" in config
|
|
|
|
|
assert "small_trade_extra_premium" in config
|
|
|
|
|
assert config["eur_min_buy"] == 10000
|
|
|
|
|
assert config["eur_max_buy"] == 300000
|
|
|
|
|
assert config["eur_min_sell"] == 12000
|
|
|
|
|
assert config["eur_max_sell"] == 320000
|
2025-12-27 12:52:43 +01:00
|
|
|
assert config["eur_increment"] == EUR_TRADE_INCREMENT
|
2025-12-26 20:20:23 +01:00
|
|
|
assert config["premium_buy"] == 5
|
|
|
|
|
assert config["premium_sell"] == 6
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# Create Exchange Tests
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCreateExchange:
|
|
|
|
|
"""Test creating exchange trades."""
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_create_exchange_buy_success(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Can successfully create a buy exchange trade."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
2025-12-23 14:40:42 +01:00
|
|
|
"bitcoin_transfer_method": "onchain",
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
"eur_amount": 10000, # €100 in cents
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert data["direction"] == "buy"
|
2025-12-23 14:40:42 +01:00
|
|
|
assert data["bitcoin_transfer_method"] == "onchain"
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
assert data["eur_amount"] == 10000
|
|
|
|
|
assert data["status"] == "booked"
|
|
|
|
|
assert data["sats_amount"] > 0
|
2025-12-26 20:24:13 +01:00
|
|
|
# For buy with €100 (small trade): base 5% + extra 2% = 7%
|
|
|
|
|
# Agreed price is market * 1.07
|
|
|
|
|
assert data["agreed_price_eur"] == pytest.approx(21400.0, rel=0.001)
|
|
|
|
|
assert data["premium_percentage"] == 7
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_create_exchange_sell_success(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Can successfully create a sell exchange trade."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T10:00:00Z",
|
|
|
|
|
"direction": "sell",
|
2025-12-23 14:40:42 +01:00
|
|
|
"bitcoin_transfer_method": "lightning",
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
"eur_amount": 20000, # €200 in cents
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert data["direction"] == "sell"
|
2025-12-23 14:40:42 +01:00
|
|
|
assert data["bitcoin_transfer_method"] == "lightning"
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
assert data["eur_amount"] == 20000
|
2025-12-26 20:24:13 +01:00
|
|
|
# For sell with €200 (at threshold): base 5% + extra 2% = 7%
|
|
|
|
|
# Agreed price is market * 0.93
|
|
|
|
|
assert data["agreed_price_eur"] == pytest.approx(18600.0, rel=0.001)
|
|
|
|
|
assert data["premium_percentage"] == 7
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_cannot_book_same_slot_twice(
|
|
|
|
|
self, client_factory, regular_user, alt_regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Cannot book the same slot twice."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
# First user books
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
2025-12-23 14:40:42 +01:00
|
|
|
"bitcoin_transfer_method": "onchain",
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
"eur_amount": 10000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
|
|
|
|
# Second user tries same slot
|
|
|
|
|
async with client_factory.create(
|
|
|
|
|
cookies=alt_regular_user["cookies"]
|
|
|
|
|
) as client:
|
|
|
|
|
response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
2025-12-23 14:40:42 +01:00
|
|
|
"bitcoin_transfer_method": "onchain",
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
"eur_amount": 10000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 409
|
|
|
|
|
assert "already been booked" in response.json()["detail"]
|
|
|
|
|
|
2025-12-23 15:50:14 +01:00
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_cannot_book_two_trades_same_day(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Cannot book two trades on the same day."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
# First trade
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
|
|
|
|
"bitcoin_transfer_method": "onchain",
|
|
|
|
|
"eur_amount": 10000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
|
|
|
|
# Try to book another trade on the same day
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T10:00:00Z",
|
|
|
|
|
"direction": "sell",
|
|
|
|
|
"bitcoin_transfer_method": "lightning",
|
|
|
|
|
"eur_amount": 20000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
assert "already have a trade booked" in response.json()["detail"]
|
|
|
|
|
assert "Trade ID:" in response.json()["detail"]
|
|
|
|
|
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_invalid_direction_rejected(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Invalid direction is rejected."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "invalid",
|
2025-12-23 14:40:42 +01:00
|
|
|
"bitcoin_transfer_method": "onchain",
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
"eur_amount": 10000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
assert "Invalid direction" in response.json()["detail"]
|
|
|
|
|
|
2025-12-23 14:40:42 +01:00
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_missing_bitcoin_transfer_method_rejected(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Missing bitcoin_transfer_method is rejected."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
|
|
|
|
"eur_amount": 10000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 422
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_invalid_bitcoin_transfer_method_rejected(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Invalid bitcoin_transfer_method is rejected."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
|
|
|
|
"bitcoin_transfer_method": "invalid",
|
|
|
|
|
"eur_amount": 10000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
assert "Invalid bitcoin_transfer_method" in response.json()["detail"]
|
|
|
|
|
|
2025-12-23 14:46:03 +01:00
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_lightning_above_threshold_rejected(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Lightning payment above threshold is rejected."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
# Try Lightning with amount above threshold (€1000 = 100000 cents)
|
|
|
|
|
response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
|
|
|
|
"bitcoin_transfer_method": "lightning",
|
|
|
|
|
"eur_amount": 110000, # €1100, above €1000 threshold
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
assert "Lightning payments are only allowed" in response.json()["detail"]
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_lightning_below_threshold_accepted(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Lightning payment below threshold is accepted."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
# Lightning with amount at threshold
|
|
|
|
|
response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
|
|
|
|
"bitcoin_transfer_method": "lightning",
|
|
|
|
|
"eur_amount": 100000, # €1000, exactly at threshold
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert data["bitcoin_transfer_method"] == "lightning"
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_onchain_above_threshold_accepted(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Onchain payment above threshold is accepted."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
# Onchain with amount above Lightning threshold
|
|
|
|
|
response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
|
|
|
|
"bitcoin_transfer_method": "onchain",
|
|
|
|
|
"eur_amount": 110000, # €1100, above €1000 threshold
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert data["bitcoin_transfer_method"] == "onchain"
|
|
|
|
|
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_amount_below_minimum_rejected(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""EUR amount below minimum is rejected."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
2025-12-23 14:40:42 +01:00
|
|
|
"bitcoin_transfer_method": "onchain",
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
"eur_amount": 5000, # €50, below min of €100
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
assert "at least" in response.json()["detail"]
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_amount_above_maximum_rejected(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""EUR amount above maximum is rejected."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
2025-12-23 14:40:42 +01:00
|
|
|
"bitcoin_transfer_method": "onchain",
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
"eur_amount": 400000, # €4000, above max of €3000
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
assert "at most" in response.json()["detail"]
|
|
|
|
|
|
2025-12-26 20:24:13 +01:00
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_premium_calculation_small_trade_extra(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Premium includes extra for small trades (amount <= threshold)."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
# Trade below threshold: should get base + extra premium
|
|
|
|
|
response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
|
|
|
|
"bitcoin_transfer_method": "onchain",
|
|
|
|
|
"eur_amount": 14000, # €140, below €200 threshold, multiple of €20
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
# Base 5% + extra 2% = 7% total
|
|
|
|
|
assert data["premium_percentage"] == 7
|
|
|
|
|
# Market 20000 * 1.07 = 21400
|
|
|
|
|
assert data["agreed_price_eur"] == pytest.approx(21400.0, rel=0.001)
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_premium_calculation_large_trade_base_only(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Premium is base only for large trades (amount > threshold)."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
# Trade above threshold: should get base premium only
|
|
|
|
|
response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T10:00:00Z",
|
|
|
|
|
"direction": "buy",
|
|
|
|
|
"bitcoin_transfer_method": "onchain",
|
|
|
|
|
"eur_amount": 24000, # €240, above €200 threshold, multiple of €20
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
# Base 5% only (no extra)
|
|
|
|
|
assert data["premium_percentage"] == 5
|
|
|
|
|
# Market 20000 * 1.05 = 21000
|
|
|
|
|
assert data["agreed_price_eur"] == pytest.approx(21000.0, rel=0.001)
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_premium_calculation_direction_specific(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Premium uses direction-specific base values."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
target_date_2 = await setup_availability_and_price(
|
|
|
|
|
client_factory, admin_user, target_date=in_days(2)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Update pricing config with different premiums for buy/sell
|
|
|
|
|
async with client_factory.get_db_session() as db:
|
|
|
|
|
from repositories.pricing import PricingRepository
|
|
|
|
|
|
|
|
|
|
repo = PricingRepository(db)
|
|
|
|
|
await repo.create_or_update(
|
|
|
|
|
premium_buy=10,
|
|
|
|
|
premium_sell=8,
|
|
|
|
|
small_trade_threshold_eur=20000,
|
|
|
|
|
small_trade_extra_premium=2,
|
|
|
|
|
eur_min_buy=10000,
|
|
|
|
|
eur_max_buy=300000,
|
|
|
|
|
eur_min_sell=10000,
|
|
|
|
|
eur_max_sell=300000,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
# Buy trade
|
|
|
|
|
buy_response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
|
|
|
|
"bitcoin_transfer_method": "onchain",
|
|
|
|
|
"eur_amount": 14000, # €140, multiple of €20
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert buy_response.status_code == 200
|
|
|
|
|
buy_data = buy_response.json()
|
|
|
|
|
# Buy: base 10% + extra 2% = 12%
|
|
|
|
|
assert buy_data["premium_percentage"] == 12
|
|
|
|
|
|
|
|
|
|
# Sell trade (different day to avoid one-trade-per-day limit)
|
|
|
|
|
sell_response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date_2}T10:00:00Z",
|
|
|
|
|
"direction": "sell",
|
|
|
|
|
"bitcoin_transfer_method": "onchain",
|
|
|
|
|
"eur_amount": 14000, # €140, multiple of €20
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert sell_response.status_code == 200
|
|
|
|
|
sell_data = sell_response.json()
|
|
|
|
|
# Sell: base 8% + extra 2% = 10%
|
|
|
|
|
assert sell_data["premium_percentage"] == 10
|
|
|
|
|
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_amount_not_multiple_of_increment_rejected(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""EUR amount not a multiple of increment is rejected."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
2025-12-27 12:52:43 +01:00
|
|
|
# Calculate an amount that's not a multiple of the increment
|
|
|
|
|
# Use a valid base amount (multiple of increment) and add a small offset
|
|
|
|
|
base_amount = 11500 # €115.00
|
|
|
|
|
invalid_amount = base_amount + (EUR_TRADE_INCREMENT - 1) # Not a multiple
|
|
|
|
|
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
2025-12-23 14:40:42 +01:00
|
|
|
"bitcoin_transfer_method": "onchain",
|
2025-12-27 12:52:43 +01:00
|
|
|
"eur_amount": invalid_amount,
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
assert "multiple" in response.json()["detail"]
|
|
|
|
|
|
2025-12-23 12:17:40 +01:00
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_slot_not_on_minute_boundary_rejected(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Slot start time not on slot duration boundary is rejected."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:07:00Z", # 07 is not on 15-min boundary
|
|
|
|
|
"direction": "buy",
|
2025-12-23 14:40:42 +01:00
|
|
|
"bitcoin_transfer_method": "onchain",
|
2025-12-23 12:17:40 +01:00
|
|
|
"eur_amount": 10000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
assert "boundary" in response.json()["detail"].lower()
|
|
|
|
|
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_stale_price_blocks_booking(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Cannot book when price is stale."""
|
|
|
|
|
# Set up availability
|
|
|
|
|
async with client_factory.create(cookies=admin_user["cookies"]) as admin_client:
|
|
|
|
|
await admin_client.put(
|
|
|
|
|
"/api/admin/availability",
|
|
|
|
|
json={
|
|
|
|
|
"date": str(tomorrow()),
|
|
|
|
|
"slots": [{"start_time": "09:00:00", "end_time": "17:00:00"}],
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
2025-12-26 20:24:13 +01:00
|
|
|
# Create stale price and pricing config
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
async with client_factory.get_db_session() as db:
|
|
|
|
|
await create_price_in_db(db, price=20000.0, minutes_ago=10)
|
2025-12-26 20:24:13 +01:00
|
|
|
from repositories.pricing import PricingRepository
|
|
|
|
|
|
|
|
|
|
repo = PricingRepository(db)
|
|
|
|
|
await repo.create_or_update(
|
|
|
|
|
premium_buy=5,
|
|
|
|
|
premium_sell=5,
|
|
|
|
|
small_trade_threshold_eur=20000,
|
|
|
|
|
small_trade_extra_premium=2,
|
|
|
|
|
eur_min_buy=10000,
|
|
|
|
|
eur_max_buy=300000,
|
|
|
|
|
eur_min_sell=10000,
|
|
|
|
|
eur_max_sell=300000,
|
|
|
|
|
)
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{tomorrow()}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
2025-12-23 14:40:42 +01:00
|
|
|
"bitcoin_transfer_method": "onchain",
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
"eur_amount": 10000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 503
|
|
|
|
|
assert "stale" in response.json()["detail"].lower()
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_slot_outside_availability_rejected(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Slot outside availability is rejected."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T20:00:00Z", # Outside 09-17
|
|
|
|
|
"direction": "buy",
|
2025-12-23 14:40:42 +01:00
|
|
|
"bitcoin_transfer_method": "onchain",
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
"eur_amount": 10000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
assert "not available" in response.json()["detail"]
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_admin_cannot_create_exchange(self, client_factory, admin_user):
|
|
|
|
|
"""Admin cannot create exchange."""
|
|
|
|
|
async with client_factory.create(cookies=admin_user["cookies"]) as client:
|
|
|
|
|
response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{tomorrow()}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
2025-12-23 14:40:42 +01:00
|
|
|
"bitcoin_transfer_method": "onchain",
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
"eur_amount": 10000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 403
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# User Trades Tests
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestUserTrades:
|
|
|
|
|
"""Test user's trades endpoints."""
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_get_my_trades_empty(self, client_factory, regular_user):
|
|
|
|
|
"""Returns empty list when user has no trades."""
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
response = await client.get("/api/trades")
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert response.json() == []
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_get_my_trades_with_exchanges(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Returns user's trades."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
2025-12-23 15:57:43 +01:00
|
|
|
target_date_2 = await setup_availability_and_price(
|
|
|
|
|
client_factory, admin_user, target_date=in_days(2)
|
|
|
|
|
)
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
2025-12-23 15:57:43 +01:00
|
|
|
# Book two trades on different days
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
2025-12-23 14:40:42 +01:00
|
|
|
"bitcoin_transfer_method": "onchain",
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
"eur_amount": 10000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
2025-12-23 15:57:43 +01:00
|
|
|
"slot_start": f"{target_date_2}T10:00:00Z",
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
"direction": "sell",
|
2025-12-23 14:40:42 +01:00
|
|
|
"bitcoin_transfer_method": "lightning",
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
"eur_amount": 20000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Get trades
|
|
|
|
|
response = await client.get("/api/trades")
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert len(data) == 2
|
|
|
|
|
|
|
|
|
|
|
2025-12-23 15:52:02 +01:00
|
|
|
class TestGetMyTrade:
|
|
|
|
|
"""Test getting a single trade by ID."""
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_get_my_trade_success(self, client_factory, regular_user, admin_user):
|
|
|
|
|
"""Can get own trade by ID."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
# Create a trade
|
|
|
|
|
create_response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
|
|
|
|
"bitcoin_transfer_method": "onchain",
|
|
|
|
|
"eur_amount": 10000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert create_response.status_code == 200
|
2025-12-23 17:03:51 +01:00
|
|
|
public_id = create_response.json()["public_id"]
|
2025-12-23 15:52:02 +01:00
|
|
|
|
|
|
|
|
# Get the trade
|
2025-12-23 17:03:51 +01:00
|
|
|
get_response = await client.get(f"/api/trades/{public_id}")
|
2025-12-23 15:52:02 +01:00
|
|
|
|
|
|
|
|
assert get_response.status_code == 200
|
|
|
|
|
data = get_response.json()
|
2025-12-23 17:03:51 +01:00
|
|
|
assert data["public_id"] == public_id
|
2025-12-23 15:52:02 +01:00
|
|
|
assert data["direction"] == "buy"
|
|
|
|
|
assert data["bitcoin_transfer_method"] == "onchain"
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_cannot_get_other_user_trade(
|
|
|
|
|
self, client_factory, regular_user, alt_regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Cannot get another user's trade."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
# First user creates a trade
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
create_response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
|
|
|
|
"bitcoin_transfer_method": "onchain",
|
|
|
|
|
"eur_amount": 10000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert create_response.status_code == 200
|
2025-12-23 17:03:51 +01:00
|
|
|
public_id = create_response.json()["public_id"]
|
2025-12-23 15:52:02 +01:00
|
|
|
|
|
|
|
|
# Second user tries to get it
|
|
|
|
|
async with client_factory.create(
|
|
|
|
|
cookies=alt_regular_user["cookies"]
|
|
|
|
|
) as client:
|
2025-12-23 17:03:51 +01:00
|
|
|
get_response = await client.get(f"/api/trades/{public_id}")
|
2025-12-23 15:52:02 +01:00
|
|
|
|
|
|
|
|
assert get_response.status_code == 404
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_get_nonexistent_trade_returns_404(
|
|
|
|
|
self, client_factory, regular_user
|
|
|
|
|
):
|
|
|
|
|
"""Getting a nonexistent trade returns 404."""
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
2025-12-23 17:03:51 +01:00
|
|
|
# Use a valid UUID format but non-existent
|
|
|
|
|
response = await client.get(
|
|
|
|
|
"/api/trades/00000000-0000-0000-0000-000000000000"
|
|
|
|
|
)
|
2025-12-23 15:52:02 +01:00
|
|
|
|
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
|
|
|
|
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
class TestCancelTrade:
|
|
|
|
|
"""Test cancelling trades."""
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_cancel_own_trade(self, client_factory, regular_user, admin_user):
|
|
|
|
|
"""User can cancel their own trade."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
# Book trade
|
|
|
|
|
book_response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
2025-12-23 14:40:42 +01:00
|
|
|
"bitcoin_transfer_method": "onchain",
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
"eur_amount": 10000,
|
|
|
|
|
},
|
|
|
|
|
)
|
2025-12-23 17:03:51 +01:00
|
|
|
public_id = book_response.json()["public_id"]
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
# Cancel
|
2025-12-23 17:03:51 +01:00
|
|
|
response = await client.post(f"/api/trades/{public_id}/cancel")
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert data["status"] == "cancelled_by_user"
|
|
|
|
|
assert data["cancelled_at"] is not None
|
|
|
|
|
|
2025-12-23 10:41:08 +01:00
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_cancelled_slot_becomes_available_again(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""When a trade is cancelled, the slot becomes available for booking again."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
# Book a slot
|
|
|
|
|
book_response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
2025-12-23 14:40:42 +01:00
|
|
|
"bitcoin_transfer_method": "onchain",
|
2025-12-23 10:41:08 +01:00
|
|
|
"eur_amount": 10000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert book_response.status_code == 200
|
2025-12-23 17:03:51 +01:00
|
|
|
public_id = book_response.json()["public_id"]
|
2025-12-23 10:41:08 +01:00
|
|
|
|
|
|
|
|
# Verify the slot is NOT available
|
|
|
|
|
slots_response = await client.get(
|
|
|
|
|
f"/api/exchange/slots?date={target_date}"
|
|
|
|
|
)
|
|
|
|
|
assert slots_response.status_code == 200
|
|
|
|
|
slots = slots_response.json()["slots"]
|
|
|
|
|
slot_starts = [s["start_time"] for s in slots]
|
|
|
|
|
assert f"{target_date}T09:00:00Z" not in slot_starts
|
|
|
|
|
|
|
|
|
|
# Cancel the trade
|
2025-12-23 17:03:51 +01:00
|
|
|
cancel_response = await client.post(f"/api/trades/{public_id}/cancel")
|
2025-12-23 10:41:08 +01:00
|
|
|
assert cancel_response.status_code == 200
|
|
|
|
|
|
|
|
|
|
# Verify the slot IS available again
|
|
|
|
|
slots_response = await client.get(
|
|
|
|
|
f"/api/exchange/slots?date={target_date}"
|
|
|
|
|
)
|
|
|
|
|
assert slots_response.status_code == 200
|
|
|
|
|
slots = slots_response.json()["slots"]
|
|
|
|
|
slot_starts = [s["start_time"] for s in slots]
|
|
|
|
|
assert f"{target_date}T09:00:00Z" in slot_starts
|
|
|
|
|
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_cannot_cancel_others_trade(
|
|
|
|
|
self, client_factory, regular_user, alt_regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""User cannot cancel another user's trade."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
# First user books
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
book_response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
2025-12-23 14:40:42 +01:00
|
|
|
"bitcoin_transfer_method": "onchain",
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
"eur_amount": 10000,
|
|
|
|
|
},
|
|
|
|
|
)
|
2025-12-23 17:03:51 +01:00
|
|
|
public_id = book_response.json()["public_id"]
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
# Second user tries to cancel (no mock needed for this)
|
|
|
|
|
async with client_factory.create(cookies=alt_regular_user["cookies"]) as client:
|
2025-12-23 17:03:51 +01:00
|
|
|
response = await client.post(f"/api/trades/{public_id}/cancel")
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
assert response.status_code == 403
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_cannot_cancel_nonexistent_trade(self, client_factory, regular_user):
|
|
|
|
|
"""Returns 404 for non-existent trade."""
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
2025-12-23 17:03:51 +01:00
|
|
|
response = await client.post(
|
|
|
|
|
"/api/trades/00000000-0000-0000-0000-000000000000/cancel"
|
|
|
|
|
)
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_cannot_cancel_already_cancelled(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Cannot cancel an already cancelled trade."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
# Book and cancel
|
|
|
|
|
book_response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
2025-12-23 14:40:42 +01:00
|
|
|
"bitcoin_transfer_method": "onchain",
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
"eur_amount": 10000,
|
|
|
|
|
},
|
|
|
|
|
)
|
2025-12-23 17:03:51 +01:00
|
|
|
public_id = book_response.json()["public_id"]
|
|
|
|
|
await client.post(f"/api/trades/{public_id}/cancel")
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
# Try to cancel again
|
2025-12-23 17:03:51 +01:00
|
|
|
response = await client.post(f"/api/trades/{public_id}/cancel")
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
assert "cancelled_by_user" in response.json()["detail"]
|
|
|
|
|
|
2025-12-23 10:39:09 +01:00
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_cannot_cancel_past_trade(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Cannot cancel a trade after the slot time has passed."""
|
|
|
|
|
# Create a past trade directly in DB
|
|
|
|
|
async with client_factory.get_db_session() as db:
|
|
|
|
|
await create_price_in_db(db, price=20000.0)
|
|
|
|
|
past_time = datetime.now(UTC) - timedelta(hours=1)
|
|
|
|
|
exchange = Exchange(
|
|
|
|
|
user_id=regular_user["user"]["id"],
|
|
|
|
|
slot_start=past_time,
|
|
|
|
|
slot_end=past_time + timedelta(minutes=15),
|
|
|
|
|
direction=TradeDirection.BUY,
|
2025-12-23 14:34:22 +01:00
|
|
|
bitcoin_transfer_method=BitcoinTransferMethod.ONCHAIN,
|
2025-12-23 10:39:09 +01:00
|
|
|
eur_amount=10000,
|
|
|
|
|
sats_amount=500000,
|
|
|
|
|
market_price_eur=20000.0,
|
|
|
|
|
agreed_price_eur=21000.0,
|
|
|
|
|
premium_percentage=5,
|
|
|
|
|
status=ExchangeStatus.BOOKED,
|
|
|
|
|
)
|
|
|
|
|
db.add(exchange)
|
|
|
|
|
await db.commit()
|
|
|
|
|
await db.refresh(exchange)
|
2025-12-23 17:03:51 +01:00
|
|
|
public_id = exchange.public_id
|
2025-12-23 10:39:09 +01:00
|
|
|
|
|
|
|
|
# User tries to cancel
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
2025-12-23 17:03:51 +01:00
|
|
|
response = await client.post(f"/api/trades/{public_id}/cancel")
|
2025-12-23 10:39:09 +01:00
|
|
|
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
assert "already passed" in response.json()["detail"]
|
|
|
|
|
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# Admin Trades Tests
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestAdminUpcomingTrades:
|
|
|
|
|
"""Test admin viewing upcoming trades."""
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_admin_can_view_upcoming_trades(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Admin can view upcoming trades."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
# User books
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
2025-12-23 14:40:42 +01:00
|
|
|
"bitcoin_transfer_method": "onchain",
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
"eur_amount": 10000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Admin views upcoming trades
|
|
|
|
|
async with client_factory.create(cookies=admin_user["cookies"]) as client:
|
|
|
|
|
response = await client.get("/api/admin/trades/upcoming")
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert len(data) >= 1
|
|
|
|
|
# Check user contact info is present
|
|
|
|
|
assert "user_contact" in data[0]
|
|
|
|
|
assert "email" in data[0]["user_contact"]
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_regular_user_cannot_view_upcoming(
|
|
|
|
|
self, client_factory, regular_user
|
|
|
|
|
):
|
|
|
|
|
"""Regular user cannot access admin endpoint."""
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
response = await client.get("/api/admin/trades/upcoming")
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 403
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestAdminPastTrades:
|
|
|
|
|
"""Test admin viewing past trades."""
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_admin_can_view_past_trades(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Admin can view past trades."""
|
|
|
|
|
# Create a past trade directly in DB
|
|
|
|
|
async with client_factory.get_db_session() as db:
|
|
|
|
|
await create_price_in_db(db, price=20000.0)
|
|
|
|
|
past_time = datetime.now(UTC) - timedelta(hours=2)
|
|
|
|
|
exchange = Exchange(
|
|
|
|
|
user_id=regular_user["user"]["id"],
|
|
|
|
|
slot_start=past_time,
|
|
|
|
|
slot_end=past_time + timedelta(minutes=15),
|
|
|
|
|
direction=TradeDirection.BUY,
|
2025-12-23 14:34:22 +01:00
|
|
|
bitcoin_transfer_method=BitcoinTransferMethod.ONCHAIN,
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
eur_amount=10000,
|
|
|
|
|
sats_amount=500000,
|
|
|
|
|
market_price_eur=20000.0,
|
|
|
|
|
agreed_price_eur=21000.0,
|
|
|
|
|
premium_percentage=5,
|
|
|
|
|
status=ExchangeStatus.BOOKED,
|
|
|
|
|
)
|
|
|
|
|
db.add(exchange)
|
|
|
|
|
await db.commit()
|
|
|
|
|
|
|
|
|
|
# Admin views past trades
|
|
|
|
|
async with client_factory.create(cookies=admin_user["cookies"]) as client:
|
|
|
|
|
response = await client.get("/api/admin/trades/past")
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert len(data) >= 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestAdminCompleteTrade:
|
|
|
|
|
"""Test admin completing trades."""
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_admin_can_complete_trade(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Admin can mark a past trade as completed."""
|
|
|
|
|
# Create a past trade in DB
|
|
|
|
|
async with client_factory.get_db_session() as db:
|
|
|
|
|
past_time = datetime.now(UTC) - timedelta(hours=1)
|
|
|
|
|
exchange = Exchange(
|
|
|
|
|
user_id=regular_user["user"]["id"],
|
|
|
|
|
slot_start=past_time,
|
|
|
|
|
slot_end=past_time + timedelta(minutes=15),
|
|
|
|
|
direction=TradeDirection.BUY,
|
2025-12-23 14:34:22 +01:00
|
|
|
bitcoin_transfer_method=BitcoinTransferMethod.ONCHAIN,
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
eur_amount=10000,
|
|
|
|
|
sats_amount=500000,
|
|
|
|
|
market_price_eur=20000.0,
|
|
|
|
|
agreed_price_eur=21000.0,
|
|
|
|
|
premium_percentage=5,
|
|
|
|
|
status=ExchangeStatus.BOOKED,
|
|
|
|
|
)
|
|
|
|
|
db.add(exchange)
|
|
|
|
|
await db.commit()
|
|
|
|
|
await db.refresh(exchange)
|
2025-12-23 17:03:51 +01:00
|
|
|
public_id = exchange.public_id
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
# Admin completes
|
|
|
|
|
async with client_factory.create(cookies=admin_user["cookies"]) as client:
|
2025-12-23 17:03:51 +01:00
|
|
|
response = await client.post(f"/api/admin/trades/{public_id}/complete")
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert data["status"] == "completed"
|
|
|
|
|
assert data["completed_at"] is not None
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_cannot_complete_future_trade(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Cannot complete a trade that hasn't started yet."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
# User books future trade
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
book_response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
2025-12-23 14:40:42 +01:00
|
|
|
"bitcoin_transfer_method": "onchain",
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
"eur_amount": 10000,
|
|
|
|
|
},
|
|
|
|
|
)
|
2025-12-23 17:03:51 +01:00
|
|
|
public_id = book_response.json()["public_id"]
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
# Admin tries to complete
|
|
|
|
|
async with client_factory.create(cookies=admin_user["cookies"]) as client:
|
2025-12-23 17:03:51 +01:00
|
|
|
response = await client.post(f"/api/admin/trades/{public_id}/complete")
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
assert "not yet started" in response.json()["detail"]
|
|
|
|
|
|
2025-12-23 11:00:32 +01:00
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_regular_user_cannot_complete_trade(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Regular user cannot complete trades (requires COMPLETE_EXCHANGE permission)."""
|
|
|
|
|
# Create a past trade in DB
|
|
|
|
|
async with client_factory.get_db_session() as db:
|
|
|
|
|
past_time = datetime.now(UTC) - timedelta(hours=1)
|
|
|
|
|
exchange = Exchange(
|
|
|
|
|
user_id=regular_user["user"]["id"],
|
|
|
|
|
slot_start=past_time,
|
|
|
|
|
slot_end=past_time + timedelta(minutes=15),
|
|
|
|
|
direction=TradeDirection.BUY,
|
2025-12-23 14:34:22 +01:00
|
|
|
bitcoin_transfer_method=BitcoinTransferMethod.ONCHAIN,
|
2025-12-23 11:00:32 +01:00
|
|
|
eur_amount=10000,
|
|
|
|
|
sats_amount=500000,
|
|
|
|
|
market_price_eur=20000.0,
|
|
|
|
|
agreed_price_eur=21000.0,
|
|
|
|
|
premium_percentage=5,
|
|
|
|
|
status=ExchangeStatus.BOOKED,
|
|
|
|
|
)
|
|
|
|
|
db.add(exchange)
|
|
|
|
|
await db.commit()
|
|
|
|
|
await db.refresh(exchange)
|
2025-12-23 17:03:51 +01:00
|
|
|
public_id = exchange.public_id
|
2025-12-23 11:00:32 +01:00
|
|
|
|
|
|
|
|
# Regular user tries to complete
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
2025-12-23 17:03:51 +01:00
|
|
|
response = await client.post(f"/api/admin/trades/{public_id}/complete")
|
2025-12-23 11:00:32 +01:00
|
|
|
|
|
|
|
|
assert response.status_code == 403
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_regular_user_cannot_mark_no_show(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Regular user cannot mark trades as no-show (requires COMPLETE_EXCHANGE permission)."""
|
|
|
|
|
# Create a past trade in DB
|
|
|
|
|
async with client_factory.get_db_session() as db:
|
|
|
|
|
past_time = datetime.now(UTC) - timedelta(hours=1)
|
|
|
|
|
exchange = Exchange(
|
|
|
|
|
user_id=regular_user["user"]["id"],
|
|
|
|
|
slot_start=past_time,
|
|
|
|
|
slot_end=past_time + timedelta(minutes=15),
|
|
|
|
|
direction=TradeDirection.BUY,
|
2025-12-23 14:34:22 +01:00
|
|
|
bitcoin_transfer_method=BitcoinTransferMethod.ONCHAIN,
|
2025-12-23 11:00:32 +01:00
|
|
|
eur_amount=10000,
|
|
|
|
|
sats_amount=500000,
|
|
|
|
|
market_price_eur=20000.0,
|
|
|
|
|
agreed_price_eur=21000.0,
|
|
|
|
|
premium_percentage=5,
|
|
|
|
|
status=ExchangeStatus.BOOKED,
|
|
|
|
|
)
|
|
|
|
|
db.add(exchange)
|
|
|
|
|
await db.commit()
|
|
|
|
|
await db.refresh(exchange)
|
2025-12-23 17:03:51 +01:00
|
|
|
public_id = exchange.public_id
|
2025-12-23 11:00:32 +01:00
|
|
|
|
|
|
|
|
# Regular user tries to mark as no-show
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
2025-12-23 17:03:51 +01:00
|
|
|
response = await client.post(f"/api/admin/trades/{public_id}/no-show")
|
2025-12-23 11:00:32 +01:00
|
|
|
|
|
|
|
|
assert response.status_code == 403
|
|
|
|
|
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
class TestAdminNoShowTrade:
|
|
|
|
|
"""Test admin marking trades as no-show."""
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_admin_can_mark_no_show(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Admin can mark a past trade as no-show."""
|
|
|
|
|
# Create a past trade in DB
|
|
|
|
|
async with client_factory.get_db_session() as db:
|
|
|
|
|
past_time = datetime.now(UTC) - timedelta(hours=1)
|
|
|
|
|
exchange = Exchange(
|
|
|
|
|
user_id=regular_user["user"]["id"],
|
|
|
|
|
slot_start=past_time,
|
|
|
|
|
slot_end=past_time + timedelta(minutes=15),
|
|
|
|
|
direction=TradeDirection.BUY,
|
2025-12-23 14:34:22 +01:00
|
|
|
bitcoin_transfer_method=BitcoinTransferMethod.ONCHAIN,
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
eur_amount=10000,
|
|
|
|
|
sats_amount=500000,
|
|
|
|
|
market_price_eur=20000.0,
|
|
|
|
|
agreed_price_eur=21000.0,
|
|
|
|
|
premium_percentage=5,
|
|
|
|
|
status=ExchangeStatus.BOOKED,
|
|
|
|
|
)
|
|
|
|
|
db.add(exchange)
|
|
|
|
|
await db.commit()
|
|
|
|
|
await db.refresh(exchange)
|
2025-12-23 17:03:51 +01:00
|
|
|
public_id = exchange.public_id
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
# Admin marks no-show
|
|
|
|
|
async with client_factory.create(cookies=admin_user["cookies"]) as client:
|
2025-12-23 17:03:51 +01:00
|
|
|
response = await client.post(f"/api/admin/trades/{public_id}/no-show")
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert data["status"] == "no_show"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestAdminCancelTrade:
|
|
|
|
|
"""Test admin cancelling trades."""
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_admin_can_cancel_trade(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Admin can cancel any trade."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
# User books
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
book_response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
2025-12-23 14:40:42 +01:00
|
|
|
"bitcoin_transfer_method": "onchain",
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
"eur_amount": 10000,
|
|
|
|
|
},
|
|
|
|
|
)
|
2025-12-23 17:03:51 +01:00
|
|
|
public_id = book_response.json()["public_id"]
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
# Admin cancels
|
|
|
|
|
async with client_factory.create(cookies=admin_user["cookies"]) as client:
|
2025-12-23 17:03:51 +01:00
|
|
|
response = await client.post(f"/api/admin/trades/{public_id}/cancel")
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert data["status"] == "cancelled_by_admin"
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_regular_user_cannot_admin_cancel(
|
|
|
|
|
self, client_factory, regular_user, admin_user
|
|
|
|
|
):
|
|
|
|
|
"""Regular user cannot use admin cancel endpoint."""
|
|
|
|
|
target_date = await setup_availability_and_price(client_factory, admin_user)
|
|
|
|
|
|
|
|
|
|
with mock_price_fetcher(20000.0):
|
|
|
|
|
# User books
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
book_response = await client.post(
|
|
|
|
|
"/api/exchange",
|
|
|
|
|
json={
|
|
|
|
|
"slot_start": f"{target_date}T09:00:00Z",
|
|
|
|
|
"direction": "buy",
|
2025-12-23 14:40:42 +01:00
|
|
|
"bitcoin_transfer_method": "onchain",
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
"eur_amount": 10000,
|
|
|
|
|
},
|
|
|
|
|
)
|
2025-12-23 17:03:51 +01:00
|
|
|
public_id = book_response.json()["public_id"]
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
# User tries admin cancel
|
2025-12-23 17:03:51 +01:00
|
|
|
response = await client.post(f"/api/admin/trades/{public_id}/cancel")
|
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel
Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
|
|
|
|
|
|
|
|
assert response.status_code == 403
|
2025-12-23 10:55:44 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# User Search Tests
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestAdminUserSearch:
|
|
|
|
|
"""Test admin user search endpoint."""
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_admin_can_search_users(
|
|
|
|
|
self, client_factory, admin_user, regular_user
|
|
|
|
|
):
|
|
|
|
|
"""Admin can search for users by email."""
|
|
|
|
|
async with client_factory.create(cookies=admin_user["cookies"]) as client:
|
|
|
|
|
# Search for the regular user
|
|
|
|
|
response = await client.get(
|
|
|
|
|
f"/api/admin/users/search?q={regular_user['user']['email'][:5]}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert isinstance(data, list)
|
|
|
|
|
# Should find the regular user
|
|
|
|
|
emails = [u["email"] for u in data]
|
|
|
|
|
assert regular_user["user"]["email"] in emails
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_search_returns_limited_results(self, client_factory, admin_user):
|
|
|
|
|
"""Search results are limited to 10."""
|
|
|
|
|
async with client_factory.create(cookies=admin_user["cookies"]) as client:
|
|
|
|
|
# Search with a common pattern
|
|
|
|
|
response = await client.get("/api/admin/users/search?q=@")
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert len(data) <= 10
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_regular_user_cannot_search(self, client_factory, regular_user):
|
|
|
|
|
"""Regular user cannot access user search."""
|
|
|
|
|
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
|
|
|
|
response = await client.get("/api/admin/users/search?q=test")
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 403
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_search_requires_query(self, client_factory, admin_user):
|
|
|
|
|
"""Search requires a query parameter."""
|
|
|
|
|
async with client_factory.create(cookies=admin_user["cookies"]) as client:
|
|
|
|
|
response = await client.get("/api/admin/users/search")
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 422 # Validation error
|