Files
ServiceMap-Public/backend/tests/test_api.py
T

183 lines
7.0 KiB
Python

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