From 81c74098bbc9e1da679ebdc487aa2b7570efb5b4 Mon Sep 17 00:00:00 2001 From: Carrington Muleya <79579279+Carrington-dev@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:21:56 +0200 Subject: [PATCH 1/4] Added first two test files Co-Authored-By: Carrington <215159144+carrington-cs@users.noreply.github.com> --- tests/test_client.py | 317 +++++++++++++++++++++++++++++++++++++++++++ tests/test_config.py | 113 +++++++++++++++ tests/test_models.py | 0 3 files changed, 430 insertions(+) create mode 100644 tests/test_client.py create mode 100644 tests/test_config.py create mode 100644 tests/test_models.py diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..ca46821 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,317 @@ +"""Tests for PayFast client""" + +import pytest +from unittest.mock import Mock, AsyncMock, patch +from fastapi import Request +from fastapi.responses import HTMLResponse + +from fastapi_payfast import ( + PayFastClient, + PayFastConfig, + PayFastPaymentData, + PayFastITNData, + PaymentStatus, + SignatureVerificationError, + InvalidMerchantError +) + + +@pytest.fixture +def config(): + """Fixture for PayFast configuration""" + return PayFastConfig( + merchant_id="10000100", + merchant_key="46f0cd694581a", + passphrase="jt7NOE43FZPn", + sandbox=True + ) + + +@pytest.fixture +def client(config): + """Fixture for PayFast client""" + return PayFastClient(config) + + +@pytest.fixture +def payment_data(config): + """Fixture for payment data""" + return PayFastPaymentData( + merchant_id=config.merchant_id, + merchant_key=config.merchant_key, + amount=100.00, + item_name="Test Product", + return_url="https://example.com/success", + cancel_url="https://example.com/cancel", + notify_url="https://example.com/notify" + ) + + +class TestPayFastClient: + """Test suite for PayFastClient""" + + def test_client_initialization(self, config): + """Test client initialization""" + client = PayFastClient(config) + assert client.config == config + + def test_create_payment(self, client, payment_data): + """Test creating payment request""" + result = client.create_payment(payment_data) + + assert 'action_url' in result + assert 'data' in result + assert result['action_url'] == "https://sandbox.payfast.co.za/eng/process" + assert 'signature' in result['data'] + assert result['data']['merchant_id'] == "10000100" + assert result['data']['amount'] == 100.00 + + def test_create_payment_signature_included(self, client, payment_data): + """Test that signature is included in payment data""" + result = client.create_payment(payment_data) + + assert 'signature' in result['data'] + assert len(result['data']['signature']) == 32 # MD5 hash + + def test_create_payment_removes_none_values(self, client, config): + """Test that None values are removed from payment data""" + payment_data = PayFastPaymentData( + merchant_id=config.merchant_id, + merchant_key=config.merchant_key, + amount=100.00, + item_name="Test Product", + item_description=None # None value + ) + + result = client.create_payment(payment_data) + assert 'item_description' not in result['data'] + + def test_generate_payment_form(self, client, payment_data): + """Test generating payment form HTML""" + html = client.generate_payment_form(payment_data) + + assert isinstance(html, str) + assert '' in html + assert 'sandbox.payfast.co.za' in html + assert 'merchant_id' in html + assert 'signature' in html + + def test_generate_payment_response(self, client, payment_data): + """Test generating payment HTMLResponse""" + response = client.generate_payment_response(payment_data) + + assert isinstance(response, HTMLResponse) + assert '' in response.body.decode() + + @pytest.mark.asyncio + async def test_verify_itn_success(self, client, config): + """Test successful ITN verification""" + # Create mock request with form data + form_data = { + 'merchant_id': config.merchant_id, + 'pf_payment_id': '12345', + 'payment_status': 'COMPLETE', + 'item_name': 'Test Product', + 'amount_gross': '100.00', + 'amount_fee': '5.00', + 'amount_net': '95.00', + } + + # Generate valid signature + from fastapi_payfast.utils import generate_signature + form_data['signature'] = generate_signature(form_data, config.passphrase) + + # Create mock request + request = Mock(spec=Request) + request.form = AsyncMock(return_value=form_data) + request.client = Mock() + request.client.host = "197.97.145.144" + + # Verify ITN + itn_data = await client.verify_itn(request) + + assert isinstance(itn_data, PayFastITNData) + assert itn_data.pf_payment_id == '12345' + assert itn_data.payment_status == PaymentStatus.COMPLETE + assert itn_data.amount_gross == 100.00 + + @pytest.mark.asyncio + async def test_verify_itn_missing_signature(self, client, config): + """Test ITN verification with missing signature""" + form_data = { + 'merchant_id': config.merchant_id, + 'pf_payment_id': '12345', + 'payment_status': 'COMPLETE', + 'item_name': 'Test Product', + 'amount_gross': '100.00', + 'amount_fee': '5.00', + 'amount_net': '95.00', + } + + request = Mock(spec=Request) + request.form = AsyncMock(return_value=form_data) + + with pytest.raises(SignatureVerificationError, match="Missing signature"): + await client.verify_itn(request) + + @pytest.mark.asyncio + async def test_verify_itn_invalid_signature(self, client, config): + """Test ITN verification with invalid signature""" + form_data = { + 'merchant_id': config.merchant_id, + 'pf_payment_id': '12345', + 'payment_status': 'COMPLETE', + 'item_name': 'Test Product', + 'amount_gross': '100.00', + 'amount_fee': '5.00', + 'amount_net': '95.00', + 'signature': 'invalid_signature_123' + } + + request = Mock(spec=Request) + request.form = AsyncMock(return_value=form_data) + + with pytest.raises(SignatureVerificationError, match="Signature mismatch"): + await client.verify_itn(request) + + @pytest.mark.asyncio + async def test_verify_itn_invalid_merchant(self, client, config): + """Test ITN verification with invalid merchant ID""" + form_data = { + 'merchant_id': 'wrong_merchant_id', + 'pf_payment_id': '12345', + 'payment_status': 'COMPLETE', + 'item_name': 'Test Product', + 'amount_gross': '100.00', + 'amount_fee': '5.00', + 'amount_net': '95.00', + } + + from fastapi_payfast.utils import generate_signature + form_data['signature'] = generate_signature(form_data, config.passphrase) + + request = Mock(spec=Request) + request.form = AsyncMock(return_value=form_data) + + with pytest.raises(InvalidMerchantError): + await client.verify_itn(request) + + def test_validate_payment_amount_exact_match(self, client): + """Test payment amount validation with exact match""" + itn_data = PayFastITNData( + pf_payment_id="12345", + payment_status=PaymentStatus.COMPLETE, + item_name="Test", + amount_gross=100.00, + amount_fee=5.00, + amount_net=95.00, + merchant_id="10000100", + signature="abc123" + ) + + assert client.validate_payment_amount(itn_data, 100.00) + + def test_validate_payment_amount_within_tolerance(self, client): + """Test payment amount validation within tolerance""" + itn_data = PayFastITNData( + pf_payment_id="12345", + payment_status=PaymentStatus.COMPLETE, + item_name="Test", + amount_gross=100.00, + amount_fee=5.00, + amount_net=95.00, + merchant_id="10000100", + signature="abc123" + ) + + assert client.validate_payment_amount(itn_data, 100.005) + + def test_validate_payment_amount_outside_tolerance(self, client): + """Test payment amount validation outside tolerance""" + itn_data = PayFastITNData( + pf_payment_id="12345", + payment_status=PaymentStatus.COMPLETE, + item_name="Test", + amount_gross=100.00, + amount_fee=5.00, + amount_net=95.00, + merchant_id="10000100", + signature="abc123" + ) + + assert not client.validate_payment_amount(itn_data, 105.00) + + def test_validate_payment_amount_custom_tolerance(self, client): + """Test payment amount validation with custom tolerance""" + itn_data = PayFastITNData( + pf_payment_id="12345", + payment_status=PaymentStatus.COMPLETE, + item_name="Test", + amount_gross=100.00, + amount_fee=5.00, + amount_net=95.00, + merchant_id="10000100", + signature="abc123" + ) + + assert client.validate_payment_amount(itn_data, 105.00, tolerance=5.0) + + def test_is_payment_successful_complete(self, client): + """Test is_payment_successful with COMPLETE status""" + itn_data = PayFastITNData( + pf_payment_id="12345", + payment_status=PaymentStatus.COMPLETE, + item_name="Test", + amount_gross=100.00, + amount_fee=5.00, + amount_net=95.00, + merchant_id="10000100", + signature="abc123" + ) + + assert client.is_payment_successful(itn_data) + + def test_is_payment_successful_failed(self, client): + """Test is_payment_successful with FAILED status""" + itn_data = PayFastITNData( + pf_payment_id="12345", + payment_status=PaymentStatus.FAILED, + item_name="Test", + amount_gross=100.00, + amount_fee=5.00, + amount_net=95.00, + merchant_id="10000100", + signature="abc123" + ) + + assert not client.is_payment_successful(itn_data) + + def test_is_payment_successful_pending(self, client): + """Test is_payment_successful with PENDING status""" + itn_data = PayFastITNData( + pf_payment_id="12345", + payment_status=PaymentStatus.PENDING, + item_name="Test", + amount_gross=100.00, + amount_fee=5.00, + amount_net=95.00, + merchant_id="10000100", + signature="abc123" + ) + + assert not client.is_payment_successful(itn_data) + + def test_is_payment_successful_cancelled(self, client): + """Test is_payment_successful with CANCELLED status""" + itn_data = PayFastITNData( + pf_payment_id="12345", + payment_status=PaymentStatus.CANCELLED, + item_name="Test", + amount_gross=100.00, + amount_fee=5.00, + amount_net=95.00, + merchant_id="10000100", + signature="abc123" + ) + + assert not client.is_payment_successful(itn_data) \ No newline at end of file diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..d47a7c0 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,113 @@ +"""Tests for PayFast configuration""" + +import pytest +from fastapi_payfast.config import PayFastConfig + + +class TestPayFastConfig: + """Test suite for PayFastConfig""" + + def test_config_initialization(self): + """Test basic configuration initialization""" + config = PayFastConfig( + merchant_id="10000100", + merchant_key="46f0cd694581a", + passphrase="jt7NOE43FZPn", + sandbox=True + ) + + assert config.merchant_id == "10000100" + assert config.merchant_key == "46f0cd694581a" + assert config.passphrase == "jt7NOE43FZPn" + assert config.sandbox is True + assert config.validate_ip is True + + def test_config_sandbox_process_url(self): + """Test sandbox process URL""" + config = PayFastConfig( + merchant_id="10000100", + merchant_key="46f0cd694581a", + passphrase="jt7NOE43FZPn", + sandbox=True + ) + + assert config.process_url == "https://sandbox.payfast.co.za/eng/process" + + def test_config_production_process_url(self): + """Test production process URL""" + config = PayFastConfig( + merchant_id="10000100", + merchant_key="46f0cd694581a", + passphrase="jt7NOE43FZPn", + sandbox=False + ) + + assert config.process_url == "https://www.payfast.co.za/eng/process" + + def test_config_sandbox_validate_url(self): + """Test sandbox validate URL""" + config = PayFastConfig( + merchant_id="10000100", + merchant_key="46f0cd694581a", + passphrase="jt7NOE43FZPn", + sandbox=True + ) + + assert config.validate_url == "https://sandbox.payfast.co.za/eng/query/validate" + + def test_config_production_validate_url(self): + """Test production validate URL""" + config = PayFastConfig( + merchant_id="10000100", + merchant_key="46f0cd694581a", + passphrase="jt7NOE43FZPn", + sandbox=False + ) + + assert config.validate_url == "https://www.payfast.co.za/eng/query/validate" + + def test_config_valid_ips(self): + """Test valid PayFast IP addresses""" + config = PayFastConfig( + merchant_id="10000100", + merchant_key="46f0cd694581a", + passphrase="jt7NOE43FZPn" + ) + + expected_ips = [ + "197.97.145.144", + "41.74.179.194", + ] + + assert config.valid_ips == expected_ips + + def test_config_immutability(self): + """Test that config is immutable""" + config = PayFastConfig( + merchant_id="10000100", + merchant_key="46f0cd694581a", + passphrase="jt7NOE43FZPn" + ) + + with pytest.raises(Exception): + config.merchant_id = "new_id" + + def test_config_missing_required_fields(self): + """Test that missing required fields raise validation error""" + with pytest.raises(Exception): + PayFastConfig( + merchant_id="10000100", + merchant_key="46f0cd694581a" + # Missing passphrase + ) + + def test_config_default_values(self): + """Test default configuration values""" + config = PayFastConfig( + merchant_id="10000100", + merchant_key="46f0cd694581a", + passphrase="jt7NOE43FZPn" + ) + + assert config.sandbox is True + assert config.validate_ip is True \ No newline at end of file diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..e69de29 From e9f2743e6e96c38938a1236ac019ed1088af5617 Mon Sep 17 00:00:00 2001 From: Carrington Muleya <79579279+Carrington-dev@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:23:16 +0200 Subject: [PATCH 2/4] """Tests for PayFast client""" """Tests for PayFast client""" Co-Authored-By: Carrington <215159144+carrington-cs@users.noreply.github.com> --- tests/test_models.py | 317 +++++++++++++++++++++++++++++++++++++++++++ tests/test_utils.py | 227 +++++++++++++++++++++++++++++++ 2 files changed, 544 insertions(+) create mode 100644 tests/test_utils.py diff --git a/tests/test_models.py b/tests/test_models.py index e69de29..ca46821 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -0,0 +1,317 @@ +"""Tests for PayFast client""" + +import pytest +from unittest.mock import Mock, AsyncMock, patch +from fastapi import Request +from fastapi.responses import HTMLResponse + +from fastapi_payfast import ( + PayFastClient, + PayFastConfig, + PayFastPaymentData, + PayFastITNData, + PaymentStatus, + SignatureVerificationError, + InvalidMerchantError +) + + +@pytest.fixture +def config(): + """Fixture for PayFast configuration""" + return PayFastConfig( + merchant_id="10000100", + merchant_key="46f0cd694581a", + passphrase="jt7NOE43FZPn", + sandbox=True + ) + + +@pytest.fixture +def client(config): + """Fixture for PayFast client""" + return PayFastClient(config) + + +@pytest.fixture +def payment_data(config): + """Fixture for payment data""" + return PayFastPaymentData( + merchant_id=config.merchant_id, + merchant_key=config.merchant_key, + amount=100.00, + item_name="Test Product", + return_url="https://example.com/success", + cancel_url="https://example.com/cancel", + notify_url="https://example.com/notify" + ) + + +class TestPayFastClient: + """Test suite for PayFastClient""" + + def test_client_initialization(self, config): + """Test client initialization""" + client = PayFastClient(config) + assert client.config == config + + def test_create_payment(self, client, payment_data): + """Test creating payment request""" + result = client.create_payment(payment_data) + + assert 'action_url' in result + assert 'data' in result + assert result['action_url'] == "https://sandbox.payfast.co.za/eng/process" + assert 'signature' in result['data'] + assert result['data']['merchant_id'] == "10000100" + assert result['data']['amount'] == 100.00 + + def test_create_payment_signature_included(self, client, payment_data): + """Test that signature is included in payment data""" + result = client.create_payment(payment_data) + + assert 'signature' in result['data'] + assert len(result['data']['signature']) == 32 # MD5 hash + + def test_create_payment_removes_none_values(self, client, config): + """Test that None values are removed from payment data""" + payment_data = PayFastPaymentData( + merchant_id=config.merchant_id, + merchant_key=config.merchant_key, + amount=100.00, + item_name="Test Product", + item_description=None # None value + ) + + result = client.create_payment(payment_data) + assert 'item_description' not in result['data'] + + def test_generate_payment_form(self, client, payment_data): + """Test generating payment form HTML""" + html = client.generate_payment_form(payment_data) + + assert isinstance(html, str) + assert '' in html + assert 'sandbox.payfast.co.za' in html + assert 'merchant_id' in html + assert 'signature' in html + + def test_generate_payment_response(self, client, payment_data): + """Test generating payment HTMLResponse""" + response = client.generate_payment_response(payment_data) + + assert isinstance(response, HTMLResponse) + assert '' in response.body.decode() + + @pytest.mark.asyncio + async def test_verify_itn_success(self, client, config): + """Test successful ITN verification""" + # Create mock request with form data + form_data = { + 'merchant_id': config.merchant_id, + 'pf_payment_id': '12345', + 'payment_status': 'COMPLETE', + 'item_name': 'Test Product', + 'amount_gross': '100.00', + 'amount_fee': '5.00', + 'amount_net': '95.00', + } + + # Generate valid signature + from fastapi_payfast.utils import generate_signature + form_data['signature'] = generate_signature(form_data, config.passphrase) + + # Create mock request + request = Mock(spec=Request) + request.form = AsyncMock(return_value=form_data) + request.client = Mock() + request.client.host = "197.97.145.144" + + # Verify ITN + itn_data = await client.verify_itn(request) + + assert isinstance(itn_data, PayFastITNData) + assert itn_data.pf_payment_id == '12345' + assert itn_data.payment_status == PaymentStatus.COMPLETE + assert itn_data.amount_gross == 100.00 + + @pytest.mark.asyncio + async def test_verify_itn_missing_signature(self, client, config): + """Test ITN verification with missing signature""" + form_data = { + 'merchant_id': config.merchant_id, + 'pf_payment_id': '12345', + 'payment_status': 'COMPLETE', + 'item_name': 'Test Product', + 'amount_gross': '100.00', + 'amount_fee': '5.00', + 'amount_net': '95.00', + } + + request = Mock(spec=Request) + request.form = AsyncMock(return_value=form_data) + + with pytest.raises(SignatureVerificationError, match="Missing signature"): + await client.verify_itn(request) + + @pytest.mark.asyncio + async def test_verify_itn_invalid_signature(self, client, config): + """Test ITN verification with invalid signature""" + form_data = { + 'merchant_id': config.merchant_id, + 'pf_payment_id': '12345', + 'payment_status': 'COMPLETE', + 'item_name': 'Test Product', + 'amount_gross': '100.00', + 'amount_fee': '5.00', + 'amount_net': '95.00', + 'signature': 'invalid_signature_123' + } + + request = Mock(spec=Request) + request.form = AsyncMock(return_value=form_data) + + with pytest.raises(SignatureVerificationError, match="Signature mismatch"): + await client.verify_itn(request) + + @pytest.mark.asyncio + async def test_verify_itn_invalid_merchant(self, client, config): + """Test ITN verification with invalid merchant ID""" + form_data = { + 'merchant_id': 'wrong_merchant_id', + 'pf_payment_id': '12345', + 'payment_status': 'COMPLETE', + 'item_name': 'Test Product', + 'amount_gross': '100.00', + 'amount_fee': '5.00', + 'amount_net': '95.00', + } + + from fastapi_payfast.utils import generate_signature + form_data['signature'] = generate_signature(form_data, config.passphrase) + + request = Mock(spec=Request) + request.form = AsyncMock(return_value=form_data) + + with pytest.raises(InvalidMerchantError): + await client.verify_itn(request) + + def test_validate_payment_amount_exact_match(self, client): + """Test payment amount validation with exact match""" + itn_data = PayFastITNData( + pf_payment_id="12345", + payment_status=PaymentStatus.COMPLETE, + item_name="Test", + amount_gross=100.00, + amount_fee=5.00, + amount_net=95.00, + merchant_id="10000100", + signature="abc123" + ) + + assert client.validate_payment_amount(itn_data, 100.00) + + def test_validate_payment_amount_within_tolerance(self, client): + """Test payment amount validation within tolerance""" + itn_data = PayFastITNData( + pf_payment_id="12345", + payment_status=PaymentStatus.COMPLETE, + item_name="Test", + amount_gross=100.00, + amount_fee=5.00, + amount_net=95.00, + merchant_id="10000100", + signature="abc123" + ) + + assert client.validate_payment_amount(itn_data, 100.005) + + def test_validate_payment_amount_outside_tolerance(self, client): + """Test payment amount validation outside tolerance""" + itn_data = PayFastITNData( + pf_payment_id="12345", + payment_status=PaymentStatus.COMPLETE, + item_name="Test", + amount_gross=100.00, + amount_fee=5.00, + amount_net=95.00, + merchant_id="10000100", + signature="abc123" + ) + + assert not client.validate_payment_amount(itn_data, 105.00) + + def test_validate_payment_amount_custom_tolerance(self, client): + """Test payment amount validation with custom tolerance""" + itn_data = PayFastITNData( + pf_payment_id="12345", + payment_status=PaymentStatus.COMPLETE, + item_name="Test", + amount_gross=100.00, + amount_fee=5.00, + amount_net=95.00, + merchant_id="10000100", + signature="abc123" + ) + + assert client.validate_payment_amount(itn_data, 105.00, tolerance=5.0) + + def test_is_payment_successful_complete(self, client): + """Test is_payment_successful with COMPLETE status""" + itn_data = PayFastITNData( + pf_payment_id="12345", + payment_status=PaymentStatus.COMPLETE, + item_name="Test", + amount_gross=100.00, + amount_fee=5.00, + amount_net=95.00, + merchant_id="10000100", + signature="abc123" + ) + + assert client.is_payment_successful(itn_data) + + def test_is_payment_successful_failed(self, client): + """Test is_payment_successful with FAILED status""" + itn_data = PayFastITNData( + pf_payment_id="12345", + payment_status=PaymentStatus.FAILED, + item_name="Test", + amount_gross=100.00, + amount_fee=5.00, + amount_net=95.00, + merchant_id="10000100", + signature="abc123" + ) + + assert not client.is_payment_successful(itn_data) + + def test_is_payment_successful_pending(self, client): + """Test is_payment_successful with PENDING status""" + itn_data = PayFastITNData( + pf_payment_id="12345", + payment_status=PaymentStatus.PENDING, + item_name="Test", + amount_gross=100.00, + amount_fee=5.00, + amount_net=95.00, + merchant_id="10000100", + signature="abc123" + ) + + assert not client.is_payment_successful(itn_data) + + def test_is_payment_successful_cancelled(self, client): + """Test is_payment_successful with CANCELLED status""" + itn_data = PayFastITNData( + pf_payment_id="12345", + payment_status=PaymentStatus.CANCELLED, + item_name="Test", + amount_gross=100.00, + amount_fee=5.00, + amount_net=95.00, + merchant_id="10000100", + signature="abc123" + ) + + assert not client.is_payment_successful(itn_data) \ No newline at end of file diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..e8d5d17 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,227 @@ +"""Tests for PayFast utility functions""" + +import pytest +from fastapi_payfast.utils import generate_signature, generate_payment_form_html + + +class TestGenerateSignature: + """Test suite for generate_signature function""" + + def test_generate_signature_basic(self): + """Test basic signature generation""" + data = { + 'merchant_id': '10000100', + 'merchant_key': '46f0cd694581a', + 'amount': '100.00', + 'item_name': 'Test Product' + } + + signature = generate_signature(data) + assert isinstance(signature, str) + assert len(signature) == 32 # MD5 hash length + + def test_generate_signature_with_passphrase(self): + """Test signature generation with passphrase""" + data = { + 'merchant_id': '10000100', + 'merchant_key': '46f0cd694581a', + 'amount': '100.00', + 'item_name': 'Test Product' + } + + signature_without = generate_signature(data) + signature_with = generate_signature(data, 'test_passphrase') + + assert signature_without != signature_with + + def test_generate_signature_deterministic(self): + """Test that signature generation is deterministic""" + data = { + 'merchant_id': '10000100', + 'merchant_key': '46f0cd694581a', + 'amount': '100.00', + 'item_name': 'Test Product' + } + + sig1 = generate_signature(data) + sig2 = generate_signature(data) + + assert sig1 == sig2 + + def test_generate_signature_order_independent(self): + """Test that signature is independent of dict order""" + data1 = { + 'merchant_id': '10000100', + 'amount': '100.00', + 'merchant_key': '46f0cd694581a', + 'item_name': 'Test Product' + } + + data2 = { + 'item_name': 'Test Product', + 'merchant_key': '46f0cd694581a', + 'amount': '100.00', + 'merchant_id': '10000100' + } + + assert generate_signature(data1) == generate_signature(data2) + + def test_generate_signature_ignores_none_values(self): + """Test that None values are ignored""" + data = { + 'merchant_id': '10000100', + 'merchant_key': '46f0cd694581a', + 'amount': '100.00', + 'item_name': 'Test Product', + 'item_description': None + } + + data_without_none = { + 'merchant_id': '10000100', + 'merchant_key': '46f0cd694581a', + 'amount': '100.00', + 'item_name': 'Test Product' + } + + assert generate_signature(data) == generate_signature(data_without_none) + + def test_generate_signature_ignores_empty_strings(self): + """Test that empty strings are ignored""" + data = { + 'merchant_id': '10000100', + 'merchant_key': '46f0cd694581a', + 'amount': '100.00', + 'item_name': 'Test Product', + 'item_description': '' + } + + data_without_empty = { + 'merchant_id': '10000100', + 'merchant_key': '46f0cd694581a', + 'amount': '100.00', + 'item_name': 'Test Product' + } + + assert generate_signature(data) == generate_signature(data_without_empty) + + def test_generate_signature_ignores_signature_field(self): + """Test that signature field is ignored""" + data = { + 'merchant_id': '10000100', + 'merchant_key': '46f0cd694581a', + 'amount': '100.00', + 'item_name': 'Test Product', + 'signature': 'old_signature' + } + + data_without_sig = { + 'merchant_id': '10000100', + 'merchant_key': '46f0cd694581a', + 'amount': '100.00', + 'item_name': 'Test Product' + } + + assert generate_signature(data) == generate_signature(data_without_sig) + + def test_generate_signature_special_characters(self): + """Test signature generation with special characters""" + data = { + 'merchant_id': '10000100', + 'merchant_key': '46f0cd694581a', + 'amount': '100.00', + 'item_name': 'Test Product & Special Chars!', + 'item_description': 'With spaces and @#$%' + } + + signature = generate_signature(data) + assert isinstance(signature, str) + assert len(signature) == 32 + + +class TestGeneratePaymentFormHTML: + """Test suite for generate_payment_form_html function""" + + def test_generate_form_basic(self): + """Test basic form generation""" + action_url = "https://sandbox.payfast.co.za/eng/process" + data = { + 'merchant_id': '10000100', + 'merchant_key': '46f0cd694581a', + 'amount': '100.00', + 'item_name': 'Test Product' + } + + html = generate_payment_form_html(action_url, data) + + assert isinstance(html, str) + assert '' in html + assert action_url in html + assert 'merchant_id' in html + assert '10000100' in html + + def test_generate_form_all_fields(self): + """Test form generation with all fields""" + action_url = "https://sandbox.payfast.co.za/eng/process" + data = { + 'merchant_id': '10000100', + 'merchant_key': '46f0cd694581a', + 'amount': '100.00', + 'item_name': 'Test Product', + 'item_description': 'Test Description', + 'return_url': 'https://example.com/success', + 'signature': 'abc123' + } + + html = generate_payment_form_html(action_url, data) + + for key, value in data.items(): + assert key in html + assert str(value) in html + + def test_generate_form_auto_submit_script(self): + """Test that form includes auto-submit script""" + action_url = "https://sandbox.payfast.co.za/eng/process" + data = {'merchant_id': '10000100'} + + html = generate_payment_form_html(action_url, data) + + assert 'submit()' in html + assert '