From 9be8aba4178be4a82ee94f3c3d377fb0f0304b47 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 28 Nov 2025 13:40:35 +0900 Subject: [PATCH] the refactor part 02 --- tests/integration/conftest.py | 35 ++++---- tests/integration/test_background_tasks.py | 85 ++++++++++--------- .../integration/test_database_consistency.py | 65 ++++++-------- .../test_error_handling_edge_cases.py | 22 ++--- .../test_general_info_endpoints.py | 2 +- tests/integration/test_performance_load.py | 8 +- tests/integration/test_provider_management.py | 50 +++++------ tests/integration/test_proxy_get_endpoints.py | 8 +- .../test_reserved_balance_negative.py | 28 +++--- .../integration/test_wallet_authentication.py | 28 +++--- tests/integration/test_wallet_information.py | 28 +++--- tests/integration/test_wallet_refund.py | 30 ++++--- tests/integration/test_wallet_topup.py | 4 +- tests/integration/utils.py | 24 +----- tests/integration/verify_setup.py | 4 +- tests/unit/test_algorithm.py | 2 +- tests/unit/test_fee_consistency.py | 4 +- tests/unit/test_image_tokens.py | 16 ++-- tests/unit/test_payment_helpers.py | 46 +++------- tests/unit/test_wallet.py | 35 +++++--- 20 files changed, 238 insertions(+), 286 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 1977101f..543f7e22 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -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: diff --git a/tests/integration/test_background_tasks.py b/tests/integration/test_background_tasks.py index 8855532d..fca4f345 100644 --- a/tests/integration/test_background_tasks.py +++ b/tests/integration/test_background_tasks.py @@ -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 diff --git a/tests/integration/test_database_consistency.py b/tests/integration/test_database_consistency.py index 5c2bbe68..f30bc247 100644 --- a/tests/integration/test_database_consistency.py +++ b/tests/integration/test_database_consistency.py @@ -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() diff --git a/tests/integration/test_error_handling_edge_cases.py b/tests/integration/test_error_handling_edge_cases.py index a0612d71..509e21d1 100644 --- a/tests/integration/test_error_handling_edge_cases.py +++ b/tests/integration/test_error_handling_edge_cases.py @@ -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 diff --git a/tests/integration/test_general_info_endpoints.py b/tests/integration/test_general_info_endpoints.py index 41250f66..4c7af5f1 100644 --- a/tests/integration/test_general_info_endpoints.py +++ b/tests/integration/test_general_info_endpoints.py @@ -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 \ No newline at end of file + assert first_ids == response_ids diff --git a/tests/integration/test_performance_load.py b/tests/integration/test_performance_load.py index 581553b9..3ef30666 100644 --- a/tests/integration/test_performance_load.py +++ b/tests/integration/test_performance_load.py @@ -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 diff --git a/tests/integration/test_provider_management.py b/tests/integration/test_provider_management.py index 788ad768..36e3e7c0 100644 --- a/tests/integration/test_provider_management.py +++ b/tests/integration/test_provider_management.py @@ -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 diff --git a/tests/integration/test_proxy_get_endpoints.py b/tests/integration/test_proxy_get_endpoints.py index ff745d8d..767556a6 100644 --- a/tests/integration/test_proxy_get_endpoints.py +++ b/tests/integration/test_proxy_get_endpoints.py @@ -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 diff --git a/tests/integration/test_reserved_balance_negative.py b/tests/integration/test_reserved_balance_negative.py index 21f9b6d0..65d7482c 100644 --- a/tests/integration/test_reserved_balance_negative.py +++ b/tests/integration/test_reserved_balance_negative.py @@ -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}" - ) diff --git a/tests/integration/test_wallet_authentication.py b/tests/integration/test_wallet_authentication.py index 21a26f3b..506407ee 100644 --- a/tests/integration/test_wallet_authentication.py +++ b/tests/integration/test_wallet_authentication.py @@ -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() diff --git a/tests/integration/test_wallet_information.py b/tests/integration/test_wallet_information.py index 6f24e895..e7600021 100644 --- a/tests/integration/test_wallet_information.py +++ b/tests/integration/test_wallet_information.py @@ -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() diff --git a/tests/integration/test_wallet_refund.py b/tests/integration/test_wallet_refund.py index c2461cae..40b4cc3d 100644 --- a/tests/integration/test_wallet_refund.py +++ b/tests/integration/test_wallet_refund.py @@ -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() diff --git a/tests/integration/test_wallet_topup.py b/tests/integration/test_wallet_topup.py index 6f4d5c29..49682f35 100644 --- a/tests/integration/test_wallet_topup.py +++ b/tests/integration/test_wallet_topup.py @@ -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() diff --git a/tests/integration/utils.py b/tests/integration/utils.py index dd1fcf3d..51675fbb 100644 --- a/tests/integration/utils.py +++ b/tests/integration/utils.py @@ -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: diff --git a/tests/integration/verify_setup.py b/tests/integration/verify_setup.py index 992da14c..7af1eb10 100644 --- a/tests/integration/verify_setup.py +++ b/tests/integration/verify_setup.py @@ -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") diff --git a/tests/unit/test_algorithm.py b/tests/unit/test_algorithm.py index d367819f..c09122f3 100644 --- a/tests/unit/test_algorithm.py +++ b/tests/unit/test_algorithm.py @@ -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( diff --git a/tests/unit/test_fee_consistency.py b/tests/unit/test_fee_consistency.py index 8e72a771..c104b096 100644 --- a/tests/unit/test_fee_consistency.py +++ b/tests/unit/test_fee_consistency.py @@ -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, ) diff --git a/tests/unit/test_image_tokens.py b/tests/unit/test_image_tokens.py index a6fae275..e236615e 100644 --- a/tests/unit/test_image_tokens.py +++ b/tests/unit/test_image_tokens.py @@ -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 diff --git a/tests/unit/test_payment_helpers.py b/tests/unit/test_payment_helpers.py index ef8dde63..d4f28894 100644 --- a/tests/unit/test_payment_helpers.py +++ b/tests/unit/test_payment_helpers.py @@ -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 diff --git a/tests/unit/test_wallet.py b/tests/unit/test_wallet.py index f9a49434..7bda33e8 100644 --- a/tests/unit/test_wallet.py +++ b/tests/unit/test_wallet.py @@ -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")