Files
Neo-ZQYY/apps/backend/tests/test_db_viewer_properties.py

140 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""数据库查看器属性测试Property-Based Testing
使用 hypothesis 验证数据库查看器的通用正确性属性:
- Property 17: SQL 写操作拦截
- Property 18: SQL 查询结果行数限制
测试策略:
- Property 17: 生成包含写操作关键词(随机大小写混合)的 SQL 字符串,
验证 _WRITE_KEYWORDS 正则表达式能匹配到
- Property 18: 生成随机长度的行列表(可能超过 1000 行),
验证截取前 _MAX_ROWS 个元素后长度 <= 1000
"""
import os
os.environ.setdefault("JWT_SECRET_KEY", "test-secret-key-for-db-viewer-properties")
from hypothesis import given, settings
from hypothesis import strategies as st
from app.routers.db_viewer import _WRITE_KEYWORDS, _MAX_ROWS
# ---------------------------------------------------------------------------
# 通用策略Strategies
# ---------------------------------------------------------------------------
# 写操作关键词列表
_WRITE_OPS = ["INSERT", "UPDATE", "DELETE", "DROP", "TRUNCATE"]
# SQL 前缀/后缀:不含写操作关键词的简单文本
_sql_filler_st = st.text(
alphabet=st.characters(
whitelist_categories=("L", "N", "S"),
blacklist_characters="\x00",
),
min_size=0,
max_size=50,
)
# 随机大小写混合的写操作关键词
_random_case_keyword_st = st.sampled_from(_WRITE_OPS).flatmap(
lambda kw: st.tuples(
st.just(kw),
st.lists(
st.booleans(),
min_size=len(kw),
max_size=len(kw),
),
).map(
lambda pair: "".join(
c.upper() if flag else c.lower()
for c, flag in zip(pair[0], pair[1])
)
)
)
# ---------------------------------------------------------------------------
# Feature: admin-web-console, Property 17: SQL 写操作拦截
# **Validates: Requirements 7.5**
# ---------------------------------------------------------------------------
@settings(max_examples=200)
@given(
prefix=_sql_filler_st,
keyword=_random_case_keyword_st,
suffix=_sql_filler_st,
)
def test_write_keywords_always_detected(prefix, keyword, suffix):
"""Property 17: SQL 写操作拦截。
包含 INSERT、UPDATE、DELETE、DROP、TRUNCATE 关键词(不区分大小写)的
SQL 语句_WRITE_KEYWORDS 正则表达式应能匹配到。
策略:在随机前缀和后缀之间插入一个随机大小写混合的写操作关键词,
用空格分隔以确保 \\b 词边界能匹配。
"""
# 用空格分隔确保词边界匹配
sql = f"{prefix} {keyword} {suffix}"
match = _WRITE_KEYWORDS.search(sql)
assert match is not None, (
f"正则表达式未能匹配到写操作关键词sql={sql!r}, keyword={keyword!r}"
)
# 匹配到的关键词(转大写后)应在写操作列表中
assert match.group(1).upper() in _WRITE_OPS, (
f"匹配到的关键词 '{match.group(1)}' 不在写操作列表中"
)
# ---------------------------------------------------------------------------
# Feature: admin-web-console, Property 18: SQL 查询结果行数限制
# **Validates: Requirements 7.4**
# ---------------------------------------------------------------------------
# 模拟数据库返回的行:每行是一个简单列表
_row_st = st.lists(
st.one_of(st.integers(), st.text(max_size=20), st.none()),
min_size=1,
max_size=5,
)
# 行列表策略0 到 3000 行(覆盖超过 _MAX_ROWS 的情况)
_rows_st = st.lists(_row_st, min_size=0, max_size=3000)
@settings(max_examples=200)
@given(rows=_rows_st)
def test_row_count_never_exceeds_max(rows):
"""Property 18: SQL 查询结果行数限制。
对任意长度的行列表,取前 _MAX_ROWS 个元素后,
结果长度应 <= 1000。
这等价于 cur.fetchmany(_MAX_ROWS) 的行为:
数据库游标最多返回 _MAX_ROWS 行。
"""
# 模拟 fetchmany(_MAX_ROWS) 的行为
truncated = rows[:_MAX_ROWS]
assert len(truncated) <= _MAX_ROWS, (
f"截取后行数 {len(truncated)} 超过上限 {_MAX_ROWS}"
)
# 额外验证:如果原始行数 <= _MAX_ROWS截取后应保留全部
if len(rows) <= _MAX_ROWS:
assert len(truncated) == len(rows), (
f"原始行数 {len(rows)} <= {_MAX_ROWS},截取后应保留全部,"
f"实际 {len(truncated)}"
)
# 额外验证:如果原始行数 > _MAX_ROWS截取后应恰好为 _MAX_ROWS
if len(rows) > _MAX_ROWS:
assert len(truncated) == _MAX_ROWS, (
f"原始行数 {len(rows)} > {_MAX_ROWS},截取后应恰好为 {_MAX_ROWS}"
f"实际 {len(truncated)}"
)