Files
HarborForge.Backend.Test/tests/test_monitor.py
zhi ed21b73a43 Add monitor API tests (B8)
- test_monitor.py: 12 test cases covering:
  - API key generation (success, 404, admin-only)
  - heartbeat-v2 endpoint (valid key, invalid key, missing key)
  - API key revocation and validation
  - ServerState data persistence (agents_json, cpu_pct, etc.)
  - Disabled server rejection
- conftest.py: import app.models.monitor for test database setup

All tests passing (pytest -v).
2026-03-19 19:39:15 +00:00

323 lines
10 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.
"""Tests for monitor API — API Key and heartbeat-v2 endpoints."""
import json
import pytest
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture()
def make_server(db):
"""Factory to create a MonitoredServer row."""
from app.models.monitor import MonitoredServer
_counter = [0]
def _make(identifier=None, display_name=None, is_enabled=True, api_key=None):
_counter[0] += 1
n = _counter[0]
s = MonitoredServer(
identifier=identifier or f"test-server-{n}",
display_name=display_name or f"Test Server {n}",
is_enabled=is_enabled,
api_key=api_key,
)
db.add(s)
db.commit()
db.refresh(s)
return s
return _make
@pytest.fixture()
def admin_auth(make_user, auth_header):
"""Create admin user and return auth header."""
user = make_user(is_admin=True)
return auth_header(user), user
# ---------------------------------------------------------------------------
# API Key Generation Tests (POST /admin/servers/{id}/api-key)
# ---------------------------------------------------------------------------
def test_generate_api_key_success(client, db, make_server, admin_auth):
"""B8-1: api-key 生成成功返回 200 + api_key"""
headers, _ = admin_auth
server = make_server()
response = client.post(f"/monitor/admin/servers/{server.id}/api-key", headers=headers)
assert response.status_code == 200
data = response.json()
assert "api_key" in data
assert data["server_id"] == server.id
assert len(data["api_key"]) > 20 # token_urlsafe(32) produces ~43 chars
# Verify key is stored in DB
db.refresh(server)
assert server.api_key == data["api_key"]
def test_generate_api_key_not_found(client, db, admin_auth):
"""B8-2: api-key 生成对不存在 server 返回 404"""
headers, _ = admin_auth
response = client.post("/monitor/admin/servers/99999/api-key", headers=headers)
assert response.status_code == 404
assert "not found" in response.json()["detail"].lower()
# ---------------------------------------------------------------------------
# Heartbeat v2 Tests (POST /server/heartbeat-v2)
# ---------------------------------------------------------------------------
def test_heartbeat_v2_valid_key(client, db, make_server):
"""B8-3: heartbeat-v2 带有效 key 返回 200"""
api_key = "test-api-key-valid-123"
server = make_server(api_key=api_key)
payload = {
"identifier": server.identifier,
"openclaw_version": "1.0.0",
"agents": [{"id": "agent1", "status": "idle"}],
"cpu_pct": 45.5,
"mem_pct": 60.0,
"disk_pct": 30.0,
"swap_pct": 10.0,
"load_avg": [0.5, 0.6, 0.7],
"uptime_seconds": 3600,
}
response = client.post(
"/monitor/server/heartbeat-v2",
json=payload,
headers={"X-API-Key": api_key},
)
assert response.status_code == 200
data = response.json()
assert data["ok"] is True
assert data["server_id"] == server.id
assert data["identifier"] == server.identifier
def test_heartbeat_v2_invalid_key(client, db, make_server):
"""B8-4: heartbeat-v2 带无效 key 返回 401"""
server = make_server(api_key="valid-key")
payload = {
"identifier": server.identifier,
"cpu_pct": 45.5,
}
response = client.post(
"/monitor/server/heartbeat-v2",
json=payload,
headers={"X-API-Key": "invalid-key"},
)
assert response.status_code == 401
assert "invalid" in response.json()["detail"].lower() or "api key" in response.json()["detail"].lower()
def test_heartbeat_v2_missing_key(client, db, make_server):
"""B8-5: heartbeat-v2 不带 key 返回 401"""
server = make_server(api_key="valid-key")
payload = {"identifier": server.identifier}
response = client.post("/monitor/server/heartbeat-v2", json=payload)
# FastAPI will reject missing required header with 422 (validation error)
# or we may expect 401 depending on implementation
assert response.status_code in [401, 422]
# ---------------------------------------------------------------------------
# Revoke API Key Tests (DELETE /admin/servers/{id}/api-key)
# ---------------------------------------------------------------------------
def test_revoke_api_key_makes_key_invalid(client, db, make_server, admin_auth):
"""B8-6: 测试 revoke 后 key 失效"""
headers, _ = admin_auth
api_key = "test-key-to-revoke"
server = make_server(api_key=api_key)
# First, verify key works
payload = {"identifier": server.identifier, "cpu_pct": 50.0}
response1 = client.post(
"/monitor/server/heartbeat-v2",
json=payload,
headers={"X-API-Key": api_key},
)
assert response1.status_code == 200
# Revoke the key
response2 = client.delete(f"/monitor/admin/servers/{server.id}/api-key", headers=headers)
assert response2.status_code == 204
# Verify key no longer works
response3 = client.post(
"/monitor/server/heartbeat-v2",
json=payload,
headers={"X-API-Key": api_key},
)
assert response3.status_code == 401
# Verify DB
db.refresh(server)
assert server.api_key is None
def test_revoke_api_key_not_found(client, db, admin_auth):
"""Revoke API key for non-existent server returns 404"""
headers, _ = admin_auth
response = client.delete("/monitor/admin/servers/99999/api-key", headers=headers)
assert response.status_code == 404
# ---------------------------------------------------------------------------
# Data Storage Tests (ServerState)
# ---------------------------------------------------------------------------
def test_heartbeat_v2_stores_data_correctly(client, db, make_server):
"""B8-7: 测试数据是否正确写入 ServerStateagents_json, cpu_pct, etc."""
from app.models.monitor import ServerState
api_key = "test-api-key-storage"
server = make_server(api_key=api_key)
agents = [{"id": "agent1", "name": "TestAgent", "status": "busy"}]
payload = {
"identifier": server.identifier,
"openclaw_version": "2.5.1",
"agents": agents,
"cpu_pct": 75.5,
"mem_pct": 82.3,
"disk_pct": 45.0,
"swap_pct": 5.5,
"load_avg": [1.2, 1.5, 1.8],
"uptime_seconds": 7200,
}
response = client.post(
"/monitor/server/heartbeat-v2",
json=payload,
headers={"X-API-Key": api_key},
)
assert response.status_code == 200
# Verify ServerState in DB
state = db.query(ServerState).filter(ServerState.server_id == server.id).first()
assert state is not None
assert state.openclaw_version == "2.5.1"
assert state.cpu_pct == 75.5
assert state.mem_pct == 82.3
assert state.disk_pct == 45.0
assert state.swap_pct == 5.5
assert state.last_seen_at is not None
# Verify agents_json
stored_agents = json.loads(state.agents_json)
assert stored_agents == agents
assert stored_agents[0]["id"] == "agent1"
assert stored_agents[0]["status"] == "busy"
def test_heartbeat_v2_updates_existing_state(client, db, make_server):
"""Test that subsequent heartbeats update the existing ServerState"""
from app.models.monitor import ServerState
api_key = "test-api-key-update"
server = make_server(api_key=api_key)
# First heartbeat
response1 = client.post(
"/monitor/server/heartbeat-v2",
json={
"identifier": server.identifier,
"cpu_pct": 30.0,
"mem_pct": 40.0,
"agents": [{"id": "a1"}],
},
headers={"X-API-Key": api_key},
)
assert response1.status_code == 200
state1 = db.query(ServerState).filter(ServerState.server_id == server.id).first()
first_seen = state1.last_seen_at
# Second heartbeat with different data
response2 = client.post(
"/monitor/server/heartbeat-v2",
json={
"identifier": server.identifier,
"cpu_pct": 60.0,
"mem_pct": 70.0,
"agents": [{"id": "a1"}, {"id": "a2"}],
},
headers={"X-API-Key": api_key},
)
assert response2.status_code == 200
# Verify update
db.refresh(state1)
assert state1.cpu_pct == 60.0
assert state1.mem_pct == 70.0
assert len(json.loads(state1.agents_json)) == 2
# last_seen_at should be updated (newer timestamp)
assert state1.last_seen_at >= first_seen
# ---------------------------------------------------------------------------
# Authorization Tests
# ---------------------------------------------------------------------------
def test_generate_api_key_requires_admin(client, make_user, auth_header, make_server):
"""Only admin can generate API keys"""
non_admin = make_user(is_admin=False)
server = make_server()
response = client.post(
f"/monitor/admin/servers/{server.id}/api-key",
headers=auth_header(non_admin),
)
assert response.status_code == 403
def test_revoke_api_key_requires_admin(client, make_user, auth_header, make_server):
"""Only admin can revoke API keys"""
non_admin = make_user(is_admin=False)
server = make_server(api_key="some-key")
response = client.delete(
f"/monitor/admin/servers/{server.id}/api-key",
headers=auth_header(non_admin),
)
assert response.status_code == 403
# ---------------------------------------------------------------------------
# Disabled Server Tests
# ---------------------------------------------------------------------------
def test_heartbeat_v2_disabled_server(client, db, make_server):
"""Heartbeat-v2 should reject disabled servers even with valid key"""
api_key = "test-key-disabled"
server = make_server(api_key=api_key, is_enabled=False)
response = client.post(
"/monitor/server/heartbeat-v2",
json={"identifier": server.identifier, "cpu_pct": 50.0},
headers={"X-API-Key": api_key},
)
assert response.status_code == 401 # Should be treated as invalid key