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:

  1. Sign up at https://sandbox.payfast.co.za

  2. Navigate to Settings → Integration

  3. Copy your sandbox credentials

  4. 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:

  1. Start Test Server

    python manage.py runserver
    
  2. Start ngrok (for webhook testing)

    ngrok http 8000
    
  3. Create Test Payment

    Navigate to: http://localhost:8000/payfast/checkout/?amount=99.99&item_name=Test

  4. Complete Payment

    • Use test card: 4242 4242 4242 4242

    • Expiry: 12/25

    • CVV: 123

  5. Verify Webhook

    Check Django admin: /admin/payfast/payfastnotification/

  6. 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