from unittest import mock import pytest from infrasynth.billing.gateways.base import ( BasePaymentGateway, CheckoutSessionResult, WebhookResult, ) from infrasynth.billing.gateways.mercadopago import MercadoPagoGateway from infrasynth.billing.gateways.stripe import StripeGateway from infrasynth.billing.gateways.wompi import WompiGateway pytestmark = pytest.mark.django_db class _FakePlan: slug = "pro" name = "Pro" price_amount = 100 price_currency = "USD" external_id = "price_123" class _FakeUser: email = "user@example.com" pk = 7 class _FakeSubscription: external_id = "sub_123" class _FakeInvoice: external_id = "inv_123" class TestBasePaymentGateway: def test_is_abstract(self): with pytest.raises(TypeError): BasePaymentGateway() def test_result_dataclasses(self): result = CheckoutSessionResult("s1", "https://x", "cs") assert result.session_id == "s1" webhook = WebhookResult("evt", True, {}) assert webhook.is_handled is True class TestStripeGateway: def test_missing_credentials_raise(self): gateway = StripeGateway({}) with pytest.raises(ValueError, match="API key"): gateway.create_checkout_session(_FakePlan(), _FakeUser()) def test_create_checkout_session(self): gateway = StripeGateway({"api_key": "sk_test"}) with mock.patch("stripe.checkout.Session.create") as create: create.return_value = mock.Mock(id="cs_1", url="https://stripe.com/checkout", client_secret="cs_sec") result = gateway.create_checkout_session(_FakePlan(), _FakeUser()) assert result.session_id == "cs_1" assert result.checkout_url == "https://stripe.com/checkout" create.assert_called_once() kwargs = create.call_args.kwargs assert kwargs["mode"] == "subscription" assert kwargs["line_items"] == [{"price": "price_123", "quantity": 1}] assert kwargs["customer_email"] == "user@example.com" def test_create_checkout_without_external_id_raises(self): gateway = StripeGateway({"api_key": "sk_test"}) plan = _FakePlan() plan.external_id = "" with pytest.raises(ValueError, match="external price ID"): gateway.create_checkout_session(plan, _FakeUser()) def test_handle_webhook(self): gateway = StripeGateway({"api_key": "sk_test", "webhook_secret": "whsec_1"}) with mock.patch("stripe.Webhook.construct_event") as construct: construct.return_value = { "type": "checkout.session.completed", "data": {"object": {"id": "cs_1"}}, } result = gateway.handle_webhook( {"id": "cs_1"}, {"Stripe-Signature": "t=1,v1=sig"}, ) assert result.event_type == "checkout.session.completed" assert result.is_handled is True assert result.data["id"] == "cs_1" def test_handle_webhook_without_secret_raises(self): gateway = StripeGateway({"api_key": "sk_test"}) with pytest.raises(ValueError, match="webhook secret"): gateway.handle_webhook({}, {}) def test_cancel_subscription(self): gateway = StripeGateway({"api_key": "sk_test"}) with mock.patch("stripe.Subscription.cancel") as cancel: assert gateway.cancel_subscription(_FakeSubscription()) is True cancel.assert_called_once_with("sub_123") def test_cancel_without_external_id(self): gateway = StripeGateway({"api_key": "sk_test"}) subscription = _FakeSubscription() subscription.external_id = "" assert gateway.cancel_subscription(subscription) is False def test_sync_subscription_maps_status(self): gateway = StripeGateway({"api_key": "sk_test"}) with mock.patch("stripe.Subscription.retrieve") as retrieve: retrieve.return_value = { "status": "past_due", "current_period_start": 1700000000, "current_period_end": 1702592000, "cancel_at_period_end": True, "canceled_at": None, "trial_end": None, "metadata": {"x": "1"}, } data = gateway.sync_subscription(_FakeSubscription()) assert data["status"] == "past_due" assert data["cancel_at_period_end"] is True assert data["current_period_end"] is not None assert data["metadata"] == {"x": "1"} def test_get_invoice(self): gateway = StripeGateway({"api_key": "sk_test"}) with mock.patch("stripe.Invoice.retrieve") as retrieve: retrieve.return_value = { "id": "inv_1", "status": "paid", "amount_due": 12000, "currency": "usd", "paid_at": 1700000000, "lines": {"data": [{"description": "Pro", "amount": 12000, "quantity": 1}]}, } data = gateway.get_invoice(_FakeInvoice()) assert data["status"] == "paid" assert data["amount"] == 120.0 assert data["currency"] == "USD" assert data["line_items"][0]["description"] == "Pro" def test_health_check(self): assert StripeGateway({"api_key": "k"}).health_check() is True assert StripeGateway({}).health_check() is False class TestMercadoPagoGateway: def test_missing_credentials_raise(self): gateway = MercadoPagoGateway({}) with pytest.raises(ValueError, match="access token"): gateway.create_checkout_session(_FakePlan(), _FakeUser()) def test_create_checkout_session(self): gateway = MercadoPagoGateway({"access_token": "APP_USR-token"}) with mock.patch("mercadopago.SDK") as sdk_cls: sdk_cls.return_value.preference().create.return_value = { "status": 201, "response": { "id": "pref_1", "init_point": "https://mercadopago.com/checkout", }, } result = gateway.create_checkout_session(_FakePlan(), _FakeUser()) assert result.session_id == "pref_1" assert result.checkout_url == "https://mercadopago.com/checkout" preference = sdk_cls.return_value.preference().create.call_args.args[0] assert preference["items"][0]["title"] == "Pro" assert preference["items"][0]["unit_price"] == 100 def test_create_checkout_error_status_raises(self): gateway = MercadoPagoGateway({"access_token": "tok"}) with mock.patch("mercadopago.SDK") as sdk_cls: sdk_cls.return_value.preference().create.return_value = { "status": 400, "response": {"message": "bad"}, } with pytest.raises(ValueError, match="MercadoPago error"): gateway.create_checkout_session(_FakePlan(), _FakeUser()) def test_handle_webhook(self): gateway = MercadoPagoGateway({"access_token": "tok"}) result = gateway.handle_webhook({"type": "payment", "data": {"id": "pay_1"}}, {}) assert result.event_type == "payment" assert result.is_handled is True assert result.data == {"id": "pay_1"} def test_cancel_subscription(self): gateway = MercadoPagoGateway({"access_token": "tok"}) with mock.patch("mercadopago.SDK") as sdk_cls: sdk_cls.return_value.preapproval().update.return_value = {"status": 200} assert gateway.cancel_subscription(_FakeSubscription()) is True sdk_cls.return_value.preapproval().update.assert_called_once_with("sub_123", {"status": "cancelled"}) def test_sync_subscription(self): gateway = MercadoPagoGateway({"access_token": "tok"}) with mock.patch("mercadopago.SDK") as sdk_cls: sdk_cls.return_value.preapproval().get.return_value = { "status": 200, "response": {"status": "cancelled", "metadata": {"a": 1}}, } data = gateway.sync_subscription(_FakeSubscription()) assert data["status"] == "cancelled" assert data["metadata"] == {"a": 1} def test_health_check(self): assert MercadoPagoGateway({"access_token": "t"}).health_check() is True assert MercadoPagoGateway({}).health_check() is False class TestWompiGateway: def test_missing_credentials_raise(self): gateway = WompiGateway({}) with pytest.raises(ValueError, match="public key"): gateway.create_checkout_session(_FakePlan(), _FakeUser()) def test_create_checkout_session(self): gateway = WompiGateway({"public_key": "pub_test"}) with mock.patch("infrasynth.billing.gateways.wompi.requests.post") as post: post.return_value.status_code = 201 post.return_value.json.return_value = {"data": {"id": "plink_1", "url": "https://checkout.wompi.co/link"}} result = gateway.create_checkout_session(_FakePlan(), _FakeUser()) assert result.session_id == "plink_1" assert result.checkout_url == "https://checkout.wompi.co/link" payload = post.call_args.kwargs["json"] assert payload["amount_in_cents"] == 10000 assert payload["currency"] == "usd" def test_create_checkout_http_error_raises(self): gateway = WompiGateway({"public_key": "pub_test"}) with mock.patch("infrasynth.billing.gateways.wompi.requests.post") as post: post.return_value.status_code = 400 post.return_value.text = "invalid" with pytest.raises(ValueError, match="400"): gateway.create_checkout_session(_FakePlan(), _FakeUser()) def test_handle_webhook_unverified_without_secret(self): gateway = WompiGateway({}) result = gateway.handle_webhook({"event": "transaction.updated", "data": {"id": "t_1"}}, {}) assert result.event_type == "transaction.updated" assert result.is_handled is True assert result.data == {"id": "t_1"} def test_handle_webhook_signature_verification(self): import hashlib import hmac import json payload = {"event": "transaction.updated", "data": {"id": "t_1"}} gateway = WompiGateway({"webhook_secret": "whsec"}) raw = json.dumps(payload, separators=(",", ":")) signature = hmac.new(b"whsec", raw.encode(), hashlib.sha256).hexdigest() result = gateway.handle_webhook(payload, {"x-signature": signature}) assert result.is_handled is True result = gateway.handle_webhook(payload, {"x-signature": "tampered"}) assert result.is_handled is False def test_cancel_subscription_voids_transaction(self): gateway = WompiGateway({"secret_key": "sk_test"}) with mock.patch("infrasynth.billing.gateways.wompi.requests.post") as post: post.return_value.status_code = 200 assert gateway.cancel_subscription(_FakeSubscription()) is True post.assert_called_once_with( "https://sandbox.wompi.co/v1/transactions/sub_123/void", headers={"Authorization": "Bearer sk_test"}, timeout=30, ) def test_cancel_without_secret_returns_false(self): gateway = WompiGateway({}) assert gateway.cancel_subscription(_FakeSubscription()) is False def test_sync_subscription(self): gateway = WompiGateway({"public_key": "pub"}) with mock.patch("infrasynth.billing.gateways.wompi.requests.get") as get: get.return_value.status_code = 200 get.return_value.json.return_value = {"data": {"status": "APPROVED", "metadata": {"m": 1}}} data = gateway.sync_subscription(_FakeSubscription()) assert data["status"] == "active" assert data["metadata"] == {"m": 1} def test_get_invoice(self): gateway = WompiGateway({"public_key": "pub"}) with mock.patch("infrasynth.billing.gateways.wompi.requests.get") as get: get.return_value.status_code = 200 get.return_value.json.return_value = { "data": { "id": "t_1", "status": "VOIDED", "amount_in_cents": 9900, "currency": "cop", } } data = gateway.get_invoice(_FakeInvoice()) assert data["status"] == "VOIDED" assert data["amount"] == 99.0 assert data["currency"] == "COP" def test_health_check(self): assert WompiGateway({"public_key": "p"}).health_check() is True assert WompiGateway({}).health_check() is False