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
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
375 lines
14 KiB
Python
Executable File
375 lines
14 KiB
Python
Executable File
"""Quota 配额系统单元测试 — #1894 档位清理后 (free/monthly/quarterly/yearly)."""
|
|
|
|
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:
|
|
def test_core_dimensions_exist(self):
|
|
assert QuotaDimension.STORAGE_GB.value == "storage_gb"
|
|
assert QuotaDimension.VIDEOS_PER_MONTH.value == "videos_per_month"
|
|
assert QuotaDimension.MAX_CONCURRENT.value == "max_concurrent"
|
|
assert QuotaDimension.MAX_TEMPLATES.value == "max_templates"
|
|
# MAX_TITLES 保留作为枚举别名(与 MAX_TEMPLATES 同值),但不再在套餐配额中独立配置
|
|
assert QuotaDimension.MAX_VOICEOVERS.value == "max_voiceovers"
|
|
assert QuotaDimension.AI_VOICE_ENABLED.value == "ai_voice_enabled"
|
|
|
|
def test_extended_dimensions_exist(self):
|
|
assert QuotaDimension.AI_VOICE_CREDITS.value == "ai_voice_credits"
|
|
assert QuotaDimension.BATCH_EXPORT_ENABLED.value == "batch_export_enabled"
|
|
assert QuotaDimension.MULTI_PLATFORM_ENABLED.value == "multi_platform_enabled"
|
|
assert QuotaDimension.DEDUP_REPORT_ENABLED.value == "dedup_report_enabled"
|
|
|
|
def test_all_dimensions_are_strings(self):
|
|
for dim in QuotaDimension:
|
|
assert isinstance(dim.value, str)
|
|
|
|
|
|
class TestQuotaTier:
|
|
def test_get_limit_defined(self):
|
|
tier = QuotaTier(name="test", limits={"storage_gb": 10, "videos": 5})
|
|
assert tier.get_limit("storage_gb") == 10
|
|
assert tier.get_limit("videos") == 5
|
|
|
|
def test_get_limit_undefined_returns_zero(self):
|
|
tier = QuotaTier(name="test", limits={"storage_gb": 10})
|
|
assert tier.get_limit("unknown_dim") == 0
|
|
|
|
def test_is_unlimited_false_for_finite(self):
|
|
tier = QuotaTier(name="test", limits={"storage_gb": 10})
|
|
assert tier.is_unlimited("storage_gb") is False
|
|
|
|
def test_is_unlimited_true_for_inf(self):
|
|
tier = QuotaTier(name="test", limits={"templates": float("inf")})
|
|
assert tier.is_unlimited("templates") is True
|
|
|
|
def test_is_unlimited_undefined(self):
|
|
tier = QuotaTier(name="test", limits={})
|
|
assert tier.is_unlimited("unknown") is True
|
|
|
|
def test_empty_limits(self):
|
|
tier = QuotaTier(name="empty")
|
|
assert tier.limits == {}
|
|
assert tier.name == "empty"
|
|
|
|
|
|
class TestQuotaTiers:
|
|
def test_core_tiers_exist(self):
|
|
"""核心四档位存在."""
|
|
assert "free" in QUOTA_TIERS
|
|
assert "monthly" in QUOTA_TIERS
|
|
assert "quarterly" in QUOTA_TIERS
|
|
assert "yearly" in QUOTA_TIERS
|
|
|
|
def test_legacy_tiers_are_aliases(self):
|
|
"""旧档位保留为别名以兼容历史数据."""
|
|
assert QUOTA_TIERS["basic"] is QUOTA_TIERS["monthly"]
|
|
assert QUOTA_TIERS["standard"] is QUOTA_TIERS["monthly"]
|
|
assert QUOTA_TIERS["premium"] is QUOTA_TIERS["quarterly"]
|
|
assert QUOTA_TIERS["pro"] is QUOTA_TIERS["quarterly"]
|
|
assert QUOTA_TIERS["enterprise"] is QUOTA_TIERS["quarterly"]
|
|
|
|
def test_free_tier_limits(self):
|
|
free = QUOTA_TIERS["free"]
|
|
assert free.get_limit("storage_gb") == 2
|
|
assert free.get_limit("videos_per_month") == 5
|
|
assert free.get_limit("max_concurrent") == 3
|
|
assert free.get_limit("max_templates") == 3
|
|
assert free.get_limit("max_voiceovers") == 10
|
|
assert free.get_limit("ai_voice_enabled") == 0
|
|
assert free.get_limit("ai_voice_credits") == 0
|
|
|
|
def test_monthly_tier_limits(self):
|
|
monthly = QUOTA_TIERS["monthly"]
|
|
assert monthly.get_limit("storage_gb") == 20
|
|
assert monthly.get_limit("videos_per_month") == 30
|
|
assert monthly.get_limit("max_concurrent") == 10
|
|
assert monthly.get_limit("max_templates") == 15
|
|
assert monthly.get_limit("max_voiceovers") == 100
|
|
assert monthly.get_limit("ai_voice_enabled") == 1
|
|
assert monthly.get_limit("ai_voice_credits") == 100
|
|
assert monthly.get_limit("batch_export_enabled") == 1
|
|
|
|
def test_quarterly_tier_limits(self):
|
|
q = QUOTA_TIERS["quarterly"]
|
|
assert q.get_limit("storage_gb") == 100
|
|
assert q.get_limit("videos_per_month") == 100
|
|
assert q.get_limit("max_concurrent") == 20
|
|
assert q.is_unlimited("max_templates") is True
|
|
assert q.get_limit("ai_voice_enabled") == 1
|
|
assert q.get_limit("ai_voice_credits") == 500
|
|
assert q.get_limit("batch_export_enabled") == 1
|
|
assert q.get_limit("multi_platform_enabled") == 1
|
|
assert q.get_limit("dedup_report_enabled") == 1
|
|
|
|
def test_yearly_tier_limits(self):
|
|
y = QUOTA_TIERS["yearly"]
|
|
assert y.get_limit("storage_gb") == 100
|
|
assert y.is_unlimited("videos_per_month") is True
|
|
assert y.get_limit("max_concurrent") == 20
|
|
assert y.is_unlimited("max_templates") is True
|
|
assert y.get_limit("max_voiceovers") == 200
|
|
assert y.get_limit("ai_voice_credits") == 2000
|
|
assert y.get_limit("batch_export_enabled") == 1
|
|
assert y.get_limit("multi_platform_enabled") == 1
|
|
assert y.get_limit("dedup_report_enabled") == 1
|
|
|
|
def test_tier_increase_monotonic(self):
|
|
free = QUOTA_TIERS["free"]
|
|
monthly = QUOTA_TIERS["monthly"]
|
|
quarterly = QUOTA_TIERS["quarterly"]
|
|
for dim in ["storage_gb", "videos_per_month", "max_concurrent", "max_voiceovers", "ai_voice_credits"]:
|
|
assert monthly.get_limit(dim) >= free.get_limit(dim)
|
|
assert quarterly.get_limit(dim) >= monthly.get_limit(dim)
|
|
|
|
|
|
class TestQuotaWarningLevel:
|
|
def test_levels_exist(self):
|
|
assert QuotaWarningLevel.NORMAL == "normal"
|
|
assert QuotaWarningLevel.WARNING == "warning"
|
|
assert QuotaWarningLevel.CRITICAL == "critical"
|
|
assert QuotaWarningLevel.EXCEEDED == "exceeded"
|
|
|
|
|
|
class TestQuotaCheckResult:
|
|
def test_usage_percent_normal(self):
|
|
result = QuotaCheckResult(
|
|
allowed=True,
|
|
dimension="storage_gb",
|
|
limit=100,
|
|
used=50,
|
|
remaining=50,
|
|
warning_level="normal",
|
|
)
|
|
assert result.usage_percent == 50.0
|
|
|
|
def test_usage_percent_exceeded(self):
|
|
result = QuotaCheckResult(
|
|
allowed=False, dimension="d", limit=100, used=150, remaining=0, warning_level="exceeded"
|
|
)
|
|
assert result.usage_percent == 100.0
|
|
|
|
def test_usage_percent_zero_used(self):
|
|
result = QuotaCheckResult(allowed=True, dimension="d", limit=100, used=0, remaining=100, warning_level="normal")
|
|
assert result.usage_percent == 0.0
|
|
|
|
def test_usage_percent_zero_limit_with_usage(self):
|
|
result = QuotaCheckResult(allowed=False, dimension="d", limit=0, used=10, remaining=0, warning_level="exceeded")
|
|
assert result.usage_percent == 100.0
|
|
|
|
def test_usage_percent_zero_limit_no_usage(self):
|
|
result = QuotaCheckResult(allowed=True, dimension="d", limit=0, used=0, remaining=0, warning_level="normal")
|
|
assert result.usage_percent == 0.0
|
|
|
|
def test_usage_percent_unlimited(self):
|
|
result = QuotaCheckResult(
|
|
allowed=True,
|
|
dimension="d",
|
|
limit=float("inf"),
|
|
used=1000,
|
|
remaining=float("inf"),
|
|
warning_level="normal",
|
|
)
|
|
assert result.usage_percent == 0.0
|
|
|
|
|
|
class TestQuotaRegistry:
|
|
def test_initial_dimensions(self):
|
|
reg = QuotaRegistry()
|
|
dims = reg.list_dimensions()
|
|
assert "storage_gb" in dims
|
|
assert "videos_per_month" in dims
|
|
assert len(dims) == len(QuotaDimension)
|
|
|
|
def test_list_tiers(self):
|
|
reg = QuotaRegistry()
|
|
tiers = reg.list_tiers()
|
|
assert "free" in tiers
|
|
assert "monthly" in tiers
|
|
assert "quarterly" in tiers
|
|
assert "yearly" in tiers
|
|
# 核心四档位必须存在
|
|
assert "free" in tiers and "monthly" in tiers and "quarterly" in tiers and "yearly" in tiers
|
|
|
|
def test_get_tier_existing(self):
|
|
reg = QuotaRegistry()
|
|
tier = reg.get_tier("free")
|
|
assert tier is not None
|
|
assert tier.name == "free"
|
|
|
|
def test_get_tier_unknown(self):
|
|
reg = QuotaRegistry()
|
|
assert reg.get_tier("unknown_plan") is None
|
|
|
|
def test_get_limit_known(self):
|
|
reg = QuotaRegistry()
|
|
assert reg.get_limit("free", "storage_gb") == 2
|
|
assert reg.get_limit("quarterly", "storage_gb") == 100
|
|
assert reg.get_limit("yearly", "storage_gb") == 100
|
|
|
|
def test_get_limit_unknown_plan(self):
|
|
reg = QuotaRegistry()
|
|
assert reg.get_limit("unknown", "storage_gb") == 0
|
|
|
|
def test_register_new_dimension(self):
|
|
reg = QuotaRegistry()
|
|
reg.register_dimension(
|
|
"new_feature", "新功能", default_limits={"free": 0, "monthly": 1, "quarterly": 5, "yearly": 10}
|
|
)
|
|
dims = reg.list_dimensions()
|
|
assert "new_feature" in dims
|
|
assert dims["new_feature"] == "新功能"
|
|
assert reg.get_limit("free", "new_feature") == 0
|
|
assert reg.get_limit("monthly", "new_feature") == 1
|
|
assert reg.get_limit("quarterly", "new_feature") == 5
|
|
assert reg.get_limit("yearly", "new_feature") == 10
|
|
|
|
def test_register_dimension_idempotent(self):
|
|
reg = QuotaRegistry()
|
|
reg.register_dimension("storage_gb", "should not change", default_limits={"free": 999})
|
|
assert reg.get_limit("free", "storage_gb") == 2
|
|
|
|
def test_register_without_defaults(self):
|
|
reg = QuotaRegistry()
|
|
reg.register_dimension("new_dim", "描述")
|
|
assert reg.get_limit("free", "new_dim") == 0
|
|
assert reg.get_limit("monthly", "new_dim") == 0
|
|
assert reg.get_limit("yearly", "new_dim") == 0
|
|
|
|
def test_register_partial_limits(self):
|
|
reg = QuotaRegistry()
|
|
reg.register_dimension("partial", "partial", default_limits={"quarterly": 42})
|
|
assert reg.get_limit("free", "partial") == 0
|
|
assert reg.get_limit("quarterly", "partial") == 42
|
|
|
|
|
|
class TestQuotaChecker:
|
|
def test_check_within_limit(self):
|
|
checker = QuotaChecker()
|
|
result = checker.check("free", "storage_gb", 1)
|
|
assert result.allowed is True
|
|
assert result.limit == 2
|
|
assert result.used == 1
|
|
assert result.remaining == 1
|
|
|
|
def test_check_exceeded(self):
|
|
checker = QuotaChecker()
|
|
result = checker.check("free", "storage_gb", 3)
|
|
assert result.allowed is False
|
|
assert result.remaining == 0
|
|
assert result.warning_level == "exceeded"
|
|
|
|
def test_check_exact_limit_not_allowed(self):
|
|
checker = QuotaChecker()
|
|
result = checker.check("free", "storage_gb", 2)
|
|
assert result.allowed is False
|
|
assert result.remaining == 0
|
|
|
|
def test_check_unlimited_quarterly_templates(self):
|
|
checker = QuotaChecker()
|
|
result = checker.check("quarterly", "max_templates", 999999)
|
|
assert result.allowed is True
|
|
assert math.isinf(result.remaining)
|
|
assert result.warning_level == "normal"
|
|
|
|
def test_check_unlimited_yearly_videos(self):
|
|
checker = QuotaChecker()
|
|
result = checker.check("yearly", "videos_per_month", 999999)
|
|
assert result.allowed is True
|
|
assert math.isinf(result.remaining)
|
|
|
|
def test_check_warning_level_normal(self):
|
|
checker = QuotaChecker()
|
|
result = checker.check("free", "storage_gb", 1) # 50%
|
|
assert result.warning_level == "normal"
|
|
|
|
def test_check_warning_level_warning(self):
|
|
checker = QuotaChecker()
|
|
result = checker.check("free", "max_templates", 2.5) # 2.5/3 ≈ 83%
|
|
assert result.warning_level == "warning"
|
|
|
|
def test_check_warning_level_critical(self):
|
|
checker = QuotaChecker()
|
|
result = checker.check("free", "max_templates", 2.9) # ≈97%
|
|
assert result.warning_level == "critical"
|
|
|
|
def test_check_warning_level_exceeded(self):
|
|
checker = QuotaChecker()
|
|
result = checker.check("free", "storage_gb", 5)
|
|
assert result.warning_level == "exceeded"
|
|
|
|
def test_check_multiple(self):
|
|
checker = QuotaChecker()
|
|
results = checker.check_multiple(
|
|
"free",
|
|
{"storage_gb": 1, "max_templates": 2, "max_voiceovers": 5},
|
|
)
|
|
assert len(results) == 3
|
|
assert all(r.allowed for r in results)
|
|
|
|
def test_check_zero_limit(self):
|
|
checker = QuotaChecker()
|
|
result = checker.check("free", "ai_voice_enabled", 0)
|
|
assert result.allowed is False
|
|
assert result.remaining == 0
|
|
|
|
def test_compute_warning_level_normal(self):
|
|
assert QuotaChecker._compute_warning_level(50, 100) == "normal"
|
|
assert QuotaChecker._compute_warning_level(79, 100) == "normal"
|
|
|
|
def test_compute_warning_level_warning_boundary(self):
|
|
assert QuotaChecker._compute_warning_level(80, 100) == "warning"
|
|
assert QuotaChecker._compute_warning_level(94, 100) == "warning"
|
|
|
|
def test_compute_warning_level_critical_boundary(self):
|
|
assert QuotaChecker._compute_warning_level(95, 100) == "critical"
|
|
assert QuotaChecker._compute_warning_level(99, 100) == "critical"
|
|
|
|
def test_compute_warning_level_exceeded(self):
|
|
assert QuotaChecker._compute_warning_level(100, 100) == "exceeded"
|
|
assert QuotaChecker._compute_warning_level(150, 100) == "exceeded"
|
|
|
|
def test_compute_warning_level_unlimited(self):
|
|
assert QuotaChecker._compute_warning_level(9999, float("inf")) == "normal"
|
|
|
|
def test_compute_warning_level_zero_limit_with_usage(self):
|
|
assert QuotaChecker._compute_warning_level(1, 0) == "exceeded"
|
|
|
|
def test_compute_warning_level_zero_limit_no_usage(self):
|
|
assert QuotaChecker._compute_warning_level(0, 0) == "normal"
|
|
|
|
|
|
class TestGetWarningLevel:
|
|
def test_convenience_function(self):
|
|
assert get_warning_level(50, 100) == "normal"
|
|
assert get_warning_level(99, 100) == "critical"
|
|
assert get_warning_level(100, 100) == "exceeded"
|
|
assert get_warning_level(0, 0) == "normal"
|
|
assert get_warning_level(1, 0) == "exceeded"
|
|
|
|
|
|
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_works(self):
|
|
result = quota_checker.check("free", "storage_gb", 1)
|
|
assert result.allowed is True
|