2df7bc9dc8
CI/CD Pipeline / Check if frontend-only change (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 / Validate - Migration (alembic) (push) Successful in 1m41s
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 / Build Staging Web Image (push) Successful in 1m45s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m54s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m1s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m34s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 5m49s
CI/CD Pipeline / Frontend Lint (push) Successful in 6m0s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m27s
CI/CD Pipeline / Integration Tests (push) Successful in 2m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m11s
CI/CD Pipeline / Unit Tests (push) Failing after 5m52s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 30s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m48s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 5m10s
425 lines
15 KiB
Python
Executable File
425 lines
15 KiB
Python
Executable File
"""Quota 领域层单元测试 - quota.py"""
|
||
|
||
import math
|
||
|
||
import pytest
|
||
|
||
from packages.domain.quota import (
|
||
QUOTA_TIERS,
|
||
QuotaChecker,
|
||
QuotaCheckResult,
|
||
QuotaDimension,
|
||
QuotaRegistry,
|
||
QuotaTier,
|
||
QuotaWarningLevel,
|
||
get_warning_level,
|
||
quota_checker,
|
||
quota_registry,
|
||
)
|
||
|
||
|
||
class TestQuotaDimension:
|
||
"""QuotaDimension 枚举测试"""
|
||
|
||
def test_all_dimensions_have_values(self):
|
||
"""所有枚举成员都有字符串值"""
|
||
for dim in QuotaDimension:
|
||
assert isinstance(dim.value, str)
|
||
assert dim.value
|
||
|
||
def test_dimension_count(self):
|
||
"""配额维度数量 >= 内置维度"""
|
||
# 至少有 storage_gb, videos_per_month, max_concurrent, max_templates 等
|
||
assert len(QuotaDimension) >= 7
|
||
|
||
def test_str_enum_behavior(self):
|
||
"""是 str 枚举,可直接当字符串用"""
|
||
assert QuotaDimension.STORAGE_GB == "storage_gb"
|
||
assert isinstance(QuotaDimension.STORAGE_GB, str)
|
||
|
||
|
||
class TestQuotaTier:
|
||
"""QuotaTier 测试"""
|
||
|
||
def test_get_limit_defined(self):
|
||
"""已定义的维度返回正确值"""
|
||
tier = QuotaTier(name="test", limits={"storage": 10, "videos": 5})
|
||
assert tier.get_limit("storage") == 10
|
||
assert tier.get_limit("videos") == 5
|
||
|
||
def test_get_limit_undefined_returns_zero(self):
|
||
"""未定义的维度返回 0"""
|
||
tier = QuotaTier(name="test", limits={"storage": 10})
|
||
assert tier.get_limit("unknown") == 0
|
||
|
||
def test_is_unlimited_true(self):
|
||
"""不限量判断 - inf"""
|
||
tier = QuotaTier(name="test", limits={"templates": float("inf")})
|
||
assert tier.is_unlimited("templates") is True
|
||
|
||
def test_is_unlimited_false(self):
|
||
"""限量判断"""
|
||
tier = QuotaTier(name="test", limits={"storage": 10})
|
||
assert tier.is_unlimited("storage") is False
|
||
|
||
def test_is_unlimited_undefined_returns_true(self):
|
||
"""未定义的维度默认 inf,is_unlimited 返回 True"""
|
||
tier = QuotaTier(name="test", limits={})
|
||
# get_limit 用 dict.get 默认 0,但 is_unlimited 用 dict.get 默认 inf
|
||
assert tier.is_unlimited("unknown") is True
|
||
|
||
|
||
class TestQuotaTiers:
|
||
"""内置套餐配额测试"""
|
||
|
||
def test_three_tiers_exist(self):
|
||
"""三个套餐等级都存在"""
|
||
assert "free" in QUOTA_TIERS
|
||
assert "basic" in QUOTA_TIERS
|
||
assert "premium" in QUOTA_TIERS
|
||
|
||
def test_free_tier_storage(self):
|
||
"""free 套餐 2GB 存储"""
|
||
assert QUOTA_TIERS["free"].get_limit(QuotaDimension.STORAGE_GB) == 2
|
||
|
||
def test_basic_tier_storage(self):
|
||
"""basic 套餐 20GB 存储"""
|
||
assert QUOTA_TIERS["basic"].get_limit(QuotaDimension.STORAGE_GB) == 20
|
||
|
||
def test_premium_tier_storage(self):
|
||
"""premium 套餐 100GB 存储"""
|
||
assert QUOTA_TIERS["premium"].get_limit(QuotaDimension.STORAGE_GB) == 100
|
||
|
||
def test_free_no_ai_voice(self):
|
||
"""free 套餐没有 AI 配音"""
|
||
assert QUOTA_TIERS["free"].get_limit(QuotaDimension.AI_VOICE_ENABLED) == 0
|
||
|
||
def test_basic_has_ai_voice(self):
|
||
"""basic 套餐有 AI 配音"""
|
||
assert QUOTA_TIERS["basic"].get_limit(QuotaDimension.AI_VOICE_ENABLED) == 1
|
||
|
||
def test_premium_templates_unlimited(self):
|
||
"""premium 套餐模板不限量"""
|
||
assert QUOTA_TIERS["premium"].is_unlimited(QuotaDimension.MAX_TEMPLATES) is True
|
||
|
||
def test_free_videos_per_month(self):
|
||
"""free 每月 5 个视频"""
|
||
assert QUOTA_TIERS["free"].get_limit(QuotaDimension.VIDEOS_PER_MONTH) == 5
|
||
|
||
def test_premium_multi_platform_enabled(self):
|
||
"""premium 支持多平台发布"""
|
||
assert QUOTA_TIERS["premium"].get_limit(QuotaDimension.MULTI_PLATFORM_ENABLED) == 1
|
||
|
||
|
||
class TestQuotaWarningLevel:
|
||
"""告警级别常量测试"""
|
||
|
||
def test_level_values(self):
|
||
"""四个告警级别都有定义"""
|
||
assert QuotaWarningLevel.NORMAL == "normal"
|
||
assert QuotaWarningLevel.WARNING == "warning"
|
||
assert QuotaWarningLevel.CRITICAL == "critical"
|
||
assert QuotaWarningLevel.EXCEEDED == "exceeded"
|
||
|
||
|
||
class TestQuotaCheckResult:
|
||
"""QuotaCheckResult 测试"""
|
||
|
||
def test_usage_percent_normal(self):
|
||
"""正常使用百分比计算"""
|
||
result = QuotaCheckResult(
|
||
allowed=True,
|
||
dimension="storage",
|
||
limit=100,
|
||
used=30,
|
||
remaining=70,
|
||
warning_level=QuotaWarningLevel.NORMAL,
|
||
)
|
||
assert result.usage_percent == 30.0
|
||
|
||
def test_usage_percent_capped_at_100(self):
|
||
"""超过 100% 时截断为 100%"""
|
||
result = QuotaCheckResult(
|
||
allowed=False,
|
||
dimension="storage",
|
||
limit=100,
|
||
used=150,
|
||
remaining=0,
|
||
warning_level=QuotaWarningLevel.EXCEEDED,
|
||
)
|
||
assert result.usage_percent == 100.0
|
||
|
||
def test_usage_percent_zero_limit_with_usage(self):
|
||
"""limit=0 但有使用量,返回 100%"""
|
||
result = QuotaCheckResult(
|
||
allowed=False,
|
||
dimension="storage",
|
||
limit=0,
|
||
used=5,
|
||
remaining=0,
|
||
warning_level=QuotaWarningLevel.EXCEEDED,
|
||
)
|
||
assert result.usage_percent == 100.0
|
||
|
||
def test_usage_percent_zero_limit_no_usage(self):
|
||
"""limit=0 且无使用量,返回 0%"""
|
||
result = QuotaCheckResult(
|
||
allowed=True,
|
||
dimension="storage",
|
||
limit=0,
|
||
used=0,
|
||
remaining=0,
|
||
warning_level=QuotaWarningLevel.NORMAL,
|
||
)
|
||
assert result.usage_percent == 0.0
|
||
|
||
def test_usage_percent_unlimited(self):
|
||
"""不限量时使用百分比为 0"""
|
||
result = QuotaCheckResult(
|
||
allowed=True,
|
||
dimension="templates",
|
||
limit=float("inf"),
|
||
used=50,
|
||
remaining=float("inf"),
|
||
warning_level=QuotaWarningLevel.NORMAL,
|
||
)
|
||
assert result.usage_percent == 0.0
|
||
|
||
|
||
class TestQuotaRegistry:
|
||
"""QuotaRegistry 测试"""
|
||
|
||
def test_initial_dimensions(self):
|
||
"""初始化时内置维度已注册"""
|
||
registry = QuotaRegistry()
|
||
dims = registry.list_dimensions()
|
||
assert QuotaDimension.STORAGE_GB in dims
|
||
assert QuotaDimension.VIDEOS_PER_MONTH in dims
|
||
|
||
def test_initial_tiers(self):
|
||
"""初始化时三个套餐已注册"""
|
||
registry = QuotaRegistry()
|
||
tiers = registry.list_tiers()
|
||
assert "free" in tiers
|
||
assert "basic" in tiers
|
||
assert "premium" in tiers
|
||
|
||
def test_register_new_dimension(self):
|
||
"""注册新的配额维度"""
|
||
registry = QuotaRegistry()
|
||
registry.register_dimension("custom_dim", "自定义维度")
|
||
dims = registry.list_dimensions()
|
||
assert "custom_dim" in dims
|
||
assert dims["custom_dim"] == "自定义维度"
|
||
|
||
def test_register_dimension_idempotent(self):
|
||
"""重复注册是幂等的"""
|
||
registry = QuotaRegistry()
|
||
registry.register_dimension("custom", "描述1")
|
||
registry.register_dimension("custom", "描述2")
|
||
# 保留第一次注册的描述
|
||
assert registry.list_dimensions()["custom"] == "描述1"
|
||
|
||
def test_register_with_default_limits(self):
|
||
"""注册时指定各套餐的默认限制"""
|
||
registry = QuotaRegistry()
|
||
registry.register_dimension(
|
||
"custom",
|
||
"自定义",
|
||
default_limits={"free": 1, "basic": 10, "premium": 100},
|
||
)
|
||
assert registry.get_limit("free", "custom") == 1
|
||
assert registry.get_limit("basic", "custom") == 10
|
||
assert registry.get_limit("premium", "custom") == 100
|
||
|
||
def test_register_without_default_limits_defaults_to_zero(self):
|
||
"""不指定默认限制时各套餐该维度为 0"""
|
||
registry = QuotaRegistry()
|
||
registry.register_dimension("custom_no_limit", "自定义")
|
||
assert registry.get_limit("free", "custom_no_limit") == 0
|
||
assert registry.get_limit("basic", "custom_no_limit") == 0
|
||
|
||
def test_register_default_limits_ignores_unknown_plan(self):
|
||
"""默认限制中未知的套餐名被忽略"""
|
||
registry = QuotaRegistry()
|
||
registry.register_dimension(
|
||
"custom",
|
||
"自定义",
|
||
default_limits={"nonexistent": 999},
|
||
)
|
||
# 不报错,但也不会创建新套餐
|
||
assert registry.get_tier("nonexistent") is None
|
||
|
||
def test_get_tier_existing(self):
|
||
"""获取存在的套餐"""
|
||
registry = QuotaRegistry()
|
||
tier = registry.get_tier("free")
|
||
assert tier is not None
|
||
assert tier.name == "free"
|
||
|
||
def test_get_tier_nonexistent(self):
|
||
"""获取不存在的套餐返回 None"""
|
||
registry = QuotaRegistry()
|
||
assert registry.get_tier("enterprise") is None
|
||
|
||
def test_get_limit_existing(self):
|
||
"""获取存在的套餐和维度的限制"""
|
||
registry = QuotaRegistry()
|
||
assert registry.get_limit("free", QuotaDimension.STORAGE_GB) == 2
|
||
|
||
def test_get_limit_nonexistent_plan(self):
|
||
"""不存在的套餐返回 0"""
|
||
registry = QuotaRegistry()
|
||
assert registry.get_limit("unknown", QuotaDimension.STORAGE_GB) == 0
|
||
|
||
def test_list_dimensions_returns_copy(self):
|
||
"""list_dimensions 返回副本,修改不影响内部"""
|
||
registry = QuotaRegistry()
|
||
dims = registry.list_dimensions()
|
||
dims["fake"] = "fake"
|
||
assert "fake" not in registry.list_dimensions()
|
||
|
||
def test_list_tiers_returns_all_three(self):
|
||
"""列出所有套餐"""
|
||
registry = QuotaRegistry()
|
||
tiers = registry.list_tiers()
|
||
assert len(tiers) == 3
|
||
assert set(tiers) == {"free", "basic", "premium"}
|
||
|
||
|
||
class TestQuotaChecker:
|
||
"""QuotaChecker 测试"""
|
||
|
||
def test_check_under_limit_allowed(self):
|
||
"""使用量低于限制,允许"""
|
||
checker = QuotaChecker()
|
||
result = checker.check("free", QuotaDimension.STORAGE_GB, 1.0)
|
||
assert result.allowed is True
|
||
assert result.remaining == 1.0
|
||
assert result.warning_level == QuotaWarningLevel.NORMAL
|
||
|
||
def test_check_at_limit_not_allowed(self):
|
||
"""使用量等于限制,不允许(used < limit 判定)"""
|
||
checker = QuotaChecker()
|
||
result = checker.check("free", QuotaDimension.STORAGE_GB, 2.0)
|
||
assert result.allowed is False
|
||
assert result.remaining == 0
|
||
assert result.warning_level == QuotaWarningLevel.EXCEEDED
|
||
|
||
def test_check_over_limit(self):
|
||
"""使用量超过限制"""
|
||
checker = QuotaChecker()
|
||
result = checker.check("free", QuotaDimension.STORAGE_GB, 3.0)
|
||
assert result.allowed is False
|
||
assert result.remaining == 0
|
||
assert result.warning_level == QuotaWarningLevel.EXCEEDED
|
||
|
||
def test_check_warning_level_80_percent(self):
|
||
"""80% 触发 WARNING"""
|
||
checker = QuotaChecker()
|
||
# 100GB 的 80% = 80GB
|
||
result = checker.check("premium", QuotaDimension.STORAGE_GB, 80.0)
|
||
assert result.warning_level == QuotaWarningLevel.WARNING
|
||
|
||
def test_check_warning_level_95_percent(self):
|
||
"""95% 触发 CRITICAL"""
|
||
checker = QuotaChecker()
|
||
result = checker.check("premium", QuotaDimension.STORAGE_GB, 95.0)
|
||
assert result.warning_level == QuotaWarningLevel.CRITICAL
|
||
|
||
def test_check_warning_level_exceeded(self):
|
||
"""100% 及以上触发 EXCEEDED"""
|
||
checker = QuotaChecker()
|
||
result = checker.check("premium", QuotaDimension.STORAGE_GB, 100.0)
|
||
assert result.warning_level == QuotaWarningLevel.EXCEEDED
|
||
|
||
def test_check_unlimited_always_allowed(self):
|
||
"""不限量的维度始终允许"""
|
||
checker = QuotaChecker()
|
||
result = checker.check("premium", QuotaDimension.MAX_TEMPLATES, 9999)
|
||
assert result.allowed is True
|
||
assert math.isinf(result.remaining)
|
||
assert result.warning_level == QuotaWarningLevel.NORMAL
|
||
|
||
def test_check_unknown_plan_zero_limit(self):
|
||
"""未知套餐限制为 0,used=0 时不允许(0 < 0 为 False)"""
|
||
checker = QuotaChecker()
|
||
result = checker.check("unknown", QuotaDimension.STORAGE_GB, 0)
|
||
assert result.limit == 0
|
||
assert result.allowed is False
|
||
|
||
def test_check_multiple(self):
|
||
"""批量检查多个维度"""
|
||
checker = QuotaChecker()
|
||
results = checker.check_multiple(
|
||
"free",
|
||
{
|
||
QuotaDimension.STORAGE_GB: 1.0,
|
||
QuotaDimension.VIDEOS_PER_MONTH: 3,
|
||
},
|
||
)
|
||
assert len(results) == 2
|
||
assert all(r.allowed for r in results)
|
||
dims = {r.dimension for r in results}
|
||
assert QuotaDimension.STORAGE_GB in dims
|
||
assert QuotaDimension.VIDEOS_PER_MONTH in dims
|
||
|
||
def test_check_with_custom_registry(self):
|
||
"""使用自定义注册表"""
|
||
registry = QuotaRegistry()
|
||
registry.register_dimension("custom", "自定义", default_limits={"free": 5})
|
||
checker = QuotaChecker(registry)
|
||
result = checker.check("free", "custom", 3)
|
||
assert result.allowed is True
|
||
assert result.limit == 5
|
||
|
||
def test_compute_warning_level_zero_limit_no_usage(self):
|
||
"""limit=0, used=0 → NORMAL"""
|
||
level = QuotaChecker._compute_warning_level(0, 0)
|
||
assert level == QuotaWarningLevel.NORMAL
|
||
|
||
def test_compute_warning_level_zero_limit_with_usage(self):
|
||
"""limit=0, used>0 → EXCEEDED"""
|
||
level = QuotaChecker._compute_warning_level(1, 0)
|
||
assert level == QuotaWarningLevel.EXCEEDED
|
||
|
||
def test_compute_warning_level_negative_limit(self):
|
||
"""limit<0 视同 0 处理"""
|
||
level = QuotaChecker._compute_warning_level(1, -1)
|
||
assert level == QuotaWarningLevel.EXCEEDED
|
||
|
||
|
||
class TestGetWarningLevel:
|
||
"""get_warning_level 便捷函数测试"""
|
||
|
||
def test_normal(self):
|
||
assert get_warning_level(50, 100) == QuotaWarningLevel.NORMAL
|
||
|
||
def test_warning(self):
|
||
assert get_warning_level(85, 100) == QuotaWarningLevel.WARNING
|
||
|
||
def test_critical(self):
|
||
assert get_warning_level(97, 100) == QuotaWarningLevel.CRITICAL
|
||
|
||
def test_exceeded(self):
|
||
assert get_warning_level(100, 100) == QuotaWarningLevel.EXCEEDED
|
||
|
||
def test_unlimited(self):
|
||
assert get_warning_level(9999, float("inf")) == QuotaWarningLevel.NORMAL
|
||
|
||
|
||
class TestGlobalSingletons:
|
||
"""全局单例测试"""
|
||
|
||
def test_quota_registry_is_instance(self):
|
||
assert isinstance(quota_registry, QuotaRegistry)
|
||
|
||
def test_quota_checker_is_instance(self):
|
||
assert isinstance(quota_checker, QuotaChecker)
|
||
|
||
def test_global_checker_uses_global_registry(self):
|
||
"""全局 checker 使用全局 registry"""
|
||
# 验证能正常工作
|
||
result = quota_checker.check("free", QuotaDimension.STORAGE_GB, 1.0)
|
||
assert result.allowed is True
|