sprint: Media Ops — shuttle status endpoint, graceful offline mode, 18 backend tests, shuttle detail in pipeline card, error states with retry, 13 frontend vitest tests
Deploy Dashboard (Main) / Backend Tests (push) Failing after 18s
Deploy Dashboard (Main) / Frontend Tests (push) Successful in 10s
Deploy Dashboard (Main) / Build Main Image (push) Failing after 7m26s
Deploy Dashboard (Main) / Deploy to Production (push) Has been skipped
Deploy Dashboard (Main) / Backend Tests (push) Failing after 18s
Deploy Dashboard (Main) / Frontend Tests (push) Successful in 10s
Deploy Dashboard (Main) / Build Main Image (push) Failing after 7m26s
Deploy Dashboard (Main) / Deploy to Production (push) Has been skipped
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import pytest
|
||||
from unittest.mock import patch, AsyncMock
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_health(test_app, admin_headers):
|
||||
async def test_get_health_ok(test_app, admin_headers):
|
||||
with patch("routers.media_mgmt.run_host_command", new_callable=AsyncMock) as mock_run:
|
||||
mock_run.return_value = (0, "All checks passed\n", "")
|
||||
response = test_app.get("/api/media/health", headers=admin_headers)
|
||||
@@ -12,16 +13,145 @@ async def test_get_health(test_app, admin_headers):
|
||||
assert data["exit_code"] == 0
|
||||
assert "All checks passed" in data["stdout"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_health_warning(test_app, admin_headers):
|
||||
with patch("routers.media_mgmt.run_host_command", new_callable=AsyncMock) as mock_run:
|
||||
mock_run.return_value = (1, "Disk >85%\n", "")
|
||||
response = test_app.get("/api/media/health", headers=admin_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "warning"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_health_error(test_app, admin_headers):
|
||||
with patch("routers.media_mgmt.run_host_command", new_callable=AsyncMock) as mock_run:
|
||||
mock_run.return_value = (2, "", "Immich API unreachable")
|
||||
response = test_app.get("/api/media/health", headers=admin_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "error"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_health_unavailable(test_app, admin_headers):
|
||||
with patch("routers.media_mgmt.run_host_command", new_callable=AsyncMock) as mock_run:
|
||||
mock_run.return_value = (999, "", "SSH connection refused")
|
||||
response = test_app.get("/api/media/health", headers=admin_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "unavailable"
|
||||
assert "NAS unreachable" in data["error"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_storage(test_app, admin_headers):
|
||||
with patch("routers.media_mgmt.run_host_command", new_callable=AsyncMock) as mock_run:
|
||||
mock_run.return_value = (0, '{"timestamp": "2026-07-11T05:37:57Z", "volume": {"path": "/volume1", "total": "14T", "used": "12T", "available": "2.2T", "used_pct": 85}}', "")
|
||||
mock_run.return_value = (
|
||||
0,
|
||||
'{"timestamp": "2026-07-11T05:37:57Z", "volume": {"path": "/volume1", "total": "14T", "used": "12T", "available": "2.2T", "used_pct": 85}}',
|
||||
"",
|
||||
)
|
||||
response = test_app.get("/api/media/storage", headers=admin_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "volume" in data
|
||||
assert data["volume"]["total"] == "14T"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_storage_unavailable(test_app, admin_headers):
|
||||
with patch("routers.media_mgmt.run_host_command", new_callable=AsyncMock) as mock_run:
|
||||
mock_run.return_value = (999, "", "SSH timeout")
|
||||
response = test_app.get("/api/media/storage", headers=admin_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "unavailable"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_storage_json_parse_error(test_app, admin_headers):
|
||||
with patch("routers.media_mgmt.run_host_command", new_callable=AsyncMock) as mock_run:
|
||||
mock_run.return_value = (0, "not valid json", "")
|
||||
response = test_app.get("/api/media/storage", headers=admin_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "error" in data
|
||||
assert "Failed to parse JSON" in data["error"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_shuttle_status_idle(test_app, admin_headers):
|
||||
"""Shuttle not running, no log file."""
|
||||
with patch("routers.media_mgmt.run_host_command", new_callable=AsyncMock) as mock_run:
|
||||
# First call: cat log, Second call: pgrep -f
|
||||
mock_run.side_effect = [
|
||||
(0, "NO_LOG", ""), # cat log
|
||||
(1, "", ""), # pgrep -f (not running)
|
||||
]
|
||||
response = test_app.get("/api/media/shuttle/status", headers=admin_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["running"] is False
|
||||
assert data["queue_depth"] == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_shuttle_status_running(test_app, admin_headers):
|
||||
"""Shuttle running with log content."""
|
||||
log_content = """=== Shuttle Started at 2026-07-11 10:00 ===
|
||||
--source /Volumes/SD_CARD --dest /Volumes/SSD
|
||||
Stage 1: copy
|
||||
[10:01] rsync: 1.2GB copied from SD_CARD to SSD
|
||||
Stage 2: verify
|
||||
[10:05] ✅ Verification passed — all files match
|
||||
[10:06] --stage ship --source /Volumes/SSD --dest naslan:/volume1/video
|
||||
"""
|
||||
with patch("routers.media_mgmt.run_host_command", new_callable=AsyncMock) as mock_run:
|
||||
mock_run.side_effect = [
|
||||
(0, log_content, ""), # cat log
|
||||
(0, "12345\n", ""), # pgrep -f (running)
|
||||
]
|
||||
response = test_app.get("/api/media/shuttle/status", headers=admin_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["running"] is True
|
||||
assert data["pid"] == "12345"
|
||||
assert data["queue_depth"] == 2 # --source entries in log
|
||||
assert data["last_transfer"] is not None
|
||||
assert "1.2GB" in data.get("last_transfer_size", "")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_shuttle_status_with_errors(test_app, admin_headers):
|
||||
"""Shuttle log has error lines."""
|
||||
log_content = """[10:01] rsync: OK
|
||||
[10:02] ❌ rsync: failed to transfer file XYZ
|
||||
[10:03] rsync: 500MB copied
|
||||
[10:04] ❌ connection reset by peer
|
||||
"""
|
||||
with patch("routers.media_mgmt.run_host_command", new_callable=AsyncMock) as mock_run:
|
||||
mock_run.side_effect = [
|
||||
(0, log_content, ""), # cat log
|
||||
(1, "", ""), # pgrep -f (not running)
|
||||
]
|
||||
response = test_app.get("/api/media/shuttle/status", headers=admin_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["recent_errors"]) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_shuttle_status_unavailable(test_app, admin_headers):
|
||||
with patch("routers.media_mgmt.run_host_command", new_callable=AsyncMock) as mock_run:
|
||||
mock_run.return_value = (999, "", "SSH timeout")
|
||||
response = test_app.get("/api/media/shuttle/status", headers=admin_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "unavailable"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_scan(test_app, admin_headers):
|
||||
with patch("routers.media_mgmt.run_host_command", new_callable=AsyncMock) as mock_run:
|
||||
@@ -33,6 +163,17 @@ async def test_run_scan(test_app, admin_headers):
|
||||
mock_run.assert_called_once()
|
||||
assert "scan-library.sh DJI-Action6" in mock_run.call_args[0][0]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_scan_unavailable(test_app, admin_headers):
|
||||
with patch("routers.media_mgmt.run_host_command", new_callable=AsyncMock) as mock_run:
|
||||
mock_run.return_value = (999, "", "SSH timeout")
|
||||
response = test_app.post("/api/media/maintenance/scan", headers=admin_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "unavailable"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_active_status(test_app, admin_headers):
|
||||
with patch("routers.media_mgmt.run_host_command", new_callable=AsyncMock) as mock_run:
|
||||
@@ -47,3 +188,41 @@ async def test_get_active_status(test_app, admin_headers):
|
||||
assert data["scan"]["running"] is True
|
||||
assert data["scan"]["pid"] == "1234"
|
||||
assert data["requeue"]["running"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_active_status_unavailable(test_app, admin_headers):
|
||||
with patch("routers.media_mgmt.run_host_command", new_callable=AsyncMock) as mock_run:
|
||||
mock_run.return_value = (999, "", "SSH timeout")
|
||||
response = test_app.get("/api/media/status", headers=admin_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "unavailable"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_logs_shuttle(test_app, admin_headers):
|
||||
"""Fetch shuttle logs successfully."""
|
||||
with patch("routers.media_mgmt.run_host_command", new_callable=AsyncMock) as mock_run:
|
||||
mock_run.return_value = (0, "line1\nline2\nline3\n", "")
|
||||
response = test_app.get("/api/media/logs/shuttle", headers=admin_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "line1" in data["content"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_logs_invalid_task(test_app, admin_headers):
|
||||
"""Invalid task name returns 400."""
|
||||
response = test_app.get("/api/media/logs/invalid-task", headers=admin_headers)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_logs_unavailable(test_app, admin_headers):
|
||||
with patch("routers.media_mgmt.run_host_command", new_callable=AsyncMock) as mock_run:
|
||||
mock_run.return_value = (999, "", "SSH timeout")
|
||||
response = test_app.get("/api/media/logs/scan", headers=admin_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "unavailable"
|
||||
|
||||
Reference in New Issue
Block a user