infrasynth-backend-kit/tests/test_billing/test_integration.py
jcv-dev 551b42eab5 feat: production-hardening pass across the kit
Close the gaps between the documented contract (API-STANDARD, TENANCY,
ENTITLEMENTS) and the implementation, and remove committed build artifacts.

Security:
- verify + process inbound webhooks (HMAC/handler verify, size limit,
  timestamp tolerance, idempotency via InboundEvent.external_id)
- real 2FA login flow (pre-auth challenge; tokens only after verify/recovery)
- wire HybridPermission into security/audit views; add API-key rotate and
  users/<id>/permissions|roles endpoints
- tenant-scoped throttling on by default; webhook replay protection
- verify MercadoPago webhook signatures
- login brute-force guard, configurable password policy, real ALTCHA PoW

Correctness:
- apply verified billing webhooks idempotently (subscription/entitlement/
  invoice/PaymentTransaction); scheduled payment lifecycle jobs
- capture audit update diffs automatically; add audit retention purge
- working notification retries, per-channel rate limits, log retention
- pluggable virus scanner, upload-size limit, pipeline toggle
- feature rollout %/environment targeting; settings-driven registrations
- workflow guards (instance cap, route depth, self-assignment, clone on re-entry)
- wire every previously-dead INFRASYNTH_* setting; drop truly dead ones

Delivery:
- README + CHANGELOG; CI format check + coverage gate
- keep test media out of the tree; untrack .coverage, __pycache__,
  egg-info, docs/ and invoice artifacts
2026-09-24 10:41:21 -05:00

213 lines
7.2 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 App, PaymentGateway, Plan
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, MonetizationModel
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 app():
return App.objects.create(slug="messenger", name="Messenger", monetization=MonetizationModel.SUBSCRIPTION)
@pytest.fixture
def plan(app, gateway):
return Plan.objects.create(
app=app,
slug="pro",
name="Pro",
price_amount=4900,
price_currency="USD",
interval="monthly",
gateway=gateway,
external_id="price_123",
)
@pytest.fixture
def notification_template():
return NotificationTemplate.all_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(tenant):
return ChannelConfig.objects.create(
tenant=tenant,
slug="email",
channel_type=ChannelType.EMAIL,
display_name="Email Channel",
config={},
is_active=True,
)
class TestSubscriptionCreatedSignalIntegration:
def test_subscription_created_signal_integration(self, tenant, plan, gateway):
sent = []
receiver_fn = lambda **kw: sent.append(kw) # noqa: E731
subscription_created.connect(receiver_fn, weak=False)
try:
BillingService().create_subscription(plan, tenant, gateway, external_id="sub_int_1")
assert len(sent) == 1
assert sent[0]["tenant_id"] == str(tenant.pk)
assert sent[0]["plan_slug"] == "pro"
assert sent[0]["app_slug"] == "messenger"
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, tenant, plan, gateway, notification_template, channel_config
):
dispatch_created = []
@receiver(subscription_created, weak=False)
def handle_sub_created(**kwargs):
svc = NotificationService()
d = svc.send(
recipient="owner@example.com",
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, tenant, gateway, external_id="sub_notif_1")
assert len(dispatch_created) == 1
dispatch = dispatch_created[0]
assert isinstance(dispatch, NotificationDispatch)
assert dispatch.recipient == "owner@example.com"
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, tenant, user):
events_created = []
@receiver(outbound_delivery_succeeded, weak=False)
def create_security_event(**kwargs):
event = SecurityEvent.objects.create(
tenant=tenant,
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, tenant, plan, gateway):
subscription = BillingService().create_subscription(plan, tenant, 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]["tenant_id"] == str(tenant.pk)
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, tenant, plan, gateway):
subscription = BillingService().create_subscription(plan, tenant, 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"] == 4900
assert sent[0]["signal"] is invoice_generated
finally:
invoice_generated.disconnect(receiver_fn)