""" Test Razorpay Payment Integration. This test file includes: 1. Unit tests for RazorpayService (using real test API keys) 2. Integration tests for payment endpoints 3. End-to-end order creation flow Run with: ./venv/bin/python -m pytest tests/test_razorpay.py -v """ import pytest import os import sys import hmac import hashlib from unittest.mock import patch, MagicMock, AsyncMock from datetime import datetime # Add parent directory sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from dotenv import load_dotenv load_dotenv() from services.razorpay_service import ( RazorpayService, RazorpayConfigError, RazorpayOrderError, CREDIT_PACKAGES, get_package, list_packages, is_razorpay_configured ) # ============================================================================= # Test Credit Packages # ============================================================================= class TestCreditPackages: """Test credit package configuration.""" def test_packages_defined(self): """Verify all expected packages exist.""" assert "starter" in CREDIT_PACKAGES assert "standard" in CREDIT_PACKAGES assert "pro" in CREDIT_PACKAGES def test_starter_package(self): """Verify starter package details.""" pkg = get_package("starter") assert pkg is not None assert pkg.credits == 100 assert pkg.amount_paise == 9900 # ₹99 assert pkg.currency == "INR" def test_standard_package(self): """Verify standard package details.""" pkg = get_package("standard") assert pkg is not None assert pkg.credits == 500 assert pkg.amount_paise == 44900 # ₹449 def test_pro_package(self): """Verify pro package details.""" pkg = get_package("pro") assert pkg is not None assert pkg.credits == 1000 assert pkg.amount_paise == 79900 # ₹799 def test_get_invalid_package(self): """Test getting non-existent package.""" assert get_package("nonexistent") is None def test_list_packages(self): """Test listing all packages.""" packages = list_packages() assert len(packages) == 3 assert all("id" in p and "credits" in p and "amount_paise" in p for p in packages) def test_package_to_dict(self): """Test package serialization.""" pkg = get_package("starter") d = pkg.to_dict() assert d["id"] == "starter" assert d["credits"] == 100 assert d["amount_rupees"] == 99.0 # ============================================================================= # Test Razorpay Service Configuration # ============================================================================= class TestRazorpayServiceConfig: """Test Razorpay service configuration.""" def test_is_configured(self): """Check if Razorpay is configured (test keys should be set).""" # This will pass if user has set RAZORPAY_KEY_ID and RAZORPAY_KEY_SECRET result = is_razorpay_configured() print(f"\n Razorpay configured: {result}") if not result: pytest.skip("Razorpay not configured - set RAZORPAY_KEY_ID and RAZORPAY_KEY_SECRET") def test_service_initialization(self): """Test service can be initialized with env vars.""" if not is_razorpay_configured(): pytest.skip("Razorpay not configured") service = RazorpayService() assert service.is_configured assert service.key_id is not None assert service.key_secret is not None def test_service_with_invalid_credentials(self): """Test service fails gracefully with no credentials.""" # Temporarily clear env vars original_key = os.environ.pop("RAZORPAY_KEY_ID", None) original_secret = os.environ.pop("RAZORPAY_KEY_SECRET", None) try: with pytest.raises(RazorpayConfigError): RazorpayService() finally: # Restore env vars if original_key: os.environ["RAZORPAY_KEY_ID"] = original_key if original_secret: os.environ["RAZORPAY_KEY_SECRET"] = original_secret # ============================================================================= # Test Order Creation (Real API Call with Test Keys) # ============================================================================= class TestRazorpayOrderCreation: """Test order creation with real Razorpay test API.""" @pytest.fixture def razorpay_service(self): """Get configured Razorpay service.""" if not is_razorpay_configured(): pytest.skip("Razorpay not configured") return RazorpayService() def test_create_order_starter_package(self, razorpay_service): """Test creating order for starter package.""" package = get_package("starter") order = razorpay_service.create_order( amount_paise=package.amount_paise, transaction_id=f"test_txn_{datetime.now().strftime('%Y%m%d%H%M%S')}", notes={"test": "true", "package": "starter"} ) print(f"\n Created order: {order['id']}") assert "id" in order assert order["id"].startswith("order_") assert order["amount"] == package.amount_paise assert order["currency"] == "INR" assert order["status"] == "created" def test_create_order_all_packages(self, razorpay_service): """Test creating orders for all packages.""" for package_id, package in CREDIT_PACKAGES.items(): order = razorpay_service.create_order( amount_paise=package.amount_paise, transaction_id=f"test_{package_id}_{datetime.now().strftime('%H%M%S')}", notes={"package": package_id} ) print(f"\n {package_id}: order={order['id']}, amount=₹{order['amount']/100}") assert order["amount"] == package.amount_paise def test_fetch_order(self, razorpay_service): """Test fetching order details.""" # First create an order order = razorpay_service.create_order( amount_paise=9900, transaction_id=f"fetch_test_{datetime.now().strftime('%H%M%S')}" ) # Fetch it back fetched = razorpay_service.fetch_order(order["id"]) assert fetched["id"] == order["id"] assert fetched["amount"] == 9900 # ============================================================================= # Test Signature Verification # ============================================================================= class TestSignatureVerification: """Test payment signature verification.""" @pytest.fixture def razorpay_service(self): """Get configured Razorpay service.""" if not is_razorpay_configured(): pytest.skip("Razorpay not configured") return RazorpayService() def test_verify_valid_signature(self, razorpay_service): """Test verification with a valid signature.""" order_id = "order_test123" payment_id = "pay_test456" # Generate valid signature message = f"{order_id}|{payment_id}" valid_signature = hmac.new( razorpay_service.key_secret.encode('utf-8'), message.encode('utf-8'), hashlib.sha256 ).hexdigest() result = razorpay_service.verify_payment_signature( order_id=order_id, payment_id=payment_id, signature=valid_signature ) assert result is True def test_verify_invalid_signature(self, razorpay_service): """Test verification with an invalid signature.""" result = razorpay_service.verify_payment_signature( order_id="order_test123", payment_id="pay_test456", signature="invalid_signature_abc123" ) assert result is False def test_verify_webhook_signature(self, razorpay_service): """Test webhook signature verification.""" if not razorpay_service.webhook_secret: pytest.skip("Webhook secret not configured") body = b'{"event":"payment.captured"}' # Generate valid webhook signature valid_signature = hmac.new( razorpay_service.webhook_secret.encode('utf-8'), body, hashlib.sha256 ).hexdigest() result = razorpay_service.verify_webhook_signature(body, valid_signature) assert result is True # Test invalid signature result = razorpay_service.verify_webhook_signature(body, "invalid") assert result is False # ============================================================================= # Test Payment Endpoints (Integration) # ============================================================================= class TestPaymentEndpoints: """Integration tests for payment API endpoints.""" @pytest.fixture def client(self): """Create test client.""" from fastapi.testclient import TestClient # Set required env vars for testing os.environ.setdefault("JWT_SECRET", "test-secret-key-for-jwt-testing") os.environ.setdefault("GOOGLE_CLIENT_ID", "test.apps.googleusercontent.com") os.environ.setdefault("RESET_DB", "true") with patch("services.drive_service.DriveService") as mock_drive: mock_instance = MagicMock() mock_instance.download_db.return_value = False mock_instance.upload_db.return_value = True mock_drive.return_value = mock_instance from app import app with TestClient(app) as c: yield c def test_get_packages_no_auth(self, client): """Test packages endpoint doesn't require auth.""" response = client.get("/payments/packages") assert response.status_code == 200 data = response.json() assert "packages" in data assert len(data["packages"]) == 3 # Verify all packages present package_ids = [p["id"] for p in data["packages"]] assert "starter" in package_ids assert "standard" in package_ids assert "pro" in package_ids print(f"\n Packages: {[p['id'] + '@₹' + str(p['amount_rupees']) for p in data['packages']]}") def test_create_order_requires_auth(self, client): """Test create-order endpoint requires authentication.""" response = client.post( "/payments/create-order", json={"package_id": "starter"} ) assert response.status_code == 401 def test_verify_requires_auth(self, client): """Test verify endpoint requires authentication.""" response = client.post( "/payments/verify", json={ "razorpay_order_id": "order_test", "razorpay_payment_id": "pay_test", "razorpay_signature": "sig_test" } ) assert response.status_code == 401 def test_history_requires_auth(self, client): """Test history endpoint requires authentication.""" response = client.get("/payments/history") assert response.status_code == 401 # ============================================================================= # Run Standalone Test Script # ============================================================================= def run_manual_tests(): """ Run manual tests - useful for quick verification. Usage: ./venv/bin/python tests/test_razorpay.py """ print("\n" + "="*60) print("RAZORPAY INTEGRATION TEST") print("="*60) # Check configuration print("\n1. Checking Razorpay configuration...") if not is_razorpay_configured(): print(" ❌ Razorpay NOT configured!") print(" Please set RAZORPAY_KEY_ID and RAZORPAY_KEY_SECRET in .env") return print(" ✓ Razorpay is configured") # Initialize service print("\n2. Initializing RazorpayService...") try: service = RazorpayService() print(f" ✓ Service initialized") print(f" Key ID: {service.key_id[:15]}...") except Exception as e: print(f" ❌ Failed: {e}") return # List packages print("\n3. Credit packages:") for pkg in list_packages(): print(f" • {pkg['name']}: {pkg['credits']} credits @ ₹{pkg['amount_rupees']}") # Create test order print("\n4. Creating test order (₹99 Starter pack)...") try: order = service.create_order( amount_paise=9900, transaction_id=f"manual_test_{datetime.now().strftime('%Y%m%d_%H%M%S')}", notes={"test": "manual", "source": "test_razorpay.py"} ) print(f" ✓ Order created!") print(f" Order ID: {order['id']}") print(f" Amount: ₹{order['amount']/100}") print(f" Status: {order['status']}") except Exception as e: print(f" ❌ Failed: {e}") return # Test signature verification print("\n5. Testing signature verification...") test_signature = hmac.new( service.key_secret.encode(), f"{order['id']}|pay_test123".encode(), hashlib.sha256 ).hexdigest() valid = service.verify_payment_signature(order['id'], "pay_test123", test_signature) print(f" ✓ Valid signature: {valid}") invalid = service.verify_payment_signature(order['id'], "pay_test123", "wrong_sig") print(f" ✓ Invalid signature rejected: {not invalid}") # Test API endpoints print("\n6. Testing API endpoints...") from fastapi.testclient import TestClient os.environ.setdefault("JWT_SECRET", "test-secret") os.environ.setdefault("GOOGLE_CLIENT_ID", "test.apps.googleusercontent.com") os.environ.setdefault("RESET_DB", "true") with patch("services.drive_service.DriveService"): from app import app with TestClient(app) as client: # Test packages endpoint resp = client.get("/payments/packages") print(f" GET /payments/packages: {resp.status_code}") # Test auth requirement resp = client.post("/payments/create-order", json={"package_id": "starter"}) print(f" POST /payments/create-order (no auth): {resp.status_code} (expected 401)") print("\n" + "="*60) print("✓ All manual tests passed!") print("="*60) print("\nNext steps:") print("1. Start your server: ./venv/bin/uvicorn app:app --reload") print("2. Login to get JWT token") print("3. Call POST /payments/create-order with token") print("4. Use returned order_id in Razorpay checkout") print("") if __name__ == "__main__": run_manual_tests()