aps-agent/tests/golden/test_audit_events_ingest.py

155 lines
6.4 KiB
Python
Raw Permalink 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.

# ============================================================
# POST /api/gov/audit/events 审计事件上报端点(矩阵 103 收口,round-45 HH)
# H1 web 登录态 → 事件写入审计链(actor=登录用户)
# H2 web 未登录 → 401(端点内显式要求登录身份,与其他 gov 端点语义一致)
# H3 desktop nonce 门禁:缺失/错误 nonce → 403;正确 nonce(无 cookie)→ 200 落链
# H4 idempotencyKey 幂等:重复上报不重复落链
# H5 非法载荷 → 4xx 明确(422 校验失败 / 403 门禁)
# ============================================================
from __future__ import annotations
import pytest
from fastapi.testclient import TestClient
from server.sidecar import SidecarIdentityApp
from tests.auth_provider import install_test_auth
NONCE = "a" * 64
class _Store:
"""最小世界状态替身(与 test_audit_anchor_api.py 同构)。"""
def __init__(self) -> None:
self.tenant_uuid = "platform"
self.world_key = "default"
self.data: dict = {"auditEvents": []}
def next_id(self, _kind: str) -> int:
return len(self.data.get("auditEvents", [])) + 1
def save(self) -> None:
pass
@pytest.fixture()
def web_client(monkeypatch):
"""web 模式:无 APS_SIDECAR_NONCE(无 nonce 门禁)+ 测试登录身份。"""
import server.gateway.app as gateway_module
install_test_auth(monkeypatch, "tenant-audit-events-0000000000000000001")
store = _Store()
monkeypatch.setattr(gateway_module, "get_store", lambda: store)
monkeypatch.delenv("APS_SIDECAR_NONCE", raising=False)
c = TestClient(gateway_module.create_app())
login = c.post("/api/auth/login", json={"username": "planner", "password": "test"})
assert login.status_code == 200, login.text
return c, store
@pytest.fixture()
def desktop_app(monkeypatch):
"""desktop 模式:SidecarIdentityApp nonce 门禁 + APS_SIDECAR_NONCE 环境变量。"""
import server.gateway.app as gateway_module
install_test_auth(monkeypatch, "tenant-audit-events-0000000000000000002")
store = _Store()
monkeypatch.setattr(gateway_module, "get_store", lambda: store)
monkeypatch.setenv("APS_SIDECAR_NONCE", NONCE)
inner = gateway_module.create_app()
return TestClient(SidecarIdentityApp(inner, NONCE)), store
def _event(**overrides):
payload = {
"category": "UPGRADE",
"action": "upgrade.succeeded",
"power": "P0",
"actor": "desktop-updater",
"target": {"type": "UPGRADE"},
"rationale": {"fromVersion": "0.1.0", "toVersion": "0.2.0"},
"timestamp": "2026-08-02T10:00:00+08:00",
"result": "SUCCESS",
"idempotencyKey": "evt-1",
}
payload.update(overrides)
return payload
# ---------------- H1:web 登录态写入审计 ----------------
def test_web_logged_in_writes_audit_event(web_client):
c, store = web_client
r = c.post("/api/gov/audit/events", json=_event())
assert r.status_code == 200, r.text
body = r.json()
assert body["ok"] is True and body["duplicate"] is False
assert body["event"]["hash"] # 链式哈希已生成
# 事件真实落链(gov/audit 可读),web 模式 actor 以登录身份为准
chain = c.get("/api/gov/audit").json()
assert [e["action"] for e in chain["events"]] == ["upgrade.succeeded"]
assert chain["events"][0]["actor"] == "planner"
assert len(store.data["auditEvents"]) == 1
# ---------------- H2:web 未登录 401 ----------------
def test_web_anonymous_gets_401(web_client):
c, _ = web_client
c.cookies.clear()
r = c.post("/api/gov/audit/events", json=_event())
assert r.status_code == 401, r.text
# ---------------- H5:非法载荷 4xx 明确 ----------------
def test_web_invalid_payload_gets_4xx(web_client):
c, _ = web_client
r = c.post("/api/gov/audit/events", json=_event(power="P9")) # power 越界
assert r.status_code == 400, r.text
assert r.json()["detail"]["code"] == "INVALID_POWER"
r2 = c.post("/api/gov/audit/events", json={"action": ""}) # action 为空
assert r2.status_code == 400, r2.text
assert r2.json()["detail"]["code"] == "INVALID_ACTION"
r3 = c.post("/api/gov/audit/events", data="not-json") # 非 JSON 请求体
assert r3.status_code == 400, r3.text
assert r3.json()["detail"]["code"] == "INVALID_JSON"
# ---------------- H3:desktop nonce 门禁 ----------------
def test_desktop_nonce_gate_missing_nonce_403(desktop_app):
c, _ = desktop_app
r = c.post("/api/gov/audit/events", json=_event())
assert r.status_code == 403, r.text # 无 nonce → 门禁 403
def test_desktop_nonce_gate_wrong_nonce_403(desktop_app):
c, _ = desktop_app
r = c.post("/api/gov/audit/events", json=_event(),
headers={"x-aps-sidecar-nonce": "b" * 64})
assert r.status_code == 403, r.text # 错误 nonce → 403
def test_desktop_updater_with_nonce_writes_event(desktop_app):
"""updater 场景:无 cookie、仅带 nonce → 200 且事件落链(nonce 门禁即身份)。"""
c, store = desktop_app
r = c.post("/api/gov/audit/events", json=_event(),
headers={"x-aps-sidecar-nonce": NONCE})
assert r.status_code == 200, r.text
assert store.data["auditEvents"][0]["actor"] == "desktop-updater"
assert store.data["auditEvents"][0]["rationale"]["idempotencyKey"] == "evt-1"
# 链完整性:新事件可被 verify_audit_chain 校验(哈希链无断点)
from server.agent_core.registry import verify_audit_chain
assert verify_audit_chain(store.data["auditEvents"])["ok"] is True
# ---------------- H4:idempotencyKey 幂等 ----------------
def test_idempotency_key_dedupes_repeated_report(web_client):
c, store = web_client
first = c.post("/api/gov/audit/events", json=_event())
assert first.status_code == 200 and first.json()["duplicate"] is False
event_id = first.json()["eventId"]
second = c.post("/api/gov/audit/events", json=_event()) # 同 idempotencyKey
assert second.status_code == 200, second.text
assert second.json()["duplicate"] is True
assert second.json()["eventId"] == event_id
assert len(store.data["auditEvents"]) == 1 # 未重复落链
third = c.post("/api/gov/audit/events", json=_event(idempotencyKey="evt-2"))
assert third.status_code == 200 and third.json()["duplicate"] is False
assert len(store.data["auditEvents"]) == 2