82 lines
3.0 KiB
Python
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': {}}})
|