Files
homelable/backend/tests/test_integration.py
T
Pouzor cc68fcf1c1 test: add deployment test tiers (shellcheck, hadolint, Docker smoke, integration)
Tier 1 — quality.yml: ShellCheck on lxc-install.sh, hadolint on both Dockerfiles
Tier 2 — docker-ci.yml: build images, smoke-test backend health + frontend 200
Tier 3 — test_integration.py: full stack pytest (auth, canvas save/reload, dimensions)
Also adds Docker healthcheck to backend service in docker-compose.yml
2026-03-28 12:33:35 +01:00

199 lines
6.2 KiB
Python

"""
Integration tests — run against a live Docker stack.
Skipped unless INTEGRATION_BASE_URL is set (done automatically in docker-ci.yml).
Usage (local):
INTEGRATION_BASE_URL=http://localhost:8000 \
INTEGRATION_USERNAME=admin \
INTEGRATION_PASSWORD=your-password \
pytest backend/tests/test_integration.py -v
"""
import os
import httpx
import pytest
BASE_URL = os.environ.get("INTEGRATION_BASE_URL", "")
USERNAME = os.environ.get("INTEGRATION_USERNAME", "admin")
PASSWORD = os.environ.get("INTEGRATION_PASSWORD", "")
pytestmark = pytest.mark.skipif(
not BASE_URL,
reason="INTEGRATION_BASE_URL not set — skipping live-stack tests",
)
@pytest.fixture(scope="module")
def token() -> str:
res = httpx.post(
f"{BASE_URL}/api/v1/auth/login",
json={"username": USERNAME, "password": PASSWORD},
timeout=10,
)
assert res.status_code == 200, f"Login failed: {res.text}"
return res.json()["access_token"]
@pytest.fixture(scope="module")
def auth(token: str) -> dict:
return {"Authorization": f"Bearer {token}"}
# ── Health ────────────────────────────────────────────────────────────────────
def test_health_endpoint():
res = httpx.get(f"{BASE_URL}/api/v1/health", timeout=10)
assert res.status_code == 200
# ── Auth ──────────────────────────────────────────────────────────────────────
def test_login_returns_token():
res = httpx.post(
f"{BASE_URL}/api/v1/auth/login",
json={"username": USERNAME, "password": PASSWORD},
timeout=10,
)
assert res.status_code == 200
data = res.json()
assert "access_token" in data
assert data["token_type"] == "bearer"
def test_login_bad_credentials():
res = httpx.post(
f"{BASE_URL}/api/v1/auth/login",
json={"username": USERNAME, "password": "wrong-password"},
timeout=10,
)
assert res.status_code == 401
def test_protected_route_without_token():
res = httpx.get(f"{BASE_URL}/api/v1/canvas", timeout=10)
assert res.status_code == 401
# ── Canvas round-trip ─────────────────────────────────────────────────────────
def test_canvas_load_returns_valid_structure(auth):
res = httpx.get(f"{BASE_URL}/api/v1/canvas", headers=auth, timeout=10)
assert res.status_code == 200
data = res.json()
assert "nodes" in data
assert "edges" in data
assert isinstance(data["nodes"], list)
assert isinstance(data["edges"], list)
def test_canvas_save_and_reload(auth):
payload = {
"nodes": [
{
"id": "integ-node-1",
"type": "server",
"position": {"x": 100, "y": 200},
"data": {
"label": "CI Server",
"type": "server",
"status": "unknown",
"services": [],
},
"width": 240,
"height": 100,
}
],
"edges": [],
"viewport": {"x": 0, "y": 0, "zoom": 1},
}
save_res = httpx.post(
f"{BASE_URL}/api/v1/canvas/save",
json=payload,
headers=auth,
timeout=10,
)
assert save_res.status_code == 200
load_res = httpx.get(f"{BASE_URL}/api/v1/canvas", headers=auth, timeout=10)
assert load_res.status_code == 200
nodes = load_res.json()["nodes"]
assert len(nodes) == 1
node = nodes[0]
assert node["label"] == "CI Server"
assert node["type"] == "server"
assert node["x"] == pytest.approx(100)
assert node["y"] == pytest.approx(200)
def test_canvas_save_preserves_node_dimensions(auth):
"""Width/height survive a save→reload cycle through the real DB."""
payload = {
"nodes": [
{
"id": "resized-node",
"type": "router",
"position": {"x": 50, "y": 50},
"data": {
"label": "Big Router",
"type": "router",
"status": "unknown",
"services": [],
},
"width": 320,
"height": 150,
}
],
"edges": [],
"viewport": {"x": 0, "y": 0, "zoom": 1},
}
httpx.post(f"{BASE_URL}/api/v1/canvas/save", json=payload, headers=auth, timeout=10)
nodes = httpx.get(f"{BASE_URL}/api/v1/canvas", headers=auth, timeout=10).json()["nodes"]
node = next((n for n in nodes if n["id"] == "resized-node"), None)
assert node is not None
assert node["width"] == pytest.approx(320)
assert node["height"] == pytest.approx(150)
def test_canvas_save_with_edge(auth):
payload = {
"nodes": [
{
"id": "n-src",
"type": "router",
"position": {"x": 0, "y": 0},
"data": {"label": "Router", "type": "router", "status": "unknown", "services": []},
},
{
"id": "n-dst",
"type": "server",
"position": {"x": 200, "y": 0},
"data": {"label": "Server", "type": "server", "status": "unknown", "services": []},
},
],
"edges": [
{
"id": "e-eth",
"source": "n-src",
"target": "n-dst",
"type": "ethernet",
"data": {"type": "ethernet"},
}
],
"viewport": {"x": 0, "y": 0, "zoom": 1},
}
save_res = httpx.post(
f"{BASE_URL}/api/v1/canvas/save", json=payload, headers=auth, timeout=10
)
assert save_res.status_code == 200
data = httpx.get(f"{BASE_URL}/api/v1/canvas", headers=auth, timeout=10).json()
assert len(data["edges"]) == 1
edge = data["edges"][0]
assert edge["source_id"] == "n-src"
assert edge["target_id"] == "n-dst"
assert edge["type"] == "ethernet"