"""Unit tests for app.services.access — the paid-tier gate. No DB; we hand-construct ``User`` rows and ``CurrentUser`` principals directly. The point is to nail down the truth table: tier | credit_until | active | source -------------|-------------------|--------|-------- free | None | False | None free | past | False | None free | future | True | credit paid | None | True | tier paid | future | True | tier (tier wins) enterprise | None | True | tier admin bearer | n/a | True | (bypass) """ from __future__ import annotations from datetime import datetime, timedelta, timezone from types import SimpleNamespace import pytest from app.auth import CurrentUser from app.services.access import is_paid_active, paid_status def _utcnow() -> datetime: return datetime.now(timezone.utc) def _make_user(*, tier: str = "free", credit_until: datetime | None = None): """Build something User-shaped without touching SQLAlchemy.""" return SimpleNamespace(tier=tier, credit_until=credit_until) # --------------------------------------------------------------------------- # paid_status — the truth table # --------------------------------------------------------------------------- def test_paid_status_free_no_credit(): st = paid_status(_make_user(tier="free")) assert st.active is False assert st.source is None assert st.expires_at is None assert st.days_remaining is None def test_paid_status_free_expired_credit(): st = paid_status(_make_user(tier="free", credit_until=_utcnow() - timedelta(days=1))) assert st.active is False assert st.source is None def test_paid_status_free_future_credit(): expiry = _utcnow() + timedelta(days=45) st = paid_status(_make_user(tier="free", credit_until=expiry)) assert st.active is True assert st.source == "credit" assert st.expires_at == expiry # Allow ±1 day slack for clock drift; integer-days floors. assert 44 <= st.days_remaining <= 45 def test_paid_status_paid_tier_no_credit(): st = paid_status(_make_user(tier="paid")) assert st.active is True assert st.source == "tier" assert st.expires_at is None def test_paid_status_paid_tier_wins_over_credit(): """A paid subscription dominates — we surface 'tier' even if a credit row also exists. Avoids confusing the user with 'X days remaining' when they're actually on a rolling subscription.""" st = paid_status(_make_user(tier="paid", credit_until=_utcnow() + timedelta(days=10))) assert st.source == "tier" assert st.days_remaining is None def test_paid_status_enterprise_tier(): st = paid_status(_make_user(tier="enterprise")) assert st.active is True assert st.source == "tier" def test_paid_status_none_user(): """No DB row → no paid status. Admin bearer-token hits this path.""" st = paid_status(None) assert st.active is False assert st.source is None def test_paid_status_handles_naive_datetime(): """MariaDB+aiomysql sometimes returns DateTime(timezone=True) as a naive datetime. The helper must normalise rather than raising 'can't compare offset-naive and offset-aware'.""" naive_future = (_utcnow() + timedelta(days=5)).replace(tzinfo=None) st = paid_status(_make_user(credit_until=naive_future)) assert st.active is True assert st.source == "credit" # --------------------------------------------------------------------------- # is_paid_active — sugar + admin bypass # --------------------------------------------------------------------------- def test_is_paid_active_admin_bearer_bypass(): """Admin bearer-token (is_admin=True, user=None) always passes — the dev/CLI path must not be artificially gated.""" principal = CurrentUser(is_admin=True, user=None) assert is_paid_active(principal) is True def test_is_paid_active_free_user_principal(): principal = CurrentUser(is_admin=False, user=_make_user(tier="free")) assert is_paid_active(principal) is False def test_is_paid_active_paid_user_principal(): principal = CurrentUser(is_admin=False, user=_make_user(tier="paid")) assert is_paid_active(principal) is True def test_is_paid_active_accepts_bare_user(): """Sugar: accepts a User row directly, not just a CurrentUser.""" assert is_paid_active(_make_user(tier="paid")) is True assert is_paid_active(_make_user(tier="free")) is False def test_is_paid_active_none(): assert is_paid_active(None) is False