Files
xiaoxia-saas/tests/unit/test_api_settings.py
xiaoxia 58fe71c483
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m48s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Failing after 1m29s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 55s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 48s
CI/CD Pipeline / Unit Tests (push) Successful in 6m16s
CI/CD Pipeline / Integration Tests (push) Successful in 2m56s
CI/CD Pipeline / Frontend Lint (push) Successful in 35s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 46s
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 (push) Successful in 5m34s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m2s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 14m0s
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 / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 23s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m50s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m10s
PR #832: Worker基础镜像三级缓存策略优化 + 测试环境变量污染修复
perf(ci): Worker基础镜像三级缓存策略优化 + 单元测试环境变量污染修复

- 三级缓存策略:L1本地daemon / L2 Gitea Registry / L3本地构建+推送
- 修复pre-build镜像tag与Dockerfile FROM地址不一致的bug
- worker.Dockerfile层顺序优化,变化少的文件放前面
- 修复test_api_settings.py autouse fixture环境变量污染
  (JWT_SECRET_KEY/DATABASE_URL被pop后影响后续测试模块)
2026-07-24 19:04:19 +08:00

338 lines
11 KiB
Python
Executable File

"""API Settings 配置单元测试."""
import os
import pytest
from packages.config.api_settings import APISettings, get_api_settings
from packages.config.base import SharedSettings, get_cached_settings, reload_settings_cache
@pytest.fixture(autouse=True)
def _reset_cache():
"""每个测试前清空配置缓存,避免单例污染."""
reload_settings_cache()
# 保存关键环境变量(避免其他测试模块的全局污染)
_saved_env = {}
for key in ["JWT_SECRET_KEY", "DATABASE_URL", "USE_IN_MEMORY_DB", "APP_ENV"]:
_saved_env[key] = os.environ.get(key)
# 设置必要的环境变量,避免 JWT 校验失败
os.environ["JWT_SECRET_KEY"] = "test-secret-key-for-unit-tests-only-12345"
# 清除可能被其他模块污染的变量,确保默认值测试准确
for key in ["DATABASE_URL", "APP_ENV"]:
os.environ.pop(key, None)
yield
reload_settings_cache()
# 恢复所有保存的环境变量,避免污染其他测试模块
for key, val in _saved_env.items():
if val is None:
os.environ.pop(key, None)
else:
os.environ[key] = val
class TestSharedSettingsDefaults:
"""SharedSettings 默认值测试."""
def test_default_environment(self):
s = SharedSettings()
assert s.environment == "development"
def test_default_debug_true(self):
s = SharedSettings()
assert s.debug is True
def test_default_auto_create_schema_false(self):
s = SharedSettings()
assert s.auto_create_schema is False
def test_default_database_url(self):
s = SharedSettings()
assert "postgresql" in s.database_url
assert "localhost" in s.database_url
def test_default_database_pool_size(self):
s = SharedSettings()
assert s.database_pool_size == 20
def test_default_database_max_overflow(self):
s = SharedSettings()
assert s.database_max_overflow == 10
def test_default_redis_url(self):
s = SharedSettings()
assert s.redis_url.startswith("redis://")
def test_default_celery_broker_url(self):
s = SharedSettings()
assert s.celery_broker_url.startswith("redis://")
def test_default_oss_endpoint(self):
s = SharedSettings()
assert "aliyuncs.com" in s.oss_endpoint
def test_default_cosyvoice_settings(self):
s = SharedSettings()
assert s.cosyvoice_model == "cosyvoice-v3-flash"
assert s.cosyvoice_format == "mp3"
assert s.cosyvoice_sample_rate == 22050
def test_default_doubao_settings(self):
s = SharedSettings()
assert "doubao" in s.doubao_model
assert s.doubao_timeout == 30
assert s.doubao_max_retries == 2
class TestAPISettingsDefaults:
"""APISettings 默认值测试."""
def test_default_app_name(self):
s = APISettings()
assert s.app_name == "xiaoxia-saas"
def test_default_app_version(self):
s = APISettings()
assert s.app_version == "0.1.61"
def test_default_api_host(self):
s = APISettings()
assert s.api_host == "0.0.0.0"
def test_default_api_port(self):
s = APISettings()
assert s.api_port == 8000
def test_default_jwt_algorithm(self):
s = APISettings()
assert s.jwt_algorithm == "HS256"
def test_default_jwt_access_expire(self):
s = APISettings()
assert s.jwt_access_token_expire_minutes == 30
def test_default_jwt_refresh_expire(self):
s = APISettings()
assert s.jwt_refresh_token_expire_days == 30
def test_default_enable_email_delivery_false(self):
s = APISettings()
assert s.enable_email_delivery is False
def test_default_smtp_config(self):
s = APISettings()
assert s.smtp_host == "smtp.gmail.com"
assert s.smtp_port == 587
assert s.smtp_use_tls is True
assert s.smtp_from_name == "小虾 SaaS"
def test_default_render_engine(self):
s = APISettings()
assert s.render_engine == "legacy"
def test_use_in_memory_db_field_exists(self, monkeypatch):
monkeypatch.delenv("USE_IN_MEMORY_DB", raising=False)
s = APISettings()
assert isinstance(s.use_in_memory_db, bool)
assert s.USE_IN_MEMORY_DB == s.use_in_memory_db
def test_default_enable_redis_sessions_false(self):
s = APISettings()
assert s.enable_redis_sessions is False
class TestJWTSecretValidation:
"""JWT 密钥校验测试."""
def test_missing_jwt_secret_raises(self, monkeypatch):
monkeypatch.delenv("JWT_SECRET_KEY", raising=False)
with pytest.raises(ValueError, match="JWT_SECRET_KEY must be set"):
APISettings()
def test_empty_jwt_secret_raises(self, monkeypatch):
monkeypatch.setenv("JWT_SECRET_KEY", "")
with pytest.raises(ValueError, match="JWT_SECRET_KEY must be set"):
APISettings()
@pytest.mark.parametrize(
"insecure_value",
["your-secret-key-change-in-production", "your-secret-key", "secret", "changeme", "password"],
)
def test_insecure_jwt_secret_raises(self, monkeypatch, insecure_value):
monkeypatch.setenv("JWT_SECRET_KEY", insecure_value)
with pytest.raises(ValueError, match="insecure"):
APISettings()
def test_strong_jwt_secret_accepted(self, monkeypatch):
monkeypatch.setenv("JWT_SECRET_KEY", "strong-random-secret-key-12345-abcde")
s = APISettings()
assert s.jwt_secret_key == "strong-random-secret-key-12345-abcde"
class TestCorsOrigins:
"""CORS 配置解析测试."""
def test_default_cors_origins(self):
s = APISettings()
origins = s.cors_origins
assert isinstance(origins, list)
assert len(origins) == 3
assert "http://localhost:3000" in origins
assert "http://localhost:5173" in origins
assert "http://localhost:8000" in origins
def test_cors_origins_strips_whitespace(self, monkeypatch):
monkeypatch.setenv("CORS_ORIGINS_RAW", " http://a.com , http://b.com ")
s = APISettings()
assert s.cors_origins == ["http://a.com", "http://b.com"]
def test_cors_origins_empty_string(self, monkeypatch):
monkeypatch.setenv("CORS_ORIGINS_RAW", "")
s = APISettings()
assert s.cors_origins == []
def test_cors_origins_single_origin(self, monkeypatch):
monkeypatch.setenv("CORS_ORIGINS_RAW", "https://api.example.com")
s = APISettings()
assert s.cors_origins == ["https://api.example.com"]
class TestUpperCaseAliases:
"""向后兼容:UPPER_CASE property 别名测试."""
def test_app_name_alias(self):
s = APISettings()
assert s.APP_NAME == s.app_name
def test_app_version_alias(self):
s = APISettings()
assert s.APP_VERSION == s.app_version
def test_database_url_alias(self):
s = APISettings()
assert s.DATABASE_URL == s.database_url
def test_redis_url_alias(self):
s = APISettings()
assert s.REDIS_URL == s.redis_url
def test_jwt_secret_alias(self):
s = APISettings()
assert s.JWT_SECRET_KEY == s.jwt_secret_key
def test_jwt_algorithm_alias(self):
s = APISettings()
assert s.JWT_ALGORITHM == s.jwt_algorithm
def test_smtp_host_alias(self):
s = APISettings()
assert s.SMTP_HOST == s.smtp_host
def test_oss_endpoint_alias(self):
s = APISettings()
assert s.OSS_ENDPOINT == s.oss_endpoint
def test_celery_broker_alias(self):
s = APISettings()
assert s.CELERY_BROKER_URL == s.celery_broker_url
def test_render_engine_alias(self):
s = APISettings()
assert s.RENDER_ENGINE == s.render_engine
class TestSettingsCache:
"""配置单例缓存测试."""
def test_get_api_settings_returns_same_instance(self):
s1 = get_api_settings()
s2 = get_api_settings()
assert s1 is s2
def test_get_cached_settings_same_class_same_instance(self):
s1 = get_cached_settings(SharedSettings)
s2 = get_cached_settings(SharedSettings)
assert s1 is s2
def test_reload_clears_cache(self):
s1 = get_cached_settings(SharedSettings)
reload_settings_cache()
s2 = get_cached_settings(SharedSettings)
assert s1 is not s2
def test_custom_cache_key(self):
s1 = get_cached_settings(SharedSettings, cache_key="custom1")
s2 = get_cached_settings(SharedSettings, cache_key="custom2")
assert s1 is not s2
def test_get_shared_settings(self):
from packages.config.base import get_shared_settings
s = get_shared_settings()
assert isinstance(s, SharedSettings)
class TestOSSAliases:
"""OSS 配置别名测试."""
def test_oss_bucket_name_alias(self):
s = APISettings()
assert s.OSS_BUCKET_NAME == s.oss_bucket_name
def test_oss_direct_upload_max_mb_alias(self):
s = APISettings()
assert s.OSS_DIRECT_UPLOAD_MAX_MB == s.oss_direct_upload_max_mb
def test_oss_direct_upload_expire_alias(self):
s = APISettings()
assert s.OSS_DIRECT_UPLOAD_EXPIRE_SECONDS == s.oss_direct_upload_expire_seconds
# ── WorkerSettings 测试 ──────────────────────────────────────
from packages.config.worker_settings import WorkerSettings, get_worker_settings
class TestWorkerSettingsDefaults:
"""WorkerSettings 默认值测试."""
def test_default_worker_name(self):
s = WorkerSettings()
assert s.worker_name == "xiaoxia-saas-worker"
def test_default_worker_concurrency(self):
s = WorkerSettings()
assert s.worker_concurrency == 4
def test_default_worker_max_tasks_per_child(self):
s = WorkerSettings()
assert s.worker_max_tasks_per_child == 1000
def test_broker_url_alias(self):
s = WorkerSettings()
assert s.broker_url == s.celery_broker_url
def test_result_backend_alias(self):
s = WorkerSettings()
assert s.result_backend == s.celery_result_backend
def test_inherits_shared_settings(self):
s = WorkerSettings()
assert s.database_url # 继承自SharedSettings
assert s.redis_url
assert s.oss_endpoint
assert s.cosyvoice_model == "cosyvoice-v3-flash"
class TestGetWorkerSettings:
"""get_worker_settings 单例测试."""
def test_returns_worker_settings_instance(self):
s = get_worker_settings()
assert isinstance(s, WorkerSettings)
def test_singleton(self):
s1 = get_worker_settings()
s2 = get_worker_settings()
assert s1 is s2