Add reviewed resolution for missing user identity links
Magent CI/CD / verify (push) Successful in 10m59s
Magent CI/CD / deploy-prod (push) Skipped
Magent CI/CD / deploy-beta (push) Successful in 1m29s

This commit is contained in:
2026-09-10 15:02:04 +12:00
parent b310e86f80
commit 77f2c1b42a
7 changed files with 231 additions and 6 deletions
+67
View File
@@ -44,6 +44,61 @@ class IdentityReviewTests(TempDatabaseMixin, unittest.IsolatedAsyncioTestCase):
def row(self, report):
return next(row for row in report["rows"] if row["user"]["id"] == self.user_id)
async def test_manual_selection_resolves_different_username_without_guessing(self):
self.jf['users'][0]['name'] = 'Different Jellyfin name'
before = review.read_snapshot()
report, _ = self.build()
self.assertEqual(self.row(report)['state'], 'unlinked')
report = review.build_report(before, self.jf, self.seerr, self.js, self.runtime, {self.user_id: JF})
self.assertTrue(self.row(report)['can_confirm'])
self.assertEqual(review.read_snapshot(), before)
review.save_confirmations(report, before, self.runtime, [self.user_id], ADMIN)
self.assertEqual(linked_user_id('Georgia', self.runtime.jellyfin_base_url), JF)
self.assertEqual(self.row(self.build()[0])['state'], 'confirmed')
async def test_manual_selection_cannot_replace_stored_or_confirmed_identity(self):
self.jf['users'].append({'id': OTHER, 'name': 'Other'})
self.seerr['users'].append({'id': 21, 'name': 'Other', 'jellyfin_id': OTHER})
self.js[OTHER] = {'state': 'matched', 'id': OTHER}
report, local = self.build()
review.save_confirmations(report, local, self.runtime, [self.user_id], ADMIN)
local = review.read_snapshot()
report = review.build_report(local, self.jf, self.seerr, self.js, self.runtime, {self.user_id: OTHER})
self.assertFalse(self.row(report)['can_confirm'])
with self.assertRaises(HTTPException):
review.save_confirmations(report, local, self.runtime, [self.user_id], ADMIN)
self.assertEqual(review.read_snapshot(), local)
async def test_manual_selection_checks_missing_ids_and_duplicate_owners(self):
self.jf['users'][0]['name'] = 'Different'
for state in ['missing', 'unavailable', 'not_configured']:
self.js[JF] = {'state': state}
report = review.build_report(review.read_snapshot(), self.jf, self.seerr, self.js, self.runtime, {self.user_id: JF})
self.assertFalse(self.row(report)['can_confirm'])
self.js[JF] = {'state': 'matched', 'id': JF}
db.create_user('Owner', 'password', auth_provider='local', jellyseerr_user_id=20)
report = review.build_report(review.read_snapshot(), self.jf, self.seerr, self.js, self.runtime, {self.user_id: JF})
self.assertEqual(self.row(report)['state'], 'conflict')
report = review.build_report(review.read_snapshot(), self.jf, self.seerr, self.js, self.runtime, {self.user_id: OTHER})
self.assertFalse(self.row(report)['can_confirm'])
with self.assertRaises(HTTPException) as error:
review.build_report(review.read_snapshot(), self.jf, self.seerr, self.js, self.runtime, {999: JF})
self.assertEqual(error.exception.status_code, 404)
async def test_resolution_rechecks_live_services_and_rejects_changed_selection(self):
with patch.object(review, 'jellyfin_directory', new_callable=AsyncMock, return_value=self.jf), \
patch.object(review, 'seerr_directory', new_callable=AsyncMock, return_value=self.seerr), \
patch.object(review.JellystatClient, 'check_user_ids', new_callable=AsyncMock, return_value=self.js):
before = review.read_snapshot()
preview = await review.resolve_identity(self.user_id, JF)
self.assertEqual(review.read_snapshot(), before)
with self.assertRaises(HTTPException) as error:
await review.resolve_identity(self.user_id, OTHER, preview['revision'], ADMIN)
self.assertEqual(error.exception.status_code, 409)
self.assertEqual(review.read_snapshot(), before)
result = await review.resolve_identity(self.user_id, JF, preview['revision'], ADMIN)
self.assertEqual(result['confirmed'], 1)
async def test_georgia_preview_is_read_only_and_uses_seerr_jellyfin_id(self):
before = review.read_snapshot()
report, _ = self.build()
@@ -258,6 +313,18 @@ class IdentityRouteTests(unittest.TestCase):
self.assertEqual(client.get("/admin/identities").status_code, status)
self.assertEqual(client.post("/admin/identities/confirm", json={"revision": "a" * 64, "user_ids": [1]}).status_code, status)
def test_resolution_requires_admin_and_strict_ids(self):
for endpoint in ['check', 'confirm']:
body = {'user_id': 1, 'jellyfin_user_id': JF}
if endpoint == 'confirm': body['revision'] = 'a' * 64
for role, status in [(None, 401), ('user', 403)]:
self.assertEqual(self.client(role).post('/admin/identities/resolve/' + endpoint, json=body).status_code, status)
for invalid in [{'user_id': True}, {'jellyfin_user_id': 'invalid'}, {'seerr_user_id': 22}]:
self.assertEqual(self.client('admin').post('/admin/identities/resolve/' + endpoint, json={**body, **invalid}).status_code, 422)
with patch.object(identities, 'resolve_identity', new_callable=AsyncMock, return_value={'row': {}}):
result = self.client('admin').post('/admin/identities/resolve/check', json={'user_id': 1, 'jellyfin_user_id': JF})
self.assertEqual(result.headers['cache-control'], 'no-store')
def test_no_store_and_no_browser_supplied_identity(self):
with patch.object(identities, "review_identities", new_callable=AsyncMock, return_value=({"rows": []}, {}, None)):
response = self.client("admin").get("/admin/identities")