import pytest @pytest.mark.asyncio async def test_health_no_auth(app_client): r = await app_client.get("/api/v1/health") assert r.status_code == 200 assert r.json()["status"] == "ok" @pytest.mark.asyncio async def test_metrics_no_auth(app_client): r = await app_client.get("/metrics") assert r.status_code == 200 @pytest.mark.asyncio async def test_missing_token_401(app_client): r = await app_client.get("/api/v1/agents") assert r.status_code == 401 body = r.json() assert body["error"]["code"] == "unauthorized" assert body["error"]["request_id"] @pytest.mark.asyncio async def test_wrong_token_401(app_client): r = await app_client.get("/api/v1/agents", headers={"Authorization": "Bearer nope"}) assert r.status_code == 401 @pytest.mark.asyncio async def test_valid_token_200(app_client, ui_auth_headers): r = await app_client.get("/api/v1/agents", headers=ui_auth_headers) assert r.status_code == 200 @pytest.mark.asyncio async def test_ui_token_is_separate_from_agent_self_key(monkeypatch, database_url): from httpx import ASGITransport, AsyncClient from monlet_server import db as db_module from monlet_server.main import create_app from monlet_server.settings import reset_settings_cache from tests._helpers import agent_headers, make_heartbeat monkeypatch.setenv("MONLET_AUTH_TOKEN", "ui-token") reset_settings_cache() await db_module.dispose_engine() app = create_app() async with app.router.lifespan_context(app): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: ui = {"Authorization": "Bearer ui-token"} agent_a = agent_headers("agent-a") agent_b = agent_headers("agent-b") assert (await client.get("/api/v1/agents", headers=ui)).status_code == 200 assert (await client.get("/api/v1/agents", headers=agent_a)).status_code == 401 r = await client.post( "/api/v1/heartbeat", json=make_heartbeat("agent-a"), headers=agent_a ) assert r.status_code == 202 r = await client.post( "/api/v1/heartbeat", json=make_heartbeat("agent-b"), headers=agent_b ) assert r.status_code == 202 r = await client.post("/api/v1/heartbeat", json=make_heartbeat("agent-c"), headers=ui) assert r.status_code == 401 await db_module.dispose_engine() monkeypatch.setenv("MONLET_AUTH_TOKEN", "test-ui-token") reset_settings_cache() @pytest.mark.asyncio async def test_multi_token_overlap(monkeypatch, database_url): """Two active tokens both authenticate; rotation can overlap without 401s.""" from httpx import ASGITransport, AsyncClient from monlet_server import db as db_module from monlet_server.main import create_app from monlet_server.settings import reset_settings_cache monkeypatch.setenv("MONLET_AUTH_TOKEN", "old-token new-token") reset_settings_cache() await db_module.dispose_engine() app = create_app() async with app.router.lifespan_context(app): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: for tok in ("old-token", "new-token"): r = await client.get("/api/v1/agents", headers={"Authorization": f"Bearer {tok}"}) assert r.status_code == 200, tok r = await client.get("/api/v1/agents", headers={"Authorization": "Bearer revoked"}) assert r.status_code == 401 await db_module.dispose_engine() monkeypatch.setenv("MONLET_AUTH_TOKEN", "test-ui-token") reset_settings_cache()