July 3rd - Review and optimize codes

This commit is contained in:
2026-07-03 14:38:49 -04:00
parent d0b1b7ae23
commit 4c275fc0e5
16 changed files with 353 additions and 136 deletions
+44
View File
@@ -0,0 +1,44 @@
# Tests
The first automated tests for JQC. Kept deliberately small and targeted at the
places where a bug is **catastrophic and non-obvious** — the money path
(Stripe webhooks), the SLA state machine, and the multi-tenant routing
invariant.
## Running
```bash
pip install -r requirements-dev.txt
pytest
```
No MySQL, Redis, or Stripe account is required. `tests/conftest.py` sets
throwaway `SECRET_KEY` / `DATABASE_URL` (in-memory SQLite) at import time so the
`app` package imports cleanly, and every test either exercises pure functions or
uses the in-memory engine.
## What's covered
| File | Scope | Infra needed |
|---|---|---|
| `test_sla.py` | SLA thresholds + `ok`/`at_risk`/`breached`/`None` boundaries | none |
| `test_billing_webhooks.py` | `_ts_to_dt`, status normalization, `handle_event` dispatch + error-swallowing | none |
| `test_tenant_routing.py` | Routing is inert with no tenant; resolved `g.tenant_engine` wins | in-memory SQLite |
## Not yet covered (needs a live control DB + ≥2 tenant DBs)
These are the highest-value **integration** tests to add next. They require a
throwaway MySQL control DB plus two provisioned tenant DBs, so they live outside
this fast unit suite for now:
1. **Cross-tenant isolation** — resolve tenant A, write a row, resolve tenant B,
assert the row is invisible. Proves `RoutingSession` never leaks across
tenants. This is the single most important test the project can have.
2. **Webhook handlers end-to-end**`_on_payment_failed` sets `past_due_since`
and `subscription_status='past_due'`; `_on_payment_succeeded` clears the
dunning columns. Assert against a real `Tenant` row.
3. **`bootstrap_tenant` vs `upgrade_tenant`** — a freshly bootstrapped tenant DB
ends stamped at chain head with the full schema (the logic
`scripts/scratch_bootstrap_test.py` proves manually today).
4. **Billing gate state machine** — trial-expired redirects to
`/billing/subscribe`; `cancelled` redirects to `/billing/suspended`.
+38
View File
@@ -0,0 +1,38 @@
"""
tests/conftest.py
-----------------
Shared pytest fixtures + import-time environment setup.
`config.py` calls `_require_env('SECRET_KEY')` and `_require_env('DATABASE_URL')`
at *import* time, so those must be present in os.environ before `app` is
imported anywhere. We set safe, throwaway values here (SQLite in-memory) so the
whole suite can run with no MySQL, no Redis, no Stripe, and no real secrets.
The pure-logic tests (SLA math, webhook helpers) never touch the DB. The
routing-inertness test uses the in-memory SQLite engine only.
"""
import os
# ── Import-time env (must be set BEFORE `import app`) ────────────────────────
os.environ.setdefault('SECRET_KEY', 'test-secret-key')
os.environ.setdefault('DATABASE_URL', 'sqlite:///:memory:')
os.environ.setdefault('MULTI_TENANT_ENABLED', 'false')
os.environ.setdefault('BILLING_ENABLED', 'false')
import pytest # noqa: E402
@pytest.fixture(scope='session')
def app():
"""A minimal single-tenant app on in-memory SQLite (multi-tenancy inert)."""
from app import create_app
application = create_app('default')
application.config.update(TESTING=True, WTF_CSRF_ENABLED=False, SQLALCHEMY_ECHO=False)
return application
@pytest.fixture
def app_ctx(app):
with app.app_context():
yield app
+81
View File
@@ -0,0 +1,81 @@
"""
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': {}}})
+84
View File
@@ -0,0 +1,84 @@
"""
Unit tests for the SLA engine (app/utils/sla.py).
These are pure-logic tests — they build a lightweight fake issue object with the
three attributes `sla_status`/`sla_deadline`/`sla_hours_remaining` read
(`severity`, `status`, `reported_at`) and assert the boundary behaviour of the
SLA state machine. No database, no app context required.
"""
from datetime import timedelta
from app.utils.sla import (
sla_status, sla_deadline, sla_hours_remaining, SLA_HOURS, AT_RISK_THRESHOLD,
)
from app.utils.time_utils import now_eastern
class FakeIssue:
"""Minimal stand-in for the Issue ORM object used by the SLA helpers."""
def __init__(self, severity, status='open', hours_ago=0.0):
self.severity = severity
self.status = status
self.reported_at = now_eastern() - timedelta(hours=hours_ago)
# ── sla_deadline ─────────────────────────────────────────────────────────────
def test_deadline_matches_window():
issue = FakeIssue('high', hours_ago=0)
expected = issue.reported_at + timedelta(hours=SLA_HOURS['high'])
assert sla_deadline(issue) == expected
def test_deadline_none_for_unknown_severity():
assert sla_deadline(FakeIssue('bogus')) is None
# ── sla_status boundaries ────────────────────────────────────────────────────
def test_status_ok_when_fresh():
# critical window is 4h; reported 1h ago → well inside → ok
assert sla_status(FakeIssue('critical', hours_ago=1)) == 'ok'
def test_status_at_risk_past_threshold():
# critical window 4h, at-risk at 3h (0.75 * 4). Reported 3.5h ago → at_risk.
assert AT_RISK_THRESHOLD == 0.75
assert sla_status(FakeIssue('critical', hours_ago=3.5)) == 'at_risk'
def test_status_breached_past_window():
# critical window 4h; reported 5h ago → breached
assert sla_status(FakeIssue('critical', hours_ago=5)) == 'breached'
def test_status_none_when_resolved():
# A resolved issue has no SLA regardless of how old it is.
assert sla_status(FakeIssue('critical', status='resolved', hours_ago=99)) is None
def test_status_none_for_unknown_severity():
assert sla_status(FakeIssue('bogus', hours_ago=1)) is None
def test_low_severity_window_is_120h_not_168h():
"""Regression guard: CLAUDE.md once documented low=168h; code says 120h."""
assert SLA_HOURS['low'] == 120
# ── sla_hours_remaining ──────────────────────────────────────────────────────
def test_hours_remaining_positive_when_fresh():
issue = FakeIssue('medium', hours_ago=0) # window 72h
remaining = sla_hours_remaining(issue)
assert 71.0 <= remaining <= 72.0
def test_hours_remaining_negative_when_breached():
issue = FakeIssue('critical', hours_ago=6) # window 4h → ~-2h
assert sla_hours_remaining(issue) < 0
def test_hours_remaining_none_when_resolved():
assert sla_hours_remaining(FakeIssue('high', status='resolved')) is None
+40
View File
@@ -0,0 +1,40 @@
"""
Tests for the tenant routing layer (app/tenancy/routing.py).
The crown-jewel invariant of the multi-tenant design is data isolation: a
request bound to tenant A must never touch tenant B's database. Full isolation
proof requires two live tenant DBs and belongs in an integration suite (see
tests/README.md). What we CAN verify cheaply and deterministically here is the
other half of the contract:
"The routing layer is completely inert until a tenant is resolved."
If routing were NOT inert when MULTI_TENANT_ENABLED=false, the existing
single-tenant deployment would break. This test locks that guarantee in.
"""
from flask import g
def test_routing_is_inert_without_a_resolved_tenant(app):
"""With no g.tenant_engine set, db.session binds to the default engine."""
from app import db
with app.test_request_context('/'):
# Multi-tenancy disabled → the resolver never sets g.tenant_engine.
assert g.get('tenant_engine') is None
# …so get_bind() must fall through to the app's default engine.
assert db.session.get_bind() is db.engine
def test_resolved_tenant_engine_wins(app, monkeypatch):
"""When g.tenant_engine IS set, get_bind() returns it, not the default."""
from app import db
class _SentinelEngine:
"""Stand-in object; get_bind should return it verbatim when present."""
sentinel = _SentinelEngine()
with app.test_request_context('/'):
g.tenant_engine = sentinel
assert db.session.get_bind() is sentinel