Step 3: Add admin API endpoints for pricing configuration

- Add PricingConfigResponse and PricingConfigUpdate schemas
- Create PricingService with validation logic
- Add GET and PUT endpoints in routes/pricing.py
- Add MANAGE_PRICING permission to admin role
- Register pricing router in main.py
- Add comprehensive API tests for permissions and validation
This commit is contained in:
counterweight 2025-12-26 20:13:24 +01:00
parent 74b934135a
commit 4d0dad8e2b
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
8 changed files with 534 additions and 0 deletions

View file

@ -0,0 +1,359 @@
"""
Pricing API Tests
Tests for the admin pricing configuration endpoints.
"""
import pytest
from repositories.pricing import PricingRepository
class TestPricingPermissions:
"""Test that only admins can access pricing endpoints."""
@pytest.mark.asyncio
async def test_admin_can_get_pricing(self, client_factory, admin_user):
"""Admin can access GET pricing endpoint."""
# Seed pricing config first
async with client_factory.get_db_session() as db:
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,
)
async with client_factory.create(cookies=admin_user["cookies"]) as client:
response = await client.get("/api/admin/pricing")
assert response.status_code == 200
data = response.json()
assert "premium_buy" in data
assert "premium_sell" in data
assert data["premium_buy"] == 5
@pytest.mark.asyncio
async def test_regular_user_cannot_get_pricing(self, client_factory, regular_user):
"""Regular user cannot access GET pricing endpoint."""
async with client_factory.create(cookies=regular_user["cookies"]) as client:
response = await client.get("/api/admin/pricing")
assert response.status_code == 403
@pytest.mark.asyncio
async def test_unauthenticated_cannot_get_pricing(self, client):
"""Unauthenticated user cannot access GET pricing endpoint."""
response = await client.get("/api/admin/pricing")
assert response.status_code == 401
@pytest.mark.asyncio
async def test_admin_can_update_pricing(self, client_factory, admin_user):
"""Admin can access PUT pricing endpoint."""
# Seed pricing config first
async with client_factory.get_db_session() as db:
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,
)
async with client_factory.create(cookies=admin_user["cookies"]) as client:
response = await client.put(
"/api/admin/pricing",
json={
"premium_buy": 7,
"premium_sell": 6,
"small_trade_threshold_eur": 25000,
"small_trade_extra_premium": 3,
"eur_min_buy": 15000,
"eur_max_buy": 350000,
"eur_min_sell": 12000,
"eur_max_sell": 320000,
},
)
assert response.status_code == 200
data = response.json()
assert data["premium_buy"] == 7
assert data["premium_sell"] == 6
@pytest.mark.asyncio
async def test_regular_user_cannot_update_pricing(
self, client_factory, regular_user
):
"""Regular user cannot access PUT pricing endpoint."""
async with client_factory.create(cookies=regular_user["cookies"]) as client:
response = await client.put(
"/api/admin/pricing",
json={
"premium_buy": 7,
"premium_sell": 6,
"small_trade_threshold_eur": 25000,
"small_trade_extra_premium": 3,
"eur_min_buy": 15000,
"eur_max_buy": 350000,
"eur_min_sell": 12000,
"eur_max_sell": 320000,
},
)
assert response.status_code == 403
@pytest.mark.asyncio
async def test_unauthenticated_cannot_update_pricing(self, client):
"""Unauthenticated user cannot access PUT pricing endpoint."""
response = await client.put(
"/api/admin/pricing",
json={
"premium_buy": 7,
"premium_sell": 6,
"small_trade_threshold_eur": 25000,
"small_trade_extra_premium": 3,
"eur_min_buy": 15000,
"eur_max_buy": 350000,
"eur_min_sell": 12000,
"eur_max_sell": 320000,
},
)
assert response.status_code == 401
class TestPricingValidation:
"""Test validation of pricing configuration updates."""
@pytest.mark.asyncio
async def test_update_with_valid_data(self, client_factory, admin_user):
"""Update with valid data succeeds."""
# Seed pricing config first
async with client_factory.get_db_session() as db:
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,
)
async with client_factory.create(cookies=admin_user["cookies"]) as client:
response = await client.put(
"/api/admin/pricing",
json={
"premium_buy": 10,
"premium_sell": 8,
"small_trade_threshold_eur": 30000,
"small_trade_extra_premium": 5,
"eur_min_buy": 20000,
"eur_max_buy": 400000,
"eur_min_sell": 15000,
"eur_max_sell": 350000,
},
)
assert response.status_code == 200
data = response.json()
assert data["premium_buy"] == 10
assert data["premium_sell"] == 8
@pytest.mark.asyncio
async def test_premium_out_of_range_rejected(self, client_factory, admin_user):
"""Premium values outside -100 to 100 are rejected."""
# Seed pricing config first
async with client_factory.get_db_session() as db:
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,
)
async with client_factory.create(cookies=admin_user["cookies"]) as client:
# Test premium_buy > 100
response = await client.put(
"/api/admin/pricing",
json={
"premium_buy": 101,
"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,
},
)
assert response.status_code == 400
# Test premium_buy < -100
response = await client.put(
"/api/admin/pricing",
json={
"premium_buy": -101,
"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,
},
)
assert response.status_code == 400
@pytest.mark.asyncio
async def test_min_greater_than_max_rejected(self, client_factory, admin_user):
"""Min >= Max is rejected."""
# Seed pricing config first
async with client_factory.get_db_session() as db:
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,
)
async with client_factory.create(cookies=admin_user["cookies"]) as client:
# Test eur_min_buy >= eur_max_buy
response = await client.put(
"/api/admin/pricing",
json={
"premium_buy": 5,
"premium_sell": 5,
"small_trade_threshold_eur": 20000,
"small_trade_extra_premium": 2,
"eur_min_buy": 300000,
"eur_max_buy": 300000,
"eur_min_sell": 10000,
"eur_max_sell": 300000,
},
)
assert response.status_code == 400
@pytest.mark.asyncio
async def test_negative_amounts_rejected(self, client_factory, admin_user):
"""Negative amounts are rejected."""
# Seed pricing config first
async with client_factory.get_db_session() as db:
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,
)
async with client_factory.create(cookies=admin_user["cookies"]) as client:
# Test negative eur_min_buy
response = await client.put(
"/api/admin/pricing",
json={
"premium_buy": 5,
"premium_sell": 5,
"small_trade_threshold_eur": 20000,
"small_trade_extra_premium": 2,
"eur_min_buy": -1000,
"eur_max_buy": 300000,
"eur_min_sell": 10000,
"eur_max_sell": 300000,
},
)
assert response.status_code == 400
@pytest.mark.asyncio
async def test_zero_amounts_rejected(self, client_factory, admin_user):
"""Zero amounts are rejected."""
# Seed pricing config first
async with client_factory.get_db_session() as db:
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,
)
async with client_factory.create(cookies=admin_user["cookies"]) as client:
# Test zero eur_min_buy
response = await client.put(
"/api/admin/pricing",
json={
"premium_buy": 5,
"premium_sell": 5,
"small_trade_threshold_eur": 20000,
"small_trade_extra_premium": 2,
"eur_min_buy": 0,
"eur_max_buy": 300000,
"eur_min_sell": 10000,
"eur_max_sell": 300000,
},
)
assert response.status_code == 400
@pytest.mark.asyncio
async def test_get_when_no_config_returns_404(self, client_factory, admin_user):
"""GET returns 404 when no config exists."""
async with client_factory.create(cookies=admin_user["cookies"]) as client:
response = await client.get("/api/admin/pricing")
assert response.status_code == 404
@pytest.mark.asyncio
async def test_update_creates_config_if_none_exists(
self, client_factory, admin_user
):
"""PUT creates config if none exists."""
async with client_factory.create(cookies=admin_user["cookies"]) as client:
response = await client.put(
"/api/admin/pricing",
json={
"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,
},
)
assert response.status_code == 200
data = response.json()
assert data["premium_buy"] == 5
# Verify it was created
response = await client.get("/api/admin/pricing")
assert response.status_code == 200