API: /config/{services,jobs,tools,static}/{name} now pin the twin they
target — _save_deployment/_delete_deployment take an explicit kind so a
patch to a 'backup' service can't bleed into a 'backup' job/tool. Add
/tools and /static endpoints; keep /deployments/{name} kind-agnostic.
New test_kind_twins proves per-kind save/delete isolation on disk.
UI: ConfigPanel and CreateDeploymentForm write to the kind-scoped
resource; GatewayPanel/NodeDetail/DeploymentsSection link via a shared
detailPath(name, kind) helper instead of the ambiguous /deployment/:name.
Includes incidental ruff-format reflow of untouched api files.
95 lines
3.3 KiB
Python
95 lines
3.3 KiB
Python
"""Tests for nodes endpoints."""
|
|
|
|
from pathlib import Path
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from castle_core.registry import Deployment, NodeConfig, NodeRegistry
|
|
|
|
from castle_api.mesh import MeshStateManager
|
|
|
|
|
|
class TestNodesList:
|
|
"""GET /nodes endpoint tests."""
|
|
|
|
def test_returns_local_node(self, client: TestClient) -> None:
|
|
"""Always returns the local node."""
|
|
response = client.get("/nodes")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data) >= 1
|
|
local = data[0]
|
|
assert local["hostname"] == "test-node"
|
|
assert local["is_local"] is True
|
|
assert local["online"] is True
|
|
|
|
def test_local_node_counts(self, client: TestClient) -> None:
|
|
"""Local node has correct deployment counts."""
|
|
response = client.get("/nodes")
|
|
data = response.json()
|
|
local = data[0]
|
|
assert local["deployed_count"] == 2 # test-svc + test-tool
|
|
assert local["service_count"] == 1 # only test-svc is a service
|
|
|
|
def test_includes_remote_nodes(
|
|
self, client: TestClient, registry_path: Path
|
|
) -> None:
|
|
"""Remote nodes from mesh state are included."""
|
|
import castle_api.mesh as mesh_mod
|
|
|
|
original = mesh_mod.mesh_state
|
|
try:
|
|
mgr = MeshStateManager()
|
|
remote_reg = NodeRegistry(
|
|
node=NodeConfig(hostname="devbox", gateway_port=9000),
|
|
deployed={
|
|
"remote-svc": Deployment(
|
|
manager="systemd",
|
|
launcher="python",
|
|
run_cmd=["svc"],
|
|
port=9050,
|
|
kind="service",
|
|
),
|
|
},
|
|
)
|
|
mgr.update_node("devbox", remote_reg)
|
|
mesh_mod.mesh_state = mgr
|
|
|
|
# Also patch the reference in the nodes module
|
|
import castle_api.nodes as nodes_mod
|
|
|
|
nodes_mod.mesh_state = mgr
|
|
|
|
response = client.get("/nodes")
|
|
data = response.json()
|
|
hostnames = [n["hostname"] for n in data]
|
|
assert "devbox" in hostnames
|
|
devbox = next(n for n in data if n["hostname"] == "devbox")
|
|
assert devbox["is_local"] is False
|
|
assert devbox["deployed_count"] == 1
|
|
finally:
|
|
mesh_mod.mesh_state = original
|
|
import castle_api.nodes as nodes_mod2
|
|
|
|
nodes_mod2.mesh_state = original
|
|
|
|
|
|
class TestNodeDetail:
|
|
"""GET /nodes/{hostname} endpoint tests."""
|
|
|
|
def test_local_node_detail(self, client: TestClient) -> None:
|
|
"""Returns local node detail with deployed components."""
|
|
response = client.get("/nodes/test-node")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["hostname"] == "test-node"
|
|
assert data["is_local"] is True
|
|
assert len(data["deployed"]) == 2 # test-svc + test-tool
|
|
svc = next(d for d in data["deployed"] if d["id"] == "test-svc")
|
|
assert svc["node"] == "test-node"
|
|
|
|
def test_unknown_node_returns_404(self, client: TestClient) -> None:
|
|
"""Returns 404 for unknown hostname."""
|
|
response = client.get("/nodes/nonexistent")
|
|
assert response.status_code == 404
|