feat(service-storage-harness): slice 2 — qbit widgets + LineSeriesChart extract
This commit is contained in:
@@ -988,3 +988,190 @@ async def test_prometheus_mean_adapter_requires_promql():
|
||||
)
|
||||
result = await adapter.fetch(service, "mean", {"promql": ""})
|
||||
assert result == {"error": "promql is required"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# qBittorrent widget source adapter
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _fake_qbit_maindata():
|
||||
"""Return a mock maindata response (server_state + torrents dict)."""
|
||||
return {
|
||||
"server_state": {"dl_info_speed": 500000, "up_info_speed": 100000},
|
||||
"torrents": {
|
||||
"h1": {
|
||||
"name": "Movie.mkv",
|
||||
"state": "downloading",
|
||||
"size": 1000,
|
||||
"progress": 0.5,
|
||||
"dlspeed": 500,
|
||||
"upspeed": 10,
|
||||
},
|
||||
"h2": {
|
||||
"name": "Show.mkv",
|
||||
"state": "uploading",
|
||||
"size": 2000,
|
||||
"progress": 1.0,
|
||||
"dlspeed": 0,
|
||||
"upspeed": 100,
|
||||
},
|
||||
"h3": {
|
||||
"name": "Queued",
|
||||
"state": "queuedDL",
|
||||
"size": 3000,
|
||||
"progress": 0.0,
|
||||
"dlspeed": 0,
|
||||
"upspeed": 0,
|
||||
},
|
||||
"h4": {
|
||||
"name": "Paused",
|
||||
"state": "pausedDL",
|
||||
"size": 4000,
|
||||
"progress": 0.3,
|
||||
"dlspeed": 0,
|
||||
"upspeed": 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_qbittorrent_totals_counts_all_torrents():
|
||||
"""Totals kind returns count of all listed items + by_state breakdown."""
|
||||
from media_library_viewer_api.widgets.sources import QbittorrentWidgetSource
|
||||
|
||||
adapter = QbittorrentWidgetSource()
|
||||
service = ServiceRecord(
|
||||
id="svc-1",
|
||||
service_type="qbittorrent",
|
||||
name="qbit",
|
||||
config={"base_url": "http://qbit:8080", "timeout_seconds": 5},
|
||||
secrets={"username": "admin", "password": "pass"},
|
||||
)
|
||||
with patch("media_library_viewer_api.widgets.sources.QbittorrentClient") as mock_client:
|
||||
mock_client.return_value.maindata.return_value = _fake_qbit_maindata()
|
||||
result = await adapter.fetch(service, "totals", {})
|
||||
|
||||
assert result["total"] == 4
|
||||
assert result["by_state"]["downloading"] == 1
|
||||
assert result["by_state"]["uploading"] == 1
|
||||
assert result["by_state"]["queuedDL"] == 1
|
||||
assert result["by_state"]["pausedDL"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_qbittorrent_active_filters_dl_ul_only():
|
||||
"""Active kind returns only downloading/uploading torrents (Q3)."""
|
||||
from media_library_viewer_api.widgets.sources import QbittorrentWidgetSource
|
||||
|
||||
adapter = QbittorrentWidgetSource()
|
||||
service = ServiceRecord(
|
||||
id="svc-1",
|
||||
service_type="qbittorrent",
|
||||
name="qbit",
|
||||
config={"base_url": "http://qbit:8080", "timeout_seconds": 5},
|
||||
secrets={"username": "admin", "password": "pass"},
|
||||
)
|
||||
with patch("media_library_viewer_api.widgets.sources.QbittorrentClient") as mock_client:
|
||||
mock_client.return_value.maindata.return_value = _fake_qbit_maindata()
|
||||
result = await adapter.fetch(service, "active", {})
|
||||
|
||||
active = result["torrents"]
|
||||
assert len(active) == 2
|
||||
names = [t["name"] for t in active]
|
||||
assert "Movie.mkv" in names
|
||||
assert "Show.mkv" in names
|
||||
# Queued and paused are excluded
|
||||
assert "Queued" not in names
|
||||
assert "Paused" not in names
|
||||
|
||||
|
||||
@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."""
|
||||
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
|
||||
|
||||
# Isolated harness so we don't pollute the real DB
|
||||
harness = ServiceDataHarness(base_dir=str(tmp_path))
|
||||
harness.register(QBITTORRENT_CONCERN)
|
||||
harness.run_migrations()
|
||||
|
||||
adapter = QbittorrentWidgetSource()
|
||||
service = ServiceRecord(
|
||||
id="svc-speed",
|
||||
service_type="qbittorrent",
|
||||
name="qbit",
|
||||
config={"base_url": "http://qbit:8080", "timeout_seconds": 5},
|
||||
secrets={"username": "admin", "password": "pass"},
|
||||
)
|
||||
with (
|
||||
patch("media_library_viewer_api.widgets.sources.QbittorrentClient") as mock_client,
|
||||
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
|
||||
real_store = QbittorrentSampleStore(harness)
|
||||
mock_store_cls.return_value = real_store
|
||||
result = await adapter.fetch(service, "speed", {})
|
||||
|
||||
assert "series" in result
|
||||
labels = [s["label"] for s in result["series"]]
|
||||
assert labels == ["download", "upload"]
|
||||
# The sample just appended should be present
|
||||
dl_points = result["series"][0]["points"]
|
||||
assert len(dl_points) >= 1
|
||||
# timestamps multiplied by 1000 for JS epoch
|
||||
assert dl_points[-1]["v"] == 500000
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_qbittorrent_adapter_missing_service():
|
||||
from media_library_viewer_api.widgets.sources import QbittorrentWidgetSource
|
||||
|
||||
adapter = QbittorrentWidgetSource()
|
||||
result = await adapter.fetch(None, "totals", {})
|
||||
assert "error" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_qbittorrent_adapter_missing_credentials():
|
||||
from media_library_viewer_api.widgets.sources import QbittorrentWidgetSource
|
||||
|
||||
adapter = QbittorrentWidgetSource()
|
||||
service = ServiceRecord(
|
||||
id="s",
|
||||
service_type="qbittorrent",
|
||||
name="qbit",
|
||||
config={"base_url": "http://qbit:8080"},
|
||||
secrets={"username": "", "password": ""},
|
||||
)
|
||||
result = await adapter.fetch(service, "totals", {})
|
||||
assert "error" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_qbittorrent_adapter_timeout():
|
||||
"""A timeout returns {error} rather than raising."""
|
||||
from media_library_viewer_api.widgets.sources import QbittorrentWidgetSource
|
||||
|
||||
adapter = QbittorrentWidgetSource()
|
||||
service = ServiceRecord(
|
||||
id="s",
|
||||
service_type="qbittorrent",
|
||||
name="qbit",
|
||||
config={"base_url": "http://qbit:8080", "timeout_seconds": 1},
|
||||
secrets={"username": "admin", "password": "pass"},
|
||||
)
|
||||
with patch("media_library_viewer_api.widgets.sources.QbittorrentClient") as mock_client:
|
||||
import asyncio as _asyncio
|
||||
|
||||
async def _slow(*a, **kw):
|
||||
await _asyncio.sleep(10)
|
||||
|
||||
# Make to_thread hang so wait_for times out
|
||||
mock_client.return_value.maindata.side_effect = lambda: (_ for _ in ()).throw(TimeoutError())
|
||||
result = await adapter.fetch(service, "totals", {})
|
||||
assert "error" in result
|
||||
|
||||
Reference in New Issue
Block a user