183 lines
7.0 KiB
Python
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
|