222 lines
7.4 KiB
Python
222 lines
7.4 KiB
Python
import pytest
|
|
|
|
from infrasynth.billing.models import (
|
|
BillingPlan,
|
|
Invoice,
|
|
PaymentGateway,
|
|
PaymentTransaction,
|
|
Subscription,
|
|
)
|
|
from infrasynth.features.models import FeatureFlag
|
|
from infrasynth.shared.enums import SubscriptionStatus
|
|
|
|
pytestmark = pytest.mark.django_db
|
|
|
|
GATEWAYS_URL = "/api/billing/gateways/"
|
|
PLANS_URL = "/api/billing/plans/"
|
|
SUBSCRIPTIONS_URL = "/api/billing/subscriptions/"
|
|
INVOICES_URL = "/api/billing/invoices/"
|
|
TRANSACTIONS_URL = "/api/billing/transactions/"
|
|
WEBHOOK_URL = "/api/billing/webhook/receive/"
|
|
|
|
|
|
@pytest.fixture
|
|
def billing_enabled():
|
|
return FeatureFlag.objects.create(slug="billing", name="Billing", is_active=True)
|
|
|
|
|
|
@pytest.fixture
|
|
def gateway(billing_enabled):
|
|
return PaymentGateway.objects.create(
|
|
slug="fake",
|
|
display_name="Fake",
|
|
gateway_class="tests.helpers.FakeGateway",
|
|
is_active=True,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def plan(gateway):
|
|
return BillingPlan.objects.create(
|
|
slug="pro",
|
|
name="Pro",
|
|
price_amount="49.00",
|
|
price_currency="USD",
|
|
interval="monthly",
|
|
gateway=gateway,
|
|
external_id="price_123",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def subscription(user, plan, gateway):
|
|
return Subscription.objects.create(
|
|
user=user,
|
|
plan=plan,
|
|
gateway=gateway,
|
|
external_id="sub_1",
|
|
status=SubscriptionStatus.ACTIVE,
|
|
)
|
|
|
|
|
|
class TestFeatureGate:
|
|
def test_disabled_feature_returns_404(self, authenticated_client):
|
|
assert authenticated_client.get(GATEWAYS_URL).status_code == 404
|
|
assert authenticated_client.get(PLANS_URL).status_code == 404
|
|
assert authenticated_client.get(SUBSCRIPTIONS_URL).status_code == 404
|
|
|
|
|
|
class TestPaymentGatewayViewSet:
|
|
def test_list_gateways(self, authenticated_client, gateway):
|
|
response = authenticated_client.get(GATEWAYS_URL)
|
|
assert response.status_code == 200
|
|
assert response.data["count"] == 1
|
|
assert response.data["results"][0]["slug"] == "fake"
|
|
|
|
def test_create_gateway(self, authenticated_client, billing_enabled):
|
|
response = authenticated_client.post(
|
|
GATEWAYS_URL,
|
|
{
|
|
"slug": "stripe",
|
|
"display_name": "Stripe",
|
|
"gateway_class": "infrasynth.billing.gateways.stripe.StripeGateway",
|
|
},
|
|
format="json",
|
|
)
|
|
assert response.status_code == 201
|
|
assert PaymentGateway.objects.count() == 1
|
|
|
|
def test_requires_auth(self, api_client, gateway):
|
|
assert api_client.get(GATEWAYS_URL).status_code == 401
|
|
|
|
|
|
class TestBillingPlanViewSet:
|
|
def test_list_plans_public(self, api_client, plan):
|
|
response = api_client.get(PLANS_URL)
|
|
assert response.status_code == 200
|
|
assert response.data["count"] == 1
|
|
assert response.data["results"][0]["slug"] == "pro"
|
|
|
|
def test_retrieve_plan(self, api_client, plan):
|
|
response = api_client.get(f"{PLANS_URL}{plan.id}/")
|
|
assert response.status_code == 200
|
|
assert response.data["price_amount"] == "49.00"
|
|
assert response.data["gateway_info"]["slug"] == "fake"
|
|
|
|
def test_retrieve_inactive_plan_404(self, api_client, plan):
|
|
plan.is_active = False
|
|
plan.save(update_fields=["is_active"])
|
|
assert api_client.get(f"{PLANS_URL}{plan.id}/").status_code == 404
|
|
|
|
|
|
class TestSubscriptionViewSet:
|
|
def test_list_subscriptions(self, authenticated_client, subscription):
|
|
response = authenticated_client.get(SUBSCRIPTIONS_URL)
|
|
assert response.status_code == 200
|
|
assert response.data["count"] == 1
|
|
assert response.data["results"][0]["status"] == "active"
|
|
|
|
def test_requires_auth(self, api_client, subscription):
|
|
assert api_client.get(SUBSCRIPTIONS_URL).status_code == 401
|
|
|
|
def test_subscribe_action(self, authenticated_client, plan):
|
|
response = authenticated_client.post(
|
|
f"{SUBSCRIPTIONS_URL}subscribe/",
|
|
{"plan_slug": "pro"},
|
|
format="json",
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.data["checkout_url"] == "https://checkout.example.com/cs_fake_123"
|
|
assert response.data["session_id"] == "cs_fake_123"
|
|
assert response.data["gateway"] == "fake"
|
|
|
|
def test_subscribe_invalid_plan(self, authenticated_client):
|
|
response = authenticated_client.post(
|
|
f"{SUBSCRIPTIONS_URL}subscribe/",
|
|
{"plan_slug": "nope"},
|
|
format="json",
|
|
)
|
|
assert response.status_code == 404
|
|
|
|
|
|
class TestInvoiceViewSet:
|
|
def test_list_invoices(self, authenticated_client, subscription, user):
|
|
Invoice.objects.create(
|
|
subscription=subscription,
|
|
user=user,
|
|
invoice_number="INV-1",
|
|
amount=49,
|
|
currency="USD",
|
|
status="open",
|
|
)
|
|
response = authenticated_client.get(INVOICES_URL)
|
|
assert response.status_code == 200
|
|
assert response.data["count"] == 1
|
|
assert response.data["results"][0]["invoice_number"] == "INV-1"
|
|
|
|
def test_requires_auth(self, api_client, subscription, user):
|
|
assert api_client.get(INVOICES_URL).status_code == 401
|
|
|
|
|
|
class TestPaymentTransactionViewSet:
|
|
def test_list_transactions(self, authenticated_client, subscription, user):
|
|
PaymentTransaction.objects.create(
|
|
invoice=None,
|
|
gateway=subscription.gateway,
|
|
amount=49,
|
|
currency="USD",
|
|
status="approved",
|
|
)
|
|
response = authenticated_client.get(TRANSACTIONS_URL)
|
|
assert response.status_code == 200
|
|
assert response.data["count"] == 1
|
|
assert response.data["results"][0]["status"] == "approved"
|
|
|
|
def test_requires_auth(self, api_client, billing_enabled):
|
|
assert api_client.get(TRANSACTIONS_URL).status_code == 401
|
|
|
|
|
|
class TestWebhookReceiveView:
|
|
def test_receive_processed(self, api_client, gateway):
|
|
response = api_client.post(
|
|
WEBHOOK_URL,
|
|
{
|
|
"gateway_slug": "fake",
|
|
"event": "checkout.session.completed",
|
|
"data": {"id": "cs_1"},
|
|
},
|
|
format="json",
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.data["status"] == "processed"
|
|
assert response.data["event_type"] == "checkout.session.completed"
|
|
|
|
def test_receive_ignored_event(self, api_client, gateway):
|
|
response = api_client.post(
|
|
WEBHOOK_URL,
|
|
{"gateway_slug": "fake", "event": "ignored", "data": {}},
|
|
format="json",
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.data["status"] == "ignored"
|
|
|
|
def test_receive_uses_first_active_gateway(self, api_client, gateway):
|
|
response = api_client.post(
|
|
WEBHOOK_URL,
|
|
{"event": "checkout.session.completed", "data": {}},
|
|
format="json",
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.data["event_type"] == "checkout.session.completed"
|
|
|
|
def test_receive_no_gateway_404(self, api_client):
|
|
response = api_client.post(WEBHOOK_URL, {"event": "x", "data": {}}, format="json")
|
|
assert response.status_code == 404
|
|
|
|
def test_receive_is_public(self, api_client, gateway):
|
|
response = api_client.post(
|
|
WEBHOOK_URL,
|
|
{"gateway_slug": "fake", "event": "e", "data": {}},
|
|
format="json",
|
|
)
|
|
assert response.status_code == 200
|