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

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,191 @@
# -*- coding: utf-8 -*-
"""环境配置属性测试Property-Based Testing
使用 hypothesis 验证环境配置管理的通用正确性属性:
- Property 15: .env 解析与敏感值掩码
- Property 16: .env 写入往返一致性
测试策略:
- Property 15: 生成随机 .env 内容(含敏感和非敏感键),验证 _parse_env + _is_sensitive 对敏感值掩码
- Property 16: 生成随机键值对,序列化为 .env 格式后再解析,验证往返一致性
"""
import os
os.environ.setdefault("JWT_SECRET_KEY", "test-secret-key-for-env-config-properties")
from hypothesis import given, settings, assume
from hypothesis import strategies as st
from app.routers.env_config import _parse_env, _is_sensitive, _MASK, _SENSITIVE_KEYWORDS
# ---------------------------------------------------------------------------
# 通用策略Strategies
# ---------------------------------------------------------------------------
# 合法的环境变量键名:字母或下划线开头,后跟字母、数字、下划线
_key_start_char = st.sampled_from(
list("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_")
)
_key_rest_char = st.sampled_from(
list("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_")
)
_env_key_st = st.builds(
lambda first, rest: first + rest,
first=_key_start_char,
rest=st.text(alphabet=_key_rest_char, min_size=0, max_size=30),
)
# 值:不含换行符的可打印字符串(排除引号以避免解析歧义)
_env_value_st = st.text(
alphabet=st.characters(
whitelist_categories=("L", "N", "P", "S"),
blacklist_characters='\n\r"\'#',
),
min_size=0,
max_size=50,
)
# 敏感键:在随机键名中嵌入敏感关键词
_sensitive_keyword_st = st.sampled_from(list(_SENSITIVE_KEYWORDS))
_sensitive_key_st = st.builds(
lambda prefix, kw, suffix: prefix + kw + suffix,
prefix=st.text(alphabet=_key_rest_char, min_size=0, max_size=10),
kw=_sensitive_keyword_st,
suffix=st.text(alphabet=_key_rest_char, min_size=0, max_size=10),
).filter(lambda k: len(k) > 0 and k[0].isalpha() or k[0] == "_")
# 确保敏感键以字母或下划线开头
_safe_sensitive_key_st = st.builds(
lambda prefix, kw: prefix + "_" + kw,
prefix=st.sampled_from(["DB", "API", "ETL", "APP", "MY"]),
kw=_sensitive_keyword_st,
)
# ---------------------------------------------------------------------------
# Feature: admin-web-console, Property 15: .env 解析与敏感值掩码
# **Validates: Requirements 6.1, 6.3**
# ---------------------------------------------------------------------------
@settings(max_examples=100)
@given(
sensitive_keys=st.lists(_safe_sensitive_key_st, min_size=1, max_size=5, unique=True),
sensitive_values=st.lists(
st.text(min_size=1, max_size=30, alphabet=st.characters(
whitelist_categories=("L", "N"),
)),
min_size=1, max_size=5,
),
normal_keys=st.lists(_env_key_st, min_size=1, max_size=5, unique=True),
normal_values=st.lists(_env_value_st, min_size=1, max_size=5),
)
def test_sensitive_values_masked(sensitive_keys, sensitive_values, normal_keys, normal_values):
"""Property 15: .env 解析与敏感值掩码。
包含敏感键PASSWORD、TOKEN、SECRET、DSN的 .env 文件内容,
API 返回的键值对列表中这些键的值应被掩码替换,不包含原始敏感值。
"""
# 确保敏感键和普通键不重叠
normal_keys_filtered = [k for k in normal_keys if k not in sensitive_keys]
assume(len(normal_keys_filtered) >= 1)
# 对齐列表长度
s_vals = (sensitive_values * ((len(sensitive_keys) // len(sensitive_values)) + 1))[:len(sensitive_keys)]
n_vals = (normal_values * ((len(normal_keys_filtered) // len(normal_values)) + 1))[:len(normal_keys_filtered)]
# 构造 .env 内容
lines = []
for k, v in zip(sensitive_keys, s_vals):
lines.append(f"{k}={v}")
for k, v in zip(normal_keys_filtered, n_vals):
lines.append(f"{k}={v}")
env_content = "\n".join(lines) + "\n"
# 解析
parsed = _parse_env(env_content)
entries = [line for line in parsed if line["type"] == "entry"]
# 模拟 GET 端点的掩码逻辑
masked_entries = {}
for entry in entries:
if _is_sensitive(entry["key"]):
masked_entries[entry["key"]] = _MASK
else:
masked_entries[entry["key"]] = entry["value"]
# 验证:敏感键的值应被掩码
for k, v in zip(sensitive_keys, s_vals):
assert k in masked_entries, f"敏感键 {k} 应出现在解析结果中"
assert masked_entries[k] == _MASK, (
f"敏感键 {k} 的值应为掩码 '{_MASK}',实际为 '{masked_entries[k]}'"
)
# 原始敏感值不应出现在掩码后的结果中
assert masked_entries[k] != v, (
f"敏感键 {k} 的原始值 '{v}' 不应出现在掩码结果中"
)
# 验证:非敏感键的值应保持原样
for k, v in zip(normal_keys_filtered, n_vals):
if not _is_sensitive(k):
assert k in masked_entries, f"普通键 {k} 应出现在解析结果中"
assert masked_entries[k] == v, (
f"普通键 {k} 的值应为 '{v}',实际为 '{masked_entries[k]}'"
)
# ---------------------------------------------------------------------------
# Feature: admin-web-console, Property 16: .env 写入往返一致性
# **Validates: Requirements 6.2**
# ---------------------------------------------------------------------------
@settings(max_examples=100)
@given(
entries=st.lists(
st.tuples(_env_key_st, _env_value_st),
min_size=1,
max_size=10,
unique_by=lambda t: t[0], # 键唯一
),
)
def test_env_write_read_round_trip(entries):
"""Property 16: .env 写入往返一致性。
有效的键值对集合(不含注释和空行),写入 .env 文件后再读取解析,
应得到与原始集合等价的键值对。
"""
# 过滤掉值中可能导致解析歧义的情况(值前后空白会被 strip
clean_entries = [(k, v.strip()) for k, v in entries]
# 排除空键(策略已保证非空,但防御性检查)
clean_entries = [(k, v) for k, v in clean_entries if k]
assume(len(clean_entries) >= 1)
# 模拟写入:构造 .env 文件内容
lines = [f"{k}={v}" for k, v in clean_entries]
env_content = "\n".join(lines) + "\n"
# 解析
parsed = _parse_env(env_content)
parsed_entries = {
line["key"]: line["value"]
for line in parsed
if line["type"] == "entry"
}
# 验证往返一致性:每个写入的键值对都应在解析结果中
for k, v in clean_entries:
assert k in parsed_entries, (
f"'{k}' 应出现在解析结果中,实际键集合: {list(parsed_entries.keys())}"
)
assert parsed_entries[k] == v, (
f"'{k}' 的值不一致:写入='{v}',解析='{parsed_entries[k]}'"
)
# 验证:解析结果的键数量应与写入的一致
assert len(parsed_entries) == len(clean_entries), (
f"解析结果键数量 {len(parsed_entries)} 应等于写入数量 {len(clean_entries)}"
)