246 lines
8.0 KiB
Python
246 lines
8.0 KiB
Python
"""What the background work has been doing, made answerable.
|
|
|
|
Three jobs run unattended and every one reported only into a log file. "How many
|
|
customers lapsed this month and how many we could not reach" was answerable in
|
|
principle and, in practice, never answered.
|
|
|
|
These are read-only summaries. The tests care about two things: the numbers are
|
|
right, and they are superadmin-only — every one of them is a platform-wide view,
|
|
which is precisely what a workspace must not have.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, datetime, timedelta, timezone
|
|
|
|
import pytest
|
|
|
|
from app.core.tenant_context import scoped_to, unscoped
|
|
|
|
from .conftest import requires_db, utc_today
|
|
|
|
pytestmark = requires_db
|
|
|
|
TODAY = utc_today()
|
|
|
|
|
|
@pytest.fixture
|
|
def superadmin_token(client, user_factory):
|
|
user_factory(tenant=None, email="ops@example.com", is_superadmin=True)
|
|
token = client.post(
|
|
"/api/auth/signin",
|
|
json={"email": "ops@example.com", "password": "CorrectHorse!9"},
|
|
).json()["access_token"]
|
|
return {"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
@pytest.fixture
|
|
def member_token(client, tenant_factory, user_factory):
|
|
tenant = tenant_factory()
|
|
user_factory(tenant=tenant, email="member@example.com")
|
|
token = client.post(
|
|
"/api/auth/signin",
|
|
json={"email": "member@example.com", "password": "CorrectHorse!9"},
|
|
).json()["access_token"]
|
|
return {"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"path",
|
|
["subscription-notices", "outbox", "sessions"],
|
|
)
|
|
def test_a_workspace_member_cannot_see_the_platform_view(client, member_token, path):
|
|
"""Every one of these counts across all customers. That is the whole point,
|
|
and the reason a workspace must not reach it."""
|
|
assert client.get(
|
|
f"/api/admin/operations/{path}", headers=member_token
|
|
).status_code == 403
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"path",
|
|
["subscription-notices", "outbox", "sessions"],
|
|
)
|
|
def test_a_superadmin_may(client, superadmin_token, path):
|
|
assert client.get(
|
|
f"/api/admin/operations/{path}", headers=superadmin_token
|
|
).status_code == 200
|
|
|
|
|
|
def test_notices_are_summarised_by_kind(client, db, superadmin_token,
|
|
tenant_factory, plan_factory):
|
|
from app.services.auth import subscription_notices
|
|
|
|
plan = plan_factory(grace_period_days=5)
|
|
tenant = tenant_factory(
|
|
plan_id=plan.id, end_date=TODAY + timedelta(days=2),
|
|
billing_email="pays@example.com",
|
|
)
|
|
|
|
with unscoped():
|
|
subscription_notices.record(
|
|
db,
|
|
subscription_notices.Notice(
|
|
tenant_id=tenant.id, tenant_name=tenant.tenant_name,
|
|
kind=subscription_notices.EXPIRING_SOON,
|
|
for_end_date=tenant.end_date, to="pays@example.com",
|
|
),
|
|
)
|
|
db.commit()
|
|
|
|
body = client.get(
|
|
"/api/admin/operations/subscription-notices", headers=superadmin_token
|
|
).json()
|
|
|
|
assert body["by_kind"].get(subscription_notices.EXPIRING_SOON) == 1
|
|
assert body["total"] >= 1
|
|
assert any(r["tenant_name"] == tenant.tenant_name for r in body["recent"])
|
|
|
|
|
|
def test_workspaces_nobody_can_be_warned_about_are_counted(
|
|
client, db, superadmin_token, tenant_factory, plan_factory
|
|
):
|
|
"""The number that matters. A workspace with no billing address gets no
|
|
warning at all before it lapses, and the only previous sign was a log line."""
|
|
plan = plan_factory()
|
|
tenant_factory(
|
|
plan_id=plan.id, end_date=TODAY + timedelta(days=60), billing_email=None
|
|
)
|
|
|
|
with unscoped():
|
|
db.commit()
|
|
|
|
body = client.get(
|
|
"/api/admin/operations/subscription-notices", headers=superadmin_token
|
|
).json()
|
|
|
|
assert body["workspaces_with_no_billing_contact"] >= 1
|
|
|
|
|
|
def test_a_workspace_with_no_end_date_is_not_counted_as_unreachable(
|
|
client, db, superadmin_token, tenant_factory, plan_factory
|
|
):
|
|
"""It will never be chased, so having nobody to chase is not a gap."""
|
|
plan = plan_factory()
|
|
tenant_factory(plan_id=plan.id, end_date=None, billing_email=None)
|
|
|
|
with unscoped():
|
|
db.commit()
|
|
before = client.get(
|
|
"/api/admin/operations/subscription-notices", headers=superadmin_token
|
|
).json()["workspaces_with_no_billing_contact"]
|
|
|
|
tenant_factory(plan_id=plan.id, end_date=None, billing_email=None)
|
|
db.commit()
|
|
|
|
after = client.get(
|
|
"/api/admin/operations/subscription-notices", headers=superadmin_token
|
|
).json()["workspaces_with_no_billing_contact"]
|
|
|
|
assert after == before
|
|
|
|
|
|
def test_a_module_that_keeps_refusing_shows_up(client, db, superadmin_token,
|
|
tenant_factory, module_factory,
|
|
environment_factory,
|
|
tenant_module_factory,
|
|
monkeypatch):
|
|
"""`stuck` is the number worth an alert: pending, overdue, already retried.
|
|
|
|
A module refusing deliveries for a day appears here long before anyone
|
|
notices its data is stale.
|
|
"""
|
|
from app.services.auth import event_service
|
|
from app.services.auth.event_service import EventService
|
|
|
|
class Refuses:
|
|
status_code = 503
|
|
text = "unavailable"
|
|
|
|
monkeypatch.setattr(
|
|
event_service.httpx, "post", lambda *a, **kw: Refuses()
|
|
)
|
|
|
|
tenant = tenant_factory()
|
|
module = module_factory()
|
|
environment_factory(module, slug="prod", is_default=True)
|
|
tenant_module_factory(tenant, module)
|
|
|
|
with unscoped():
|
|
EventService.emit_event(
|
|
db, "TENANT_UPDATED", {"tenant_id": str(tenant.id)}, tenant_id=tenant.id
|
|
)
|
|
from app.models.system.event_log_model import EventLog
|
|
|
|
row = db.query(EventLog).order_by(EventLog.created_at.desc()).first()
|
|
EventService.process_queue_item(db, str(row.event_id))
|
|
|
|
row.next_retry_at = datetime.now(timezone.utc) - timedelta(minutes=1)
|
|
db.commit()
|
|
|
|
body = client.get(
|
|
"/api/admin/operations/outbox", headers=superadmin_token
|
|
).json()
|
|
|
|
assert body["stuck"] >= 1
|
|
assert any("503" in (t["last_error"] or "") for t in body["failing_targets"])
|
|
|
|
|
|
def test_a_healthy_outbox_reports_nothing_stuck(client, db, superadmin_token):
|
|
body = client.get(
|
|
"/api/admin/operations/outbox", headers=superadmin_token
|
|
).json()
|
|
assert body["stuck"] == 0
|
|
assert body["failing_targets"] == []
|
|
|
|
|
|
def test_a_detected_token_reuse_is_surfaced(client, db, superadmin_token,
|
|
tenant_factory, user_factory):
|
|
"""A security signal, not a capacity one.
|
|
|
|
Non-zero means somebody presented a refresh token the legitimate client had
|
|
already spent — which is what a copied token looks like. Nothing surfaced it
|
|
before; it was a revocation reason in a column nobody queried.
|
|
"""
|
|
from app.services.auth import session_service
|
|
|
|
tenant = tenant_factory()
|
|
user = user_factory(tenant=tenant)
|
|
|
|
with unscoped():
|
|
session = session_service.start(db, user)
|
|
spent = session.current_jti
|
|
session_service.rotate(db, spent)
|
|
|
|
with pytest.raises(session_service.SessionRevoked):
|
|
session_service.rotate(db, spent)
|
|
db.commit()
|
|
|
|
body = client.get(
|
|
"/api/admin/operations/sessions", headers=superadmin_token
|
|
).json()
|
|
|
|
assert body["reuse_detected"] >= 1
|
|
assert body["ended_by_reason"].get("reuse_detected", 0) >= 1
|
|
|
|
|
|
def test_live_sessions_are_counted(client, db, superadmin_token, tenant_factory,
|
|
user_factory):
|
|
from app.services.auth import session_service
|
|
|
|
tenant = tenant_factory()
|
|
user = user_factory(tenant=tenant)
|
|
|
|
with unscoped():
|
|
before = client.get(
|
|
"/api/admin/operations/sessions", headers=superadmin_token
|
|
).json()["active"]
|
|
session_service.start(db, user)
|
|
db.commit()
|
|
|
|
after = client.get(
|
|
"/api/admin/operations/sessions", headers=superadmin_token
|
|
).json()["active"]
|
|
|
|
assert after == before + 1
|