"""Endpoint tests for the wiring-graph router (/api/v1/graph*). The graph router resolves stores via the ``dependencies`` getters *directly* (not FastAPI ``Depends``), so these tests populate the ``deps._deps`` registry rather than using ``app.dependency_overrides``. Auth stays real (conftest pins a test API key) so the rejection path is covered too. """ import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from ledgrab.api import dependencies as deps from ledgrab.api.routes.graph import router _AUTH = {"Authorization": "Bearer test-api-key-12345"} class _Ent: """Minimal stand-in for a storage model exposing ``to_dict``.""" def __init__(self, data: dict): self._data = data def to_dict(self) -> dict: return self._data def _store(*entities: dict): class _S: def get_all(self): return [_Ent(e) for e in entities] return _S() @pytest.fixture def client(test_config, monkeypatch): import ledgrab.config as config_mod monkeypatch.setattr(config_mod, "config", test_config) # Populate only the kinds under test; the rest resolve to RuntimeError and # are gracefully treated as empty by the route's _gather_entities. monkeypatch.setattr( deps, "_deps", { "device_store": _store({"id": "dev_1", "name": "Strip", "device_type": "wled"}), "picture_source_store": _store({"id": "ps_1", "name": "Cap", "stream_type": "raw"}), "color_strip_store": _store( { "id": "css_1", "name": "CSS", "source_type": "picture", "picture_source_id": "ps_1", } ), "output_target_store": _store( { "id": "ot_1", "name": "TV", "target_type": "led", "device_id": "dev_1", "color_strip_source_id": "css_1", } ), }, ) app = FastAPI() app.include_router(router) return TestClient(app, raise_server_exceptions=False) def test_schema_endpoint_returns_registry(client): resp = client.get("/api/v1/graph/schema", headers=_AUTH) assert resp.status_code == 200 data = resp.json() assert "device" in data["kinds"] assert any( c["target_kind"] == "output_target" and c["field"] == "device_id" for c in data["connections"] ) # Bindable + nested flags must be carried through. assert any(c["bindable"] and c["nested"] for c in data["connections"]) def test_schema_endpoint_requires_auth(client): assert client.get("/api/v1/graph/schema").status_code in (401, 403) def test_graph_endpoint_builds_topology(client): data = client.get("/api/v1/graph", headers=_AUTH).json() assert {n["id"] for n in data["nodes"]} == {"dev_1", "ps_1", "css_1", "ot_1"} edges = {(e["from"], e["to"]) for e in data["edges"]} assert ("dev_1", "ot_1") in edges assert ("css_1", "ot_1") in edges assert ("ps_1", "css_1") in edges assert data["issues"]["broken_refs"] == [] def test_dependents_endpoint(client): data = client.get("/api/v1/graph/dependents/color_strip_source/css_1", headers=_AUTH).json() assert data["dependents"] == [ {"id": "ot_1", "kind": "output_target", "name": "TV", "field": "color_strip_source_id"} ] def test_dependents_endpoint_rejects_unknown_kind(client): resp = client.get("/api/v1/graph/dependents/bogus/x", headers=_AUTH) assert resp.status_code == 404 def test_validate_connection_endpoint_accepts_valid(client): resp = client.post( "/api/v1/graph/validate-connection", json={ "target_kind": "output_target", "target_id": "ot_1", "field": "color_strip_source_id", "source_id": "css_1", }, headers=_AUTH, ) assert resp.status_code == 200 assert resp.json() == {"ok": True, "error": None} def test_validate_connection_endpoint_rejects_missing_source(client): resp = client.post( "/api/v1/graph/validate-connection", json={ "target_kind": "output_target", "target_id": "ot_1", "field": "color_strip_source_id", "source_id": "ghost", }, headers=_AUTH, ) data = resp.json() assert data["ok"] is False assert "not found" in data["error"]