204 lines
6.9 KiB
Python
204 lines
6.9 KiB
Python
import uuid
|
|
|
|
import pytest
|
|
from django.dispatch import receiver
|
|
from django.test import override_settings
|
|
|
|
from infrasynth.audit.models import SecurityEvent
|
|
from infrasynth.billing.models import BillingPlan, PaymentGateway
|
|
from infrasynth.billing.services import BillingService
|
|
from infrasynth.billing.signals import (
|
|
invoice_generated,
|
|
subscription_cancelled,
|
|
subscription_created,
|
|
)
|
|
from infrasynth.notifications.models import ChannelConfig, NotificationDispatch, NotificationTemplate
|
|
from infrasynth.notifications.services import NotificationService
|
|
from infrasynth.shared.enums import ChannelType
|
|
from infrasynth.webhooks.signals import outbound_delivery_succeeded
|
|
|
|
pytestmark = pytest.mark.django_db
|
|
|
|
|
|
@pytest.fixture
|
|
def gateway():
|
|
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 notification_template():
|
|
return NotificationTemplate.objects.create(
|
|
slug="sub-created",
|
|
name="Subscription Created",
|
|
channel=ChannelType.EMAIL,
|
|
subject_template="Your subscription is active",
|
|
body_template="Hello, your {{ plan }} plan is now active.",
|
|
is_html=False,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def channel_config():
|
|
return ChannelConfig.objects.create(
|
|
slug="email",
|
|
channel_type=ChannelType.EMAIL,
|
|
display_name="Email Channel",
|
|
config={},
|
|
is_active=True,
|
|
)
|
|
|
|
|
|
class TestSubscriptionCreatedSignalIntegration:
|
|
def test_subscription_created_signal_integration(self, user, plan, gateway):
|
|
sent = []
|
|
receiver_fn = lambda **kw: sent.append(kw) # noqa: E731
|
|
subscription_created.connect(receiver_fn, weak=False)
|
|
|
|
try:
|
|
BillingService().create_subscription(plan, user, gateway, external_id="sub_int_1")
|
|
|
|
assert len(sent) == 1
|
|
assert sent[0]["user"] == user
|
|
assert sent[0]["plan_slug"] == "pro"
|
|
assert sent[0]["gateway"] == "fake"
|
|
assert sent[0]["external_id"] == "sub_int_1"
|
|
assert sent[0]["signal"] is subscription_created
|
|
finally:
|
|
subscription_created.disconnect(receiver_fn)
|
|
|
|
|
|
class TestBillingSignalTriggersNotificationDispatch:
|
|
@override_settings(
|
|
INFRASYNTH_NOTIFICATIONS={
|
|
"DISPATCH_BACKEND": "celery",
|
|
"CHANNELS": {
|
|
"email": {
|
|
"primary": "infrasynth.notifications.channels.email_smtp.SMTPChannel",
|
|
},
|
|
},
|
|
}
|
|
)
|
|
def test_billing_signal_triggers_notification_dispatch(
|
|
self, user, plan, gateway, notification_template, channel_config
|
|
):
|
|
dispatch_created = []
|
|
|
|
@receiver(subscription_created, weak=False)
|
|
def handle_sub_created(**kwargs):
|
|
svc = NotificationService()
|
|
d = svc.send(
|
|
recipient=kwargs["user"].email,
|
|
subject="Subscription Active",
|
|
body="Your subscription is active.",
|
|
channel=ChannelType.EMAIL,
|
|
template=notification_template,
|
|
context={"plan": kwargs["plan_slug"]},
|
|
)
|
|
dispatch_created.append(d)
|
|
|
|
try:
|
|
BillingService().create_subscription(plan, user, gateway, external_id="sub_notif_1")
|
|
|
|
assert len(dispatch_created) == 1
|
|
dispatch = dispatch_created[0]
|
|
assert isinstance(dispatch, NotificationDispatch)
|
|
assert dispatch.recipient == user.email
|
|
assert dispatch.channel == ChannelType.EMAIL
|
|
assert dispatch.template == notification_template
|
|
assert dispatch.status == NotificationDispatch.Status.PENDING
|
|
finally:
|
|
subscription_created.disconnect(handle_sub_created)
|
|
|
|
|
|
class TestWebhookSignalTriggersAuditLog:
|
|
def test_webhook_signal_triggers_audit_log(self, user):
|
|
events_created = []
|
|
|
|
@receiver(outbound_delivery_succeeded, weak=False)
|
|
def create_security_event(**kwargs):
|
|
event = SecurityEvent.objects.create(
|
|
event_type="webhook_delivery_succeeded",
|
|
actor=user,
|
|
metadata={
|
|
"delivery_id": kwargs["delivery_id"],
|
|
"event_name": kwargs.get("event_name", ""),
|
|
"status_code": kwargs.get("status_code", 0),
|
|
},
|
|
request_id=str(uuid.uuid4()),
|
|
)
|
|
events_created.append(event)
|
|
|
|
try:
|
|
outbound_delivery_succeeded.send(
|
|
sender=None,
|
|
delivery_id=42,
|
|
event_name="subscription.created",
|
|
status_code=200,
|
|
)
|
|
|
|
assert len(events_created) == 1
|
|
event = SecurityEvent.objects.get(pk=events_created[0].pk)
|
|
assert event.event_type == "webhook_delivery_succeeded"
|
|
assert event.actor == user
|
|
assert event.metadata["delivery_id"] == 42
|
|
assert event.metadata["event_name"] == "subscription.created"
|
|
assert event.metadata["status_code"] == 200
|
|
finally:
|
|
outbound_delivery_succeeded.disconnect(create_security_event)
|
|
|
|
|
|
class TestSubscriptionCancelledSignal:
|
|
def test_subscription_cancelled_signal(self, user, plan, gateway):
|
|
subscription = BillingService().create_subscription(plan, user, gateway, external_id="sub_to_cancel")
|
|
|
|
sent = []
|
|
receiver_fn = lambda **kw: sent.append(kw) # noqa: E731
|
|
subscription_cancelled.connect(receiver_fn, weak=False)
|
|
|
|
try:
|
|
BillingService().cancel_subscription(subscription)
|
|
|
|
assert len(sent) == 1
|
|
assert sent[0]["user"] == user
|
|
assert sent[0]["plan_slug"] == "pro"
|
|
assert sent[0]["reason"] == "user_requested"
|
|
assert sent[0]["signal"] is subscription_cancelled
|
|
finally:
|
|
subscription_cancelled.disconnect(receiver_fn)
|
|
|
|
|
|
class TestInvoiceGeneratedSignal:
|
|
def test_invoice_generated_signal(self, user, plan, gateway):
|
|
subscription = BillingService().create_subscription(plan, user, gateway, external_id="sub_inv_1")
|
|
|
|
sent = []
|
|
receiver_fn = lambda **kw: sent.append(kw) # noqa: E731
|
|
invoice_generated.connect(receiver_fn, weak=False)
|
|
|
|
try:
|
|
invoice = BillingService().generate_invoice(subscription)
|
|
|
|
assert len(sent) == 1
|
|
assert sent[0]["invoice_id"] == invoice.id
|
|
assert sent[0]["amount"] == 49.0
|
|
assert sent[0]["signal"] is invoice_generated
|
|
finally:
|
|
invoice_generated.disconnect(receiver_fn)
|