import pytest from httpx import AsyncClient, ASGITransport from app.main import app @pytest.fixture async def client(): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as c: yield c @pytest.fixture async def admin_token(client: AsyncClient): resp = await client.post("/api/auth/login", json={"username": "admin", "password": "admin123"}) return resp.json()["access_token"] @pytest.fixture async def auth_headers(admin_token: str): return {"Authorization": f"Bearer {admin_token}"} async def test_health(client: AsyncClient): resp = await client.get("/api/health") assert resp.status_code == 200 assert resp.json()["status"] == "healthy" async def test_login_success(client: AsyncClient): resp = await client.post("/api/auth/login", json={"username": "admin", "password": "admin123"}) assert resp.status_code == 200 assert "access_token" in resp.json() async def test_login_invalid(client: AsyncClient): resp = await client.post("/api/auth/login", json={"username": "admin", "password": "wrong"}) assert resp.status_code == 401 async def test_get_me(client: AsyncClient, auth_headers: dict): resp = await client.get("/api/auth/me", headers=auth_headers) assert resp.status_code == 200 assert resp.json()["username"] == "admin" async def test_list_classes(client: AsyncClient, auth_headers: dict): resp = await client.get("/api/classes", headers=auth_headers) assert resp.status_code == 200 data = resp.json() assert len(data) >= 4 # Hardware, NetworkDevice, Software, Storage async def test_list_types(client: AsyncClient, auth_headers: dict): resp = await client.get("/api/types", headers=auth_headers) assert resp.status_code == 200 assert len(resp.json()) >= 6 async def test_list_locations(client: AsyncClient, auth_headers: dict): resp = await client.get("/api/locations", headers=auth_headers) assert resp.status_code == 200 assert len(resp.json()) >= 3 async def test_list_cis(client: AsyncClient, auth_headers: dict): resp = await client.get("/api/ci", headers=auth_headers) assert resp.status_code == 200 data = resp.json() assert data["total"] >= 25 assert data["page"] == 1 async def test_list_cis_pagination(client: AsyncClient, auth_headers: dict): resp = await client.get("/api/ci", headers=auth_headers, params={"page": 1, "page_size": 5}) data = resp.json() assert len(data["items"]) == 5 assert data["page_size"] == 5 async def test_list_cis_search(client: AsyncClient, auth_headers: dict): resp = await client.get("/api/ci", headers=auth_headers, params={"search": "proxmox"}) data = resp.json() assert data["total"] >= 2 assert all("proxmox" in i["name"].lower() for i in data["items"]) async def test_list_cis_filter_status(client: AsyncClient, auth_headers: dict): resp = await client.get("/api/ci", headers=auth_headers, params={"status": "active"}) data = resp.json() assert all(i["status"] == "active" for i in data["items"]) async def test_get_ci_detail(client: AsyncClient, auth_headers: dict): list_resp = await client.get("/api/ci", headers=auth_headers) ci_id = list_resp.json()["items"][0]["id"] resp = await client.get(f"/api/ci/{ci_id}", headers=auth_headers) assert resp.status_code == 200 ci = resp.json() assert "ip_addresses" in ci assert "relationships_out" in ci assert "relationships_in" in ci async def test_create_ci(client: AsyncClient, auth_headers: dict): types_resp = await client.get("/api/types", headers=auth_headers) type_id = types_resp.json()[0]["id"] resp = await client.post("/api/ci", headers=auth_headers, json={ "name": "test-ci-auto", "ci_type_id": type_id, "description": "Auto-created for testing", "status": "active", "tags": ["test", "automated"], "attributes": {"cpu_cores": 2, "ram_gb": 4}, }) assert resp.status_code == 201 ci = resp.json() assert ci["name"] == "test-ci-auto" assert ci["version"] == 1 async def test_update_ci(client: AsyncClient, auth_headers: dict): list_resp = await client.get("/api/ci", headers=auth_headers) ci_id = list_resp.json()["items"][0]["id"] resp = await client.patch(f"/api/ci/{ci_id}", headers=auth_headers, json={ "description": "Updated description" }) assert resp.status_code == 200 assert resp.json()["description"] == "Updated description" async def test_delete_ci_soft(client: AsyncClient, auth_headers: dict): # Create then delete types_resp = await client.get("/api/types", headers=auth_headers) type_id = types_resp.json()[0]["id"] create_resp = await client.post("/api/ci", headers=auth_headers, json={ "name": "to-be-deleted", "ci_type_id": type_id, "status": "active", }) ci_id = create_resp.json()["id"] del_resp = await client.delete(f"/api/ci/{ci_id}", headers=auth_headers) assert del_resp.status_code == 204 # Should not appear in list list_resp = await client.get("/api/ci", headers=auth_headers, params={"search": "to-be-deleted"}) assert list_resp.json()["total"] == 0 async def test_get_nonexistent_ci(client: AsyncClient, auth_headers: dict): resp = await client.get("/api/ci/00000000-0000-0000-0000-000000000000", headers=auth_headers) assert resp.status_code == 404 async def test_dashboard_stats(client: AsyncClient, auth_headers: dict): resp = await client.get("/api/dashboard/stats", headers=auth_headers) assert resp.status_code == 200 stats = resp.json() assert stats["total_cis"] > 0 assert stats["total_relationships"] > 0 async def test_graph(client: AsyncClient, auth_headers: dict): resp = await client.get("/api/ci/graph/visualize", headers=auth_headers, params={"depth": 1}) assert resp.status_code == 200 graph = resp.json() assert "nodes" in graph assert "edges" in graph assert len(graph["nodes"]) > 0 async def test_bulk_import(client: AsyncClient, auth_headers: dict): types_resp = await client.get("/api/types", headers=auth_headers) type_name = types_resp.json()[0]["name"] resp = await client.post("/api/ci/bulk/import", headers=auth_headers, json={ "items": [ {"name": "bulk-1", "ci_type_name": type_name, "status": "active"}, {"name": "bulk-2", "ci_type_name": type_name, "status": "active"}, ] }) assert resp.status_code == 201 assert resp.json()["created"] == 2 async def test_viewer_cannot_create(client: AsyncClient): resp = await client.post("/api/auth/login", json={"username": "viewer", "password": "view123"}) token = resp.json()["access_token"] headers = {"Authorization": f"Bearer {token}"} types_resp = await client.get("/api/types", headers=headers) type_id = types_resp.json()[0]["id"] resp = await client.post("/api/ci", headers=headers, json={ "name": "should-fail", "ci_type_id": type_id, }) assert resp.status_code == 403 async def test_unauthorized_access(client: AsyncClient): resp = await client.get("/api/ci") assert resp.status_code == 403 # No auth header → HTTPBearer returns 403