from pathlib import Path from fastapi.testclient import TestClient from app import main as main_module from app.discovery import DiscoveryManager from app.discovery_store import DiscoveryStore from app.models import DiscoveryConfidence, RelationEvidence, RelationType from app.store import Store def test_discovery_routes_reject_request_controlled_targets(tmp_path: Path, monkeypatch) -> None: main_module.store = Store(str(tmp_path / "servicemap.db")) main_module.discovery_store = DiscoveryStore(main_module.store) main_module.discovery_manager = DiscoveryManager(main_module.store, main_module.discovery_store) monkeypatch.setenv("SERVICEMAP_DOCKER_HOST", "unix:///var/run/docker.sock") monkeypatch.setenv("SERVICEMAP_APPOPS_URL", "http://appops.internal:1216") client = TestClient(main_module.app) assert client.post( "/api/v1/docker/discover", json={"docker_host": "tcp://attacker.internal:2375"} ).status_code == 403 assert client.post( "/api/v1/discovery/run", json={"docker_host": "tcp://attacker.internal:2375"} ).status_code == 403 assert client.post( "/api/v1/discovery/run", json={"appops_url": "http://attacker.internal"} ).status_code == 403 def test_service_api_crud_cycle_guard_and_export(tmp_path: Path) -> None: main_module.store = Store(str(tmp_path / "servicemap.db")) client = TestClient(main_module.app) database_response = client.post( "/api/v1/services", json={"name": "Database", "kind": "tcp", "host": "db.local", "port": 5432, "depends_on": []}, ) assert database_response.status_code == 201 database = database_response.json() api_response = client.post( "/api/v1/services", json={ "name": "API", "kind": "http", "url": "https://api.local", "depends_on": [database["id"]], }, ) assert api_response.status_code == 201 api_service = api_response.json() cycle_response = client.put( f"/api/v1/services/{database['id']}", json={ "name": "Database", "kind": "tcp", "host": "db.local", "port": 5432, "depends_on": [api_service["id"]], }, ) assert cycle_response.status_code == 422 assert "cycle" in cycle_response.json()["detail"].lower() listing = client.get("/api/v1/services") assert listing.status_code == 200 assert len(listing.json()) == 2 exported = client.get("/api/v1/config?format=yaml") assert exported.status_code == 200 assert "Database" in exported.text assert "API" in exported.text deleted = client.delete(f"/api/v1/services/{database['id']}") assert deleted.status_code == 204 api_snapshot = client.get(f"/api/v1/services/{api_service['id']}").json() assert api_snapshot["service"]["depends_on"] == [] def test_overview_counts_only_visible_services(tmp_path: Path) -> None: main_module.store = Store(str(tmp_path / "servicemap.db")) client = TestClient(main_module.app) for name, visibility in (("Shown", "visible"), ("Retired", "hidden")): response = client.post( "/api/v1/services", json={"name": name, "kind": "tcp", "host": f"{name.lower()}.local", "port": 9000, "visibility": visibility}, ) assert response.status_code == 201 assert len(client.get("/api/v1/services").json()) == 1 assert client.get("/api/v1/overview").json()["total_services"] == 1 def test_config_import_endpoint_loads_validated_graph(tmp_path: Path) -> None: main_module.store = Store(str(tmp_path / "servicemap.db")) client = TestClient(main_module.app) content = """ version: 1 services: - name: Redis kind: tcp host: redis.local port: 6379 depends_on: [] - name: Worker kind: tcp host: worker.local port: 9000 depends_on: [Redis] """ response = client.post( "/api/v1/config/import", json={"content": content, "format": "yaml", "mode": "replace"}, ) assert response.status_code == 200 assert response.json() == {"created": 2, "updated": 0, "total": 2} assert len(client.get("/api/v1/services").json()) == 2 def test_service_api_rejects_invalid_endpoint_and_dependency_references(tmp_path: Path) -> None: main_module.store = Store(str(tmp_path / "servicemap.db")) client = TestClient(main_module.app) invalid_url = client.post( "/api/v1/services", json={"name": "Invalid", "kind": "http", "url": "ftp://example.test"}, ) assert invalid_url.status_code == 422 missing_dependency = client.post( "/api/v1/services", json={ "name": "API", "kind": "http", "url": "https://api.local", "depends_on": ["missing-service-id"], }, ) assert missing_dependency.status_code == 422 assert "unknown dependency" in missing_dependency.json()["detail"].lower() def test_semantic_topology_and_logical_relations_are_backend_derived(tmp_path: Path) -> None: main_module.store = Store(str(tmp_path / "servicemap.db")) main_module.discovery_store = DiscoveryStore(main_module.store) main_module.discovery_manager = DiscoveryManager(main_module.store, main_module.discovery_store) client = TestClient(main_module.app) database = client.post("/api/v1/services", json={ "name": "PostgreSQL", "kind": "tcp", "host": "db.local", "port": 5432, "application": "Zabbix", "domain": "Monitoring", "component_role": "database", }).json() api = client.post("/api/v1/services", json={ "name": "Zabbix API", "kind": "http", "url": "https://zabbix.local", "application": "Zabbix", "domain": "Monitoring", "component_role": "api", "depends_on": [database["id"]], }).json() now = main_module.store.get_service(database["id"]).created_at main_module.discovery_store.save_evidence(RelationEvidence( id="compose-edge", source_service_id=api["id"], target_service_id=database["id"], relation_type=RelationType.DEPENDS_ON, evidence_type="compose", confidence=DiscoveryConfidence.HIGH, explanation="Compose dependency", first_seen_at=now, last_seen_at=now, )) main_module.discovery_store.save_evidence(RelationEvidence( id="env-edge", source_service_id=api["id"], target_service_id=database["id"], relation_type=RelationType.DEPENDS_ON, evidence_type="environment", confidence=DiscoveryConfidence.MEDIUM, explanation="Environment dependency", first_seen_at=now, last_seen_at=now, )) semantic = client.get("/api/v1/topology/semantic") relations = client.get("/api/v1/topology/relations") services = client.get("/api/v1/services") assert semantic.status_code == 200 assert semantic.json()["domains"][0]["name"] == "Monitoring" assert semantic.json()["applications"][0]["component_count"] == 2 assert relations.json()[0]["confidence"] == "confirmed" assert len(relations.json()[0]["evidence"]) == 2 assert services.json()[0]["availability_24h"]["hours"] == 24