Files
Profeto/tests/test_security_check.py

129 lines
5.4 KiB
Python

"""单测:生产环境启动安全校验。
覆盖:
- 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