import pytest from sqlalchemy import select from monlet_server.models import Agent from ._helpers import make_heartbeat @pytest.mark.asyncio async def test_heartbeat_creates_agent(app_client, auth_headers, session): r = await app_client.post("/api/v1/heartbeat", json=make_heartbeat(), headers=auth_headers) assert r.status_code == 202 res = await session.execute(select(Agent).where(Agent.agent_id == "agent-1")) a = res.scalar_one() assert a.hostname == "host-1" assert a.status == "alive" @pytest.mark.asyncio async def test_heartbeat_upsert(app_client, auth_headers, session): await app_client.post("/api/v1/heartbeat", json=make_heartbeat(), headers=auth_headers) hb = make_heartbeat() hb["features"] = {"push": True, "metrics": False} await app_client.post("/api/v1/heartbeat", json=hb, headers=auth_headers) res = await session.execute(select(Agent).where(Agent.agent_id == "agent-1")) a = res.scalar_one() assert a.features == {"push": True, "metrics": False} @pytest.mark.asyncio async def test_heartbeat_validation_400(app_client, auth_headers): r = await app_client.post( "/api/v1/heartbeat", json={ "agent_id": "bad id!", "observed_at": "x", "hostname": "h", "features": {"push": True, "metrics": False}, }, headers=auth_headers, ) assert r.status_code == 400 assert r.json()["error"]["code"] == "validation" @pytest.mark.asyncio @pytest.mark.parametrize("observed_at", ["2026-05-27T09:00:00", "2026-05-27T09:00:00+04:00"]) async def test_heartbeat_requires_utc_timestamp(app_client, auth_headers, observed_at): hb = make_heartbeat() hb["observed_at"] = observed_at r = await app_client.post("/api/v1/heartbeat", json=hb, headers=auth_headers) assert r.status_code == 400 assert r.json()["error"]["code"] == "validation" @pytest.mark.asyncio async def test_heartbeat_rejects_extra_fields(app_client, auth_headers): hb = make_heartbeat() hb["mode"] = "hybrid" r = await app_client.post("/api/v1/heartbeat", json=hb, headers=auth_headers) assert r.status_code == 400 assert r.json()["error"]["code"] == "validation" @pytest.mark.asyncio async def test_heartbeat_rejects_sensitive_label_keys(app_client, auth_headers): hb = make_heartbeat() hb["labels"] = {"api_key": "secret"} r = await app_client.post("/api/v1/heartbeat", json=hb, headers=auth_headers) assert r.status_code == 400 assert r.json()["error"]["code"] == "validation"