Spaces:
Sleeping
Sleeping
| """ | |
| 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.""" | |
| 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.""" | |
| 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.""" | |
| 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() | |