import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:nomadcode_app/src/integrations/mattermost/mattermost_auth_service.dart'; import 'package:nomadcode_app/src/integrations/mattermost/mattermost_push_client.dart'; import 'package:nomadcode_app/src/integrations/mattermost/mattermost_push_host_integration.dart'; class _FakePushClient implements MattermostPushClient { int initializeCalls = 0; int navigateChannelAssignments = 0; int navigateThreadAssignments = 0; final StreamController> _controller = StreamController>.broadcast(); void Function(String serverUrl, String channelId)? _navigateChannel; void Function(String serverUrl, String rootId)? _navigateThread; @override Stream> get onNotification => _controller.stream; void emit(Map data) => _controller.add(data); Future dispose() => _controller.close(); @override Future initialize() async { initializeCalls += 1; } @override Future getDeviceToken() async => null; @override Future setAuthToken( String serverUrl, String token, { String? identifier, }) async {} @override Future setSigningKey(String serverUrl, String signingKey) async {} @override set onDeviceTokenReady(Future Function(String token)? callback) {} @override set onNavigateToChannel( void Function(String serverUrl, String channelId)? callback, ) { navigateChannelAssignments += 1; _navigateChannel = callback; } @override set onNavigateToThread( void Function(String serverUrl, String rootId)? callback, ) { navigateThreadAssignments += 1; _navigateThread = callback; } void triggerNavigateChannel(String serverUrl, String channelId) { _navigateChannel?.call(serverUrl, channelId); } void triggerNavigateThread(String serverUrl, String rootId) { _navigateThread?.call(serverUrl, rootId); } } class _NoopAuthService implements MattermostAuthService { int autoLoginCalls = 0; final bool throwOnLogin; _NoopAuthService({this.throwOnLogin = false}); @override Future autoLoginAndRegister() async { autoLoginCalls += 1; if (throwOnLogin) { throw StateError('credentials missing'); } } @override noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } void main() { test('initialize calls push client init exactly once', () async { final push = _FakePushClient(); final auth = _NoopAuthService(); final host = MattermostPushHostIntegration( pushClient: push, authServiceFactory: (_) => auth, ); await host.initialize(); await host.initialize(); expect(push.initializeCalls, equals(1)); expect(auth.autoLoginCalls, equals(1)); expect(host.isInitialized, isTrue); await push.dispose(); }); test('auto-login failure does not block initialize', () async { final push = _FakePushClient(); final auth = _NoopAuthService(throwOnLogin: true); final host = MattermostPushHostIntegration( pushClient: push, authServiceFactory: (_) => auth, ); await host.initialize(); expect(host.isInitialized, isTrue); expect(push.initializeCalls, equals(1)); await push.dispose(); }); test( 'navigation callbacks are assigned exactly once during initialize', () async { final push = _FakePushClient(); final host = MattermostPushHostIntegration( pushClient: push, authServiceFactory: (_) => _NoopAuthService(), ); String? capturedChannel; String? capturedThread; await host.initialize( onNavigateToChannel: (_, channelId) => capturedChannel = channelId, onNavigateToThread: (_, rootId) => capturedThread = rootId, ); expect(push.navigateChannelAssignments, equals(1)); expect(push.navigateThreadAssignments, equals(1)); push.triggerNavigateChannel('https://srv', 'channel-1'); push.triggerNavigateThread('https://srv', 'root-1'); expect(capturedChannel, equals('channel-1')); expect(capturedThread, equals('root-1')); await push.dispose(); }, ); test('notification stream remains consumable through integration', () async { final push = _FakePushClient(); final host = MattermostPushHostIntegration( pushClient: push, authServiceFactory: (_) => _NoopAuthService(), ); await host.initialize(); final received = >[]; final sub = host.onNotification.listen(received.add); push.emit(const {'type': 'message', 'message': 'hi'}); await Future.delayed(Duration.zero); expect(received, hasLength(1)); expect(received.first['message'], equals('hi')); await sub.cancel(); await push.dispose(); }); }