Files
plezy/test/profiles/profile_connection_registry_test.dart
T
edde746 1059658515 refactor(profiles): remove dead registry surface
Delete unused registry members (ProfileConnectionRegistry.insertIfAbsent /
removeAllForProfile, ProfileRegistry.reorder, ProfilesView.countFor,
ConnectionRegistry.getDefault) and their orphan tests, refreshing the stale
docs that named removeAllForProfile as the profile-delete cleanup path.

Repair the "one default per profile" join-row invariant: the connectionId FK
cascade (foreign_keys=ON) silently drops a profile's default row while its
other rows survive, leaving it defaultless. Add promoteMissingDefaults, share
a deterministic re-promotion helper with remove(), and re-promote in
removeAllForConnection.
2026-07-02 11:41:25 +02:00

204 lines
8.5 KiB
Dart

import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/database/app_database.dart';
import 'package:plezy/profiles/profile_connection.dart';
import 'package:plezy/profiles/profile_connection_registry.dart';
import '../test_helpers/prefs.dart';
void main() {
late AppDatabase db;
late ProfileConnectionRegistry registry;
setUp(() async {
resetSharedPreferencesForTest();
db = AppDatabase.forTesting(NativeDatabase.memory());
registry = ProfileConnectionRegistry(db);
// Seed the parent rows that ProfileConnections now FK-references.
// Without this, every upsert below trips the FK constraint added in
// schema v17.
final now = DateTime.now().millisecondsSinceEpoch;
for (final id in ['p1', 'p2']) {
await db
.into(db.profiles)
.insert(ProfilesCompanion.insert(id: id, kind: 'local', displayName: id, configJson: '{}', createdAt: now));
}
for (final id in ['c1', 'c2']) {
await db
.into(db.connections)
.insert(ConnectionsCompanion.insert(id: id, kind: 'plex', displayName: id, configJson: '{}', createdAt: now));
}
});
tearDown(() async {
await db.close();
});
group('ProfileConnectionRegistry', () {
test('listForProfile is empty initially', () async {
expect(await registry.listForProfile('p1'), isEmpty);
});
test('upsert inserts and round-trips', () async {
await registry.upsert(
const ProfileConnection(profileId: 'p1', connectionId: 'c1', userToken: 'tok', userIdentifier: 'uid-1'),
);
final list = await registry.listForProfile('p1');
expect(list, hasLength(1));
expect(list.first.userToken, 'tok');
expect(list.first.userIdentifier, 'uid-1');
final raw = await db.select(db.profileConnections).getSingle();
expect(raw.userToken, isNot('tok'));
});
test('first row for a profile is auto-default', () async {
await registry.upsert(
const ProfileConnection(profileId: 'p1', connectionId: 'c1', userToken: 't', userIdentifier: 'u'),
);
final pc = await registry.get('p1', 'c1');
expect(pc!.isDefault, isTrue);
});
test('subsequent rows do not auto-replace the default', () async {
await registry.upsert(
const ProfileConnection(profileId: 'p1', connectionId: 'c1', userToken: 't1', userIdentifier: 'u1'),
);
await registry.upsert(
const ProfileConnection(profileId: 'p1', connectionId: 'c2', userToken: 't2', userIdentifier: 'u2'),
);
final list = await registry.listForProfile('p1');
final defaults = list.where((pc) => pc.isDefault).toList();
expect(defaults, hasLength(1));
expect(defaults.first.connectionId, 'c1');
});
test('re-upsert preserves the existing default flag', () async {
// Regression: re-upserting a default row used to clobber its
// `isDefault` because the fast path always wrote `isFirst`.
await registry.upsert(
const ProfileConnection(profileId: 'p1', connectionId: 'c1', userToken: 't1', userIdentifier: 'u1'),
);
await registry.upsert(
const ProfileConnection(profileId: 'p1', connectionId: 'c2', userToken: 't2', userIdentifier: 'u2'),
);
expect((await registry.get('p1', 'c1'))!.isDefault, isTrue);
// Re-upsert c1 (token refresh) — the default flag must survive.
await registry.upsert(
const ProfileConnection(profileId: 'p1', connectionId: 'c1', userToken: 't1-refreshed', userIdentifier: 'u1'),
);
expect((await registry.get('p1', 'c1'))!.isDefault, isTrue);
expect((await registry.get('p1', 'c2'))!.isDefault, isFalse);
});
test('setDefault flips the default flag exclusively', () async {
await registry.upsert(
const ProfileConnection(profileId: 'p1', connectionId: 'c1', userToken: 't1', userIdentifier: 'u1'),
);
await registry.upsert(
const ProfileConnection(profileId: 'p1', connectionId: 'c2', userToken: 't2', userIdentifier: 'u2'),
);
await registry.setDefault('p1', 'c2');
final list = await registry.listForProfile('p1');
final c1 = list.firstWhere((pc) => pc.connectionId == 'c1');
final c2 = list.firstWhere((pc) => pc.connectionId == 'c2');
expect(c1.isDefault, isFalse);
expect(c2.isDefault, isTrue);
});
test('recordToken caches a fresh user token', () async {
await registry.upsert(
const ProfileConnection(profileId: 'p1', connectionId: 'c1', userToken: '', userIdentifier: 'u1'),
);
await registry.recordToken('p1', 'c1', 'fresh-token');
final pc = await registry.get('p1', 'c1');
expect(pc!.userToken, 'fresh-token');
expect(pc.tokenAcquiredAt, isNotNull);
});
test('remove drops the row and promotes the next one as default', () async {
await registry.upsert(
const ProfileConnection(profileId: 'p1', connectionId: 'c1', userToken: 't1', userIdentifier: 'u1'),
);
await registry.upsert(
const ProfileConnection(profileId: 'p1', connectionId: 'c2', userToken: 't2', userIdentifier: 'u2'),
);
// c1 is default initially. Removing it should promote c2.
await registry.remove('p1', 'c1');
final remaining = await registry.listForProfile('p1');
expect(remaining, hasLength(1));
expect(remaining.first.connectionId, 'c2');
expect(remaining.first.isDefault, isTrue);
});
test('removeAllForConnection cascades across profiles', () async {
await registry.upsert(
const ProfileConnection(profileId: 'p1', connectionId: 'c1', userToken: 't', userIdentifier: 'u'),
);
await registry.upsert(
const ProfileConnection(profileId: 'p2', connectionId: 'c1', userToken: 't', userIdentifier: 'u'),
);
final removed = await registry.removeAllForConnection('c1');
expect(removed, 2);
expect(await registry.listForConnection('c1'), isEmpty);
});
test('removeAllForConnection re-promotes a default for a surviving profile', () async {
await registry.upsert(
const ProfileConnection(profileId: 'p1', connectionId: 'c1', userToken: 't', userIdentifier: 'u'),
makeDefault: true,
);
await registry.upsert(
const ProfileConnection(profileId: 'p1', connectionId: 'c2', userToken: 't', userIdentifier: 'u'),
);
// c1 is default; removing it connection-wide must leave p1 with c2 as
// its new default rather than defaultless.
await registry.removeAllForConnection('c1');
final remaining = await registry.listForProfile('p1');
expect(remaining, hasLength(1));
expect(remaining.single.connectionId, 'c2');
expect(remaining.single.isDefault, isTrue);
});
test('promoteMissingDefaults repairs a profile the FK cascade left defaultless', () async {
await registry.upsert(
const ProfileConnection(profileId: 'p1', connectionId: 'c1', userToken: 't', userIdentifier: 'u'),
makeDefault: true,
);
await registry.upsert(
const ProfileConnection(profileId: 'p1', connectionId: 'c2', userToken: 't', userIdentifier: 'u'),
);
// Deleting the connection cascades away p1's default join row (c1) via
// the connectionId FK, silently leaving p1 with no default flag.
await (db.delete(db.connections)..where((t) => t.id.equals('c1'))).go();
final beforeRepair = await registry.listForProfile('p1');
expect(beforeRepair.single.connectionId, 'c2');
expect(beforeRepair.single.isDefault, isFalse);
await registry.promoteMissingDefaults();
expect((await registry.listForProfile('p1')).single.isDefault, isTrue);
});
test('upsert succeeds for a virtual plex_home profile id with no parent row', () async {
// Regression for the v17 FK that broke Plex Home activation: virtual
// plex_home profiles are never persisted in `profiles`, so the join
// row's profile_id has no parent. v20 dropped the FK; this insert
// must round-trip without a FOREIGN KEY constraint failure.
const plexHomeId = 'plex-home-plex.acc-uuid-1234';
await registry.upsert(
const ProfileConnection(
profileId: plexHomeId,
connectionId: 'c1',
userToken: 'home-tok',
userIdentifier: 'uuid-1234',
),
);
final list = await registry.listForProfile(plexHomeId);
expect(list, hasLength(1));
expect(list.first.userToken, 'home-tok');
expect(list.first.userIdentifier, 'uuid-1234');
});
});
}