在准备环境前提交次全部更改。

This commit is contained in:
Neo
2026-02-19 08:35:13 +08:00
parent ded6dfb9d8
commit 4eac07da47
1387 changed files with 6107191 additions and 33002 deletions

View File

@@ -0,0 +1,291 @@
# -*- coding: utf-8 -*-
"""环境配置路由单元测试
覆盖 3 个端点GET / PUT / GET /export
通过 mock 绕过文件 I/O专注路由逻辑验证。
"""
import os
os.environ.setdefault("JWT_SECRET_KEY", "test-secret-key-for-unit-tests")
from unittest.mock import patch, MagicMock
import pytest
from fastapi.testclient import TestClient
from app.auth.dependencies import CurrentUser, get_current_user
from app.main import app
_TEST_USER = CurrentUser(user_id=1, site_id=100)
def _override_auth():
return _TEST_USER
app.dependency_overrides[get_current_user] = _override_auth
client = TestClient(app)
# 模拟 .env 文件内容
_SAMPLE_ENV = """\
# 数据库配置
DB_HOST=localhost
DB_PORT=5432
DB_PASSWORD=super_secret_123
JWT_SECRET_KEY=my-jwt-secret
# ETL 配置
ETL_DB_DSN=postgresql://user:pass@host/db
TIMEZONE=Asia/Shanghai
"""
_MOCK_ENV_PATH = "app.routers.env_config._ENV_PATH"
def _mock_path(content: str | None = _SAMPLE_ENV, exists: bool = True):
"""构造 mock Path 对象。"""
mock = MagicMock()
mock.exists.return_value = exists
if content is not None:
mock.read_text.return_value = content
return mock
# ---------------------------------------------------------------------------
# GET /api/env-config
# ---------------------------------------------------------------------------
class TestGetEnvConfig:
@patch(_MOCK_ENV_PATH)
def test_returns_entries_with_masked_sensitive(self, mock_path_obj):
mock_path_obj.__class__ = type(MagicMock())
mock_path_obj.exists.return_value = True
mock_path_obj.read_text.return_value = _SAMPLE_ENV
resp = client.get("/api/env-config")
assert resp.status_code == 200
data = resp.json()
entries = {e["key"]: e["value"] for e in data["entries"]}
# 非敏感值原样返回
assert entries["DB_HOST"] == "localhost"
assert entries["DB_PORT"] == "5432"
assert entries["TIMEZONE"] == "Asia/Shanghai"
# 敏感值掩码
assert entries["DB_PASSWORD"] == "****"
assert entries["JWT_SECRET_KEY"] == "****"
assert entries["ETL_DB_DSN"] == "****"
@patch(_MOCK_ENV_PATH)
def test_file_not_found(self, mock_path_obj):
mock_path_obj.exists.return_value = False
resp = client.get("/api/env-config")
assert resp.status_code == 404
@patch(_MOCK_ENV_PATH)
def test_empty_file(self, mock_path_obj):
mock_path_obj.exists.return_value = True
mock_path_obj.read_text.return_value = ""
resp = client.get("/api/env-config")
assert resp.status_code == 200
assert resp.json()["entries"] == []
@patch(_MOCK_ENV_PATH)
def test_comments_and_blank_lines_excluded(self, mock_path_obj):
mock_path_obj.exists.return_value = True
mock_path_obj.read_text.return_value = "# comment\n\nKEY=val\n"
resp = client.get("/api/env-config")
assert resp.status_code == 200
entries = resp.json()["entries"]
assert len(entries) == 1
assert entries[0]["key"] == "KEY"
# ---------------------------------------------------------------------------
# PUT /api/env-config
# ---------------------------------------------------------------------------
class TestUpdateEnvConfig:
@patch(_MOCK_ENV_PATH)
def test_update_existing_key(self, mock_path_obj):
mock_path_obj.exists.return_value = True
mock_path_obj.read_text.return_value = "DB_HOST=localhost\nDB_PORT=5432\n"
resp = client.put("/api/env-config", json={
"entries": [
{"key": "DB_HOST", "value": "192.168.1.1"},
{"key": "DB_PORT", "value": "5433"},
]
})
assert resp.status_code == 200
# 验证写入内容
written = mock_path_obj.write_text.call_args[0][0]
assert "DB_HOST=192.168.1.1" in written
assert "DB_PORT=5433" in written
@patch(_MOCK_ENV_PATH)
def test_add_new_key(self, mock_path_obj):
mock_path_obj.exists.return_value = True
mock_path_obj.read_text.return_value = "DB_HOST=localhost\n"
resp = client.put("/api/env-config", json={
"entries": [
{"key": "DB_HOST", "value": "localhost"},
{"key": "NEW_KEY", "value": "new_value"},
]
})
assert resp.status_code == 200
written = mock_path_obj.write_text.call_args[0][0]
assert "NEW_KEY=new_value" in written
@patch(_MOCK_ENV_PATH)
def test_masked_value_preserves_original(self, mock_path_obj):
"""掩码值(****)不应覆盖原始敏感值。"""
mock_path_obj.exists.return_value = True
mock_path_obj.read_text.return_value = "DB_PASSWORD=real_secret\nDB_HOST=localhost\n"
resp = client.put("/api/env-config", json={
"entries": [
{"key": "DB_PASSWORD", "value": "****"},
{"key": "DB_HOST", "value": "newhost"},
]
})
assert resp.status_code == 200
written = mock_path_obj.write_text.call_args[0][0]
# 原始密码应保留
assert "DB_PASSWORD=real_secret" in written
assert "DB_HOST=newhost" in written
@patch(_MOCK_ENV_PATH)
def test_preserves_comments(self, mock_path_obj):
mock_path_obj.exists.return_value = True
mock_path_obj.read_text.return_value = "# 注释行\nDB_HOST=localhost\n\n# 另一个注释\n"
resp = client.put("/api/env-config", json={
"entries": [{"key": "DB_HOST", "value": "newhost"}]
})
assert resp.status_code == 200
written = mock_path_obj.write_text.call_args[0][0]
assert "# 注释行" in written
assert "# 另一个注释" in written
def test_invalid_key_format(self):
resp = client.put("/api/env-config", json={
"entries": [{"key": "123BAD", "value": "val"}]
})
assert resp.status_code == 422
def test_empty_key(self):
resp = client.put("/api/env-config", json={
"entries": [{"key": "", "value": "val"}]
})
assert resp.status_code == 422
@patch(_MOCK_ENV_PATH)
def test_file_not_exists_creates_new(self, mock_path_obj):
"""文件不存在时,应创建新文件。"""
mock_path_obj.exists.return_value = False
resp = client.put("/api/env-config", json={
"entries": [{"key": "NEW_KEY", "value": "value"}]
})
assert resp.status_code == 200
written = mock_path_obj.write_text.call_args[0][0]
assert "NEW_KEY=value" in written
@patch(_MOCK_ENV_PATH)
def test_update_sensitive_with_new_value(self, mock_path_obj):
"""显式提供新密码时应更新。"""
mock_path_obj.exists.return_value = True
mock_path_obj.read_text.return_value = "DB_PASSWORD=old_secret\n"
resp = client.put("/api/env-config", json={
"entries": [{"key": "DB_PASSWORD", "value": "new_secret"}]
})
assert resp.status_code == 200
written = mock_path_obj.write_text.call_args[0][0]
assert "DB_PASSWORD=new_secret" in written
# 返回值中敏感键仍然掩码
entries = {e["key"]: e["value"] for e in resp.json()["entries"]}
assert entries["DB_PASSWORD"] == "****"
# ---------------------------------------------------------------------------
# GET /api/env-config/export
# ---------------------------------------------------------------------------
class TestExportEnvConfig:
@patch(_MOCK_ENV_PATH)
def test_export_masks_sensitive(self, mock_path_obj):
mock_path_obj.exists.return_value = True
mock_path_obj.read_text.return_value = _SAMPLE_ENV
resp = client.get("/api/env-config/export")
assert resp.status_code == 200
assert resp.headers["content-type"].startswith("text/plain")
assert "attachment" in resp.headers.get("content-disposition", "")
content = resp.text
# 非敏感值保留
assert "DB_HOST=localhost" in content
assert "TIMEZONE=Asia/Shanghai" in content
# 敏感值掩码
assert "super_secret_123" not in content
assert "my-jwt-secret" not in content
assert "DB_PASSWORD=****" in content
assert "JWT_SECRET_KEY=****" in content
@patch(_MOCK_ENV_PATH)
def test_export_preserves_comments(self, mock_path_obj):
mock_path_obj.exists.return_value = True
mock_path_obj.read_text.return_value = _SAMPLE_ENV
content = client.get("/api/env-config/export").text
assert "# 数据库配置" in content
assert "# ETL 配置" in content
@patch(_MOCK_ENV_PATH)
def test_export_file_not_found(self, mock_path_obj):
mock_path_obj.exists.return_value = False
resp = client.get("/api/env-config/export")
assert resp.status_code == 404
# ---------------------------------------------------------------------------
# 认证测试
# ---------------------------------------------------------------------------
class TestEnvConfigAuth:
def test_requires_auth(self):
"""移除 auth override 后,所有端点应返回 401/403。"""
# 临时移除 override
original = app.dependency_overrides.pop(get_current_user, None)
try:
for method, url in [
("GET", "/api/env-config"),
("PUT", "/api/env-config"),
("GET", "/api/env-config/export"),
]:
resp = client.request(method, url)
assert resp.status_code in (401, 403), f"{method} {url} 应需要认证"
finally:
if original:
app.dependency_overrides[get_current_user] = original