apigateway / tests /test_razorpay.py
jebin2's picture
payment
de3cb16
"""
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()