Harden production workflows and agent admission
This commit is contained in:
@@ -1,21 +1,19 @@
|
||||
import pytest
|
||||
|
||||
from ._helpers import make_event, make_heartbeat
|
||||
from ._helpers import agent_headers, make_event, make_heartbeat, register_agent
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agents_cursor(app_client, auth_headers):
|
||||
async def test_agents_cursor(app_client, ui_auth_headers):
|
||||
for i in range(5):
|
||||
await app_client.post(
|
||||
"/api/v1/heartbeat", json=make_heartbeat(f"a-{i}"), headers=auth_headers
|
||||
)
|
||||
r1 = await app_client.get("/api/v1/agents?limit=2", headers=auth_headers)
|
||||
await register_agent(app_client, ui_auth_headers, f"a-{i}")
|
||||
r1 = await app_client.get("/api/v1/agents?limit=2", headers=ui_auth_headers)
|
||||
p1 = r1.json()
|
||||
assert len(p1["items"]) == 2
|
||||
assert p1["next_cursor"]
|
||||
|
||||
r2 = await app_client.get(
|
||||
f"/api/v1/agents?limit=2&cursor={p1['next_cursor']}", headers=auth_headers
|
||||
f"/api/v1/agents?limit=2&cursor={p1['next_cursor']}", headers=ui_auth_headers
|
||||
)
|
||||
p2 = r2.json()
|
||||
assert len(p2["items"]) == 2
|
||||
@@ -25,76 +23,204 @@ async def test_agents_cursor(app_client, auth_headers):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_checks_cursor(app_client, auth_headers):
|
||||
await app_client.post("/api/v1/heartbeat", json=make_heartbeat(), headers=auth_headers)
|
||||
async def test_agents_cursor_sorts_pending_before_accepted(app_client, ui_auth_headers):
|
||||
for i in range(3):
|
||||
await register_agent(app_client, ui_auth_headers, f"accepted-{i}")
|
||||
for i in range(2):
|
||||
await app_client.post(
|
||||
"/api/v1/heartbeat",
|
||||
json=make_heartbeat(f"pending-{i}"),
|
||||
headers=agent_headers(f"pending-{i}"),
|
||||
)
|
||||
|
||||
r1 = await app_client.get("/api/v1/agents?limit=3", headers=ui_auth_headers)
|
||||
p1 = r1.json()
|
||||
assert [i["admission_state"] for i in p1["items"][:2]] == ["pending", "pending"]
|
||||
assert p1["next_cursor"]
|
||||
|
||||
r2 = await app_client.get(
|
||||
f"/api/v1/agents?limit=3&cursor={p1['next_cursor']}", headers=ui_auth_headers
|
||||
)
|
||||
assert {i["agent_id"] for i in r2.json()["items"]}.isdisjoint(
|
||||
{i["agent_id"] for i in p1["items"]}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_checks_cursor(app_client, auth_headers, ui_auth_headers):
|
||||
auth_headers = await register_agent(app_client, ui_auth_headers)
|
||||
for i in range(4):
|
||||
ev = make_event(check_id=f"chk-{i}")
|
||||
await app_client.post(
|
||||
"/api/v1/events", json={"agent_id": "agent-1", "events": [ev]}, headers=auth_headers
|
||||
)
|
||||
r1 = await app_client.get("/api/v1/checks?limit=2", headers=auth_headers)
|
||||
r1 = await app_client.get("/api/v1/checks?limit=2", headers=ui_auth_headers)
|
||||
p1 = r1.json()
|
||||
assert len(p1["items"]) == 2
|
||||
assert p1["next_cursor"]
|
||||
r2 = await app_client.get(
|
||||
f"/api/v1/checks?limit=2&cursor={p1['next_cursor']}", headers=auth_headers
|
||||
f"/api/v1/checks?limit=2&cursor={p1['next_cursor']}", headers=ui_auth_headers
|
||||
)
|
||||
p2 = r2.json()
|
||||
assert len(p2["items"]) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_checks_rejects_invalid_agent_id_query(app_client, auth_headers):
|
||||
r = await app_client.get("/api/v1/checks?agent_id=bad%20id", headers=auth_headers)
|
||||
async def test_checks_rejects_invalid_agent_id_query(app_client, ui_auth_headers):
|
||||
r = await app_client.get("/api/v1/checks?agent_id=bad%20id", headers=ui_auth_headers)
|
||||
assert r.status_code == 400
|
||||
assert r.json()["error"]["code"] == "validation"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_events_query_rejects_invalid_id_queries(app_client, auth_headers):
|
||||
r = await app_client.get("/api/v1/events/query?agent_id=bad%20id", headers=auth_headers)
|
||||
async def test_events_query_rejects_invalid_id_queries(app_client, ui_auth_headers):
|
||||
r = await app_client.get("/api/v1/events/query?agent_id=bad%20id", headers=ui_auth_headers)
|
||||
assert r.status_code == 400
|
||||
assert r.json()["error"]["code"] == "validation"
|
||||
|
||||
r = await app_client.get("/api/v1/events/query?check_id=bad%20id", headers=auth_headers)
|
||||
r = await app_client.get("/api/v1/events/query?check_id=bad%20id", headers=ui_auth_headers)
|
||||
assert r.status_code == 400
|
||||
assert r.json()["error"]["code"] == "validation"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_incidents_cursor(app_client, auth_headers):
|
||||
await app_client.post("/api/v1/heartbeat", json=make_heartbeat(), headers=auth_headers)
|
||||
async def test_incidents_cursor(app_client, auth_headers, ui_auth_headers):
|
||||
auth_headers = await register_agent(app_client, ui_auth_headers)
|
||||
for i in range(3):
|
||||
ev = make_event(check_id=f"chk-{i}", status="critical", exit_code=2)
|
||||
await app_client.post(
|
||||
"/api/v1/events", json={"agent_id": "agent-1", "events": [ev]}, headers=auth_headers
|
||||
)
|
||||
r1 = await app_client.get("/api/v1/incidents?limit=2", headers=auth_headers)
|
||||
r1 = await app_client.get("/api/v1/incidents?limit=2", headers=ui_auth_headers)
|
||||
p1 = r1.json()
|
||||
assert len(p1["items"]) == 2
|
||||
assert p1["next_cursor"]
|
||||
r2 = await app_client.get(
|
||||
f"/api/v1/incidents?limit=2&cursor={p1['next_cursor']}", headers=auth_headers
|
||||
f"/api/v1/incidents?limit=2&cursor={p1['next_cursor']}", headers=ui_auth_headers
|
||||
)
|
||||
p2 = r2.json()
|
||||
assert len(p2["items"]) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_outbox_cursor(app_client, auth_headers):
|
||||
await app_client.post("/api/v1/heartbeat", json=make_heartbeat(), headers=auth_headers)
|
||||
async def test_incidents_default_sort_is_status(app_client, auth_headers, ui_auth_headers):
|
||||
auth_headers = await register_agent(app_client, ui_auth_headers)
|
||||
warning = make_event(check_id="warn-check", status="warning", exit_code=1)
|
||||
critical = make_event(check_id="crit-check", status="critical", exit_code=2)
|
||||
await app_client.post(
|
||||
"/api/v1/events",
|
||||
json={"agent_id": "agent-1", "events": [warning, critical]},
|
||||
headers=auth_headers,
|
||||
)
|
||||
|
||||
r = await app_client.get("/api/v1/incidents?limit=10", headers=ui_auth_headers)
|
||||
assert r.status_code == 200
|
||||
items = r.json()["items"]
|
||||
assert [i["severity"] for i in items[:2]] == ["critical", "warning"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_outbox_cursor(app_client, auth_headers, ui_auth_headers):
|
||||
auth_headers = await register_agent(app_client, ui_auth_headers)
|
||||
for i in range(3):
|
||||
ev = make_event(check_id=f"chk-{i}", status="critical", exit_code=2)
|
||||
await app_client.post(
|
||||
"/api/v1/events", json={"agent_id": "agent-1", "events": [ev]}, headers=auth_headers
|
||||
)
|
||||
r1 = await app_client.get("/api/v1/notifiers/outbox?limit=2", headers=auth_headers)
|
||||
r1 = await app_client.get("/api/v1/notifiers/outbox?limit=2", headers=ui_auth_headers)
|
||||
p1 = r1.json()
|
||||
assert len(p1["items"]) == 2
|
||||
assert p1["next_cursor"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalid_cursor_400(app_client, auth_headers):
|
||||
r = await app_client.get("/api/v1/agents?cursor=$$$bad", headers=auth_headers)
|
||||
async def test_invalid_cursor_400(app_client, ui_auth_headers):
|
||||
r = await app_client.get("/api/v1/agents?cursor=$$$bad", headers=ui_auth_headers)
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_events_query_invalid_cursor_uuid_400(app_client, ui_auth_headers):
|
||||
"""PH-010: cursor encodes (received_at, event_id) and event_id must parse
|
||||
as a UUID before reaching the SQL layer."""
|
||||
import base64
|
||||
|
||||
bad = base64.urlsafe_b64encode(b"2026-01-01T00:00:00+00:00|not-a-uuid").decode().rstrip("=")
|
||||
r = await app_client.get(f"/api/v1/events/query?cursor={bad}", headers=ui_auth_headers)
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_incidents_bidirectional_cursor_round_trip(app_client, auth_headers, ui_auth_headers):
|
||||
"""PH-review: page1 → next → prev returns page1 with the same items and
|
||||
order. prev_cursor must be null on the first page."""
|
||||
auth_headers = await register_agent(app_client, ui_auth_headers)
|
||||
for i in range(6):
|
||||
ev = make_event(check_id=f"chk-{i}", status="critical", exit_code=2)
|
||||
await app_client.post(
|
||||
"/api/v1/events",
|
||||
json={"agent_id": "agent-1", "events": [ev]},
|
||||
headers=auth_headers,
|
||||
)
|
||||
|
||||
r1 = await app_client.get("/api/v1/incidents?limit=3", headers=ui_auth_headers)
|
||||
p1 = r1.json()
|
||||
assert len(p1["items"]) == 3
|
||||
assert p1["next_cursor"]
|
||||
assert p1.get("prev_cursor") is None # first page
|
||||
|
||||
r2 = await app_client.get(
|
||||
f"/api/v1/incidents?limit=3&cursor={p1['next_cursor']}", headers=ui_auth_headers
|
||||
)
|
||||
p2 = r2.json()
|
||||
assert len(p2["items"]) == 3
|
||||
assert p2["prev_cursor"]
|
||||
ids1 = [i["id"] for i in p1["items"]]
|
||||
ids2 = [i["id"] for i in p2["items"]]
|
||||
assert set(ids1).isdisjoint(set(ids2))
|
||||
|
||||
r_back = await app_client.get(
|
||||
f"/api/v1/incidents?limit=3&cursor={p2['prev_cursor']}&back=true",
|
||||
headers=ui_auth_headers,
|
||||
)
|
||||
p_back = r_back.json()
|
||||
assert [i["id"] for i in p_back["items"]] == ids1
|
||||
# back to first page => prev_cursor must be null again
|
||||
assert p_back.get("prev_cursor") is None
|
||||
assert p_back["next_cursor"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_outbox_bidirectional_cursor_round_trip(app_client, auth_headers, ui_auth_headers):
|
||||
auth_headers = await register_agent(app_client, ui_auth_headers)
|
||||
for i in range(5):
|
||||
ev = make_event(check_id=f"chk-{i}", status="critical", exit_code=2)
|
||||
await app_client.post(
|
||||
"/api/v1/events",
|
||||
json={"agent_id": "agent-1", "events": [ev]},
|
||||
headers=auth_headers,
|
||||
)
|
||||
|
||||
r1 = await app_client.get("/api/v1/notifiers/outbox?limit=2", headers=ui_auth_headers)
|
||||
p1 = r1.json()
|
||||
assert len(p1["items"]) == 2
|
||||
assert p1["next_cursor"]
|
||||
assert p1.get("prev_cursor") is None
|
||||
|
||||
r2 = await app_client.get(
|
||||
f"/api/v1/notifiers/outbox?limit=2&cursor={p1['next_cursor']}",
|
||||
headers=ui_auth_headers,
|
||||
)
|
||||
p2 = r2.json()
|
||||
assert p2["prev_cursor"]
|
||||
ids1 = [i["id"] for i in p1["items"]]
|
||||
ids2 = [i["id"] for i in p2["items"]]
|
||||
assert set(ids1).isdisjoint(set(ids2))
|
||||
|
||||
r_back = await app_client.get(
|
||||
f"/api/v1/notifiers/outbox?limit=2&cursor={p2['prev_cursor']}&back=true",
|
||||
headers=ui_auth_headers,
|
||||
)
|
||||
p_back = r_back.json()
|
||||
assert [i["id"] for i in p_back["items"]] == ids1
|
||||
assert p_back.get("prev_cursor") is None
|
||||
|
||||
Reference in New Issue
Block a user