From 7a0e838017b1de5e9eda20246036f71c14f77313 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sun, 12 Jul 2026 01:27:13 +0200 Subject: [PATCH] fix(profiles): remint empty Plex resource tokens --- lib/profiles/active_profile_binder.dart | 7 ++ test/profiles/active_profile_binder_test.dart | 85 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/lib/profiles/active_profile_binder.dart b/lib/profiles/active_profile_binder.dart index 7a7fb2e2..4a2b85c5 100644 --- a/lib/profiles/active_profile_binder.dart +++ b/lib/profiles/active_profile_binder.dart @@ -589,6 +589,13 @@ class ActiveProfileBinder { } try { servers = await _unwrapServerFetch(fetchOutcome); + if (servers.isEmpty) { + appLogger.w( + 'ActiveProfileBinder: cached local Plex token returned 0 servers for ${profile.displayName} — re-minting', + ); + await profileConnections.recordToken(profile.id, conn.id, ''); + userToken = null; + } } on MediaServerHttpException catch (e) { if (e.statusCode == 401 || e.statusCode == 403) { appLogger.w( diff --git a/test/profiles/active_profile_binder_test.dart b/test/profiles/active_profile_binder_test.dart index 63b517c5..7e790723 100644 --- a/test/profiles/active_profile_binder_test.dart +++ b/test/profiles/active_profile_binder_test.dart @@ -512,6 +512,73 @@ void main() { }); }); + test('local cached Plex token remints when resources returns zero servers', () async { + binder.dispose(); + multiServerProvider.dispose(); + + var resourceCalls = 0; + var switchCalls = 0; + final httpClient = MockClient((request) async { + if (request.url.path.endsWith('/resources')) { + resourceCalls++; + final body = resourceCalls == 1 ? [] : [_serverJson()]; + return http.Response(jsonEncode(body), 200, headers: {'content-type': 'application/json'}); + } + if (request.url.path.endsWith('/home/users/home-user-uuid/switch')) { + switchCalls++; + return http.Response( + jsonEncode({'authToken': 'fresh-user-token'}), + 201, + headers: {'content-type': 'application/json'}, + ); + } + fail('Unexpected request: ${request.method} ${request.url}'); + }); + + final recoveringManager = _FailThenSucceedPlexManager(); + manager = recoveringManager; + multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager)); + binder = ActiveProfileBinder( + activeProfile: activeProfile, + connections: connections, + profileConnections: profileConnections, + serverManager: manager, + multiServerProvider: multiServerProvider, + pinPrompt: (_, {String? errorMessage}) async => null, + shouldDeferInitialBind: (_) async => false, + plexAuth: PlexAuthService.forTesting(http: MediaServerHttpClient(client: httpClient)), + ); + + final account = PlexAccountConnection( + id: 'plex.account', + accountToken: 'account-token', + clientIdentifier: 'client-id', + accountLabel: 'Owner', + servers: [_server(accessToken: 'stale-server-token')], + createdAt: DateTime(2026, 1, 1), + ); + await connections.upsert(account); + final profile = await createActiveLocalProfile('local-plex-remint'); + await profileConnections.upsert( + ProfileConnection( + profileId: profile.id, + connectionId: account.id, + userToken: 'stale-user-token', + userIdentifier: 'home-user-uuid', + ), + ); + + await binder.rebindActive(); + + final row = await profileConnections.get(profile.id, account.id); + expect(resourceCalls, 2); + expect(switchCalls, 1); + expect(row?.userToken, 'fresh-user-token'); + expect(recoveringManager.calls, 2); + expect(activeProfile.lastBindingSucceeded, isTrue); + expect(multiServerProvider.onlineServerIds, ['srv-1']); + }); + group('rebind cycle semantics', () { test('queued same-id rebind settles once, after the last pass', () async { binder.dispose(); @@ -785,6 +852,24 @@ class _FailingPlexMultiServerManager extends MultiServerManager { } } +class _FailThenSucceedPlexManager extends MultiServerManager { + int calls = 0; + + @override + Future> refreshTokensForProfile( + PlexAccountConnection connection, { + Duration timeout = MediaServerTimeouts.perServerConnect, + }) async { + calls++; + if (calls == 1) return const {}; + final ids = connection.servers.map((server) => server.clientIdentifier).toSet(); + for (final id in ids) { + updateServerStatus(ServerId(id), true); + } + return ids; + } +} + class _BlockingMixedMultiServerManager extends MultiServerManager { final plexStarted = Completer(); final releasePlex = Completer();