From e8e1d53aa34dad49a6a75f1b1008e843c3a00fce Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 16 Jun 2021 22:19:00 +0530 Subject: [PATCH] add client_test.dart Signed-off-by: Sahil Kumar --- .../stream_chat/lib/src/client/channel.dart | 32 +- .../stream_chat/lib/src/client/client.dart | 81 +- .../lib/src/core/api/message_api.dart | 16 +- .../test/src/api/channel_test.dart | 14 +- .../stream_chat/test/src/api/client_test.dart | 2135 +++++++++++++++++ .../test/src/core/api/message_api_test.dart | 4 +- packages/stream_chat/test/src/fakes.dart | 89 + packages/stream_chat/test/src/matchers.dart | 36 + packages/stream_chat/test/src/mocks.dart | 11 +- packages/stream_chat/test/src/utils.dart | 9 + .../lib/src/message_list_view.dart | 2 +- .../lib/src/stream_channel.dart | 2 +- 12 files changed, 2356 insertions(+), 75 deletions(-) create mode 100644 packages/stream_chat/test/src/api/client_test.dart diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index fe56aed4..9d0dc96c 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -22,9 +22,9 @@ class Channel { this._client, this._type, this._id, { - Map extraData = const {}, + Map? extraData, }) : _cid = _id != null ? '$_type:$_id' : null, - _extraData = extraData { + _extraData = extraData ?? {} { _client.logger.info('New Channel instance not initialized created'); } @@ -202,8 +202,13 @@ class Channel { } /// Channel extra data - Map get extraData => - state?._channelState.channel?.extraData ?? _extraData; + Map get extraData { + var data = state?._channelState.channel?.extraData; + if (data == null || data.isEmpty) { + data = _extraData; + } + return data; + } /// Channel extra data as a stream Stream> get extraDataStream { @@ -942,7 +947,7 @@ class Channel { state = ChannelClientState(this, channelState); if (cid != null) { - client.state.channels[cid!] = this; + client.state.channels = {cid!: this}; } if (!_initializedCompleter.isCompleted) { _initializedCompleter.complete(true); @@ -959,8 +964,8 @@ class Channel { /// Set [preferOffline] to true to avoid the api call if the data is already /// in the offline storage Future getReplies( - String parentId, - PaginationParams options, { + String parentId, { + PaginationParams? options, bool preferOffline = false, }) async { final cachedReplies = await _client.chatPersistenceClient?.getReplies( @@ -973,19 +978,22 @@ class Channel { return QueryRepliesResponse()..messages = cachedReplies; } } - final repliesResponse = await _client.getReplies(parentId, options); + final repliesResponse = await _client.getReplies( + parentId, + options: options, + ); state?.updateThreadInfo(parentId, repliesResponse.messages); return repliesResponse; } /// List the reactions for a message in the channel Future getReactions( - String messageId, - PaginationParams options, - ) => + String messageId, { + PaginationParams? options, + }) => _client.getReactions( messageId, - options, + options: options, ); /// Retrieves a list of messages by ID diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 7a22ac50..ecadfe3b 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -63,7 +63,7 @@ class StreamChatClient { /// application. StreamChatClient( String apiKey, { - this.logLevel = Level.ALL, + this.logLevel = Level.WARNING, LogHandlerFunction? logHandlerFunction, RetryPolicy? retryPolicy, Location? location, @@ -71,9 +71,11 @@ class StreamChatClient { Duration connectTimeout = const Duration(seconds: 6), Duration receiveTimeout = const Duration(seconds: 6), StreamChatApi? chatApi, + WebSocket? ws, AttachmentFileUploader? attachmentFileUploader, }) { - _setupLogger(logHandlerFunction); + this.logHandlerFunction = logHandlerFunction ?? _defaultLogHandler; + logger.info('Initiating new StreamChatClient'); final options = StreamHttpClientOptions( baseUrl: baseURL, @@ -92,13 +94,14 @@ class StreamChatClient { logger: detachedLogger('🕸️'), ); - _ws = WebSocket( - apiKey: apiKey, - baseUrl: options.baseUrl, - tokenManager: _tokenManager, - handler: handleEvent, - logger: detachedLogger('🔌'), - ); + _ws = ws ?? + WebSocket( + apiKey: apiKey, + baseUrl: options.baseUrl, + tokenManager: _tokenManager, + handler: handleEvent, + logger: detachedLogger('🔌'), + ); _retryPolicy = retryPolicy ?? RetryPolicy( @@ -107,8 +110,6 @@ class StreamChatClient { ); state = ClientState(this); - - logger.info('instantiating new client'); } late final StreamChatApi _chatApi; @@ -160,7 +161,7 @@ class StreamChatClient { /// Client specific logger instance. /// Refer to the class [Logger] to learn more about the specific /// implementation. - final Logger logger = Logger.detached('📡'); + late final Logger logger = detachedLogger('📡'); /// A function that has a parameter of type [LogRecord]. /// This is called on every new log record. @@ -185,7 +186,7 @@ class StreamChatClient { final _eventController = BehaviorSubject(); - /// Stream of [Event] coming from websocket connection + /// Stream of [Event] coming from [_ws] connection /// Listen to this or use the [on] method to filter specific event types Stream get eventStream => _eventController.stream; @@ -195,12 +196,12 @@ class StreamChatClient { set _wsConnectionStatus(ConnectionStatus status) => _wsConnectionStatusController.add(status); - /// The current status value of the websocket connection + /// The current status value of the [_ws] connection ConnectionStatus get wsConnectionStatus => _wsConnectionStatusController.value; - /// This notifies the connection status of the websocket connection. - /// Listen to this to get notified when the websocket tries to reconnect. + /// This notifies the connection status of the [_ws] connection. + /// Listen to this to get notified when the [_ws] tries to reconnect. Stream get wsConnectionStatusStream => _wsConnectionStatusController.stream.distinct(); @@ -215,19 +216,9 @@ class StreamChatClient { }; /// - Logger detachedLogger( - String name, - ) => - Logger.detached(name) - ..level = logLevel - ..onRecord.listen(logHandlerFunction); - - void _setupLogger(LogHandlerFunction? logHandlerFunction) { - logger.level = logLevel; - this.logHandlerFunction = logHandlerFunction ?? _defaultLogHandler; - logger.onRecord.listen(this.logHandlerFunction); - logger.info('logger setup'); - } + Logger detachedLogger(String name) => Logger.detached(name) + ..level = logLevel + ..onRecord.listen(logHandlerFunction); /// Connects the current user, this triggers a connection to the API. /// It returns a [Future] that resolves when the connection is setup. @@ -325,9 +316,13 @@ class StreamChatClient { _wsConnectionStatus = ConnectionStatus.connecting; - _connectionStatusSubscription = _ws.connectionStatusStream.listen( - _connectionStatusHandler, - ); + // skipping `ws` seed connection status -> ConnectionStatus.disconnected + // otherwise `client.wsConnectionStatusStream` will emit in order + // 1. ConnectionStatus.disconnected -> client seed status + // 2. ConnectionStatus.connecting -> client connecting status + // 3. ConnectionStatus.disconnected -> ws seed status + _connectionStatusSubscription = + _ws.connectionStatusStream.skip(1).listen(_connectionStatusHandler); try { return await _ws.connect(user); @@ -768,13 +763,13 @@ class StreamChatClient { _chatApi.device.removeDevice(id); /// Get a development token - String devToken(String userId) => Token.development(userId).rawValue; + Token devToken(String userId) => Token.development(userId); /// Returns a channel client with the given type, id and custom data. Channel channel( String type, { String? id, - Map extraData = const {}, + Map? extraData, }) { if (id != null && state.channels.containsKey('$type:$id')) { return state.channels['$type:$id']!; @@ -796,6 +791,7 @@ class StreamChatClient { ); /// watches the provided channel + /// Creates first if not yet created Future watchChannel( String channelType, { String? channelId, @@ -809,6 +805,7 @@ class StreamChatClient { ); /// Query the API, get messages, members or other channel fields + /// Creates the channel first if not yet created Future queryChannel( String channelType, { bool state = true, @@ -1139,22 +1136,22 @@ class StreamChatClient { /// Lists all the message replies for the [parentId] Future getReplies( - String parentId, - PaginationParams options, - ) => + String parentId, { + PaginationParams? options, + }) => _chatApi.message.getReplies( parentId, - options, + options: options, ); /// Get all the reactions for a [messageId] Future getReactions( - String messageId, - PaginationParams options, - ) => + String messageId, { + PaginationParams? options, + }) => _chatApi.message.getReactions( messageId, - options, + options: options, ); /// Update the given message diff --git a/packages/stream_chat/lib/src/core/api/message_api.dart b/packages/stream_chat/lib/src/core/api/message_api.dart index 6d80f35b..22f7bfef 100644 --- a/packages/stream_chat/lib/src/core/api/message_api.dart +++ b/packages/stream_chat/lib/src/core/api/message_api.dart @@ -142,13 +142,13 @@ class MessageApi { /// Get all the reactions for a [messageId] Future getReactions( - String messageId, - PaginationParams options, - ) async { + String messageId, { + PaginationParams? options, + }) async { final response = await _client.get( '/messages/$messageId/reactions', queryParameters: { - ...options.toJson(), + if (options != null) ...options.toJson(), }, ); return QueryReactionsResponse.fromJson(response.data); @@ -168,13 +168,13 @@ class MessageApi { /// Lists all the message replies for the [parentId] Future getReplies( - String parentId, - PaginationParams options, - ) async { + String parentId, { + PaginationParams? options, + }) async { final response = await _client.get( '/messages/$parentId/replies', queryParameters: { - ...options.toJson(), + if (options != null) ...options.toJson(), }, ); return QueryRepliesResponse.fromJson(response.data); diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index 41b50360..22d04a43 100644 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -1490,7 +1490,6 @@ void main() { test('`.getReplies`', () async { const parentId = 'test-parent-id'; - const options = PaginationParams(); final messages = List.generate( 3, @@ -1500,22 +1499,21 @@ void main() { ), ); - when(() => client.getReplies(parentId, options)).thenAnswer( + when(() => client.getReplies(parentId)).thenAnswer( (_) async => QueryRepliesResponse()..messages = messages, ); - final res = await channel.getReplies(parentId, options); + final res = await channel.getReplies(parentId); expect(res, isNotNull); expect(res.messages.length, messages.length); expect(res.messages.every((it) => it.parentId == parentId), isTrue); - verify(() => client.getReplies(parentId, options)).called(1); + verify(() => client.getReplies(parentId)).called(1); }); test('`.getReactions`', () async { const messageId = 'test-message-id'; - const options = PaginationParams(); final reactions = List.generate( 3, @@ -1525,17 +1523,17 @@ void main() { ), ); - when(() => client.getReactions(messageId, options)).thenAnswer( + when(() => client.getReactions(messageId)).thenAnswer( (_) async => QueryReactionsResponse()..reactions = reactions, ); - final res = await channel.getReactions(messageId, options); + final res = await channel.getReactions(messageId); expect(res, isNotNull); expect(res.reactions.length, reactions.length); expect(res.reactions.every((it) => it.messageId == messageId), isTrue); - verify(() => client.getReactions(messageId, options)).called(1); + verify(() => client.getReactions(messageId)).called(1); }); test('`.getMessagesById`', () async { diff --git a/packages/stream_chat/test/src/api/client_test.dart b/packages/stream_chat/test/src/api/client_test.dart new file mode 100644 index 00000000..b0b30c83 --- /dev/null +++ b/packages/stream_chat/test/src/api/client_test.dart @@ -0,0 +1,2135 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/client/client.dart'; +import 'package:stream_chat/src/core/api/device_api.dart'; +import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/error/error.dart'; +import 'package:stream_chat/src/core/http/token.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/filter.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/own_user.dart'; +import 'package:stream_chat/src/core/models/user.dart'; +import 'package:stream_chat/src/event_type.dart'; +import 'package:stream_chat/src/ws/connection_status.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/scaffolding.dart'; +import 'package:test/test.dart'; + +import '../fakes.dart'; +import '../matchers.dart'; +import '../mocks.dart'; +import '../utils.dart'; + +void main() { + group('Fake web-socket connection functions', () { + const apiKey = 'test-api-key'; + late final api = FakeChatApi(); + + late StreamChatClient client; + + setUpAll(() { + // fallback values + registerFallbackValue(FakeUser()); + }); + + setUp(() { + final ws = FakeWebSocket(); + client = StreamChatClient(apiKey, ws: ws, chatApi: api); + }); + + tearDown(() { + client.dispose(); + }); + + test('`.connectUser` should work fine', () async { + final user = User(id: 'test-user-id'); + final token = Token.development(user.id).rawValue; + + final event = Event( + type: EventType.healthCheck, + connectionId: 'fake-connection-id', + me: OwnUser.fromUser(user), + ); + + expectLater( + // skipping first seed status -> ConnectionStatus.disconnected + client.wsConnectionStatusStream.skip(1), + emitsInOrder([ + ConnectionStatus.connecting, + ConnectionStatus.connected, + ]), + ); + + final res = await client.connectUser(user, token); + expect(res, isNotNull); + expect(res.type, event.type); + expect(res.connectionId, event.connectionId); + expect(res.me, isSameUserAs(user)); + }); + + test('`.connectUserWithProvider` should work fine', () async { + final user = User(id: 'test-user-id'); + Future tokenProvider(String userId) async { + expect(userId, user.id); + return Token.development(userId).rawValue; + } + + final event = Event( + type: EventType.healthCheck, + connectionId: 'fake-connection-id', + me: OwnUser.fromUser(user), + ); + + expectLater( + // skipping first seed status -> ConnectionStatus.disconnected + client.wsConnectionStatusStream.skip(1), + emitsInOrder([ + ConnectionStatus.connecting, + ConnectionStatus.connected, + ]), + ); + + final res = await client.connectUserWithProvider(user, tokenProvider); + expect(res, isNotNull); + expect(res.type, event.type); + expect(res.connectionId, event.connectionId); + expect(res.me, isSameUserAs(user)); + }); + + group('`.connectGuestUser`', () { + test('should work fine', () async { + final user = User(id: 'test-user-id'); + final token = Token.development(user.id).rawValue; + + when(() => api.guest.getGuestUser(any(that: isSameUserAs(user)))) + .thenAnswer( + (_) async => ConnectGuestUserResponse() + ..user = user + ..accessToken = token, + ); + + final event = Event( + type: EventType.healthCheck, + connectionId: 'fake-connection-id', + me: OwnUser.fromUser(user), + ); + + expectLater( + // skipping first seed status -> ConnectionStatus.disconnected + client.wsConnectionStatusStream.skip(1), + emitsInOrder([ + ConnectionStatus.connecting, + ConnectionStatus.connected, + ]), + ); + + final res = await client.connectGuestUser(user); + expect(res, isNotNull); + expect(res.type, event.type); + expect(res.connectionId, event.connectionId); + expect(res.me, isSameUserAs(user)); + + verify( + () => api.guest.getGuestUser(any(that: isSameUserAs(user))), + ).called(1); + }); + + test('should throw if `.getGuestUser` fails', () async { + final user = User(id: 'test-user-id'); + + when(() => api.guest.getGuestUser(any(that: isSameUserAs(user)))) + .thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); + + expectLater( + client.wsConnectionStatusStream, + emitsInOrder([ + // only emits the seed -> disconnected status + // as the call never reaches `ws.connect` + ConnectionStatus.disconnected, + ]), + ); + + try { + await client.connectGuestUser(user); + } catch (e) { + expect(e, isA()); + } + + verify( + () => api.guest.getGuestUser(any(that: isSameUserAs(user))), + ).called(1); + }); + }); + + test('`.connectAnonymousUser` should work fine', () async { + expectLater( + // skipping first seed status -> ConnectionStatus.disconnected + client.wsConnectionStatusStream.skip(1), + emitsInOrder([ + ConnectionStatus.connecting, + ConnectionStatus.connected, + ]), + ); + + final res = await client.connectAnonymousUser(); + expect(res, isNotNull); + expect(res.type, EventType.healthCheck); + expect(res.connectionId, 'fake-connection-id'); + expect(res.me, isNotNull); + }); + + group('`.openConnection`', () { + test('should throw if state does not contain user', () async { + expect(client.state.user, isNull); + try { + await client.openConnection(); + } catch (e) { + expect(e, isA()); + } + }); + + test('should throw if connection is already in progress', () async { + expect(client.state.user, isNull); + try { + await client.connectAnonymousUser(); + await client.openConnection(); + } catch (e) { + expect(e, isA()); + final err = e as StreamChatError; + expect( + err.message.contains('Connection already in progress for'), + isTrue, + ); + } + }); + + test('should throw if connection is already available', () async { + expect(client.state.user, isNull); + try { + await client.connectAnonymousUser(); + // waiting 300ms for `wsConnectionStatusStream` to emit + await delay(300); + + await client.openConnection(); + } catch (e) { + expect(e, isA()); + final err = e as StreamChatError; + expect( + err.message.contains('Connection already available for'), + isTrue, + ); + } + }); + + test('should open connection for closed connection', () async { + expectLater( + client.wsConnectionStatusStream.skip(1), + emitsInOrder([ + // initial connectUser + ConnectionStatus.connecting, + ConnectionStatus.connected, + // close connection + ConnectionStatus.disconnected, + // open connection + ConnectionStatus.connecting, + ConnectionStatus.connected, + ]), + ); + + await client.connectAnonymousUser(); + // waiting 300ms for `wsConnectionStatusStream` to emit + await delay(300); + + client.closeConnection(); + + await client.openConnection(); + }); + }); + }); + + group('Fake web-socket connection functions failure', () { + const apiKey = 'test-api-key'; + late final api = FakeChatApi(); + + late StreamChatClient client; + + setUpAll(() { + // fallback values + registerFallbackValue(FakeUser()); + }); + + setUp(() { + final ws = FakeWebSocketWithConnectionError(); + client = StreamChatClient(apiKey, chatApi: api, ws: ws); + }); + + tearDown(() { + client.dispose(); + }); + + test('`.connectUser` should throw if `ws.connect` fails', () async { + final user = User(id: 'test-user-id'); + final token = Token.development(user.id).rawValue; + + try { + await client.connectUser(user, token); + } catch (e) { + expect(e, isA()); + } + }); + + test( + '`.connectUserWithProvider` should throw if `ws.connect` fails', + () async { + final user = User(id: 'test-user-id'); + Future tokenProvider(String userId) async { + expect(userId, user.id); + return Token.development(userId).rawValue; + } + + try { + await client.connectUserWithProvider(user, tokenProvider); + } catch (e) { + expect(e, isA()); + } + }, + ); + + test('`.connectGuestUser` should throw if `ws.connect` fails', () async { + final user = User(id: 'test-user-id'); + final token = Token.development(user.id).rawValue; + + when(() => api.guest.getGuestUser(any(that: isSameUserAs(user)))) + .thenAnswer( + (_) async => ConnectGuestUserResponse() + ..user = user + ..accessToken = token, + ); + + try { + await client.connectGuestUser(user); + } catch (e) { + expect(e, isA()); + } + verify( + () => api.guest.getGuestUser(any(that: isSameUserAs(user))), + ).called(1); + }); + + test( + '`.connectAnonymousUser` should throw if `ws.connect` fails', + () async { + try { + await client.connectAnonymousUser(); + } catch (e) { + expect(e, isA()); + } + }, + ); + }); + + group('Client with connected user with persistence', () { + const apiKey = 'test-api-key'; + late final api = FakeChatApi(); + late final ws = FakeWebSocket(); + late final persistence = MockPersistenceClient(); + + final user = User(id: 'test-user-id'); + final token = Token.development(user.id).rawValue; + + late StreamChatClient client; + + setUpAll(() { + // fallback values + registerFallbackValue(FakeEvent()); + registerFallbackValue(const PaginationParams()); + registerFallbackValue(FakeChannelState()); + }); + + setUp(() async { + client = StreamChatClient(apiKey, chatApi: api, ws: ws) + ..chatPersistenceClient = persistence; + await client.connectUser(user, token); + await delay(300); + expect(client.persistenceEnabled, isTrue); + expect(client.wsConnectionStatus, ConnectionStatus.connected); + }); + + tearDown(() { + client.dispose(); + }); + + group('`.sync`', () { + test( + 'should update persistence connectionInfo and lastSync when sync succeeds', + () async { + const cids = ['test-cid-1', 'test-cid-2', 'test-cid-3']; + final lastSyncAt = DateTime.now(); + + when(() => api.general.sync(cids, lastSyncAt)) + .thenAnswer((_) async => SyncResponse() + ..events = [ + Event( + isLocal: false, + type: EventType.healthCheck, + connectionId: 'test-connection-id', + me: OwnUser.fromUser(user), + ), + Event( + isLocal: false, + type: EventType.messageDeleted, + message: Message(id: 'test-message-id'), + ), + ]); + + when(() => persistence.updateConnectionInfo(any())) + .thenAnswer((_) => Future.value()); + when(() => persistence.updateLastSyncAt(any())) + .thenAnswer((_) => Future.value()); + + await client.sync(cids: cids, lastSyncAt: lastSyncAt); + + verify(() => persistence.updateConnectionInfo(any())).called(1); + verify(() => persistence.updateLastSyncAt(any())).called(1); + verify(() => api.general.sync(cids, lastSyncAt)).called(1); + }, + ); + + test( + 'should work fine if persistence contains sync params', + () async { + const cids = ['test-cid-1', 'test-cid-2', 'test-cid-3']; + final lastSyncAt = DateTime.now(); + + when(persistence.getChannelCids).thenAnswer((_) async => cids); + when(persistence.getLastSyncAt).thenAnswer((_) async => lastSyncAt); + + when(() => api.general.sync(cids, lastSyncAt)) + .thenAnswer((_) async => SyncResponse() + ..events = [ + Event( + isLocal: false, + type: EventType.healthCheck, + connectionId: 'test-connection-id', + me: OwnUser.fromUser(user), + ), + Event( + isLocal: false, + type: EventType.messageDeleted, + message: Message(id: 'test-message-id', text: 'Hey!'), + ), + ]); + + when(() => persistence.updateConnectionInfo(any())) + .thenAnswer((_) => Future.value()); + when(() => persistence.updateLastSyncAt(any())) + .thenAnswer((_) => Future.value()); + + await client.sync(); + + verify(() => persistence.updateConnectionInfo(any())).called(1); + verify(() => persistence.updateLastSyncAt(any())).called(1); + verify(() => api.general.sync(cids, lastSyncAt)).called(1); + verify(persistence.getChannelCids).called(1); + verify(persistence.getLastSyncAt).called(1); + }, + ); + }); + + group('`.queryChannels`', () { + test( + 'should emit channels twice if persistence contains some channels', + () async { + final persistentChannelStates = List.generate( + 3, + (index) => ChannelState( + channel: ChannelModel(cid: 'p-test-type-$index:p-test-id-$index'), + ), + ); + + when(() => persistence.getChannelStates( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + paginationParams: any(named: 'paginationParams'), + )).thenAnswer((_) async => persistentChannelStates); + + final channelStates = List.generate( + 3, + (index) => ChannelState( + channel: ChannelModel(cid: 'test-type-$index:test-id-$index'), + ), + ); + + when(() => api.channel.queryChannels( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), + paginationParams: any(named: 'paginationParams'), + )).thenAnswer( + (_) async => QueryChannelsResponse()..channels = channelStates, + ); + + when(() => persistence.getChannelThreads(any())) + .thenAnswer((_) async => {}); + when(() => persistence.updateMessages(any(), any())) + .thenAnswer((_) => Future.value()); + when(() => persistence.getChannelStateByCid(any(), + messagePagination: any(named: 'messagePagination'), + pinnedMessagePagination: + any(named: 'pinnedMessagePagination'))).thenAnswer( + (invocation) async => ChannelState( + channel: ChannelModel(cid: invocation.positionalArguments.first), + ), + ); + when(() => persistence.updateChannelQueries(any(), any(), + clearQueryCache: any(named: 'clearQueryCache'))) + .thenAnswer((_) => Future.value()); + + expectLater( + client.queryChannels(), + emitsInOrder([ + // emits persistent channels first + persistentChannelStates.map(isCorrectChannelFor), + // makes api call and emits network fetched channels + channelStates.map(isCorrectChannelFor), + ]), + ); + + // Hack as `teardown` gets called even + // before our stream starts emitting data + await delay(300); + + verify(() => persistence.getChannelStates( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + paginationParams: any(named: 'paginationParams'), + )).called(1); + + verify(() => api.channel.queryChannels( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), + paginationParams: any(named: 'paginationParams'), + )).called(1); + + verify(() => persistence.getChannelThreads(any())) + .called((persistentChannelStates + channelStates).length); + verify(() => persistence.updateMessages(any(), any())) + .called((persistentChannelStates + channelStates).length); + verify( + () => persistence.getChannelStateByCid(any(), + messagePagination: any(named: 'messagePagination'), + pinnedMessagePagination: any(named: 'pinnedMessagePagination')), + ).called((persistentChannelStates + channelStates).length); + verify(() => persistence.updateChannelQueries(any(), any(), + clearQueryCache: any(named: 'clearQueryCache'))).called(1); + }, + ); + + test( + 'should never rethrow network call if persistence already emitted some channels', + () async { + final persistentChannelStates = List.generate( + 3, + (index) => ChannelState( + channel: ChannelModel(cid: 'p-test-type-$index:p-test-id-$index'), + ), + ); + + when(() => persistence.getChannelStates( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + paginationParams: any(named: 'paginationParams'), + )).thenAnswer((_) async => persistentChannelStates); + + when(() => api.channel.queryChannels( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), + paginationParams: any(named: 'paginationParams'), + )).thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); + + when(() => persistence.getChannelThreads(any())) + .thenAnswer((_) async => {}); + when(() => persistence.updateMessages(any(), any())) + .thenAnswer((_) => Future.value()); + when(() => persistence.getChannelStateByCid(any(), + messagePagination: any(named: 'messagePagination'), + pinnedMessagePagination: + any(named: 'pinnedMessagePagination'))).thenAnswer( + (invocation) async => ChannelState( + channel: ChannelModel(cid: invocation.positionalArguments.first), + ), + ); + + expectLater( + client.queryChannels(), + emitsInOrder([ + // emits persistent channels + persistentChannelStates.map(isCorrectChannelFor), + ]), + ); + + // Hack as `teardown` gets called even + // before our stream starts emitting data + await delay(300); + + verify(() => persistence.getChannelStates( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + paginationParams: any(named: 'paginationParams'), + )).called(1); + + verify(() => api.channel.queryChannels( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), + paginationParams: any(named: 'paginationParams'), + )).called(1); + + verify(() => persistence.getChannelThreads(any())) + .called(persistentChannelStates.length); + verify(() => persistence.updateMessages(any(), any())) + .called(persistentChannelStates.length); + verify( + () => persistence.getChannelStateByCid(any(), + messagePagination: any(named: 'messagePagination'), + pinnedMessagePagination: any(named: 'pinnedMessagePagination')), + ).called(persistentChannelStates.length); + }, + ); + }); + + test('`.disconnectUser` should reset state and user', () async { + expect(client.state.user, isNotNull); + expect(client.wsConnectionStatus, ConnectionStatus.connected); + + expectLater( + // skipping initial connected value + client.wsConnectionStatusStream.skip(1), + emits(ConnectionStatus.disconnected), + ); + + await client.disconnectUser(); + + expect(client.state.user, isNull); + expect(client.wsConnectionStatus, ConnectionStatus.disconnected); + }); + }); + + group('Client with connected user without persistence', () { + const apiKey = 'test-api-key'; + late final api = FakeChatApi(); + late final ws = FakeWebSocket(); + + final user = User(id: 'test-user-id'); + final token = Token.development(user.id).rawValue; + + late StreamChatClient client; + + setUpAll(() { + // fallback values + registerFallbackValue(FakeEvent()); + registerFallbackValue(FakeMessage()); + registerFallbackValue(const PaginationParams()); + }); + + setUp(() async { + client = StreamChatClient(apiKey, chatApi: api, ws: ws); + await client.connectUser(user, token); + await delay(300); + expect(client.persistenceEnabled, isFalse); + expect(client.wsConnectionStatus, ConnectionStatus.connected); + }); + + tearDown(() { + client.dispose(); + }); + + group('`.sync`', () { + test('should work fine', () async { + const cids = ['test-cid-1', 'test-cid-2', 'test-cid-3']; + final lastSyncAt = DateTime.now(); + + when(() => api.general.sync(cids, lastSyncAt)) + .thenAnswer((_) async => SyncResponse() + ..events = [ + Event( + isLocal: false, + type: EventType.healthCheck, + connectionId: 'test-connection-id', + me: OwnUser.fromUser(user), + ), + Event( + isLocal: false, + type: EventType.messageDeleted, + message: Message(id: 'test-message-id'), + ), + ]); + + await client.sync(cids: cids, lastSyncAt: lastSyncAt); + + verify(() => api.general.sync(cids, lastSyncAt)).called(1); + }); + + test('should return if `cids` is not available', () async { + expect(client.sync, returnsNormally); + verifyNever(() => api.general.sync(any(), any())); + }); + + test('should return if `lastSyncAt` is not available', () async { + expect(() => client.sync(cids: ['test-cid-1']), returnsNormally); + verifyNever(() => api.general.sync(any(), any())); + }); + }); + + group('`.queryChannels`', () { + test('should work fine without persistent channels', () async { + final channelStates = List.generate( + 3, + (index) => ChannelState( + channel: ChannelModel(cid: 'test-type-$index:test-id-$index'), + ), + ); + + when(() => api.channel.queryChannels( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), + paginationParams: any(named: 'paginationParams'), + )).thenAnswer( + (_) async => QueryChannelsResponse()..channels = channelStates, + ); + + expectLater( + client.queryChannels(), + emitsInOrder([channelStates.map(isCorrectChannelFor)]), + ); + + // Hack as `teardown` gets called even + // before our stream starts emitting data + await delay(300); + + verify(() => api.channel.queryChannels( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), + paginationParams: any(named: 'paginationParams'), + )).called(1); + }); + + test( + 'should rethrow if `.queryChannelsOnline` throws and persistence channels are empty', + () async { + when(() => api.channel.queryChannels( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), + paginationParams: any(named: 'paginationParams'), + )).thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); + + expectLater( + client.queryChannels(), + emitsError(isA()), + ); + + // Hack as `teardown` gets called even + // before our stream starts emitting data + await delay(300); + + verify(() => api.channel.queryChannels( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), + paginationParams: any(named: 'paginationParams'), + )).called(1); + }, + ); + }); + + test('`.queryUsers`', () async { + final users = List.generate( + 3, + (index) => User(id: 'test-user-id-$index'), + ); + + when(() => api.user.queryUsers( + presence: any(named: 'presence'), + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + )).thenAnswer((_) async => QueryUsersResponse()..users = users); + + expectLater( + // skipping initial seed event -> {} users + client.state.usersStream.skip(1), + emitsInOrder([ + {for (var user in users) user.id: user}, + ]), + ); + + final res = await client.queryUsers(); + expect(res, isNotNull); + expect(res.users.length, users.length); + + verify(() => api.user.queryUsers( + presence: any(named: 'presence'), + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + )).called(1); + verifyNoMoreInteractions(api.user); + }); + + test('`.search`', () async { + const cid = 'test-type:test-id'; + final filter = Filter.in_('cid', const [cid]); + + final messages = List.generate( + 3, + (index) => GetMessageResponse() + ..channel = ChannelModel(cid: cid) + ..message = Message(id: 'test-message-id-$index'), + ); + + when(() => api.general.searchMessages(filter, + query: any(named: 'query'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + messageFilters: any(named: 'messageFilters'))) + .thenAnswer( + (_) async => SearchMessagesResponse()..results = messages); + + final res = await client.search(filter); + expect(res, isNotNull); + expect(res.results.length, messages.length); + + verify(() => api.general.searchMessages(filter, + query: any(named: 'query'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + messageFilters: any(named: 'messageFilters'))).called(1); + verifyNoMoreInteractions(api.general); + }); + + test('`.sendFile`', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + final file = AttachmentFile(size: 33, path: 'test-file-path'); + + const fileUrl = 'test-file-url'; + + when(() => api.fileUploader.sendFile(file, channelId, channelType)) + .thenAnswer((_) async => SendFileResponse()..file = fileUrl); + + final res = await client.sendFile(file, channelId, channelType); + expect(res, isNotNull); + expect(res.file, fileUrl); + + verify(() => api.fileUploader.sendFile(file, channelId, channelType)) + .called(1); + verifyNoMoreInteractions(api.fileUploader); + }); + + test('`.sendImage`', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + final image = AttachmentFile(size: 33, path: 'test-image-path'); + + const fileUrl = 'test-image-url'; + + when(() => api.fileUploader.sendImage(image, channelId, channelType)) + .thenAnswer((_) async => SendImageResponse()..file = fileUrl); + + final res = await client.sendImage(image, channelId, channelType); + expect(res, isNotNull); + expect(res.file, fileUrl); + + verify(() => api.fileUploader.sendImage(image, channelId, channelType)) + .called(1); + verifyNoMoreInteractions(api.fileUploader); + }); + + test('`.deleteFile`', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const fileUrl = 'test-file-url'; + + when(() => api.fileUploader.deleteFile(fileUrl, channelId, channelType)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.deleteFile(fileUrl, channelId, channelType); + expect(res, isNotNull); + + verify(() => api.fileUploader.deleteFile(fileUrl, channelId, channelType)) + .called(1); + verifyNoMoreInteractions(api.fileUploader); + }); + + test('`.deleteImage`', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const imageUrl = 'test-image-url'; + + when(() => api.fileUploader.deleteImage(imageUrl, channelId, channelType)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.deleteImage(imageUrl, channelId, channelType); + expect(res, isNotNull); + + verify( + () => api.fileUploader.deleteImage(imageUrl, channelId, channelType), + ).called(1); + verifyNoMoreInteractions(api.fileUploader); + }); + + test('`.updateChannel`', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const data = {'name': 'test-channel'}; + + when(() => api.channel.updateChannel(channelId, channelType, data)) + .thenAnswer((invocation) async => UpdateChannelResponse() + ..channel = ChannelModel( + id: channelId, + type: channelType, + extraData: {...data}, + )); + + final res = await client.updateChannel(channelId, channelType, data); + expect(res, isNotNull); + expect(res.channel.cid, '$channelType:$channelId'); + expect(res.channel.extraData['name'], 'test-channel'); + + verify(() => api.channel.updateChannel(channelId, channelType, data)) + .called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.updateChannelPartial`', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const set = { + 'name': 'Stream Team', + 'profile_image': 'test-profile-image', + }; + const unset = ['tag', 'last_name']; + + when(() => api.channel.updateChannelPartial(channelId, channelType, + set: set, unset: unset)) + .thenAnswer((invocation) async => PartialUpdateChannelResponse() + ..channel = ChannelModel( + id: channelId, + type: channelType, + extraData: {...set}, + )); + + final res = await client.updateChannelPartial( + channelId, + channelType, + set: set, + unset: unset, + ); + expect(res, isNotNull); + expect(res.channel.cid, '$channelType:$channelId'); + expect(res.channel.extraData, set); + + verify(() => api.channel.updateChannelPartial(channelId, channelType, + set: set, unset: unset)).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.addDevice`', () async { + const id = 'test-device-id'; + const provider = PushProvider.firebase; + + when(() => api.device.addDevice(id, provider)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.addDevice(id, provider); + expect(res, isNotNull); + + verify(() => api.device.addDevice(id, provider)).called(1); + verifyNoMoreInteractions(api.device); + }); + + test('`.getDevices`', () async { + final devices = List.generate( + 3, + (index) => Device( + id: 'test-device-id-$index', + pushProvider: PushProvider.firebase.name, + ), + ); + + when(() => api.device.getDevices()) + .thenAnswer((_) async => ListDevicesResponse()..devices = devices); + + final res = await client.getDevices(); + expect(res, isNotNull); + expect(res.devices.length, devices.length); + + verify(() => api.device.getDevices()).called(1); + verifyNoMoreInteractions(api.device); + }); + + test('`.removeDevice`', () async { + const deviceId = 'test-device-id'; + + when(() => api.device.removeDevice(deviceId)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.removeDevice(deviceId); + expect(res, isNotNull); + + verify(() => api.device.removeDevice(deviceId)).called(1); + verifyNoMoreInteractions(api.device); + }); + + test('`.devToken`', () async { + const userId = 'test-user-id'; + + final token = client.devToken(userId); + + expect(token, isNotNull); + expect(token.userId, userId); + expect(token.authType, AuthType.jwt); + }); + + group('`.channel`', () { + test('should return back a new channel instance', () { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelData = {'name': 'test-channel-name'}; + + final channel = client.channel( + channelType, + id: channelId, + extraData: channelData, + ); + + expect(channel, isNotNull); + expect(channel.type, channelType); + expect(channel.id, channelId); + expect(channel.cid, '$channelType:$channelId'); + expect(channel.extraData, channelData); + }); + + test('should return back in memory channel instance if available', + () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelData = {'name': 'test-channel-name'}; + const channelCid = '$channelType:$channelId'; + + final channel = client.channel( + channelType, + id: channelId, + extraData: channelData, + ); + + final channelState = ChannelState( + channel: ChannelModel(cid: channelCid), + ); + + when(() => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + )).thenAnswer((_) async => channelState); + + expectLater( + client.state.channelsStream.skip(1), + emitsInOrder([ + {channelCid: isCorrectChannelFor(channelState)} + ]), + ); + + await channel.watch(); + + final newChannel = client.channel(channelType, id: channelId); + expect(newChannel, channel); + + verify(() => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + )).called(1); + }); + }); + + test('`.createChannel`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelData = {'name': 'test-channel-name'}; + const channelCid = '$channelType:$channelId'; + + final channelState = ChannelState( + channel: ChannelModel(cid: channelCid, extraData: channelData), + ); + + when(() => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + )).thenAnswer((_) async => channelState); + + final res = await client.createChannel( + channelType, + channelId: channelId, + channelData: channelData, + ); + + expect(res, isNotNull); + expect(res.channel, isNotNull); + final channel = res.channel!; + expect(channel.type, channelType); + expect(channel.id, channelId); + expect(channel.cid, '$channelType:$channelId'); + expect(channel.extraData, channelData); + + verify(() => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + )).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.watchChannel`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelData = {'name': 'test-channel-name'}; + const channelCid = '$channelType:$channelId'; + + final channelState = ChannelState( + channel: ChannelModel(cid: channelCid, extraData: channelData), + ); + + when(() => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + )).thenAnswer((_) async => channelState); + + final res = await client.watchChannel( + channelType, + channelId: channelId, + channelData: channelData, + ); + + expect(res, isNotNull); + expect(res.channel, isNotNull); + final channel = res.channel!; + expect(channel.type, channelType); + expect(channel.id, channelId); + expect(channel.cid, '$channelType:$channelId'); + expect(channel.extraData, channelData); + + verify(() => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + )).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.queryChannel`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelData = {'name': 'test-channel-name'}; + const channelCid = '$channelType:$channelId'; + + final channelState = ChannelState( + channel: ChannelModel(cid: channelCid, extraData: channelData), + ); + + when(() => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + )).thenAnswer((_) async => channelState); + + final res = await client.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + ); + + expect(res, isNotNull); + expect(res.channel, isNotNull); + final channel = res.channel!; + expect(channel.type, channelType); + expect(channel.id, channelId); + expect(channel.cid, '$channelType:$channelId'); + expect(channel.extraData, channelData); + + verify(() => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + )).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.queryMembers`', () async { + const channelType = 'test-channel-type'; + + final members = List.generate( + 3, + (index) => Member(userId: 'test-user-id-$index'), + ); + + when(() => api.general.queryMembers(channelType)).thenAnswer( + (_) async => QueryMembersResponse()..members = members, + ); + + final res = await client.queryMembers(channelType); + expect(res, isNotNull); + expect(res.members.length, members.length); + + verify(() => api.general.queryMembers(channelType)).called(1); + verifyNoMoreInteractions(api.general); + }); + + test('`.hideChannel`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + + when(() => api.channel.hideChannel(channelId, channelType)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.hideChannel(channelId, channelType); + + expect(res, isNotNull); + + verify(() => api.channel.hideChannel(channelId, channelType)).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.showChannel`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + + when(() => api.channel.showChannel(channelId, channelType)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.showChannel(channelId, channelType); + + expect(res, isNotNull); + + verify(() => api.channel.showChannel(channelId, channelType)).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.deleteChannel`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + + when(() => api.channel.deleteChannel(channelId, channelType)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.deleteChannel(channelId, channelType); + + expect(res, isNotNull); + + verify(() => api.channel.deleteChannel(channelId, channelType)).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.truncateChannel`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + + when(() => api.channel.truncateChannel(channelId, channelType)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.truncateChannel(channelId, channelType); + + expect(res, isNotNull); + + verify( + () => api.channel.truncateChannel(channelId, channelType), + ).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.muteChannel`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelCid = '$channelType:$channelId'; + + when(() => api.moderation.muteChannel(channelCid)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.muteChannel(channelCid); + + expect(res, isNotNull); + + verify(() => api.moderation.muteChannel(channelCid)).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.unmuteChannel`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelCid = '$channelType:$channelId'; + + when(() => api.moderation.unmuteChannel(channelCid)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.unmuteChannel(channelCid); + + expect(res, isNotNull); + + verify(() => api.moderation.unmuteChannel(channelCid)).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.acceptChannelInvite`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelCid = '$channelType:$channelId'; + + when(() => api.channel.acceptChannelInvite(channelId, channelType)) + .thenAnswer((_) async => + AcceptInviteResponse()..channel = ChannelModel(cid: channelCid)); + + final res = await client.acceptChannelInvite(channelId, channelType); + expect(res, isNotNull); + expect(res.channel.cid, channelCid); + + verify(() => api.channel.acceptChannelInvite(channelId, channelType)) + .called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.rejectChannelInvite`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelCid = '$channelType:$channelId'; + + when(() => api.channel.rejectChannelInvite(channelId, channelType)) + .thenAnswer((_) async => + RejectInviteResponse()..channel = ChannelModel(cid: channelCid)); + + final res = await client.rejectChannelInvite(channelId, channelType); + expect(res, isNotNull); + expect(res.channel.cid, channelCid); + + verify(() => api.channel.rejectChannelInvite(channelId, channelType)) + .called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.addChannelMembers`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelCid = '$channelType:$channelId'; + + final members = List.generate( + 3, + (index) => Member(userId: 'test-user-id-$index'), + ); + + final memberIds = members.map((e) => e.userId!).toList(growable: false); + + when(() => api.channel.addMembers(channelId, channelType, memberIds)) + .thenAnswer((_) async => AddMembersResponse() + ..channel = ChannelModel(cid: channelCid) + ..members = members); + + final res = await client.addChannelMembers( + channelId, + channelType, + memberIds, + ); + + expect(res, isNotNull); + expect(res.channel.cid, channelCid); + expect(res.members.length, memberIds.length); + + verify( + () => api.channel.addMembers(channelId, channelType, memberIds), + ).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.removeChannelMembers`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelCid = '$channelType:$channelId'; + + final members = List.generate( + 3, + (index) => Member(userId: 'test-user-id-$index'), + ); + + final memberIds = members.map((e) => e.userId!).toList(growable: false); + + when(() => api.channel.removeMembers(channelId, channelType, memberIds)) + .thenAnswer((_) async => RemoveMembersResponse() + ..channel = ChannelModel(cid: channelCid) + ..members = members); + + final res = await client.removeChannelMembers( + channelId, + channelType, + memberIds, + ); + + expect(res, isNotNull); + expect(res.channel.cid, channelCid); + expect(res.members.length, memberIds.length); + + verify( + () => api.channel.removeMembers(channelId, channelType, memberIds), + ).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.inviteChannelMembers`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelCid = '$channelType:$channelId'; + + final members = List.generate( + 3, + (index) => Member(userId: 'test-user-id-$index'), + ); + + final memberIds = members.map((e) => e.userId!).toList(growable: false); + + when(() => api.channel + .inviteChannelMembers(channelId, channelType, memberIds)) + .thenAnswer((_) async => InviteMembersResponse() + ..channel = ChannelModel(cid: channelCid) + ..members = members); + + final res = await client.inviteChannelMembers( + channelId, + channelType, + memberIds, + ); + + expect(res, isNotNull); + expect(res.channel.cid, channelCid); + expect(res.members.length, memberIds.length); + + verify(() => api.channel + .inviteChannelMembers(channelId, channelType, memberIds)).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.stopChannelWatching`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + + when(() => api.channel.stopWatching(channelId, channelType)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.stopChannelWatching(channelId, channelType); + expect(res, isNotNull); + + verify(() => api.channel.stopWatching(channelId, channelType)).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.sendAction`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const messageId = 'test-message-id'; + const formData = {'key': 'value'}; + + when(() => api.message + .sendAction(channelId, channelType, messageId, formData)) + .thenAnswer((_) async => SendActionResponse()); + + final res = await client.sendAction( + channelId, + channelType, + messageId, + formData, + ); + + expect(res, isNotNull); + + verify(() => api.message + .sendAction(channelId, channelType, messageId, formData)).called(1); + verifyNoMoreInteractions(api.message); + }); + + test('`.markChannelRead`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + + when(() => api.channel.markRead(channelId, channelType)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.markChannelRead(channelId, channelType); + + expect(res, isNotNull); + + verify(() => api.channel.markRead(channelId, channelType)).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.updateUser`', () async { + final user = User( + id: 'test-user-id', + extraData: const {'name': 'test-user'}, + ); + + when(() => api.user.updateUsers([user])).thenAnswer( + (_) async => UpdateUsersResponse()..users = {user.id: user}); + + final res = await client.updateUser(user); + + expect(res, isNotNull); + expect(res.users, {user.id: user}); + + verify(() => api.user.updateUsers([user])).called(1); + verifyNoMoreInteractions(api.user); + }); + + test('`.banUser`', () async { + const userId = 'test-user-id'; + + when(() => api.moderation.banUser(userId, options: any(named: 'options'))) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.banUser(userId); + + expect(res, isNotNull); + + verify( + () => api.moderation.banUser(userId, options: any(named: 'options')), + ).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.unbanUser`', () async { + const userId = 'test-user-id'; + + when(() => + api.moderation.unbanUser(userId, options: any(named: 'options'))) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.unbanUser(userId); + + expect(res, isNotNull); + + verify( + () => api.moderation.unbanUser(userId, options: any(named: 'options')), + ).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.shadowBan`', () async { + const userId = 'test-user-id'; + + when(() => api.moderation.banUser(userId, options: {'shadow': true})) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.shadowBan(userId); + + expect(res, isNotNull); + + verify( + () => api.moderation.banUser(userId, options: {'shadow': true}), + ).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.removeShadowBan`', () async { + const userId = 'test-user-id'; + + when(() => api.moderation.unbanUser(userId, options: {'shadow': true})) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.removeShadowBan(userId); + + expect(res, isNotNull); + + verify( + () => api.moderation.unbanUser(userId, options: {'shadow': true}), + ).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.muteUser`', () async { + const userId = 'test-user-id'; + + when(() => api.moderation.muteUser(userId)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.muteUser(userId); + + expect(res, isNotNull); + + verify(() => api.moderation.muteUser(userId)).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.unmuteUser`', () async { + const userId = 'test-user-id'; + + when(() => api.moderation.unmuteUser(userId)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.unmuteUser(userId); + + expect(res, isNotNull); + + verify(() => api.moderation.unmuteUser(userId)).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.flagMessage`', () async { + const messageId = 'test-message-id'; + + when(() => api.moderation.flagMessage(messageId)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.flagMessage(messageId); + + expect(res, isNotNull); + + verify(() => api.moderation.flagMessage(messageId)).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.unflagMessage`', () async { + const messageId = 'test-message-id'; + + when(() => api.moderation.unflagMessage(messageId)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.unflagMessage(messageId); + + expect(res, isNotNull); + + verify(() => api.moderation.unflagMessage(messageId)).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.flagUser`', () async { + const userId = 'test-message-id'; + + when(() => api.moderation.flagUser(userId)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.flagUser(userId); + + expect(res, isNotNull); + + verify(() => api.moderation.flagUser(userId)).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.unflagUser`', () async { + const userId = 'test-message-id'; + + when(() => api.moderation.unflagUser(userId)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.unflagUser(userId); + + expect(res, isNotNull); + + verify(() => api.moderation.unflagUser(userId)).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.markAllRead`', () async { + when(() => api.channel.markAllRead()) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.markAllRead(); + expect(res, isNotNull); + + verify(() => api.channel.markAllRead()).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.sendEvent`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + final event = Event(type: EventType.any); + + when( + () => api.channel.sendEvent( + channelId, + channelType, + any(that: isSameEventAs(event)), + ), + ).thenAnswer((_) async => EmptyResponse()); + + final res = await client.sendEvent(channelId, channelType, event); + expect(res, isNotNull); + + verify(() => api.channel.sendEvent( + channelId, + channelType, + any(that: isSameEventAs(event)), + )).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.sendReaction`', () async { + const messageId = 'test-message-id'; + const reactionType = 'like'; + + when(() => api.message.sendReaction(messageId, reactionType)) + .thenAnswer((_) async => SendReactionResponse() + ..message = Message(id: messageId) + ..reaction = Reaction(type: reactionType, messageId: messageId)); + + final res = await client.sendReaction(messageId, reactionType); + expect(res, isNotNull); + expect(res.message.id, messageId); + expect(res.reaction.type, reactionType); + expect(res.reaction.messageId, messageId); + + verify(() => api.message.sendReaction(messageId, reactionType)).called(1); + verifyNoMoreInteractions(api.message); + }); + + test('`.deleteReaction`', () async { + const messageId = 'test-message-id'; + const reactionType = 'like'; + + when(() => api.message.deleteReaction(messageId, reactionType)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.deleteReaction(messageId, reactionType); + expect(res, isNotNull); + + verify( + () => api.message.deleteReaction(messageId, reactionType), + ).called(1); + verifyNoMoreInteractions(api.message); + }); + + test('`.sendMessage`', () async { + final message = Message(id: 'test-message-id'); + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + + when(() => api.message.sendMessage( + channelId, channelType, any(that: isSameMessageAs(message)))) + .thenAnswer((_) async => SendMessageResponse()..message = message); + + final res = await client.sendMessage(message, channelId, channelType); + expect(res, isNotNull); + expect(res.message, isSameMessageAs(message)); + + verify(() => api.message.sendMessage( + channelId, + channelType, + any(that: isSameMessageAs(message)), + )).called(1); + verifyNoMoreInteractions(api.message); + }); + + test('`.getReplies`', () async { + const parentId = 'test-parent-id'; + + final messages = List.generate( + 3, + (index) => Message(id: 'test-message-id-$index'), + ); + + when(() => api.message.getReplies(parentId)) + .thenAnswer((_) async => QueryRepliesResponse()..messages = messages); + + final res = await client.getReplies(parentId); + expect(res, isNotNull); + expect(res.messages.length, messages.length); + + verify(() => api.message.getReplies(parentId)).called(1); + verifyNoMoreInteractions(api.message); + }); + + test('`.getReactions`', () async { + const messageId = 'test-parent-id'; + + final reactions = List.generate( + 3, + (index) => Reaction( + type: 'test-reactions-type-$index', + messageId: messageId, + ), + ); + + when(() => api.message.getReactions(messageId)).thenAnswer( + (_) async => QueryReactionsResponse()..reactions = reactions); + + final res = await client.getReactions(messageId); + expect(res, isNotNull); + expect(res.reactions.length, reactions.length); + expect(res.reactions.every((it) => it.messageId == messageId), isTrue); + + verify(() => api.message.getReactions(messageId)).called(1); + verifyNoMoreInteractions(api.message); + }); + + test('`.updateMessage`', () async { + final message = Message(id: 'test-message-id', text: 'Hello!'); + + when(() => api.message.updateMessage(any(that: isSameMessageAs(message)))) + .thenAnswer((_) async => UpdateMessageResponse()..message = message); + + final res = await client.updateMessage(message); + expect(res, isNotNull); + expect(res.message, isSameMessageAs(message)); + + verify( + () => api.message.updateMessage(any(that: isSameMessageAs(message))), + ).called(1); + verifyNoMoreInteractions(api.message); + }); + + test('`.deleteMessage`', () async { + const messageId = 'test-message-id'; + + when(() => api.message.deleteMessage(messageId)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.deleteMessage(messageId); + expect(res, isNotNull); + + verify(() => api.message.deleteMessage(messageId)).called(1); + verifyNoMoreInteractions(api.message); + }); + + test('`.getMessage`', () async { + const messageId = 'test-message-id'; + final message = Message(id: messageId); + + when(() => api.message.getMessage(messageId)) + .thenAnswer((_) async => GetMessageResponse()..message = message); + + final res = await client.getMessage(messageId); + expect(res, isNotNull); + expect(res.message.id, messageId); + + verify(() => api.message.getMessage(messageId)).called(1); + verifyNoMoreInteractions(api.message); + }); + + test('`.getMessagesById`', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const messageIds = ['test-message-id']; + + final messages = messageIds.map((id) => Message(id: id)).toList(); + + when( + () => api.message.getMessagesById(channelId, channelType, messageIds), + ).thenAnswer((_) async => GetMessagesByIdResponse()..messages = messages); + + final res = await client.getMessagesById( + channelId, + channelType, + messageIds, + ); + expect(res, isNotNull); + expect(res.messages.length, messageIds.length); + + verify( + () => api.message.getMessagesById(channelId, channelType, messageIds), + ).called(1); + verifyNoMoreInteractions(api.message); + }); + + test('`.translateMessage`', () async { + const messageId = 'test-message-id'; + const language = 'hi'; // Hindi + const translatedMessageText = 'नमस्ते'; + final translatedMessage = TranslatedMessage(const { + language: translatedMessageText, + }); + + when(() => api.message.translateMessage(messageId, language)).thenAnswer( + (_) async => TranslateMessageResponse()..message = translatedMessage, + ); + + final res = await client.translateMessage(messageId, language); + + expect(res, isNotNull); + expect(res.message.i18n, translatedMessage.i18n); + + verify(() => api.message.translateMessage(messageId, language)).called(1); + verifyNoMoreInteractions(api.message); + }); + + test('`.partialUpdateMessage`', () async { + const messageId = 'test-message-id'; + final message = Message(id: messageId); + + const set = {'text': 'Update Message text'}; + const unset = ['pinExpires']; + + final updateMessageResponse = UpdateMessageResponse() + ..message = message.copyWith(text: set['text'], pinExpires: null); + + when(() => api.message.partialUpdateMessage( + message.id, + set: set, + unset: unset, + )).thenAnswer((_) async => updateMessageResponse); + + final res = await client.partialUpdateMessage( + messageId, + set: set, + unset: unset, + ); + + expect(res, isNotNull); + expect(res.message.id, message.id); + expect(res.message.id, message.id); + expect(res.message.text, set['text']); + expect(res.message.pinExpires, isNull); + + verify(() => api.message.partialUpdateMessage( + message.id, + set: set, + unset: unset, + )).called(1); + verifyNoMoreInteractions(api.message); + }); + + group('`.pinMessage`', () { + test('should work fine without passing timeoutOrExpirationDate', + () async { + const messageId = 'test-message-id'; + final message = Message(id: messageId); + + when(() => api.message.partialUpdateMessage( + messageId, + set: any(named: 'set'), + unset: any(named: 'unset'), + )).thenAnswer((_) async => UpdateMessageResponse() + ..message = message.copyWith( + pinned: true, + pinExpires: null, + status: MessageSendingStatus.sent, + )); + + final res = await client.pinMessage(messageId); + + expect(res, isNotNull); + expect(res.message.pinned, isTrue); + expect(res.message.pinExpires, isNull); + + verify(() => api.message.partialUpdateMessage( + messageId, + set: any(named: 'set'), + unset: any(named: 'unset'), + )).called(1); + verifyNoMoreInteractions(api.message); + }); + + test( + 'should work fine if passed timeoutOrExpirationDate as num(seconds)', + () async { + const messageId = 'test-message-id'; + final message = Message(id: messageId); + const timeoutOrExpirationDate = 300; // 300 seconds + + when(() => api.message.partialUpdateMessage( + message.id, + set: any(named: 'set'), + unset: any(named: 'unset'), + )).thenAnswer((_) async => UpdateMessageResponse() + ..message = message.copyWith( + pinned: true, + pinExpires: DateTime.now().add( + const Duration(seconds: timeoutOrExpirationDate), + ), + status: MessageSendingStatus.sent, + )); + + final res = await client.pinMessage( + messageId, + timeoutOrExpirationDate: timeoutOrExpirationDate, + ); + + expect(res, isNotNull); + expect(res.message.pinned, isTrue); + expect(res.message.pinExpires, isNotNull); + + verify(() => api.message.partialUpdateMessage( + messageId, + set: any(named: 'set'), + unset: any(named: 'unset'), + )).called(1); + verifyNoMoreInteractions(api.message); + }, + ); + + test( + 'should work fine if passed timeoutOrExpirationDate as DateTime', + () async { + const messageId = 'test-message-id'; + final message = Message(id: messageId); + final timeoutOrExpirationDate = + DateTime.now().add(const Duration(days: 3)); // 3 days + + when(() => api.message.partialUpdateMessage( + messageId, + set: any(named: 'set'), + unset: any(named: 'unset'), + )).thenAnswer((_) async => UpdateMessageResponse() + ..message = message.copyWith( + pinned: true, + pinExpires: timeoutOrExpirationDate, + status: MessageSendingStatus.sent, + )); + + final res = await client.pinMessage( + messageId, + timeoutOrExpirationDate: timeoutOrExpirationDate, + ); + + expect(res, isNotNull); + expect(res.message.pinned, isTrue); + expect(res.message.pinExpires, isNotNull); + expect(res.message.pinExpires, timeoutOrExpirationDate.toUtc()); + + verify(() => api.message.partialUpdateMessage( + messageId, + set: any(named: 'set'), + unset: any(named: 'unset'), + )).called(1); + verifyNoMoreInteractions(api.message); + }, + ); + + test( + 'should throw if invalid timeoutOrExpirationDate is passed', + () async { + const messageId = 'test-message-id'; + const timeoutOrExpirationDate = 'invalid-value'; + + try { + await client.pinMessage( + messageId, + timeoutOrExpirationDate: timeoutOrExpirationDate, + ); + } catch (e) { + expect(e, isA()); + } + }, + ); + }); + + test('`.unpinMessage`', () async { + const messageId = 'test-message-id'; + final message = Message(id: messageId, pinned: true); + + when(() => api.message.partialUpdateMessage( + messageId, + set: {'pinned': false}, + )).thenAnswer((_) async => UpdateMessageResponse() + ..message = message.copyWith( + pinned: false, + status: MessageSendingStatus.sent, + )); + + final res = await client.unpinMessage(messageId); + + expect(res, isNotNull); + expect(res.message.pinned, isFalse); + + verify(() => api.message.partialUpdateMessage( + messageId, + set: {'pinned': false}, + )).called(1); + verifyNoMoreInteractions(api.message); + }); + }); +} diff --git a/packages/stream_chat/test/src/core/api/message_api_test.dart b/packages/stream_chat/test/src/core/api/message_api_test.dart index 041c18f5..d7fa93cd 100644 --- a/packages/stream_chat/test/src/core/api/message_api_test.dart +++ b/packages/stream_chat/test/src/core/api/message_api_test.dart @@ -252,7 +252,7 @@ void main() { 'reactions': [...reactions.map((it) => it.toJson())] })); - final res = await messageApi.getReactions(messageId, options); + final res = await messageApi.getReactions(messageId, options: options); expect(res, isNotNull); expect(res.reactions.length, reactions.length); @@ -311,7 +311,7 @@ void main() { 'messages': [...messages.map((it) => it.toJson())] })); - final res = await messageApi.getReplies(parentId, options); + final res = await messageApi.getReplies(parentId, options: options); expect(res, isNotNull); expect(res.messages.length, messages.length); diff --git a/packages/stream_chat/test/src/fakes.dart b/packages/stream_chat/test/src/fakes.dart index 74280a0b..15d5991c 100644 --- a/packages/stream_chat/test/src/fakes.dart +++ b/packages/stream_chat/test/src/fakes.dart @@ -1,5 +1,8 @@ +import 'dart:async'; + import 'package:dio/dio.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/core/api/channel_api.dart'; import 'package:stream_chat/src/core/api/device_api.dart'; import 'package:stream_chat/src/core/api/general_api.dart'; @@ -10,6 +13,7 @@ import 'package:stream_chat/src/core/api/user_api.dart'; import 'package:stream_chat/src/core/api/guest_api.dart'; import 'package:stream_chat/src/core/http/token.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; +import 'package:stream_chat/src/ws/websocket.dart'; import 'package:stream_chat/stream_chat.dart'; import 'mocks.dart'; @@ -96,3 +100,88 @@ class FakeMessage extends Fake implements Message {} class FakeAttachmentFile extends Fake implements AttachmentFile {} class FakeEvent extends Fake implements Event {} + +class FakeUser extends Fake implements User {} + +class FakeWebSocket extends Fake implements WebSocket { + BehaviorSubject? _connectionStatusController; + + BehaviorSubject get connectionStatusController => + _connectionStatusController ??= + BehaviorSubject.seeded(ConnectionStatus.disconnected); + + set connectionStatus(ConnectionStatus value) { + connectionStatusController.add(value); + } + + @override + ConnectionStatus get connectionStatus => connectionStatusController.value; + + @override + Stream get connectionStatusStream => + connectionStatusController.stream; + + @override + Completer? connectionCompleter; + + @override + Future connect(User user) async { + connectionStatus = ConnectionStatus.connecting; + final event = Event( + type: EventType.healthCheck, + connectionId: 'fake-connection-id', + me: OwnUser.fromUser(user), + ); + connectionCompleter = Completer()..complete(event); + connectionStatus = ConnectionStatus.connected; + return connectionCompleter!.future; + } + + @override + void disconnect() { + connectionStatus = ConnectionStatus.disconnected; + connectionCompleter = null; + _connectionStatusController?.close(); + _connectionStatusController = null; + } +} + +class FakeWebSocketWithConnectionError extends Fake implements WebSocket { + BehaviorSubject? _connectionStatusController; + + BehaviorSubject get connectionStatusController => + _connectionStatusController ??= + BehaviorSubject.seeded(ConnectionStatus.disconnected); + + set connectionStatus(ConnectionStatus value) { + connectionStatusController.add(value); + } + + @override + ConnectionStatus get connectionStatus => connectionStatusController.value; + + @override + Stream get connectionStatusStream => + connectionStatusController.stream; + + @override + Completer? connectionCompleter; + + @override + Future connect(User user) async { + connectionStatus = ConnectionStatus.connecting; + const error = StreamWebSocketError('Error Connecting'); + connectionCompleter = Completer()..completeError(error); + return connectionCompleter!.future; + } + + @override + void disconnect() { + connectionStatus = ConnectionStatus.disconnected; + connectionCompleter = null; + _connectionStatusController?.close(); + _connectionStatusController = null; + } +} + +class FakeChannelState extends Fake implements ChannelState {} diff --git a/packages/stream_chat/test/src/matchers.dart b/packages/stream_chat/test/src/matchers.dart index 967f3c21..c039b416 100644 --- a/packages/stream_chat/test/src/matchers.dart +++ b/packages/stream_chat/test/src/matchers.dart @@ -1,7 +1,11 @@ import 'package:collection/collection.dart'; import 'package:dio/dio.dart' show MultipartFile; +import 'package:stream_chat/src/client/channel.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/channel_state.dart'; import 'package:stream_chat/src/core/models/event.dart'; import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/user.dart'; import 'package:test/test.dart'; Matcher isSameMultipartFileAs(MultipartFile targetFile) => @@ -96,3 +100,35 @@ class _IsSameMessageAs extends Matcher { return matches; } } + +Matcher isSameUserAs(User targetUser) => _IsSameUserAs(targetUser: targetUser); + +class _IsSameUserAs extends Matcher { + const _IsSameUserAs({required this.targetUser}); + + final User targetUser; + + @override + Description describe(Description description) => + description.add('is same user as $targetUser'); + + @override + bool matches(covariant User user, Map matchState) => user.id == targetUser.id; +} + +Matcher isCorrectChannelFor(ChannelState channelState) => + _IsCorrectChannelFor(channelState: channelState); + +class _IsCorrectChannelFor extends Matcher { + const _IsCorrectChannelFor({required this.channelState}); + + final ChannelState channelState; + + @override + Description describe(Description description) => + description.add('is correct channel for $channelState'); + + @override + bool matches(covariant Channel channel, Map matchState) => + channel.cid == channelState.channel?.cid; +} diff --git a/packages/stream_chat/test/src/mocks.dart b/packages/stream_chat/test/src/mocks.dart index 31e768e8..528c8a91 100644 --- a/packages/stream_chat/test/src/mocks.dart +++ b/packages/stream_chat/test/src/mocks.dart @@ -18,6 +18,7 @@ import 'package:stream_chat/src/core/http/token_manager.dart'; import 'package:stream_chat/src/core/models/channel_config.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:stream_chat/src/db/chat_persistence_client.dart'; +import 'package:stream_chat/src/ws/websocket.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'db/chat_persistence_client_test.dart'; @@ -66,7 +67,13 @@ class MockGeneralApi extends Mock implements GeneralApi {} class MockAttachmentFileUploader extends Mock implements AttachmentFileUploader {} -class MockPersistenceClient extends Mock implements ChatPersistenceClient {} +class MockPersistenceClient extends Mock implements ChatPersistenceClient { + @override + Future connect(String userId) => Future.value(); + + @override + Future disconnect({bool flush = false}) => Future.value(); +} class MockStreamChatClient extends Mock implements StreamChatClient { @override @@ -105,3 +112,5 @@ class MockRetryQueueChannel extends Mock implements Channel { @override StreamChatClient get client => _client ??= MockStreamChatClient(); } + +class MockWebSocket extends Mock implements WebSocket {} diff --git a/packages/stream_chat/test/src/utils.dart b/packages/stream_chat/test/src/utils.dart index b17e35a9..91a61204 100644 --- a/packages/stream_chat/test/src/utils.dart +++ b/packages/stream_chat/test/src/utils.dart @@ -13,3 +13,12 @@ Directory get currentDirectory { } return directory; } + +// Extension function to convert int into durations +extension IntX on num { + Duration toDuration() => Duration(milliseconds: toInt()); +} + +// Top level util function to delay the code execution +Future delay(num milliseconds) => + Future.delayed(Duration(milliseconds: milliseconds.toInt())); diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index aae9d193..976aacb0 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -791,7 +791,7 @@ class _MessageListViewState extends State { final channel = streamChannel.channel; if (_upToDate && channel.config?.readEvents == true && - channel.state!.unreadCount! > 0) { + channel.state!.unreadCount > 0) { streamChannel.channel.markRead(); } } diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart index 20304cdd..1dcf9c3c 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart @@ -174,7 +174,7 @@ class StreamChannelState extends State { try { final response = await channel.getReplies( parentId, - PaginationParams( + options: PaginationParams( lessThan: message?.id, limit: limit, ),