""" Integration tests for wallet authentication system including API key generation and validation. Tests POST /v1/wallet/topup endpoint and authorization header validation. """ import hashlib from datetime import datetime, timedelta from typing import Any import pytest from httpx import AsyncClient from sqlmodel import select from routstr.core.db import ApiKey from .utils import ( CashuTokenGenerator, ConcurrencyTester, ResponseValidator, ) @pytest.mark.integration @pytest.mark.asyncio async def test_api_key_generation_valid_token( integration_client: AsyncClient, testmint_wallet: Any, db_snapshot: Any, integration_session: Any, ) -> None: """Test API key generation from a valid Cashu token""" # Generate a valid test token amount = 1000 # 1k sats token = await testmint_wallet.mint_tokens(amount) # Use token as Bearer auth to create API key on first use integration_client.headers["Authorization"] = f"Bearer {token}" response = await integration_client.get("/v1/wallet/info") # Should succeed assert response.status_code == 200 data = response.json() # Validate response structure assert "api_key" in data assert "balance" in data assert data["balance"] == amount * 1000 # Convert to msats # API key should have proper format api_key = data["api_key"] assert api_key.startswith("sk-") assert len(api_key) > 10 # 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] ) 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}" wallet_response = await integration_client.get("/v1/wallet/") assert wallet_response.status_code == 200 wallet_data = wallet_response.json() assert wallet_data["balance"] == amount * 1000 @pytest.mark.integration @pytest.mark.asyncio async def test_api_key_generation_invalid_token( integration_client: AsyncClient, db_snapshot: Any ) -> None: """Test API key generation with various invalid tokens""" # Capture initial state await db_snapshot.capture() # Test various invalid tokens invalid_tokens = [ CashuTokenGenerator.generate_invalid_token(), # Malformed token "not-a-cashu-token", # Wrong format "cashuA", # Empty token "cashuA" + "x" * 1000, # Invalid base64 ] for invalid_token in invalid_tokens: 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" ) # Validate error response validator = ResponseValidator() error_validation = validator.validate_error_response( response, expected_status=401, expected_error_key="detail" ) assert error_validation["valid"] # Verify no database changes diff = await db_snapshot.diff() assert len(diff["api_keys"]["added"]) == 0 assert len(diff["api_keys"]["modified"]) == 0 @pytest.mark.integration @pytest.mark.asyncio async def test_duplicate_token_handling( integration_client: AsyncClient, testmint_wallet: Any, db_snapshot: Any ) -> None: """Test that duplicate tokens return the same API key without double-spending""" # Generate a valid token amount = 500 # 500 sats token = await testmint_wallet.mint_tokens(amount) # First use of token integration_client.headers["Authorization"] = f"Bearer {token}" response1 = await integration_client.get("/v1/wallet/info") assert response1.status_code == 200 api_key1 = response1.json()["api_key"] balance1 = response1.json()["balance"] # Capture state after first submission await db_snapshot.capture() # Second use of same token - should return same API key since it's already created response2 = await integration_client.get("/v1/wallet/info") assert response2.status_code == 200 api_key2 = response2.json()["api_key"] balance2 = response2.json()["balance"] # Should return the same API key and balance assert api_key1 == api_key2 assert balance1 == balance2 # Verify no additional database changes diff = await db_snapshot.diff() assert len(diff["api_keys"]["added"]) == 0 assert len(diff["api_keys"]["modified"]) == 0 # Original API key should still work with original balance integration_client.headers["Authorization"] = f"Bearer {api_key1}" wallet_response = await integration_client.get("/v1/wallet/") assert wallet_response.status_code == 200 assert wallet_response.json()["balance"] == balance1 @pytest.mark.integration @pytest.mark.asyncio async def test_authorization_header_validation( integration_client: AsyncClient, testmint_wallet: Any ) -> None: """Test various authorization header scenarios""" # Create a valid API key first token = await testmint_wallet.mint_tokens(1000) integration_client.headers["Authorization"] = f"Bearer {token}" response = await integration_client.get("/v1/wallet/info") assert response.status_code == 200 valid_api_key = response.json()["api_key"] # Test scenarios test_cases = [ # (headers, expected_status, description) ( {}, 422, "Missing authorization header", ), # FastAPI returns 422 for missing required headers ({"Authorization": ""}, 401, "Empty authorization header"), ({"Authorization": "Bearer"}, 401, "Bearer without token"), ({"Authorization": "Bearer "}, 401, "Bearer with space only"), ({"Authorization": "InvalidFormat"}, 401, "Invalid format"), ({"Authorization": "Basic dGVzdDp0ZXN0"}, 401, "Wrong auth type"), ({"Authorization": "Bearer invalid-key-12345"}, 401, "Invalid API key"), ({"Authorization": f"Bearer {valid_api_key}"}, 200, "Valid API key"), ({"authorization": f"Bearer {valid_api_key}"}, 200, "Lowercase header"), ({"AUTHORIZATION": f"Bearer {valid_api_key}"}, 200, "Uppercase header"), ] for headers, expected_status, description in test_cases: # Clear existing headers integration_client.headers.pop("Authorization", None) integration_client.headers.pop("authorization", None) # Set test headers integration_client.headers.update(headers) # Make request to protected endpoint response = await integration_client.get("/v1/wallet/") assert response.status_code == expected_status, ( f"{description}: Expected {expected_status}, got {response.status_code}" ) if expected_status == 401: assert "detail" in response.json() @pytest.mark.integration @pytest.mark.asyncio async def test_malformed_authorization_header(integration_client: AsyncClient) -> None: """Test malformed authorization headers return 400""" # Test malformed headers that should return 400 malformed_headers = [ "Bearer\x00null", # Null byte "Bearer " + "x" * 10000, # Extremely long token "Bearer sk-\n\r", # Newline characters "Bearer sk-