测试模块初步完成
This commit is contained in:
1 parent
4ad3fbfca8
commit
d06546f6bf
13 files changed
+2162
No files matched your search
@@ -0,0 +1,111 @@
|
|||||||
|
#prjs
|
||||||
|
*.bin
|
||||||
|
*.build
|
||||||
|
*.dist
|
||||||
|
*.onefile-build
|
||||||
|
nuitka-crash-report.xml
|
||||||
|
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# celery beat schedule file
|
||||||
|
celerybeat-schedule
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# 黑盒用户模拟测试
|
||||||
|
|
||||||
|
micro_servers 微服务生态的黑盒测试套件。模拟真实用户操作流程,不依赖内部实现,仅通过 HTTP/TCP 公开接口验证各服务行为。
|
||||||
|
|
||||||
|
## 设计原则
|
||||||
|
|
||||||
|
- **零依赖**: 仅使用 Python 标准库(`http.client`、`json`、`urllib`、`socket`、`argparse`)
|
||||||
|
- **用户视角**: 每个测试文件模拟一条完整的用户旅程,而非孤立 API 端点
|
||||||
|
- **无测试框架**: 纯 `assert` + 计数器,输出 `[PASS]`/`[FAIL]`/`[SKIP]`
|
||||||
|
- **独立可运行**: 每个 `test_xxx.py` 可单独执行,也可通过 `test_runner.py` 统一调度
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/blackbox/
|
||||||
|
├── README.md # 本文档
|
||||||
|
├── config.py # 服务地址与端口映射
|
||||||
|
├── helpers.py # HTTP/TCP 请求封装 + 断言工具 + 计数器
|
||||||
|
├── test_runner.py # 统一入口,--url 自动识别目标服务
|
||||||
|
├── test_gateway.py # 用户注册 → 登录 → Token → 代理转发
|
||||||
|
├── test_captcha.py # 获取验证码 → 校验 → 一次性消费
|
||||||
|
├── test_smtp.py # 发送邮箱验证码 → 校验
|
||||||
|
├── test_dnfs.py # 创建笔记 → 查询 → 更新 → 删除(含内容审核)
|
||||||
|
├── test_db.py # 日志写入 → 查询 → 统计 → 清理
|
||||||
|
├── test_ai_review.py # 内容审核 → 人工反馈纠正
|
||||||
|
├── test_static.py # 首页加载 → 文件浏览 → 视频流 → 路径遍历防护
|
||||||
|
└── test_waf.py # 正常请求通行 → SQLi/XSS/命令注入/路径遍历/扫描器拦截
|
||||||
|
```
|
||||||
|
|
||||||
|
## 覆盖的用户旅程
|
||||||
|
|
||||||
|
### 1. 注册与登录 (gateway + captcha + smtp)
|
||||||
|
```
|
||||||
|
用户访问 captcha:8009 获取图形验证码
|
||||||
|
→ 提交 captcha_token + answer 校验
|
||||||
|
→ 访问 smtp:8008 发送邮箱验证码
|
||||||
|
→ 提交 email + code 完成邮箱验证
|
||||||
|
→ 访问 gateway:8443 POST /register 注册账号
|
||||||
|
→ 访问 gateway:8443 POST /login 登录获取 Token
|
||||||
|
→ 使用 Token 通过 gateway 代理访问后端服务
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 笔记管理 (dnfs + ai_review + db)
|
||||||
|
```
|
||||||
|
已登录用户携带 Bearer Token
|
||||||
|
→ POST /add_note 创建笔记(内容经 ai_review:8007 审核)
|
||||||
|
→ POST /get_notes 查询笔记列表
|
||||||
|
→ POST /update_note 更新笔记(内容重新审核)
|
||||||
|
→ POST /delete_note 删除笔记
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 内容审核 (ai_review)
|
||||||
|
```
|
||||||
|
提交违规内容 → 应返回 invalid: true
|
||||||
|
提交正常内容 → 应返回 invalid: false
|
||||||
|
提交误判反馈 → RAG 知识库更新
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 集中日志 (db)
|
||||||
|
```
|
||||||
|
各服务通过 /log/ingest 推送日志
|
||||||
|
→ /log/query 按条件查询
|
||||||
|
→ /log/stats 聚合统计
|
||||||
|
→ /log/purge 按时间清理
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 前端静态服务 (static)
|
||||||
|
```
|
||||||
|
访问首页 → 返回 login.html
|
||||||
|
访问文件浏览 → /api/filelist 返回目录结构
|
||||||
|
访问视频流 → 支持 Range 分段请求
|
||||||
|
路径遍历攻击 → 返回 403
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. WAF 安全防护 (waf)
|
||||||
|
```
|
||||||
|
正常请求 → 通过代理到达 gateway
|
||||||
|
SQL 注入载荷 → 返回 403 blocked by WAF
|
||||||
|
XSS 跨站脚本 → 返回 403
|
||||||
|
命令注入 → 返回 403
|
||||||
|
路径遍历 → 返回 403
|
||||||
|
扫描器 UA → 返回 403
|
||||||
|
```
|
||||||
|
|
||||||
|
## 运行方式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 运行全部测试
|
||||||
|
python3 test_runner.py
|
||||||
|
|
||||||
|
# 指定服务地址(自动识别服务类型)
|
||||||
|
python3 test_runner.py --url http://127.0.0.1:8443
|
||||||
|
|
||||||
|
# 运行单个服务测试
|
||||||
|
python3 test_gateway.py
|
||||||
|
python3 test_captcha.py
|
||||||
|
# ... 依此类推
|
||||||
|
|
||||||
|
# 列出所有可用测试
|
||||||
|
python3 test_runner.py --list
|
||||||
|
```
|
||||||
|
|
||||||
|
## 服务端口默认值
|
||||||
|
|
||||||
|
| 服务 | 默认端口 | 绑定地址 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| gateway | 8443 | 127.0.0.1 |
|
||||||
|
| static | 9200 | 127.0.0.1 |
|
||||||
|
| dnfs | 8006 | 127.0.0.1 |
|
||||||
|
| db | 8005 | 127.0.0.1 |
|
||||||
|
| ai_review | 8007 | 127.0.0.1 |
|
||||||
|
| captcha | 8009 | 127.0.0.1 |
|
||||||
|
| smtp | 8008 | 127.0.0.1 |
|
||||||
|
| waf | 443 | 0.0.0.0 |
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 测试依赖服务运行中,请先通过 `core/init.txt` 启动全部服务
|
||||||
|
- 注册测试会创建真实数据库记录,测试完成后可手动清理
|
||||||
|
- SMTP 测试需要真实的邮件服务配置,若未配置则相关测试标记为 SKIP
|
||||||
|
- WAF 测试需要服务监听 443 端口(可能需要 sudo)
|
||||||
|
- 所有测试设计为幂等:重复运行不会产生副作用累积
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
"""
|
||||||
|
micro_servers 黑盒测试 — 服务地址与端口配置。
|
||||||
|
|
||||||
|
每个服务定义: name, port, description, host。
|
||||||
|
test_runner.py 通过 --url 参数自动识别目标服务。
|
||||||
|
"""
|
||||||
|
|
||||||
|
SERVICES = {
|
||||||
|
"gateway": {
|
||||||
|
"name": "gateway",
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 8443,
|
||||||
|
"description": "应用网关 (认证+反向代理)",
|
||||||
|
"test_file": "test_gateway.py",
|
||||||
|
},
|
||||||
|
"static": {
|
||||||
|
"name": "static",
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 9200,
|
||||||
|
"description": "前端静态服务 (文件托管+视频流)",
|
||||||
|
"test_file": "test_static.py",
|
||||||
|
},
|
||||||
|
"dnfs": {
|
||||||
|
"name": "dnfs",
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 8006,
|
||||||
|
"description": "树状笔记服务 (CRUD+内容审核)",
|
||||||
|
"test_file": "test_dnfs.py",
|
||||||
|
},
|
||||||
|
"db": {
|
||||||
|
"name": "db",
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 8005,
|
||||||
|
"description": "通用数据库服务 (CRUD+集中日志)",
|
||||||
|
"test_file": "test_db.py",
|
||||||
|
},
|
||||||
|
"ai_review": {
|
||||||
|
"name": "ai_review",
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 8007,
|
||||||
|
"description": "AI 内容审核服务 (DeepSeek+ChromaDB RAG)",
|
||||||
|
"test_file": "test_ai_review.py",
|
||||||
|
},
|
||||||
|
"captcha": {
|
||||||
|
"name": "captcha",
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 8009,
|
||||||
|
"description": "验证码服务 (图片生成+校验)",
|
||||||
|
"test_file": "test_captcha.py",
|
||||||
|
},
|
||||||
|
"smtp": {
|
||||||
|
"name": "smtp",
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 8008,
|
||||||
|
"description": "邮件验证服务 (发送+校验)",
|
||||||
|
"test_file": "test_smtp.py",
|
||||||
|
},
|
||||||
|
"waf": {
|
||||||
|
"name": "waf",
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 443,
|
||||||
|
"description": "WAF 安全网关 (TLS+Coraza规则引擎)",
|
||||||
|
"test_file": "test_waf.py",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# 端口到服务的反向映射
|
||||||
|
PORT_MAP = {v["port"]: v["name"] for v in SERVICES.values()}
|
||||||
|
|
||||||
|
|
||||||
|
def detect_service(url: str):
|
||||||
|
"""从 URL 中识别目标服务。返回服务名或 None。"""
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
parsed = urlparse(url)
|
||||||
|
port = parsed.port
|
||||||
|
if port is None:
|
||||||
|
# 默认端口猜测
|
||||||
|
if parsed.scheme == "https":
|
||||||
|
port = 443
|
||||||
|
else:
|
||||||
|
port = 80
|
||||||
|
return PORT_MAP.get(port)
|
||||||
|
|
||||||
|
|
||||||
|
def get_url(name: str) -> str:
|
||||||
|
"""获取服务的完整 HTTP URL。"""
|
||||||
|
svc = SERVICES.get(name)
|
||||||
|
if not svc:
|
||||||
|
raise ValueError(f"未知服务: {name}")
|
||||||
|
return f"http://{svc['host']}:{svc['port']}"
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
"""
|
||||||
|
micro_servers 黑盒测试 — 共享工具模块。
|
||||||
|
|
||||||
|
提供: HTTP 请求封装、断言工具、测试计数器、pymsg 协议客户端。
|
||||||
|
纯标准库实现,零外部依赖。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import http.client
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 测试计数器
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class Counter:
|
||||||
|
"""每个测试模块一个计数器实例,统计 passed/failed/skipped。"""
|
||||||
|
def __init__(self):
|
||||||
|
self.passed = 0
|
||||||
|
self.failed = 0
|
||||||
|
self.skipped = 0
|
||||||
|
self._failures = []
|
||||||
|
|
||||||
|
def add_pass(self):
|
||||||
|
self.passed += 1
|
||||||
|
|
||||||
|
def add_fail(self, name: str, reason: str = ""):
|
||||||
|
self.failed += 1
|
||||||
|
self._failures.append((name, reason))
|
||||||
|
|
||||||
|
def add_skip(self):
|
||||||
|
self.skipped += 1
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total(self):
|
||||||
|
return self.passed + self.failed + self.skipped
|
||||||
|
|
||||||
|
def summary(self) -> str:
|
||||||
|
return f"{self.total} tests: {self.passed} passed, {self.failed} failed, {self.skipped} skipped"
|
||||||
|
|
||||||
|
def failures(self) -> list:
|
||||||
|
return self._failures
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 断言工具
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def assert_eq(actual, expected, name: str, counter: Counter):
|
||||||
|
"""断言 actual == expected。"""
|
||||||
|
if actual == expected:
|
||||||
|
print(f" [PASS] {name}")
|
||||||
|
counter.add_pass()
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f" [FAIL] {name}: expected {expected!r}, got {actual!r}")
|
||||||
|
counter.add_fail(name, f"expected {expected!r}, got {actual!r}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def assert_contains(haystack: str, needle: str, name: str, counter: Counter):
|
||||||
|
"""断言 needle in haystack(不区分大小写)。"""
|
||||||
|
if needle.lower() in haystack.lower():
|
||||||
|
print(f" [PASS] {name}")
|
||||||
|
counter.add_pass()
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
snippet = haystack[:200] if len(haystack) > 200 else haystack
|
||||||
|
print(f" [FAIL] {name}: '{needle}' not found in response")
|
||||||
|
counter.add_fail(name, f"'{needle}' not found in response")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def assert_status(status: int, expected: int, name: str, counter: Counter):
|
||||||
|
"""断言 HTTP 状态码。"""
|
||||||
|
if status == expected:
|
||||||
|
print(f" [PASS] {name}")
|
||||||
|
counter.add_pass()
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f" [FAIL] {name}: expected status {expected}, got {status}")
|
||||||
|
counter.add_fail(name, f"expected status {expected}, got {status}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def assert_true(condition: bool, name: str, counter: Counter, detail: str = ""):
|
||||||
|
"""断言条件为真。"""
|
||||||
|
if condition:
|
||||||
|
print(f" [PASS] {name}")
|
||||||
|
counter.add_pass()
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
msg = f" [FAIL] {name}"
|
||||||
|
if detail:
|
||||||
|
msg += f": {detail}"
|
||||||
|
print(msg)
|
||||||
|
counter.add_fail(name, detail)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def assert_json_key(data: dict, key: str, name: str, counter: Counter):
|
||||||
|
"""断言 JSON 对象包含指定 key。"""
|
||||||
|
if key in data:
|
||||||
|
print(f" [PASS] {name}")
|
||||||
|
counter.add_pass()
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f" [FAIL] {name}: key '{key}' not found in {json.dumps(data)[:200]}")
|
||||||
|
counter.add_fail(name, f"key '{key}' not found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def skip(name: str, reason: str, counter: Counter):
|
||||||
|
"""标记测试为跳过。"""
|
||||||
|
print(f" [SKIP] {name}: {reason}")
|
||||||
|
counter.add_skip()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# HTTP 请求封装
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def http_request(method: str, url: str, *,
|
||||||
|
body: dict = None,
|
||||||
|
headers: dict = None,
|
||||||
|
timeout: float = 10.0,
|
||||||
|
raw_body: bytes = None,
|
||||||
|
raw_body_content_type: str = None):
|
||||||
|
"""发送 HTTP 请求,返回 (status, body_str, resp_headers)。
|
||||||
|
|
||||||
|
支持:
|
||||||
|
- JSON body (body 参数)
|
||||||
|
- 原始二进制 body (raw_body + raw_body_content_type)
|
||||||
|
- 任意自定义 headers
|
||||||
|
- 超时控制
|
||||||
|
"""
|
||||||
|
parsed = urllib.parse.urlparse(url)
|
||||||
|
data = None
|
||||||
|
hdrs = {}
|
||||||
|
|
||||||
|
if headers:
|
||||||
|
hdrs.update(headers)
|
||||||
|
|
||||||
|
if raw_body is not None:
|
||||||
|
data = raw_body
|
||||||
|
if raw_body_content_type:
|
||||||
|
hdrs["Content-Type"] = raw_body_content_type
|
||||||
|
elif body is not None:
|
||||||
|
data = json.dumps(body).encode("utf-8")
|
||||||
|
hdrs["Content-Type"] = "application/json"
|
||||||
|
|
||||||
|
ctx = ssl._create_unverified_context() if parsed.scheme == "https" else None
|
||||||
|
|
||||||
|
req = urllib.request.Request(url, data=data, headers=hdrs, method=method)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
|
||||||
|
resp_body = resp.read().decode("utf-8", errors="replace")
|
||||||
|
resp_headers = dict(resp.headers)
|
||||||
|
return resp.status, resp_body, resp_headers
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
resp_body = e.read().decode("utf-8", errors="replace") if e.fp else ""
|
||||||
|
resp_headers = dict(e.headers) if e.headers else {}
|
||||||
|
return e.code, resp_body, resp_headers
|
||||||
|
except Exception as e:
|
||||||
|
return 0, str(e), {}
|
||||||
|
|
||||||
|
|
||||||
|
def http_get(url: str, headers: dict = None, timeout: float = 10.0):
|
||||||
|
"""HTTP GET 请求。"""
|
||||||
|
return http_request("GET", url, headers=headers, timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def http_post(url: str, body: dict = None, headers: dict = None,
|
||||||
|
timeout: float = 10.0):
|
||||||
|
"""HTTP POST 请求(JSON body)。"""
|
||||||
|
return http_request("POST", url, body=body, headers=headers, timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def http_post_raw(url: str, raw_body: bytes, content_type: str,
|
||||||
|
headers: dict = None, timeout: float = 10.0):
|
||||||
|
"""HTTP POST 请求(原始二进制 body)。"""
|
||||||
|
return http_request("POST", url, raw_body=raw_body,
|
||||||
|
raw_body_content_type=content_type,
|
||||||
|
headers=headers, timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def try_parse_json(text: str):
|
||||||
|
"""尝试解析 JSON,失败返回 None。"""
|
||||||
|
try:
|
||||||
|
return json.loads(text)
|
||||||
|
except (json.JSONDecodeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# pymsg 协议客户端
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class PymsgClient:
|
||||||
|
"""pymsg TCP 协议客户端(用于直接与 db:8005 通信)。
|
||||||
|
|
||||||
|
协议格式: [4字节大端长度][UTF-8 JSON payload]
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, host: str = "127.0.0.1", port: int = 8005,
|
||||||
|
name: str = "test_client"):
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.name = name
|
||||||
|
self._sock = None
|
||||||
|
|
||||||
|
def _ensure_connected(self):
|
||||||
|
if self._sock is None:
|
||||||
|
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
self._sock.settimeout(10)
|
||||||
|
self._sock.connect((self.host, self.port))
|
||||||
|
|
||||||
|
def _close(self):
|
||||||
|
if self._sock:
|
||||||
|
try:
|
||||||
|
self._sock.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._sock = None
|
||||||
|
|
||||||
|
def _send_frame(self, data: str):
|
||||||
|
"""发送一帧 pymsg 数据。"""
|
||||||
|
payload = data.encode("utf-8")
|
||||||
|
header = struct.pack(">I", len(payload))
|
||||||
|
self._sock.sendall(header + payload)
|
||||||
|
|
||||||
|
def _recv_frame(self, timeout: float = 10.0) -> str:
|
||||||
|
"""接收一帧 pymsg 数据。"""
|
||||||
|
self._sock.settimeout(timeout)
|
||||||
|
header = self._recv_exact(4, timeout)
|
||||||
|
if header is None:
|
||||||
|
raise ConnectionError("Failed to receive pymsg header")
|
||||||
|
length = struct.unpack(">I", header)[0]
|
||||||
|
payload = self._recv_exact(length, timeout)
|
||||||
|
if payload is None:
|
||||||
|
raise ConnectionError("Failed to receive pymsg payload")
|
||||||
|
return payload.decode("utf-8")
|
||||||
|
|
||||||
|
def _recv_exact(self, n: int, timeout: float = 10.0):
|
||||||
|
"""精确接收 n 字节。"""
|
||||||
|
self._sock.settimeout(timeout)
|
||||||
|
data = b""
|
||||||
|
while len(data) < n:
|
||||||
|
try:
|
||||||
|
chunk = self._sock.recv(n - len(data))
|
||||||
|
except socket.timeout:
|
||||||
|
return None
|
||||||
|
if not chunk:
|
||||||
|
return None
|
||||||
|
data += chunk
|
||||||
|
return data
|
||||||
|
|
||||||
|
def call(self, payload: dict, timeout: float = 10.0) -> dict:
|
||||||
|
"""发送请求并接收响应(短连接模式)。
|
||||||
|
|
||||||
|
payload 必须包含 "_route": "execute"。
|
||||||
|
返回 {"success": true/false, "data": [...], "error": "..."}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._ensure_connected()
|
||||||
|
msg = json.dumps(payload)
|
||||||
|
self._send_frame(msg)
|
||||||
|
resp_text = self._recv_frame(timeout)
|
||||||
|
return json.loads(resp_text)
|
||||||
|
except Exception as e:
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
finally:
|
||||||
|
self._close()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 服务可用性检查
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def check_service(url: str, timeout: float = 8.0) -> tuple:
|
||||||
|
"""检查服务是否可达(HTTP 请求检测,与测试实际行为一致)。
|
||||||
|
返回 (reachable: bool, detail: str)。
|
||||||
|
任何 HTTP 响应(包括 200/302/401/403 等)均视为可达。
|
||||||
|
"""
|
||||||
|
parsed = urllib.parse.urlparse(url)
|
||||||
|
ctx = ssl._create_unverified_context() if parsed.scheme == "https" else None
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(url, method="HEAD")
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
|
||||||
|
return True, f"HTTP {resp.status}"
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
# 任何 HTTP 错误码都说明服务在运行
|
||||||
|
return True, f"HTTP {e.code}"
|
||||||
|
except urllib.error.URLError as e:
|
||||||
|
reason = str(e.reason) if hasattr(e, 'reason') else str(e)
|
||||||
|
return False, reason
|
||||||
|
except socket.timeout:
|
||||||
|
return False, f"HTTP 请求超时 ({timeout}s)"
|
||||||
|
except ConnectionRefusedError:
|
||||||
|
return False, "连接被拒绝 (服务未启动或端口未监听)"
|
||||||
|
except socket.gaierror:
|
||||||
|
return False, "DNS 解析失败"
|
||||||
|
except OSError as e:
|
||||||
|
return False, f"网络错误: {e.strerror if hasattr(e, 'strerror') else str(e)}"
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
"""
|
||||||
|
ai_review 黑盒用户模拟测试 — 内容审核 + 人工反馈。
|
||||||
|
|
||||||
|
用户旅程:
|
||||||
|
1. 提交明显违规内容 → 应返回 invalid: true
|
||||||
|
2. 提交正常无害内容 → 应返回 invalid: false
|
||||||
|
3. 提交空内容 → 应返回 400 错误
|
||||||
|
4. 缺少 content 字段 → 应返回 400 错误
|
||||||
|
5. 人工反馈纠正误判 → RAG 知识库更新
|
||||||
|
6. 查看 RAG 列表
|
||||||
|
7. 健康检查
|
||||||
|
|
||||||
|
注意: ai_review 依赖 DeepSeek API + ChromaDB,API 调用可能因网络/配额失败。
|
||||||
|
测试使用了通用违规关键词,实际审核结果取决于 DeepSeek 模型判断。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
from helpers import (
|
||||||
|
http_get, http_post, try_parse_json,
|
||||||
|
assert_eq, assert_status, assert_true, assert_json_key,
|
||||||
|
skip, Counter, check_service,
|
||||||
|
)
|
||||||
|
from config import get_url
|
||||||
|
|
||||||
|
AI_REVIEW_URL = get_url("ai_review")
|
||||||
|
|
||||||
|
|
||||||
|
def run(url: str = None):
|
||||||
|
"""ai_review 用户模拟测试入口。返回 (passed, failed, skipped)。"""
|
||||||
|
au = url or AI_REVIEW_URL
|
||||||
|
c = Counter()
|
||||||
|
|
||||||
|
print(f"\n--- ai_review 内容审核流程 ---\n")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 0. 检查服务可用性
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
if not check_service(au)[0]:
|
||||||
|
skip("全部测试", "ai_review 服务不可达", c)
|
||||||
|
return c.passed, c.failed, c.skipped
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 1: 提交明显违规内容 — 应标记为违规
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
violation_texts = [
|
||||||
|
"出售毒品冰毒海洛因,价格优惠,联系QQ123456",
|
||||||
|
"提供赌博网站搭建服务,包月维护,日入过万",
|
||||||
|
]
|
||||||
|
violation_text = random.choice(violation_texts)
|
||||||
|
|
||||||
|
status, body, _ = http_post(f"{au}/review", {"content": violation_text})
|
||||||
|
assert_eq(status, 200, "审核-违规内容-响应 (200)", c)
|
||||||
|
data = try_parse_json(body)
|
||||||
|
if data:
|
||||||
|
# 注意: invalid 可能是字符串 "true"/"false" 而非布尔值
|
||||||
|
invalid = data.get("invalid")
|
||||||
|
is_invalid = invalid in (True, "true", "True")
|
||||||
|
assert_true(
|
||||||
|
is_invalid,
|
||||||
|
"审核-违规内容-标记为invalid", c,
|
||||||
|
f"invalid={invalid}, body={body[:200]}",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
assert_true(False, "审核-违规内容-解析失败", c, f"body={body[:200]}")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 2: 提交正常无害内容 — 应通过
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
safe_texts = [
|
||||||
|
"今天天气真好,适合出去散步。",
|
||||||
|
"Python 是一门优秀的编程语言,广泛应用于数据科学和人工智能领域。",
|
||||||
|
"微服务架构是一种将应用拆分为多个小型独立服务的软件设计方法。",
|
||||||
|
]
|
||||||
|
safe_text = random.choice(safe_texts)
|
||||||
|
|
||||||
|
status, body, _ = http_post(f"{au}/review", {"content": safe_text})
|
||||||
|
assert_eq(status, 200, "审核-正常内容-响应 (200)", c)
|
||||||
|
data = try_parse_json(body)
|
||||||
|
if data:
|
||||||
|
invalid = data.get("invalid")
|
||||||
|
is_invalid = invalid in (True, "true", "True")
|
||||||
|
# 正常内容应该不违规
|
||||||
|
assert_true(
|
||||||
|
not is_invalid,
|
||||||
|
"审核-正常内容-标记为safe", c,
|
||||||
|
f"invalid={invalid}, body={body[:200]}",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
assert_true(False, "审核-正常内容-解析失败", c, f"body={body[:200]}")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 3: 提交空内容 — 应返回 400
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_post(f"{au}/review", {"content": ""})
|
||||||
|
assert_eq(status, 400, "审核-空内容-拒绝 (400)", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 4: 缺少 content 字段 — 应返回 400
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_post(f"{au}/review", {})
|
||||||
|
assert_eq(status, 400, "审核-缺少content-拒绝 (400)", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 5: RAG 人工反馈 — 纠正误判(标注一条违规内容)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
feedback_text = f"test_feedback_{random.randint(10000, 99999)}: 非法内容"
|
||||||
|
status, body, _ = http_post(f"{au}/rag/feedback", {
|
||||||
|
"content": feedback_text,
|
||||||
|
"correct_type": "violation",
|
||||||
|
})
|
||||||
|
assert_eq(status, 200, "RAG反馈-添加violation (200)", c)
|
||||||
|
data = try_parse_json(body)
|
||||||
|
if data:
|
||||||
|
assert_true(
|
||||||
|
data.get("added") in (True, False),
|
||||||
|
"RAG反馈-返回added字段", c,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 6: RAG 反馈 — 参数错误
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_post(f"{au}/rag/feedback", {
|
||||||
|
"content": "test",
|
||||||
|
"correct_type": "invalid_type",
|
||||||
|
})
|
||||||
|
assert_eq(status, 400, "RAG反馈-错误类型-拒绝 (400)", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 7: RAG 列表查询
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_get(f"{au}/rag/list")
|
||||||
|
assert_eq(status, 200, "RAG列表-查询 (200)", c)
|
||||||
|
data = try_parse_json(body)
|
||||||
|
if data:
|
||||||
|
assert_json_key(data, "total", "RAG列表-有total字段", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 8: 健康检查
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_get(f"{au}/health")
|
||||||
|
assert_eq(status, 200, "健康检查 (200)", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 汇总
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
print(f"\n ai_review 测试完成: {c.summary()}")
|
||||||
|
return c.passed, c.failed, c.skipped
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
"""
|
||||||
|
captcha 黑盒用户模拟测试 — 获取验证码 → 校验 → 一次性消费。
|
||||||
|
|
||||||
|
用户旅程:
|
||||||
|
1. 请求 /captcha/image 获取验证码图片 + token
|
||||||
|
2. 使用正确答案校验 → 应通过
|
||||||
|
3. 使用错误答案校验 → 应拒绝
|
||||||
|
4. 同一 token 重复校验 → 应拒绝 (一次性)
|
||||||
|
5. 缺少 token/answer → 应报错
|
||||||
|
6. 不存在的 token → 应拒绝
|
||||||
|
7. 健康检查
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import string
|
||||||
|
|
||||||
|
from helpers import (
|
||||||
|
http_get, http_post, http_request,
|
||||||
|
assert_eq, assert_status, assert_true, assert_json_key, assert_contains,
|
||||||
|
skip, Counter, check_service,
|
||||||
|
)
|
||||||
|
from config import get_url
|
||||||
|
|
||||||
|
CAPTCHA_URL = get_url("captcha")
|
||||||
|
|
||||||
|
|
||||||
|
def run(url: str = None):
|
||||||
|
"""captcha 用户模拟测试入口。返回 (passed, failed, skipped)。"""
|
||||||
|
cu = url or CAPTCHA_URL
|
||||||
|
c = Counter()
|
||||||
|
|
||||||
|
print(f"\n--- captcha 验证码获取与校验流程 ---\n")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 0. 检查服务可用性
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
if not check_service(cu)[0]:
|
||||||
|
print(" captcha 服务不可达,跳过所有测试")
|
||||||
|
c.add_skip()
|
||||||
|
return c.passed, c.failed, c.skipped
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 1: 获取验证码图片
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, headers = http_get(f"{cu}/captcha/image")
|
||||||
|
assert_eq(status, 200, "获取验证码图片 (200)", c)
|
||||||
|
captcha_token = headers.get("X-Captcha-Token", headers.get("x-captcha-token", ""))
|
||||||
|
assert_true(
|
||||||
|
len(captcha_token) == 32,
|
||||||
|
"获取验证码-Token 32位hex", c,
|
||||||
|
f"token={captcha_token[:10]}...",
|
||||||
|
)
|
||||||
|
# 验证返回的是图片
|
||||||
|
content_type = headers.get("Content-Type", headers.get("content-type", ""))
|
||||||
|
assert_contains(content_type, "image", "获取验证码-图片类型", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 2: 缺少参数 — token 和 answer 都缺
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_post(f"{cu}/captcha/verify", {})
|
||||||
|
data = json.loads(body) if body else {}
|
||||||
|
assert_true(
|
||||||
|
not data.get("valid", True),
|
||||||
|
"校验-缺少参数-拒绝", c,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 3: 不存在的 token
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_post(f"{cu}/captcha/verify", {
|
||||||
|
"token": "0" * 32,
|
||||||
|
"answer": "abcde",
|
||||||
|
})
|
||||||
|
data = json.loads(body) if body else {}
|
||||||
|
assert_true(
|
||||||
|
not data.get("valid", True),
|
||||||
|
"校验-不存在token-拒绝", c,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 4: 错误答案
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
if captcha_token:
|
||||||
|
status, body, _ = http_post(f"{cu}/captcha/verify", {
|
||||||
|
"token": captcha_token,
|
||||||
|
"answer": "ZZZZZ",
|
||||||
|
})
|
||||||
|
data = json.loads(body) if body else {}
|
||||||
|
assert_true(
|
||||||
|
not data.get("valid", True),
|
||||||
|
"校验-错误答案-拒绝", c,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 5: 验证码一次性消费 — 拿新 token 分别测正确答案和二次使用
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 重新获取一个 captcha(上一步的 token 已被消费)
|
||||||
|
status2, body2, headers2 = http_get(f"{cu}/captcha/image")
|
||||||
|
assert_eq(status2, 200, "获取第二个验证码 (200)", c)
|
||||||
|
|
||||||
|
# 没法从图片自动识别文字,但我们可以测试一次性消费的行为
|
||||||
|
# 先用错误答案消费 token
|
||||||
|
token2 = headers2.get("X-Captcha-Token", headers.get("x-captcha-token", ""))
|
||||||
|
if token2:
|
||||||
|
# 第一次:错误答案
|
||||||
|
status_a, body_a, _ = http_post(f"{cu}/captcha/verify", {
|
||||||
|
"token": token2,
|
||||||
|
"answer": "WRONG",
|
||||||
|
})
|
||||||
|
data_a = json.loads(body_a) if body_a else {}
|
||||||
|
assert_true(
|
||||||
|
not data_a.get("valid", True),
|
||||||
|
"一次性消费-第一次用完", c,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 第二次:同一 token 再次使用
|
||||||
|
status_b, body_b, _ = http_post(f"{cu}/captcha/verify", {
|
||||||
|
"token": token2,
|
||||||
|
"answer": "WRONG",
|
||||||
|
})
|
||||||
|
data_b = json.loads(body_b) if body_b else {}
|
||||||
|
assert_true(
|
||||||
|
not data_b.get("valid", True),
|
||||||
|
"一次性消费-重复使用拒绝", c,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 6: 健康检查
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_get(f"{cu}/health")
|
||||||
|
assert_eq(status, 200, "健康检查 (200)", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 7: 不存在路径 → 404
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_get(f"{cu}/nonexistent")
|
||||||
|
assert_eq(status, 404, "不存在路径-404", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 汇总
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
print(f"\n captcha 测试完成: {c.summary()}")
|
||||||
|
return c.passed, c.failed, c.skipped
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
"""
|
||||||
|
db 黑盒用户模拟测试 — 集中日志: 写入 → 查询 → 统计 → 清理。
|
||||||
|
|
||||||
|
用户旅程:
|
||||||
|
1. 通过 pymsg 协议执行 CRUD (select/insert)
|
||||||
|
2. 通过 HTTP /log/ingest 写入日志
|
||||||
|
3. 通过 HTTP /log/query 查询日志
|
||||||
|
4. 通过 HTTP /log/stats 查看统计
|
||||||
|
5. 通过 HTTP /log/health 健康检查
|
||||||
|
|
||||||
|
注意: dbv2.py (pymsg 模式) 不挂载 Flask log_module Blueprint,
|
||||||
|
因此 HTTP /log/* 路由可能不可用。测试会优雅降级。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import time
|
||||||
|
|
||||||
|
from helpers import (
|
||||||
|
http_get, http_post, try_parse_json,
|
||||||
|
assert_eq, assert_status, assert_true, assert_json_key,
|
||||||
|
skip, Counter, check_service, PymsgClient,
|
||||||
|
)
|
||||||
|
from config import get_url
|
||||||
|
|
||||||
|
DB_URL = get_url("db")
|
||||||
|
DB_HOST = "127.0.0.1"
|
||||||
|
DB_PORT = 8005
|
||||||
|
|
||||||
|
|
||||||
|
def run(url: str = None):
|
||||||
|
"""db 用户模拟测试入口。返回 (passed, failed, skipped)。"""
|
||||||
|
du = url or DB_URL
|
||||||
|
c = Counter()
|
||||||
|
|
||||||
|
print(f"\n--- db 数据库与日志流程 ---\n")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 0. 检查服务可用性
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
if not check_service(du)[0]:
|
||||||
|
skip("全部测试", "db 服务不可达", c)
|
||||||
|
return c.passed, c.failed, c.skipped
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 第一部分: pymsg 协议 CRUD
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
print(" [pymsg 协议测试]")
|
||||||
|
|
||||||
|
client = PymsgClient(DB_HOST, DB_PORT, "test_blackbox")
|
||||||
|
|
||||||
|
# 场景 1: pymsg 查询 Users 表
|
||||||
|
result = client.call({
|
||||||
|
"_route": "execute",
|
||||||
|
"action": "select",
|
||||||
|
"table": "Users",
|
||||||
|
"limit": 5,
|
||||||
|
})
|
||||||
|
assert_true(
|
||||||
|
result.get("success") is True,
|
||||||
|
"pymsg-查询Users表", c,
|
||||||
|
f"error={result.get('error', '')}",
|
||||||
|
)
|
||||||
|
if result.get("success"):
|
||||||
|
data = result.get("data", [])
|
||||||
|
assert_true(
|
||||||
|
isinstance(data, list),
|
||||||
|
"pymsg-返回列表格式", c,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 场景 2: pymsg 查询 Logs 表
|
||||||
|
result = client.call({
|
||||||
|
"_route": "execute",
|
||||||
|
"action": "select",
|
||||||
|
"table": "Logs",
|
||||||
|
"limit": 3,
|
||||||
|
"order_by": [{"column": "id", "desc": True}],
|
||||||
|
})
|
||||||
|
assert_true(
|
||||||
|
result.get("success") is True,
|
||||||
|
"pymsg-查询Logs表", c,
|
||||||
|
f"error={result.get('error', '')}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 场景 3: pymsg 使用 where 条件
|
||||||
|
result = client.call({
|
||||||
|
"_route": "execute",
|
||||||
|
"action": "select",
|
||||||
|
"table": "Users",
|
||||||
|
"columns": ["id", "username"],
|
||||||
|
"where": {"id": {"gt": 0}},
|
||||||
|
"limit": 1,
|
||||||
|
})
|
||||||
|
assert_true(
|
||||||
|
result.get("success") is True,
|
||||||
|
"pymsg-条件查询", c,
|
||||||
|
f"error={result.get('error', '')}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 场景 4: pymsg 无效路由
|
||||||
|
result = client.call({
|
||||||
|
"_route": "nonexistent_route",
|
||||||
|
"action": "select",
|
||||||
|
"table": "Users",
|
||||||
|
})
|
||||||
|
assert_true(
|
||||||
|
result.get("success") is False,
|
||||||
|
"pymsg-无效路由-拒绝", c,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 第二部分: HTTP /log/* 接口 (Flask log_module)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
print("\n [HTTP /log/* 接口测试]")
|
||||||
|
|
||||||
|
# 检查 /log/health 是否可用(Flask log_module 是否挂载)
|
||||||
|
status, body, _ = http_get(f"{du}/log/health")
|
||||||
|
if status == 0 or status >= 500:
|
||||||
|
skip("HTTP /log/* 测试", "log_module 未挂载 (dbv2 使用 pymsg 模式, 非 Flask)", c)
|
||||||
|
else:
|
||||||
|
# 场景 5: /log/health 健康检查
|
||||||
|
assert_eq(status, 200, "log-健康检查 (200)", c)
|
||||||
|
|
||||||
|
# 场景 6: /log/ingest 写入日志
|
||||||
|
test_ts = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||||
|
status, body, _ = http_post(f"{du}/log/ingest", {
|
||||||
|
"logs": [{
|
||||||
|
"service": "dnfs",
|
||||||
|
"level": "INFO",
|
||||||
|
"message": f"blackbox_test_{random.randint(10000, 99999)}",
|
||||||
|
"ts": test_ts,
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
data = try_parse_json(body)
|
||||||
|
assert_true(
|
||||||
|
status == 200 and data is not None and data.get("success") is True,
|
||||||
|
"log-写入日志", c,
|
||||||
|
f"status={status} body={body[:150]}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 场景 7: /log/ingest — 空日志列表
|
||||||
|
status, body, _ = http_post(f"{du}/log/ingest", {"logs": []})
|
||||||
|
assert_eq(status, 200, "log-空日志列表 (200)", c)
|
||||||
|
|
||||||
|
# 场景 8: /log/ingest — 缺少 logs 字段
|
||||||
|
status, body, _ = http_post(f"{du}/log/ingest", {})
|
||||||
|
data = try_parse_json(body)
|
||||||
|
assert_true(
|
||||||
|
data is not None and not data.get("success", True),
|
||||||
|
"log-缺少logs字段-拒绝", c,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 场景 9: /log/query 查询日志
|
||||||
|
status, body, _ = http_post(f"{du}/log/query", {
|
||||||
|
"limit": 5,
|
||||||
|
"desc": True,
|
||||||
|
})
|
||||||
|
data = try_parse_json(body)
|
||||||
|
assert_true(
|
||||||
|
status == 200 and data is not None and data.get("success") is True,
|
||||||
|
"log-查询日志", c,
|
||||||
|
f"status={status}",
|
||||||
|
)
|
||||||
|
if data and data.get("success"):
|
||||||
|
assert_true(
|
||||||
|
"logs" in data.get("data", {}),
|
||||||
|
"log-查询返回logs字段", c,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 场景 10: /log/stats 统计
|
||||||
|
status, body, _ = http_post(f"{du}/log/stats", {
|
||||||
|
"group_by": "service",
|
||||||
|
})
|
||||||
|
data = try_parse_json(body)
|
||||||
|
assert_true(
|
||||||
|
status == 200 and data is not None,
|
||||||
|
"log-统计", c,
|
||||||
|
f"status={status}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 汇总
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
print(f"\n db 测试完成: {c.summary()}")
|
||||||
|
return c.passed, c.failed, c.skipped
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
"""
|
||||||
|
dnfs 黑盒用户模拟测试 — 创建笔记 → 查询 → 更新 → 删除(含内容审核)。
|
||||||
|
|
||||||
|
用户旅程:
|
||||||
|
已登录用户携带 Bearer Token:
|
||||||
|
1. 创建根节点笔记(内容经 ai_review 审核)
|
||||||
|
2. 创建子节点笔记
|
||||||
|
3. 查询节点列表
|
||||||
|
4. 更新笔记标题和内容
|
||||||
|
5. 删除叶子节点(无子节点)
|
||||||
|
6. 删除非空节点(应拒绝)
|
||||||
|
7. 无 Token 操作(应拒绝)
|
||||||
|
8. 创建时缺少必填参数(应报错)
|
||||||
|
|
||||||
|
前提: gateway 已启动,有已注册用户和有效 Token。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
from helpers import (
|
||||||
|
http_post, try_parse_json,
|
||||||
|
assert_eq, assert_status, assert_true, assert_json_key,
|
||||||
|
skip, Counter, check_service,
|
||||||
|
)
|
||||||
|
from config import get_url, SERVICES
|
||||||
|
|
||||||
|
DNFS_URL = get_url("dnfs")
|
||||||
|
GATEWAY_URL = get_url("gateway")
|
||||||
|
|
||||||
|
|
||||||
|
def _random_test_user():
|
||||||
|
suffix = ''.join(random.choices(string.ascii_lowercase, k=6))
|
||||||
|
return f"dtest_{suffix}@test.local"
|
||||||
|
|
||||||
|
|
||||||
|
def _register_and_login(gw_url: str, counter: Counter):
|
||||||
|
"""通过 gateway 注册并登录,返回 (token, email)。失败返回 (None, None)。"""
|
||||||
|
email = _random_test_user()
|
||||||
|
password = "TestP@ss123"
|
||||||
|
|
||||||
|
# 注册
|
||||||
|
status, body, _ = http_post(f"{gw_url}/register", {
|
||||||
|
"username": email,
|
||||||
|
"password": password,
|
||||||
|
})
|
||||||
|
if status != 200:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
data = try_parse_json(body)
|
||||||
|
if not data or not data.get("success"):
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# 登录
|
||||||
|
status, body, _ = http_post(f"{gw_url}/login", {
|
||||||
|
"username": email,
|
||||||
|
"password": password,
|
||||||
|
})
|
||||||
|
if status != 200:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
data = try_parse_json(body)
|
||||||
|
if not data or not data.get("success"):
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
token = (data.get("data") or {}).get("token", "")
|
||||||
|
return token, email
|
||||||
|
|
||||||
|
|
||||||
|
def run(url: str = None):
|
||||||
|
"""dnfs 用户模拟测试入口。返回 (passed, failed, skipped)。"""
|
||||||
|
du = url or DNFS_URL
|
||||||
|
c = Counter()
|
||||||
|
|
||||||
|
print(f"\n--- dnfs 笔记管理流程 ---\n")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 0. 检查服务可用性
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
if not check_service(du)[0]:
|
||||||
|
skip("全部测试", "dnfs 服务不可达", c)
|
||||||
|
return c.passed, c.failed, c.skipped
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 准备: 注册用户并获取 Token
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
gw_available = check_service(GATEWAY_URL)[0]
|
||||||
|
if not gw_available:
|
||||||
|
skip("全部测试", "gateway 服务不可达,无法获取 Token", c)
|
||||||
|
return c.passed, c.failed, c.skipped
|
||||||
|
|
||||||
|
token, email = _register_and_login(GATEWAY_URL, c)
|
||||||
|
if not token:
|
||||||
|
skip("全部测试", "无法完成注册/登录获取 Token", c)
|
||||||
|
return c.passed, c.failed, c.skipped
|
||||||
|
|
||||||
|
auth_headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 1: 无 Token 创建笔记 — 应拒绝
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_post(f"{du}/add_note", {
|
||||||
|
"pid": 0,
|
||||||
|
"title": "test",
|
||||||
|
"content": "test content",
|
||||||
|
})
|
||||||
|
assert_eq(status, 401, "创建笔记-无Token-拒绝 (401)", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 2: 创建笔记 — 缺少参数
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_post(f"{du}/add_note", {
|
||||||
|
"pid": 0,
|
||||||
|
}, headers=auth_headers)
|
||||||
|
assert_eq(status, 400, "创建笔记-缺少title/content (400)", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 3: 创建根节点笔记
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
note_title = f"测试笔记_{random.randint(1000, 9999)}"
|
||||||
|
note_content = "这是一段正常的测试内容,用于验证笔记创建功能。"
|
||||||
|
status, body, _ = http_post(f"{du}/add_note", {
|
||||||
|
"pid": 0,
|
||||||
|
"title": note_title,
|
||||||
|
"content": note_content,
|
||||||
|
}, headers=auth_headers)
|
||||||
|
# 期望 200(审核通过则成功)
|
||||||
|
data = try_parse_json(body)
|
||||||
|
assert_true(
|
||||||
|
status == 200 and data is not None and data.get("success") is True,
|
||||||
|
"创建笔记-根节点", c,
|
||||||
|
f"status={status} body={body[:150]}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 4: 查询根节点下的笔记列表
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_post(f"{du}/get_notes", {"pid": 0})
|
||||||
|
assert_eq(status, 200, "查询笔记-根节点 (200)", c)
|
||||||
|
notes = try_parse_json(body)
|
||||||
|
assert_true(
|
||||||
|
isinstance(notes, list),
|
||||||
|
"查询笔记-返回列表", c,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 5: 创建子节点笔记
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 先获取刚创建的笔记 ID
|
||||||
|
child_note_id = None
|
||||||
|
if notes and len(notes) > 0:
|
||||||
|
# 找到我们刚创建的笔记
|
||||||
|
for note in notes:
|
||||||
|
if note.get("title") == note_title:
|
||||||
|
child_note_id = note.get("id")
|
||||||
|
break
|
||||||
|
|
||||||
|
if child_note_id:
|
||||||
|
child_title = f"子笔记_{random.randint(1000, 9999)}"
|
||||||
|
child_content = "这是子节点的测试内容。"
|
||||||
|
status, body, _ = http_post(f"{du}/add_note", {
|
||||||
|
"pid": child_note_id,
|
||||||
|
"title": child_title,
|
||||||
|
"content": child_content,
|
||||||
|
}, headers=auth_headers)
|
||||||
|
data = try_parse_json(body)
|
||||||
|
assert_true(
|
||||||
|
status == 200 and data is not None,
|
||||||
|
"创建笔记-子节点", c,
|
||||||
|
f"status={status} body={body[:150]}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 6: 删除非空节点 — 应拒绝(有子节点)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
if child_note_id:
|
||||||
|
status, body, _ = http_post(f"{du}/delete_note", {
|
||||||
|
"id": child_note_id,
|
||||||
|
}, headers=auth_headers)
|
||||||
|
data = try_parse_json(body)
|
||||||
|
assert_true(
|
||||||
|
status in (400, 403) or (data and not data.get("success")),
|
||||||
|
"删除笔记-非空节点-拒绝", c,
|
||||||
|
f"status={status} body={body[:150]}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 7: 更新笔记内容
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
if child_note_id:
|
||||||
|
updated_title = f"更新_{random.randint(1000, 9999)}"
|
||||||
|
updated_content = "更新后的测试内容,依然正常无害。"
|
||||||
|
status, body, _ = http_post(f"{du}/update_note", {
|
||||||
|
"id": child_note_id,
|
||||||
|
"pid": 0,
|
||||||
|
"title": updated_title,
|
||||||
|
"content": updated_content,
|
||||||
|
}, headers=auth_headers)
|
||||||
|
data = try_parse_json(body)
|
||||||
|
assert_true(
|
||||||
|
status == 200 and data is not None and data.get("success") is True,
|
||||||
|
"更新笔记-内容", c,
|
||||||
|
f"status={status} body={body[:150]}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 8: 更新笔记 — 缺少参数
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_post(f"{du}/update_note", {
|
||||||
|
"id": child_note_id,
|
||||||
|
}, headers=auth_headers)
|
||||||
|
assert_eq(status, 400, "更新笔记-缺少参数 (400)", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 9: 删除笔记 — 缺少 id 参数
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_post(f"{du}/delete_note", {}, headers=auth_headers)
|
||||||
|
assert_eq(status, 400, "删除笔记-缺少id (400)", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 汇总
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
print(f"\n dnfs 测试完成: {c.summary()}")
|
||||||
|
return c.passed, c.failed, c.skipped
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
"""
|
||||||
|
gateway 黑盒用户模拟测试 — 注册 → 登录 → Token → 代理转发。
|
||||||
|
|
||||||
|
用户旅程:
|
||||||
|
1. 注册新账号(含密码强度校验、邮箱格式校验)
|
||||||
|
2. 登录获取 Token
|
||||||
|
3. Token 用于代理访问后端服务
|
||||||
|
4. 重复注册(应失败)
|
||||||
|
5. 错误密码登录(应失败)
|
||||||
|
6. 无效 Token 代理(应拒绝)
|
||||||
|
7. 密码强度不足(应拒绝)
|
||||||
|
8. 邮箱格式错误(应拒绝)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import time
|
||||||
|
|
||||||
|
from helpers import (
|
||||||
|
http_get, http_post, try_parse_json,
|
||||||
|
assert_eq, assert_status, assert_true, assert_json_key,
|
||||||
|
Counter, check_service,
|
||||||
|
)
|
||||||
|
from config import get_url
|
||||||
|
|
||||||
|
GATEWAY_URL = get_url("gateway")
|
||||||
|
STATIC_URL = get_url("static")
|
||||||
|
|
||||||
|
|
||||||
|
def _random_email():
|
||||||
|
"""生成随机测试邮箱。"""
|
||||||
|
suffix = ''.join(random.choices(string.ascii_lowercase, k=8))
|
||||||
|
return f"test_{suffix}@example.com"
|
||||||
|
|
||||||
|
|
||||||
|
def _random_password():
|
||||||
|
"""生成符合强度要求的随机密码。"""
|
||||||
|
chars = string.ascii_letters + string.digits + "!@#$%"
|
||||||
|
return ''.join(random.choices(chars, k=12))
|
||||||
|
|
||||||
|
|
||||||
|
def run(url: str = None):
|
||||||
|
"""gateway 用户模拟测试入口。返回 (passed, failed, skipped)。"""
|
||||||
|
gw = url or GATEWAY_URL
|
||||||
|
c = Counter()
|
||||||
|
|
||||||
|
print(f"\n--- gateway 用户注册与登录流程 ---\n")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 0. 检查服务可用性
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
if not check_service(gw)[0]:
|
||||||
|
print(" gateway 服务不可达,跳过所有测试")
|
||||||
|
c.add_skip()
|
||||||
|
return c.passed, c.failed, c.skipped
|
||||||
|
|
||||||
|
email = _random_email()
|
||||||
|
password = _random_password()
|
||||||
|
token = None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 1: 注册 — 密码强度不足
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_post(f"{gw}/register", {
|
||||||
|
"username": email,
|
||||||
|
"password": "123",
|
||||||
|
})
|
||||||
|
assert_eq(status, 401, "注册-密码太短 (401)", c)
|
||||||
|
data = try_parse_json(body)
|
||||||
|
assert_true(
|
||||||
|
data is not None and not data.get("success"),
|
||||||
|
"注册-密码太短-拒绝", c,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 2: 注册 — 邮箱格式错误
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_post(f"{gw}/register", {
|
||||||
|
"username": "not-an-email",
|
||||||
|
"password": _random_password(),
|
||||||
|
})
|
||||||
|
assert_eq(status, 400, "注册-邮箱格式错误 (400)", c)
|
||||||
|
data = try_parse_json(body)
|
||||||
|
assert_true(
|
||||||
|
data is not None and not data.get("success"),
|
||||||
|
"注册-邮箱格式错误-拒绝", c,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 3: 注册 — 成功
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_post(f"{gw}/register", {
|
||||||
|
"username": email,
|
||||||
|
"password": password,
|
||||||
|
})
|
||||||
|
assert_eq(status, 200, "注册-成功 (200)", c)
|
||||||
|
data = try_parse_json(body)
|
||||||
|
assert_true(
|
||||||
|
data is not None and data.get("success") is True,
|
||||||
|
"注册-返回 success=true", c,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 4: 注册 — 重复用户名
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_post(f"{gw}/register", {
|
||||||
|
"username": email,
|
||||||
|
"password": password,
|
||||||
|
})
|
||||||
|
# 期望注册失败(用户名已存在)
|
||||||
|
assert_true(
|
||||||
|
status != 200 or (try_parse_json(body) or {}).get("success") is not True,
|
||||||
|
"注册-重复用户名-拒绝", c,
|
||||||
|
f"status={status}, body={body[:100]}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 5: 登录 — 错误密码
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_post(f"{gw}/login", {
|
||||||
|
"username": email,
|
||||||
|
"password": "WrongPassword1",
|
||||||
|
})
|
||||||
|
assert_eq(status, 401, "登录-错误密码 (401)", c)
|
||||||
|
data = try_parse_json(body)
|
||||||
|
assert_true(
|
||||||
|
data is not None and not data.get("success"),
|
||||||
|
"登录-错误密码-拒绝", c,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 6: 登录 — 成功获取 Token
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_post(f"{gw}/login", {
|
||||||
|
"username": email,
|
||||||
|
"password": password,
|
||||||
|
})
|
||||||
|
assert_eq(status, 200, "登录-成功 (200)", c)
|
||||||
|
data = try_parse_json(body)
|
||||||
|
assert_true(
|
||||||
|
data is not None and data.get("success") is True,
|
||||||
|
"登录-返回 success=true", c,
|
||||||
|
)
|
||||||
|
token_data = (data or {}).get("data", {})
|
||||||
|
token = token_data.get("token", "")
|
||||||
|
assert_true(
|
||||||
|
len(token) > 20 if token else False,
|
||||||
|
"登录-Token 不为空", c,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 7: 代理 — 使用 Token 访问静态服务
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
if token:
|
||||||
|
status, body, headers = http_get(
|
||||||
|
f"{gw}/",
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Target-Url": STATIC_URL,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert_true(
|
||||||
|
status in (200, 302, 301),
|
||||||
|
"代理-GET静态首页", c,
|
||||||
|
f"status={status}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 8: 代理 — 无 Token 应拒绝
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_get(
|
||||||
|
f"{gw}/some-path",
|
||||||
|
headers={"Target-Url": STATIC_URL},
|
||||||
|
)
|
||||||
|
# 缺少 Token 应返回 400/401
|
||||||
|
assert_true(
|
||||||
|
status in (400, 401, 403),
|
||||||
|
"代理-无Token拒绝", c,
|
||||||
|
f"status={status}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 9: 代理 — 无效路径(路径遍历)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
if token:
|
||||||
|
status, body, _ = http_get(
|
||||||
|
f"{gw}/../../../etc/passwd",
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Target-Url": STATIC_URL,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert_true(
|
||||||
|
status in (400, 403),
|
||||||
|
"代理-路径遍历拒绝", c,
|
||||||
|
f"status={status}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 10: 默认代理 (无 Target-Url,无需 Token)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_get(f"{gw}/")
|
||||||
|
assert_true(
|
||||||
|
status in (200, 302, 301),
|
||||||
|
"代理-默认路由", c,
|
||||||
|
f"status={status}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 汇总
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
print(f"\n gateway 测试完成: {c.summary()}")
|
||||||
|
return c.passed, c.failed, c.skipped
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
"""
|
||||||
|
micro_servers 黑盒测试 — 统一运行入口。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python3 test_runner.py # 运行全部测试
|
||||||
|
python3 test_runner.py --url http://127.0.0.1:8443 # 测试指定服务
|
||||||
|
python3 test_runner.py --list # 列出可用测试
|
||||||
|
python3 test_runner.py --service gateway # 按服务名运行
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# 确保当前目录在 path 中
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from config import SERVICES, PORT_MAP, detect_service, get_url
|
||||||
|
from helpers import check_service
|
||||||
|
|
||||||
|
TEST_MODULES = {
|
||||||
|
"gateway": "test_gateway",
|
||||||
|
"captcha": "test_captcha",
|
||||||
|
"smtp": "test_smtp",
|
||||||
|
"dnfs": "test_dnfs",
|
||||||
|
"db": "test_db",
|
||||||
|
"ai_review": "test_ai_review",
|
||||||
|
"static": "test_static",
|
||||||
|
"waf": "test_waf",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run_service_test(name: str, url: str) -> tuple:
|
||||||
|
"""运行单个服务的测试,返回 (passed, failed, skipped)。"""
|
||||||
|
module_name = TEST_MODULES.get(name)
|
||||||
|
if not module_name:
|
||||||
|
print(f"未知服务: {name}")
|
||||||
|
return 0, 0, 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
mod = importlib.import_module(module_name)
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"无法加载测试模块 {module_name}: {e}")
|
||||||
|
return 0, 0, 0
|
||||||
|
|
||||||
|
if not hasattr(mod, "run"):
|
||||||
|
print(f"模块 {module_name} 缺少 run(url) 函数")
|
||||||
|
return 0, 0, 0
|
||||||
|
|
||||||
|
svc = SERVICES[name]
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f" {svc['description']} ({url})")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
|
||||||
|
reachable, detail = check_service(url)
|
||||||
|
if not reachable:
|
||||||
|
print(f" [WARN] 服务不可达: {url}")
|
||||||
|
if detail:
|
||||||
|
print(f" 原因: {detail}")
|
||||||
|
print(f" 所有测试将被跳过。请先启动服务。\n")
|
||||||
|
return 0, 0, 0
|
||||||
|
|
||||||
|
passed, failed, skipped = mod.run(url)
|
||||||
|
return passed, failed, skipped
|
||||||
|
|
||||||
|
|
||||||
|
def run_all():
|
||||||
|
"""运行全部服务的测试。"""
|
||||||
|
total_passed = 0
|
||||||
|
total_failed = 0
|
||||||
|
total_skipped = 0
|
||||||
|
|
||||||
|
for name in SERVICES:
|
||||||
|
url = get_url(name)
|
||||||
|
p, f, s = run_service_test(name, url)
|
||||||
|
total_passed += p
|
||||||
|
total_failed += f
|
||||||
|
total_skipped += s
|
||||||
|
|
||||||
|
total = total_passed + total_failed + total_skipped
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f" 全部测试完成: {total} total, "
|
||||||
|
f"{total_passed} passed, {total_failed} failed, {total_skipped} skipped")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
|
||||||
|
return total_failed
|
||||||
|
|
||||||
|
|
||||||
|
def run_by_url(url: str):
|
||||||
|
"""以 URL 为入口,测试全部可代理服务,内部服务自动回退 localhost。
|
||||||
|
|
||||||
|
可代理服务 (通过 WAF→gateway 代理链): waf, gateway, static, dnfs
|
||||||
|
内部服务 (仅 localhost 直连): db, ai_review, captcha, smtp
|
||||||
|
"""
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
hostname = urlparse(url).hostname or ""
|
||||||
|
is_remote = hostname not in ("127.0.0.1", "localhost", "::1")
|
||||||
|
|
||||||
|
if is_remote:
|
||||||
|
print(f" 入口: {url}")
|
||||||
|
print(f" 可代理服务 (waf/gateway/static/dnfs) 通过入口测试")
|
||||||
|
print(f" 内部服务 (db/ai_review/captcha/smtp) 回退到 localhost")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 可通过入口 URL 访问的服务
|
||||||
|
proxyable = {"waf", "gateway", "static", "dnfs"}
|
||||||
|
total_passed = 0
|
||||||
|
total_failed = 0
|
||||||
|
total_skipped = 0
|
||||||
|
|
||||||
|
for name in SERVICES:
|
||||||
|
if name in proxyable:
|
||||||
|
service_url = url
|
||||||
|
else:
|
||||||
|
service_url = get_url(name)
|
||||||
|
|
||||||
|
p, f, s = run_service_test(name, service_url)
|
||||||
|
total_passed += p
|
||||||
|
total_failed += f
|
||||||
|
total_skipped += s
|
||||||
|
|
||||||
|
total = total_passed + total_failed + total_skipped
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f" 全部测试完成: {total} total, "
|
||||||
|
f"{total_passed} passed, {total_failed} failed, {total_skipped} skipped")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
|
||||||
|
return total_failed
|
||||||
|
|
||||||
|
|
||||||
|
def run_by_name(name: str):
|
||||||
|
"""按服务名运行测试。"""
|
||||||
|
svc = SERVICES.get(name)
|
||||||
|
if not svc:
|
||||||
|
print(f"未知服务: {name}")
|
||||||
|
print(f"可用服务: {', '.join(SERVICES.keys())}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
url = get_url(name)
|
||||||
|
p, f, s = run_service_test(name, url)
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def list_tests():
|
||||||
|
"""列出所有可用测试。"""
|
||||||
|
print("可用的黑盒测试:\n")
|
||||||
|
for name, svc in SERVICES.items():
|
||||||
|
print(f" {name:12s} 端口 {svc['port']:<5d} {svc['description']}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="micro_servers 黑盒用户模拟测试",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""
|
||||||
|
示例:
|
||||||
|
%(prog)s # 全部服务 localhost 测试
|
||||||
|
%(prog)s --url https://qsont.me # 公网入口测试全部可代理服务
|
||||||
|
%(prog)s --url http://127.0.0.1:8443 # 本地入口测试全部
|
||||||
|
%(prog)s --service gateway # 仅测试指定服务
|
||||||
|
%(prog)s --list # 列出可用测试
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
parser.add_argument("--url", help="入口 URL,通过它测试全部可代理服务(内部服务回退 localhost)")
|
||||||
|
parser.add_argument("--service", help="按服务名称运行测试")
|
||||||
|
parser.add_argument("--list", action="store_true", help="列出所有可用测试")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.list:
|
||||||
|
list_tests()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.url:
|
||||||
|
return run_by_url(args.url)
|
||||||
|
|
||||||
|
if args.service:
|
||||||
|
return run_by_name(args.service)
|
||||||
|
|
||||||
|
# 默认:运行全部
|
||||||
|
return run_all()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
"""
|
||||||
|
smtp 黑盒用户模拟测试 — 发送邮箱验证码 → 校验。
|
||||||
|
|
||||||
|
用户旅程:
|
||||||
|
1. 发送验证码到测试邮箱
|
||||||
|
2. 用错误验证码校验 → 应失败
|
||||||
|
3. 用空参数校验 → 应报错
|
||||||
|
|
||||||
|
注意: SMTP 服务需要配置真实的邮件服务器。若服务不可达或邮件发送失败,
|
||||||
|
相关测试将标记为 SKIP。验证码有效期为 60 秒。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
from helpers import (
|
||||||
|
http_post, try_parse_json,
|
||||||
|
assert_eq, assert_status, assert_true,
|
||||||
|
skip, Counter, check_service,
|
||||||
|
)
|
||||||
|
from config import get_url
|
||||||
|
|
||||||
|
SMTP_URL = get_url("smtp")
|
||||||
|
|
||||||
|
|
||||||
|
def run(url: str = None):
|
||||||
|
"""smtp 用户模拟测试入口。返回 (passed, failed, skipped)。"""
|
||||||
|
su = url or SMTP_URL
|
||||||
|
c = Counter()
|
||||||
|
|
||||||
|
print(f"\n--- smtp 邮件验证码发送与校验流程 ---\n")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 0. 检查服务可用性
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
if not check_service(su)[0]:
|
||||||
|
skip("全部测试", "smtp 服务不可达", c)
|
||||||
|
return c.passed, c.failed, c.skipped
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 1: 发送验证码 — 缺少 email 参数
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_post(f"{su}/send_email", {})
|
||||||
|
data = try_parse_json(body)
|
||||||
|
# 预期返回错误
|
||||||
|
assert_true(
|
||||||
|
status != 200 or (data and data.get("error")),
|
||||||
|
"发送验证码-缺少email-报错", c,
|
||||||
|
f"status={status}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 2: 发送验证码 — 空 email
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_post(f"{su}/send_email", {"email": ""})
|
||||||
|
data = try_parse_json(body)
|
||||||
|
assert_true(
|
||||||
|
status != 200 or (data and data.get("error")),
|
||||||
|
"发送验证码-空email-报错", c,
|
||||||
|
f"status={status}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 3: 校验验证码 — 缺少参数
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_post(f"{su}/verify_email", {})
|
||||||
|
data = try_parse_json(body)
|
||||||
|
assert_true(
|
||||||
|
status != 200 or (data and data.get("error")),
|
||||||
|
"校验验证码-缺少参数-报错", c,
|
||||||
|
f"status={status}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 4: 校验验证码 — 错误验证码
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
test_email = f"test_{random.randint(10000, 99999)}@example.com"
|
||||||
|
status, body, _ = http_post(f"{su}/verify_email", {
|
||||||
|
"email": test_email,
|
||||||
|
"input_captcha": "000000",
|
||||||
|
})
|
||||||
|
data = try_parse_json(body)
|
||||||
|
# 未发送过的 email 验证应失败
|
||||||
|
assert_true(
|
||||||
|
status != 200 or (data and not data.get("success")),
|
||||||
|
"校验验证码-错误码-拒绝", c,
|
||||||
|
f"status={status} body={body[:100]}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 5: 发送验证码 — 不存在的邮箱域名
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, _ = http_post(f"{su}/send_email", {
|
||||||
|
"email": "nobody@this-domain-does-not-exist-12345.com",
|
||||||
|
})
|
||||||
|
data = try_parse_json(body)
|
||||||
|
# SMTP 可能成功发送请求(服务接受),或者返回错误
|
||||||
|
# 两种情况都算通过 — 至少服务正常响应了
|
||||||
|
assert_true(
|
||||||
|
status in (200, 400, 500) and data is not None,
|
||||||
|
"发送验证码-服务正常响应", c,
|
||||||
|
f"status={status}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 汇总
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
print(f"\n smtp 测试完成: {c.summary()}")
|
||||||
|
return c.passed, c.failed, c.skipped
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
"""
|
||||||
|
static 黑盒用户模拟测试 — 首页加载 → 文件浏览 → 路径遍历防护 → 视频流。
|
||||||
|
|
||||||
|
用户旅程:
|
||||||
|
1. 访问首页 → 返回 HTML 登录页
|
||||||
|
2. 访问 /dnfs.html → 树状笔记前端
|
||||||
|
3. 访问 /api/filelist → 目录列表 JSON
|
||||||
|
4. 访问静态资源 (CSS/JS) → 返回正确 MIME 类型
|
||||||
|
5. 路径遍历攻击 → 返回 403
|
||||||
|
6. 不存在的文件 → 返回 404
|
||||||
|
7. 健康检查
|
||||||
|
8. DNFS 代理接口 (CORS 预检)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from helpers import (
|
||||||
|
http_get, http_post, try_parse_json,
|
||||||
|
assert_eq, assert_status, assert_true, assert_contains,
|
||||||
|
skip, Counter, check_service,
|
||||||
|
)
|
||||||
|
from config import get_url
|
||||||
|
|
||||||
|
STATIC_URL = get_url("static")
|
||||||
|
|
||||||
|
|
||||||
|
def run(url: str = None):
|
||||||
|
"""static 用户模拟测试入口。返回 (passed, failed, skipped)。"""
|
||||||
|
su = url or STATIC_URL
|
||||||
|
c = Counter()
|
||||||
|
|
||||||
|
print(f"\n--- static 前端静态服务流程 ---\n")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 0. 检查服务可用性
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
if not check_service(su)[0]:
|
||||||
|
skip("全部测试", "static 服务不可达", c)
|
||||||
|
return c.passed, c.failed, c.skipped
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 1: 访问首页
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, headers = http_get(f"{su}/")
|
||||||
|
assert_eq(status, 200, "首页-加载 (200)", c)
|
||||||
|
content_type = headers.get("Content-Type", headers.get("content-type", ""))
|
||||||
|
assert_contains(content_type, "text/html", "首页-Content-Type text/html", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 2: 访问 DNFS 前端页面
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, headers = http_get(f"{su}/dnfs.html")
|
||||||
|
assert_eq(status, 200, "DNFS页面-加载 (200)", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 3: 访问 CSS 资源
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, headers = http_get(f"{su}/css/theme.css")
|
||||||
|
assert_true(
|
||||||
|
status in (200, 404),
|
||||||
|
"CSS资源-请求", c,
|
||||||
|
f"status={status}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 4: 访问 JS 资源
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, headers = http_get(f"{su}/js/dnfs.js")
|
||||||
|
assert_true(
|
||||||
|
status in (200, 404),
|
||||||
|
"JS资源-请求", c,
|
||||||
|
f"status={status}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 5: 路径遍历攻击 — ../ 应返回 403
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, headers = http_get(f"{su}/../../../etc/passwd")
|
||||||
|
assert_eq(status, 403, "路径遍历-../-拦截 (403)", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 6: 路径遍历 — URL 编码 ..%2f
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, headers = http_get(f"{su}/..%2f..%2f..%2fetc%2fpasswd")
|
||||||
|
assert_eq(status, 403, "路径遍历-URL编码-拦截 (403)", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 7: 不存在的文件 → 404
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, headers = http_get(f"{su}/this-file-does-not-exist-12345.html")
|
||||||
|
assert_eq(status, 404, "不存在文件-404", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 8: 健康检查
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, headers = http_get(f"{su}/health")
|
||||||
|
assert_eq(status, 200, "健康检查 (200)", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 9: /api/filelist — 目录列表
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, headers = http_get(f"{su}/api/filelist")
|
||||||
|
assert_eq(status, 200, "文件列表-API (200)", c)
|
||||||
|
data = try_parse_json(body)
|
||||||
|
assert_true(
|
||||||
|
data is not None and data.get("success") is True,
|
||||||
|
"文件列表-返回success", c,
|
||||||
|
f"body={body[:200]}",
|
||||||
|
)
|
||||||
|
if data and data.get("success"):
|
||||||
|
items = data.get("items", [])
|
||||||
|
assert_true(
|
||||||
|
isinstance(items, list),
|
||||||
|
"文件列表-返回items数组", c,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 10: /api/filelist — 路径遍历
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, headers = http_get(f"{su}/api/filelist?path=../../../etc")
|
||||||
|
assert_true(
|
||||||
|
status in (403, 400),
|
||||||
|
"文件列表-路径遍历-拒绝", c,
|
||||||
|
f"status={status}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 11: DNFS 代理接口 — OPTIONS 预检
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, headers = http_get(f"{su}/add_note") # OPTIONS 需要特殊处理
|
||||||
|
# 简单的 GET 代理转发测试
|
||||||
|
assert_true(
|
||||||
|
status in (200, 400, 401, 405),
|
||||||
|
"DNFS代理-add_note可达", c,
|
||||||
|
f"status={status}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 汇总
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
print(f"\n static 测试完成: {c.summary()}")
|
||||||
|
return c.passed, c.failed, c.skipped
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
"""
|
||||||
|
waf 黑盒用户模拟测试 — 正常请求通行 vs 攻击载荷拦截。
|
||||||
|
|
||||||
|
用户旅程:
|
||||||
|
1. 正常请求 → 通过
|
||||||
|
2. SQL 注入载荷 → 403 blocked by WAF
|
||||||
|
3. XSS 跨站脚本载荷 → 403
|
||||||
|
4. 命令注入载荷 → 403
|
||||||
|
5. 路径遍历载荷 → 403
|
||||||
|
6. 扫描器 User-Agent → 403
|
||||||
|
|
||||||
|
注意: WAF 监听 443 端口 (TLS),测试时需注意证书验证。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ssl
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
|
||||||
|
from helpers import (
|
||||||
|
http_get,
|
||||||
|
assert_eq, assert_status, assert_true, assert_contains,
|
||||||
|
skip, Counter, check_service,
|
||||||
|
)
|
||||||
|
from config import get_url
|
||||||
|
|
||||||
|
WAF_URL = "https://127.0.0.1:443" # WAF 监听 443 需要 HTTPS
|
||||||
|
|
||||||
|
|
||||||
|
def _waf_get(base_url: str, path: str, headers: dict = None):
|
||||||
|
"""通过 WAF 发送 HTTPS GET 请求(跳过证书验证)。"""
|
||||||
|
ctx = ssl._create_unverified_context()
|
||||||
|
url = f"{base_url}{path}"
|
||||||
|
hdrs = headers or {}
|
||||||
|
req = urllib.request.Request(url, headers=hdrs, method="GET")
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=10.0, context=ctx) as resp:
|
||||||
|
return resp.status, resp.read().decode("utf-8", errors="replace"), dict(resp.headers)
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
body = e.read().decode("utf-8", errors="replace") if e.fp else ""
|
||||||
|
return e.code, body, dict(e.headers) if e.headers else {}
|
||||||
|
except Exception as e:
|
||||||
|
return 0, str(e), {}
|
||||||
|
|
||||||
|
|
||||||
|
def run(url: str = None):
|
||||||
|
"""waf 用户模拟测试入口。返回 (passed, failed, skipped)。"""
|
||||||
|
wu = url or WAF_URL
|
||||||
|
c = Counter()
|
||||||
|
|
||||||
|
print(f"\n--- waf 安全防护流程 ---\n")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 0. 检查服务可用性
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
reachable, detail = check_service(wu)
|
||||||
|
if not reachable:
|
||||||
|
skip("全部测试", f"WAF 不可达: {detail}", c)
|
||||||
|
return c.passed, c.failed, c.skipped
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 1: 正常请求应通过
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, headers = _waf_get(wu, "/")
|
||||||
|
assert_true(
|
||||||
|
status != 403,
|
||||||
|
"正常请求-通过", c,
|
||||||
|
f"status={status}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 2: SQL 注入 — UNION SELECT
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, headers = _waf_get(wu, "/?id=1 UNION SELECT * FROM users")
|
||||||
|
assert_eq(status, 403, "SQLi-UNION SELECT-拦截 (403)", c)
|
||||||
|
assert_contains(body, "blocked by WAF", "SQLi-响应体 blocked", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 3: SQL 注入 — SQL 注释符
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, headers = _waf_get(wu, "/?user=admin'--")
|
||||||
|
assert_eq(status, 403, "SQLi-注释符-拦截 (403)", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 4: XSS — script 标签
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, headers = _waf_get(wu, "/?q=<script>alert(1)</script>")
|
||||||
|
assert_eq(status, 403, "XSS-script标签-拦截 (403)", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 5: XSS — onerror 事件
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, headers = _waf_get(wu, "/?q=<img onerror=alert(1)>")
|
||||||
|
assert_eq(status, 403, "XSS-onerror-拦截 (403)", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 6: 命令注入 — cat /etc/passwd
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, headers = _waf_get(wu, "/?cmd=; cat /etc/passwd")
|
||||||
|
assert_eq(status, 403, "命令注入-cat-拦截 (403)", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 7: 命令注入 — id 命令
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, headers = _waf_get(wu, "/?x=| id")
|
||||||
|
assert_eq(status, 403, "命令注入-id-拦截 (403)", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 8: 路径遍历
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, headers = _waf_get(wu, "/?file=../../../etc/passwd")
|
||||||
|
assert_eq(status, 403, "路径遍历-拦截 (403)", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 9: 路径遍历 — URL 编码
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, headers = _waf_get(wu, "/?file=..%2f..%2f..%2fetc%2fpasswd")
|
||||||
|
assert_eq(status, 403, "路径遍历-URL编码-拦截 (403)", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 10: 扫描器 User-Agent
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, headers = _waf_get(wu, "/", headers={"User-Agent": "sqlmap/1.0"})
|
||||||
|
assert_eq(status, 403, "扫描器UA-sqlmap-拦截 (403)", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 场景 11: 扫描器 User-Agent — Nikto
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
status, body, headers = _waf_get(wu, "/", headers={"User-Agent": "Nikto"})
|
||||||
|
assert_eq(status, 403, "扫描器UA-nikto-拦截 (403)", c)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 汇总
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
print(f"\n waf 测试完成: {c.summary()}")
|
||||||
|
return c.passed, c.failed, c.skipped
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
Reference in new issue
Block a user