Files
xiaoxia-saas/tests/unit/test_subscription_routes_legacy.py
T
xiaoxia 67a1ed6430
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 2s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 37s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 38s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Successful in 20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 43s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m3s
CI/CD Pipeline / Integration Tests (push) Successful in 3m7s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m40s
CI/CD Pipeline / Validate - Style (push) Successful in 3m52s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m17s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 3m6s
CI/CD Pipeline / Validate - Security (push) Successful in 7m14s
CI/CD Pipeline / Unit Tests (push) Successful in 9m33s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 35m54s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
feat(#1894): 清理文案库多余字段 / 废弃标题库 API / 旧定价档位清理 (#1968)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-16 18:58:04 +08:00

187 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""#1894 旧档位归一化逻辑测试(覆盖 _build_subscription_info / change_plan / cancel 等分支)"""
from __future__ import annotations
from dataclasses import dataclass, replace
from datetime import UTC, datetime
from unittest import mock
import pytest
from app.api.routes import subscription
from app.auth import AuthenticatedUser
from app.schemas.subscription import (
BillingCycle,
ChangePlanRequest,
MembershipType,
ToggleAutoRenewRequest,
)
@dataclass
class _FakeUserModel:
id: str = "u-1234567890"
subscription_plan: str | None = MembershipType.FREE
subscription_status: str | None = "active"
subscription_expires_at: datetime | None = None
created_at: datetime | None = None
@pytest.fixture
def auth_user():
return AuthenticatedUser(user=_FakeUserModel())
class TestBuildSubscriptionInfoLegacy:
"""覆盖 _build_subscription_info 旧档位归一化(subscription.py 57-58 行)"""
def test_legacy_standard_plan_normalized_to_monthly(self, auth_user):
new_user = replace(auth_user.user, subscription_plan="standard")
auth_user = replace(auth_user, user=new_user)
info = subscription._build_subscription_info(auth_user)
assert info.plan_id == MembershipType.MONTHLY
assert info.plan_name == "月卡会员"
def test_legacy_pro_plan_normalized_to_monthly(self, auth_user):
new_user = replace(auth_user.user, subscription_plan="pro")
auth_user = replace(auth_user, user=new_user)
info = subscription._build_subscription_info(auth_user)
# 旧 pro/standard/enterprise 都归一化到 monthly(按代码逻辑 {standard,pro,enterprise} → monthly)
assert info.plan_id == MembershipType.MONTHLY
def test_legacy_enterprise_plan_normalized(self, auth_user):
new_user = replace(auth_user.user, subscription_plan="enterprise")
auth_user = replace(auth_user, user=new_user)
info = subscription._build_subscription_info(auth_user)
assert info.plan_id == MembershipType.MONTHLY
def test_no_expiry_gives_now_period(self, auth_user):
"""无过期时间时 period_start 和 period_end 都为 now(覆盖 else 分支 55-56 行)"""
new_user = replace(auth_user.user, subscription_expires_at=None)
auth_user = replace(auth_user, user=new_user)
info = subscription._build_subscription_info(auth_user)
# 两个时间都应非空且接近当前时间
assert info.current_period_start
assert info.current_period_end
def test_free_user_billing_cycle_defaults_to_monthly(self, auth_user):
"""免费用户 billing_cycle 回落到 monthly(覆盖第 64 行 !=FREE 判定 else 分支)"""
new_user = replace(auth_user.user, subscription_plan=MembershipType.FREE)
auth_user = replace(auth_user, user=new_user)
info = subscription._build_subscription_info(auth_user)
assert info.billing_cycle == BillingCycle.MONTHLY
assert info.amount == 0
def test_yearly_user_passthrough(self, auth_user):
"""yearly 用户档位直接透传"""
new_user = replace(auth_user.user, subscription_plan=MembershipType.YEARLY)
auth_user = replace(auth_user, user=new_user)
info = subscription._build_subscription_info(auth_user)
assert info.plan_id == MembershipType.YEARLY
assert info.plan_name == "年卡会员"
class TestChangePlanValidation:
"""覆盖 change_plan 入参校验 / 同档位提示 / 旧档位归一化"""
@pytest.mark.asyncio
async def test_invalid_plan_returns_400(self, auth_user):
"""无效 plan_id → 400(覆盖 169 行)"""
from fastapi import HTTPException
req = ChangePlanRequest(target_plan_id="totally_bogus_plan", billing_cycle=BillingCycle.MONTHLY)
with pytest.raises(HTTPException) as exc:
await subscription.change_plan(request=req, current_user=auth_user, user_repository=mock.MagicMock())
assert exc.value.status_code == 400
assert "无效" in exc.value.detail
@pytest.mark.asyncio
async def test_invalid_billing_cycle_returns_400(self, auth_user):
"""无效 billing_cycle → 400(覆盖 176/178-179 行)"""
from fastapi import HTTPException
req = ChangePlanRequest(target_plan_id=MembershipType.MONTHLY, billing_cycle="bogus_cycle")
with pytest.raises(HTTPException) as exc:
await subscription.change_plan(request=req, current_user=auth_user, user_repository=mock.MagicMock())
assert exc.value.status_code == 400
@pytest.mark.asyncio
async def test_same_plan_returns_message(self, auth_user):
"""同档位变更 → 返回提示(覆盖 185 行分支)"""
new_user = replace(auth_user.user, subscription_plan=MembershipType.MONTHLY)
auth_user = replace(auth_user, user=new_user)
req = ChangePlanRequest(target_plan_id=MembershipType.MONTHLY, billing_cycle=BillingCycle.MONTHLY)
resp = await subscription.change_plan(request=req, current_user=auth_user, user_repository=mock.MagicMock())
assert resp.success is False
assert "已经是" in resp.message
@pytest.mark.asyncio
async def test_legacy_plan_normalized_for_same_plan_check(self, auth_user):
"""旧档位用户升级到 monthly → 应先归一化 current_plan 到 monthly,再判定为'同档位'"""
new_user = replace(auth_user.user, subscription_plan="standard")
auth_user = replace(auth_user, user=new_user)
# legacy → monthly
req = ChangePlanRequest(target_plan_id=MembershipType.MONTHLY, billing_cycle=BillingCycle.MONTHLY)
resp = await subscription.change_plan(request=req, current_user=auth_user, user_repository=mock.MagicMock())
# standard 归一化到 monthly,所以 target monthly == current monthly → same plan
assert resp.success is False
class TestCancelSubscription:
"""覆盖 cancel_subscription 分支"""
@pytest.mark.asyncio
async def test_cancel_free_user_returns_400(self, auth_user):
"""免费用户取消订阅 → 400(覆盖 252 行)"""
from fastapi import HTTPException
new_user = replace(auth_user.user, subscription_plan=MembershipType.FREE)
auth_user = replace(auth_user, user=new_user)
with pytest.raises(HTTPException) as exc:
await subscription.cancel_subscription(current_user=auth_user, user_repository=mock.MagicMock())
assert exc.value.status_code == 400
assert "免费" in exc.value.detail
@pytest.mark.asyncio
async def test_cancel_paid_user_marks_cancelled(self, auth_user):
"""付费用户取消订阅 → save 被调用且 subscription_status='cancelled'"""
new_user = replace(auth_user.user, subscription_plan=MembershipType.MONTHLY)
auth_user = replace(auth_user, user=new_user)
repo = mock.MagicMock()
resp = await subscription.cancel_subscription(current_user=auth_user, user_repository=repo)
assert resp.success is True
repo.save.assert_called_once()
saved_user = repo.save.call_args[0][0]
assert saved_user.subscription_status == "cancelled"
class TestToggleAutoRenew:
"""覆盖 toggle_auto_renew"""
@pytest.mark.asyncio
@pytest.mark.parametrize("enabled,msg", [(True, "已开启"), (False, "已关闭")])
async def test_toggle_returns_message(self, auth_user, enabled, msg):
req = ToggleAutoRenewRequest(enabled=enabled)
resp = await subscription.toggle_auto_renew(request=req, current_user=auth_user)
assert resp.success is True
assert msg in resp.message
class TestBuildSubscriptionInfoEdgeCases:
"""覆盖 _build_subscription_info 的边缘分支"""
def test_created_at_none_uses_now(self, auth_user):
"""user.created_at 为 None 时,created_at 字段回落到 now.isoformat(覆盖 69 行)"""
new_user = replace(auth_user.user, created_at=None, subscription_plan=MembershipType.MONTHLY)
auth_user2 = replace(auth_user, user=new_user)
info = subscription._build_subscription_info(auth_user2)
assert info.created_at # 非空
# 应为 ISO 格式字符串
from datetime import datetime
# 能解析即通过
datetime.fromisoformat(info.created_at)