feat(stats): buffer visits in memory and expose POST /api/visit + /api/stats

This commit is contained in:
kairee committed 2026-11-01 15:34:53 +08:00
1 parent cdd11823b9
commit 3c16caac1a
8 files changed
+905 -5

No files matched your search

+87 -1
View File
@@ -189,6 +189,87 @@ def test_readonly_routes_and_static():
running.close()
def test_visit_endpoint_and_stats():
with tempfile.TemporaryDirectory() as tmp:
running = RunningServer(tmp)
try:
ua = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/155.0"}
# 上报:可信对端 127.0.0.1 + X-Real-IP → 记录真实 IP 与归一化路由
status, _, payload = running.json(
"/api/visit", method="POST",
headers={**ua, "Content-Type": "application/json", "X-Real-IP": "203.0.113.7"},
body=json.dumps({"path": "/video/123"}),
)
assert status == 200 and payload["data"]["recorded"] is True
assert payload["data"]["route"] == "/video/:aid"
status, _, payload = running.json(
"/api/visit", method="POST",
headers={**ua, "Content-Type": "application/json", "X-Real-IP": "203.0.113.7"},
body=json.dumps({"path": "/videos"}),
)
assert status == 200 and payload["data"]["recorded"] is True
collector = running.server.context.visits
assert collector.pending_keys == 2
# 机器人 UA 被过滤,不计数
status, _, payload = running.json(
"/api/visit", method="POST",
headers={"User-Agent": "Googlebot/2.1", "Content-Type": "application/json"},
body=json.dumps({"path": "/home"}),
)
assert status == 200 and payload["data"]["recorded"] is False
assert payload["data"]["reason"] == "bot"
assert collector.pending_keys == 2
# 超大请求体被拒绝
status, _, _ = running.json(
"/api/visit", method="POST",
headers={**ua, "Content-Type": "application/json"},
body=json.dumps({"path": "/" + "x" * 4000}),
)
assert status == 400
# GET 埋点端点 → 405;其他业务 POST 仍 405
status, _, _ = running.request("/api/visit")
assert status == 405
status, _, _ = running.request("/api/videos/101", method="POST")
assert status == 405
# 未 flush 前库里没有行,但 /api/stats 合并内存缓冲可见
assert running.db.read(
lambda c: c.execute("SELECT COUNT(*) FROM visit").fetchone()[0]
) == 0
status, _, payload = running.json("/api/stats?days=7")
assert status == 200
data = payload["data"]
assert data["pending"] is True
assert data["today_pv"] == 2
assert data["today_uv"] == 1
pages = {page["path"]: page for page in data["top_pages"]}
assert pages["/video/:aid"]["pv"] == 1
assert pages["/videos"]["pv"] == 1
# 手动 flush 后落库,且 IP 正确
assert collector.flush() == 2
assert running.db.read(
lambda c: c.execute("SELECT COUNT(*) FROM visit").fetchone()[0]
) == 2
ips = running.db.read(
lambda c: [row[0] for row in c.execute("SELECT DISTINCT ip FROM visit")]
)
assert ips == ["203.0.113.7"]
status, _, payload = running.json("/api/stats?days=7")
data = payload["data"]
assert data["pending"] is False
assert data["today_pv"] == 2
assert data["range"]["end"] == "2026-10-31"
finally:
running.close()
def test_token_auth_and_login():
with tempfile.TemporaryDirectory() as tmp:
running = RunningServer(tmp, token="test-token")
@@ -210,6 +291,8 @@ def test_token_auth_and_login():
assert status == 401 and body["error"]["code"] == "unauthorized"
status, _, body = running.json("/api/system")
assert status == 401
status, _, body = running.json("/api/stats")
assert status == 401
status, headers, _ = running.request("/candidates")
assert status == 303 and headers["Location"] == "/login?next=/candidates"
status, headers, _ = running.request("/system")
@@ -246,6 +329,8 @@ def test_token_auth_and_login():
assert status == 200 and body["ok"] is True
status, _, body = running.json("/api/system", headers={"Cookie": cookie})
assert status == 200 and body["ok"] is True
status, _, body = running.json("/api/stats", headers={"Cookie": cookie})
assert status == 200 and body["ok"] is True
status, _, body = running.json(
"/api/candidates", headers={"Authorization": "Bearer test-token"}
)
@@ -299,9 +384,10 @@ def test_preview_and_loopback_guards():
def main_test():
test_readonly_routes_and_static()
test_visit_endpoint_and_stats()
test_token_auth_and_login()
test_preview_and_loopback_guards()
print("OK: Web 只读路由/鉴权/预览/安全边界测试全部通过")
print("OK: Web 只读路由/鉴权/预览/访问统计/安全边界测试全部通过")
if __name__ == "__main__":