""" 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': {}}})