# -*- 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)}" )