diff --git a/lib/connection/connection_bootstrap.dart b/lib/connection/connection_bootstrap.dart index 5079666b..92c0c377 100644 --- a/lib/connection/connection_bootstrap.dart +++ b/lib/connection/connection_bootstrap.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import '../models/plex/plex_home.dart'; import '../models/plex/plex_home_user.dart'; import '../profiles/profile.dart'; +import '../profiles/plex_home_cache_codec.dart'; import '../profiles/profile_registry.dart'; import '../services/plex_auth_service.dart'; import '../services/server_registry.dart'; @@ -225,9 +226,7 @@ class ConnectionBootstrap { final raw = storage.getPlexHomeUsersCacheJson(connectionId); if (raw == null || raw.isEmpty) return null; try { - final decoded = jsonDecode(raw); - if (decoded is! List) return null; - return decoded.whereType>().map(PlexHomeUser.fromJson).toList(); + return decodePlexHomeUsersCache(raw); } catch (e, st) { appLogger.w('Migration: failed to read Plex Home cache for $connectionId', error: e, stackTrace: st); return null; @@ -238,7 +237,7 @@ class ConnectionBootstrap { try { final users = await _plexHomeUserFetcher(account.accountToken); if (users.isNotEmpty) { - await storage.savePlexHomeUsersCache(account.id, users.map((u) => u.toJson()).toList()); + await storage.savePlexHomeUsersCache(account.id, encodePlexHomeUsersCache(users)); appLogger.i('Migration: fetched ${users.length} Plex Home users for ${account.id}'); } return users; diff --git a/lib/profiles/plex_home_cache_codec.dart b/lib/profiles/plex_home_cache_codec.dart new file mode 100644 index 00000000..b3783cfc --- /dev/null +++ b/lib/profiles/plex_home_cache_codec.dart @@ -0,0 +1,19 @@ +import 'dart:convert'; + +import '../models/plex/plex_home_user.dart'; + +List> encodePlexHomeUsersCache(List users) { + return users.map((user) => user.toJson()).toList(growable: false); +} + +String encodePlexHomeUsersCacheJson(List users) { + return jsonEncode(encodePlexHomeUsersCache(users)); +} + +List decodePlexHomeUsersCache(String raw) { + final decoded = jsonDecode(raw); + if (decoded is! List) { + throw const FormatException('Plex Home users cache is not a list'); + } + return decoded.whereType>().map(PlexHomeUser.fromJson).toList(growable: false); +} diff --git a/lib/profiles/plex_home_service.dart b/lib/profiles/plex_home_service.dart index 0b1c7520..455b6e48 100644 --- a/lib/profiles/plex_home_service.dart +++ b/lib/profiles/plex_home_service.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import '../connection/connection.dart'; import '../connection/connection_registry.dart'; @@ -9,6 +8,7 @@ import '../services/plex_auth_service.dart'; import '../services/storage_service.dart'; import '../utils/app_logger.dart'; import 'profile_connection_registry.dart'; +import 'plex_home_cache_codec.dart'; /// Live source of truth for Plex Home users — Plex owns these, so we never /// persist them as `Profile` rows. The service fetches `/home/users` per @@ -188,11 +188,12 @@ class PlexHomeService { appLogger.d('PlexHomeService: dropping fetch result for removed account ${conn.accountLabel}'); return false; } - final encoded = users.map((u) => u.toJson()).toList(); + final encoded = encodePlexHomeUsersCache(users); // Unchanged fetches (the hourly ticker, mostly) must not emit: every // emission fans out through ActiveProfileProvider into a full // recompute/notify cascade across the app. - if (_byConnection.containsKey(conn.id) && storage.getPlexHomeUsersCacheJson(conn.id) == jsonEncode(encoded)) { + if (_byConnection.containsKey(conn.id) && + storage.getPlexHomeUsersCacheJson(conn.id) == encodePlexHomeUsersCacheJson(users)) { appLogger.d('PlexHomeService: home users unchanged for ${conn.accountLabel}'); return true; } @@ -213,12 +214,7 @@ class PlexHomeService { final raw = storage.getPlexHomeUsersCacheJson(connectionId); if (raw == null) return null; try { - final decoded = jsonDecode(raw); - if (decoded is! List) { - appLogger.w('PlexHomeService: cache for $connectionId is not a list — ignoring'); - return null; - } - return decoded.whereType>().map(PlexHomeUser.fromJson).toList(); + return decodePlexHomeUsersCache(raw); } catch (e, st) { appLogger.w('PlexHomeService: failed to read cache for $connectionId', error: e, stackTrace: st); return null; diff --git a/test/profiles/plex_home_cache_codec_test.dart b/test/profiles/plex_home_cache_codec_test.dart new file mode 100644 index 00000000..28036973 --- /dev/null +++ b/test/profiles/plex_home_cache_codec_test.dart @@ -0,0 +1,34 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:plezy/models/plex/plex_home_user.dart'; +import 'package:plezy/profiles/plex_home_cache_codec.dart'; + +void main() { + final user = PlexHomeUser( + id: 1, + uuid: 'user-1', + title: 'Owner', + thumb: '', + hasPassword: false, + restricted: false, + updatedAt: null, + admin: true, + guest: false, + protected: false, + ); + + test('round-trips the persisted Plex Home users format', () { + final decoded = decodePlexHomeUsersCache(encodePlexHomeUsersCacheJson([user])); + + expect(decoded.map((entry) => entry.toJson()), [user.toJson()]); + }); + + test('rejects cache payloads that are not lists', () { + expect(() => decodePlexHomeUsersCache('{"uuid":"user-1"}'), throwsFormatException); + }); + + test('ignores non-object entries for compatibility with existing caches', () { + final decoded = decodePlexHomeUsersCache('[null, 1, "bad", ${encodePlexHomeUsersCacheJson([user]).substring(1)}'); + + expect(decoded.map((entry) => entry.toJson()), [user.toJson()]); + }); +}