129 lines
5.4 KiB
Python
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
|