Files
geMoldInsight/tests/test_html_report_router.py
T
2026-09-18 18:21:01 +08:00

193 lines
6.7 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.
"""D11:/html/{filename} 报告代理路由。
解析链:RustFS 报告键(新产物裸文件)→ HTMLFile 遗留记录(JSON 包装)
→ 节点本地 html_output(存量兜底)→ 404;路径穿越一律 404。
"""
import json
from types import SimpleNamespace
import pytest
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from moldinsight.api import html_report_router
from moldinsight.storage.rustfs_storage import rustfs_manager
@pytest.fixture
def app():
application = FastAPI()
application.include_router(html_report_router.router)
return application
@pytest.fixture
async def client(app):
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as c:
yield c
@pytest.fixture(autouse=True)
def _isolate_external_sources(monkeypatch, tmp_path):
"""默认隔离:RustFS 未连接、本地兜底目录指向空临时目录,由各测试按需打开。"""
monkeypatch.setattr(rustfs_manager, "is_connected", False)
monkeypatch.setattr(html_report_router, "LOCAL_HTML_DIR", tmp_path / "html_output")
# ── 链路 1:RustFS 报告键(新产物) ──────────────────────────────
async def test_report_served_from_rustfs_report_key(client, monkeypatch):
async def fake_download_report(filename):
assert filename == "mold_demo_20260918.html"
return b"<html>fresh</html>"
monkeypatch.setattr(rustfs_manager, "is_connected", True)
monkeypatch.setattr(rustfs_manager, "download_report_artifact", fake_download_report)
resp = await client.get("/html/mold_demo_20260918.html")
assert resp.status_code == 200
assert resp.content == b"<html>fresh</html>"
assert "text/html" in resp.headers["content-type"]
# ── 链路 2:HTMLFile 遗留记录(html/{hash}.json JSON 包装) ──────
class _FakeScalars:
def __init__(self, record):
self._record = record
def first(self):
return self._record
class _FakeResult:
def __init__(self, record):
self._record = record
def scalars(self):
return _FakeScalars(self._record)
class _FakeSession:
def __init__(self, record):
self._record = record
async def __aenter__(self):
return self
async def __aexit__(self, *exc):
return False
async def execute(self, stmt):
return _FakeResult(self._record)
class _FakeDBManager:
def __init__(self, record):
self._record = record
def session(self):
return _FakeSession(self._record)
async def test_falls_back_to_legacy_json_wrapper(client, monkeypatch):
async def fake_download_report(filename):
raise RuntimeError("报告键未命中")
async def fake_download_file(file_type, object_key):
assert file_type == "html_files"
assert object_key == "html/abc123.json"
return json.dumps(
{"content": "<html>legacy</html>", "filename": "mold_old.html"}
).encode("utf-8")
record = SimpleNamespace(object_key="html/abc123.json", filename="mold_old.html")
monkeypatch.setattr(rustfs_manager, "is_connected", True)
monkeypatch.setattr(rustfs_manager, "download_report_artifact", fake_download_report)
monkeypatch.setattr(rustfs_manager, "download_file", fake_download_file)
monkeypatch.setattr(html_report_router, "db_manager", _FakeDBManager(record))
resp = await client.get("/html/mold_old.html")
assert resp.status_code == 200
assert resp.content == b"<html>legacy</html>"
async def test_legacy_record_with_report_key_returns_raw(client, monkeypatch):
"""新格式记录(键在报告前缀下)必须按裸文件返回,不能当 JSON 包装解析。"""
async def fake_download_report(filename):
raise RuntimeError("报告键直取瞬时失败")
async def fake_download_file(file_type, object_key):
assert object_key == "html/reports/mold_new.html"
return b"<html>raw-by-report-key</html>"
record = SimpleNamespace(object_key="html/reports/mold_new.html", filename="mold_new.html")
monkeypatch.setattr(rustfs_manager, "is_connected", True)
monkeypatch.setattr(rustfs_manager, "download_report_artifact", fake_download_report)
monkeypatch.setattr(rustfs_manager, "download_file", fake_download_file)
monkeypatch.setattr(html_report_router, "db_manager", _FakeDBManager(record))
resp = await client.get("/html/mold_new.html")
assert resp.status_code == 200
assert resp.content == b"<html>raw-by-report-key</html>"
# ── 链路 3:节点本地 html_output(存量兜底) ─────────────────────
async def test_falls_back_to_local_dir(client, tmp_path):
local_dir = tmp_path / "html_output"
local_dir.mkdir()
(local_dir / "mold_local.html").write_text("<html>local</html>", encoding="utf-8")
resp = await client.get("/html/mold_local.html")
assert resp.status_code == 200
assert resp.content == b"<html>local</html>"
async def test_json_media_type_from_extension(client, tmp_path):
local_dir = tmp_path / "html_output"
local_dir.mkdir()
(local_dir / "mold_a_data.json").write_bytes(b"{}")
resp = await client.get("/html/mold_a_data.json")
assert resp.status_code == 200
assert "application/json" in resp.headers["content-type"]
# ── D3 收敛:单点聚合注册 ───────────────────────────────────────
def test_register_moldinsight_routers_mounts_api_and_html():
"""register_moldinsight_routers 一次挂载 /api 聚合路由 + 根路径 /html 报告代理
(入口侧不再重复 include_html_report)。"""
from moldinsight.api import register_moldinsight_routers
application = FastAPI()
register_moldinsight_routers(application)
paths = {route.path for route in application.routes}
assert "/api/upload" in paths
# Starlette route.path 保留 :path 转换器(OpenAPI 路径才显示 /html/{filename})
assert any(p.startswith("/html/{") for p in paths)
# ── 未命中与防护 ────────────────────────────────────────────────
async def test_404_when_all_sources_miss(client):
resp = await client.get("/html/missing.html")
assert resp.status_code == 404
async def test_path_traversal_rejected(client):
resp = await client.get("/html/%2e%2e/secret.txt")
assert resp.status_code == 404
async def test_hidden_filename_rejected(client):
resp = await client.get("/html/%2e%2eetc%2fpasswd")
assert resp.status_code == 404