Testing Guide
Comprehensive guide to testing PayFast integrations in your Django application.
Overview
Testing payment systems is crucial for:
Preventing Production Issues: Catch bugs before they affect real payments
Regulatory Compliance: Ensure PCI DSS compliance
User Experience: Verify smooth payment flows
Business Continuity: Avoid revenue loss from broken payments
Testing Environments
PayFast Sandbox
PayFast provides a sandbox environment for testing without processing real money.
Sandbox URL: https://sandbox.payfast.co.za
Getting Sandbox Credentials:
Sign up at https://sandbox.payfast.co.za
Navigate to Settings → Integration
Copy your sandbox credentials
Generate a test passphrase
Sandbox vs Production Differences:
Feature |
Sandbox |
Production |
|---|---|---|
URL |
sandbox.payfast.co.za |
www.payfast.co.za |
Real Money |
No charges |
Real payments |
Test Cards |
Use test card numbers |
Real card numbers only |
Webhooks |
Same as production |
Same as sandbox |
Configuring Test Mode
# settings.py
# Sandbox Configuration
PAYFAST_MERCHANT_ID = '10000100' # Sandbox merchant ID
PAYFAST_MERCHANT_KEY = '46f0cd694581a' # Sandbox merchant key
PAYFAST_PASSPHRASE = 'jt7NOE43FZPn' # Test passphrase
PAYFAST_TEST_MODE = True # Enable sandbox
Using Environment Variables:
# settings.py
import os
PAYFAST_TEST_MODE = os.environ.get('PAYFAST_TEST_MODE', 'True') == 'True'
if PAYFAST_TEST_MODE:
# Sandbox credentials
PAYFAST_MERCHANT_ID = os.environ.get('PAYFAST_SANDBOX_MERCHANT_ID')
PAYFAST_MERCHANT_KEY = os.environ.get('PAYFAST_SANDBOX_MERCHANT_KEY')
PAYFAST_PASSPHRASE = os.environ.get('PAYFAST_SANDBOX_PASSPHRASE')
else:
# Production credentials
PAYFAST_MERCHANT_ID = os.environ.get('PAYFAST_MERCHANT_ID')
PAYFAST_MERCHANT_KEY = os.environ.get('PAYFAST_MERCHANT_KEY')
PAYFAST_PASSPHRASE = os.environ.get('PAYFAST_PASSPHRASE')
Unit Testing
Testing Models
# tests/test_models.py
from django.test import TestCase
from django.contrib.auth import get_user_model
from payfast.models import PayFastPayment, PayFastNotification
import uuid
User = get_user_model()
class PayFastPaymentModelTests(TestCase):
def setUp(self):
"""Create test user and payment"""
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass123'
)
self.payment = PayFastPayment.objects.create(
user=self.user,
m_payment_id=str(uuid.uuid4()),
amount=99.99,
item_name='Test Product',
email_address=self.user.email,
)
def test_payment_creation(self):
"""Test payment is created with correct defaults"""
self.assertEqual(self.payment.status, 'pending')
self.assertEqual(self.payment.amount, 99.99)
self.assertEqual(self.payment.user, self.user)
self.assertIsNotNone(self.payment.m_payment_id)
def test_mark_complete(self):
"""Test marking payment as complete"""
self.payment.mark_complete()
self.assertEqual(self.payment.status, 'complete')
self.assertIsNotNone(self.payment.completed_at)
def test_mark_failed(self):
"""Test marking payment as failed"""
self.payment.mark_failed()
self.assertEqual(self.payment.status, 'failed')
def test_string_representation(self):
"""Test __str__ method"""
expected = f'Payment {self.payment.m_payment_id} - pending'
self.assertEqual(str(self.payment), expected)
def test_custom_fields(self):
"""Test custom fields storage"""
payment = PayFastPayment.objects.create(
m_payment_id=str(uuid.uuid4()),
amount=199.99,
item_name='Premium Plan',
email_address='user@example.com',
custom_str1='order_123',
custom_int1=5,
)
self.assertEqual(payment.custom_str1, 'order_123')
self.assertEqual(payment.custom_int1, 5)
Testing Forms
# tests/test_forms.py
from django.test import TestCase
from payfast.forms import PayFastPaymentForm
from payfast import conf
class PayFastPaymentFormTests(TestCase):
def test_form_initialization(self):
"""Test form creates with initial data"""
initial_data = {
'amount': 99.99,
'item_name': 'Test Item',
'm_payment_id': 'test-123',
'email_address': 'test@example.com',
'notify_url': 'https://example.com/notify/',
}
form = PayFastPaymentForm(initial=initial_data)
self.assertIsNotNone(form.fields['signature'].initial)
self.assertEqual(form.fields['merchant_id'].initial, conf.PAYFAST_MERCHANT_ID)
def test_signature_generation(self):
"""Test signature is generated correctly"""
initial_data = {
'amount': 100.00,
'item_name': 'Test',
'm_payment_id': 'test-456',
'email_address': 'test@example.com',
'notify_url': 'https://example.com/notify/',
}
form = PayFastPaymentForm(initial=initial_data)
signature = form.fields['signature'].initial
self.assertIsNotNone(signature)
self.assertEqual(len(signature), 32) # MD5 hash length
def test_get_action_url(self):
"""Test form action URL"""
form = PayFastPaymentForm()
action_url = form.get_action_url()
if conf.PAYFAST_TEST_MODE:
self.assertIn('sandbox', action_url)
else:
self.assertNotIn('sandbox', action_url)
Testing Utilities
# tests/test_utils.py
from django.test import TestCase
from payfast.utils import (
generate_signature,
verify_signature,
validate_ip
)
class UtilityTests(TestCase):
def test_generate_signature(self):
"""Test signature generation"""
data = {
'merchant_id': '10000100',
'merchant_key': '46f0cd694581a',
'amount': '100.00',
'item_name': 'Test',
}
signature = generate_signature(data, 'testpass')
self.assertIsNotNone(signature)
self.assertEqual(len(signature), 32)
def test_verify_signature_valid(self):
"""Test signature verification with valid signature"""
data = {
'merchant_id': '10000100',
'amount': '100.00',
'item_name': 'Test',
}
# Generate signature
signature = generate_signature(data, 'testpass')
data['signature'] = signature
# Verify
is_valid = verify_signature(data, 'testpass')
self.assertTrue(is_valid)
def test_verify_signature_invalid(self):
"""Test signature verification with invalid signature"""
data = {
'merchant_id': '10000100',
'amount': '100.00',
'signature': 'invalid_signature',
}
is_valid = verify_signature(data, 'testpass')
self.assertFalse(is_valid)
def test_validate_ip(self):
"""Test IP validation"""
# Valid PayFast IP (simplified test)
is_valid = validate_ip('127.0.0.1')
self.assertIsNotNone(is_valid)
Integration Testing
Testing Views
# tests/test_views.py
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth import get_user_model
from payfast.models import PayFastPayment, PayFastNotification
import uuid
User = get_user_model()
class CheckoutViewTests(TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass123'
)
self.client.login(username='testuser', password='testpass123')
def test_checkout_view_requires_login(self):
"""Test checkout requires authentication"""
self.client.logout()
response = self.client.get(reverse('payfast:checkout'))
self.assertEqual(response.status_code, 302) # Redirect to login
def test_checkout_view_creates_payment(self):
"""Test checkout creates payment record"""
response = self.client.get(
reverse('payfast:checkout'),
{
'amount': 99.99,
'item_name': 'Test Item',
'email_address': 'test@example.com',
}
)
self.assertEqual(response.status_code, 200)
# Check payment was created
payment_exists = PayFastPayment.objects.filter(
user=self.user,
amount=99.99
).exists()
self.assertTrue(payment_exists)
class WebhookViewTests(TestCase):
def setUp(self):
self.client = Client()
self.payment = PayFastPayment.objects.create(
m_payment_id='test-123',
amount=99.99,
item_name='Test',
email_address='test@example.com',
)
def test_webhook_post_creates_notification(self):
"""Test webhook creates notification record"""
webhook_data = {
'm_payment_id': 'test-123',
'pf_payment_id': '1234567',
'payment_status': 'COMPLETE',
'amount_gross': '99.99',
'amount_fee': '5.75',
'amount_net': '94.24',
'item_name': 'Test',
'email_address': 'test@example.com',
}
response = self.client.post(
reverse('payfast:notify'),
data=webhook_data
)
# Check notification was created
notification_exists = PayFastNotification.objects.filter(
payment=self.payment
).exists()
self.assertTrue(notification_exists)
Testing Signals
# tests/test_signals.py
from django.test import TestCase
from payfast.models import PayFastPayment
from unittest.mock import patch, MagicMock
class SignalTests(TestCase):
@patch('myapp.signals.grant_access')
def test_payment_complete_signal(self, mock_grant_access):
"""Test signal fires on payment completion"""
# Create payment
payment = PayFastPayment.objects.create(
m_payment_id='test-123',
amount=99.99,
item_name='Test',
email_address='test@example.com',
status='pending',
)
# Mark as complete
payment.mark_complete()
payment.save()
# Check signal handler was called
mock_grant_access.assert_called_once()
Testing Webhooks
Mocking PayFast Webhooks
# tests/test_webhooks.py
from django.test import TestCase, Client
from django.urls import reverse
from payfast.models import PayFastPayment
from payfast.utils import generate_signature
class WebhookTests(TestCase):
def setUp(self):
self.client = Client()
self.payment = PayFastPayment.objects.create(
m_payment_id='test-123',
amount=99.99,
item_name='Test Product',
email_address='test@example.com',
)
def _generate_webhook_data(self, payment_status='COMPLETE'):
"""Helper to generate valid webhook data"""
data = {
'm_payment_id': self.payment.m_payment_id,
'pf_payment_id': '1234567',
'payment_status': payment_status,
'amount_gross': str(self.payment.amount),
'amount_fee': '5.75',
'amount_net': '94.24',
'item_name': self.payment.item_name,
'email_address': self.payment.email_address,
}
# Generate valid signature
from django.conf import settings
signature = generate_signature(data, settings.PAYFAST_PASSPHRASE)
data['signature'] = signature
return data
def test_webhook_successful_payment(self):
"""Test webhook processes successful payment"""
webhook_data = self._generate_webhook_data('COMPLETE')
response = self.client.post(
reverse('payfast:notify'),
data=webhook_data
)
self.assertEqual(response.status_code, 200)
# Verify payment was updated
self.payment.refresh_from_db()
self.assertEqual(self.payment.status, 'complete')
self.assertEqual(self.payment.payment_status, 'COMPLETE')
def test_webhook_failed_payment(self):
"""Test webhook processes failed payment"""
webhook_data = self._generate_webhook_data('FAILED')
response = self.client.post(
reverse('payfast:notify'),
data=webhook_data
)
self.assertEqual(response.status_code, 200)
# Verify payment was marked as failed
self.payment.refresh_from_db()
self.assertEqual(self.payment.status, 'failed')
Manual Testing
Test Card Numbers
Use these test cards in PayFast sandbox:
Successful Payment:
Card Number: 4242 4242 4242 4242
Expiry: Any future date
CVV: Any 3 digits
Insufficient Funds:
Card Number: 4000 0000 0000 0002
Expiry: Any future date
CVV: Any 3 digits
Testing Payment Flow
Step-by-Step Manual Test:
Start Test Server
python manage.py runserver
Start ngrok (for webhook testing)
ngrok http 8000
Create Test Payment
Navigate to: http://localhost:8000/payfast/checkout/?amount=99.99&item_name=Test
Complete Payment
Use test card: 4242 4242 4242 4242
Expiry: 12/25
CVV: 123
Verify Webhook
Check Django admin: /admin/payfast/payfastnotification/
Verify Payment Status
Check: /admin/payfast/payfastpayment/
Testing Checklist
Before Production
Unit Tests:
☐ Model creation and methods
☐ Form validation and signature generation
☐ Utility functions
☐ Signal handlers
Integration Tests:
☐ View endpoints
☐ Webhook processing
☐ Database transactions
☐ Authentication/authorization
Manual Tests:
☐ Complete payment flow (sandbox)
☐ Failed payment handling
☐ Cancelled payment handling
☐ Webhook delivery
☐ Email notifications
☐ User interface flows
Security Tests:
☐ Signature verification
☐ IP validation
☐ CSRF protection
☐ SQL injection prevention
☐ XSS prevention
Production Readiness:
☐ HTTPS enabled
☐ Production credentials configured
☐ PAYFAST_TEST_MODE = False
☐ Webhook URL accessible
☐ Error monitoring configured
☐ Logging enabled
☐ Backup strategy in place
Next Steps
Webhooks Guide - Understanding webhooks in detail
security - Security best practices
Troubleshooting Guide - Solving common issues