Initial curated ServiceMap source from 7a0a61232d05ba9097432abef6f7f4102a64d0b8
This commit is contained in:
commit
105251b9c8
96 files changed
+23587
No files matched your search
@@ -0,0 +1,182 @@
|
||||
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
|
||||
Reference in new issue
Block a user