"""
Integration tests for Controller API endpoints.
"""
import re
from unittest import mock
import pytest
from fastapi.testclient import TestClient
from controller import __version__
class TestHealthEndpoint:
"""Tests for /api/health endpoint."""
def test_health_returns_200(self, test_client):
"""Health endpoint should return 200 OK."""
response = test_client.get("/api/health")
assert response.status_code == 200
def test_health_returns_expected_fields(self, test_client):
"""Health endpoint should return expected fields."""
response = test_client.get("/api/health")
data = response.json()
assert data["status"] == "healthy"
assert data["version"] == __version__
assert data["docker_mode"] is False
assert "drive_mounted" in data
assert isinstance(data["drive_mounted"], bool)
def test_health_returns_docker_mode_true(self, test_client_docker_mode):
"""Health endpoint should return docker_mode=true when in Docker."""
response = test_client_docker_mode.get("/api/health")
data = response.json()
assert data["docker_mode"] is True
class TestDashboardEndpoint:
"""Tests for / dashboard endpoint (UI)."""
def test_dashboard_basic_response(self, test_client):
"""Dashboard should return 200 with HTML content-type and a recognisable body."""
response = test_client.get("/")
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
assert b"Controller" in response.content
assert b"WROLPi" in response.content
@pytest.mark.parametrize("needle", [
# Status cards.
"CPU", "Memory", "Load", "Storage",
# Sections / structural ids.
"Disks", 'id="disks"', "Services", 'id="services"',
# System actions.
"System Actions", "Restart All Services",
# Mount modal.
'id="mount-modal"', 'id="mount-point-input"', 'id="mount-persist-checkbox"',
"Persistent (survive reboots)",
# Disks JS table.
"
Persist | ", "function togglePersist", "api/disks/fstab",
])
def test_dashboard_contains(self, test_client, needle):
"""Dashboard body should contain each expected fragment (native mode)."""
response = test_client.get("/")
assert needle in response.text
def test_dashboard_native_mode_shows_boot_column_and_hides_docker_banner(self, test_client):
"""Native (non-Docker) mode: Boot column visible, no Docker banner."""
response = test_client.get("/")
content = response.text
assert "Boot | " in content
assert 'class="toggle"' in content
assert "Running in Docker mode" not in content
def test_dashboard_docker_mode_shows_banner_and_hides_boot_column(self, test_client_docker_mode):
"""Docker mode: Docker banner visible, no Boot toggles in rendered tbody."""
response = test_client_docker_mode.get("/")
content = response.text
assert "Running in Docker mode" in content
assert "Boot | " not in content
# JS template literals may mention 'id="boot-' but the server-rendered tbody must not.
tbody_match = re.search(r'(.*?)', content, re.DOTALL)
if tbody_match:
assert 'id="boot-' not in tbody_match.group(1)
class TestOpenAPI:
"""Tests for the OpenAPI schema and its expected endpoint coverage."""
@pytest.fixture
def openapi(self, test_client):
"""Fetch the OpenAPI schema once per parametrized test."""
response = test_client.get("/openapi.json")
assert response.status_code == 200
return response.json()
def test_openapi_metadata(self, openapi):
"""Schema title and version should match the package metadata."""
assert openapi["info"]["title"] == "WROLPi Controller"
assert openapi["info"]["version"] == __version__
@pytest.mark.parametrize("path", [
# Health.
"/api/health",
# Stats (per-resource + aggregated).
"/api/stats/cpu", "/api/stats/memory", "/api/stats/load",
"/api/stats/drives/primary", "/api/stats/network", "/api/stats/power",
# Admin: hotspot, bluetooth, throttle, timezone, power.
"/api/hotspot/status", "/api/hotspot/enable", "/api/hotspot/disable",
"/api/bluetooth/status", "/api/bluetooth/enable", "/api/bluetooth/disable",
"/api/throttle/status", "/api/throttle/enable", "/api/throttle/disable",
"/api/timezone/status", "/api/timezone/set",
"/api/shutdown", "/api/reboot", "/api/restart",
# Disks.
"/api/disks", "/api/disks/mounts", "/api/disks/mount", "/api/disks/unmount",
"/api/disks/fstab", "/api/disks/smart",
# Services.
"/api/services", "/api/services/{name}",
"/api/services/{name}/start", "/api/services/{name}/stop",
"/api/services/{name}/restart", "/api/services/{name}/enable",
"/api/services/{name}/disable", "/api/services/{name}/logs",
# Scripts.
"/api/scripts", "/api/scripts/status",
"/api/scripts/{name}/start", "/api/scripts/{name}/output",
])
def test_openapi_has_path(self, openapi, path):
"""Every documented API path should be present in the OpenAPI schema."""
assert path in openapi["paths"], f"OpenAPI schema is missing {path}"
def test_openapi_has_aggregated_stats(self, openapi):
"""Aggregated /api/stats endpoint (may register with or without trailing slash)."""
paths = openapi["paths"]
assert "/api/stats" in paths or "/api/stats/" in paths
class TestDocsEndpoint:
"""Tests for Swagger UI documentation endpoint."""
def test_docs_returns_200(self, test_client):
"""Docs endpoint should return 200 OK."""
response = test_client.get("/docs")
assert response.status_code == 200
def test_docs_returns_html(self, test_client):
"""Docs endpoint should return HTML."""
response = test_client.get("/docs")
assert "text/html" in response.headers["content-type"]
class TestDriveMountedStatus:
"""Tests for drive mounted status in responses."""
@pytest.mark.parametrize("endpoint,mounted,expected", [
("/api/health", True, True),
("/api/health", False, False),
])
def test_drive_mounted_status(self, reset_runtime_config, mock_docker_mode, endpoint, mounted, expected):
"""Endpoints should return correct drive_mounted status."""
from controller.main import app
with mock.patch(
"controller.main.is_primary_drive_mounted", return_value=mounted
):
with TestClient(app) as client:
response = client.get(endpoint)
data = response.json()
assert data["drive_mounted"] is expected
def test_dashboard_shows_onboarding_when_drive_not_mounted(self, reset_runtime_config, mock_docker_mode):
"""Dashboard should show onboarding banner in native mode when drive is not mounted."""
from controller.main import app
with mock.patch(
"controller.main.is_primary_drive_mounted", return_value=False
):
with TestClient(app) as client:
response = client.get("/")
assert "Welcome to WROLPi" in response.text
def test_dashboard_shows_drive_not_mounted_banner_in_docker(self, reset_runtime_config, mock_docker_mode_enabled):
"""Dashboard should show info banner in Docker mode when drive is not mounted."""
from controller.main import app
with mock.patch(
"controller.main.is_primary_drive_mounted", return_value=False
):
with TestClient(app) as client:
response = client.get("/")
assert "Primary drive not mounted" in response.text