mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-08-09 02:54:37 +00:00
the refactor part 02
This commit is contained in:
@@ -66,7 +66,7 @@ os.environ.update(test_env)
|
||||
os.environ.pop("ADMIN_PASSWORD", None)
|
||||
|
||||
|
||||
from routstr.core.db import ApiKey, get_session # noqa: E402
|
||||
from routstr.core.db import TemporaryCredit, get_session # noqa: E402
|
||||
from routstr.core.main import app, lifespan # noqa: E402
|
||||
|
||||
|
||||
@@ -280,7 +280,7 @@ class TestmintWallet:
|
||||
return 100000 # 100k sats
|
||||
|
||||
async def credit_balance(
|
||||
self, cashu_token: str, key: ApiKey, session: AsyncSession
|
||||
self, cashu_token: str, key: TemporaryCredit, session: AsyncSession
|
||||
) -> int:
|
||||
"""Credit balance to API key - test implementation"""
|
||||
try:
|
||||
@@ -301,9 +301,9 @@ class TestmintWallet:
|
||||
|
||||
# Use atomic update to avoid lost update problem in concurrent scenarios
|
||||
stmt = (
|
||||
update(ApiKey)
|
||||
.where(col(ApiKey.hashed_key) == key.hashed_key)
|
||||
.values(balance=ApiKey.balance + amount_msat)
|
||||
update(TemporaryCredit)
|
||||
.where(col(TemporaryCredit.hashed_key) == key.hashed_key)
|
||||
.values(balance=TemporaryCredit.balance + amount_msat)
|
||||
)
|
||||
await session.execute(stmt)
|
||||
await session.commit()
|
||||
@@ -390,7 +390,7 @@ class DatabaseSnapshot:
|
||||
async def capture(self) -> Dict[str, List[Dict]]:
|
||||
"""Capture current database state"""
|
||||
# Get all API keys with their data
|
||||
result = await self.session.execute(select(ApiKey))
|
||||
result = await self.session.execute(select(TemporaryCredit))
|
||||
api_keys = result.scalars().all()
|
||||
|
||||
snapshot = {
|
||||
@@ -398,10 +398,8 @@ class DatabaseSnapshot:
|
||||
{
|
||||
"hashed_key": key.hashed_key,
|
||||
"balance": key.balance,
|
||||
"total_spent": key.total_spent,
|
||||
"total_requests": key.total_requests,
|
||||
"refund_address": key.refund_address,
|
||||
"key_expiry_time": key.key_expiry_time,
|
||||
"refund_expiration_time": key.refund_expiration_time,
|
||||
}
|
||||
for key in api_keys
|
||||
]
|
||||
@@ -447,10 +445,8 @@ class DatabaseSnapshot:
|
||||
|
||||
for field in [
|
||||
"balance",
|
||||
"total_spent",
|
||||
"total_requests",
|
||||
"refund_address",
|
||||
"key_expiry_time",
|
||||
"refund_expiration_time",
|
||||
]:
|
||||
if old[field] != new[field]:
|
||||
changes[field] = {
|
||||
@@ -522,18 +518,17 @@ async def integration_app(
|
||||
with (
|
||||
patch("routstr.core.db.engine", integration_engine),
|
||||
patch.object(_settings, "cashu_mints", [mint_url]),
|
||||
patch("routstr.wallet.credit_balance", testmint_wallet.credit_balance),
|
||||
patch("routstr.wallet.send_token", testmint_wallet.send_token),
|
||||
patch("routstr.wallet.send_to_lnurl", testmint_wallet.send_to_lnurl),
|
||||
patch("routstr.wallet.recieve_token", testmint_wallet.redeem_token),
|
||||
patch("routstr.wallet.get_balance", testmint_wallet.get_balance),
|
||||
patch("routstr.payment.wallet.credit_balance", testmint_wallet.credit_balance),
|
||||
patch("routstr.payment.wallet.send_token", testmint_wallet.send_token),
|
||||
patch("routstr.payment.wallet.recieve_token", testmint_wallet.redeem_token),
|
||||
patch("routstr.payment.wallet.get_balance", testmint_wallet.get_balance),
|
||||
patch("routstr.balance.send_token", testmint_wallet.send_token),
|
||||
patch("routstr.balance.send_to_lnurl", testmint_wallet.send_to_lnurl),
|
||||
patch("websockets.connect") as mock_websockets,
|
||||
patch("routstr.payment.price.btc_usd_price", return_value=50000.0),
|
||||
patch("routstr.payment.price.sats_usd_price", return_value=0.0005),
|
||||
patch(
|
||||
"routstr.payment.helpers.calculate_discounted_max_cost",
|
||||
"routstr.payment.cost.calculate_discounted_max_cost",
|
||||
side_effect=_passthrough_discount,
|
||||
),
|
||||
):
|
||||
@@ -705,8 +700,8 @@ async def background_tasks_controller() -> AsyncGenerator[Any, None]:
|
||||
original_periodic_payout: Optional[Callable] = None
|
||||
|
||||
try:
|
||||
from routstr.payment.models import update_sats_pricing
|
||||
from routstr.wallet import periodic_payout
|
||||
from routstr.models.models import update_sats_pricing
|
||||
from routstr.payment.wallet import periodic_payout
|
||||
|
||||
async def controlled_update_pricing() -> None:
|
||||
while not controller.cancelled:
|
||||
|
||||
@@ -9,9 +9,9 @@ from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from routstr.core.db import ApiKey
|
||||
from routstr.payment.models import Model, Pricing, update_sats_pricing
|
||||
from routstr.wallet import periodic_payout
|
||||
from routstr.core.db import TemporaryCredit
|
||||
from routstr.models.models import Model, Pricing, update_sats_pricing
|
||||
from routstr.payment.wallet import periodic_payout
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -201,12 +201,12 @@ class TestRefundCheckTask:
|
||||
) -> None:
|
||||
"""Test that expired keys with balance and refund address are refunded"""
|
||||
# Create an expired API key with balance
|
||||
expired_key = ApiKey(
|
||||
expired_key = TemporaryCredit(
|
||||
hashed_key="expired_test_key",
|
||||
balance=5000, # 5 sats in msats
|
||||
refund_address="lnurl1test",
|
||||
key_expiry_time=int(time.time()) - 3600, # Expired 1 hour ago
|
||||
created_at=datetime.utcnow() - timedelta(days=1),
|
||||
refund_expiration_time=int(time.time()) - 3600, # Expired 1 hour ago
|
||||
created=datetime.utcnow() - timedelta(days=1),
|
||||
)
|
||||
integration_session.add(expired_key)
|
||||
await integration_session.commit()
|
||||
@@ -214,7 +214,7 @@ class TestRefundCheckTask:
|
||||
# Mock the wallet send_to_lnurl method and get_session
|
||||
with (
|
||||
patch(
|
||||
"routstr.wallet.send_to_lnurl", AsyncMock(return_value=5)
|
||||
"routstr.payment.lnurl.send_to_lnurl", AsyncMock(return_value=5)
|
||||
) as mock_send_to_lnurl,
|
||||
patch("routstr.core.db.get_session") as mock_get_session,
|
||||
):
|
||||
@@ -233,8 +233,8 @@ class TestRefundCheckTask:
|
||||
if (
|
||||
expired_key.balance > 0
|
||||
and expired_key.refund_address
|
||||
and expired_key.key_expiry_time
|
||||
and expired_key.key_expiry_time < current_time
|
||||
and expired_key.refund_expiration_time
|
||||
and expired_key.refund_expiration_time < current_time
|
||||
):
|
||||
# Call wallet send_to_lnurl to trigger the refund
|
||||
amount_sats = expired_key.balance // 1000
|
||||
@@ -258,12 +258,12 @@ class TestRefundCheckTask:
|
||||
"""Test that refund check continues after mint errors"""
|
||||
# Create multiple expired keys
|
||||
for i in range(3):
|
||||
key = ApiKey(
|
||||
key = TemporaryCredit(
|
||||
hashed_key=f"expired_key_{i}",
|
||||
balance=1000 * (i + 1),
|
||||
refund_address=f"lnurl{i}",
|
||||
key_expiry_time=int(time.time()) - 3600,
|
||||
created_at=datetime.utcnow(),
|
||||
refund_expiration_time=int(time.time()) - 3600,
|
||||
created=datetime.utcnow(),
|
||||
)
|
||||
integration_session.add(key)
|
||||
await integration_session.commit()
|
||||
@@ -279,7 +279,7 @@ class TestRefundCheckTask:
|
||||
|
||||
with (
|
||||
patch(
|
||||
"routstr.wallet.send_to_lnurl", mock_send_to_lnurl
|
||||
"routstr.payment.lnurl.send_to_lnurl", mock_send_to_lnurl
|
||||
) as mock_send_to_lnurl_patch,
|
||||
patch("routstr.core.db.get_session") as mock_get_session,
|
||||
):
|
||||
@@ -293,15 +293,15 @@ class TestRefundCheckTask:
|
||||
current_time = int(time.time())
|
||||
from sqlalchemy import select as sa_select
|
||||
|
||||
result = await integration_session.execute(sa_select(ApiKey))
|
||||
result = await integration_session.execute(sa_select(TemporaryCredit))
|
||||
keys = result.scalars().all()
|
||||
|
||||
for key in keys:
|
||||
if (
|
||||
key.balance > 0
|
||||
and key.refund_address
|
||||
and key.key_expiry_time
|
||||
and key.key_expiry_time < current_time
|
||||
and key.refund_expiration_time
|
||||
and key.refund_expiration_time < current_time
|
||||
):
|
||||
amount_sats = key.balance // 1000
|
||||
try:
|
||||
@@ -352,21 +352,21 @@ class TestRefundCheckTask:
|
||||
|
||||
current_time = int(time.time())
|
||||
for data in keys_data:
|
||||
key = ApiKey(
|
||||
key = TemporaryCredit(
|
||||
hashed_key=data["hashed_key"],
|
||||
balance=data["balance"],
|
||||
refund_address=data["refund_address"],
|
||||
key_expiry_time=current_time - 3600
|
||||
refund_expiration_time=current_time - 3600
|
||||
if data["expired"]
|
||||
else current_time + 3600,
|
||||
created_at=datetime.utcnow(),
|
||||
created=datetime.utcnow(),
|
||||
)
|
||||
integration_session.add(key)
|
||||
await integration_session.commit()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"routstr.wallet.send_to_lnurl", AsyncMock(return_value=1)
|
||||
"routstr.payment.lnurl.send_to_lnurl", AsyncMock(return_value=1)
|
||||
) as mock_send_to_lnurl,
|
||||
patch("routstr.core.db.get_session") as mock_get_session,
|
||||
):
|
||||
@@ -382,15 +382,15 @@ class TestRefundCheckTask:
|
||||
current_time = int(time.time())
|
||||
from sqlalchemy import select as sa_select
|
||||
|
||||
result = await integration_session.execute(sa_select(ApiKey))
|
||||
result = await integration_session.execute(sa_select(TemporaryCredit))
|
||||
keys = result.scalars().all()
|
||||
|
||||
for key in keys:
|
||||
if (
|
||||
key.balance > 0
|
||||
and key.refund_address
|
||||
and key.key_expiry_time
|
||||
and key.key_expiry_time < current_time
|
||||
and key.refund_expiration_time
|
||||
and key.refund_expiration_time < current_time
|
||||
):
|
||||
amount_sats = key.balance // 1000
|
||||
await mock_send_to_lnurl(key.refund_address, amount=amount_sats)
|
||||
@@ -410,7 +410,7 @@ class TestRefundCheckTask:
|
||||
# Check final state
|
||||
from sqlalchemy import select as sa_select
|
||||
|
||||
result = await integration_session.execute(sa_select(ApiKey))
|
||||
result = await integration_session.execute(sa_select(TemporaryCredit))
|
||||
remaining_keys_list = result.scalars().all()
|
||||
remaining_ids = [k.hashed_key for k in remaining_keys_list]
|
||||
|
||||
@@ -454,10 +454,10 @@ class TestPeriodicPayoutTask:
|
||||
for i in range(5):
|
||||
balance = 10000 * (i + 1) # 10, 20, 30, 40, 50 sats
|
||||
total_user_balance += balance
|
||||
key = ApiKey(
|
||||
key = TemporaryCredit(
|
||||
hashed_key=f"user_key_{i}",
|
||||
balance=balance,
|
||||
created_at=datetime.utcnow(),
|
||||
created=datetime.utcnow(),
|
||||
)
|
||||
integration_session.add(key)
|
||||
await integration_session.commit()
|
||||
@@ -466,9 +466,12 @@ class TestPeriodicPayoutTask:
|
||||
wallet_balance = 200000 # 200 sats total
|
||||
|
||||
with (
|
||||
patch("routstr.wallet.get_balance", AsyncMock(return_value=wallet_balance)),
|
||||
patch(
|
||||
"routstr.wallet.send_to_lnurl", AsyncMock(return_value=None)
|
||||
"routstr.payment.wallet.get_balance",
|
||||
AsyncMock(return_value=wallet_balance),
|
||||
),
|
||||
patch(
|
||||
"routstr.payment.lnurl.send_to_lnurl", AsyncMock(return_value=None)
|
||||
) as mock_send_to_lnurl,
|
||||
):
|
||||
# Mock environment variables
|
||||
@@ -481,7 +484,7 @@ class TestPeriodicPayoutTask:
|
||||
},
|
||||
):
|
||||
# Call periodic_payout directly (pay_out was renamed/refactored)
|
||||
from routstr.wallet import periodic_payout
|
||||
from routstr.payment.wallet import periodic_payout
|
||||
|
||||
await periodic_payout()
|
||||
|
||||
@@ -501,7 +504,7 @@ class TestPeriodicPayoutTask:
|
||||
# key = ApiKey(
|
||||
# hashed_key="single_user",
|
||||
# balance=50000, # 50 sats
|
||||
# created_at=datetime.utcnow(),
|
||||
# created=datetime.utcnow(),
|
||||
# )
|
||||
# integration_session.add(key)
|
||||
# await integration_session.commit()
|
||||
@@ -538,7 +541,7 @@ class TestPeriodicPayoutTask:
|
||||
# key = ApiKey(
|
||||
# hashed_key="low_revenue_user",
|
||||
# balance=95000, # 95 sats
|
||||
# created_at=datetime.utcnow(),
|
||||
# created=datetime.utcnow(),
|
||||
# )
|
||||
# integration_session.add(key)
|
||||
# await integration_session.commit()
|
||||
@@ -648,14 +651,14 @@ class TestTaskInteractions:
|
||||
"""Test that database operations don't deadlock during concurrent task execution"""
|
||||
# Create test data
|
||||
for i in range(10):
|
||||
key = ApiKey(
|
||||
key = TemporaryCredit(
|
||||
hashed_key=f"concurrent_key_{i}",
|
||||
balance=1000 * i,
|
||||
refund_address=f"lnurl{i}" if i % 2 == 0 else None,
|
||||
key_expiry_time=int(time.time()) - 3600
|
||||
refund_expiration_time=int(time.time()) - 3600
|
||||
if i % 3 == 0
|
||||
else int(time.time()) + 3600,
|
||||
created_at=datetime.utcnow(),
|
||||
created=datetime.utcnow(),
|
||||
)
|
||||
integration_session.add(key)
|
||||
await integration_session.commit()
|
||||
@@ -664,14 +667,14 @@ class TestTaskInteractions:
|
||||
async def read_operation() -> int:
|
||||
from sqlalchemy import select as sa_select
|
||||
|
||||
result = await integration_session.execute(sa_select(ApiKey))
|
||||
result = await integration_session.execute(sa_select(TemporaryCredit))
|
||||
return len(result.scalars().all())
|
||||
|
||||
async def write_operation(key_id: int) -> None:
|
||||
from sqlalchemy import select as sa_select
|
||||
|
||||
stmt = sa_select(ApiKey).where(
|
||||
ApiKey.hashed_key == f"concurrent_key_{key_id}" # type: ignore[arg-type]
|
||||
stmt = sa_select(TemporaryCredit).where(
|
||||
TemporaryCredit.hashed_key == f"concurrent_key_{key_id}" # type: ignore[arg-type]
|
||||
)
|
||||
result = await integration_session.execute(stmt)
|
||||
key = result.scalar_one_or_none()
|
||||
@@ -708,14 +711,16 @@ class TestTaskInteractions:
|
||||
# Patch the actual task functions
|
||||
with (
|
||||
patch(
|
||||
"routstr.payment.models.update_sats_pricing",
|
||||
"routstr.models.models.update_sats_pricing",
|
||||
lambda: task_with_cleanup("pricing"),
|
||||
),
|
||||
patch(
|
||||
"routstr.wallet.periodic_payout", lambda: task_with_cleanup("refund")
|
||||
"routstr.payment.wallet.periodic_payout",
|
||||
lambda: task_with_cleanup("refund"),
|
||||
),
|
||||
patch(
|
||||
"routstr.wallet.periodic_payout", lambda: task_with_cleanup("payout")
|
||||
"routstr.payment.wallet.periodic_payout",
|
||||
lambda: task_with_cleanup("payout"),
|
||||
),
|
||||
):
|
||||
# Start all tasks
|
||||
|
||||
@@ -11,7 +11,7 @@ from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlmodel import select
|
||||
|
||||
from routstr.core.db import ApiKey
|
||||
from routstr.core.db import TemporaryCredit
|
||||
|
||||
|
||||
class TestTransactionAtomicity:
|
||||
@@ -34,7 +34,7 @@ class TestTransactionAtomicity:
|
||||
api_key_header[3:] if api_key_header.startswith("sk-") else api_key_header
|
||||
)
|
||||
|
||||
stmt = select(ApiKey).where(ApiKey.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
stmt = select(TemporaryCredit).where(TemporaryCredit.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
result = await integration_session.execute(stmt)
|
||||
api_key = result.scalar_one()
|
||||
initial_balance = api_key.balance
|
||||
@@ -47,7 +47,9 @@ class TestTransactionAtomicity:
|
||||
try:
|
||||
# Get api key in new session
|
||||
result = await test_session.execute(
|
||||
select(ApiKey).where(ApiKey.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
select(TemporaryCredit).where(
|
||||
TemporaryCredit.hashed_key == api_key_hash
|
||||
) # type: ignore[arg-type]
|
||||
)
|
||||
test_api_key = result.scalar_one()
|
||||
|
||||
@@ -72,14 +74,14 @@ class TestTransactionAtomicity:
|
||||
|
||||
try:
|
||||
await integration_session.execute(
|
||||
update(ApiKey)
|
||||
.where(ApiKey.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
.values(balance=ApiKey.balance - 1000)
|
||||
update(TemporaryCredit)
|
||||
.where(TemporaryCredit.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
.values(balance=TemporaryCredit.balance - 1000)
|
||||
)
|
||||
# Force a constraint violation or error
|
||||
await integration_session.execute(
|
||||
update(ApiKey)
|
||||
.where(ApiKey.hashed_key == "non_existent_key") # type: ignore[arg-type]
|
||||
update(TemporaryCredit)
|
||||
.where(TemporaryCredit.hashed_key == "non_existent_key") # type: ignore[arg-type]
|
||||
.values(balance=-1) # This should fail
|
||||
)
|
||||
await integration_session.commit()
|
||||
@@ -108,13 +110,13 @@ class TestTransactionAtomicity:
|
||||
api_key_header[3:] if api_key_header.startswith("sk-") else api_key_header
|
||||
)
|
||||
|
||||
stmt = select(ApiKey).where(ApiKey.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
stmt = select(TemporaryCredit).where(TemporaryCredit.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
result = await integration_session.execute(stmt)
|
||||
api_key = result.scalar_one()
|
||||
initial_balance = api_key.balance
|
||||
|
||||
# Mock wallet to fail after token validation
|
||||
with patch("routstr.wallet.send_token") as mock_wallet_func:
|
||||
with patch("routstr.payment.wallet.send_token") as mock_wallet_func:
|
||||
mock_proof = MagicMock()
|
||||
mock_proof.amount = 1000
|
||||
mock_wallet = AsyncMock()
|
||||
@@ -158,7 +160,7 @@ class TestTransactionAtomicity:
|
||||
)
|
||||
|
||||
# Set a known balance
|
||||
stmt = select(ApiKey).where(ApiKey.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
stmt = select(TemporaryCredit).where(TemporaryCredit.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
result = await integration_session.execute(stmt)
|
||||
api_key = result.scalar_one()
|
||||
api_key.balance = 10000
|
||||
@@ -166,12 +168,12 @@ class TestTransactionAtomicity:
|
||||
|
||||
# Simulate concurrent balance updates through direct database operations
|
||||
async def update_balance(session: AsyncSession, amount: int) -> bool:
|
||||
stmt = select(ApiKey).where(ApiKey.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
stmt = select(TemporaryCredit).where(
|
||||
TemporaryCredit.hashed_key == api_key_hash
|
||||
) # type: ignore[arg-type]
|
||||
result = await session.execute(stmt)
|
||||
key = result.scalar_one()
|
||||
key.balance -= amount
|
||||
key.total_spent += amount
|
||||
key.total_requests += 1
|
||||
try:
|
||||
await session.commit()
|
||||
return True
|
||||
@@ -248,7 +250,7 @@ class TestConcurrentOperations:
|
||||
)
|
||||
|
||||
# Set initial balance
|
||||
stmt = select(ApiKey).where(ApiKey.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
stmt = select(TemporaryCredit).where(TemporaryCredit.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
result = await integration_session.execute(stmt)
|
||||
api_key = result.scalar_one()
|
||||
initial_balance = 5000
|
||||
@@ -256,7 +258,7 @@ class TestConcurrentOperations:
|
||||
await integration_session.commit()
|
||||
|
||||
# Mock wallet for topup
|
||||
with patch("routstr.wallet.send_token") as mock_wallet_func:
|
||||
with patch("routstr.payment.wallet.send_token") as mock_wallet_func:
|
||||
mock_proof = MagicMock()
|
||||
mock_proof.amount = 2000
|
||||
mock_wallet = AsyncMock()
|
||||
@@ -323,12 +325,10 @@ class TestConcurrentOperations:
|
||||
)
|
||||
|
||||
# Set a specific balance
|
||||
stmt = select(ApiKey).where(ApiKey.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
stmt = select(TemporaryCredit).where(TemporaryCredit.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
result = await integration_session.execute(stmt)
|
||||
api_key = result.scalar_one()
|
||||
api_key.balance = 1000
|
||||
api_key.total_spent = 0
|
||||
api_key.total_requests = 0
|
||||
await integration_session.commit()
|
||||
|
||||
# Create a controlled race condition scenario
|
||||
@@ -336,7 +336,9 @@ class TestConcurrentOperations:
|
||||
|
||||
async def check_and_update_balance() -> bool:
|
||||
# Read current balance
|
||||
stmt = select(ApiKey).where(ApiKey.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
stmt = select(TemporaryCredit).where(
|
||||
TemporaryCredit.hashed_key == api_key_hash
|
||||
) # type: ignore[arg-type]
|
||||
result = await integration_session.execute(stmt)
|
||||
current_api_key = result.scalar_one()
|
||||
current_balance = current_api_key.balance
|
||||
@@ -347,8 +349,6 @@ class TestConcurrentOperations:
|
||||
|
||||
# Try to update based on read value
|
||||
current_api_key.balance = current_balance - 100
|
||||
current_api_key.total_spent += 100
|
||||
current_api_key.total_requests += 1
|
||||
|
||||
try:
|
||||
await integration_session.commit()
|
||||
@@ -371,8 +371,6 @@ class TestConcurrentOperations:
|
||||
# Final balance should reflect successful updates
|
||||
expected_balance = 1000 - (successful_updates * 100)
|
||||
assert api_key.balance == expected_balance
|
||||
assert api_key.total_spent == successful_updates * 100
|
||||
assert api_key.total_requests == successful_updates
|
||||
|
||||
|
||||
class TestDataIntegrity:
|
||||
@@ -396,7 +394,7 @@ class TestDataIntegrity:
|
||||
)
|
||||
|
||||
# Set low balance
|
||||
stmt = select(ApiKey).where(ApiKey.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
stmt = select(TemporaryCredit).where(TemporaryCredit.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
result = await integration_session.execute(stmt)
|
||||
api_key = result.scalar_one()
|
||||
api_key.balance = 0
|
||||
@@ -431,9 +429,7 @@ class TestDataIntegrity:
|
||||
)
|
||||
|
||||
# Try to manually insert duplicate key with same hash
|
||||
duplicate_key = ApiKey(
|
||||
hashed_key=api_key_hash, balance=5000, total_spent=0, total_requests=0
|
||||
)
|
||||
duplicate_key = TemporaryCredit(hashed_key=api_key_hash, balance=5000)
|
||||
|
||||
integration_session.add(duplicate_key)
|
||||
|
||||
@@ -481,20 +477,13 @@ class TestDataIntegrity:
|
||||
api_key_header[3:] if api_key_header.startswith("sk-") else api_key_header
|
||||
)
|
||||
|
||||
stmt = select(ApiKey).where(ApiKey.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
stmt = select(TemporaryCredit).where(TemporaryCredit.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
result = await integration_session.execute(stmt)
|
||||
api_key = result.scalar_one()
|
||||
|
||||
# Test setting invalid values directly
|
||||
# These should maintain integrity
|
||||
assert api_key.balance >= 0
|
||||
assert api_key.total_spent >= 0
|
||||
assert api_key.total_requests >= 0
|
||||
|
||||
# Verify calculations are consistent
|
||||
if api_key.total_requests > 0:
|
||||
average_cost = api_key.total_spent / api_key.total_requests
|
||||
assert average_cost >= 0
|
||||
|
||||
|
||||
class TestPerformance:
|
||||
@@ -522,7 +511,7 @@ class TestPerformance:
|
||||
operation_times["select"].append((end - start) * 1000) # Convert to ms
|
||||
|
||||
# Test UPDATE performance (via topup)
|
||||
with patch("routstr.wallet.send_token") as mock_wallet_func:
|
||||
with patch("routstr.payment.wallet.send_token") as mock_wallet_func:
|
||||
mock_proof = MagicMock()
|
||||
mock_proof.amount = 100
|
||||
mock_wallet = AsyncMock()
|
||||
@@ -607,7 +596,7 @@ class TestPerformance:
|
||||
|
||||
# Primary key lookup should be fast
|
||||
start = time.time()
|
||||
stmt = select(ApiKey).where(ApiKey.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
stmt = select(TemporaryCredit).where(TemporaryCredit.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
result = await integration_session.execute(stmt)
|
||||
api_key = result.scalar_one()
|
||||
end = time.time()
|
||||
|
||||
@@ -11,7 +11,7 @@ from httpx import ASGITransport, AsyncClient, ConnectError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlmodel import select
|
||||
|
||||
from routstr.core.db import ApiKey
|
||||
from routstr.core.db import TemporaryCredit
|
||||
|
||||
|
||||
class TestNetworkFailureScenarios:
|
||||
@@ -27,7 +27,7 @@ class TestNetworkFailureScenarios:
|
||||
# Patch the wallet send function to simulate failure across all modules
|
||||
with (
|
||||
patch(
|
||||
"routstr.wallet.send_token",
|
||||
"routstr.payment.wallet.send_token",
|
||||
AsyncMock(side_effect=ConnectError("Mint service unavailable")),
|
||||
),
|
||||
patch(
|
||||
@@ -422,7 +422,7 @@ class TestRecoveryScenarios:
|
||||
api_key_header[3:] if api_key_header.startswith("sk-") else api_key_header
|
||||
)
|
||||
|
||||
stmt = select(ApiKey).where(ApiKey.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
stmt = select(TemporaryCredit).where(TemporaryCredit.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
result = await integration_session.execute(stmt)
|
||||
initial_key = result.scalar_one()
|
||||
initial_balance = initial_key.balance
|
||||
@@ -458,17 +458,15 @@ class TestRecoveryScenarios:
|
||||
api_key_header[3:] if api_key_header.startswith("sk-") else api_key_header
|
||||
)
|
||||
|
||||
stmt = select(ApiKey).where(ApiKey.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
stmt = select(TemporaryCredit).where(TemporaryCredit.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
result = await integration_session.execute(stmt)
|
||||
api_key = result.scalar_one()
|
||||
initial_balance = api_key.balance
|
||||
initial_requests = api_key.total_requests
|
||||
|
||||
# Simulate operations that might be interrupted
|
||||
try:
|
||||
# Start a transaction
|
||||
api_key.reserved_balance += 1000
|
||||
api_key.total_requests += 1
|
||||
# Don't commit - simulate crash
|
||||
raise Exception("Simulated database crash")
|
||||
except Exception:
|
||||
@@ -478,7 +476,6 @@ class TestRecoveryScenarios:
|
||||
# Verify state is consistent after "recovery"
|
||||
await integration_session.refresh(api_key)
|
||||
assert api_key.balance == initial_balance
|
||||
assert api_key.total_requests == initial_requests
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_state_consistency_after_failures(
|
||||
@@ -527,10 +524,7 @@ class TestRecoveryScenarios:
|
||||
for mod in diff["api_keys"]["modified"]:
|
||||
# Only acceptable changes are request counts
|
||||
for field, change in mod["changes"].items():
|
||||
if field == "total_requests":
|
||||
# Request count might increase
|
||||
assert change["delta"] >= 0
|
||||
elif field == "balance":
|
||||
if field == "balance":
|
||||
# Balance should not decrease from failed operations
|
||||
assert change["delta"] >= 0
|
||||
else:
|
||||
@@ -641,12 +635,10 @@ class TestEdgeCaseCombinations:
|
||||
api_key_hash = test_key[3:] # Remove sk- prefix
|
||||
|
||||
# Create the API key with only 500 msats (less than one request cost)
|
||||
new_key = ApiKey(
|
||||
new_key = TemporaryCredit(
|
||||
hashed_key=api_key_hash,
|
||||
balance=500, # Less than fixed cost per request (1000 msats)
|
||||
reserved_balance=0,
|
||||
total_spent=0,
|
||||
total_requests=0,
|
||||
)
|
||||
integration_session.add(new_key)
|
||||
await integration_session.commit()
|
||||
@@ -687,7 +679,7 @@ class TestEdgeCaseCombinations:
|
||||
assert insufficient_funds_count > 0
|
||||
|
||||
# Balance should never go negative
|
||||
stmt = select(ApiKey).where(ApiKey.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
stmt = select(TemporaryCredit).where(TemporaryCredit.hashed_key == api_key_hash) # type: ignore[arg-type]
|
||||
result = await integration_session.execute(stmt)
|
||||
final_key = result.scalar_one()
|
||||
assert final_key.balance >= 0
|
||||
|
||||
@@ -379,4 +379,4 @@ async def test_info_endpoints_response_consistency(
|
||||
# Model IDs should be the same
|
||||
first_ids = {m["id"] for m in first_models}
|
||||
response_ids = {m["id"] for m in models}
|
||||
assert first_ids == response_ids
|
||||
assert first_ids == response_ids
|
||||
|
||||
@@ -149,15 +149,13 @@ class TestPerformanceBaseline:
|
||||
"""Test database operation performance"""
|
||||
from sqlmodel import select
|
||||
|
||||
from routstr.core.db import ApiKey
|
||||
from routstr.core.db import TemporaryCredit
|
||||
|
||||
# Create test data
|
||||
for i in range(100):
|
||||
key = ApiKey(
|
||||
key = TemporaryCredit(
|
||||
hashed_key=f"test_key_{i}",
|
||||
balance=1000000,
|
||||
total_spent=0,
|
||||
total_requests=0,
|
||||
)
|
||||
integration_session.add(key)
|
||||
await integration_session.commit()
|
||||
@@ -168,7 +166,7 @@ class TestPerformanceBaseline:
|
||||
for _ in range(100):
|
||||
start = time.time()
|
||||
result = await integration_session.execute(
|
||||
select(ApiKey).where(ApiKey.balance > 0) # type: ignore[arg-type]
|
||||
select(TemporaryCredit).where(TemporaryCredit.balance > 0) # type: ignore[arg-type]
|
||||
)
|
||||
_ = result.all()
|
||||
duration = (time.time() - start) * 1000
|
||||
|
||||
@@ -9,7 +9,7 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
from routstr.discovery import _PROVIDERS_CACHE
|
||||
from routstr.nostr.discovery import _PROVIDERS_CACHE
|
||||
|
||||
from .utils import PerformanceValidator, ResponseValidator
|
||||
|
||||
@@ -62,9 +62,9 @@ async def test_providers_endpoint_default_response(
|
||||
}
|
||||
|
||||
with patch(
|
||||
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
"routstr.nostr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
):
|
||||
with patch("routstr.discovery.fetch_provider_health") as mock_fetch:
|
||||
with patch("routstr.nostr.discovery.fetch_provider_health") as mock_fetch:
|
||||
# Configure mock to return appropriate responses
|
||||
mock_fetch.side_effect = lambda url: mock_fetch_responses.get(
|
||||
url, {"status_code": 500, "json": {"error": "Unknown provider"}}
|
||||
@@ -126,9 +126,9 @@ async def test_providers_endpoint_with_include_json(
|
||||
}
|
||||
|
||||
with patch(
|
||||
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
"routstr.nostr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
):
|
||||
with patch("routstr.discovery.fetch_provider_health") as mock_fetch:
|
||||
with patch("routstr.nostr.discovery.fetch_provider_health") as mock_fetch:
|
||||
mock_fetch.return_value = {
|
||||
"status_code": 200,
|
||||
"json": mock_provider_response,
|
||||
@@ -200,9 +200,9 @@ async def test_providers_data_structure_validation(
|
||||
}
|
||||
|
||||
with patch(
|
||||
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
"routstr.nostr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
):
|
||||
with patch("routstr.discovery.fetch_provider_health") as mock_fetch:
|
||||
with patch("routstr.nostr.discovery.fetch_provider_health") as mock_fetch:
|
||||
mock_fetch.return_value = mock_health_response
|
||||
|
||||
response = await integration_client.get("/v1/providers/?include_json=true")
|
||||
@@ -247,7 +247,7 @@ async def test_providers_endpoint_no_providers_found(
|
||||
]
|
||||
|
||||
with patch(
|
||||
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
"routstr.nostr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
):
|
||||
response = await integration_client.get("/v1/providers/")
|
||||
|
||||
@@ -308,10 +308,10 @@ async def test_providers_endpoint_offline_providers(
|
||||
}
|
||||
|
||||
with patch(
|
||||
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
"routstr.nostr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
):
|
||||
with patch(
|
||||
"routstr.discovery.fetch_provider_health",
|
||||
"routstr.nostr.discovery.fetch_provider_health",
|
||||
side_effect=mock_fetch_provider_health,
|
||||
):
|
||||
response = await integration_client.get("/v1/providers/?include_json=true")
|
||||
@@ -377,9 +377,9 @@ async def test_providers_endpoint_duplicate_urls(
|
||||
]
|
||||
|
||||
with patch(
|
||||
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
"routstr.nostr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
):
|
||||
with patch("routstr.discovery.fetch_provider_health") as mock_fetch:
|
||||
with patch("routstr.nostr.discovery.fetch_provider_health") as mock_fetch:
|
||||
mock_fetch.return_value = {
|
||||
"status_code": 200,
|
||||
"endpoint": "root",
|
||||
@@ -416,7 +416,7 @@ async def test_providers_endpoint_nostr_relay_failures(
|
||||
raise Exception("Connection to relay failed")
|
||||
|
||||
with patch(
|
||||
"routstr.discovery.query_nostr_relay_for_providers", side_effect=failing_query
|
||||
"routstr.nostr.discovery.query_nostr_relay_for_providers", side_effect=failing_query
|
||||
):
|
||||
response = await integration_client.get("/v1/providers/")
|
||||
|
||||
@@ -454,9 +454,9 @@ async def test_providers_endpoint_malformed_urls(
|
||||
]
|
||||
|
||||
with patch(
|
||||
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
"routstr.nostr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
):
|
||||
with patch("routstr.discovery.fetch_provider_health") as mock_fetch:
|
||||
with patch("routstr.nostr.discovery.fetch_provider_health") as mock_fetch:
|
||||
mock_fetch.return_value = {"status_code": 200, "json": {"status": "online"}}
|
||||
|
||||
response = await integration_client.get("/v1/providers/")
|
||||
@@ -486,9 +486,9 @@ async def test_providers_endpoint_response_format(
|
||||
]
|
||||
|
||||
with patch(
|
||||
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
"routstr.nostr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
):
|
||||
with patch("routstr.discovery.fetch_provider_health") as mock_fetch:
|
||||
with patch("routstr.nostr.discovery.fetch_provider_health") as mock_fetch:
|
||||
mock_fetch.return_value = {"status_code": 200, "json": {"status": "online"}}
|
||||
|
||||
# Test default format
|
||||
@@ -536,9 +536,9 @@ async def test_providers_endpoint_performance(integration_client: AsyncClient) -
|
||||
validator = PerformanceValidator()
|
||||
|
||||
with patch(
|
||||
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
"routstr.nostr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
):
|
||||
with patch("routstr.discovery.fetch_provider_health") as mock_fetch:
|
||||
with patch("routstr.nostr.discovery.fetch_provider_health") as mock_fetch:
|
||||
mock_fetch.return_value = {"status_code": 200, "json": {"status": "online"}}
|
||||
|
||||
# Test multiple requests
|
||||
@@ -576,9 +576,9 @@ async def test_providers_endpoint_concurrent_requests(
|
||||
]
|
||||
|
||||
with patch(
|
||||
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
"routstr.nostr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
):
|
||||
with patch("routstr.discovery.fetch_provider_health") as mock_fetch:
|
||||
with patch("routstr.nostr.discovery.fetch_provider_health") as mock_fetch:
|
||||
mock_fetch.return_value = {"status_code": 200, "json": {"status": "online"}}
|
||||
|
||||
# Create concurrent requests
|
||||
@@ -618,9 +618,9 @@ async def test_providers_endpoint_parameter_validation(
|
||||
]
|
||||
|
||||
with patch(
|
||||
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
"routstr.nostr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
):
|
||||
with patch("routstr.discovery.fetch_provider_health") as mock_fetch:
|
||||
with patch("routstr.nostr.discovery.fetch_provider_health") as mock_fetch:
|
||||
mock_fetch.return_value = {"status_code": 200, "json": {"status": "online"}}
|
||||
|
||||
# Test various parameter values
|
||||
@@ -670,9 +670,9 @@ async def test_no_database_changes_during_provider_operations(
|
||||
]
|
||||
|
||||
with patch(
|
||||
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
"routstr.nostr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
):
|
||||
with patch("routstr.discovery.fetch_provider_health") as mock_fetch:
|
||||
with patch("routstr.nostr.discovery.fetch_provider_health") as mock_fetch:
|
||||
mock_fetch.return_value = {"status_code": 200, "json": {"status": "online"}}
|
||||
|
||||
# Make multiple requests with different parameters
|
||||
|
||||
@@ -14,7 +14,7 @@ import pytest
|
||||
from httpx import AsyncClient
|
||||
from sqlmodel import select
|
||||
|
||||
from routstr.core.db import ApiKey
|
||||
from routstr.core.db import TemporaryCredit
|
||||
|
||||
from .utils import (
|
||||
ConcurrencyTester,
|
||||
@@ -297,8 +297,8 @@ async def test_proxy_get_insufficient_balance(
|
||||
from sqlmodel import update
|
||||
|
||||
await integration_session.execute(
|
||||
update(ApiKey)
|
||||
.where(ApiKey.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
update(TemporaryCredit)
|
||||
.where(TemporaryCredit.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
.values(balance=100) # Only 0.1 sats
|
||||
)
|
||||
await integration_session.commit()
|
||||
@@ -382,7 +382,7 @@ async def test_proxy_get_database_state_verification(
|
||||
|
||||
# Get initial key state
|
||||
result = await integration_session.execute(
|
||||
select(ApiKey).where(ApiKey.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
select(TemporaryCredit).where(TemporaryCredit.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
)
|
||||
initial_key = result.scalar_one()
|
||||
initial_balance = initial_key.balance
|
||||
|
||||
@@ -7,7 +7,7 @@ import pytest
|
||||
from httpx import AsyncClient
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from routstr.core.db import ApiKey, create_session
|
||||
from routstr.core.db import TemporaryCredit, create_session
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -16,7 +16,7 @@ async def test_reserved_balance_never_negative(integration_client: AsyncClient)
|
||||
|
||||
# Create a test API key with limited balance
|
||||
async with create_session() as session:
|
||||
test_key = ApiKey(
|
||||
test_key = TemporaryCredit(
|
||||
hashed_key="test_reserved_balance_key",
|
||||
balance=1000, # 1 sat
|
||||
reserved_balance=0,
|
||||
@@ -40,7 +40,7 @@ async def test_reserved_balance_never_negative(integration_client: AsyncClient)
|
||||
|
||||
# Check reserved balance after failed request
|
||||
async with create_session() as session:
|
||||
key = await session.get(ApiKey, "test_reserved_balance_key")
|
||||
key = await session.get(TemporaryCredit, "test_reserved_balance_key")
|
||||
assert key is not None
|
||||
assert key.reserved_balance >= 0, (
|
||||
f"Reserved balance went negative: {key.reserved_balance}"
|
||||
@@ -69,7 +69,7 @@ async def test_reserved_balance_never_negative(integration_client: AsyncClient)
|
||||
|
||||
# Check final state
|
||||
async with create_session() as session:
|
||||
key = await session.get(ApiKey, "test_reserved_balance_key")
|
||||
key = await session.get(TemporaryCredit, "test_reserved_balance_key")
|
||||
assert key is not None
|
||||
assert key.reserved_balance >= 0, (
|
||||
f"Reserved balance went negative after concurrent requests: {key.reserved_balance}"
|
||||
@@ -86,7 +86,7 @@ async def test_reserved_balance_with_successful_requests(
|
||||
# Create a test API key with more balance
|
||||
async with create_session() as session:
|
||||
unique_key = f"test_successful_key_{uuid.uuid4().hex[:8]}"
|
||||
test_key = ApiKey(
|
||||
test_key = TemporaryCredit(
|
||||
hashed_key=unique_key,
|
||||
balance=100000, # 100 sats
|
||||
reserved_balance=0,
|
||||
@@ -111,24 +111,23 @@ async def test_reserved_balance_with_successful_requests(
|
||||
|
||||
# Check that reserved balance was properly adjusted
|
||||
async with create_session() as session:
|
||||
key = await session.get(ApiKey, unique_key)
|
||||
key = await session.get(TemporaryCredit, unique_key)
|
||||
assert key is not None
|
||||
assert key.reserved_balance >= 0, (
|
||||
f"Reserved balance went negative: {key.reserved_balance}"
|
||||
)
|
||||
# Check if the request was processed (might fail due to model pricing in test env)
|
||||
# The important part is that reserved_balance doesn't go negative
|
||||
if key.total_spent > 0:
|
||||
assert key.balance < 100000, (
|
||||
"Balance should decrease after successful request"
|
||||
)
|
||||
if key.balance < 100000:
|
||||
# Balance decreased
|
||||
pass
|
||||
else:
|
||||
# Request failed, but reserved balance should still be non-negative
|
||||
assert key.balance == 100000, (
|
||||
"Balance should remain unchanged if request failed"
|
||||
)
|
||||
print(
|
||||
f"After successful request - Balance: {key.balance}, Reserved: {key.reserved_balance}, Spent: {key.total_spent}"
|
||||
f"After successful request - Balance: {key.balance}, Reserved: {key.reserved_balance}"
|
||||
)
|
||||
|
||||
|
||||
@@ -137,11 +136,11 @@ async def test_insufficient_reserved_balance_for_revert(
|
||||
integration_session: AsyncSession,
|
||||
) -> None:
|
||||
"""Test revert_pay_for_request behavior with insufficient reserved balance."""
|
||||
from routstr.auth import revert_pay_for_request
|
||||
from routstr.payment.helpers import revert_pay_for_request
|
||||
|
||||
# Create key with zero reserved balance
|
||||
unique_key = f"test_revert_key_{uuid.uuid4().hex[:8]}"
|
||||
test_key = ApiKey(
|
||||
test_key = TemporaryCredit(
|
||||
hashed_key=unique_key,
|
||||
balance=1000,
|
||||
reserved_balance=0,
|
||||
@@ -160,6 +159,3 @@ async def test_insufficient_reserved_balance_for_revert(
|
||||
assert test_key.reserved_balance == -100, (
|
||||
f"Expected reserved_balance to be -100, got: {test_key.reserved_balance}"
|
||||
)
|
||||
assert test_key.total_requests == -1, (
|
||||
f"Expected total_requests to be -1, got: {test_key.total_requests}"
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ import pytest
|
||||
from httpx import AsyncClient
|
||||
from sqlmodel import select
|
||||
|
||||
from routstr.core.db import ApiKey
|
||||
from routstr.core.db import TemporaryCredit
|
||||
|
||||
from .utils import (
|
||||
CashuTokenGenerator,
|
||||
@@ -55,13 +55,11 @@ async def test_api_key_generation_valid_token(
|
||||
# Verify database state directly
|
||||
hashed_key = api_key[3:] # Remove "sk-" prefix
|
||||
result = await integration_session.execute(
|
||||
select(ApiKey).where(ApiKey.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
select(TemporaryCredit).where(TemporaryCredit.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
)
|
||||
db_key = result.scalar_one()
|
||||
|
||||
assert db_key.balance == amount * 1000
|
||||
assert db_key.total_spent == 0
|
||||
assert db_key.total_requests == 0
|
||||
|
||||
# Verify the API key can be used for authentication
|
||||
integration_client.headers["Authorization"] = f"Bearer {api_key}"
|
||||
@@ -93,15 +91,15 @@ async def test_api_key_generation_invalid_token(
|
||||
integration_client.headers["Authorization"] = f"Bearer {invalid_token}"
|
||||
response = await integration_client.get("/v1/wallet/info")
|
||||
|
||||
# Should fail with 401
|
||||
assert response.status_code == 401, (
|
||||
f"Token {invalid_token[:20]}... should be invalid"
|
||||
# Should fail with 401 or 400
|
||||
assert response.status_code in [400, 401], (
|
||||
f"Token {invalid_token[:20]}... should be invalid (400 or 401)"
|
||||
)
|
||||
|
||||
# Validate error response
|
||||
validator = ResponseValidator()
|
||||
error_validation = validator.validate_error_response(
|
||||
response, expected_status=401, expected_error_key="detail"
|
||||
response, expected_status=response.status_code, expected_error_key="detail"
|
||||
)
|
||||
assert error_validation["valid"]
|
||||
|
||||
@@ -169,7 +167,7 @@ async def test_authorization_header_validation(
|
||||
valid_api_key = response.json()["api_key"]
|
||||
|
||||
# Test scenarios
|
||||
test_cases = [
|
||||
test_cases: list[tuple[dict[str, str], int, str]] = [
|
||||
# (headers, expected_status, description)
|
||||
(
|
||||
{},
|
||||
@@ -193,7 +191,7 @@ async def test_authorization_header_validation(
|
||||
integration_client.headers.pop("authorization", None)
|
||||
|
||||
# Set test headers
|
||||
integration_client.headers.update(headers)
|
||||
integration_client.headers.update(headers) # type: ignore[arg-type]
|
||||
|
||||
# Make request to protected endpoint
|
||||
response = await integration_client.get("/v1/wallet/")
|
||||
@@ -256,16 +254,14 @@ async def test_database_state_api_key_creation(
|
||||
# Verify database record
|
||||
hashed_key = api_key[3:] # Remove "sk-" prefix
|
||||
result = await integration_session.execute(
|
||||
select(ApiKey).where(ApiKey.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
select(TemporaryCredit).where(TemporaryCredit.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
)
|
||||
db_key = result.scalar_one()
|
||||
|
||||
# Validate stored data
|
||||
assert db_key.balance == amount * 1000 # msats
|
||||
assert db_key.total_spent == 0
|
||||
assert db_key.total_requests == 0
|
||||
assert db_key.refund_address is None
|
||||
assert db_key.key_expiry_time is None
|
||||
assert db_key.refund_expiration_time is None
|
||||
|
||||
# Creation timestamp should be recent (within last minute)
|
||||
# Note: The model doesn't have a creation timestamp field,
|
||||
@@ -446,7 +442,7 @@ async def test_concurrent_token_submissions(
|
||||
for api_key in api_keys:
|
||||
hashed_key = api_key[3:] # Remove "sk-" prefix
|
||||
result = await integration_session.execute(
|
||||
select(ApiKey).where(ApiKey.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
select(TemporaryCredit).where(TemporaryCredit.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
)
|
||||
db_key = result.scalar_one()
|
||||
assert db_key.balance == expected_balances[hashed_key]
|
||||
@@ -568,7 +564,7 @@ async def test_database_timestamp_accuracy(
|
||||
# Verify key exists in database
|
||||
hashed_key = api_key[3:] # Remove "sk-" prefix
|
||||
result = await integration_session.execute(
|
||||
select(ApiKey).where(ApiKey.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
select(TemporaryCredit).where(TemporaryCredit.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
)
|
||||
db_key = result.scalar_one()
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import pytest
|
||||
from httpx import AsyncClient
|
||||
from sqlmodel import select, update
|
||||
|
||||
from routstr.core.db import ApiKey
|
||||
from routstr.core.db import TemporaryCredit
|
||||
|
||||
from .utils import ConcurrencyTester, ResponseValidator
|
||||
|
||||
@@ -49,7 +49,7 @@ async def test_wallet_endpoint_with_valid_api_key(
|
||||
hashed_key = api_key[3:] # Remove "sk-" prefix
|
||||
|
||||
result = await integration_session.execute(
|
||||
select(ApiKey).where(ApiKey.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
select(TemporaryCredit).where(TemporaryCredit.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
)
|
||||
db_key = result.scalar_one()
|
||||
|
||||
@@ -138,7 +138,9 @@ async def test_wallet_with_zero_balance(
|
||||
# Manually set balance to zero in database
|
||||
hashed_key = api_key[3:] # Remove "sk-" prefix
|
||||
await integration_session.execute(
|
||||
update(ApiKey).where(ApiKey.hashed_key == hashed_key).values(balance=0) # type: ignore[arg-type]
|
||||
update(TemporaryCredit)
|
||||
.where(TemporaryCredit.hashed_key == hashed_key)
|
||||
.values(balance=0) # type: ignore[arg-type]
|
||||
)
|
||||
await integration_session.commit()
|
||||
|
||||
@@ -181,9 +183,11 @@ async def test_expired_api_key_behavior(
|
||||
from sqlmodel import update
|
||||
|
||||
await integration_session.execute(
|
||||
update(ApiKey)
|
||||
.where(ApiKey.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
.values(key_expiry_time=past_expiry, refund_address="test@lightning.address")
|
||||
update(TemporaryCredit)
|
||||
.where(TemporaryCredit.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
.values(
|
||||
refund_expiration_time=past_expiry, refund_address="test@lightning.address"
|
||||
)
|
||||
)
|
||||
await integration_session.commit()
|
||||
|
||||
@@ -197,10 +201,10 @@ async def test_expired_api_key_behavior(
|
||||
# Verify expiry time was stored
|
||||
hashed_key = api_key[3:] # Remove "sk-" prefix
|
||||
result = await integration_session.execute(
|
||||
select(ApiKey).where(ApiKey.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
select(TemporaryCredit).where(TemporaryCredit.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
)
|
||||
db_key = result.scalar_one()
|
||||
assert db_key.key_expiry_time == past_expiry
|
||||
assert db_key.refund_expiration_time == past_expiry
|
||||
assert db_key.refund_address == "test@lightning.address"
|
||||
|
||||
|
||||
@@ -270,7 +274,7 @@ async def test_wallet_info_data_consistency(
|
||||
# Verify against database
|
||||
hashed_key = api_key[3:] # Remove "sk-" prefix
|
||||
result = await integration_session.execute(
|
||||
select(ApiKey).where(ApiKey.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
select(TemporaryCredit).where(TemporaryCredit.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
)
|
||||
db_key = result.scalar_one()
|
||||
|
||||
@@ -360,12 +364,10 @@ async def test_wallet_after_partial_spending(
|
||||
hashed_key = api_key[3:] # Remove "sk-" prefix
|
||||
|
||||
await integration_session.execute(
|
||||
update(ApiKey)
|
||||
.where(ApiKey.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
update(TemporaryCredit)
|
||||
.where(TemporaryCredit.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
.values(
|
||||
balance=initial_balance - spent_amount,
|
||||
total_spent=spent_amount,
|
||||
total_requests=5, # Simulate 5 requests
|
||||
)
|
||||
)
|
||||
await integration_session.commit()
|
||||
|
||||
@@ -13,7 +13,7 @@ import pytest
|
||||
from httpx import AsyncClient
|
||||
from sqlmodel import select
|
||||
|
||||
from routstr.core.db import ApiKey
|
||||
from routstr.core.db import TemporaryCredit
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@@ -123,7 +123,9 @@ async def test_zero_balance_refund_handling(
|
||||
from sqlmodel import update
|
||||
|
||||
await integration_session.execute(
|
||||
update(ApiKey).where(ApiKey.hashed_key == hashed_key).values(balance=0) # type: ignore[arg-type]
|
||||
update(TemporaryCredit)
|
||||
.where(TemporaryCredit.hashed_key == hashed_key)
|
||||
.values(balance=0) # type: ignore[arg-type]
|
||||
)
|
||||
await integration_session.commit()
|
||||
|
||||
@@ -136,7 +138,7 @@ async def test_zero_balance_refund_handling(
|
||||
|
||||
# Key should still exist
|
||||
result = await integration_session.execute(
|
||||
select(ApiKey).where(ApiKey.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
select(TemporaryCredit).where(TemporaryCredit.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
)
|
||||
assert result.scalar_one_or_none() is not None
|
||||
|
||||
@@ -159,7 +161,7 @@ async def test_refund_amount_validation(
|
||||
|
||||
# Verify the key has no refund address (needed for the "too small" check)
|
||||
result = await integration_session.execute(
|
||||
select(ApiKey).where(ApiKey.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
select(TemporaryCredit).where(TemporaryCredit.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
)
|
||||
key = result.scalar_one()
|
||||
assert key.refund_address is None
|
||||
@@ -192,8 +194,8 @@ async def test_refund_with_lightning_address(
|
||||
from sqlmodel import update
|
||||
|
||||
await integration_session.execute(
|
||||
update(ApiKey)
|
||||
.where(ApiKey.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
update(TemporaryCredit)
|
||||
.where(TemporaryCredit.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
.values(refund_address=refund_address)
|
||||
)
|
||||
await integration_session.commit()
|
||||
@@ -252,7 +254,7 @@ async def test_database_state_after_refund(
|
||||
|
||||
# Verify key exists before refund
|
||||
result = await integration_session.execute(
|
||||
select(ApiKey).where(ApiKey.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
select(TemporaryCredit).where(TemporaryCredit.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
)
|
||||
key_before = result.scalar_one()
|
||||
assert key_before.balance == 10_000_000
|
||||
@@ -263,12 +265,12 @@ async def test_database_state_after_refund(
|
||||
|
||||
# Verify key is deleted after refund
|
||||
result = await integration_session.execute(
|
||||
select(ApiKey).where(ApiKey.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
select(TemporaryCredit).where(TemporaryCredit.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
)
|
||||
assert result.scalar_one_or_none() is None
|
||||
|
||||
# Count total keys to ensure only the specific one was deleted
|
||||
result = await integration_session.execute(select(ApiKey))
|
||||
result = await integration_session.execute(select(TemporaryCredit))
|
||||
remaining_keys = result.scalars().all()
|
||||
# Should have no keys left (assuming clean test environment)
|
||||
assert len(remaining_keys) == 0
|
||||
@@ -480,8 +482,8 @@ async def test_refund_error_handling(
|
||||
from sqlmodel import update
|
||||
|
||||
await integration_session.execute(
|
||||
update(ApiKey)
|
||||
.where(ApiKey.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
update(TemporaryCredit)
|
||||
.where(TemporaryCredit.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
.values(balance=-1000) # Invalid negative balance
|
||||
)
|
||||
await integration_session.commit()
|
||||
@@ -517,9 +519,9 @@ async def test_refund_with_expired_key(
|
||||
from sqlmodel import update
|
||||
|
||||
await integration_session.execute(
|
||||
update(ApiKey)
|
||||
.where(ApiKey.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
.values(key_expiry_time=past_expiry, refund_address="expired@ln.address")
|
||||
update(TemporaryCredit)
|
||||
.where(TemporaryCredit.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
.values(refund_expiration_time=past_expiry, refund_address="expired@ln.address")
|
||||
)
|
||||
await integration_session.commit()
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import pytest
|
||||
from httpx import AsyncClient
|
||||
from sqlmodel import select
|
||||
|
||||
from routstr.core.db import ApiKey
|
||||
from routstr.core.db import TemporaryCredit
|
||||
|
||||
from .utils import (
|
||||
CashuTokenGenerator,
|
||||
@@ -64,7 +64,7 @@ async def test_topup_with_valid_token( # type: ignore[no-untyped-def]
|
||||
# Get the hashed key from the API key
|
||||
hashed_key = api_key[3:] # Remove "sk-" prefix
|
||||
result = await integration_session.execute(
|
||||
select(ApiKey).where(ApiKey.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
select(TemporaryCredit).where(TemporaryCredit.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
)
|
||||
db_key = result.scalar_one()
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import httpx
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlmodel import select
|
||||
|
||||
from routstr.core.db import ApiKey
|
||||
from routstr.core.db import TemporaryCredit
|
||||
|
||||
|
||||
class CashuTokenGenerator:
|
||||
@@ -97,11 +97,11 @@ class DatabaseStateValidator:
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self.session = session
|
||||
|
||||
async def get_api_key(self, api_key: str) -> Optional[ApiKey]:
|
||||
async def get_api_key(self, api_key: str) -> Optional[TemporaryCredit]:
|
||||
"""Get API key from database"""
|
||||
hashed_key = hashlib.sha256(api_key.encode()).hexdigest()
|
||||
result = await self.session.execute(
|
||||
select(ApiKey).where(ApiKey.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
select(TemporaryCredit).where(TemporaryCredit.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@@ -125,20 +125,6 @@ class DatabaseStateValidator:
|
||||
"current_balance": key_obj.balance,
|
||||
}
|
||||
|
||||
async def validate_request_count(
|
||||
self, api_key: str, expected_count: int
|
||||
) -> Dict[str, Any]:
|
||||
"""Validate request count for an API key"""
|
||||
key_obj = await self.get_api_key(api_key)
|
||||
if not key_obj:
|
||||
return {"valid": False, "error": "API key not found"}
|
||||
|
||||
return {
|
||||
"valid": key_obj.total_requests == expected_count,
|
||||
"expected": expected_count,
|
||||
"actual": key_obj.total_requests,
|
||||
}
|
||||
|
||||
async def validate_atomic_update(
|
||||
self, api_key: str, field: str, expected_value: Any
|
||||
) -> bool:
|
||||
@@ -315,7 +301,7 @@ class ConcurrencyTester:
|
||||
)
|
||||
|
||||
tasks = [make_request(req) for req in requests]
|
||||
return await asyncio.gather(*tasks, return_exceptions=False)
|
||||
return await asyncio.gather(*tasks, return_exceptions=False) # type: ignore
|
||||
|
||||
@staticmethod
|
||||
async def test_race_condition(
|
||||
@@ -463,8 +449,6 @@ class TestDataBuilder:
|
||||
"""Create test API key data"""
|
||||
data: Dict[str, Any] = {
|
||||
"balance": balance,
|
||||
"total_spent": 0,
|
||||
"total_requests": 0,
|
||||
}
|
||||
|
||||
if refund_address:
|
||||
|
||||
@@ -44,9 +44,9 @@ def check_imports() -> bool:
|
||||
print("Conftest fixtures imported successfully")
|
||||
|
||||
# Check routstr modules - imports are for verification only
|
||||
from routstr.core.db import ApiKey
|
||||
from routstr.core.db import TemporaryCredit
|
||||
|
||||
del ApiKey
|
||||
del TemporaryCredit
|
||||
|
||||
print("Router modules imported successfully")
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from routstr.algorithm import ( # noqa: E402
|
||||
get_provider_penalty,
|
||||
should_prefer_model,
|
||||
)
|
||||
from routstr.payment.models import Architecture, Model, Pricing # noqa: E402
|
||||
from routstr.models.models import Architecture, Model, Pricing # noqa: E402
|
||||
|
||||
|
||||
def create_test_model(
|
||||
|
||||
@@ -21,11 +21,11 @@ import pytest
|
||||
os.environ["UPSTREAM_BASE_URL"] = "http://test"
|
||||
os.environ["UPSTREAM_API_KEY"] = "test"
|
||||
|
||||
from routstr.payment.models import ( # noqa: E402
|
||||
from routstr.models.crud import _model_to_row_payload
|
||||
from routstr.models.models import ( # noqa: E402
|
||||
Architecture,
|
||||
Model,
|
||||
Pricing,
|
||||
_model_to_row_payload,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ from io import BytesIO
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from routstr.payment.helpers import (
|
||||
from routstr.payment.cost import (
|
||||
_calculate_image_tokens,
|
||||
_estimate_image_tokens_in_messages,
|
||||
_get_image_dimensions,
|
||||
estimate_image_tokens_in_messages,
|
||||
)
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ async def test_estimate_image_tokens_base64() -> None:
|
||||
}
|
||||
]
|
||||
|
||||
tokens = await estimate_image_tokens_in_messages(messages)
|
||||
tokens = await _estimate_image_tokens_in_messages(messages)
|
||||
assert tokens > 0
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ async def test_estimate_image_tokens_multiple_images() -> None:
|
||||
}
|
||||
]
|
||||
|
||||
tokens = await estimate_image_tokens_in_messages(messages)
|
||||
tokens = await _estimate_image_tokens_in_messages(messages)
|
||||
assert tokens > 0
|
||||
|
||||
|
||||
@@ -148,8 +148,8 @@ async def test_estimate_image_tokens_with_detail() -> None:
|
||||
}
|
||||
]
|
||||
|
||||
tokens_low = await estimate_image_tokens_in_messages(messages_low)
|
||||
tokens_high = await estimate_image_tokens_in_messages(messages_high)
|
||||
tokens_low = await _estimate_image_tokens_in_messages(messages_low)
|
||||
tokens_high = await _estimate_image_tokens_in_messages(messages_high)
|
||||
|
||||
assert tokens_low == 85
|
||||
assert tokens_high > tokens_low
|
||||
@@ -163,7 +163,7 @@ async def test_estimate_image_tokens_no_images() -> None:
|
||||
{"role": "assistant", "content": "I'm doing well, thank you!"},
|
||||
]
|
||||
|
||||
tokens = await estimate_image_tokens_in_messages(messages)
|
||||
tokens = await _estimate_image_tokens_in_messages(messages)
|
||||
assert tokens == 0
|
||||
|
||||
|
||||
@@ -185,5 +185,5 @@ async def test_estimate_image_tokens_input_image_type() -> None:
|
||||
}
|
||||
]
|
||||
|
||||
tokens = await estimate_image_tokens_in_messages(messages)
|
||||
tokens = await _estimate_image_tokens_in_messages(messages)
|
||||
assert tokens > 0
|
||||
|
||||
@@ -7,11 +7,11 @@ os.environ["UPSTREAM_BASE_URL"] = "http://test"
|
||||
os.environ["UPSTREAM_API_KEY"] = "test"
|
||||
|
||||
from routstr.core.settings import settings # noqa: E402
|
||||
from routstr.payment.helpers import get_max_cost_for_model # noqa: E402
|
||||
from routstr.payment.cost import get_max_cost_for_model # noqa: E402
|
||||
|
||||
|
||||
async def test_get_max_cost_for_model_known() -> None:
|
||||
from routstr.payment.models import Pricing
|
||||
from routstr.models.models import Pricing
|
||||
|
||||
# Mock DB session behavior
|
||||
mock_session = AsyncMock()
|
||||
@@ -63,48 +63,32 @@ async def test_get_max_cost_for_model_known() -> None:
|
||||
|
||||
with patch.object(settings, "fixed_pricing", False):
|
||||
with patch.object(settings, "tolerance_percentage", 0):
|
||||
cost = await get_max_cost_for_model(
|
||||
"gpt-4", session=mock_session, model_obj=mock_model
|
||||
)
|
||||
cost = get_max_cost_for_model(model_obj=mock_model)
|
||||
assert cost == 500000 # 500 sats * 1000 = msats
|
||||
|
||||
|
||||
async def test_get_max_cost_for_model_unknown() -> None:
|
||||
mock_session = AsyncMock()
|
||||
mock_model = Mock()
|
||||
mock_model.sats_pricing = None
|
||||
mock_model.id = "unknown-model"
|
||||
|
||||
# Mock the exec results to return no model override
|
||||
async def async_mock_exec(query: Any) -> Any:
|
||||
result = Mock()
|
||||
result.first = Mock(return_value=None)
|
||||
result.all = Mock(return_value=[])
|
||||
return result
|
||||
|
||||
mock_session.exec = AsyncMock(side_effect=async_mock_exec)
|
||||
mock_session.get = AsyncMock(return_value=None)
|
||||
|
||||
# Mock get_upstreams to return empty list
|
||||
with patch("routstr.proxy.get_upstreams", return_value=[]):
|
||||
with patch.object(settings, "fixed_cost_per_request", 100):
|
||||
with patch.object(settings, "tolerance_percentage", 0):
|
||||
cost = await get_max_cost_for_model(
|
||||
"unknown-model", session=mock_session, model_obj=None
|
||||
)
|
||||
assert cost == 100000
|
||||
with patch.object(settings, "fixed_cost_per_request", 100):
|
||||
with patch.object(settings, "tolerance_percentage", 0):
|
||||
cost = get_max_cost_for_model(model_obj=mock_model)
|
||||
assert cost == 100000
|
||||
|
||||
|
||||
async def test_get_max_cost_for_model_disabled() -> None:
|
||||
mock_session = AsyncMock()
|
||||
mock_model = Mock()
|
||||
with patch.object(settings, "fixed_pricing", True):
|
||||
with patch.object(settings, "fixed_cost_per_request", 200):
|
||||
with patch.object(settings, "tolerance_percentage", 0):
|
||||
cost = await get_max_cost_for_model("any-model", session=mock_session)
|
||||
cost = get_max_cost_for_model(model_obj=mock_model)
|
||||
assert cost == 200000
|
||||
|
||||
|
||||
async def test_get_max_cost_for_model_tolerance() -> None:
|
||||
from routstr.payment.models import Pricing
|
||||
|
||||
mock_session = AsyncMock()
|
||||
from routstr.models.models import Pricing
|
||||
|
||||
# Mock the model with sats_pricing
|
||||
mock_pricing = Pricing(
|
||||
@@ -121,7 +105,5 @@ async def test_get_max_cost_for_model_tolerance() -> None:
|
||||
|
||||
with patch.object(settings, "fixed_pricing", False):
|
||||
with patch.object(settings, "tolerance_percentage", 10):
|
||||
cost = await get_max_cost_for_model(
|
||||
"gpt-4", session=mock_session, model_obj=mock_model
|
||||
)
|
||||
cost = get_max_cost_for_model(model_obj=mock_model)
|
||||
assert cost == 450000 # 500 sats * 1000 * 0.9 = 450000
|
||||
|
||||
+23
-12
@@ -4,8 +4,13 @@ from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from routstr.core.db import ApiKey
|
||||
from routstr.wallet import credit_balance, get_balance, recieve_token, send_token
|
||||
from routstr.core.db import TemporaryCredit
|
||||
from routstr.payment.wallet import (
|
||||
credit_balance,
|
||||
get_balance,
|
||||
recieve_token,
|
||||
send_token,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -15,7 +20,7 @@ async def test_get_balance() -> None:
|
||||
mock_wallet.load_mint = AsyncMock()
|
||||
mock_wallet.load_proofs = AsyncMock()
|
||||
|
||||
with patch("routstr.wallet.Wallet.with_db", return_value=mock_wallet):
|
||||
with patch("routstr.payment.wallet.Wallet.with_db", return_value=mock_wallet):
|
||||
balance = await get_balance("sat")
|
||||
assert balance == 50000
|
||||
|
||||
@@ -43,7 +48,9 @@ async def test_recieve_token_valid() -> None:
|
||||
from routstr.core.settings import settings
|
||||
|
||||
with patch.object(settings, "cashu_mints", ["http://mint:3338"]):
|
||||
with patch("routstr.wallet.deserialize_token_from_string") as mock_deserialize:
|
||||
with patch(
|
||||
"routstr.payment.wallet.deserialize_token_from_string"
|
||||
) as mock_deserialize:
|
||||
mock_token = Mock()
|
||||
mock_token.keysets = ["keyset1"]
|
||||
mock_token.mint = "http://mint:3338"
|
||||
@@ -54,7 +61,9 @@ async def test_recieve_token_valid() -> None:
|
||||
|
||||
mock_wallet.load_mint = AsyncMock()
|
||||
mock_wallet.load_proofs = AsyncMock()
|
||||
with patch("routstr.wallet.Wallet.with_db", return_value=mock_wallet):
|
||||
with patch(
|
||||
"routstr.payment.wallet.Wallet.with_db", return_value=mock_wallet
|
||||
):
|
||||
amount, unit, mint = await recieve_token(token_str)
|
||||
assert amount == 1000
|
||||
assert unit == "sat"
|
||||
@@ -65,8 +74,8 @@ async def test_recieve_token_valid() -> None:
|
||||
async def test_send_token() -> None:
|
||||
mock_wallet = Mock()
|
||||
|
||||
with patch("routstr.wallet.Wallet.with_db", return_value=mock_wallet):
|
||||
with patch("routstr.wallet.send", return_value=(1000, "test_token")):
|
||||
with patch("routstr.payment.wallet.Wallet.with_db", return_value=mock_wallet):
|
||||
with patch("routstr.payment.wallet.send", return_value=(1000, "test_token")):
|
||||
token = await send_token(1000, "sat", "http://mint:3338")
|
||||
assert token == "test_token"
|
||||
|
||||
@@ -87,7 +96,7 @@ async def test_credit_balance() -> None:
|
||||
mock_session = AsyncMock()
|
||||
|
||||
# Mock session.refresh to update the balance (simulates DB reload)
|
||||
async def mock_refresh(key: ApiKey) -> None:
|
||||
async def mock_refresh(key: TemporaryCredit) -> None:
|
||||
key.balance = 6000000
|
||||
|
||||
mock_session.refresh.side_effect = mock_refresh
|
||||
@@ -96,7 +105,7 @@ async def test_credit_balance() -> None:
|
||||
|
||||
with patch.object(settings, "cashu_mints", ["http://mint:3338"]):
|
||||
with patch(
|
||||
"routstr.wallet.recieve_token",
|
||||
"routstr.payment.wallet.recieve_token",
|
||||
return_value=(1000, "sat", "http://mint:3338"),
|
||||
):
|
||||
amount = await credit_balance(token_str, mock_key, mock_session)
|
||||
@@ -112,7 +121,9 @@ async def test_credit_balance() -> None:
|
||||
async def test_recieve_token_untrusted_mint() -> None:
|
||||
mock_wallet = Mock()
|
||||
|
||||
with patch("routstr.wallet.deserialize_token_from_string") as mock_deserialize:
|
||||
with patch(
|
||||
"routstr.payment.wallet.deserialize_token_from_string"
|
||||
) as mock_deserialize:
|
||||
mock_token = Mock()
|
||||
mock_token.keysets = ["keyset1"]
|
||||
mock_token.mint = "http://untrusted:3338"
|
||||
@@ -122,9 +133,9 @@ async def test_recieve_token_untrusted_mint() -> None:
|
||||
|
||||
mock_wallet.load_mint = AsyncMock()
|
||||
mock_wallet.load_proofs = AsyncMock()
|
||||
with patch("routstr.wallet.Wallet.with_db", return_value=mock_wallet):
|
||||
with patch("routstr.payment.wallet.Wallet.with_db", return_value=mock_wallet):
|
||||
with patch(
|
||||
"routstr.wallet.swap_to_primary_mint",
|
||||
"routstr.payment.wallet.swap_to_primary_mint",
|
||||
return_value=(900, "sat", "http://mint:3338"),
|
||||
):
|
||||
amount, unit, mint = await recieve_token("test_token")
|
||||
|
||||
Reference in New Issue
Block a user