diff --git a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart index 2edaed4f..73c7b0fa 100644 --- a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart +++ b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart @@ -6,19 +6,11 @@ import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/converter/converter.dart'; import 'package:stream_chat_persistence/src/dao/dao.dart'; import 'package:stream_chat_persistence/src/entity/entity.dart'; -import 'package:stream_chat_persistence/src/db/shared/shared_db.dart'; + +export 'shared/shared_db.dart'; part 'moor_chat_database.g.dart'; -LazyDatabase _openConnection( - String userId, { - bool logStatements = false, -}) => - LazyDatabase(() async => SharedDB.constructDatabase( - userId, - logStatements: logStatements, - )); - /// A chat database implemented using moor @UseMoor(tables: [ Channels, @@ -44,12 +36,9 @@ LazyDatabase _openConnection( class MoorChatDatabase extends _$MoorChatDatabase { /// Creates a new moor chat database instance MoorChatDatabase( - this._userId, { - logStatements = false, - }) : super(_openConnection( - _userId, - logStatements: logStatements, - )); + this._userId, + QueryExecutor executor, + ) : super(executor); /// Instantiate a new database instance MoorChatDatabase.connect( diff --git a/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart b/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart index a3b1ce8e..0868ad69 100644 --- a/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart +++ b/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart @@ -14,15 +14,40 @@ import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; /// A Helper class to construct new instances of [MoorChatDatabase] specifically /// for native platform applications class SharedDB { - /// Returns a new instance of [VmDatabase] created using [userId] - /// on a regular isolate. - /// - /// Generally used with [ConnectionMode.regular]. - static Future constructDatabase( + /// Returns a new instance of [MoorChatDatabase]. + static MoorChatDatabase constructDatabase( String userId, { bool logStatements = false, - }) async { + ConnectionMode connectionMode = ConnectionMode.regular, + }) { final dbName = 'db_$userId'; + if (connectionMode == ConnectionMode.background) { + return MoorChatDatabase.connect( + userId, + DatabaseConnection.delayed(Future(() async { + final isolate = await _createMoorIsolate( + dbName, + logStatements: logStatements, + ); + return isolate.connect(); + })), + ); + } + return MoorChatDatabase( + userId, + LazyDatabase( + () async => _constructDatabase( + dbName, + logStatements: logStatements, + ), + ), + ); + } + + static Future _constructDatabase( + String dbName, { + bool logStatements = false, + }) async { if (Platform.isIOS || Platform.isAndroid) { final dir = await getApplicationDocumentsDirectory(); final path = join(dir.path, '$dbName.sqlite'); @@ -66,27 +91,6 @@ class SharedDB { return await receivePort.first as MoorIsolate; } - - /// Returns a new instance of [MoorChatDatabase] using the factory constructor - /// [MoorChatDatabase.connect] created on a background isolate. - /// - /// Generally used with [ConnectionMode.background]. - static MoorChatDatabase constructMoorChatDatabase( - String userId, { - bool logStatements = false, - }) { - final dbName = 'db_$userId'; - return MoorChatDatabase.connect( - userId, - DatabaseConnection.delayed(Future(() async { - final isolate = await _createMoorIsolate( - dbName, - logStatements: logStatements, - ); - return isolate.connect(); - })), - ); - } } class _IsolateStartRequest { diff --git a/packages/stream_chat_persistence/lib/src/db/shared/unsupported_db.dart b/packages/stream_chat_persistence/lib/src/db/shared/unsupported_db.dart index 15734a7c..e2877030 100644 --- a/packages/stream_chat_persistence/lib/src/db/shared/unsupported_db.dart +++ b/packages/stream_chat_persistence/lib/src/db/shared/unsupported_db.dart @@ -1,28 +1,16 @@ -import 'package:moor/backends.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/stream_chat_persistence.dart'; /// A Helper class to construct new instances of [MoorChatDatabase] class SharedDB { - /// Returns a new instance of database. - /// - /// Generally used with [ConnectionMode.regular]. - static Future constructDatabase( + /// Returns a new instance of [MoorChatDatabase]. + static MoorChatDatabase constructDatabase( String userId, { bool logStatements = false, + ConnectionMode connectionMode = ConnectionMode.regular, }) { throw UnsupportedError( - 'No implementation of the constructDatabase api provided'); - } - - /// Return a new instance of moor chat database. - /// - /// Generally used with [ConnectionMode.background]. - static MoorChatDatabase constructMoorChatDatabase( - String userId, { - bool logStatements = false, - }) { - throw UnsupportedError( - 'No implementation of the constructMoorChatDatabase api provided'); + 'No implementation of the constructDatabase api provided', + ); } } diff --git a/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart b/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart index 7388543e..9c504a11 100644 --- a/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart +++ b/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart @@ -6,26 +6,14 @@ import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; /// A Helper class to construct new instances of [MoorChatDatabase] specifically /// for Web applications class SharedDB { - /// Returns a new instance of [WebDatabase] created using [userId]. - /// - /// Generally used with [ConnectionMode.regular]. - static Future constructDatabase( - String userId, { - bool logStatements = false, - }) async { - final dbName = 'db_$userId'; - return WebDatabase(dbName, logStatements: logStatements); - } - - /// Returns a new instance of [MoorChatDatabase] creating using the - /// default constructor. - /// - /// Generally used with [ConnectionMode.background]. - static MoorChatDatabase constructMoorChatDatabase( + /// Returns a new instance of [MoorChatDatabase]. + static MoorChatDatabase constructDatabase( String userId, { bool logStatements = false, + ConnectionMode connectionMode = ConnectionMode.regular, // Ignored on web }) { final dbName = 'db_$userId'; - return MoorChatDatabase(dbName, logStatements: logStatements); + final queryExecutor = WebDatabase(dbName, logStatements: logStatements); + return MoorChatDatabase(userId, queryExecutor); } } diff --git a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart index e7ee060b..2c9d28fb 100644 --- a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart +++ b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart @@ -4,7 +4,6 @@ import 'package:mutex/mutex.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; -import 'package:stream_chat_persistence/src/db/shared/shared_db.dart'; /// Various connection modes on which [StreamChatPersistenceClient] can work enum ConnectionMode { @@ -15,6 +14,9 @@ enum ConnectionMode { background, } +/// Signature for a function which provides instance of [MoorChatDatabase] +typedef DatabaseProvider = MoorChatDatabase Function(String, ConnectionMode); + final _levelEmojiMapper = { Level.INFO: 'ℹ️', Level.WARNING: '⚠️', @@ -36,26 +38,6 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { _logger.onRecord.listen(logHandlerFunction ?? _defaultLogHandler); } - /// A function that has a parameter of type [LogRecord]. - /// This is called on every new log record. - /// By default the client will use the handler returned by - /// [_getDefaultLogHandler]. - /// Setting it you can handle the log messages directly instead of have them - /// written to stdout, - /// this is very convenient if you use an error tracking tool or if you want - /// to centralize your logs into one facility. - /// - /// ```dart - /// myLogHandlerFunction = (LogRecord record) { - /// // do something with the record (ie. send it to Sentry or Fabric) - /// } - /// - /// final client = StreamChatPersistenceClient( - /// logHandlerFunction: myLogHandlerFunction, - /// ); - ///``` - LogHandlerFunction logHandlerFunction; - /// [MoorChatDatabase] instance used by this client. @visibleForTesting MoorChatDatabase db; @@ -84,24 +66,25 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { return ret; } + MoorChatDatabase _defaultDatabaseProvider( + String userId, + ConnectionMode mode, + ) => + SharedDB.constructDatabase(userId, connectionMode: mode); + @override - Future connect(String userId) async { + Future connect( + String userId, { + DatabaseProvider databaseProvider, // Used only for testing + }) async { if (db != null) { throw Exception( 'An instance of StreamChatDatabase is already connected.\n' 'disconnect the previous instance before connecting again.', ); } - switch (_connectionMode) { - case ConnectionMode.regular: - _logger.info('Connecting on a regular isolate'); - db = MoorChatDatabase(userId); - return; - case ConnectionMode.background: - _logger.info('Connecting on background isolate'); - db = SharedDB.constructMoorChatDatabase(userId); - return; - } + db = databaseProvider?.call(userId, _connectionMode) ?? + _defaultDatabaseProvider(userId, _connectionMode); } @override @@ -259,9 +242,9 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { @override Future updateChannelQueries( Map filter, - List cids, - bool clearQueryCache, - ) => + List cids, { + bool clearQueryCache = false, + }) => _readProtected(() async { _logger.info('updateChannelQueries'); return db.channelQueryDao.updateChannelQueries( diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index 8b235710..5328e1a2 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -27,6 +27,7 @@ dependency_overrides: path: ../stream_chat dev_dependencies: - test: ^1.15.7 build_runner: ^1.11.0 - moor_generator: ^3.4.1 \ No newline at end of file + mockito: ^4.1.3 + moor_generator: ^3.4.1 + test: ^1.15.7 diff --git a/packages/stream_chat_persistence/test/mock_chat_database.dart b/packages/stream_chat_persistence/test/mock_chat_database.dart new file mode 100644 index 00000000..a60bd39b --- /dev/null +++ b/packages/stream_chat_persistence/test/mock_chat_database.dart @@ -0,0 +1,71 @@ +import 'package:mockito/mockito.dart'; +import 'package:stream_chat_persistence/src/dao/dao.dart'; +import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; + +class MockChatDatabase extends Mock implements MoorChatDatabase { + UserDao _userDao; + + @override + UserDao get userDao => _userDao ??= MockUserDao(); + + ChannelDao _channelDao; + + @override + ChannelDao get channelDao => _channelDao ??= MockChannelDao(); + + MessageDao _messageDao; + + @override + MessageDao get messageDao => _messageDao ??= MockMessageDao(); + + PinnedMessageDao _pinnedMessageDao; + + @override + PinnedMessageDao get pinnedMessageDao => + _pinnedMessageDao ??= MockPinnedMessageDao(); + + MemberDao _memberDao; + + @override + MemberDao get memberDao => _memberDao ??= MockMemberDao(); + + ReactionDao _reactionDao; + + @override + ReactionDao get reactionDao => _reactionDao ??= MockReactionDao(); + + ReadDao _readDao; + + @override + ReadDao get readDao => _readDao ??= MockReadDao(); + + ChannelQueryDao _channelQueryDao; + + @override + ChannelQueryDao get channelQueryDao => + _channelQueryDao ??= MockChannelQueryDao(); + + ConnectionEventDao _connectionEventDao; + + @override + ConnectionEventDao get connectionEventDao => + _connectionEventDao ??= MockConnectionEventDao(); +} + +class MockUserDao extends Mock implements UserDao {} + +class MockChannelDao extends Mock implements ChannelDao {} + +class MockMessageDao extends Mock implements MessageDao {} + +class MockPinnedMessageDao extends Mock implements PinnedMessageDao {} + +class MockMemberDao extends Mock implements MemberDao {} + +class MockReactionDao extends Mock implements ReactionDao {} + +class MockReadDao extends Mock implements ReadDao {} + +class MockChannelQueryDao extends Mock implements ChannelQueryDao {} + +class MockConnectionEventDao extends Mock implements ConnectionEventDao {} diff --git a/packages/stream_chat_persistence/test/src/stream_chat_persistence_client_test.dart b/packages/stream_chat_persistence/test/src/stream_chat_persistence_client_test.dart deleted file mode 100644 index 561ca04d..00000000 --- a/packages/stream_chat_persistence/test/src/stream_chat_persistence_client_test.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; -import 'package:stream_chat_persistence/src/stream_chat_persistence_client.dart'; -import 'package:test/test.dart'; - -void main() { - group('connect', () { - test('throws exception because already connected', () { - final streamChatPersistenceClient = StreamChatPersistenceClient( - connectionMode: ConnectionMode.background, - logLevel: Level.INFO, - )..db = MoorChatDatabase( - 'test', - persistOnDisk: false, - ); - - expect( - () => streamChatPersistenceClient.connect('test'), - throwsA(allOf(isException, predicate((e) { - return e.message == - 'An instance of StreamChatDatabase is already connected.\n' - 'disconnect the previous instance before connecting again.'; - }))), - ); - }); - }); -} diff --git a/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart new file mode 100644 index 00000000..ef7fd020 --- /dev/null +++ b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart @@ -0,0 +1,524 @@ +import 'package:mockito/mockito.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/stream_chat_persistence_client.dart'; +import 'package:test/test.dart'; +import 'mock_chat_database.dart'; +import 'src/utils/date_matcher.dart'; + +MoorChatDatabase _testDatabaseProvider(String userId, ConnectionMode mode) => + MoorChatDatabase.testable(userId); + +void main() { + group('connect', () { + const userId = 'testUserId'; + test('successfully connects with the Database', () async { + final client = StreamChatPersistenceClient(logLevel: Level.ALL); + expect(client.db, isNull); + await client.connect(userId, databaseProvider: _testDatabaseProvider); + expect(client.db, isNotNull); + expect(client.db, isA()); + expect(client.db.userId, userId); + + addTearDown(() async { + await client.disconnect(); + }); + }); + + test('throws if already connected', () async { + final client = StreamChatPersistenceClient(logLevel: Level.ALL); + expect(client.db, isNull); + await client.connect(userId, databaseProvider: _testDatabaseProvider); + expect(client.db, isNotNull); + expect(client.db, isNotNull); + expect(client.db, isA()); + expect(client.db.userId, userId); + expect( + () => client.connect(userId, databaseProvider: _testDatabaseProvider), + throwsException, + ); + + addTearDown(() async { + await client.disconnect(); + }); + }); + }); + + test('disconnect', () async { + const userId = 'testUserId'; + final client = StreamChatPersistenceClient(logLevel: Level.ALL); + await client.connect(userId, databaseProvider: _testDatabaseProvider); + expect(client.db, isNotNull); + await client.disconnect(flush: true); + expect(client.db, isNull); + }); + + group('client functions', () { + const userId = 'testUserId'; + final mockDatabase = MockChatDatabase(); + MoorChatDatabase _mockDatabaseProvider(_, __) => mockDatabase; + StreamChatPersistenceClient client; + + setUp(() async { + client = StreamChatPersistenceClient(logLevel: Level.ALL); + await client.connect(userId, databaseProvider: _mockDatabaseProvider); + }); + + test('getReplies', () async { + const parentId = 'testParentId'; + final replies = List.generate(3, (index) => Message(id: 'testId$index')); + + when(mockDatabase.messageDao.getThreadMessagesByParentId(parentId)) + .thenAnswer((_) async => replies); + + final fetchedReplies = await client.getReplies(parentId); + expect(fetchedReplies.length, replies.length); + verify(mockDatabase.messageDao.getThreadMessagesByParentId(parentId)) + .called(1); + }); + + test('getConnectionInfo', () async { + final event = Event(type: 'testEvent'); + when(mockDatabase.connectionEventDao.connectionEvent) + .thenAnswer((_) async => event); + + final fetchedEvent = await client.getConnectionInfo(); + expect(fetchedEvent.type, event.type); + verify(mockDatabase.connectionEventDao.connectionEvent).called(1); + }); + + test('getLastSyncAt', () async { + final lastSync = DateTime.now(); + when(mockDatabase.connectionEventDao.lastSyncAt) + .thenAnswer((_) async => lastSync); + + final fetchedLastSync = await client.getLastSyncAt(); + expect(fetchedLastSync, isSameDateAs(lastSync)); + verify(mockDatabase.connectionEventDao.lastSyncAt).called(1); + }); + + test('updateConnectionInfo', () async { + final event = Event(type: 'testEvent'); + when(mockDatabase.connectionEventDao.updateConnectionEvent(event)) + .thenAnswer((_) async { + return; + }); + + await client.updateConnectionInfo(event); + verify(mockDatabase.connectionEventDao.updateConnectionEvent(event)) + .called(1); + }); + + test('updateLastSyncAt', () async { + final lastSync = DateTime.now(); + when(mockDatabase.connectionEventDao.updateLastSyncAt(lastSync)) + .thenAnswer((_) { + return; + }); + + await client.updateLastSyncAt(lastSync); + verify(mockDatabase.connectionEventDao.updateLastSyncAt(lastSync)) + .called(1); + }); + + test('getChannelCids', () async { + final channelCids = List.generate(3, (index) => 'testCid$index'); + when(mockDatabase.channelDao.cids).thenAnswer((_) async => channelCids); + + final fetchedChannelCids = await client.getChannelCids(); + expect(fetchedChannelCids.length, channelCids.length); + verify(mockDatabase.channelDao.cids).called(1); + }); + + test('getChannelByCid', () async { + const cid = 'testCid'; + final channelModel = ChannelModel(cid: cid); + when(mockDatabase.channelDao.getChannelByCid(cid)) + .thenAnswer((_) async => channelModel); + + final fetchedChannelModel = await client.getChannelByCid(cid); + expect(fetchedChannelModel.cid, channelModel.cid); + verify(mockDatabase.channelDao.getChannelByCid(cid)).called(1); + }); + + test('getMembersByCid', () async { + const cid = 'testCid'; + final members = List.generate(3, (index) => Member()); + when(mockDatabase.memberDao.getMembersByCid(cid)) + .thenAnswer((_) async => members); + + final fetchedMembers = await client.getMembersByCid(cid); + expect(fetchedMembers.length, members.length); + verify(mockDatabase.memberDao.getMembersByCid(cid)).called(1); + }); + + test('getReadsByCid', () async { + const cid = 'testCid'; + final reads = List.generate(3, (index) => Read()); + when(mockDatabase.readDao.getReadsByCid(cid)) + .thenAnswer((_) async => reads); + + final fetchedReads = await client.getReadsByCid(cid); + expect(fetchedReads.length, reads.length); + verify(mockDatabase.readDao.getReadsByCid(cid)).called(1); + }); + + test('getMessagesByCid', () async { + const cid = 'testCid'; + final messages = List.generate(3, (index) => Message()); + when(mockDatabase.messageDao.getMessagesByCid(cid)) + .thenAnswer((_) async => messages); + + final fetchedMessages = await client.getMessagesByCid(cid); + expect(fetchedMessages.length, messages.length); + verify(mockDatabase.messageDao.getMessagesByCid(cid)).called(1); + }); + + test('getPinnedMessagesByCid', () async { + const cid = 'testCid'; + final messages = List.generate(3, (index) => Message()); + when(mockDatabase.pinnedMessageDao.getMessagesByCid(cid)) + .thenAnswer((_) async => messages); + + final fetchedMessages = await client.getPinnedMessagesByCid(cid); + expect(fetchedMessages.length, messages.length); + verify(mockDatabase.pinnedMessageDao.getMessagesByCid(cid)).called(1); + }); + + test('getChannelStateByCid', () async { + const cid = 'testCid'; + final messages = List.generate(3, (index) => Message()); + final members = List.generate(3, (index) => Member()); + final reads = List.generate(3, (index) => Read()); + final channel = ChannelModel(cid: cid); + + when(mockDatabase.memberDao.getMembersByCid(cid)) + .thenAnswer((_) async => members); + when(mockDatabase.readDao.getReadsByCid(cid)) + .thenAnswer((_) async => reads); + when(mockDatabase.channelDao.getChannelByCid(cid)) + .thenAnswer((_) async => channel); + when(mockDatabase.messageDao.getMessagesByCid(cid)) + .thenAnswer((_) async => messages); + when(mockDatabase.pinnedMessageDao.getMessagesByCid(cid)) + .thenAnswer((_) async => messages); + + final fetchedChannelState = await client.getChannelStateByCid(cid); + expect(fetchedChannelState.messages.length, messages.length); + expect(fetchedChannelState.pinnedMessages.length, messages.length); + expect(fetchedChannelState.members.length, members.length); + expect(fetchedChannelState.read.length, reads.length); + expect(fetchedChannelState.channel.cid, channel.cid); + + verify(mockDatabase.memberDao.getMembersByCid(cid)).called(1); + verify(mockDatabase.readDao.getReadsByCid(cid)).called(1); + verify(mockDatabase.channelDao.getChannelByCid(cid)).called(1); + verify(mockDatabase.messageDao.getMessagesByCid(cid)).called(1); + verify(mockDatabase.pinnedMessageDao.getMessagesByCid(cid)).called(1); + }); + + test('getChannelStates', () async { + const cid = 'testCid'; + final channels = List.generate(3, (index) => ChannelModel(cid: cid)); + final messages = List.generate(3, (index) => Message()); + final members = List.generate(3, (index) => Member()); + final reads = List.generate(3, (index) => Read()); + final channel = ChannelModel(cid: cid); + final channelStates = channels + .map( + (channel) => ChannelState( + channel: channel, + messages: messages, + pinnedMessages: messages, + members: members, + read: reads, + ), + ) + .toList(growable: false); + + when(mockDatabase.channelQueryDao.getChannels()) + .thenAnswer((_) async => channels); + when(mockDatabase.memberDao.getMembersByCid(cid)) + .thenAnswer((_) async => members); + when(mockDatabase.readDao.getReadsByCid(cid)) + .thenAnswer((_) async => reads); + when(mockDatabase.channelDao.getChannelByCid(cid)) + .thenAnswer((_) async => channel); + when(mockDatabase.messageDao.getMessagesByCid(cid)) + .thenAnswer((_) async => messages); + when(mockDatabase.pinnedMessageDao.getMessagesByCid(cid)) + .thenAnswer((_) async => messages); + + final fetchedChannelStates = await client.getChannelStates(); + expect(fetchedChannelStates.length, channelStates.length); + + for (var i = 0; i < fetchedChannelStates.length; i++) { + final original = channelStates[i]; + final fetched = fetchedChannelStates[i]; + expect(fetched.members.length, original.members.length); + expect(fetched.messages.length, original.messages.length); + expect(fetched.pinnedMessages.length, original.pinnedMessages.length); + expect(fetched.read.length, original.read.length); + expect(fetched.channel.cid, original.channel.cid); + } + + verify(mockDatabase.channelQueryDao.getChannels()).called(1); + verify(mockDatabase.memberDao.getMembersByCid(cid)).called(3); + verify(mockDatabase.readDao.getReadsByCid(cid)).called(3); + verify(mockDatabase.channelDao.getChannelByCid(cid)).called(3); + verify(mockDatabase.messageDao.getMessagesByCid(cid)).called(3); + verify(mockDatabase.pinnedMessageDao.getMessagesByCid(cid)).called(3); + }); + + test('updateChannelQueries', () async { + const filter = {}; + const cids = []; + when(mockDatabase.channelQueryDao.updateChannelQueries(filter, cids)) + .thenAnswer((realInvocation) async { + return; + }); + + await client.updateChannelQueries(filter, cids); + verify(mockDatabase.channelQueryDao.updateChannelQueries(filter, cids)) + .called(1); + }); + + test('deleteMessageById', () async { + const messageId = 'testMessageId'; + when(mockDatabase.messageDao.deleteMessageByIds([messageId])) + .thenAnswer((_) async { + return; + }); + + await client.deleteMessageById(messageId); + verify(mockDatabase.messageDao.deleteMessageByIds([messageId])).called(1); + }); + + test('deletePinnedMessageById', () async { + const messageId = 'testMessageId'; + when(mockDatabase.pinnedMessageDao.deleteMessageByIds([messageId])) + .thenAnswer((_) async { + return; + }); + + await client.deletePinnedMessageById(messageId); + verify(mockDatabase.pinnedMessageDao.deleteMessageByIds([messageId])) + .called(1); + }); + + test('deleteMessageByIds', () async { + const messageIds = []; + when(mockDatabase.messageDao.deleteMessageByIds(messageIds)) + .thenAnswer((_) async { + return; + }); + + await client.deleteMessageByIds(messageIds); + verify(mockDatabase.messageDao.deleteMessageByIds(messageIds)).called(1); + }); + + test('deletePinnedMessageByIds', () async { + const messageIds = []; + when(mockDatabase.pinnedMessageDao.deleteMessageByIds(messageIds)) + .thenAnswer((_) async { + return; + }); + + await client.deletePinnedMessageByIds(messageIds); + verify(mockDatabase.pinnedMessageDao.deleteMessageByIds(messageIds)) + .called(1); + }); + + test('deleteMessageByCid', () async { + const cid = 'testCid'; + when(mockDatabase.messageDao.deleteMessageByCids([cid])) + .thenAnswer((_) async { + return; + }); + + await client.deleteMessageByCid(cid); + verify(mockDatabase.messageDao.deleteMessageByCids([cid])).called(1); + }); + + test('deletePinnedMessageByCid', () async { + const cid = 'testCid'; + when(mockDatabase.pinnedMessageDao.deleteMessageByCids([cid])) + .thenAnswer((_) async { + return; + }); + + await client.deletePinnedMessageByCid(cid); + verify(mockDatabase.pinnedMessageDao.deleteMessageByCids([cid])) + .called(1); + }); + + test('deleteMessageByCids', () async { + const cids = []; + when(mockDatabase.messageDao.deleteMessageByCids(cids)) + .thenAnswer((_) async { + return; + }); + + await client.deleteMessageByCids(cids); + verify(mockDatabase.messageDao.deleteMessageByCids(cids)).called(1); + }); + + test('deletePinnedMessageByCids', () async { + const cids = []; + when(mockDatabase.pinnedMessageDao.deleteMessageByCids(cids)) + .thenAnswer((_) async { + return; + }); + + await client.deletePinnedMessageByCids(cids); + verify(mockDatabase.pinnedMessageDao.deleteMessageByCids(cids)).called(1); + }); + + test('deleteChannels', () async { + const cids = []; + when(mockDatabase.channelDao.deleteChannelByCids(cids)) + .thenAnswer((_) async { + return; + }); + + await client.deleteChannels(cids); + verify(mockDatabase.channelDao.deleteChannelByCids(cids)).called(1); + }); + + test('updateMessages', () async { + const cid = 'testCid'; + final messages = List.generate(3, (index) => Message()); + when(mockDatabase.messageDao.updateMessages(cid, messages)) + .thenAnswer((_) async { + return; + }); + + await client.updateMessages(cid, messages); + verify(mockDatabase.messageDao.updateMessages(cid, messages)).called(1); + }); + + test('updatePinnedMessages', () async { + const cid = 'testCid'; + final messages = List.generate(3, (index) => Message()); + when(mockDatabase.pinnedMessageDao.updateMessages(cid, messages)) + .thenAnswer((_) async { + return; + }); + + await client.updatePinnedMessages(cid, messages); + verify(mockDatabase.pinnedMessageDao.updateMessages(cid, messages)) + .called(1); + }); + + test('getChannelThreads', () async { + const cid = 'testCid'; + final messages = + List.generate(3, (index) => Message(parentId: 'testParentId$index')); + final threads = messages.fold>>( + {}, + (prev, curr) { + return prev + ..update( + curr.parentId, + (value) => [...value, curr], + ifAbsent: () => [], + ); + }, + ); + when(mockDatabase.messageDao.getThreadMessages(cid)) + .thenAnswer((realInvocation) async => messages); + + final fetchedThreads = await client.getChannelThreads(cid); + expect(fetchedThreads.length, threads.length); + for (var i = 0; i < fetchedThreads.length; i++) { + final original = threads.entries.elementAt(i); + final fetched = fetchedThreads.entries.elementAt(i); + expect(fetched.key, original.key); + } + + verify(mockDatabase.messageDao.getThreadMessages(cid)).called(1); + }); + + test('updateChannels', () async { + final channels = List.generate(3, (index) => ChannelModel()); + when(mockDatabase.channelDao.updateChannels(channels)) + .thenAnswer((_) async { + return; + }); + + await client.updateChannels(channels); + verify(mockDatabase.channelDao.updateChannels(channels)).called(1); + }); + + test('updateMembers', () async { + const cid = 'testCid'; + final members = List.generate(3, (index) => Member()); + when(mockDatabase.memberDao.updateMembers(cid, members)) + .thenAnswer((_) async { + return; + }); + + await client.updateMembers(cid, members); + verify(mockDatabase.memberDao.updateMembers(cid, members)).called(1); + }); + + test('updateReads', () async { + const cid = 'testCid'; + final reads = List.generate(3, (index) => Read()); + when(mockDatabase.readDao.updateReads(cid, reads)).thenAnswer((_) async { + return; + }); + + await client.updateReads(cid, reads); + verify(mockDatabase.readDao.updateReads(cid, reads)).called(1); + }); + + test('updateUsers', () async { + final users = List.generate(3, (index) => User()); + when(mockDatabase.userDao.updateUsers(users)).thenAnswer((_) async { + return; + }); + + await client.updateUsers(users); + verify(mockDatabase.userDao.updateUsers(users)).called(1); + }); + + test('updateReactions', () async { + final reactions = List.generate(3, (index) => Reaction()); + when(mockDatabase.reactionDao.updateReactions(reactions)) + .thenAnswer((_) async { + return; + }); + + await client.updateReactions(reactions); + verify(mockDatabase.reactionDao.updateReactions(reactions)).called(1); + }); + + test('deleteReactionsByMessageId', () async { + final messageIds = []; + when(mockDatabase.reactionDao.deleteReactionsByMessageIds(messageIds)) + .thenAnswer((_) async { + return; + }); + + await client.deleteReactionsByMessageId(messageIds); + verify(mockDatabase.reactionDao.deleteReactionsByMessageIds(messageIds)) + .called(1); + }); + + test('deleteMembersByCids', () async { + final cids = []; + when(mockDatabase.memberDao.deleteMemberByCids(cids)) + .thenAnswer((_) async { + return; + }); + + await client.deleteMembersByCids(cids); + verify(mockDatabase.memberDao.deleteMemberByCids(cids)).called(1); + }); + + tearDown(() async { + await client.disconnect(flush: true); + }); + }); +}