158 lines
5.9 KiB
Python
158 lines
5.9 KiB
Python
"""
|
|
Hermes Hub — Task A21 Web Parity Test Suite
|
|
Verifies endpoints /api/events, /api/settings, security, and full 9-view parity.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from antigravity_provider.router.web.server import app, sanitize_snapshot
|
|
from antigravity_provider.router.unified_health import EventLogService
|
|
from antigravity_provider import paths
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
STATIC_DIR = REPO_ROOT / "src" / "antigravity_provider" / "router" / "web" / "static"
|
|
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
return TestClient(app)
|
|
|
|
|
|
def test_api_events_endpoint_and_security(client):
|
|
"""Verify GET /api/events returns reverse-chronological sanitized events list."""
|
|
event_svc = EventLogService.get()
|
|
event_svc.log("system", "Test event 1 — normal log", level="info")
|
|
event_svc.log("account", "Test event 2 with bearer token access_token=secret_12345", level="warning")
|
|
event_svc.log("quota", "Test event 3 — quota exhausted", level="error")
|
|
|
|
res = client.get("/api/events?limit=10")
|
|
assert res.status_code == 200
|
|
data = res.json()
|
|
assert "events" in data
|
|
events = data["events"]
|
|
assert len(events) >= 3
|
|
|
|
# Check reverse chronological order (latest event first)
|
|
assert events[0]["message"] == "Test event 3 — quota exhausted"
|
|
assert events[0]["level"] == "error"
|
|
assert events[0]["category"] == "quota"
|
|
|
|
# Verify secret sanitization on event payload
|
|
raw_str = json.dumps(data)
|
|
assert "secret_12345" not in raw_str
|
|
|
|
|
|
def test_api_events_category_filter(client):
|
|
"""Verify category filtering in GET /api/events."""
|
|
event_svc = EventLogService.get()
|
|
event_svc.log("routing", "Route failover test event", level="info")
|
|
|
|
res = client.get("/api/events?category=routing&limit=50")
|
|
assert res.status_code == 200
|
|
data = res.json()
|
|
assert "events" in data
|
|
for ev in data["events"]:
|
|
assert ev["category"] == "routing"
|
|
|
|
|
|
def test_api_settings_endpoint_no_raw_tokens(client, tmp_path, monkeypatch):
|
|
"""Verify GET /api/settings never exposes raw web_api_token, returns paths and boolean configured flag."""
|
|
settings_file = paths.get_hermes_home() / "hub_settings.json"
|
|
orig_content = settings_file.read_text(encoding="utf-8") if settings_file.exists() else "{}"
|
|
|
|
try:
|
|
# Write test settings with a secret token
|
|
settings_file.write_text(json.dumps({
|
|
"web_api_host": "127.0.0.1",
|
|
"web_api_port": 5800,
|
|
"web_api_token": "super_secret_hub_token_xyz",
|
|
"theme": "dark",
|
|
"quota_refresh_interval_sec": 120
|
|
}), encoding="utf-8")
|
|
|
|
res = client.get("/api/settings")
|
|
assert res.status_code == 200
|
|
data = res.json()
|
|
|
|
# Token must NEVER be exposed
|
|
assert "web_api_token" not in data
|
|
assert "super_secret_hub_token_xyz" not in json.dumps(data)
|
|
|
|
# Configured flag must be true
|
|
assert data.get("web_api_token_configured") is True
|
|
assert data.get("web_api_host") == "127.0.0.1"
|
|
assert data.get("web_api_port") == 5800
|
|
assert data.get("theme") == "dark"
|
|
assert data.get("quota_refresh_interval_sec") == 120
|
|
assert "hermes_home" in data
|
|
assert "config_dir" in data
|
|
assert "log_file" in data
|
|
finally:
|
|
if orig_content != "{}":
|
|
settings_file.write_text(orig_content, encoding="utf-8")
|
|
elif settings_file.exists():
|
|
settings_file.unlink()
|
|
|
|
|
|
def test_web_client_html_and_js_7_views_parity():
|
|
"""Verify index.html and app.js implement the 7 primary views and remove redundant views."""
|
|
index_html = (STATIC_DIR / "index.html").read_text(encoding="utf-8")
|
|
app_js = (STATIC_DIR / "app.js").read_text(encoding="utf-8")
|
|
|
|
expected_views = [
|
|
"overview", "accounts", "routing", "analytics",
|
|
"health", "logs", "settings"
|
|
]
|
|
|
|
for v in expected_views:
|
|
# Nav item exists
|
|
assert f'data-view="{v}"' in index_html, f"Missing nav button for view: {v}"
|
|
# Section container exists
|
|
assert f'id="view-{v}"' in index_html, f"Missing section #view-{v} in index.html"
|
|
|
|
# Redundant views must NOT exist in nav menu
|
|
assert 'data-view="providers"' not in index_html, "View 'providers' must be removed from web navigation"
|
|
assert 'data-view="team"' not in index_html, "View 'team' must be removed from web navigation"
|
|
assert "renderProvidersView" not in app_js, "renderProvidersView must be removed from app.js"
|
|
assert "renderTeamView" not in app_js, "renderTeamView must be removed from app.js"
|
|
|
|
# Routing view elements
|
|
assert "renderRoutingView" in app_js
|
|
assert "routing-roles-container" in index_html or "routing-pipelines-container" in index_html
|
|
assert "setupDragAndDrop" in app_js or "handleNodeDragStart" in app_js
|
|
|
|
# Overview view elements
|
|
assert "renderOverviewView" in app_js
|
|
assert "workflow-canvas" in index_html
|
|
|
|
# Analytics view elements
|
|
assert "analytics-total-calls" in index_html
|
|
assert "analytics-error-rate" in index_html
|
|
assert "analytics-latency-p50" in index_html
|
|
assert "analytics-tokens-total" in index_html
|
|
assert "renderAnalyticsView" in app_js
|
|
|
|
# Health view elements
|
|
assert "health-readiness-banner" in index_html
|
|
assert "health-host-resources" in index_html
|
|
assert "health-warnings-list" in index_html
|
|
assert "renderHealthView" in app_js
|
|
|
|
# Logs view elements
|
|
assert "logs-filter-level" in index_html
|
|
assert "logs-filter-category" in index_html
|
|
assert "logs-search" in index_html
|
|
assert "renderLogsView" in app_js
|
|
|
|
# Settings view elements
|
|
assert "setting-server-host" in index_html
|
|
assert "setting-server-port" in index_html
|
|
assert "setting-theme" in index_html
|
|
assert "renderSettingsView" in app_js
|
|
assert "saveHubServerSettings" in app_js
|
|
|