feat: add typed qBittorrent scheduled polling

This commit is contained in:
Developer
2026-07-14 15:22:34 +00:00
parent eac9b5d33d
commit a9488af0b4
22 changed files with 2036 additions and 56 deletions
+16 -10
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import time
from types import SimpleNamespace
from typing import Any
from unittest.mock import patch
@@ -22,6 +23,7 @@ from media_library_viewer_api.widgets.sources import (
)
TEST_KEY = Fernet.generate_key().decode()
PROMQL_REQUIRED_ERROR = "promql is required"
@pytest.fixture(autouse=True)
@@ -479,6 +481,7 @@ def test_jellyfin_definition_has_now_playing_widget():
from media_library_viewer_api.integrations.registry import get_service_definition
definition = get_service_definition("jellyfin")
assert definition is not None
kinds = {wk.kind for wk in definition.widget_kinds}
assert "now_playing" in kinds
assert "activity" in kinds
@@ -543,7 +546,7 @@ async def test_prometheus_chart_adapter_requires_promql():
secrets={"grafana_api_key": "key"},
)
result = await adapter.fetch(service, "chart", {"promql": ""})
assert result == {"error": "promql is required"}
assert result == {"error": PROMQL_REQUIRED_ERROR}
@pytest.mark.asyncio
@@ -639,7 +642,7 @@ async def test_jellyfin_activity_shows_all_sessions():
@pytest.fixture
def widget_ref_client(monkeypatch):
def widget_ref_client(monkeypatch, request):
"""TestClient with an isolated SettingsStore + encryption key."""
monkeypatch.setenv(
"MANAGE_ENCRYPTION_KEY",
@@ -659,8 +662,8 @@ def widget_ref_client(monkeypatch):
app.dependency_overrides[get_settings_store] = get_store_override
client = TestClient(app)
yield client, store
app.dependency_overrides.pop(get_settings_store, None)
request.addfinalizer(lambda: app.dependency_overrides.pop(get_settings_store, None))
return client, store
def test_widget_reference_lifecycle(widget_ref_client):
@@ -936,7 +939,7 @@ async def test_prometheus_gauge_adapter_requires_promql():
secrets={"grafana_api_key": "key"},
)
result = await adapter.fetch(service, "gauge", {"promql": ""})
assert result == {"error": "promql is required"}
assert result == {"error": PROMQL_REQUIRED_ERROR}
@pytest.mark.asyncio
@@ -1038,7 +1041,7 @@ async def test_prometheus_mean_adapter_requires_promql():
secrets={"grafana_api_key": "key"},
)
result = await adapter.fetch(service, "mean", {"promql": ""})
assert result == {"error": "promql is required"}
assert result == {"error": PROMQL_REQUIRED_ERROR}
# ---------------------------------------------------------------------------
@@ -1139,8 +1142,8 @@ async def test_qbittorrent_active_filters_dl_ul_only():
@pytest.mark.asyncio
async def test_qbittorrent_speed_appends_and_returns_series(tmp_path):
"""Speed kind appends a sample and returns {series} with two labeled series."""
async def test_qbittorrent_speed_reads_samples_without_polling(tmp_path):
"""Speed kind reads persisted samples and never calls qBittorrent itself."""
from media_library_viewer_api.services.qbittorrent_store import QBITTORRENT_CONCERN, QbittorrentSampleStore
from media_library_viewer_api.services.service_data import ServiceDataHarness
from media_library_viewer_api.widgets.sources import QbittorrentWidgetSource
@@ -1163,15 +1166,18 @@ async def test_qbittorrent_speed_appends_and_returns_series(tmp_path):
patch("media_library_viewer_api.widgets.sources.QbittorrentSampleStore") as mock_store_cls,
):
mock_client.return_value.maindata.return_value = _fake_qbit_maindata()
# Wire the mock store to a real isolated store
# Wire the mock store to a real isolated store and seed headless data.
real_store = QbittorrentSampleStore(harness)
real_store.append("svc-speed", round(time.time()), 500000, 1000)
mock_store_cls.return_value = real_store
result = await adapter.fetch(service, "speed", {})
mock_client.assert_not_called()
assert "series" in result
labels = [s["label"] for s in result["series"]]
assert labels == ["download", "upload"]
# The sample just appended should be present
# The scheduler-supplied sample should be present.
dl_points = result["series"][0]["points"]
assert len(dl_points) >= 1
# timestamps multiplied by 1000 for JS epoch