Files
NODEDC_MISSION_CORE/tests/test_compute_contour_api.py

147 lines
4.6 KiB
Python

from __future__ import annotations
import stat
from collections.abc import Callable
from pathlib import Path
from typing import Any
import pytest
from fastapi import APIRouter, HTTPException
from fastapi.routing import APIRoute
from k1link.web.compute_contour_api import (
CATALOG_SCHEMA,
ComputeContourCreate,
ComputeContourPut,
ComputeContourStore,
build_compute_contour_router,
)
def _endpoint(router: APIRouter, path: str, method: str) -> Callable[..., Any]:
for route in router.routes:
if (
isinstance(route, APIRoute)
and route.path == path
and route.methods is not None
and method in route.methods
):
return route.endpoint
raise AssertionError(f"{method} {path} route is missing")
def test_contour_store_migrates_worker_006_as_first_configuration(
tmp_path: Path,
) -> None:
store = ComputeContourStore(tmp_path / "system")
contours = store.list_contours()
assert len(contours) == 1
assert contours[0].contour_id == "worker-006"
assert contours[0].telemetry_mode == "agent-mqtt"
assert contours[0].telemetry_poll_interval_seconds == 3
assert contours[0].mqtt_publish_interval_seconds == 2
assert contours[0].mqtt_bind_address == "127.0.0.1"
assert not store.path.exists()
def test_contour_store_creates_and_updates_private_catalog(tmp_path: Path) -> None:
store = ComputeContourStore(tmp_path / "system")
created = store.create(
ComputeContourCreate(
display_name="Field Worker",
expected_node_id="FIELD-01",
platform="linux",
address="192.0.2.25",
mqtt_host="192.0.2.5",
mqtt_bind_address="192.168.10.5",
)
)
updated = store.update(
created.contour_id,
ComputeContourPut(
revision=created.revision,
display_name="Field Worker 01",
expected_node_id="FIELD-01",
platform="linux",
telemetry_mode="agent-mqtt",
address="192.0.2.25",
ssh_port=22,
mqtt_host="192.0.2.5",
mqtt_port=1883,
mqtt_bind_address="192.168.10.5",
telemetry_poll_interval_seconds=1,
mqtt_publish_interval_seconds=4,
),
)
assert updated.display_name == "Field Worker 01"
assert updated.telemetry_poll_interval_seconds == 1
assert updated.mqtt_publish_interval_seconds == 4
assert updated.revision == 1
assert stat.S_IMODE(store.path.stat().st_mode) == 0o600
assert len(store.list_contours()) == 2
with pytest.raises(RuntimeError, match="revision changed"):
store.update(
created.contour_id,
ComputeContourPut(
revision=0,
display_name="stale",
expected_node_id="FIELD-01",
platform="linux",
telemetry_mode="agent-mqtt",
address="",
ssh_port=22,
mqtt_host="127.0.0.1",
mqtt_port=1883,
mqtt_bind_address="127.0.0.1",
telemetry_poll_interval_seconds=3,
mqtt_publish_interval_seconds=2,
),
)
def test_contour_router_exposes_catalog_and_safe_install_contract(
tmp_path: Path,
) -> None:
router = build_compute_contour_router(
root_provider=lambda: tmp_path / "system"
)
list_contours = _endpoint(router, "/api/v1/system/contours", "GET")
install = _endpoint(
router,
"/api/v1/system/contours/{contour_id}/agent-install",
"GET",
)
catalog = list_contours()
assert catalog["schema_version"] == CATALOG_SCHEMA
assert catalog["contours"][0]["contour_id"] == "worker-006"
document = install("worker-006")
assert document["agent"]["distribution"] == "Telegraf"
assert "MQTT password" in document["command"]
assert "password" not in document["agent"]["environment"]
assert document["agent"]["environment"]["MISSIONCORE_TELEMETRY_INTERVAL"] == "2s"
assert document["ready"] is False
network = _endpoint(
router,
"/api/v1/system/contours/{contour_id}/network",
"GET",
)
assert callable(network)
def test_contour_router_returns_404_for_unknown_contour(tmp_path: Path) -> None:
router = build_compute_contour_router(
root_provider=lambda: tmp_path / "system"
)
install = _endpoint(
router,
"/api/v1/system/contours/{contour_id}/agent-install",
"GET",
)
with pytest.raises(HTTPException) as error:
install("missing")
assert error.value.status_code == 404