Files
xiaoxia-saas/tests/unit/test_credit_system_switch.py
T
xiaoxia d959dd874f
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 / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 5s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (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
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m46s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m18s
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 / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (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 2m37s
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 / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m16s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m59s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 3m58s
CI/CD Pipeline / Integration Tests (push) Successful in 4m21s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 4m26s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 4m26s
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m9s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 5m14s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 52s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m7s
AI Code Review / AI Code Review (pull_request) Successful in 6m48s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m35s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m58s
CI/CD Pipeline / Validate - Security (push) Successful in 9m26s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m4s
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
feat(1895): 暂停积分系统 ENABLE_CREDIT_SYSTEM=false(保留全部代码/表/接口) (#1996)
Co-authored-by: backend-dev <dev@xiaoxiajianji.com>
Co-committed-by: backend-dev <dev@xiaoxiajianji.com>
2026-09-20 01:39:02 +08:00

230 lines
9.0 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.
"""积分系统暂停开关测试 (#1895, ENABLE_CREDIT_SYSTEM)。
产品要求:暂停积分系统但保留全部代码/表/接口。
- 默认 false:所有 AI 功能免费放行,不扣积分、不做余额拦截;
- /points/check 恒返回 allowed=True、required_points=0;
- /points/deduct 为 no-op,余额不变;
- 查询接口(balance/transactions/rules/packages/membership/usage)照常可用;
- 旧环境变量 POINTS_ENABLED 作为兼容别名仍可开启。
"""
from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
def _make_cu(user_id="user-1", is_member=False, member_type=None):
cu = MagicMock()
cu.user.id = user_id
cu.user.is_member = is_member
cu.user.member_type = member_type
cu.user.member_expires_at = None
return cu
# ── 配置层 ────────────────────────────────────────────────────────────────
class TestCreditSystemConfig:
def test_default_disabled(self):
import os
from packages.config.base import SharedSettings
assert os.environ.get("ENABLE_CREDIT_SYSTEM") is None
assert os.environ.get("POINTS_ENABLED") is None
s = SharedSettings(_env_file=None)
assert s.credits_enabled is False
# 旧属性名仍可用(业务代码大量引用 settings.points_enabled)
assert s.points_enabled is False
def test_enable_credit_system_env(self, monkeypatch):
from packages.config import base as base_mod
monkeypatch.setenv("ENABLE_CREDIT_SYSTEM", "true")
s = base_mod.SharedSettings(_env_file=None)
assert s.points_enabled is True
assert s.credits_enabled is True
def test_legacy_points_enabled_env_alias(self, monkeypatch):
from packages.config import base as base_mod
monkeypatch.setenv("ENABLE_CREDIT_SYSTEM", "false")
monkeypatch.setenv("POINTS_ENABLED", "true")
s = base_mod.SharedSettings(_env_file=None)
assert s.points_enabled is True
assert s.credits_enabled is False
assert s.points_enabled_compat is True
def test_legacy_setter_back_compat(self):
from packages.config.base import SharedSettings
s = SharedSettings(_env_file=None)
s.points_enabled = True
assert s.credits_enabled is True
assert s.points_enabled is True
s.points_enabled = False
assert s.points_enabled is False
# ── /points/check:关闭时恒放行、需 0 积分 ────────────────────────────────
class TestCheckEndpointWhenDisabled:
def test_check_allowed_zero_required(self):
from app.api.routes.points import check_points
from app.schemas.points import PointsCheckRequest
svc = MagicMock()
svc.get_or_create_account.return_value = {"balance": 0}
db = MagicMock()
cu = _make_cu()
body = PointsCheckRequest(scene_key="ai_voice", quantity=1, duration_minutes=5)
with (
patch("app.api.routes.points._credits_enabled", return_value=False),
patch("app.api.routes.points._get_service", return_value=svc),
):
resp = check_points(body=body, current_user=cu, db=db)
assert resp.allowed is True
assert resp.required_points == 0
assert resp.remaining_after == 0
# 不再走免费额度判定
svc.check_daily_free_clip.assert_not_called()
def test_unknown_scene_still_400_when_disabled(self):
"""未知 scene 即使系统关闭也返回 400(参数校验先于开关)。"""
from app.api.routes.points import check_points
from app.schemas.points import PointsCheckRequest
from fastapi import HTTPException
with pytest.raises(HTTPException) as exc:
check_points(body=PointsCheckRequest(scene_key="nope"), current_user=_make_cu(), db=MagicMock())
assert exc.value.status_code == 400
def test_check_enabled_calculates_cost(self):
"""开关开启时保持原有计费校验。"""
from app.api.routes.points import check_points
from app.schemas.points import PointsCheckRequest
svc = MagicMock()
svc.check_daily_free_clip.return_value = False
svc.get_or_create_account.return_value = {"balance": 100}
body = PointsCheckRequest(scene_key="ai_title", quantity=1)
with (
patch("app.api.routes.points._credits_enabled", return_value=True),
patch("app.api.routes.points._get_service", return_value=svc),
):
resp = check_points(body=body, current_user=_make_cu(), db=MagicMock())
assert resp.required_points == 2 # 免费用户 ceil(1*1.15)=2
# ── /points/deduct:关闭时 no-op,余额不变 ────────────────────────────────
class TestDeductEndpointWhenDisabled:
def test_deduct_is_noop(self):
from app.api.routes.points import deduct_points
from app.schemas.points import PointsDeductRequest
svc = MagicMock()
svc.get_or_create_account.return_value = {"balance": 7}
body = PointsDeductRequest(scene_key="ai_voice", amount=999)
with (
patch("app.api.routes.points._credits_enabled", return_value=False),
patch("app.api.routes.points._get_service", return_value=svc),
):
resp = deduct_points(body=body, current_user=_make_cu(), db=MagicMock())
svc.deduct_points.assert_not_called()
assert resp.success is True
assert resp.data["balance"] == 7
assert resp.data["transaction_id"] == ""
def test_deduct_enabled_works_as_before(self):
from app.api.routes.points import deduct_points
from app.schemas.points import PointsDeductRequest
svc = MagicMock()
svc.deduct_points.return_value = {"success": True, "balance": 8, "transaction_id": "tx-1"}
body = PointsDeductRequest(scene_key="ai_title", amount=2)
with (
patch("app.api.routes.points._credits_enabled", return_value=True),
patch("app.api.routes.points._get_service", return_value=svc),
):
resp = deduct_points(body=body, current_user=_make_cu(), db=MagicMock())
svc.deduct_points.assert_called_once()
assert resp.data["balance"] == 8
assert resp.data["transaction_id"] == "tx-1"
# ── 查询接口:系统关闭时仍全部可用 ────────────────────────────────────────
class TestQueryEndpointsRemainAvailable:
def test_balance_route_works_when_disabled(self):
from app.api.routes.points import get_balance
svc = MagicMock()
svc.get_or_create_account.return_value = {"balance": 0, "total_earned": 0, "total_spent": 0}
with (
patch("app.api.routes.points._credits_enabled", return_value=False),
patch("app.api.routes.points._get_service", return_value=svc),
):
resp = get_balance(current_user=_make_cu(), db=MagicMock())
assert resp.balance == 0
assert resp.is_member is False
def test_transactions_route_works_when_disabled(self):
from app.api.routes.points import get_transactions
svc = MagicMock()
svc.get_transactions.return_value = {"items": [], "total": 0, "page": 1, "page_size": 20}
with (
patch("app.api.routes.points._credits_enabled", return_value=False),
patch("app.api.routes.points._get_service", return_value=svc),
):
resp = get_transactions(current_user=_make_cu(), db=MagicMock())
assert resp.total == 0
def test_daily_usage_route_works_when_disabled(self):
from app.api.routes.points import get_daily_usage
svc = MagicMock()
svc.get_daily_usage.return_value = {
"free_clips_used": 0,
"free_clips_limit": 2,
"free_clips_remaining": 2,
"reset_at": "2026-09-20T00:00:00Z",
}
with (
patch("app.api.routes.points._credits_enabled", return_value=False),
patch("app.api.routes.points._get_service", return_value=svc),
):
resp = get_daily_usage(current_user=_make_cu(), db=MagicMock())
assert resp.free_clips_limit == 2
# ── 业务路由:开关关闭时 PointsService 不实例化、不扣分 ───────────────────
class TestBusinessRoutesBypassWhenDisabled:
def test_lipsync_route_skips_points(self):
"""lipsync 创建任务路由:settings.points_enabled=False 时不构造 PointsService。"""
from app.api.routes import lipsync as lipsync_mod
assert bool(getattr(lipsync_mod.settings, "points_enabled", False)) is False
def test_tts_route_skips_points(self):
from app.api.routes import tts as tts_mod
assert bool(getattr(tts_mod.settings, "points_enabled", False)) is False