Rebase services-as-hub-ia onto mobile-responsive-parity
Combine both branches into a single coherent branch: - Full mobile responsive parity (useIsMobile, MobileCardRow, SheetForm, .mobile-touch-target, mobile cards, SheetForm forms, 44px targets, dirty-state confirm, TablePagination, refetchIntervalInBackground). - Full services-as-hub IA (data-driven nav, service-page tab skeleton, new service types, Authentik directory + messaging, named dashboards, legacy routes 404, Observability split, Jellyseerr absorbed). Enhancement: service tabs now use mobile-parity primitives: - MediaTab: MobileCardRow below md (title/size/HDR/library/year) + TablePagination; DataTable at md+ (desktop branch preserved). - FilesTab: MobileCardRow below md (name/type/size/modified) + handleRowClick; DataTable at md+. - ServicePage: SheetForm branch below md (open-on-mount, sticky header + save bar, cancel navigates back to /services, dirty-state guard). - Dashboard: single-column + section anchors below md (from mobile-parity) + empty-state CTA (from services-hub). - App.tsx: useIsMobile() replaces inline matchMedia (from mobile-parity) + data-driven useNavItems (from services-hub). - Backup tables (BackupAlerts/Jobs/Runs) already have MobileCardRow from mobile-parity; JobsTab inherits mobile behavior through its sub-components. Conflict resolutions: - Backend: entirely from services-hub (mobile didn't touch it). - Deleted pages (Media/FileBrowser/Actions/Users/UsersPage/Applications/ ObservabilityPage/BackupsPage + hooks/useUsers + tests): kept deleted (services-hub deleted them; content moved into service tabs). - New service-tabs/*: from services-hub, enhanced with mobile patterns. - App.tsx: services-hub's data-driven nav + mobile-parity's useIsMobile. - Dashboard.tsx: merged (services-hub CTA + mobile-parity sections/anchors). - ServicePage.tsx: services-hub's tab skeleton + mobile-parity's SheetForm. - Primitives (useIsMobile/mobile-card/sheet-form/etc.): from mobile-parity. 117 frontend tests pass (mobile-parity's 122 - 5 deleted page tests + services-hub's new tab/dashboard tests); 271 backend tests pass; lint/ build green both sides.
This commit is contained in:
+1
-152
@@ -14,8 +14,6 @@ from fastapi.testclient import TestClient
|
||||
from media_library_viewer_api.clients.ssh import CommandResult
|
||||
from media_library_viewer_api.dependencies import (
|
||||
get_jellyfin_client,
|
||||
get_jellyseerr_client,
|
||||
get_mail_queue,
|
||||
get_settings_store,
|
||||
get_ssh_client,
|
||||
get_user_id,
|
||||
@@ -70,38 +68,6 @@ def mock_jellyfin():
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_jellyseerr():
|
||||
"""Mock Jellyseerr client."""
|
||||
client = MagicMock()
|
||||
client.jellyfin_users.return_value = [
|
||||
{"id": "jf1", "username": "alex", "thumb": "/avatarproxy/alex", "email": "alex@example.com"},
|
||||
{"id": "jf2", "username": "sam", "thumb": "/avatarproxy/sam", "email": "sam@example.com"},
|
||||
]
|
||||
client.users.return_value = [
|
||||
{
|
||||
"id": 7,
|
||||
"username": "alex",
|
||||
"email": "alex@example.com",
|
||||
"avatar": "/avatarproxy/alex",
|
||||
"userType": 3,
|
||||
"permissions": 10,
|
||||
"requestCount": 3,
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"username": "sam",
|
||||
"email": "sam@example.com",
|
||||
"avatar": "/avatarproxy/sam",
|
||||
"userType": 2,
|
||||
"permissions": 32,
|
||||
"requestCount": 1,
|
||||
},
|
||||
]
|
||||
client.absolute_url.side_effect = lambda path: f"https://requests.example.com{path}"
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ssh():
|
||||
"""Mock SSH client."""
|
||||
@@ -132,10 +98,9 @@ def mock_ssh():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_client(mock_jellyfin, mock_jellyseerr, mock_ssh, tmp_path):
|
||||
def test_client(mock_jellyfin, mock_ssh, tmp_path):
|
||||
"""FastAPI test client with mocked dependencies."""
|
||||
app.dependency_overrides[get_jellyfin_client] = lambda: mock_jellyfin
|
||||
app.dependency_overrides[get_jellyseerr_client] = lambda: mock_jellyseerr
|
||||
app.dependency_overrides[get_ssh_client] = lambda: mock_ssh
|
||||
app.dependency_overrides[get_user_id] = lambda: "user123"
|
||||
store = SettingsStore(tmp_path / "settings.sqlite")
|
||||
@@ -293,122 +258,6 @@ class TestSettingsReset:
|
||||
assert len(store.list_machines()) == 0
|
||||
|
||||
|
||||
# --- Users ---
|
||||
|
||||
|
||||
class TestUsers:
|
||||
def test_users_list_enriched(self, test_client):
|
||||
response = test_client.get("/api/users")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] == 2
|
||||
assert data["jellyseerr_configured"] is True
|
||||
assert data["jellyseerr_available"] is True
|
||||
assert data["jellyseerr_error"] == ""
|
||||
|
||||
alex = next(item for item in data["items"] if item["username"] == "alex")
|
||||
assert alex["email"] == "alex@example.com"
|
||||
assert alex["email_source"] == "jellyseerr:user"
|
||||
assert alex["contactable"] is True
|
||||
assert alex["avatar"].startswith("https://requests.example.com/")
|
||||
assert alex["avatar_source"] == "jellyseerr:user"
|
||||
assert alex["permissions"] == 10
|
||||
assert alex["permissions_label"] == "admin, manage_users"
|
||||
assert alex["role"] == "admin"
|
||||
assert alex["user_type_label"] == "jellyfin"
|
||||
assert alex["request_count"] == 3
|
||||
assert "name=jellyfin" in alex["source_summary"]
|
||||
assert "email=jellyseerr:user" in alex["source_summary"]
|
||||
|
||||
sam = next(item for item in data["items"] if item["username"] == "sam")
|
||||
assert sam["role"] == "requester"
|
||||
assert sam["user_type_label"] == "local"
|
||||
assert sam["email"] == "sam@example.com"
|
||||
|
||||
def test_users_message_status(self, test_client):
|
||||
mail_queue = MagicMock()
|
||||
mail_queue.status.return_value = {
|
||||
"state": "idle",
|
||||
"worker_running": True,
|
||||
"stop_requested": False,
|
||||
"pending_count": 0,
|
||||
"active_request_id": None,
|
||||
"last_request_id": None,
|
||||
"last_result": None,
|
||||
"last_error": "",
|
||||
"last_error_at": None,
|
||||
"last_success_at": None,
|
||||
"last_activity_at": None,
|
||||
"sent_count": 0,
|
||||
"failed_count": 0,
|
||||
}
|
||||
app.dependency_overrides[get_mail_queue] = lambda: mail_queue
|
||||
try:
|
||||
response = test_client.get("/api/users/message/status")
|
||||
finally:
|
||||
app.dependency_overrides.pop(get_mail_queue, None)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["state"] == "idle"
|
||||
assert response.json()["pending_count"] == 0
|
||||
|
||||
def test_users_message_is_queued(self, test_client):
|
||||
mail_queue = MagicMock()
|
||||
mail_queue.status.return_value = {
|
||||
"state": "idle",
|
||||
"worker_running": True,
|
||||
"stop_requested": False,
|
||||
"pending_count": 0,
|
||||
"active_request_id": None,
|
||||
"last_request_id": None,
|
||||
"last_result": None,
|
||||
"last_error": "",
|
||||
"last_error_at": None,
|
||||
"last_success_at": None,
|
||||
"last_activity_at": None,
|
||||
"sent_count": 0,
|
||||
"failed_count": 0,
|
||||
}
|
||||
mail_queue.enqueue.return_value = "mail-123456"
|
||||
app.dependency_overrides[get_mail_queue] = lambda: mail_queue
|
||||
settings = SimpleNamespace(
|
||||
smtp_host="smtp.example.com",
|
||||
smtp_port=587,
|
||||
smtp_username="mailer@example.com",
|
||||
smtp_password="secret",
|
||||
smtp_from_address="mailer@example.com",
|
||||
smtp_from_name="Manage",
|
||||
smtp_use_tls=True,
|
||||
smtp_use_ssl=False,
|
||||
smtp_timeout=15,
|
||||
)
|
||||
|
||||
try:
|
||||
with patch("media_library_viewer_api.routers.users_impl.get_settings", return_value=settings):
|
||||
response = test_client.post(
|
||||
"/api/users/message",
|
||||
data={
|
||||
"recipient_ids": json.dumps(["jf1", "jf2"]),
|
||||
"subject": "Hello team",
|
||||
"html_body": "<p>Hi there</p>",
|
||||
"text_body": "Hi there",
|
||||
},
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.pop(get_mail_queue, None)
|
||||
|
||||
assert response.status_code == 202
|
||||
data = response.json()
|
||||
assert data["status"] == "queued"
|
||||
assert data["request_id"] == "mail-123456"
|
||||
assert data["recipient_count"] == 2
|
||||
assert data["attachment_count"] == 0
|
||||
mail_queue.enqueue.assert_called_once()
|
||||
kwargs = mail_queue.enqueue.call_args.kwargs
|
||||
assert kwargs["recipients"] == ["alex@example.com", "sam@example.com"]
|
||||
assert kwargs["subject"] == "Hello team"
|
||||
assert kwargs["settings"] is settings
|
||||
|
||||
|
||||
# --- Files ---
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user