"""单测:生产环境启动安全校验。 覆盖: - production 缺 SECRET_KEY → 阻断(sys.exit) - production 缺鉴权 → 阻断 - production 全配置 → 通过 - development 缺配置 → 仅警告(不退出) - DATABASE_URL 弱密码 → production 阻断 / development 仅警告 """ from __future__ import annotations import asyncio import logging from unittest.mock import AsyncMock, patch import pytest from src.core.config import Settings from src.core.security_check import ( _WEAK_SECRET_KEYS, _WEAK_DB_PATTERNS, assert_security_on_startup, validate_security, ) def _base_settings(**overrides) -> Settings: """构造测试用 Settings,默认模拟一个"已合规"的基线。""" defaults = dict( APP_ENV="production", SECRET_KEY="aSwLuw2mqoQdSKUfB3eFVfW2Tv7VnJRRixMxOwZZi5M=", ADMIN_PASSWORD="", ADMIN_API_KEY="", DATABASE_URL="postgresql+asyncpg://user:StrongP@ssw0rd@db:5432/prod", REQUIRE_ADMIN_AUTH=False, ) defaults.update(overrides) return Settings(**defaults) class TestValidateSecurity: """validate_security 逻辑。""" @pytest.mark.asyncio async def test_production_fully_configured_passes(self): with patch("src.core.security_check.settings", _base_settings()), \ patch("src.core.security_check._auth_configured", AsyncMock(return_value=True)): result = await validate_security() assert result["ok"] is True assert result["errors"] == [] @pytest.mark.asyncio async def test_production_missing_secret_key_fails(self): with patch("src.core.security_check.settings", _base_settings(SECRET_KEY="")), \ patch("src.core.security_check._auth_configured", AsyncMock(return_value=True)): result = await validate_security() assert result["ok"] is False assert any("SECRET_KEY" in e for e in result["errors"]) @pytest.mark.asyncio async def test_production_weak_secret_key_fails(self): with patch("src.core.security_check.settings", _base_settings(SECRET_KEY="changeme")), \ patch("src.core.security_check._auth_configured", AsyncMock(return_value=True)): result = await validate_security() assert result["ok"] is False assert any("SECRET_KEY" in e for e in result["errors"]) @pytest.mark.asyncio async def test_production_missing_auth_fails(self): with patch("src.core.security_check.settings", _base_settings()), \ patch("src.core.security_check._auth_configured", AsyncMock(return_value=False)): result = await validate_security() assert result["ok"] is False assert any("鉴权" in e or "ADMIN" in e for e in result["errors"]) @pytest.mark.asyncio async def test_production_weak_db_password_fails(self): with patch("src.core.security_check.settings", _base_settings(DATABASE_URL="postgresql+asyncpg://football:football@localhost:5432/football")), \ patch("src.core.security_check._auth_configured", AsyncMock(return_value=True)): result = await validate_security() assert result["ok"] is False assert any("DATABASE_URL" in e or "弱密码" in e for e in result["errors"]) @pytest.mark.asyncio async def test_development_missing_config_only_warns(self): """development:缺 SECRET_KEY/鉴权 → errors 存在但弱 DB 密码不进 errors。""" with patch("src.core.security_check.settings", _base_settings(APP_ENV="development", SECRET_KEY="", ADMIN_API_KEY="", DATABASE_URL="postgresql+asyncpg://football:football@localhost:5432/football")), \ patch("src.core.security_check._auth_configured", AsyncMock(return_value=False)): result = await validate_security() # development 下:弱 DB 密码只在 warnings,不会升级到 errors assert result["ok"] is False # 仍有 errors(缺密钥 + 缺鉴权) assert any("DATABASE_URL" in w for w in result["warnings"]) class TestAssertSecurityOnStartup: """assert_security_on_startup 退出行为。""" @pytest.mark.asyncio async def test_production_failure_exits(self): with patch("src.core.security_check.settings", _base_settings(SECRET_KEY="")), \ patch("src.core.security_check._auth_configured", AsyncMock(return_value=True)): with pytest.raises(SystemExit) as exc: await assert_security_on_startup() assert exc.value.code == 1 @pytest.mark.asyncio async def test_production_pass_does_not_exit(self): with patch("src.core.security_check.settings", _base_settings()), \ patch("src.core.security_check._auth_configured", AsyncMock(return_value=True)): # 不应抛异常 / 退出 await assert_security_on_startup() @pytest.mark.asyncio async def test_development_failure_does_not_exit(self): with patch("src.core.security_check.settings", _base_settings(APP_ENV="development", SECRET_KEY="")), \ patch("src.core.security_check._auth_configured", AsyncMock(return_value=True)): # development 即使有问题也不退出 await assert_security_on_startup() def test_weak_secret_keys_list_not_empty(): """防御性:弱密钥表应包含常见弱值。""" assert "changeme" in _WEAK_SECRET_KEYS assert "football:football@" in _WEAK_DB_PATTERNS