Files
JQC_multi_tenant/tests/test_billing_webhooks.py

82 lines
3.0 KiB
Python

"""
Unit tests for the Stripe webhook dispatcher (app/billing/webhooks.py).
Covers the pure, DB-free surface:
- _ts_to_dt timestamp conversion
- _STRIPE_STATUS_MAP status normalization (the 'canceled'/'cancelled' trap)
- handle_event dispatch: unknown types are a no-op; known types call the right
handler; a handler that raises is swallowed (one bad event must not abort the
rest of a delivery batch).
The individual _on_* handlers open a control_session and are covered separately
by integration tests against a live control DB (not included here — see
tests/README.md).
"""
from datetime import datetime
import app.billing.webhooks as wh
# ── _ts_to_dt ────────────────────────────────────────────────────────────────
def test_ts_to_dt_none_returns_none():
assert wh._ts_to_dt(None) is None
def test_ts_to_dt_epoch():
# 0 → 1970-01-01T00:00:00, naive (tzinfo stripped)
dt = wh._ts_to_dt(0)
assert dt == datetime(1970, 1, 1, 0, 0, 0)
assert dt.tzinfo is None
def test_ts_to_dt_known_value():
# 2021-01-01T00:00:00Z == 1609459200
assert wh._ts_to_dt(1609459200) == datetime(2021, 1, 1, 0, 0, 0)
# ── _STRIPE_STATUS_MAP ───────────────────────────────────────────────────────
def test_status_map_both_cancel_spellings():
# Stripe emits 'canceled' (one L); we must normalize to our 'cancelled'.
assert wh._STRIPE_STATUS_MAP['canceled'] == 'cancelled'
assert wh._STRIPE_STATUS_MAP['cancelled'] == 'cancelled'
def test_status_map_trial_and_pastdue():
assert wh._STRIPE_STATUS_MAP['trialing'] == 'trial'
assert wh._STRIPE_STATUS_MAP['past_due'] == 'past_due'
assert wh._STRIPE_STATUS_MAP['unpaid'] == 'past_due'
# ── handle_event dispatch ────────────────────────────────────────────────────
def test_unknown_event_type_is_noop():
# Should simply return without raising and without touching any handler.
wh.handle_event({'type': 'ping.unhandled', 'data': {'object': {}}})
def test_known_event_dispatches_to_handler(monkeypatch):
called = {}
def fake_handler(obj):
called['obj'] = obj
monkeypatch.setattr(wh, '_on_payment_failed', fake_handler)
payload = {'customer': 'cus_123'}
wh.handle_event({'type': 'invoice.payment_failed', 'data': {'object': payload}})
assert called.get('obj') == payload
def test_handler_exception_is_swallowed(monkeypatch):
def boom(obj):
raise RuntimeError('stripe handler blew up')
monkeypatch.setattr(wh, '_on_subscription_deleted', boom)
# Must NOT propagate — a single malformed event cannot abort the batch.
wh.handle_event({'type': 'customer.subscription.deleted', 'data': {'object': {}}})