140 lines
4.6 KiB
Python
140 lines
4.6 KiB
Python
# -*- 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)}"
|
||
)
|