tasq/test/realtime_controller_test.dart

198 lines
7.7 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:tasq/providers/realtime_controller.dart';
import 'package:tasq/providers/stream_recovery.dart';
// ---------------------------------------------------------------------------
// RealtimeController only accesses `_client.auth.onAuthStateChange` during
// `_init()`, which wraps the subscription in a try-catch. We use a minimal
// SupabaseClient pointed at localhost — the auth stream subscription will
// either return an empty stream or throw (caught internally), so no network
// activity occurs during tests.
// ---------------------------------------------------------------------------
void main() {
group('RealtimeController', () {
late RealtimeController controller;
setUp(() {
// SupabaseClient constructor does not connect eagerly; _init() catches
// any exception thrown when accessing auth.onAuthStateChange.
controller = RealtimeController(
SupabaseClient('http://localhost', 'test-anon-key',
authOptions: const AuthClientOptions(autoRefreshToken: false)),
);
});
tearDown(() {
controller.dispose();
});
// ── Initial state ───────────────────────────────────────────────────
test('starts with no recovering channels', () {
expect(controller.recoveringChannels, isEmpty);
expect(controller.isAnyStreamRecovering, isFalse);
});
test('isChannelRecovering returns false for unknown channel', () {
expect(controller.isChannelRecovering('tasks'), isFalse);
});
// ── markChannelRecovering ───────────────────────────────────────────
test('markChannelRecovering adds channel to the set', () {
controller.markChannelRecovering('tasks');
expect(controller.isChannelRecovering('tasks'), isTrue);
expect(controller.isAnyStreamRecovering, isTrue);
});
test('markChannelRecovering with multiple channels tracks all of them', () {
controller.markChannelRecovering('tasks');
controller.markChannelRecovering('tickets');
expect(controller.isChannelRecovering('tasks'), isTrue);
expect(controller.isChannelRecovering('tickets'), isTrue);
expect(controller.recoveringChannels.length, 2);
});
test('markChannelRecovering notifies listeners exactly once per new channel',
() {
int notifyCount = 0;
controller.addListener(() => notifyCount++);
controller.markChannelRecovering('tasks');
expect(notifyCount, 1);
// Adding the same channel again must NOT trigger a second notification.
controller.markChannelRecovering('tasks');
expect(notifyCount, 1);
});
// ── markChannelRecovered ────────────────────────────────────────────
test('markChannelRecovered removes channel from the set', () {
controller.markChannelRecovering('tasks');
controller.markChannelRecovered('tasks');
expect(controller.isChannelRecovering('tasks'), isFalse);
expect(controller.isAnyStreamRecovering, isFalse);
});
test('markChannelRecovered on unknown channel does not notify', () {
int notifyCount = 0;
controller.addListener(() => notifyCount++);
// Recovering a channel that was never marked recovering is a no-op.
controller.markChannelRecovered('nonexistent');
expect(notifyCount, 0);
});
test('markChannelRecovered notifies listeners when channel is removed', () {
controller.markChannelRecovering('tasks');
int notifyCount = 0;
controller.addListener(() => notifyCount++);
controller.markChannelRecovered('tasks');
expect(notifyCount, 1);
});
test('recovering two channels and recovering one leaves the other active',
() {
controller.markChannelRecovering('tasks');
controller.markChannelRecovering('tickets');
controller.markChannelRecovered('tasks');
expect(controller.isChannelRecovering('tasks'), isFalse);
expect(controller.isChannelRecovering('tickets'), isTrue);
expect(controller.isAnyStreamRecovering, isTrue);
});
// ── recoveringChannels unmodifiable view ────────────────────────────
test('recoveringChannels returns an unmodifiable set', () {
controller.markChannelRecovering('tasks');
final view = controller.recoveringChannels;
expect(() => (view as dynamic).add('other'), throwsUnsupportedError);
});
// ── handleChannelStatus ─────────────────────────────────────────────
test('handleChannelStatus with connected marks channel as recovered', () {
controller.markChannelRecovering('tasks');
controller.handleChannelStatus('tasks', StreamConnectionStatus.connected);
expect(controller.isChannelRecovering('tasks'), isFalse);
});
test('handleChannelStatus with polling marks channel as recovered', () {
controller.markChannelRecovering('tasks');
controller.handleChannelStatus('tasks', StreamConnectionStatus.polling);
expect(controller.isChannelRecovering('tasks'), isFalse);
});
test('handleChannelStatus with recovering marks channel as recovering', () {
controller.handleChannelStatus(
'tasks', StreamConnectionStatus.recovering);
expect(controller.isChannelRecovering('tasks'), isTrue);
});
test('handleChannelStatus with stale marks channel as recovering', () {
controller.handleChannelStatus('tasks', StreamConnectionStatus.stale);
expect(controller.isChannelRecovering('tasks'), isTrue);
});
test('handleChannelStatus with failed marks channel as recovering', () {
controller.handleChannelStatus('tasks', StreamConnectionStatus.failed);
expect(controller.isChannelRecovering('tasks'), isTrue);
});
test('handleChannelStatus connected after recovering clears it', () {
controller.handleChannelStatus(
'tasks', StreamConnectionStatus.recovering);
expect(controller.isAnyStreamRecovering, isTrue);
controller.handleChannelStatus('tasks', StreamConnectionStatus.connected);
expect(controller.isAnyStreamRecovering, isFalse);
});
// ── Legacy compat ───────────────────────────────────────────────────
test('markStreamRecovering maps to _global channel', () {
controller.markStreamRecovering();
expect(controller.isChannelRecovering('_global'), isTrue);
expect(controller.isAnyStreamRecovering, isTrue);
});
test('markStreamRecovered clears _global channel', () {
controller.markStreamRecovering();
controller.markStreamRecovered();
expect(controller.isChannelRecovering('_global'), isFalse);
expect(controller.isAnyStreamRecovering, isFalse);
});
// ── isAnyStreamRecovering aggregate ────────────────────────────────
test('isAnyStreamRecovering is false only when all channels are recovered',
() {
controller.markChannelRecovering('a');
controller.markChannelRecovering('b');
controller.markChannelRecovered('a');
expect(controller.isAnyStreamRecovering, isTrue);
controller.markChannelRecovered('b');
expect(controller.isAnyStreamRecovering, isFalse);
});
});
}