Files
minstrel/flutter_client/test/quarantine/quarantine_provider_test.dart
T
bvandeusen 83a7099db3 test(flutter): stub the transitive provider deps the drift cohort needs
Last 2 of the 6 surfaced drift-cohort failures. Diagnoses below.

quarantine_provider_test 'flag keeps drift optimistic + queues
mutation on server failure':
  MyQuarantineController.build() schedules unawaited _refreshFromServer()
  which reads connectivityProvider (a StreamProvider<bool>). In tests
  without an override, that stream never emits — its real impl listens
  to connectivity_plus's platform channel which has no fixture in
  flutter test. The success-path quarantine tests resolve their main
  flow before _refreshFromServer's chain reaches the connectivity read,
  so they don't trip the "disposed during loading" guard at tearDown.
  The throwing-API variant's `await ref.read(mutationQueueProvider)
  .enqueue(...)` advances enough async work that the connectivity read
  IS reached, then container.dispose() trips Riverpod's invariant.

  Fix: override connectivityProvider in the shared _container helper
  with `Stream.value(true)` so it resolves immediately. The previous
  `await Future.delayed(Duration.zero)` workaround is removed — the
  cleaner fix makes that band-aid unnecessary.

like_button_test 'tap toggles icon optimistically; rollback on error':
  LikesController.toggle:
    final user = _ref.read(authControllerProvider).value;
    if (user == null) return;
  The test only overrode `likesApiProvider`; authControllerProvider was
  in AsyncLoading state at toggle() time → user was null → early return
  → no drift write → likedIdsProvider never emits "liked" → heart never
  fills → assertion fails. The test would never have passed against the
  current controller; it was a stale survivor of an older API.

  Fix: stub authControllerProvider with _FakeAuthController that yields
  User(id: 'u1', …) immediately. Also add overrides for appDbProvider
  (NativeDatabase.memory, bypasses drift_flutter's Timer) and
  connectivityProvider (Stream.value(true), unblocks cacheFirst's
  isOnline check) — both are transitive dependencies of
  likedIdsProvider's cacheFirst.

After this push, the full drift cohort should be all-green. Fable #399
/ local #62 closes when CI lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:12:23 -04:00

145 lines
4.9 KiB
Dart

import 'package:drift/native.dart' show NativeDatabase;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:minstrel/api/endpoints/quarantine.dart';
import 'package:minstrel/cache/audio_cache_manager.dart' show appDbProvider;
import 'package:minstrel/cache/connectivity_provider.dart';
import 'package:minstrel/cache/db.dart';
import 'package:minstrel/models/track.dart';
import 'package:minstrel/quarantine/quarantine_provider.dart';
class _StubQuarantineApi implements QuarantineApi {
_StubQuarantineApi({this.shouldThrow = false});
bool shouldThrow;
int flagCalls = 0;
int unflagCalls = 0;
@override
Future<void> flag(String trackId, String reason, {String notes = ''}) async {
flagCalls++;
if (shouldThrow) throw Exception('simulated server failure');
}
@override
Future<void> unflag(String trackId) async {
unflagCalls++;
if (shouldThrow) throw Exception('simulated server failure');
}
}
const _track = TrackRef(
id: 't1',
title: 'Roygbiv',
albumId: 'a1',
albumTitle: 'Geogaddi',
artistId: 'ar1',
artistName: 'Boards of Canada',
durationSec: 137,
trackNumber: 4,
streamUrl: '',
);
ProviderContainer _container({required AppDb db, required QuarantineApi api}) {
return ProviderContainer(overrides: [
appDbProvider.overrideWithValue(db),
quarantineApiProvider.overrideWith((ref) async => api),
// _refreshFromServer() in build() reads connectivityProvider; in tests
// without this override it's a StreamProvider that never emits, so
// tearDown trips Riverpod's "disposed during loading" — visible on
// the throwing-API variant where the catch path's await mutationQueue
// .enqueue advances _refreshFromServer's chain enough to surface it.
connectivityProvider.overrideWith((ref) => Stream.value(true)),
]);
}
Future<void> _seed(AppDb db) async {
await db.into(db.cachedQuarantineMine).insert(
CachedQuarantineMineCompanion.insert(
trackId: 't1',
reason: 'bad_rip',
createdAt: '2026-05-01T00:00:00Z',
trackTitle: 'Roygbiv',
albumId: 'a1',
albumTitle: 'Geogaddi',
artistId: 'ar1',
artistName: 'Boards of Canada',
),
);
}
void main() {
test('flag inserts a drift row and calls the server',
() async {
final db = AppDb(NativeDatabase.memory());
addTearDown(db.close);
final api = _StubQuarantineApi();
final container = _container(db: db, api: api);
addTearDown(container.dispose);
await container.read(myQuarantineProvider.future);
await container
.read(myQuarantineProvider.notifier)
.flag(_track, 'bad_rip', '');
expect(api.flagCalls, 1);
final rows = await db.select(db.cachedQuarantineMine).get();
expect(rows, hasLength(1));
expect(rows.first.trackId, 't1');
expect(rows.first.reason, 'bad_rip');
});
test('flag keeps drift optimistic + queues mutation on server failure', () async {
final db = AppDb(NativeDatabase.memory());
addTearDown(db.close);
final api = _StubQuarantineApi(shouldThrow: true);
final container = _container(db: db, api: api);
addTearDown(container.dispose);
await container.read(myQuarantineProvider.future);
// No longer rethrows — flag swallows the REST failure and queues
// for replay so the user's intent persists offline.
await container
.read(myQuarantineProvider.notifier)
.flag(_track, 'bad_rip', '');
final rows = await db.select(db.cachedQuarantineMine).get();
expect(rows, hasLength(1),
reason: 'optimistic drift row should persist across REST failure');
final mutations = await db.select(db.cachedMutations).get();
expect(mutations, hasLength(1),
reason: 'failed flag should have been enqueued for replay');
expect(mutations.first.kind, 'quarantine.flag');
});
test('unflag deletes the drift row and calls the server',
() async {
final db = AppDb(NativeDatabase.memory());
addTearDown(db.close);
await _seed(db);
final api = _StubQuarantineApi();
final container = _container(db: db, api: api);
addTearDown(container.dispose);
await container.read(myQuarantineProvider.future);
await container.read(myQuarantineProvider.notifier).unflag('t1');
expect(api.unflagCalls, 1);
final rows = await db.select(db.cachedQuarantineMine).get();
expect(rows, isEmpty);
});
test('isHidden reflects drift state', () async {
final db = AppDb(NativeDatabase.memory());
addTearDown(db.close);
await _seed(db);
final api = _StubQuarantineApi();
final container = _container(db: db, api: api);
addTearDown(container.dispose);
await container.read(myQuarantineProvider.future);
final ctrl = container.read(myQuarantineProvider.notifier);
expect(ctrl.isHidden('t1'), isTrue);
expect(ctrl.isHidden('t2'), isFalse);
});
}