Files
xiaoxia-saas/tests/unit/test_catalog.py
T
xiaoxia 042512a527
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
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 / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 28s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m17s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m56s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m39s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 5m1s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 5m16s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m44s
CI/CD Pipeline / Validate - Style (push) Successful in 5m53s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 6m18s
CI/CD Pipeline / Integration Tests (push) Successful in 6m23s
AI Code Review / AI Code Review (pull_request) Successful in 6m54s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
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) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 8m20s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m32s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m20s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m41s
CI/CD Pipeline / Validate - Security (push) Successful in 15m1s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 5m43s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 6m3s
CI/CD Pipeline / Unit Tests (push) Successful in 16m38s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
feat: 用户端套餐/积分包改读管理后台真实数据 (#2167)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-10-04 01:01:02 +08:00

199 lines
6.3 KiB
Python

"""catalog 应用服务单测:会员套餐 / 积分包从共享库读取与字段映射。"""
from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
@pytest.fixture(autouse=True)
def _clear_cache():
from packages.application.catalog import admin_catalog
admin_catalog._cache.clear()
yield
admin_catalog._cache.clear()
def _row(**kw):
row = MagicMock()
for k, v in kw.items():
setattr(row, k, v)
return row
class TestMembershipPlans:
def test_yearly_plan_mapping(self):
from packages.application.catalog import admin_catalog
row = _row(
plan_key="premium_yearly",
name="高级会员年卡",
description="年度订阅",
monthly_price=0,
yearly_price=399,
quotas={"4k": True, "batch_render": True, "credits_per_month": 500},
display_order=1,
)
session = MagicMock()
session.execute.return_value.fetchall.return_value = [row]
sl = MagicMock(return_value=session)
with patch("packages.adapters.sqlalchemy_impl.session.SessionLocal", sl, create=True):
plans = admin_catalog.get_membership_plans()
assert len(plans) == 1
p = plans[0]
assert p["plan_id"] == "premium_yearly"
assert p["billing_cycle"] == "yearly"
assert p["price_cents"] == 39900
assert p["monthly_price_cents"] == 3325
assert p["duration_days"] == 365
assert p["features"]["4K 超清分辨率"] is True
assert p["features"]["credits_per_month"] == 500
session.close.assert_called_once()
def test_monthly_plan_mapping(self):
from packages.application.catalog import admin_catalog
row = _row(
plan_key="premium_monthly",
name="高级会员月卡",
description=None,
monthly_price=39,
yearly_price=0,
quotas=None,
display_order=2,
)
session = MagicMock()
session.execute.return_value.fetchall.return_value = [row]
sl = MagicMock(return_value=session)
with patch("packages.adapters.sqlalchemy_impl.session.SessionLocal", sl, create=True):
plans = admin_catalog.get_membership_plans()
assert len(plans) == 1
p = plans[0]
assert p["billing_cycle"] == "monthly"
assert p["price_cents"] == 3900
assert p["monthly_price_cents"] == 3900
assert p["duration_days"] == 30
assert p["features"] == {}
def test_both_cycles_expanded(self):
from packages.application.catalog import admin_catalog
row = _row(
plan_key="premium",
name="高级会员",
description=None,
monthly_price=39,
yearly_price=399,
quotas={},
display_order=1,
)
session = MagicMock()
session.execute.return_value.fetchall.return_value = [row]
sl = MagicMock(return_value=session)
with patch("packages.adapters.sqlalchemy_impl.session.SessionLocal", sl, create=True):
plans = admin_catalog.get_membership_plans()
cycles = {p["billing_cycle"] for p in plans}
assert cycles == {"yearly", "monthly"}
def test_no_session_returns_empty(self):
from packages.application.catalog import admin_catalog
with patch("packages.adapters.sqlalchemy_impl.session.SessionLocal", None, create=True):
assert admin_catalog.get_membership_plans() == []
class TestPointsPackages:
def test_package_mapping_with_bonus(self):
from packages.application.catalog import admin_catalog
row = _row(
package_key="pkg_100",
name="100元充值包",
price=100,
credits=1000,
bonus_credits=100,
is_recommended=True,
description="推荐",
sort_order=4,
)
session = MagicMock()
session.execute.return_value.fetchall.return_value = [row]
sl = MagicMock(return_value=session)
with patch("packages.adapters.sqlalchemy_impl.session.SessionLocal", sl, create=True):
packages = admin_catalog.get_points_packages()
assert len(packages) == 1
pkg = packages[0]
assert pkg["code"] == "pkg_100"
assert pkg["points"] == 1100
assert pkg["price_cents"] == 10000
assert pkg["is_recommended"] is True
assert pkg["unit_price"] == "¥0.091/积分"
def test_zero_credits_unit_price_safe(self):
from packages.application.catalog import admin_catalog
row = _row(
package_key="pkg_0",
name="空包",
price=0,
credits=0,
bonus_credits=0,
is_recommended=False,
description=None,
sort_order=0,
)
session = MagicMock()
session.execute.return_value.fetchall.return_value = [row]
sl = MagicMock(return_value=session)
with patch("packages.adapters.sqlalchemy_impl.session.SessionLocal", sl, create=True):
packages = admin_catalog.get_points_packages()
assert packages[0]["points"] == 0
assert packages[0]["price_cents"] == 0
assert packages[0]["unit_price"] == "¥0.000/积分"
def test_no_session_returns_empty(self):
from packages.application.catalog import admin_catalog
with patch("packages.adapters.sqlalchemy_impl.session.SessionLocal", None, create=True):
assert admin_catalog.get_points_packages() == []
class TestPackagesRoute:
def test_get_packages_route_returns_items(self):
from app.api.routes.points import get_packages
cu = MagicMock()
cu.user.member_type = None
rows = [
{
"code": "pkg_10",
"name": "10元充值包",
"points": 100,
"price_cents": 1000,
"unit_price": "¥0.100/积分",
}
]
with patch(
"packages.application.catalog.admin_catalog.get_points_packages",
return_value=rows,
):
resp = get_packages(current_user=cu)
assert len(resp.packages) == 1
item = resp.packages[0]
assert item.code == "pkg_10"
assert item.points == 100
assert item.price_cents == 1000