Files
resolutionflow/backend/tests/test_subscription_guards.py
Michael Chihlas 9ec208f6e7 feat(deps): add require_active_subscription guard with allowlist
Mounts on Pro routers (trees, sessions, scripts, FlowPilot, etc.) and
returns 402 with structured detail when an account's subscription is
missing or locked. Allowlist bypasses billing/account/auth flows so
users can recover from a lapsed subscription.

Conftest now seeds a default Pro/active Subscription on test_user and
test_admin (delete-then-insert because the register endpoint already
creates a free/active sub by default). Two existing tests adapted to
the new seeded plan; tenant-isolation tests seed Subscription rows for
the accounts they create directly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 19:14:30 -04:00

90 lines
3.7 KiB
Python

"""Tests for require_active_subscription dependency.
Verifies the 402 gating logic for Pro-guarded routers and the allowlist
that lets billing/account/auth flows through even when locked.
"""
import uuid
import pytest
from datetime import datetime, timezone, timedelta
from sqlalchemy import delete
from app.models.subscription import Subscription
async def _set_subscription(test_db, account_id, **fields):
"""Replace any existing Subscription on the account with one matching `fields`."""
await test_db.execute(delete(Subscription).where(Subscription.account_id == account_id))
test_db.add(Subscription(account_id=account_id, **fields))
await test_db.commit()
@pytest.mark.asyncio
async def test_active_subscription_passes(client, test_db, test_user, auth_headers):
account_id = uuid.UUID(test_user["user_data"]["account_id"])
await _set_subscription(test_db, account_id, plan="pro", status="active")
response = await client.get("/api/v1/trees", headers=auth_headers)
assert response.status_code != 402
@pytest.mark.asyncio
async def test_complimentary_subscription_passes(client, test_db, test_user, auth_headers):
account_id = uuid.UUID(test_user["user_data"]["account_id"])
await _set_subscription(test_db, account_id, plan="pro", status="complimentary")
response = await client.get("/api/v1/trees", headers=auth_headers)
assert response.status_code != 402
@pytest.mark.asyncio
async def test_trialing_unexpired_passes(client, test_db, test_user, auth_headers):
account_id = uuid.UUID(test_user["user_data"]["account_id"])
await _set_subscription(
test_db, account_id,
plan="pro", status="trialing",
current_period_end=datetime.now(timezone.utc) + timedelta(days=5),
)
response = await client.get("/api/v1/trees", headers=auth_headers)
assert response.status_code != 402
@pytest.mark.asyncio
async def test_trialing_expired_returns_402(client, test_db, test_user, auth_headers):
account_id = uuid.UUID(test_user["user_data"]["account_id"])
await _set_subscription(
test_db, account_id,
plan="pro", status="trialing",
current_period_end=datetime.now(timezone.utc) - timedelta(hours=1),
)
response = await client.get("/api/v1/trees", headers=auth_headers)
assert response.status_code == 402
body = response.json()
assert body["detail"]["error"] == "subscription_inactive"
@pytest.mark.asyncio
async def test_canceled_returns_402(client, test_db, test_user, auth_headers):
account_id = uuid.UUID(test_user["user_data"]["account_id"])
await _set_subscription(test_db, account_id, plan="pro", status="canceled")
response = await client.get("/api/v1/trees", headers=auth_headers)
assert response.status_code == 402
@pytest.mark.asyncio
async def test_no_subscription_returns_402(client, test_db, test_user, auth_headers):
account_id = uuid.UUID(test_user["user_data"]["account_id"])
# Remove the seeded default subscription
await test_db.execute(delete(Subscription).where(Subscription.account_id == account_id))
await test_db.commit()
response = await client.get("/api/v1/trees", headers=auth_headers)
assert response.status_code == 402
body = response.json()
assert body["detail"]["error"] == "no_subscription"
@pytest.mark.asyncio
async def test_auth_me_bypasses_guard(client, test_db, test_user, auth_headers):
"""Allowlisted route works even when subscription is canceled."""
account_id = uuid.UUID(test_user["user_data"]["account_id"])
await _set_subscription(test_db, account_id, plan="pro", status="canceled")
response = await client.get("/api/v1/auth/me", headers=auth_headers)
assert response.status_code == 200