2026-04-21 07:41:47 -03:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
"""Tests for lab health query helpers and route payloads."""
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
from urllib.error import URLError
|
|
|
|
|
|
|
|
|
|
from atlas_portal.app_factory import create_app
|
|
|
|
|
from atlas_portal.routes import lab
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DummyUrlResponse:
|
|
|
|
|
"""Small context-manager response for urlopen tests."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, payload: dict | str, *, status: int = 200) -> None:
|
|
|
|
|
self.payload = payload
|
|
|
|
|
self.status = status
|
|
|
|
|
|
|
|
|
|
def __enter__(self):
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def read(self, size: int | None = None) -> bytes:
|
|
|
|
|
"""Return JSON or string content as response bytes."""
|
|
|
|
|
|
|
|
|
|
if isinstance(self.payload, str):
|
|
|
|
|
return self.payload.encode("utf-8")
|
|
|
|
|
return json.dumps(self.payload).encode("utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_vm_query_success_and_empty_paths(monkeypatch) -> None:
|
|
|
|
|
payloads = [
|
|
|
|
|
{"status": "success", "data": {"result": [{"value": [0, "2"]}, {"value": [0, "5"]}]}},
|
|
|
|
|
{"status": "error"},
|
|
|
|
|
{"status": "success", "data": {"result": []}},
|
|
|
|
|
{"status": "success", "data": {"result": [{"bad": []}]}},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def fake_urlopen(url, timeout):
|
|
|
|
|
return DummyUrlResponse(payloads.pop(0))
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(lab, "urlopen", fake_urlopen)
|
|
|
|
|
|
|
|
|
|
assert lab._vm_query("up") == 5.0
|
|
|
|
|
assert lab._vm_query("up") is None
|
|
|
|
|
assert lab._vm_query("up") is None
|
|
|
|
|
assert lab._vm_query("up") is None
|
|
|
|
|
|
|
|
|
|
|
2026-06-29 13:23:07 -03:00
|
|
|
def test_vm_query_vector_filters_bad_samples(monkeypatch) -> None:
|
|
|
|
|
payload = {
|
|
|
|
|
"status": "success",
|
|
|
|
|
"data": {
|
|
|
|
|
"result": [
|
|
|
|
|
{"metric": {"service": "nextcloud"}, "value": [0, "12"]},
|
|
|
|
|
{"metric": {"service": "broken"}, "value": []},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(lab, "urlopen", lambda url, timeout: DummyUrlResponse(payload))
|
|
|
|
|
|
|
|
|
|
assert lab._vm_query_vector("topk(1, up)") == [{"metric": {"service": "nextcloud"}, "value": 12.0}]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_public_active_service_uses_external_ai_chat_url(monkeypatch) -> None:
|
|
|
|
|
monkeypatch.setattr(lab.settings, "LAB_ACTIVE_SERVICE_QUERY", "topk(1, fake)")
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
lab,
|
|
|
|
|
"_vm_query_vector",
|
|
|
|
|
lambda expr: [{"metric": {"service": "atlas-ai-chat"}, "value": 9.0}],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert lab._public_active_service() == {
|
|
|
|
|
"known": True,
|
|
|
|
|
"label": "AI Chat",
|
|
|
|
|
"url": "https://bstein.dev/ai/chat",
|
|
|
|
|
"window": lab.settings.LAB_ACTIVE_SERVICE_WINDOW,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-04-21 07:41:47 -03:00
|
|
|
def test_http_ok_status_substring_and_errors(monkeypatch) -> None:
|
|
|
|
|
responses = [
|
|
|
|
|
DummyUrlResponse("service ok"),
|
|
|
|
|
DummyUrlResponse("wrong body"),
|
|
|
|
|
DummyUrlResponse("bad", status=503),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def fake_urlopen(url, timeout):
|
|
|
|
|
item = responses.pop(0)
|
|
|
|
|
if item == "raise":
|
|
|
|
|
raise URLError("offline")
|
|
|
|
|
return item
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(lab, "urlopen", fake_urlopen)
|
|
|
|
|
|
|
|
|
|
assert lab._http_ok("https://grafana", expect_substring="ok")
|
|
|
|
|
assert not lab._http_ok("https://grafana", expect_substring="ok")
|
|
|
|
|
assert not lab._http_ok("https://grafana")
|
|
|
|
|
responses.append("raise")
|
|
|
|
|
assert not lab._http_ok("https://grafana")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_lab_status_uses_cache_and_probe_fallbacks(monkeypatch) -> None:
|
|
|
|
|
app = create_app()
|
|
|
|
|
client = app.test_client()
|
|
|
|
|
lab._LAB_STATUS_CACHE["ts"] = 0.0
|
|
|
|
|
lab._LAB_STATUS_CACHE["value"] = None
|
2026-06-29 13:23:07 -03:00
|
|
|
monkeypatch.setattr(lab.settings, "ATLAS_DB_HOST_HEALTH_URL", "https://db.example.dev/health")
|
|
|
|
|
monkeypatch.setattr(lab.settings, "ATLAS_JUMPHOST_HEALTH_URL", "https://jump.example.dev/health")
|
2026-04-21 07:41:47 -03:00
|
|
|
|
|
|
|
|
calls: list[str] = []
|
|
|
|
|
|
|
|
|
|
def fake_http_ok(url, expect_substring=None):
|
|
|
|
|
calls.append(url)
|
2026-06-29 13:23:07 -03:00
|
|
|
return "grafana" in url or "db" in url or "jump" in url
|
2026-04-21 07:41:47 -03:00
|
|
|
|
|
|
|
|
monkeypatch.setattr(lab, "_http_ok", fake_http_ok)
|
|
|
|
|
monkeypatch.setattr(lab, "_vm_query", lambda expr: 1.0)
|
2026-06-29 13:23:07 -03:00
|
|
|
monkeypatch.setattr(
|
|
|
|
|
lab,
|
|
|
|
|
"_vm_query_vector",
|
|
|
|
|
lambda expr: [{"metric": {"service": "nextcloud-web"}, "value": 8.0}],
|
|
|
|
|
)
|
2026-04-21 07:41:47 -03:00
|
|
|
|
|
|
|
|
response = client.get("/api/lab/status")
|
|
|
|
|
payload = response.get_json()
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
2026-06-29 15:19:10 -03:00
|
|
|
assert response.headers["Cache-Control"] == "no-store, max-age=0"
|
|
|
|
|
assert response.headers["Pragma"] == "no-cache"
|
2026-04-21 07:41:47 -03:00
|
|
|
assert payload["connected"] is True
|
|
|
|
|
assert payload["atlas"]["source"] == "grafana"
|
2026-06-29 13:23:07 -03:00
|
|
|
assert payload["dedicated_hosts"]["up"] is True
|
|
|
|
|
assert payload["dedicated_hosts"]["up_count"] == 2
|
|
|
|
|
assert payload["dedicated_hosts"]["hosts"][0]["label"] == "Database node"
|
|
|
|
|
assert payload["active_service"] == {
|
|
|
|
|
"known": True,
|
|
|
|
|
"label": "Nextcloud",
|
|
|
|
|
"url": "https://cloud.bstein.dev",
|
|
|
|
|
"window": lab.settings.LAB_ACTIVE_SERVICE_WINDOW,
|
|
|
|
|
}
|
|
|
|
|
assert payload["oceanus"]["source"] == "atlas-member"
|
2026-04-21 07:41:47 -03:00
|
|
|
|
|
|
|
|
second = client.get("/api/lab/status")
|
|
|
|
|
assert second.get_json() == payload
|
2026-06-29 15:19:10 -03:00
|
|
|
assert second.headers["Cache-Control"] == "no-store, max-age=0"
|
2026-04-21 07:41:47 -03:00
|
|
|
|
|
|
|
|
lab._LAB_STATUS_CACHE["ts"] = 0.0
|
|
|
|
|
lab._LAB_STATUS_CACHE["value"] = None
|
2026-06-29 13:23:07 -03:00
|
|
|
monkeypatch.setattr(lab.settings, "ATLAS_DB_HOST_HEALTH_URL", "")
|
|
|
|
|
monkeypatch.setattr(lab.settings, "ATLAS_JUMPHOST_HEALTH_URL", "")
|
2026-04-21 07:41:47 -03:00
|
|
|
monkeypatch.setattr(lab, "_http_ok", lambda *a, **k: False)
|
|
|
|
|
monkeypatch.setattr(lab, "_vm_query", lambda expr: 0.0)
|
2026-06-29 13:23:07 -03:00
|
|
|
monkeypatch.setattr(lab, "_vm_query_vector", lambda expr: [])
|
2026-04-21 07:41:47 -03:00
|
|
|
|
|
|
|
|
response = client.get("/api/lab/status")
|
|
|
|
|
payload = response.get_json()
|
|
|
|
|
|
|
|
|
|
assert payload["atlas"]["source"] == "victoria-metrics"
|
|
|
|
|
assert payload["atlas"]["up"] is False
|
2026-06-29 13:23:07 -03:00
|
|
|
assert payload["dedicated_hosts"]["known"] is True
|
|
|
|
|
assert payload["dedicated_hosts"]["up"] is False
|
|
|
|
|
assert payload["active_service"]["known"] is False
|
2026-04-21 07:41:47 -03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_lab_status_handles_probe_exceptions(monkeypatch) -> None:
|
|
|
|
|
app = create_app()
|
|
|
|
|
client = app.test_client()
|
|
|
|
|
lab._LAB_STATUS_CACHE["ts"] = 0.0
|
|
|
|
|
lab._LAB_STATUS_CACHE["value"] = None
|
|
|
|
|
|
|
|
|
|
def boom(*args, **kwargs):
|
|
|
|
|
raise RuntimeError("offline")
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(lab, "_http_ok", boom)
|
|
|
|
|
monkeypatch.setattr(lab, "_vm_query", boom)
|
2026-06-29 13:23:07 -03:00
|
|
|
monkeypatch.setattr(lab, "_vm_query_vector", boom)
|
2026-04-21 07:41:47 -03:00
|
|
|
|
|
|
|
|
response = client.get("/api/lab/status")
|
|
|
|
|
payload = response.get_json()
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert payload["connected"] is False
|
|
|
|
|
assert payload["atlas"]["known"] is False
|
2026-06-29 13:23:07 -03:00
|
|
|
assert payload["dedicated_hosts"]["known"] is False
|
|
|
|
|
assert payload["active_service"]["known"] is False
|