feat(release): publish minimal self-contained Magent source

This commit is contained in:
Magent release tooling
2026-09-19 16:58:12 +12:00
commit 5fa5d45535
272 changed files with 79305 additions and 0 deletions
+201
View File
@@ -0,0 +1,201 @@
import json
import unittest
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
import httpx
from fastapi import FastAPI
from fastapi.testclient import TestClient
from backend.app import db
from backend.app.clients.jellystat import HistoryLimitError, JellystatClient, JellystatError
from backend.app.routers import admin, insights as router
from backend.app.services import insights
from backend.app.services.jellyfin_identity import link_user, linked_user_id
from backend.tests.test_backend_quality import TempDatabaseMixin
NOW = datetime(2026, 9, 7, 12, tzinfo=timezone.utc)
USER = {"username": "viewer", "role": "user", "auth_provider": "jellyfin", "jellyseerr_user_id": 42}
LIBRARIES = [{"Id": "movies", "CollectionType": "movies"}, {"Id": "music", "CollectionType": "music"}]
def play(id="play-1", **extra):
return {"Id": id, "UserId": "jf-viewer", "UserName": "PRIVATE NAME", "NowPlayingItemId": "movie-1",
"NowPlayingItemName": "Arrival", "ParentId": "movies", "PlaybackDuration": 3600,
"ActivityDateInserted": NOW.isoformat(), "RemoteEndPoint": "PRIVATE IP", "DeviceId": "PRIVATE DEVICE",
"PlayState": {"secret": "PRIVATE STATE"}, "Client": "Jellyfin Web", "PlayMethod": "DirectPlay", **extra}
class JellystatClientTests(unittest.IsolatedAsyncioTestCase):
async def history(self, handler, **kwargs):
original = httpx.AsyncClient
with patch("backend.app.clients.jellystat.httpx.AsyncClient", side_effect=lambda **options: original(transport=httpx.MockTransport(handler), **options)):
return await JellystatClient("http://jellystat/base", "secret-api-key").get_user_history(
kwargs.get("user_id", "jf-viewer"), NOW - timedelta(days=7), NOW)
async def test_paginates_and_sends_only_backend_identity_and_header_credential(self):
calls = []
def handler(request):
calls.append(request)
self.assertEqual(request.headers["x-api-token"], "secret-api-key")
self.assertNotIn("secret-api-key", str(request.url))
if request.url.path == "/base/api/getLibraries":
return httpx.Response(200, json=LIBRARIES)
self.assertEqual(request.method, "POST")
self.assertEqual(request.url.path, "/base/api/getUserHistory")
self.assertEqual(json.loads(request.content), {"userid": "jf-viewer"})
self.assertNotIn("search", request.url.params)
self.assertEqual(json.loads(request.url.params["filters"])[0]["field"], "ActivityDateInserted")
return httpx.Response(200, json={"pages": 2, "results": [play(request.url.params["page"])]})
history, libraries = await self.history(handler)
self.assertEqual(len(calls), 3)
self.assertEqual(len(history), 2)
self.assertEqual(libraries, LIBRARIES)
async def test_rejects_foreign_history_malformed_responses_and_overflow(self):
for payload, exception in [
({"pages": 1, "results": [play(UserId="someone-else")]}, JellystatError),
({"pages": 1, "results": [play(UserId=None)]}, JellystatError),
({"results": []}, JellystatError),
({"pages": 51, "results": []}, HistoryLimitError),
({"pages": 2, "results": []}, JellystatError),
({"pages": 0, "results": [play()]}, JellystatError),
]:
with self.subTest(payload=payload):
def handler(request):
return httpx.Response(200, json=LIBRARIES if request.method == "GET" else payload)
with self.assertRaises(exception):
await self.history(handler)
async def test_empty_history_is_valid(self):
result, _ = await self.history(lambda request: httpx.Response(200, json=LIBRARIES if request.method == "GET" else {"pages": 0, "results": []}))
self.assertEqual(result, [])
async def test_upstream_failure_is_sanitized(self):
with self.assertRaises(JellystatError) as error:
await self.history(lambda _: httpx.Response(401, text="private upstream error"))
self.assertNotIn("private", str(error.exception))
self.assertNotIn("secret-api-key", str(error.exception))
class SummaryTests(unittest.TestCase):
def test_units_media_counts_deduplication_ranges_streaks_and_privacy(self):
rows = [play(), play(), play("rewatch"),
play("episode", EpisodeId="e1", SeriesName="Severance", NowPlayingItemId="series-1", PlaybackDuration="1200",
ActivityDateInserted=(NOW - timedelta(days=1)).isoformat()),
play("episode-rewatch", EpisodeId="e1", SeriesName="Severance", NowPlayingItemId="series-1", PlaybackDuration=1200,
ActivityDateInserted=(NOW - timedelta(days=2)).isoformat()),
play("song", ParentId="music", NowPlayingItemId="song-1", PlaybackDuration=180),
play("old", ActivityDateInserted=(NOW - timedelta(days=8)).isoformat()),
play("zero", PlaybackDuration=0)]
data = insights.summarize(rows, LIBRARIES, NOW - timedelta(days=7), NOW)
self.assertEqual(data["summary"], {"minutes": 163, "plays": 5, "movies": 1, "episodes": 1,
"active_days": 3, "current_streak": 3, "longest_streak": 3})
self.assertAlmostEqual(sum(day["minutes"] for day in data["daily"]), 163)
self.assertEqual(data["top_titles"][0]["title"], "Arrival")
self.assertEqual(len(data["recent"]), 5)
self.assertNotIn("PRIVATE", json.dumps(data))
def test_invalid_durations_do_not_become_zero_or_nan(self):
for value in [-1, "NaN", "Infinity", "nonsense"]:
with self.subTest(value=value), self.assertRaises(JellystatError):
insights.summarize([play(PlaybackDuration=value)], LIBRARIES, NOW - timedelta(days=7), NOW)
def test_empty_history_has_zero_filled_days(self):
result = insights.summarize([], LIBRARIES, NOW - timedelta(days=7), NOW)
self.assertEqual(result["summary"]["minutes"], 0)
self.assertEqual(len(result["daily"]), 8)
self.assertEqual(result["summary"]["current_streak"], 0)
class InsightsIntegrationTests(TempDatabaseMixin, unittest.IsolatedAsyncioTestCase):
def setUp(self):
super().setUp()
insights._cache.clear()
db.create_user("viewer", "Test-Password123!", auth_provider="jellyfin")
self.runtime = SimpleNamespace(jellyfin_base_url="http://jellyfin", jellyfin_api_key="jf-key",
jellystat_base_url="http://jellystat", jellystat_api_key="stats-key")
async def test_identity_does_not_change_with_username_reuse_or_server_changes(self):
link_user("viewer", "jf-original", "http://jellyfin/")
link_user("viewer", "jf-replacement", "http://jellyfin")
self.assertEqual(linked_user_id("viewer", "http://jellyfin"), "jf-original")
self.assertIsNone(linked_user_id("viewer", "http://other-server"))
async def test_local_account_cannot_claim_same_name_and_verified_user_can_bootstrap(self):
with patch.object(insights.JellyfinClient, "get_users", new_callable=AsyncMock, return_value=[{"Id": "jf-viewer", "Name": "viewer"}]) as remote:
self.assertIsNone(await insights.resolve_identity({**USER, "auth_provider": "local"}, self.runtime))
remote.assert_not_called()
self.assertEqual(await insights.resolve_identity(USER, self.runtime), "jf-viewer")
self.assertEqual(await insights.resolve_identity(USER, self.runtime), "jf-viewer")
self.assertEqual(remote.await_count, 1)
async def test_requests_use_seerr_id_even_when_name_matches_another_user(self):
for request_id, seerr_id in [(1, 42), (2, 99)]:
db.upsert_request_cache(request_id, request_id, "movie", 2, "Request", 2026,
"viewer", "viewer", seerr_id, NOW.isoformat(), NOW.isoformat(), "{}")
report = insights.request_summary(USER, NOW - timedelta(days=7), NOW)
self.assertEqual(report["total"], 1)
self.assertEqual(report["recent"][0]["request_id"], 1)
async def test_cache_isolated_by_identity_period_and_configuration(self):
link_user("viewer", "jf-viewer", "http://jellyfin")
db.create_user("second", "Test-Password123!", auth_provider="jellyfin")
link_user("second", "jf-second", "http://jellyfin")
with patch.object(insights, "get_runtime_settings", return_value=self.runtime), \
patch.object(JellystatClient, "get_user_history", new_callable=AsyncMock, return_value=([], LIBRARIES)) as remote:
await insights.get_insights(USER, 7)
await insights.get_insights(USER, 7)
self.assertEqual(remote.await_count, 1)
await insights.get_insights({**USER, "username": "second"}, 7)
await insights.get_insights(USER, 30)
self.runtime.jellystat_api_key = "rotated-key"
await insights.get_insights(USER, 7)
self.assertEqual(remote.await_count, 4)
async def test_disabled_integration_never_calls_upstream(self):
self.runtime.jellystat_api_key = None
with patch.object(insights, "get_runtime_settings", return_value=self.runtime), \
patch.object(JellystatClient, "get_user_history", new_callable=AsyncMock) as remote:
result = await insights.get_insights(USER, 30)
self.assertEqual(result["state"], "not_configured")
self.assertIsNone(result["summary"])
remote.assert_not_called()
async def test_settings_mask_jellystat_credential(self):
db.set_setting("jellystat_api_key", "private-stats-key")
result = await admin.list_settings()
setting = next(row for row in result["settings"] if row["key"] == "jellystat_api_key")
self.assertTrue(setting["sensitive"])
self.assertTrue(setting["isSet"])
self.assertNotIn("private-stats-key", json.dumps(result))
class InsightsRouteTests(unittest.TestCase):
def app(self, authenticated=True):
app = FastAPI()
app.include_router(router.router)
if authenticated:
app.dependency_overrides[router.get_current_user] = lambda: {**USER, "features": {"stats": True}}
return TestClient(app)
def test_requires_authentication(self):
self.assertEqual(self.app(False).get("/insights").status_code, 401)
def test_query_accepts_period_and_forbids_identity_and_scope_overrides(self):
with patch.object(router, "get_insights", new_callable=AsyncMock, return_value={"state": "ready"}) as report:
client = self.app()
for days in [7, 30, 90, 365]:
response = client.get(f"/insights?days={days}")
self.assertEqual(response.status_code, 200, response.text)
self.assertEqual(response.headers["cache-control"], "no-store")
report.assert_awaited_with({**USER, "features": {"stats": True}}, 365)
for query in ["days=-1", "days=999999", "days=invalid", "userid=other", "user_id=other", "scope=server"]:
self.assertEqual(client.get(f"/insights?{query}").status_code, 422, query)
def test_errors_do_not_leak_upstream_details(self):
with patch.object(router, "get_insights", new_callable=AsyncMock, side_effect=JellystatError("PRIVATE key and URL")):
response = self.app().get("/insights")
self.assertEqual(response.status_code, 502)
self.assertNotIn("PRIVATE", response.text)