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