fix(remote): live auth-context refresh + lifecycle races
Keep a running companion-remote host's crypto identity in sync with the active profile: subscribe to PlexHome/connection/join streams and rebuild auth contexts (restarting the broadcast when they change) so a removed home user or revoked borrowed connection stops controlling the host. Serialize host start/stop/crypto-rebuild through a lifecycle lock, clean up peer subscriptions before re-listening, and guard the replaced-client onDone against clobbering the new client's session. Drop the context.mounted guards that aborted an app-level host start, fix the DiscoveryView init-flash/stuck-on-throw, and delete the dead initialize*Crypto helpers.
This commit is contained in:
@@ -3,10 +3,13 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connection/connection_registry.dart';
|
||||
import '../focus/key_event_utils.dart';
|
||||
import '../media/ids.dart';
|
||||
import '../media/media_server_client.dart';
|
||||
import '../profiles/active_profile_provider.dart';
|
||||
import '../profiles/plex_home_service.dart';
|
||||
import '../profiles/profile_connection_registry.dart';
|
||||
import '../providers/companion_remote_provider.dart';
|
||||
import '../providers/discover_provider.dart';
|
||||
import '../providers/hidden_libraries_provider.dart';
|
||||
@@ -159,7 +162,21 @@ class _ProfileSessionScreenState extends State<ProfileSessionScreen> {
|
||||
),
|
||||
ChangeNotifierProvider(create: (context) => PlaybackStateProvider()),
|
||||
ChangeNotifierProvider(create: (context) => WatchTogetherProvider()),
|
||||
ChangeNotifierProvider(create: (context) => CompanionRemoteProvider()),
|
||||
ChangeNotifierProvider(
|
||||
create: (context) {
|
||||
final provider = CompanionRemoteProvider();
|
||||
// Keep a running host's crypto identity live: a home user
|
||||
// removed or a borrowed connection revoked mid-session must
|
||||
// stop controlling the broadcast.
|
||||
provider.bindProfileServices(
|
||||
connections: context.read<ConnectionRegistry>(),
|
||||
activeProfile: context.read<ActiveProfileProvider>(),
|
||||
profileConnections: context.read<ProfileConnectionRegistry>(),
|
||||
plexHome: context.read<PlexHomeService>(),
|
||||
);
|
||||
return provider;
|
||||
},
|
||||
),
|
||||
],
|
||||
child: _ProfileSessionNavigator(
|
||||
isOfflineMode: widget.isOfflineMode,
|
||||
|
||||
@@ -12,6 +12,7 @@ import '../models/companion_remote/remote_session.dart';
|
||||
import '../models/plex/plex_home.dart';
|
||||
import '../profiles/active_plex_identity.dart';
|
||||
import '../profiles/active_profile_provider.dart';
|
||||
import '../profiles/plex_home_service.dart';
|
||||
import '../profiles/profile.dart';
|
||||
import '../profiles/profile_connection_registry.dart';
|
||||
import '../services/companion_remote/companion_remote_peer_service.dart';
|
||||
@@ -50,6 +51,21 @@ class CompanionRemoteProvider with ChangeNotifier, DisposableChangeNotifierMixin
|
||||
List<RemoteAuthContext> _authContexts = const [];
|
||||
String? _cryptoProfileId;
|
||||
|
||||
// Profile-scoped services watched so a running host's crypto identity tracks
|
||||
// home-user / connection changes (a removed home user or revoked borrowed
|
||||
// connection must stop controlling an already-broadcasting host).
|
||||
ConnectionRegistry? _boundConnections;
|
||||
ActiveProfileProvider? _boundActiveProfile;
|
||||
ProfileConnectionRegistry? _boundProfileConnections;
|
||||
PlexHomeService? _boundPlexHome;
|
||||
final List<StreamSubscription<void>> _profileServiceSubs = [];
|
||||
bool _authRefreshScheduled = false;
|
||||
|
||||
// Serializes host start/stop/crypto-rebuild so overlapping lifecycle calls
|
||||
// (a user action and a live auth-context refresh) can't interleave and
|
||||
// corrupt the peer service.
|
||||
Future<void> _lifecycleLock = Future<void>.value();
|
||||
|
||||
int get reconnectAttempts => _reconnectAttempts;
|
||||
|
||||
StreamSubscription<RemoteCommand>? _commandSubscription;
|
||||
@@ -109,101 +125,86 @@ class CompanionRemoteProvider with ChangeNotifier, DisposableChangeNotifierMixin
|
||||
safeNotifyListeners();
|
||||
}
|
||||
|
||||
/// Initialize crypto context from Plex home data plus the active profile's
|
||||
/// connection. The clientIdentifier is the parent Plex account's
|
||||
/// `clientIdentifier` (used as the LAN device id) and the userUUID is the
|
||||
/// active home user's uuid (used to scope per-user LAN traffic).
|
||||
Future<bool> initializeCrypto({
|
||||
required PlexHome? home,
|
||||
required PlexAccountConnection? account,
|
||||
required Profile? activeProfile,
|
||||
String? activeUserUuid,
|
||||
}) async {
|
||||
if (home == null || home.adminUser == null) {
|
||||
appLogger.w('CompanionRemote: Cannot init crypto — no home data');
|
||||
return false;
|
||||
}
|
||||
if (account == null) {
|
||||
appLogger.w('CompanionRemote: Cannot init crypto — no Plex account');
|
||||
return false;
|
||||
}
|
||||
/// Bind the profile-scoped services whose changes must keep a running host's
|
||||
/// crypto identity current: a removed home user or revoked borrowed
|
||||
/// connection has to stop controlling an already-broadcasting host. Wired
|
||||
/// once when the provider is created; a second call is a no-op.
|
||||
void bindProfileServices({
|
||||
required ConnectionRegistry connections,
|
||||
required ActiveProfileProvider activeProfile,
|
||||
required ProfileConnectionRegistry profileConnections,
|
||||
required PlexHomeService plexHome,
|
||||
}) {
|
||||
if (_boundActiveProfile != null) return;
|
||||
_boundConnections = connections;
|
||||
_boundActiveProfile = activeProfile;
|
||||
_boundProfileConnections = profileConnections;
|
||||
_boundPlexHome = plexHome;
|
||||
|
||||
try {
|
||||
final auth = RemoteAuthService.instance;
|
||||
final homeSecret = await auth.deriveHomeSecretFromHome(home);
|
||||
final discoveryKey = await auth.deriveDiscoveryKey(homeSecret);
|
||||
final userUuid = activeUserUuid ?? activeProfile?.plexHomeUserUuid ?? home.adminUser!.uuid;
|
||||
final allowedUserUuids = {
|
||||
for (final user in home.users)
|
||||
if (user.uuid.isNotEmpty) user.uuid,
|
||||
if (userUuid.isNotEmpty) userUuid,
|
||||
}.toList();
|
||||
_authContexts = [
|
||||
RemoteAuthContext(
|
||||
id: auth.computeAuthContextId(homeSecret),
|
||||
backend: 'plex',
|
||||
connectionId: account.id,
|
||||
homeSecret: homeSecret,
|
||||
discoveryKey: discoveryKey,
|
||||
clientIdentifier: account.clientIdentifier.isNotEmpty ? account.clientIdentifier : account.id,
|
||||
userUuid: userUuid,
|
||||
allowedUserUuids: allowedUserUuids,
|
||||
),
|
||||
];
|
||||
_cryptoProfileId = activeProfile?.id;
|
||||
|
||||
appLogger.d('CompanionRemote: Crypto context initialized');
|
||||
return true;
|
||||
} catch (e) {
|
||||
appLogger.e('CompanionRemote: Failed to init crypto', error: e);
|
||||
return false;
|
||||
}
|
||||
_profileServiceSubs.add(plexHome.stream.listen((_) => _scheduleAuthContextRefresh()));
|
||||
_profileServiceSubs.add(connections.watchConnections().listen((_) => _scheduleAuthContextRefresh()));
|
||||
_profileServiceSubs.add(profileConnections.watchAll().listen((_) => _scheduleAuthContextRefresh()));
|
||||
activeProfile.addListener(_scheduleAuthContextRefresh);
|
||||
}
|
||||
|
||||
Future<bool> initializeJellyfinCrypto({
|
||||
required JellyfinConnection connection,
|
||||
required Profile? activeProfile,
|
||||
}) async {
|
||||
if (connection.accessToken.isEmpty || connection.userId.isEmpty || connection.serverMachineId.isEmpty) {
|
||||
appLogger.w('CompanionRemote: Cannot init Jellyfin crypto — incomplete connection');
|
||||
return false;
|
||||
}
|
||||
/// Coalesce a burst of stream events into a single rebuild. Only a running
|
||||
/// host needs live identity updates — discovery/remote sessions resolve
|
||||
/// crypto at connect time.
|
||||
void _scheduleAuthContextRefresh() {
|
||||
if (!isHostServerRunning) return;
|
||||
if (_authRefreshScheduled) return;
|
||||
_authRefreshScheduled = true;
|
||||
scheduleMicrotask(() {
|
||||
_authRefreshScheduled = false;
|
||||
unawaited(_refreshHostAuthContexts());
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
final auth = RemoteAuthService.instance;
|
||||
final homeSecret = await auth.deriveJellyfinSecret(
|
||||
serverMachineId: connection.serverMachineId,
|
||||
userId: connection.userId,
|
||||
Future<void> _refreshHostAuthContexts() async {
|
||||
final connections = _boundConnections;
|
||||
final activeProfile = _boundActiveProfile;
|
||||
final profileConnections = _boundProfileConnections;
|
||||
final plexHome = _boundPlexHome;
|
||||
if (connections == null || activeProfile == null || profileConnections == null || plexHome == null) {
|
||||
return;
|
||||
}
|
||||
if (!isHostServerRunning) return;
|
||||
|
||||
await _serializeLifecycle(() async {
|
||||
if (!isHostServerRunning) return;
|
||||
final ok = await _ensureCryptoReadyLocked(
|
||||
null,
|
||||
connections: connections,
|
||||
activeProfile: activeProfile,
|
||||
profileConnections: profileConnections,
|
||||
plexHomeForConnection: plexHome.materializePlexHomeForConnection,
|
||||
);
|
||||
final discoveryKey = await auth.deriveDiscoveryKey(homeSecret);
|
||||
_authContexts = [
|
||||
RemoteAuthContext(
|
||||
id: auth.computeAuthContextId(homeSecret),
|
||||
backend: 'jellyfin',
|
||||
connectionId: connection.id,
|
||||
homeSecret: homeSecret,
|
||||
discoveryKey: discoveryKey,
|
||||
clientIdentifier: connection.deviceId.isNotEmpty ? connection.deviceId : connection.id,
|
||||
userUuid: connection.userId,
|
||||
allowedUserUuids: [connection.userId],
|
||||
),
|
||||
];
|
||||
_cryptoProfileId = activeProfile?.id;
|
||||
// Unchanged identities leave the host running (no restart). When they
|
||||
// change, the rebuild tore the host down — bring it back so the new set
|
||||
// is what's broadcasting. When every identity is gone the host stays
|
||||
// down by design (`ok` is false).
|
||||
if (ok && !isHostServerRunning) {
|
||||
await _startHostServerLocked();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
appLogger.d('CompanionRemote: Jellyfin crypto context initialized');
|
||||
return true;
|
||||
} catch (e) {
|
||||
appLogger.e('CompanionRemote: Failed to init Jellyfin crypto', error: e);
|
||||
return false;
|
||||
}
|
||||
/// Run [action] after every previously-queued lifecycle action settles, so
|
||||
/// start/stop/crypto-rebuild never overlap. The chain survives a throwing
|
||||
/// action (errors surface to that action's caller, not the next in line).
|
||||
Future<T> _serializeLifecycle<T>(Future<T> Function() action) {
|
||||
final result = _lifecycleLock.then((_) => action());
|
||||
_lifecycleLock = result.then((_) {}, onError: (_) {});
|
||||
return result;
|
||||
}
|
||||
|
||||
RemoteAuthContext? get _primaryAuthContext => _authContexts.isEmpty ? null : _authContexts.first;
|
||||
|
||||
bool get isCryptoReady => _authContexts.isNotEmpty;
|
||||
|
||||
/// Convenience: ensure crypto is initialized for every remote identity
|
||||
/// attached to the active profile.
|
||||
/// Ensure crypto is initialized for every remote identity attached to the
|
||||
/// active profile. Serialized against host start/stop so a live refresh and
|
||||
/// a user-driven start can't interleave.
|
||||
/// Returns true if crypto is ready (already initialized or just initialized).
|
||||
Future<bool> ensureCryptoReady(
|
||||
PlexHome? home, {
|
||||
@@ -213,6 +214,28 @@ class CompanionRemoteProvider with ChangeNotifier, DisposableChangeNotifierMixin
|
||||
ActivePlexIdentity? identity,
|
||||
PlexAccountConnection? account,
|
||||
PlexHomeResolver? plexHomeForConnection,
|
||||
}) {
|
||||
return _serializeLifecycle(
|
||||
() => _ensureCryptoReadyLocked(
|
||||
home,
|
||||
connections: connections,
|
||||
activeProfile: activeProfile,
|
||||
profileConnections: profileConnections,
|
||||
identity: identity,
|
||||
account: account,
|
||||
plexHomeForConnection: plexHomeForConnection,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _ensureCryptoReadyLocked(
|
||||
PlexHome? home, {
|
||||
required ConnectionRegistry connections,
|
||||
required ActiveProfileProvider activeProfile,
|
||||
required ProfileConnectionRegistry profileConnections,
|
||||
ActivePlexIdentity? identity,
|
||||
PlexAccountConnection? account,
|
||||
PlexHomeResolver? plexHomeForConnection,
|
||||
}) async {
|
||||
await activeProfile.initialize();
|
||||
final profile = activeProfile.active;
|
||||
@@ -404,8 +427,10 @@ class CompanionRemoteProvider with ChangeNotifier, DisposableChangeNotifierMixin
|
||||
}
|
||||
|
||||
Future<void> _prepareForCryptoRebuild() async {
|
||||
// Always invoked from inside the lifecycle lock — use the unlocked stop so
|
||||
// we don't deadlock on our own chain.
|
||||
if (isInSession || isHostServerRunning) {
|
||||
await stopHostServer();
|
||||
await _stopHostServerLocked();
|
||||
} else {
|
||||
stopDiscovery();
|
||||
_cleanupSubscriptions();
|
||||
@@ -421,17 +446,19 @@ class CompanionRemoteProvider with ChangeNotifier, DisposableChangeNotifierMixin
|
||||
/// Fully tear down network/session state and forget derived crypto material.
|
||||
/// Used by logout so an app-level provider surviving route replacement does
|
||||
/// not keep broadcasting with the previous Plex Home identity.
|
||||
Future<void> resetForLogout() async {
|
||||
_reconnectTimer?.cancel();
|
||||
_reconnectAttempts = 0;
|
||||
_lastHostAddresses = null;
|
||||
_lastHostClientId = null;
|
||||
_lastAuthContextId = null;
|
||||
await stopHostServer();
|
||||
stopDiscovery();
|
||||
_clearCryptoContext();
|
||||
RemoteAuthService.instance.clearCache();
|
||||
safeNotifyListeners();
|
||||
Future<void> resetForLogout() {
|
||||
return _serializeLifecycle(() async {
|
||||
_reconnectTimer?.cancel();
|
||||
_reconnectAttempts = 0;
|
||||
_lastHostAddresses = null;
|
||||
_lastHostClientId = null;
|
||||
_lastAuthContextId = null;
|
||||
await _stopHostServerLocked();
|
||||
stopDiscovery();
|
||||
_clearCryptoContext();
|
||||
RemoteAuthService.instance.clearCache();
|
||||
safeNotifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
@@ -446,7 +473,9 @@ class CompanionRemoteProvider with ChangeNotifier, DisposableChangeNotifierMixin
|
||||
@visibleForTesting
|
||||
List<String> get debugCryptoConnectionIds => _authContexts.map((context) => context.connectionId).toList();
|
||||
|
||||
Future<void> startHostServer() async {
|
||||
Future<void> startHostServer() => _serializeLifecycle(_startHostServerLocked);
|
||||
|
||||
Future<void> _startHostServerLocked() async {
|
||||
if (_peerService?.isServerRunning == true) return;
|
||||
if (!isCryptoReady) {
|
||||
appLogger.w('CompanionRemote: Cannot start host — crypto not initialized');
|
||||
@@ -494,7 +523,9 @@ class CompanionRemoteProvider with ChangeNotifier, DisposableChangeNotifierMixin
|
||||
}
|
||||
|
||||
/// Stop the host server and LAN broadcasting.
|
||||
Future<void> stopHostServer() async {
|
||||
Future<void> stopHostServer() => _serializeLifecycle(_stopHostServerLocked);
|
||||
|
||||
Future<void> _stopHostServerLocked() async {
|
||||
_intentionalDisconnect = true;
|
||||
await _discoveryService?.stopBroadcasting();
|
||||
|
||||
@@ -617,6 +648,10 @@ class CompanionRemoteProvider with ChangeNotifier, DisposableChangeNotifierMixin
|
||||
}
|
||||
|
||||
void _setupPeerServiceListeners() {
|
||||
// A rebuild/reconnect can re-enter here with live subscriptions from the
|
||||
// previous peer service still attached; drop them first so events don't
|
||||
// fan out to a stale service.
|
||||
_cleanupSubscriptions();
|
||||
_commandSubscription = _peerService!.onCommandReceived.listen(
|
||||
(command) {
|
||||
appLogger.d('CompanionRemote: Command received: ${command.type}');
|
||||
@@ -825,6 +860,11 @@ class CompanionRemoteProvider with ChangeNotifier, DisposableChangeNotifierMixin
|
||||
@override
|
||||
void dispose() {
|
||||
_reconnectTimer?.cancel();
|
||||
_boundActiveProfile?.removeListener(_scheduleAuthContextRefresh);
|
||||
for (final sub in _profileServiceSubs) {
|
||||
sub.cancel();
|
||||
}
|
||||
_profileServiceSubs.clear();
|
||||
_discoveryService?.dispose();
|
||||
_peerService?.dispose();
|
||||
RemoteAuthService.instance.clearCache();
|
||||
|
||||
@@ -14,6 +14,9 @@ Future<bool> startCompanionRemoteHost(BuildContext context) async {
|
||||
if (companionRemote.isHostServerRunning) return true;
|
||||
|
||||
try {
|
||||
// The host is an app-level service, not bound to this widget: everything
|
||||
// it needs is captured up front, so an unmount mid-await must not abort a
|
||||
// start the user asked for. Hence no `context.mounted` guards below.
|
||||
final connections = context.read<ConnectionRegistry>();
|
||||
final activeProfile = context.read<ActiveProfileProvider>();
|
||||
final profileConnections = context.read<ProfileConnectionRegistry>();
|
||||
@@ -23,9 +26,7 @@ Future<bool> startCompanionRemoteHost(BuildContext context) async {
|
||||
connections: connections,
|
||||
profileConnections: profileConnections,
|
||||
);
|
||||
if (!context.mounted) return false;
|
||||
final home = identity == null ? null : await plexHome.materializePlexHomeForConnection(identity.account.id);
|
||||
if (!context.mounted) return false;
|
||||
final ok = await companionRemote.ensureCryptoReady(
|
||||
home,
|
||||
connections: connections,
|
||||
@@ -34,7 +35,7 @@ Future<bool> startCompanionRemoteHost(BuildContext context) async {
|
||||
identity: identity,
|
||||
plexHomeForConnection: plexHome.materializePlexHomeForConnection,
|
||||
);
|
||||
if (!context.mounted || !ok) return false;
|
||||
if (!ok) return false;
|
||||
|
||||
await companionRemote.startHostServer();
|
||||
return companionRemote.isHostServerRunning;
|
||||
|
||||
@@ -388,7 +388,10 @@ class CompanionRemotePeerService with KeepaliveMixin {
|
||||
onDone: () {
|
||||
authTimeout?.cancel();
|
||||
appLogger.d('CompanionRemote: WebSocket connection closed');
|
||||
if (isAuthenticated) {
|
||||
// A replaced client's socket closes AFTER the new client already took
|
||||
// over `_clientSocket`; only the socket that still owns the session may
|
||||
// tear it down, or we'd clobber the live connection.
|
||||
if (isAuthenticated && identical(_clientSocket, socket)) {
|
||||
_clientSocket = null;
|
||||
_sessionEncKey = null;
|
||||
_isAuthenticated = false;
|
||||
|
||||
@@ -38,6 +38,7 @@ class _DiscoveryViewState extends State<DiscoveryView> with ControllerDisposerMi
|
||||
String? _errorMessage;
|
||||
bool _showManualEntry = false;
|
||||
bool _cryptoReady = false;
|
||||
bool _initializing = true;
|
||||
|
||||
late final CompanionRemoteProvider _provider;
|
||||
StreamSubscription<List<DiscoveredHost>>? _discoverySubscription;
|
||||
@@ -64,36 +65,47 @@ class _DiscoveryViewState extends State<DiscoveryView> with ControllerDisposerMi
|
||||
}
|
||||
|
||||
Future<void> _initCryptoAndDiscover() async {
|
||||
final connections = context.read<ConnectionRegistry>();
|
||||
final activeProfile = context.read<ActiveProfileProvider>();
|
||||
final profileConnections = context.read<ProfileConnectionRegistry>();
|
||||
final plexHome = context.read<PlexHomeService>();
|
||||
final identity = await resolveActivePlexIdentity(
|
||||
activeProfile: activeProfile,
|
||||
connections: connections,
|
||||
profileConnections: profileConnections,
|
||||
);
|
||||
if (!mounted) return;
|
||||
final home = await _resolveHome(identity?.account.id);
|
||||
if (!mounted) return;
|
||||
await _provider.ensureCryptoReady(
|
||||
home,
|
||||
connections: connections,
|
||||
activeProfile: activeProfile,
|
||||
profileConnections: profileConnections,
|
||||
identity: identity,
|
||||
plexHomeForConnection: plexHome.materializePlexHomeForConnection,
|
||||
);
|
||||
try {
|
||||
final connections = context.read<ConnectionRegistry>();
|
||||
final activeProfile = context.read<ActiveProfileProvider>();
|
||||
final profileConnections = context.read<ProfileConnectionRegistry>();
|
||||
final plexHome = context.read<PlexHomeService>();
|
||||
final identity = await resolveActivePlexIdentity(
|
||||
activeProfile: activeProfile,
|
||||
connections: connections,
|
||||
profileConnections: profileConnections,
|
||||
);
|
||||
if (!mounted) return;
|
||||
final home = await _resolveHome(identity?.account.id);
|
||||
if (!mounted) return;
|
||||
await _provider.ensureCryptoReady(
|
||||
home,
|
||||
connections: connections,
|
||||
activeProfile: activeProfile,
|
||||
profileConnections: profileConnections,
|
||||
identity: identity,
|
||||
plexHomeForConnection: plexHome.materializePlexHomeForConnection,
|
||||
);
|
||||
} catch (e) {
|
||||
appLogger.e('CompanionRemote: crypto init failed', error: e);
|
||||
}
|
||||
if (!mounted) return;
|
||||
|
||||
// The "crypto init failed" card only surfaces once init has actually
|
||||
// finished — while it's running the section shows a loading state so we
|
||||
// don't flash an error at open, and a thrown init still resolves to a
|
||||
// stable (non-stuck) failure card.
|
||||
if (_provider.isCryptoReady) {
|
||||
setState(() => _cryptoReady = true);
|
||||
setState(() {
|
||||
_cryptoReady = true;
|
||||
_initializing = false;
|
||||
});
|
||||
_startDiscovery();
|
||||
} else {
|
||||
setState(() {
|
||||
_cryptoReady = false;
|
||||
_initializing = false;
|
||||
_isSearching = false;
|
||||
_errorMessage = t.companionRemote.pairing.cryptoInitFailed;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -245,6 +257,21 @@ class _DiscoveryViewState extends State<DiscoveryView> with ControllerDisposerMi
|
||||
}
|
||||
|
||||
Widget _buildDiscoverySection() {
|
||||
if (_initializing) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(width: 32, height: 32, child: CircularProgressIndicator(strokeWidth: 3)),
|
||||
const SizedBox(height: 16),
|
||||
Text(t.companionRemote.pairing.searchingForDevices, style: Theme.of(context).textTheme.bodyMedium),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!_cryptoReady) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
|
||||
@@ -55,7 +55,7 @@ void main() {
|
||||
p.dispose();
|
||||
});
|
||||
|
||||
test('isCryptoReady is false until initializeCrypto is called', () {
|
||||
test('isCryptoReady is false until ensureCryptoReady succeeds', () {
|
||||
final p = CompanionRemoteProvider();
|
||||
expect(p.isCryptoReady, isFalse);
|
||||
p.dispose();
|
||||
@@ -186,21 +186,36 @@ void main() {
|
||||
final accountB = _plexAccount('plex-b', 'client-b');
|
||||
final profileA = _localProfile('profile-a');
|
||||
final profileB = _localProfile('profile-b');
|
||||
await connections.upsert(accountA);
|
||||
await connections.upsert(accountB);
|
||||
await profiles.upsert(profileA);
|
||||
await profiles.upsert(profileB);
|
||||
await profileConnections.upsert(
|
||||
ProfileConnection(profileId: profileA.id, connectionId: accountA.id, userIdentifier: 'admin-a'),
|
||||
makeDefault: true,
|
||||
);
|
||||
await profileConnections.upsert(
|
||||
ProfileConnection(profileId: profileB.id, connectionId: accountB.id, userIdentifier: 'admin-b'),
|
||||
makeDefault: true,
|
||||
);
|
||||
await storage.setActiveProfileId(profileB.id);
|
||||
await storage.setActiveProfileId(profileA.id);
|
||||
await active.initialize();
|
||||
|
||||
final provider = CompanionRemoteProvider();
|
||||
addTearDown(provider.dispose);
|
||||
await provider.initializeCrypto(home: _home('admin-a'), account: accountA, activeProfile: profileA);
|
||||
final okA = await provider.ensureCryptoReady(
|
||||
_home('admin-a'),
|
||||
connections: connections,
|
||||
activeProfile: active,
|
||||
profileConnections: profileConnections,
|
||||
account: accountA,
|
||||
);
|
||||
expect(okA, isTrue);
|
||||
expect(provider.debugCryptoConnectionId, accountA.id);
|
||||
expect(provider.debugCryptoProfileId, profileA.id);
|
||||
|
||||
await active.activate(profileB);
|
||||
|
||||
final ok = await provider.ensureCryptoReady(
|
||||
_home('admin-b'),
|
||||
connections: connections,
|
||||
@@ -418,12 +433,49 @@ void main() {
|
||||
});
|
||||
|
||||
test('resetForLogout clears crypto context', () async {
|
||||
final db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
final connections = ConnectionRegistry(db);
|
||||
final profileConnections = ProfileConnectionRegistry(db);
|
||||
final profiles = ProfileRegistry(db);
|
||||
final storage = await StorageService.getInstance();
|
||||
final plexHome = PlexHomeService(
|
||||
connections: connections,
|
||||
profileConnections: profileConnections,
|
||||
storage: storage,
|
||||
plexHomeUserFetcher: (_) async => const [],
|
||||
);
|
||||
final active = ActiveProfileProvider(
|
||||
registry: profiles,
|
||||
plexHome: plexHome,
|
||||
connections: connections,
|
||||
storage: storage,
|
||||
);
|
||||
addTearDown(() async {
|
||||
await active.resetForTesting();
|
||||
active.dispose();
|
||||
await plexHome.dispose();
|
||||
await db.close();
|
||||
});
|
||||
|
||||
final account = _plexAccount('plex-a', 'client-a');
|
||||
final profile = _localProfile('profile-a');
|
||||
await connections.upsert(account);
|
||||
await profiles.upsert(profile);
|
||||
await profileConnections.upsert(
|
||||
ProfileConnection(profileId: profile.id, connectionId: account.id, userIdentifier: 'admin-a'),
|
||||
makeDefault: true,
|
||||
);
|
||||
await storage.setActiveProfileId(profile.id);
|
||||
await active.initialize();
|
||||
|
||||
final provider = CompanionRemoteProvider();
|
||||
addTearDown(provider.dispose);
|
||||
await provider.initializeCrypto(
|
||||
home: _home('admin-a'),
|
||||
account: _plexAccount('plex-a', 'client-a'),
|
||||
activeProfile: _localProfile('profile-a'),
|
||||
await provider.ensureCryptoReady(
|
||||
_home('admin-a'),
|
||||
connections: connections,
|
||||
activeProfile: active,
|
||||
profileConnections: profileConnections,
|
||||
account: account,
|
||||
);
|
||||
expect(provider.isCryptoReady, isTrue);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user