mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 10:51:36 -04:00
Introduce an onboarding system in the Controller that guides users through selecting and mounting their primary media drive. This is the fallback UI flow used when the main React app is unavailable. Key changes: - Add onboarding API endpoints and schemas (controller/api/onboarding.py) - Add onboarding logic to detect drives, handle already-mounted drives (needed for RPi which auto-mounts), and save primary drive selection to the controller config when no mounts are configured - Extend the Controller's index.html template with the onboarding UI - Skip root cert creation until onboarding is complete - Update first_startup.sh to support the onboarding/repair flow - Add comprehensive tests for onboarding logic and API
234 lines
9.4 KiB
Python
234 lines
9.4 KiB
Python
"""
|
|
Integration tests for Controller API endpoints.
|
|
"""
|
|
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_returns_200(self, test_client):
|
|
"""Dashboard endpoint should return 200 OK."""
|
|
response = test_client.get("/")
|
|
assert response.status_code == 200
|
|
|
|
def test_dashboard_returns_html(self, test_client):
|
|
"""Dashboard endpoint should return HTML."""
|
|
response = test_client.get("/")
|
|
assert "text/html" in response.headers["content-type"]
|
|
|
|
def test_dashboard_contains_title(self, test_client):
|
|
"""Dashboard should contain title."""
|
|
response = test_client.get("/")
|
|
# Title format is "Controller - {hostname} WROLPi"
|
|
assert b"Controller" in response.content
|
|
assert b"WROLPi" in response.content
|
|
|
|
def test_dashboard_contains_version(self, test_client):
|
|
"""Dashboard should contain version."""
|
|
response = test_client.get("/")
|
|
assert __version__.encode() in response.content
|
|
|
|
def test_dashboard_contains_status_cards(self, test_client):
|
|
"""Dashboard should contain status cards."""
|
|
response = test_client.get("/")
|
|
content = response.text
|
|
assert "CPU" in content
|
|
assert "Memory" in content
|
|
assert "Load" in content
|
|
assert "Storage" in content
|
|
|
|
def test_dashboard_contains_disks_section(self, test_client):
|
|
"""Dashboard should contain disks section."""
|
|
response = test_client.get("/")
|
|
content = response.text
|
|
assert "Disks" in content
|
|
assert 'id="disks"' in content
|
|
|
|
def test_dashboard_contains_services_section(self, test_client):
|
|
"""Dashboard should contain services section."""
|
|
response = test_client.get("/")
|
|
content = response.text
|
|
assert "Services" in content
|
|
assert 'id="services"' in content
|
|
|
|
def test_dashboard_contains_system_actions(self, test_client):
|
|
"""Dashboard should contain system actions section."""
|
|
response = test_client.get("/")
|
|
content = response.text
|
|
assert "System Actions" in content
|
|
assert "Restart All Services" in content
|
|
|
|
def test_dashboard_shows_docker_banner_when_dockerized(self, test_client_docker_mode):
|
|
"""Dashboard should show Docker mode banner when in Docker."""
|
|
response = test_client_docker_mode.get("/")
|
|
content = response.text
|
|
assert "Running in Docker mode" in content
|
|
|
|
def test_dashboard_no_docker_banner_when_not_dockerized(self, test_client):
|
|
"""Dashboard should not show Docker mode banner when not in Docker."""
|
|
response = test_client.get("/")
|
|
content = response.text
|
|
assert "Running in Docker mode" not in content
|
|
|
|
def test_dashboard_shows_boot_column_when_not_dockerized(self, test_client):
|
|
"""Dashboard should show Boot column in services table when not in Docker."""
|
|
response = test_client.get("/")
|
|
content = response.text
|
|
assert "<th>Boot</th>" in content
|
|
# Should have toggle switches
|
|
assert 'class="toggle"' in content
|
|
|
|
def test_dashboard_no_boot_column_when_dockerized(self, test_client_docker_mode):
|
|
"""Dashboard should not show Boot column in services table when in Docker."""
|
|
response = test_client_docker_mode.get("/")
|
|
content = response.text
|
|
assert "<th>Boot</th>" not in content
|
|
# The table body (rendered server-side) should not have boot toggles
|
|
# Note: JavaScript template literals may contain 'id="boot-' for dynamic updates,
|
|
# but the actual rendered <tbody> should not have boot toggles in Docker mode
|
|
import re
|
|
tbody_match = re.search(r'<tbody>(.*?)</tbody>', content, re.DOTALL)
|
|
if tbody_match:
|
|
tbody_content = tbody_match.group(1)
|
|
assert 'id="boot-' not in tbody_content
|
|
|
|
def test_dashboard_has_upgrade_banner_element(self, test_client):
|
|
"""Dashboard should have upgrade banner element for upgrade mode."""
|
|
response = test_client.get("/")
|
|
content = response.text
|
|
assert 'id="upgrade-banner"' in content
|
|
assert "Upgrade in Progress" in content
|
|
|
|
def test_dashboard_has_mount_modal(self, test_client):
|
|
"""Dashboard should have mount modal for disk mounting."""
|
|
response = test_client.get("/")
|
|
content = response.text
|
|
assert 'id="mount-modal"' in content
|
|
assert 'id="mount-point-input"' in content
|
|
assert 'id="mount-persist-checkbox"' in content
|
|
assert "Persistent (survive reboots)" in content
|
|
|
|
def test_dashboard_has_persist_column_in_disks_js(self, test_client):
|
|
"""Dashboard JavaScript should include Persist column in disk table."""
|
|
response = test_client.get("/")
|
|
content = response.text
|
|
# The loadDisks() function should create a table with Persist column
|
|
assert "<th>Persist</th>" in content
|
|
# Should have togglePersist function
|
|
assert "function togglePersist" in content
|
|
# Should fetch fstab entries
|
|
assert "api/disks/fstab" in content
|
|
|
|
|
|
class TestOpenAPIEndpoint:
|
|
"""Tests for OpenAPI documentation endpoint."""
|
|
|
|
def test_openapi_returns_200(self, test_client):
|
|
"""OpenAPI endpoint should return 200 OK."""
|
|
response = test_client.get("/openapi.json")
|
|
assert response.status_code == 200
|
|
|
|
def test_openapi_has_title(self, test_client):
|
|
"""OpenAPI schema should have correct title."""
|
|
response = test_client.get("/openapi.json")
|
|
data = response.json()
|
|
assert data["info"]["title"] == "WROLPi Controller"
|
|
|
|
def test_openapi_has_version(self, test_client):
|
|
"""OpenAPI schema should have version."""
|
|
response = test_client.get("/openapi.json")
|
|
data = response.json()
|
|
assert data["info"]["version"] == __version__
|
|
|
|
def test_openapi_has_api_paths(self, test_client):
|
|
"""OpenAPI schema should have API paths defined."""
|
|
response = test_client.get("/openapi.json")
|
|
data = response.json()
|
|
assert "/api/health" in data["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
|