Files

501 lines
15 KiB
Python

import httpx
import pytest
from backend.app.connectors.unraid import UnraidConnector
from backend.app.core.config import settings
def test_unraid_request_preserves_api_key_for_same_host_https_redirect(monkeypatch):
calls = []
class FakeClient:
def __init__(self, **_kwargs):
pass
def __enter__(self):
return self
def __exit__(self, *_args):
return None
def post(self, url, *, headers, json):
calls.append((url, headers.copy(), json.copy()))
request = httpx.Request('POST', url)
if len(calls) == 1:
return httpx.Response(
302,
headers={'location': 'https://example-host:5001/graphql'},
request=request,
)
return httpx.Response(200, json={'data': {'__typename': 'Query'}}, request=request)
monkeypatch.setitem(settings.apis, 'UNRAID_URL', 'http://example-host:5000')
monkeypatch.setitem(settings.apis, 'UNRAID_API_KEY', 'test-api-key')
monkeypatch.setattr('backend.app.connectors.unraid.httpx.Client', FakeClient)
result = UnraidConnector()._request('query { __typename }')
assert result == {'__typename': 'Query'}
assert [call[0] for call in calls] == [
'http://example-host:5000/graphql',
'https://example-host:5001/graphql',
]
assert calls[1][1]['x-api-key'] == 'test-api-key'
def test_unraid_request_refuses_cross_host_redirect(monkeypatch):
class FakeClient:
def __init__(self, **_kwargs):
pass
def __enter__(self):
return self
def __exit__(self, *_args):
return None
def post(self, url, **_kwargs):
return httpx.Response(
302,
headers={'location': 'https://attacker.example/graphql'},
request=httpx.Request('POST', url),
)
monkeypatch.setitem(settings.apis, 'UNRAID_URL', 'http://example-host:5000')
monkeypatch.setitem(settings.apis, 'UNRAID_API_KEY', 'test-api-key')
monkeypatch.setattr('backend.app.connectors.unraid.httpx.Client', FakeClient)
with pytest.raises(RuntimeError, match='untrusted redirect'):
UnraidConnector()._request('query { __typename }')
def test_unraid_disk_warnings_maps_live_array_disk_schema(monkeypatch):
connector = UnraidConnector()
def fake_query(_queries):
return (
{
"array": {
"disks": [
{
"name": "disk1",
"status": "DISK_OK",
"warning": None,
"critical": None,
"fsUsed": 2_097_152,
"fsFree": 1_048_576,
"fsSize": 3_145_728,
},
{
"name": "disk2",
"status": "DISK_DSBL",
"warning": "SMART warning",
"critical": None,
},
]
}
},
"ok",
)
monkeypatch.setattr(connector, "_query", fake_query)
result = connector.get_disk_warnings()
assert result["status"] == "ok"
assert result["diskWarnings"] == 1
assert result["disks"][0] | {"type": None, "temperature": None} == {
"name": "disk1",
"status": "DISK_OK",
"healthy": True,
"type": None,
"usedGiB": 2,
"sizeGiB": 3,
"freeGiB": 1,
"temperature": None,
}
assert result["disks"][1]["name"] == "disk2"
assert result["disks"][1]["healthy"] is False
def test_unraid_share_summary_maps_live_share_schema(monkeypatch):
connector = UnraidConnector()
def fake_query(_queries):
return (
{
"shares": [
{
"name": "media",
"used": 2_097_152,
"free": 1_048_576,
"size": 0,
},
{
"name": "backups",
"used": 10,
"free": 5,
"size": 20,
},
]
},
"ok",
)
monkeypatch.setattr(connector, "_query", fake_query)
result = connector.get_share_summary()
assert result["status"] == "ok"
assert result["shares"] == [
{"name": "media", "usedGiB": 2, "sizeGiB": 3, "critical": False},
{"name": "backups", "usedGiB": 10, "sizeGiB": 20, "critical": False},
]
def test_unraid_docker_status_maps_live_docker_schema(monkeypatch):
connector = UnraidConnector()
def fake_query(_queries):
return (
{
"docker": {
"containers": [
{"id": "1", "names": ["plex"], "state": "running"},
{"id": "2", "names": ["radarr"], "state": "exited"},
],
"networks": [{"id": "n1", "name": "bridge"}],
}
},
"ok",
)
monkeypatch.setattr(connector, "_query", fake_query)
result = connector.get_docker_service_status()
assert result["status"] == "ok"
assert result["running"] is True
assert result["serviceStatus"] == "running"
assert result["containers"] == 2
def test_unraid_vm_summary_maps_live_vms_domains_schema(monkeypatch):
connector = UnraidConnector()
def fake_query(queries):
assert "vms { domains" in queries[0]
return ({"vms": {"domains": [{"id": "vm-1", "uuid": "uuid-1", "name": "Windows", "state": "RUNNING"}]}}, "ok")
monkeypatch.setattr(connector, "_query", fake_query)
result = connector.get_vm_summary()
assert result == {"status": "ok", "vms": [{"id": "vm-1", "name": "Windows", "state": "RUNNING"}]}
def test_unraid_vm_summary_does_not_claim_vm_manager_is_disabled(monkeypatch):
connector = UnraidConnector()
monkeypatch.setattr(connector, "_query", lambda _queries: ({"error": "GraphQL error: Failed to retrieve VM domains: VMs are not available"}, "error"))
result = connector.get_vm_summary()
assert result["status"] == "api_unavailable"
assert "vm:read" in result["reason"]
assert "kan nog steeds actief zijn" in result["reason"]
assert result["vms"] == []
def test_unraid_vm_mutation_uses_escaped_graphql_id(monkeypatch):
connector = UnraidConnector()
monkeypatch.setattr(connector, "is_configured", lambda: True)
captured = {}
def fake_request(query):
captured["query"] = query
return {"vm": {"start": True}}
monkeypatch.setattr(connector, "_request", fake_request)
result = connector.mutate_vm('server:uuid-"quoted"', "start")
assert result["success"] is True
assert result["status"] == "ok"
assert 'start(id: "server:uuid-\\\"quoted\\\"")' in captured["query"]
def test_unraid_vm_mutation_blocks_unsafe_actions_without_request(monkeypatch):
connector = UnraidConnector()
monkeypatch.setattr(connector, "_request", lambda _query: (_ for _ in ()).throw(AssertionError("must not call Unraid")))
result = connector.mutate_vm("vm-1", "forceStop")
assert result == {
"success": False,
"status": "blocked",
"message": "unsupported_or_blocked_vm_action",
"action": "forcestop",
}
def test_unraid_query_stops_after_vm_api_reports_unavailable(monkeypatch):
connector = UnraidConnector()
calls = []
def fake_request(query):
calls.append(query)
raise RuntimeError("GraphQL error: Failed to retrieve VM domains: VMs are not available")
monkeypatch.setattr(connector, "_request", fake_request)
payload, status = connector._query(["vm-query", "legacy-fallback"])
assert status == "unavailable"
assert "VMs are not available" in payload["error"]
assert calls == ["vm-query"]
def test_unraid_cache_status_maps_live_array_cache_schema(monkeypatch):
connector = UnraidConnector()
def fake_query(_queries):
return (
{
"array": {
"caches": [
{"name": "cache", "fsUsed": 25, "fsSize": 100},
{"name": "cache2", "fsUsed": 10, "fsSize": 100},
]
}
},
"ok",
)
monkeypatch.setattr(connector, "_query", fake_query)
result = connector.get_cache_status()
assert result["status"] == "ok"
assert result["cacheUsage"] == 17
assert [pool["name"] for pool in result["pools"]] == ["cache", "cache2"]
def test_unraid_cache_status_returns_cache_pool_details(monkeypatch):
connector = UnraidConnector()
def fake_query(_queries):
return (
{
"array": {
"caches": [
{
"name": "cache",
"status": "DISK_OK",
"fsUsed": 25,
"fsFree": 75,
"fsSize": 100,
"type": "CACHE",
},
{
"name": "fastpool",
"status": "DISK_DSBL",
"fsUsed": 90,
"fsFree": 10,
"fsSize": 100,
"warning": "pool warning",
},
]
}
},
"ok",
)
monkeypatch.setattr(connector, "_query", fake_query)
result = connector.get_cache_status()
assert result["status"] == "ok"
assert result["cacheUsage"] == 57
assert result["pools"] == [
{
"name": "cache",
"status": "DISK_OK",
"healthy": True,
"type": "CACHE",
"usedGiB": 25,
"sizeGiB": 100,
"freeGiB": 75,
"usagePercent": 25,
},
{
"name": "fastpool",
"status": "DISK_DSBL",
"healthy": False,
"type": None,
"usedGiB": 90,
"sizeGiB": 100,
"freeGiB": 10,
"usagePercent": 90,
},
]
def test_unraid_parity_status_uses_live_parity_schema(monkeypatch):
connector = UnraidConnector()
def fake_query(queries):
assert "parityCheckStatus" in queries[0]
assert "parities" in queries[0]
return (
{
"array": {
"parityCheckStatus": {
"status": "IDLE",
"running": False,
"paused": False,
"correcting": False,
"progress": 100,
"speed": 0,
"errors": 3,
},
"parities": [{"name": "parity", "status": "DISK_OK", "warning": None, "critical": None}],
}
},
"ok",
)
monkeypatch.setattr(connector, "_query", fake_query)
result = connector.get_parity_status()
assert result["status"] == "ok"
assert result["parityStatus"] == "valid"
assert result["checkStatus"] == "idle"
assert result["errors"] == 3
def test_unraid_parity_status_reports_running_check(monkeypatch):
connector = UnraidConnector()
monkeypatch.setattr(
connector,
"_query",
lambda _queries: (
{
"array": {
"parityCheckStatus": {
"status": "RUNNING",
"running": None,
"paused": False,
"correcting": False,
"progress": 42,
"speed": 1234,
"errors": 0,
},
"parities": [{"name": "parity", "status": "DISK_OK"}],
}
},
"ok",
),
)
result = connector.get_parity_status()
assert result["parityStatus"] == "checking"
assert result["running"] is True
assert result["progress"] == 42
def test_unraid_parity_status_is_invalid_for_unhealthy_or_missing_parity(monkeypatch):
connector = UnraidConnector()
for parities in ([{"name": "parity", "status": "DISK_DSBL"}], []):
monkeypatch.setattr(
connector,
"_query",
lambda _queries, value=parities: ({"array": {"parities": value}}, "ok"),
)
result = connector.get_parity_status()
assert result["parityStatus"] == "invalid"
def test_unraid_system_metrics_maps_live_metrics_schema(monkeypatch):
connector = UnraidConnector()
def fake_query(_queries):
return (
{
"metrics": {
"cpu": {
"cpus": [
{"percentIdle": 80.0, "percentUser": 10.0, "percentSystem": 10.0},
{"percentIdle": 60.0, "percentUser": 25.0, "percentSystem": 15.0},
]
},
"memory": {
"total": 1000,
"used": 640,
"free": 360,
"available": 300,
},
}
},
"ok",
)
monkeypatch.setattr(connector, "_query", fake_query)
result = connector.get_system_metrics()
assert result["status"] == "ok"
assert result["cpuUsage"] == 30
assert result["ramUsage"] == 70
assert result["sources"] == {
"cpu": "metrics.cpu.cpus.percentIdle",
"ram": "metrics.memory.available_total",
}
def test_unraid_system_metrics_reports_partial_instead_of_inventing_zero(monkeypatch):
connector = UnraidConnector()
def fake_query(_queries):
return (
{
"metrics": {
"cpu": {"cpus": []},
"memory": {"total": 1000, "used": 920},
}
},
"ok",
)
monkeypatch.setattr(connector, "_query", fake_query)
result = connector.get_system_metrics()
assert result["status"] == "partial"
assert result["cpuUsage"] is None
assert result["ramUsage"] == 92
assert "cpu_unavailable" in result["reason"]
def test_unraid_system_metrics_preserves_fractional_load(monkeypatch):
connector = UnraidConnector()
monkeypatch.setattr(connector, '_query', lambda queries: ({'metrics': {
'cpu': {'cpus': [{'percentIdle': 79.15}, {'percentIdle': 80.25}]},
'memory': {'total': 10000, 'available': 5432, 'used': 9000},
}}, 'ok'))
result = connector.get_system_metrics()
assert result['cpuUsage'] == 20.3
assert result['ramUsage'] == 45.68
def test_unraid_system_metrics_rejects_non_finite_load(monkeypatch):
connector = UnraidConnector()
monkeypatch.setattr(connector, '_query', lambda queries: ({'metrics': {
'cpu': {'cpus': [{'percentIdle': float('nan')}]},
'memory': {'total': 10000, 'used': float('inf')},
}}, 'ok'))
result = connector.get_system_metrics()
assert result['cpuUsage'] is None
assert result['ramUsage'] is None