diff --git a/.github/workflows/dispatch_workflows.yml b/.github/workflows/dispatch_workflows.yml index 5743a0aa..6d626fdb 100644 --- a/.github/workflows/dispatch_workflows.yml +++ b/.github/workflows/dispatch_workflows.yml @@ -20,7 +20,7 @@ jobs: repo: GetStream/flutter-samples token: ${{ secrets.GH_TOKEN }} dispatch_stable: - if: github.ref == 'refs/heads/master' + if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: benc-uk/workflow-dispatch@v1 diff --git a/.github/workflows/scripts/coverage.sh b/.github/workflows/scripts/coverage.sh new file mode 100755 index 00000000..2710a45d --- /dev/null +++ b/.github/workflows/scripts/coverage.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +# Fast fail the script on failures. +set -e + +pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r '.g.dart$' \ No newline at end of file diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index 52a504fe..91808df2 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -8,6 +8,7 @@ on: push: branches: - master + - develop paths-ignore: - 'docs/**' @@ -32,6 +33,7 @@ jobs: melos exec -c 3 --ignore="*example*" -- \ tuneup check - name: 'Pub Check' + if: github.ref == 'refs/heads/master' run: | melos exec -c 1 --no-private --ignore="*example*" -- \ pub publish --dry-run @@ -68,27 +70,31 @@ jobs: run: | ./.github/workflows/scripts/install-tools.sh flutter pub global activate coverage + flutter pub global activate remove_from_coverage - name: 'Bootstrap Workspace' run: melos bootstrap - name: 'Dart Test' run: | cd packages/stream_chat flutter pub run test --coverage coverage/ - format_coverage --lcov --in=coverage/ --out=lcov.info --packages=.packages --report-on=lib + format_coverage --lcov --in=coverage/ --out=coverage/lcov.info --packages=.packages --report-on=lib - name: 'Flutter Test' run: | melos exec -c 3 --flutter --dir-exists=test --ignore="*example*" --ignore="*web*" -- \ flutter test --coverage - name: CodeCov - run: bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_TOKEN }} + run: | + melos exec -c 3 --fail-fast --dir-exists=test --ignore="*example*" --ignore="*web*" -- \ + "\$MELOS_ROOT_PATH/.github/workflows/scripts/coverage.sh" + bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_TOKEN }} - uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 with: - path: packages/stream_chat/lcov.info - min_coverage: 50 + path: packages/stream_chat/coverage/lcov.info + min_coverage: 40 - uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 with: path: packages/stream_chat_persistence/coverage/lcov.info - min_coverage: 0.2 + min_coverage: 95 - uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 with: path: packages/stream_chat_flutter_core/coverage/lcov.info diff --git a/packages/stream_chat/lib/src/client.dart b/packages/stream_chat/lib/src/client.dart index 6a4a77f4..f46ba178 100644 --- a/packages/stream_chat/lib/src/client.dart +++ b/packages/stream_chat/lib/src/client.dart @@ -758,7 +758,8 @@ class StreamChatClient { await _chatPersistenceClient?.updateChannelQueries( filter, channels.map((c) => c.channel.cid).toList(), - paginationParams?.offset == null || paginationParams.offset == 0, + clearQueryCache: + paginationParams?.offset == null || paginationParams.offset == 0, ); state.channels = updateData.key; diff --git a/packages/stream_chat/lib/src/db/chat_persistence_client.dart b/packages/stream_chat/lib/src/db/chat_persistence_client.dart index d90872a2..d15772df 100644 --- a/packages/stream_chat/lib/src/db/chat_persistence_client.dart +++ b/packages/stream_chat/lib/src/db/chat_persistence_client.dart @@ -100,10 +100,9 @@ abstract class ChatPersistenceClient { /// the list of matching rows will be deleted Future updateChannelQueries( Map filter, - List cids, - // ignore: avoid_positional_boolean_parameters - bool clearQueryCache, - ); + List cids, { + bool clearQueryCache = false, + }); /// Remove a message by [messageId] Future deleteMessageById(String messageId) => diff --git a/packages/stream_chat/lib/src/models/event.dart b/packages/stream_chat/lib/src/models/event.dart index b1140ebc..28cf1b38 100644 --- a/packages/stream_chat/lib/src/models/event.dart +++ b/packages/stream_chat/lib/src/models/event.dart @@ -121,6 +121,46 @@ class Event { _$EventToJson(this), topLevelFields, ); + + /// Creates a copy of [Event] with specified attributes overridden. + Event copyWith({ + String type, + String cid, + String channelId, + String channelType, + String connectionId, + DateTime createdAt, + OwnUser me, + User user, + Message message, + EventChannel channel, + Member member, + Reaction reaction, + int totalUnreadCount, + int unreadChannels, + bool online, + String parentId, + Map extraData, + }) => + Event( + type: type ?? this.type, + cid: cid ?? this.cid, + connectionId: connectionId ?? this.connectionId, + createdAt: createdAt ?? this.createdAt, + me: me ?? this.me, + user: user ?? this.user, + message: message ?? this.message, + totalUnreadCount: totalUnreadCount ?? this.totalUnreadCount, + unreadChannels: unreadChannels ?? this.unreadChannels, + reaction: reaction ?? this.reaction, + online: online ?? this.online, + channel: channel ?? this.channel, + member: member ?? this.member, + channelId: channelId ?? this.channelId, + channelType: channelType ?? this.channelType, + parentId: parentId ?? this.parentId, + extraData: extraData ?? this.extraData, + ); } /// The channel embedded in the event object diff --git a/packages/stream_chat/lib/src/models/read.dart b/packages/stream_chat/lib/src/models/read.dart index 2be639d5..fc8ef1bc 100644 --- a/packages/stream_chat/lib/src/models/read.dart +++ b/packages/stream_chat/lib/src/models/read.dart @@ -27,4 +27,16 @@ class Read { /// Serialize to json Map toJson() => _$ReadToJson(this); + + /// Creates a copy of [Read] with specified attributes overridden. + Read copyWith({ + DateTime lastRead, + User user, + int unreadMessages, + }) => + Read( + lastRead: lastRead ?? this.lastRead, + user: user ?? this.user, + unreadMessages: unreadMessages ?? this.unreadMessages, + ); } diff --git a/packages/stream_chat/lib/src/models/user.dart b/packages/stream_chat/lib/src/models/user.dart index 618c1986..3b9f9add 100644 --- a/packages/stream_chat/lib/src/models/user.dart +++ b/packages/stream_chat/lib/src/models/user.dart @@ -100,4 +100,28 @@ class User { /// Serialize to json Map toJson() => Serialization.moveFromExtraDataToRoot(_$UserToJson(this), topLevelFields); + + /// Creates a copy of [User] with specified attributes overridden. + User copyWith({ + String id, + String role, + DateTime createdAt, + DateTime updatedAt, + DateTime lastActive, + bool online, + Map extraData, + bool banned, + List teams, + }) => + User( + id: id ?? this.id, + role: role ?? this.role, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + lastActive: lastActive ?? this.lastActive, + online: online ?? this.online, + extraData: extraData ?? this.extraData, + banned: banned ?? this.banned, + teams: teams ?? this.teams, + ); } diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index 9ba34f3a..3243988b 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -170,14 +170,14 @@ class ChannelPreview extends StatelessWidget { if (lastMessageAt.millisecondsSinceEpoch >= startOfDay.millisecondsSinceEpoch) { - stringDate = Jiffy(lastMessageAt.toLocal()).format('HH:mm'); + stringDate = Jiffy(lastMessageAt.toLocal()).jm; } else if (lastMessageAt.millisecondsSinceEpoch >= startOfDay.subtract(Duration(days: 1)).millisecondsSinceEpoch) { stringDate = 'Yesterday'; } else if (startOfDay.difference(lastMessageAt).inDays < 7) { stringDate = Jiffy(lastMessageAt.toLocal()).EEEE; } else { - stringDate = Jiffy(lastMessageAt.toLocal()).format('dd/MM/yyyy'); + stringDate = Jiffy(lastMessageAt.toLocal()).yMd; } return Text( diff --git a/packages/stream_chat_flutter/lib/src/date_divider.dart b/packages/stream_chat_flutter/lib/src/date_divider.dart index c7cfe296..20b8151d 100644 --- a/packages/stream_chat_flutter/lib/src/date_divider.dart +++ b/packages/stream_chat_flutter/lib/src/date_divider.dart @@ -28,14 +28,14 @@ class DateDivider extends StatelessWidget { now.subtract(Duration(days: 7)), Units.DAY, )) { - dayInfo = createdAt.format('EEEE'); + dayInfo = createdAt.EEEE; } else if (Jiffy(createdAt).isAfter( Jiffy(now).subtract(years: 1), Units.DAY, )) { - dayInfo = createdAt.format('MMMM d'); + dayInfo = createdAt.MMMd; } else { - dayInfo = createdAt.format('MMMM d'); + dayInfo = createdAt.MMMd; } if (uppercase) dayInfo = dayInfo.toUpperCase(); diff --git a/packages/stream_chat_flutter/lib/src/full_screen_media.dart b/packages/stream_chat_flutter/lib/src/full_screen_media.dart index 88c20a51..91c66ca4 100644 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/full_screen_media.dart @@ -220,7 +220,7 @@ class _FullScreenMediaState extends State 24) { return 'yesterday'; } else { - return 'on ${Jiffy(dateTime).format("MMM do")}'; + return 'on ${Jiffy(dateTime).MMMd}'; } } diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 4a18bf02..fc79b56c 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -2233,7 +2233,7 @@ class MessageInputState extends State { void _parseExistingMessage(Message message) { textEditingController.text = message.text; _messageIsPresent = true; - for (final attachment in message.attachments) { + for (final attachment in message?.attachments) { _attachments[attachment.id] = attachment.copyWith( uploadState: attachment.uploadState ?? UploadState.success(), ); diff --git a/packages/stream_chat_flutter/lib/src/message_search_item.dart b/packages/stream_chat_flutter/lib/src/message_search_item.dart index 335f7ff4..4cf7b5ed 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_item.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_item.dart @@ -84,9 +84,9 @@ class MessageSearchItem extends StatelessWidget { if (now.year != createdAt.year || now.month != createdAt.month || now.day != createdAt.day) { - stringDate = Jiffy(createdAt.toLocal()).format('dd/MM/yyyy'); + stringDate = Jiffy(createdAt.toLocal()).yMd; } else { - stringDate = Jiffy(createdAt.toLocal()).format('HH:mm'); + stringDate = Jiffy(createdAt.toLocal()).jm; } return Text( diff --git a/packages/stream_chat_flutter/lib/src/stream_chat.dart b/packages/stream_chat_flutter/lib/src/stream_chat.dart index 66b49d88..470591e7 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat.dart @@ -3,9 +3,11 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.dart'; +import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'dart:ui' as ui; /// Widget used to provide information about the chat to the widget tree /// @@ -121,4 +123,11 @@ class StreamChatState extends State { void initState() { super.initState(); } + + @override + void didChangeDependencies() { + final locale = ui.window.locale; + Jiffy.locale(locale.languageCode); + super.didChangeDependencies(); + } } diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index f0ed42a5..c95861d3 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -36,11 +36,9 @@ dependencies: lottie: ^1.0.0 substring_highlight: ^0.1.2 flutter_slidable: ^0.5.7 - clipboard: ^0.1.2+8 image_gallery_saver: ^1.6.7 share_plus: ^2.0.0 photo_manager: ^1.0.0 - transparent_image: ^1.0.0 ezanimation: ^0.4.1 synchronized: ^3.0.0 characters: ^1.0.0 diff --git a/packages/stream_chat_flutter/test/src/channel_preview_test.dart b/packages/stream_chat_flutter/test/src/channel_preview_test.dart index 34b9bf92..094c74a5 100644 --- a/packages/stream_chat_flutter/test/src/channel_preview_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_preview_test.dart @@ -69,7 +69,7 @@ void main() { ), )); - expect(find.text('22/06/2020'), findsOneWidget); + expect(find.text('6/22/2020'), findsOneWidget); expect(find.text('test name'), findsOneWidget); expect(find.text('1'), findsOneWidget); expect(find.text('hello'), findsOneWidget); diff --git a/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart b/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart index e75cead6..ce2e1db3 100644 --- a/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart @@ -19,10 +19,10 @@ class ChannelDao extends DatabaseAccessor (select(channels)..where((c) => c.cid.equals(cid))).join([ leftOuterJoin(users, channels.createdById.equalsExp(users.id)), ]).map((rows) { - final channel = rows.readTable(channels); - final createdBy = rows.readTable(users); + final channel = rows.readTableOrNull(channels); + final createdBy = rows.readTableOrNull(users); return channel.toChannelModel(createdBy: createdBy?.toUser()); - }).getSingle(); + }).getSingleOrNull(); /// Delete all channels by matching cid in [cids] /// diff --git a/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart b/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart index 785b5946..233fd4ef 100644 --- a/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart @@ -32,7 +32,7 @@ class ChannelQueryDao extends DatabaseAccessor Future updateChannelQueries( Map filter, List cids, { - bool clearQueryCache, + bool clearQueryCache = false, }) async => transaction(() async { final hash = _computeHash(filter); @@ -57,6 +57,14 @@ class ChannelQueryDao extends DatabaseAccessor }); }); + /// + Future> getCachedChannelCids(Map filter) { + final hash = _computeHash(filter); + return (select(channelQueries)..where((c) => c.queryHash.equals(hash))) + .map((c) => c.channelCid) + .get(); + } + /// Get list of channels by filter, sort and paginationParams Future> getChannels({ Map filter, @@ -72,12 +80,7 @@ class ChannelQueryDao extends DatabaseAccessor return true; }(), ''); - final hash = _computeHash(filter); - final cachedChannelCids = await (select(channelQueries) - ..where((c) => c.queryHash.equals(hash))) - .map((c) => c.channelCid) - .get(); - + final cachedChannelCids = await getCachedChannelCids(filter); final query = select(channels)..where((c) => c.cid.isIn(cachedChannelCids)); final cachedChannels = await (query.join([ diff --git a/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart b/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart index 94b0d333..e80a814c 100644 --- a/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart @@ -2,14 +2,13 @@ import 'package:moor/moor.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/entity/connection_events.dart'; -import 'package:stream_chat_persistence/src/entity/users.dart'; import 'package:stream_chat_persistence/src/mapper/mapper.dart'; part 'connection_event_dao.g.dart'; /// The Data Access Object for operations in [ConnectionEvents] table. -@UseDao(tables: [ConnectionEvents, Users]) +@UseDao(tables: [ConnectionEvents]) class ConnectionEventDao extends DatabaseAccessor with _$ConnectionEventDaoMixin { /// Creates a new connection event dao instance @@ -18,16 +17,16 @@ class ConnectionEventDao extends DatabaseAccessor /// Get the latest stored connection event Future get connectionEvent => select(connectionEvents) .map((eventEntity) => eventEntity.toEvent()) - .getSingle(); + .getSingleOrNull(); /// Get the latest stored lastSyncAt Future get lastSyncAt => - select(connectionEvents).getSingle().then((r) => r?.lastSyncAt); + select(connectionEvents).getSingleOrNull().then((r) => r?.lastSyncAt); /// Update stored connection event with latest data Future updateConnectionEvent(Event event) async => transaction(() async { - final connectionInfo = await select(connectionEvents).getSingle(); + final connectionInfo = await select(connectionEvents).getSingleOrNull(); await into(connectionEvents).insert( ConnectionEventEntity( id: 1, diff --git a/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.g.dart b/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.g.dart index a8245982..28eb0913 100644 --- a/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.g.dart +++ b/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.g.dart @@ -9,5 +9,4 @@ part of 'connection_event_dao.dart'; mixin _$ConnectionEventDaoMixin on DatabaseAccessor { $ConnectionEventsTable get connectionEvents => attachedDatabase.connectionEvents; - $UsersTable get users => attachedDatabase.users; } diff --git a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart index 0be4446b..1b27c763 100644 --- a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart @@ -36,9 +36,9 @@ class MessageDao extends DatabaseAccessor (delete(messages)..where((tbl) => tbl.channelCid.isIn(cids))).go(); Future _messageFromJoinRow(TypedResult rows) async { - final userEntity = rows.readTable(_users); - final pinnedByEntity = rows.readTable(_pinnedByUsers); - final msgEntity = rows.readTable(messages); + final userEntity = rows.readTableOrNull(_users); + final pinnedByEntity = rows.readTableOrNull(_pinnedByUsers); + final msgEntity = rows.readTableOrNull(messages); final latestReactions = await _db.reactionDao.getReactions(msgEntity.id); final ownReactions = await _db.reactionDao.getReactionsByUserId( msgEntity.id, @@ -66,13 +66,13 @@ class MessageDao extends DatabaseAccessor ]) ..where(messages.id.equals(id))) .map(_messageFromJoinRow) - .getSingle(); + .getSingleOrNull(); /// Returns all the messages of a particular thread by matching /// [Messages.channelCid] with [cid] Future> getThreadMessages(String cid) async => Future.wait(await (select(messages).join([ - leftOuterJoin(users, messages.userId.equalsExp(_users.id)), + leftOuterJoin(_users, messages.userId.equalsExp(_users.id)), leftOuterJoin(_pinnedByUsers, messages.pinnedByUserId.equalsExp(_pinnedByUsers.id)), ]) @@ -89,18 +89,36 @@ class MessageDao extends DatabaseAccessor PaginationParams options, }) async { final msgList = await Future.wait(await (select(messages).join([ - innerJoin(_users, messages.userId.equalsExp(_users.id)), - innerJoin( + leftOuterJoin(_users, messages.userId.equalsExp(_users.id)), + leftOuterJoin( _pinnedByUsers, messages.pinnedByUserId.equalsExp(_pinnedByUsers.id)), ]) + ..where(isNotNull(messages.parentId)) ..where(messages.parentId.equals(parentId)) ..orderBy([OrderingTerm.asc(messages.createdAt)])) .map(_messageFromJoinRow) .get()); - if (options?.lessThan != null && msgList.isNotEmpty) { - final lessThanIndex = msgList.indexWhere((m) => m.id == options.lessThan); - msgList.removeRange(lessThanIndex, msgList.length); + if (msgList.isNotEmpty) { + if (options?.lessThan != null) { + final lessThanIndex = msgList.indexWhere( + (m) => m.id == options.lessThan, + ); + if (lessThanIndex != -1) { + msgList.removeRange(lessThanIndex, msgList.length); + } + } + if (options?.greaterThanOrEqual != null) { + final greaterThanIndex = msgList.indexWhere( + (m) => m.id == options.greaterThanOrEqual, + ); + if (greaterThanIndex != -1) { + msgList.removeRange(0, greaterThanIndex); + } + } + if (options?.limit != null) { + return msgList.take(options.limit).toList(); + } } return msgList; } @@ -123,24 +141,26 @@ class MessageDao extends DatabaseAccessor .map(_messageFromJoinRow) .get()); - if (messagePagination?.lessThan != null) { - final lessThanIndex = msgList.indexWhere( - (m) => m.id == messagePagination.lessThan, - ); - if (lessThanIndex != -1) { - msgList.removeRange(lessThanIndex, msgList.length); + if (msgList.isNotEmpty) { + if (messagePagination?.lessThan != null) { + final lessThanIndex = msgList.indexWhere( + (m) => m.id == messagePagination.lessThan, + ); + if (lessThanIndex != -1) { + msgList.removeRange(lessThanIndex, msgList.length); + } } - } - if (messagePagination?.greaterThanOrEqual != null) { - final greaterThanIndex = msgList.indexWhere( - (m) => m.id == messagePagination.greaterThanOrEqual, - ); - if (greaterThanIndex != -1) { - msgList.removeRange(0, greaterThanIndex); + if (messagePagination?.greaterThanOrEqual != null) { + final greaterThanIndex = msgList.indexWhere( + (m) => m.id == messagePagination.greaterThanOrEqual, + ); + if (greaterThanIndex != -1) { + msgList.removeRange(0, greaterThanIndex); + } + } + if (messagePagination?.limit != null) { + return msgList.take(messagePagination.limit).toList(); } - } - if (messagePagination?.limit != null) { - return msgList.take(messagePagination.limit).toList(); } return msgList; } diff --git a/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart b/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart index 5e0ae721..42974520 100644 --- a/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart @@ -36,9 +36,9 @@ class PinnedMessageDao extends DatabaseAccessor (delete(pinnedMessages)..where((tbl) => tbl.channelCid.isIn(cids))).go(); Future _messageFromJoinRow(TypedResult rows) async { - final userEntity = rows.readTable(users); - final pinnedByEntity = rows.readTable(_pinnedByUsers); - final msgEntity = rows.readTable(pinnedMessages); + final userEntity = rows.readTableOrNull(users); + final pinnedByEntity = rows.readTableOrNull(_pinnedByUsers); + final msgEntity = rows.readTableOrNull(pinnedMessages); final latestReactions = await _db.reactionDao.getReactions(msgEntity.id); final ownReactions = await _db.reactionDao.getReactionsByUserId( msgEntity.id, @@ -66,7 +66,7 @@ class PinnedMessageDao extends DatabaseAccessor ]) ..where(pinnedMessages.id.equals(id))) .map(_messageFromJoinRow) - .getSingle(); + .getSingleOrNull(); /// Returns all the messages of a particular thread by matching /// [PinnedMessages.channelCid] with [cid] @@ -89,18 +89,36 @@ class PinnedMessageDao extends DatabaseAccessor PaginationParams options, }) async { final msgList = await Future.wait(await (select(pinnedMessages).join([ - innerJoin(_users, pinnedMessages.userId.equalsExp(_users.id)), - innerJoin(_pinnedByUsers, + leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)), + leftOuterJoin(_pinnedByUsers, pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id)), ]) + ..where(isNotNull(pinnedMessages.parentId)) ..where(pinnedMessages.parentId.equals(parentId)) ..orderBy([OrderingTerm.asc(pinnedMessages.createdAt)])) .map(_messageFromJoinRow) .get()); - if (options?.lessThan != null) { - final lessThanIndex = msgList.indexWhere((m) => m.id == options.lessThan); - msgList.removeRange(lessThanIndex, msgList.length); + if (msgList.isNotEmpty) { + if (options?.lessThan != null) { + final lessThanIndex = msgList.indexWhere( + (m) => m.id == options.lessThan, + ); + if (lessThanIndex != -1) { + msgList.removeRange(lessThanIndex, msgList.length); + } + } + if (options?.greaterThanOrEqual != null) { + final greaterThanIndex = msgList.indexWhere( + (m) => m.id == options.greaterThanOrEqual, + ); + if (greaterThanIndex != -1) { + msgList.removeRange(0, greaterThanIndex); + } + } + if (options?.limit != null) { + return msgList.take(options.limit).toList(); + } } return msgList; } @@ -123,24 +141,26 @@ class PinnedMessageDao extends DatabaseAccessor .map(_messageFromJoinRow) .get()); - if (messagePagination?.lessThan != null) { - final lessThanIndex = msgList.indexWhere( - (m) => m.id == messagePagination.lessThan, - ); - if (lessThanIndex != -1) { - msgList.removeRange(lessThanIndex, msgList.length); + if (msgList.isNotEmpty) { + if (messagePagination?.lessThan != null) { + final lessThanIndex = msgList.indexWhere( + (m) => m.id == messagePagination.lessThan, + ); + if (lessThanIndex != -1) { + msgList.removeRange(lessThanIndex, msgList.length); + } } - } - if (messagePagination?.greaterThanOrEqual != null) { - final greaterThanIndex = msgList.indexWhere( - (m) => m.id == messagePagination.greaterThanOrEqual, - ); - if (greaterThanIndex != -1) { - msgList.removeRange(0, greaterThanIndex); + if (messagePagination?.greaterThanOrEqual != null) { + final greaterThanIndex = msgList.indexWhere( + (m) => m.id == messagePagination.greaterThanOrEqual, + ); + if (greaterThanIndex != -1) { + msgList.removeRange(0, greaterThanIndex); + } + } + if (messagePagination?.limit != null) { + return msgList.take(messagePagination.limit).toList(); } - } - if (messagePagination?.limit != null) { - return msgList.take(messagePagination.limit).toList(); } return msgList; } diff --git a/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart b/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart index ef06326a..a2a92b2e 100644 --- a/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart @@ -23,8 +23,8 @@ class ReactionDao extends DatabaseAccessor ..where(reactions.messageId.equals(messageId)) ..orderBy([OrderingTerm.asc(reactions.createdAt)])) .map((rows) { - final userEntity = rows.readTable(users); - final reactionEntity = rows.readTable(reactions); + final userEntity = rows.readTableOrNull(users); + final reactionEntity = rows.readTableOrNull(reactions); return reactionEntity.toReaction(user: userEntity?.toUser()); }).get(); diff --git a/packages/stream_chat_persistence/lib/src/dao/user_dao.dart b/packages/stream_chat_persistence/lib/src/dao/user_dao.dart index 65ce1521..9cdff8e7 100644 --- a/packages/stream_chat_persistence/lib/src/dao/user_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/user_dao.dart @@ -20,4 +20,10 @@ class UserDao extends DatabaseAccessor with _$UserDaoMixin { mode: InsertMode.insertOrReplace, ), ); + + /// Returns the list of all the users stored in db + Future> getUsers() => + (select(users)..orderBy([(u) => OrderingTerm.desc(u.createdAt)])) + .map((it) => it.toUser()) + .get(); } 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 65686cb1..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 @@ -1,22 +1,15 @@ +import 'package:meta/meta.dart'; +import 'package:moor/ffi.dart'; import 'package:moor/moor.dart'; 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/db/shared/shared_db.dart'; import 'package:stream_chat_persistence/src/entity/entity.dart'; -part 'moor_chat_database.g.dart'; +export 'shared/shared_db.dart'; -LazyDatabase _openConnection( - String userId, { - bool logStatements = false, - bool persistOnDisk = true, -}) => - LazyDatabase(() async => SharedDB.constructDatabase( - userId, - logStatements: logStatements, - persistOnDisk: persistOnDisk, - )); +part 'moor_chat_database.g.dart'; /// A chat database implemented using moor @UseMoor(tables: [ @@ -43,14 +36,9 @@ LazyDatabase _openConnection( class MoorChatDatabase extends _$MoorChatDatabase { /// Creates a new moor chat database instance MoorChatDatabase( - this._userId, { - logStatements = false, - bool persistOnDisk = true, - }) : super(_openConnection( - _userId, - logStatements: logStatements, - persistOnDisk: persistOnDisk, - )); + this._userId, + QueryExecutor executor, + ) : super(executor); /// Instantiate a new database instance MoorChatDatabase.connect( @@ -58,6 +46,10 @@ class MoorChatDatabase extends _$MoorChatDatabase { DatabaseConnection connection, ) : super.connect(connection); + /// Custom constructor used only for testing + @visibleForTesting + MoorChatDatabase.testable(this._userId) : super(VmDatabase.memory()); + final String _userId; /// User id to which the database is connected 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 7592247e..27aa23b5 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 @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:io'; import 'dart:isolate'; @@ -14,27 +15,49 @@ 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, - bool persistOnDisk = true, - }) async { + ConnectionMode connectionMode = ConnectionMode.regular, + }) { final dbName = 'db_$userId'; - if (persistOnDisk) { - if (Platform.isIOS || Platform.isAndroid) { - final dir = await getApplicationDocumentsDirectory(); - final path = join(dir.path, '$dbName.sqlite'); - final file = File(path); - return VmDatabase(file, logStatements: logStatements); - } - if (Platform.isMacOS || Platform.isLinux) { - final file = File('$dbName.sqlite'); - return VmDatabase(file, logStatements: logStatements); - } + 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'); + final file = File(path); + return VmDatabase(file, logStatements: logStatements); + } + if (Platform.isMacOS || Platform.isLinux) { + final file = File('$dbName.sqlite'); + return VmDatabase(file, logStatements: logStatements); } return VmDatabase.memory(logStatements: logStatements); } @@ -69,27 +92,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 3a764d24..1054a1df 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,29 +1,17 @@ -import 'package:moor/backends.dart'; +// coverage:ignore-file 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, - bool persistOnDisk = true, + 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 32b108ba..bb664443 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 @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'package:moor/moor_web.dart'; import 'package:stream_chat_persistence/src/stream_chat_persistence_client.dart'; @@ -6,27 +7,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, - bool persistOnDisk = true, // ignored on web - }) 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/entity/channel_queries.dart b/packages/stream_chat_persistence/lib/src/entity/channel_queries.dart index 301e3592..61985d26 100644 --- a/packages/stream_chat_persistence/lib/src/entity/channel_queries.dart +++ b/packages/stream_chat_persistence/lib/src/entity/channel_queries.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'package:moor/moor.dart'; /// Represents a [ChannelQueries] table in [MoorChatDatabase]. diff --git a/packages/stream_chat_persistence/lib/src/entity/channels.dart b/packages/stream_chat_persistence/lib/src/entity/channels.dart index 7e32906e..6f60fac3 100644 --- a/packages/stream_chat_persistence/lib/src/entity/channels.dart +++ b/packages/stream_chat_persistence/lib/src/entity/channels.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'package:moor/moor.dart'; import 'package:stream_chat_persistence/src/converter/map_converter.dart'; diff --git a/packages/stream_chat_persistence/lib/src/entity/connection_events.dart b/packages/stream_chat_persistence/lib/src/entity/connection_events.dart index 7caa0407..69538edc 100644 --- a/packages/stream_chat_persistence/lib/src/entity/connection_events.dart +++ b/packages/stream_chat_persistence/lib/src/entity/connection_events.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'package:moor/moor.dart'; import 'package:stream_chat_persistence/src/converter/map_converter.dart'; diff --git a/packages/stream_chat_persistence/lib/src/entity/members.dart b/packages/stream_chat_persistence/lib/src/entity/members.dart index 9eafd4aa..c7316640 100644 --- a/packages/stream_chat_persistence/lib/src/entity/members.dart +++ b/packages/stream_chat_persistence/lib/src/entity/members.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'package:moor/moor.dart'; /// Represents a [Members] table in [MoorChatDatabase]. diff --git a/packages/stream_chat_persistence/lib/src/entity/messages.dart b/packages/stream_chat_persistence/lib/src/entity/messages.dart index 0350d9de..8840d85f 100644 --- a/packages/stream_chat_persistence/lib/src/entity/messages.dart +++ b/packages/stream_chat_persistence/lib/src/entity/messages.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'package:moor/moor.dart'; import 'package:stream_chat_persistence/src/converter/list_converter.dart'; import 'package:stream_chat_persistence/src/converter/map_converter.dart'; diff --git a/packages/stream_chat_persistence/lib/src/entity/pinned_messages.dart b/packages/stream_chat_persistence/lib/src/entity/pinned_messages.dart index de081552..043f69c5 100644 --- a/packages/stream_chat_persistence/lib/src/entity/pinned_messages.dart +++ b/packages/stream_chat_persistence/lib/src/entity/pinned_messages.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'package:moor/moor.dart'; import 'package:stream_chat_persistence/src/entity/messages.dart'; diff --git a/packages/stream_chat_persistence/lib/src/entity/reactions.dart b/packages/stream_chat_persistence/lib/src/entity/reactions.dart index b7e6342f..53cbdd11 100644 --- a/packages/stream_chat_persistence/lib/src/entity/reactions.dart +++ b/packages/stream_chat_persistence/lib/src/entity/reactions.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'package:moor/moor.dart'; import 'package:stream_chat_persistence/src/converter/map_converter.dart'; diff --git a/packages/stream_chat_persistence/lib/src/entity/reads.dart b/packages/stream_chat_persistence/lib/src/entity/reads.dart index 4b9ce6fb..7943ed9e 100644 --- a/packages/stream_chat_persistence/lib/src/entity/reads.dart +++ b/packages/stream_chat_persistence/lib/src/entity/reads.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'package:moor/moor.dart'; /// Represents a [Reads] table in [MoorChatDatabase]. diff --git a/packages/stream_chat_persistence/lib/src/entity/users.dart b/packages/stream_chat_persistence/lib/src/entity/users.dart index 4916ad28..7ece9600 100644 --- a/packages/stream_chat_persistence/lib/src/entity/users.dart +++ b/packages/stream_chat_persistence/lib/src/entity/users.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'package:moor/moor.dart'; import 'package:stream_chat_persistence/src/converter/map_converter.dart'; diff --git a/packages/stream_chat_persistence/lib/src/mapper/channel_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/channel_mapper.dart index 5c8e3e2a..65315519 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/channel_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/channel_mapper.dart @@ -46,14 +46,14 @@ extension ChannelModelX on ChannelModel { id: id, type: type, cid: cid, - config: config.toJson(), + config: config?.toJson(), frozen: frozen, lastMessageAt: lastMessageAt, createdAt: createdAt, updatedAt: updatedAt, deletedAt: deletedAt, memberCount: memberCount, - createdById: createdBy.id, + createdById: createdBy?.id, extraData: extraData, ); } 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/moor_schemas/moor_schema_v1.json b/packages/stream_chat_persistence/moor_schemas/moor_schema_v1.json new file mode 100644 index 00000000..1a1b39a9 --- /dev/null +++ b/packages/stream_chat_persistence/moor_schemas/moor_schema_v1.json @@ -0,0 +1,798 @@ +{ + "_meta": { + "description": "This file contains a serialized version of schema entities for moor.", + "version": "0.1.0-dev-preview" + }, + "entities": [ + { + "id": 0, + "references": [], + "type": "table", + "data": { + "name": "channels", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "cid", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "config", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "MapConverter()", + "dart_type_name": "Map" + } + }, + { + "name": "frozen", + "moor_type": "ColumnType.boolean", + "nullable": false, + "customConstraints": null, + "default_dart": "Constant(false)", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_message_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "deleted_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "member_count", + "moor_type": "ColumnType.integer", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_by_id", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "extra_data", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "MapConverter()", + "dart_type_name": "Map" + } + } + ], + "is_virtual": false, + "explicit_pk": [ + "cid" + ] + } + }, + { + "id": 1, + "references": [], + "type": "table", + "data": { + "name": "messages", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "message_text", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "attachments", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "ListConverter()", + "dart_type_name": "List" + } + }, + { + "name": "status", + "moor_type": "ColumnType.integer", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "MessageSendingStatusConverter()", + "dart_type_name": "MessageSendingStatus" + } + }, + { + "name": "type", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "mentioned_users", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "ListConverter()", + "dart_type_name": "List" + } + }, + { + "name": "reaction_counts", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "MapConverter()", + "dart_type_name": "Map" + } + }, + { + "name": "reaction_scores", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "MapConverter()", + "dart_type_name": "Map" + } + }, + { + "name": "parent_id", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quoted_message_id", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "reply_count", + "moor_type": "ColumnType.integer", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "show_in_channel", + "moor_type": "ColumnType.boolean", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "shadowed", + "moor_type": "ColumnType.boolean", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "command", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "moor_type": "ColumnType.datetime", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "deleted_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "user_id", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "channel_cid", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": "NULLABLE REFERENCES channels(cid) ON DELETE CASCADE", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "extra_data", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "MapConverter()", + "dart_type_name": "Map" + } + } + ], + "is_virtual": false, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 2, + "references": [], + "type": "table", + "data": { + "name": "reactions", + "was_declared_in_moor": false, + "columns": [ + { + "name": "user_id", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "message_id", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": "REFERENCES messages(id) ON DELETE CASCADE", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "moor_type": "ColumnType.datetime", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "score", + "moor_type": "ColumnType.integer", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "extra_data", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "MapConverter()", + "dart_type_name": "Map" + } + } + ], + "is_virtual": false, + "explicit_pk": [ + "message_id", + "type", + "user_id" + ] + } + }, + { + "id": 3, + "references": [], + "type": "table", + "data": { + "name": "users", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "role", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_active", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "online", + "moor_type": "ColumnType.boolean", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "banned", + "moor_type": "ColumnType.boolean", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "extra_data", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "MapConverter()", + "dart_type_name": "Map" + } + } + ], + "is_virtual": false, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 4, + "references": [], + "type": "table", + "data": { + "name": "members", + "was_declared_in_moor": false, + "columns": [ + { + "name": "user_id", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "channel_cid", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": "REFERENCES channels(cid) ON DELETE CASCADE", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "role", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "invite_accepted_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "invite_rejected_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "invited", + "moor_type": "ColumnType.boolean", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "banned", + "moor_type": "ColumnType.boolean", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "shadow_banned", + "moor_type": "ColumnType.boolean", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_moderator", + "moor_type": "ColumnType.boolean", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "moor_type": "ColumnType.datetime", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "explicit_pk": [ + "user_id", + "channel_cid" + ] + } + }, + { + "id": 5, + "references": [], + "type": "table", + "data": { + "name": "reads", + "was_declared_in_moor": false, + "columns": [ + { + "name": "last_read", + "moor_type": "ColumnType.datetime", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "user_id", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "channel_cid", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": "REFERENCES channels(cid) ON DELETE CASCADE", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "unread_messages", + "moor_type": "ColumnType.integer", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "explicit_pk": [ + "user_id", + "channel_cid" + ] + } + }, + { + "id": 6, + "references": [], + "type": "table", + "data": { + "name": "channel_queries", + "was_declared_in_moor": false, + "columns": [ + { + "name": "query_hash", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "channel_cid", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "explicit_pk": [ + "query_hash", + "channel_cid" + ] + } + }, + { + "id": 7, + "references": [], + "type": "table", + "data": { + "name": "connection_events", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "moor_type": "ColumnType.integer", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "own_user", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "MapConverter()", + "dart_type_name": "Map" + } + }, + { + "name": "total_unread_count", + "moor_type": "ColumnType.integer", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "unread_channels", + "moor_type": "ColumnType.integer", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_event_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_sync_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "explicit_pk": [ + "id" + ] + } + } + ] +} \ No newline at end of file diff --git a/packages/stream_chat_persistence/moor_schemas/moor_schema_v2.json b/packages/stream_chat_persistence/moor_schemas/moor_schema_v2.json new file mode 100644 index 00000000..0f1caee5 --- /dev/null +++ b/packages/stream_chat_persistence/moor_schemas/moor_schema_v2.json @@ -0,0 +1,1089 @@ +{ + "_meta": { + "description": "This file contains a serialized version of schema entities for moor.", + "version": "0.1.0-dev-preview" + }, + "entities": [ + { + "id": 0, + "references": [], + "type": "table", + "data": { + "name": "channels", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "cid", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "config", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "MapConverter()", + "dart_type_name": "Map" + } + }, + { + "name": "frozen", + "moor_type": "ColumnType.boolean", + "nullable": false, + "customConstraints": null, + "default_dart": "Constant(false)", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_message_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "deleted_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "member_count", + "moor_type": "ColumnType.integer", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_by_id", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "extra_data", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "MapConverter()", + "dart_type_name": "Map" + } + } + ], + "is_virtual": false, + "explicit_pk": [ + "cid" + ] + } + }, + { + "id": 1, + "references": [], + "type": "table", + "data": { + "name": "messages", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "message_text", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "attachments", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "ListConverter()", + "dart_type_name": "List" + } + }, + { + "name": "status", + "moor_type": "ColumnType.integer", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "MessageSendingStatusConverter()", + "dart_type_name": "MessageSendingStatus" + } + }, + { + "name": "type", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "mentioned_users", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "ListConverter()", + "dart_type_name": "List" + } + }, + { + "name": "reaction_counts", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "MapConverter()", + "dart_type_name": "Map" + } + }, + { + "name": "reaction_scores", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "MapConverter()", + "dart_type_name": "Map" + } + }, + { + "name": "parent_id", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quoted_message_id", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "reply_count", + "moor_type": "ColumnType.integer", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "show_in_channel", + "moor_type": "ColumnType.boolean", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "shadowed", + "moor_type": "ColumnType.boolean", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "command", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "moor_type": "ColumnType.datetime", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "deleted_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "user_id", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "pinned", + "moor_type": "ColumnType.boolean", + "nullable": false, + "customConstraints": null, + "default_dart": "const Constant(false)", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "pinned_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "pin_expires", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "pinned_by_user_id", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "channel_cid", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": "NULLABLE REFERENCES channels(cid) ON DELETE CASCADE", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "extra_data", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "MapConverter()", + "dart_type_name": "Map" + } + } + ], + "is_virtual": false, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 2, + "references": [], + "type": "table", + "data": { + "name": "pinned_messages", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "message_text", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "attachments", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "ListConverter()", + "dart_type_name": "List" + } + }, + { + "name": "status", + "moor_type": "ColumnType.integer", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "MessageSendingStatusConverter()", + "dart_type_name": "MessageSendingStatus" + } + }, + { + "name": "type", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "mentioned_users", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "ListConverter()", + "dart_type_name": "List" + } + }, + { + "name": "reaction_counts", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "MapConverter()", + "dart_type_name": "Map" + } + }, + { + "name": "reaction_scores", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "MapConverter()", + "dart_type_name": "Map" + } + }, + { + "name": "parent_id", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quoted_message_id", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "reply_count", + "moor_type": "ColumnType.integer", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "show_in_channel", + "moor_type": "ColumnType.boolean", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "shadowed", + "moor_type": "ColumnType.boolean", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "command", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "moor_type": "ColumnType.datetime", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "deleted_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "user_id", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "pinned", + "moor_type": "ColumnType.boolean", + "nullable": false, + "customConstraints": null, + "default_dart": "const Constant(false)", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "pinned_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "pin_expires", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "pinned_by_user_id", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "channel_cid", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": "NULLABLE REFERENCES channels(cid) ON DELETE CASCADE", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "extra_data", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "MapConverter()", + "dart_type_name": "Map" + } + } + ], + "is_virtual": false, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 3, + "references": [], + "type": "table", + "data": { + "name": "reactions", + "was_declared_in_moor": false, + "columns": [ + { + "name": "user_id", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "message_id", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": "REFERENCES messages(id) ON DELETE CASCADE", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "moor_type": "ColumnType.datetime", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "score", + "moor_type": "ColumnType.integer", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "extra_data", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "MapConverter()", + "dart_type_name": "Map" + } + } + ], + "is_virtual": false, + "explicit_pk": [ + "message_id", + "type", + "user_id" + ] + } + }, + { + "id": 4, + "references": [], + "type": "table", + "data": { + "name": "users", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "role", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_active", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "online", + "moor_type": "ColumnType.boolean", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "banned", + "moor_type": "ColumnType.boolean", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "extra_data", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "MapConverter()", + "dart_type_name": "Map" + } + } + ], + "is_virtual": false, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 5, + "references": [], + "type": "table", + "data": { + "name": "members", + "was_declared_in_moor": false, + "columns": [ + { + "name": "user_id", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "channel_cid", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": "REFERENCES channels(cid) ON DELETE CASCADE", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "role", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "invite_accepted_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "invite_rejected_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "invited", + "moor_type": "ColumnType.boolean", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "banned", + "moor_type": "ColumnType.boolean", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "shadow_banned", + "moor_type": "ColumnType.boolean", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_moderator", + "moor_type": "ColumnType.boolean", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "moor_type": "ColumnType.datetime", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "explicit_pk": [ + "user_id", + "channel_cid" + ] + } + }, + { + "id": 6, + "references": [], + "type": "table", + "data": { + "name": "reads", + "was_declared_in_moor": false, + "columns": [ + { + "name": "last_read", + "moor_type": "ColumnType.datetime", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "user_id", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "channel_cid", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": "REFERENCES channels(cid) ON DELETE CASCADE", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "unread_messages", + "moor_type": "ColumnType.integer", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "explicit_pk": [ + "user_id", + "channel_cid" + ] + } + }, + { + "id": 7, + "references": [], + "type": "table", + "data": { + "name": "channel_queries", + "was_declared_in_moor": false, + "columns": [ + { + "name": "query_hash", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "channel_cid", + "moor_type": "ColumnType.text", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "explicit_pk": [ + "query_hash", + "channel_cid" + ] + } + }, + { + "id": 8, + "references": [], + "type": "table", + "data": { + "name": "connection_events", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "moor_type": "ColumnType.integer", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "own_user", + "moor_type": "ColumnType.text", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "MapConverter()", + "dart_type_name": "Map" + } + }, + { + "name": "total_unread_count", + "moor_type": "ColumnType.integer", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "unread_channels", + "moor_type": "ColumnType.integer", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_event_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_sync_at", + "moor_type": "ColumnType.datetime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "explicit_pk": [ + "id" + ] + } + } + ] +} \ No newline at end of file diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index 3242efc3..1dc11a9c 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -21,8 +21,13 @@ dependencies: stream_chat: path: ../stream_chat +dependency_overrides: + stream_chat: + path: ../stream_chat + dev_dependencies: build_runner: ^1.11.0 + mocktail: ^0.1.0 moor_generator: ^4.2.0 pedantic: ^1.11.0 test: ^1.16.0 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..53a9a178 --- /dev/null +++ b/packages/stream_chat_persistence/test/mock_chat_database.dart @@ -0,0 +1,71 @@ +import 'package:mocktail/mocktail.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/converter/list_coverter_test.dart b/packages/stream_chat_persistence/test/src/converter/list_coverter_test.dart new file mode 100644 index 00000000..a5bedd40 --- /dev/null +++ b/packages/stream_chat_persistence/test/src/converter/list_coverter_test.dart @@ -0,0 +1,53 @@ +import 'dart:convert'; + +import 'package:test/test.dart'; +import 'package:stream_chat_persistence/src/converter/list_converter.dart'; + +void main() { + group('mapToDart', () { + final listConverter = ListConverter(); + + test('should return null if nothing is provided', () { + final res = listConverter.mapToDart(null); + expect(res, isNull); + }); + + test('should throw type error if the provided json is not a list', () { + final json = {'test_key': 'testData'}; + expect( + () => listConverter.mapToDart(jsonEncode(json)), + throwsA(isA()), + ); + }); + + test('should throw type error if the provided json is not a list of String', + () { + final json = [22, 33, 44]; + expect( + () => listConverter.mapToDart(jsonEncode(json)), + throwsA(isA()), + ); + }); + + test('should return list of String if json data list is provided', () { + final data = ['data1', 'data2', 'data3']; + final res = listConverter.mapToDart(jsonEncode(data)); + expect(res.length, data.length); + }); + }); + + group('mapToSql', () { + final listConverter = ListConverter(); + + test('should return null if nothing is provided', () { + final res = listConverter.mapToSql(null); + expect(res, isNull); + }); + + test('should return json string if data list is provided', () { + final data = ['data1', 'data2', 'data3']; + final res = listConverter.mapToSql(data); + expect(res, jsonEncode(data)); + }); + }); +} diff --git a/packages/stream_chat_persistence/test/src/converter/map_converter_test.dart b/packages/stream_chat_persistence/test/src/converter/map_converter_test.dart new file mode 100644 index 00000000..4727cb0d --- /dev/null +++ b/packages/stream_chat_persistence/test/src/converter/map_converter_test.dart @@ -0,0 +1,64 @@ +import 'dart:convert'; + +import 'package:test/test.dart'; +import 'package:stream_chat_persistence/src/converter/map_converter.dart'; + +void main() { + group('mapToDart', () { + final mapConverter = MapConverter(); + + test('should return null if nothing is provided', () { + final res = mapConverter.mapToDart(null); + expect(res, isNull); + }); + + test('should throw type error if the provided json is not a map', () { + const json = ['testData1', 'testData2', 'testData3']; + expect( + () => mapConverter.mapToDart(jsonEncode(json)), + throwsA(isA()), + ); + }); + + test( + 'should throw type error if the provided json is not a ' + 'map of String, String', + () { + const json = {'test_key': 22, 'test_key2': 33, 'test_key3': 44}; + expect( + () => mapConverter.mapToDart(jsonEncode(json)), + throwsA(isA()), + ); + }, + ); + + test('should return map of String, String if json data is provided', () { + const data = { + 'test_key': 'testValue', + 'test_key2': 'testValue2', + 'test_key3': 'testValue3', + }; + final res = mapConverter.mapToDart(jsonEncode(data)); + expect(res, data); + }); + }); + + group('mapToSql', () { + final mapConverter = MapConverter(); + + test('should return null if nothing is provided', () { + final res = mapConverter.mapToSql(null); + expect(res, isNull); + }); + + test('should return json string if data map is provided', () { + const data = { + 'test_key': 'testValue', + 'test_key2': 'testValue2', + 'test_key3': 'testValue3', + }; + final res = mapConverter.mapToSql(data); + expect(res, jsonEncode(data)); + }); + }); +} diff --git a/packages/stream_chat_persistence/test/src/converter/message_sending_status_converter_test.dart b/packages/stream_chat_persistence/test/src/converter/message_sending_status_converter_test.dart new file mode 100644 index 00000000..1035a759 --- /dev/null +++ b/packages/stream_chat_persistence/test/src/converter/message_sending_status_converter_test.dart @@ -0,0 +1,33 @@ +import 'package:test/test.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_persistence/src/converter/message_sending_status_converter.dart'; + +void main() { + group('mapToDart', () { + final statusConverter = MessageSendingStatusConverter(); + + test('should return null if nothing is provided', () { + final res = statusConverter.mapToDart(null); + expect(res, isNull); + }); + + test('should return expected status if status code is provided', () { + final res = statusConverter.mapToDart(3); + expect(res, MessageSendingStatus.updating); + }); + }); + + group('mapToSql', () { + final statusConverter = MessageSendingStatusConverter(); + + test('should return null if nothing is provided', () { + final res = statusConverter.mapToSql(null); + expect(res, isNull); + }); + + test('should return expected code if the status is provided', () { + final res = statusConverter.mapToSql(MessageSendingStatus.updating); + expect(res, 3); + }); + }); +} diff --git a/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart new file mode 100644 index 00000000..47c655a8 --- /dev/null +++ b/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart @@ -0,0 +1,130 @@ +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_persistence/src/dao/channel_dao.dart'; +import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:test/test.dart'; + +void main() { + ChannelDao channelDao; + MoorChatDatabase database; + + setUp(() { + database = MoorChatDatabase.testable('testUserId'); + channelDao = database.channelDao; + }); + + test('getChannelByCid', () async { + const id = 'testId'; + const cid = 'testCid'; + const type = 'testType'; + + // Should be null initially + final channel = await channelDao.getChannelByCid(cid); + expect(channel, isNull); + + // Saving a dummy channel + final dummyChannel = ChannelModel( + id: id, + type: type, + cid: cid, + config: ChannelConfig(), + ); + await channelDao.updateChannels([dummyChannel]); + + // Should match the dummy channel + final updatedChannel = await channelDao.getChannelByCid(cid); + expect(updatedChannel.id, id); + expect(updatedChannel.cid, cid); + expect(updatedChannel.type, type); + }); + + test('deleteChannelByCids', () async { + const id = 'testId'; + const cid = 'testCid'; + const type = 'testType'; + + // Saving a dummy channel + final dummyChannel = ChannelModel( + id: id, + type: type, + cid: cid, + config: ChannelConfig(), + ); + await channelDao.updateChannels([dummyChannel]); + + // Should match the dummy channel + final updatedChannel = await channelDao.getChannelByCid(cid); + expect(updatedChannel.id, id); + expect(updatedChannel.cid, cid); + expect(updatedChannel.type, type); + + // Deleting the dummyChannel using cid + await channelDao.deleteChannelByCids([cid]); + + // Fetched channel Should be null + final channel = await channelDao.getChannelByCid(cid); + expect(channel, isNull); + }); + + test('cids', () async { + // Should be empty initially + final cids = await channelDao.cids; + expect(cids, []); + + const id = 'testId'; + const cid = 'testCid'; + const type = 'testType'; + + // Saving a dummy channel + final dummyChannel = ChannelModel( + id: id, + type: type, + cid: cid, + config: ChannelConfig(), + ); + await channelDao.updateChannels([dummyChannel]); + + // Should return the cid of the dummy channel + final updatedCids = await channelDao.cids; + expect(updatedCids, [cid]); + }); + + test('updateChannels', () async { + const id = 'testId'; + const cid = 'testCid'; + const type = 'testType'; + + // Should be null initially + final channel = await channelDao.getChannelByCid(cid); + expect(channel, isNull); + + // Saving a dummy channel + final dummyChannel = ChannelModel( + id: id, + type: type, + cid: cid, + config: ChannelConfig(), + ); + await channelDao.updateChannels([dummyChannel]); + + // Should match the dummy channel + final updatedChannel = await channelDao.getChannelByCid(cid); + expect(updatedChannel.id, id); + expect(updatedChannel.cid, cid); + expect(updatedChannel.type, type); + + // Updating the previously saved channel + const newType = 'newTestType'; + final newChannel = dummyChannel.copyWith(type: newType); + await channelDao.updateChannels([newChannel]); + + // Should match the new channel + final newUpdatedChannel = await channelDao.getChannelByCid(cid); + expect(newUpdatedChannel.id, id); + expect(newUpdatedChannel.cid, cid); + expect(newUpdatedChannel.type, newType); + }); + + tearDown(() async { + await database.disconnect(); + }); +} diff --git a/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart new file mode 100644 index 00000000..4566ca94 --- /dev/null +++ b/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart @@ -0,0 +1,273 @@ +import 'dart:math' as math; + +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_persistence/src/dao/channel_query_dao.dart'; +import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:test/test.dart'; + +import '../utils/date_matcher.dart'; + +void main() { + MoorChatDatabase database; + ChannelQueryDao channelQueryDao; + + setUp(() { + database = MoorChatDatabase.testable('testUserId'); + channelQueryDao = database.channelQueryDao; + }); + + test('updateChannelQueries', () async { + const filter = { + 'members': { + r'$in': ['testUserId'], + }, + }; + + const cids = ['testCid1', 'testCid2', 'testCid3']; + + final cachedCids = await channelQueryDao.getCachedChannelCids(filter); + expect(cachedCids, isEmpty); + + // Updating channel queries + await channelQueryDao.updateChannelQueries(filter, cids); + + final updatedCids = await channelQueryDao.getCachedChannelCids(filter); + expect(updatedCids, cids); + }); + + test('clear queryCache before updateChannelQueries', () async { + const filter = { + 'members': { + r'$in': ['testUserId'], + }, + }; + + const cids = ['testCid1', 'testCid2', 'testCid3']; + + final cachedCids = await channelQueryDao.getCachedChannelCids(filter); + expect(cachedCids, isEmpty); + + // Updating channel queries + await channelQueryDao.updateChannelQueries( + filter, + cids, + clearQueryCache: true, + ); + + final updatedCids = await channelQueryDao.getCachedChannelCids(filter); + expect(updatedCids, cids); + }); + + test('getCachedChannelCids', () async { + const filter = { + 'members': { + r'$in': ['testUserId'], + }, + }; + + const cids = ['testCid1', 'testCid2', 'testCid3']; + + final cachedCids = await channelQueryDao.getCachedChannelCids(filter); + expect(cachedCids, isEmpty); + + // Updating channel queries + await channelQueryDao.updateChannelQueries(filter, cids); + + final updatedCids = await channelQueryDao.getCachedChannelCids(filter); + expect(updatedCids, cids); + }); + + Future> _insertTestDataForGetChannel( + Map filter, { + int count = 3, + }) async { + final now = DateTime.now(); + final userDao = database.userDao; + final channelDao = database.channelDao; + + final cids = List.generate(count, (index) => 'testCid$index'); + final users = List.generate(count, (index) => User(id: 'testId$index')); + final channels = List.generate( + count, + (index) => ChannelModel( + id: 'testId$index', + type: 'testType$index', + cid: cids[index], + createdBy: users[index], + config: ChannelConfig(), + extraData: {'test_custom_field': math.Random().nextInt(100)}, + createdAt: now, + memberCount: math.Random().nextInt(100), + lastMessageAt: now.add(Duration(hours: index)), + ), + ).reversed.toList(growable: false); + + await userDao.updateUsers(users); + await channelDao.updateChannels(channels); + await channelQueryDao.updateChannelQueries(filter, cids); + + return channels; + } + + group('getChannels', () { + const filter = { + 'members': { + r'$in': ['testUserId'], + }, + }; + + test('should return empty list of channels', () async { + final channels = await channelQueryDao.getChannels(filter: filter); + expect(channels, isEmpty); + }); + + test('should return all the inserted channels', () async { + // Inserting test data for get channels + final insertedChannels = await _insertTestDataForGetChannel(filter); + + // Should match with the inserted channels + final updatedChannels = await channelQueryDao.getChannels(filter: filter); + expect(updatedChannels.length, insertedChannels.length); + for (var i = 0; i < updatedChannels.length; i++) { + final updatedChannel = updatedChannels[i]; + final insertedChannel = insertedChannels[i]; + + // Should match all the basic details + expect(updatedChannel.id, insertedChannel.id); + expect(updatedChannel.type, insertedChannel.type); + expect(updatedChannel.cid, insertedChannel.cid); + expect(updatedChannel.memberCount, insertedChannel.memberCount); + + // Should match createdAt date + expect( + updatedChannel.createdAt, + isSameDateAs(insertedChannel.createdAt), + ); + + // Should match lastMessageAt date + expect( + updatedChannel.lastMessageAt, + isSameDateAs(insertedChannel.lastMessageAt), + ); + } + }); + + test( + 'should return all the inserted channels along with pagination applied', + () async { + const offset = 5; + const limit = 15; + const pagination = PaginationParams(offset: offset, limit: limit); + + // Inserting test data for get channels + final insertedChannels = await _insertTestDataForGetChannel( + filter, + count: 30, + ); + + // Should match with the inserted channels + final updatedChannels = await channelQueryDao.getChannels( + filter: filter, + paginationParams: pagination, + ); + expect(updatedChannels.length, limit); + expect(updatedChannels.first.id, 'testId24'); + expect(updatedChannels.first.cid, 'testCid24'); + }, + ); + + test('should return sorted channels using member count', () async { + int sortComparator(ChannelModel a, ChannelModel b) => + b.memberCount.compareTo(a.memberCount); + + // Inserting test data for get channels + final insertedChannels = await _insertTestDataForGetChannel(filter); + insertedChannels.sort(sortComparator); + + // Should match with the inserted channels + final updatedChannels = await channelQueryDao.getChannels( + filter: filter, + sort: [SortOption('member_count', comparator: sortComparator)], + ); + + expect(updatedChannels.length, insertedChannels.length); + for (var i = 0; i < updatedChannels.length; i++) { + final updatedChannel = updatedChannels[i]; + final insertedChannel = insertedChannels[i]; + + // Should match all the basic details + expect(updatedChannel.id, insertedChannel.id); + expect(updatedChannel.type, insertedChannel.type); + expect(updatedChannel.cid, insertedChannel.cid); + expect(updatedChannel.memberCount, insertedChannel.memberCount); + + // Should match createdAt date + expect( + updatedChannel.createdAt, + isSameDateAs(insertedChannel.createdAt), + ); + + // Should match lastMessageAt date + expect( + updatedChannel.lastMessageAt, + isSameDateAs(insertedChannel.lastMessageAt), + ); + } + }); + + test('should throw if comparator is not provided in sort list', () { + expect( + () => channelQueryDao.getChannels( + sort: [const SortOption('test_custom_field')], + ), + throwsArgumentError, + ); + }); + + test('should return sorted channels using custom field', () async { + int sortComparator(ChannelModel a, ChannelModel b) { + final aData = a.extraData['test_custom_field'] as int; + final bData = b.extraData['test_custom_field'] as int; + return bData.compareTo(aData); + } + + // Inserting test data for get channels + final insertedChannels = await _insertTestDataForGetChannel(filter); + insertedChannels.sort(sortComparator); + + // Should match with the inserted channels + final updatedChannels = await channelQueryDao.getChannels( + filter: filter, + sort: [SortOption('test_custom_field', comparator: sortComparator)], + ); + + expect(updatedChannels.length, insertedChannels.length); + for (var i = 0; i < updatedChannels.length; i++) { + final updatedChannel = updatedChannels[i]; + final insertedChannel = insertedChannels[i]; + + // Should match all the basic details + expect(updatedChannel.id, insertedChannel.id); + expect(updatedChannel.type, insertedChannel.type); + expect(updatedChannel.cid, insertedChannel.cid); + expect(updatedChannel.memberCount, insertedChannel.memberCount); + + // Should match createdAt date + expect( + updatedChannel.createdAt, + isSameDateAs(insertedChannel.createdAt), + ); + + // Should match lastMessageAt date + expect( + updatedChannel.lastMessageAt, + isSameDateAs(insertedChannel.lastMessageAt), + ); + } + }); + }); + + tearDown(() async { + await database.disconnect(); + }); +} diff --git a/packages/stream_chat_persistence/test/src/dao/connection_event_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/connection_event_dao_test.dart new file mode 100644 index 00000000..2969751b --- /dev/null +++ b/packages/stream_chat_persistence/test/src/dao/connection_event_dao_test.dart @@ -0,0 +1,114 @@ +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_persistence/src/dao/connection_event_dao.dart'; +import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:test/test.dart'; + +import '../utils/date_matcher.dart'; + +void main() { + ConnectionEventDao eventDao; + MoorChatDatabase database; + + setUp(() { + database = MoorChatDatabase.testable('testUserId'); + eventDao = database.connectionEventDao; + }); + + test('connectionEvent', () async { + // Should be null initially + final event = await eventDao.connectionEvent; + expect(event, isNull); + + // Adding a new event + final newEvent = Event( + createdAt: DateTime.now(), + totalUnreadCount: 33, + unreadChannels: 3, + me: OwnUser(id: 'testUserId'), + ); + await eventDao.updateConnectionEvent(newEvent); + + // Should match the added event + final updatedEvent = await eventDao.connectionEvent; + expect(updatedEvent.me.id, newEvent.me.id); + expect(updatedEvent.totalUnreadCount, newEvent.totalUnreadCount); + expect(updatedEvent.unreadChannels, newEvent.unreadChannels); + }); + + test('lastSyncAt', () async { + // Should be null initially + final lastSyncAt = await eventDao.lastSyncAt; + expect(lastSyncAt, isNull); + + // Adding an event for testing + final event = Event( + createdAt: DateTime.now(), + totalUnreadCount: 33, + unreadChannels: 3, + me: OwnUser(id: 'testUserId'), + ); + await eventDao.updateConnectionEvent(event); + + // Updating it's last sync + final now = DateTime.now(); + await eventDao.updateLastSyncAt(now); + + // Should match the updated last sync + final updatedLastSyncAt = await eventDao.lastSyncAt; + expect(updatedLastSyncAt, isSameDateAs(now)); + }); + + test('updateConnectionEvent', () async { + // Adding and event for testing + final event = Event( + createdAt: DateTime.now(), + totalUnreadCount: 33, + unreadChannels: 3, + me: OwnUser(id: 'testUserId'), + ); + await eventDao.updateConnectionEvent(event); + + // Should match the previously added event + final fetchedEvent = await eventDao.connectionEvent; + expect(fetchedEvent.me.id, event.me.id); + expect(fetchedEvent.totalUnreadCount, event.totalUnreadCount); + expect(fetchedEvent.unreadChannels, event.unreadChannels); + + // Updating the added event + final newEvent = event.copyWith(unreadChannels: 4); + await eventDao.updateConnectionEvent(newEvent); + + // Should match the updated event + final fetchedNewEvent = await eventDao.connectionEvent; + expect(fetchedNewEvent.me.id, event.me.id); + expect(fetchedNewEvent.totalUnreadCount, event.totalUnreadCount); + expect(fetchedNewEvent.unreadChannels, newEvent.unreadChannels); + }); + + test('updateLastSyncAt', () async { + // Should be null initially + final lastSyncAt = await eventDao.lastSyncAt; + expect(lastSyncAt, isNull); + + // Adding an event just for testing + final event = Event( + createdAt: DateTime.now(), + totalUnreadCount: 33, + unreadChannels: 3, + me: OwnUser(id: 'testUserId'), + ); + await eventDao.updateConnectionEvent(event); + + // Updating it's last sync + final now = DateTime.now(); + await eventDao.updateLastSyncAt(now); + + // Should match the last sync + final updatedLastSyncAt = await eventDao.lastSyncAt; + expect(updatedLastSyncAt, isSameDateAs(now)); + }); + + tearDown(() async { + await database.disconnect(); + }); +} diff --git a/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart new file mode 100644 index 00000000..dbb8c73c --- /dev/null +++ b/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart @@ -0,0 +1,152 @@ +import 'dart:math' as math; + +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_persistence/src/dao/dao.dart'; +import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:test/test.dart'; + +import '../utils/date_matcher.dart'; + +void main() { + MemberDao memberDao; + MoorChatDatabase database; + + setUp(() { + database = MoorChatDatabase.testable('testUserId'); + memberDao = database.memberDao; + }); + + Future> _prepareTestData(String cid) async { + final users = List.generate(3, (index) => User(id: 'testUserId$index')); + final memberList = List.generate( + 3, + (index) => Member( + user: users[index], + banned: math.Random().nextBool(), + shadowBanned: math.Random().nextBool(), + createdAt: DateTime.now(), + isModerator: math.Random().nextBool(), + invited: math.Random().nextBool(), + inviteAcceptedAt: DateTime.now(), + role: 'testRole', + updatedAt: DateTime.now(), + ), + ); + await database.userDao.updateUsers(users); + await memberDao.updateMembers(cid, memberList); + return memberList; + } + + test('getMembersByCid', () async { + const cid = 'testCid'; + + // Should be empty initially + final members = await memberDao.getMembersByCid(cid); + expect(members, isEmpty); + + // Preparing test data + final memberList = await _prepareTestData(cid); + + // Should match the previous test data + final fetchedMembers = await memberDao.getMembersByCid(cid); + expect(fetchedMembers.length, memberList.length); + for (var i = 0; i < fetchedMembers.length; i++) { + final member = memberList[i]; + final fetchedMember = fetchedMembers[i]; + expect(fetchedMember.user.id, member.user.id); + expect(fetchedMember.banned, member.banned); + expect(fetchedMember.shadowBanned, member.shadowBanned); + expect(fetchedMember.createdAt, isSameDateAs(member.createdAt)); + expect(fetchedMember.isModerator, member.isModerator); + expect(fetchedMember.invited, member.invited); + expect(fetchedMember.role, member.role); + expect(fetchedMember.updatedAt, isSameDateAs(member.updatedAt)); + expect( + fetchedMember.inviteAcceptedAt, + isSameDateAs(member.inviteAcceptedAt), + ); + } + }); + + test('updateMembers', () async { + const cid = 'testCid'; + + // Preparing test data + final memberList = await _prepareTestData(cid); + + // Should match the previous test data + final fetchedMembers = await memberDao.getMembersByCid(cid); + expect(fetchedMembers.length, memberList.length); + for (var i = 0; i < fetchedMembers.length; i++) { + final member = memberList[i]; + final fetchedMember = fetchedMembers[i]; + expect(fetchedMember.user.id, member.user.id); + expect(fetchedMember.banned, member.banned); + expect(fetchedMember.shadowBanned, member.shadowBanned); + expect(fetchedMember.createdAt, isSameDateAs(member.createdAt)); + expect(fetchedMember.isModerator, member.isModerator); + expect(fetchedMember.invited, member.invited); + expect(fetchedMember.role, member.role); + expect(fetchedMember.updatedAt, isSameDateAs(member.updatedAt)); + expect( + fetchedMember.inviteAcceptedAt, + isSameDateAs(member.inviteAcceptedAt), + ); + } + + // Modifying one of the member and also adding one new + final copyMember = fetchedMembers.first.copyWith(banned: true); + final newUser = User(id: 'testUserId3'); + final newMember = Member( + user: newUser, + banned: math.Random().nextBool(), + shadowBanned: math.Random().nextBool(), + createdAt: DateTime.now(), + isModerator: math.Random().nextBool(), + invited: math.Random().nextBool(), + inviteAcceptedAt: DateTime.now(), + role: 'testRole', + updatedAt: DateTime.now(), + ); + await database.userDao.updateUsers([newUser]); + await memberDao.updateMembers(cid, [copyMember, newMember]); + + // Fetched member length should be one more than inserted members. + // copyMember `banned` modified field should be true. + // Fetched members should contain the newMember. + final newFetchedMembers = await memberDao.getMembersByCid(cid); + expect(newFetchedMembers.length, fetchedMembers.length + 1); + expect( + newFetchedMembers + .firstWhere((it) => it.user.id == copyMember.user.id) + .banned, + true, + ); + expect( + newFetchedMembers + .where((it) => it.user.id == newMember.user.id) + .isNotEmpty, + true, + ); + }); + + test('deleteMemberByCids', () async { + const cid = 'testCid'; + + // Preparing test data + final members = await _prepareTestData(cid); + final fetchedMembers = await memberDao.getMembersByCid(cid); + expect(members.length, fetchedMembers.length); + + // Deleting all the members + await memberDao.deleteMemberByCids([cid]); + + // Fetched member list should be empty + final newFetchedMembers = await memberDao.getMembersByCid(cid); + expect(newFetchedMembers, isEmpty); + }); + + tearDown(() async { + await database.disconnect(); + }); +} diff --git a/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart new file mode 100644 index 00000000..f2440ae1 --- /dev/null +++ b/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart @@ -0,0 +1,362 @@ +import 'dart:math' as math; + +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_persistence/src/dao/dao.dart'; +import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:test/test.dart'; + +void main() { + MessageDao messageDao; + MoorChatDatabase database; + + setUp(() { + database = MoorChatDatabase.testable('testUserId'); + messageDao = database.messageDao; + }); + + Future> _prepareTestData( + String cid, { + bool quoted = false, + bool threads = false, + bool mapAllThreadToFirstMessage = false, + int count = 3, + }) async { + final users = List.generate(count, (index) => User(id: 'testUserId$index')); + final messages = List.generate( + count, + (index) => Message( + id: 'testMessageId$cid$index', + type: 'testType', + user: users[index], + createdAt: DateTime.now(), + shadowed: math.Random().nextBool(), + replyCount: index, + updatedAt: DateTime.now(), + extraData: {'extra_test_field': 'extraTestData'}, + text: 'Dummy text #$index', + pinned: math.Random().nextBool(), + pinnedAt: DateTime.now(), + pinnedBy: User(id: 'testUserId$index'), + ), + ); + final quotedMessages = List.generate( + count, + (index) => Message( + id: 'testQuotedMessageId$cid$index', + type: 'testType', + user: users[index], + createdAt: DateTime.now(), + shadowed: math.Random().nextBool(), + replyCount: index, + updatedAt: DateTime.now(), + extraData: {'extra_test_field': 'extraTestData'}, + text: 'Dummy text #$index', + quotedMessageId: messages[index].id, + pinned: math.Random().nextBool(), + pinnedAt: DateTime.now(), + pinnedBy: User(id: 'testUserId$index'), + ), + ); + final threadMessages = List.generate( + count, + (index) => Message( + id: 'testThreadMessageId$cid$index', + type: 'testType', + user: users[index], + parentId: + mapAllThreadToFirstMessage ? messages[0].id : messages[index].id, + createdAt: DateTime.now(), + shadowed: math.Random().nextBool(), + replyCount: index, + updatedAt: DateTime.now(), + extraData: {'extra_test_field': 'extraTestData'}, + text: 'Dummy text #$index', + pinned: math.Random().nextBool(), + pinnedAt: DateTime.now(), + pinnedBy: User(id: 'testUserId$index'), + ), + ); + final allMessages = [ + ...messages, + if (quoted) ...quotedMessages, + if (threads) ...threadMessages + ]; + await database.userDao.updateUsers(users); + await messageDao.updateMessages(cid, allMessages); + return allMessages; + } + + test('deleteMessageByIds', () async { + const cid = 'testCid'; + + // Preparing test data + final insertedMessages = await _prepareTestData(cid); + + // Fetched message list should match the test message list length + final messages = await messageDao.getMessagesByCid(cid); + expect(messages.length, insertedMessages.length); + + // Deleting 2 messages from DB + await messageDao.deleteMessageByIds( + ['testMessageId${cid}0', 'testMessageId${cid}1'], + ); + + // New fetched messages length should 2 less than the + // previous fetched messages + final newMessages = await messageDao.getMessagesByCid(cid); + expect(newMessages.length, messages.length - 2); + }); + + group('deleteMessageByCids', () { + const cid1 = 'testCid1'; + const cid2 = 'testCid2'; + + test('should delete all the messages of first channel', () async { + // Preparing test data + final cid1InsertedMessages = await _prepareTestData(cid1); + final cid2InsertedMessages = await _prepareTestData(cid2); + + // Fetched message list should match the test message list length + final cid1Messages = await messageDao.getMessagesByCid(cid1); + final cid2Messages = await messageDao.getMessagesByCid(cid2); + expect(cid1Messages.length, cid1InsertedMessages.length); + expect(cid2Messages.length, cid2InsertedMessages.length); + + // Deleting all the messages of cid1 + await messageDao.deleteMessageByCids([cid1]); + + // Fetched messages length of only cid1 should be empty + final cid1FetchedMessages = await messageDao.getMessagesByCid(cid1); + final cid2FetchedMessages = await messageDao.getMessagesByCid(cid2); + expect(cid1FetchedMessages, isEmpty); + expect(cid2FetchedMessages, isNotEmpty); + }); + + test('should delete all the messages of both channel', () async { + // Preparing test data + final cid1InsertedMessages = await _prepareTestData(cid1); + final cid2InsertedMessages = await _prepareTestData(cid2); + + // Fetched message list should match the test message list length + final cid1Messages = await messageDao.getMessagesByCid(cid1); + final cid2Messages = await messageDao.getMessagesByCid(cid2); + expect(cid1Messages.length, cid1InsertedMessages.length); + expect(cid2Messages.length, cid2InsertedMessages.length); + + // Deleting all the messages of cid1 + await messageDao.deleteMessageByCids([cid1, cid2]); + + // Fetched messages length of both cid1 and cid2 should be empty + final cid1FetchedMessages = await messageDao.getMessagesByCid(cid1); + final cid2FetchedMessages = await messageDao.getMessagesByCid(cid2); + expect(cid1FetchedMessages, isEmpty); + expect(cid2FetchedMessages, isEmpty); + }); + }); + + test('getMessageById', () async { + const cid = 'testCid'; + const id = 'testMessageId${cid}0'; + + // Should be null initially + final message = await messageDao.getMessageById(id); + expect(message, isNull); + + // Adding test message with the cid and id + final insertedMessages = await _prepareTestData(cid, count: 1); + expect(insertedMessages.first.id, id); + + // Fetched message id should match the inserted message id + final fetchedMessage = await messageDao.getMessageById(id); + expect(fetchedMessage.id, insertedMessages.first.id); + }); + + test('getThreadMessages', () async { + const cid = 'testCid'; + + // Messages should be empty initially + final messages = await messageDao.getThreadMessages(cid); + expect(messages, isEmpty); + + // Preparing test data + final insertedMessages = await _prepareTestData(cid, threads: true); + expect(insertedMessages, isNotEmpty); + + // Should fetch all the thread messages of cid + final threadMessages = await messageDao.getThreadMessages(cid); + expect(threadMessages, isNotEmpty); + for (final message in threadMessages) { + expect(message.parentId, isNotNull); + } + }); + + test('getThreadMessagesByParentId', () async { + const cid = 'testCid'; + const parentId = 'testMessageId${cid}0'; + + // Messages should be empty initially + final messages = await messageDao.getThreadMessagesByParentId(parentId); + expect(messages, isEmpty); + + // Preparing test data + final insertedMessages = await _prepareTestData(cid, threads: true); + expect(insertedMessages, isNotEmpty); + + // Should fetch all the thread messages of parentId + final threadMessages = + await messageDao.getThreadMessagesByParentId(parentId); + expect(threadMessages.length, 1); + expect(threadMessages.first.parentId, parentId); + }); + + test('getThreadMessagesByParentId along with pagination', () async { + const cid = 'testCid'; + const parentId = 'testMessageId${cid}0'; + const options = PaginationParams( + limit: 15, + lessThan: 'testThreadMessageId${cid}25', + greaterThanOrEqual: 'testThreadMessageId${cid}5', + ); + + // Messages should be empty initially + final messages = await messageDao.getThreadMessagesByParentId( + parentId, + options: options, + ); + expect(messages, isEmpty); + + // Preparing test data + final insertedMessages = await _prepareTestData( + cid, + threads: true, + mapAllThreadToFirstMessage: true, + count: 30, + ); + expect(insertedMessages, isNotEmpty); + + // Should fetch all the thread messages of parentId and apply the pagination + final threadMessages = await messageDao.getThreadMessagesByParentId( + parentId, + options: options, + ); + expect(threadMessages.length, 15); + expect(threadMessages.first.parentId, parentId); + }); + + test('getMessagesByCid', () async { + const cid = 'testCid'; + + // Should be empty initially + final messages = await messageDao.getMessagesByCid(cid); + expect(messages, isEmpty); + + // Preparing test data + final insertedMessages = await _prepareTestData(cid); + expect(insertedMessages, isNotEmpty); + + // Fetched message should match the inserted messages + final fetchedMessages = await messageDao.getMessagesByCid(cid); + expect(fetchedMessages.length, insertedMessages.length); + for (var i = 0; i < fetchedMessages.length; i++) { + final fetchedMessage = fetchedMessages[i]; + final insertedMessage = insertedMessages[i]; + expect(fetchedMessage.id, insertedMessage.id); + } + }); + + test('getMessagesByCid along with quotedMessage', () async { + const cid = 'testCid'; + + // Should be empty initially + final messages = await messageDao.getMessagesByCid(cid); + expect(messages, isEmpty); + + // Preparing test data + final insertedMessages = await _prepareTestData(cid, quoted: true); + expect(insertedMessages, isNotEmpty); + + // Fetched message should match the inserted messages + final fetchedMessages = await messageDao.getMessagesByCid(cid); + expect(fetchedMessages.length, insertedMessages.length); + final quoted = fetchedMessages.where((it) => it.quotedMessage != null); + expect(quoted.length, insertedMessages.length / 2); + }); + + test('getMessagesByCid along with pagination', () async { + const cid = 'testCid'; + const limit = 15; + const lessThan = 'testMessageId${cid}25'; + const greaterThanOrEqual = 'testMessageId${cid}5'; + const pagination = PaginationParams( + limit: limit, + lessThan: lessThan, + greaterThanOrEqual: greaterThanOrEqual, + ); + + // Should be empty initially + final messages = await messageDao.getMessagesByCid( + cid, + messagePagination: pagination, + ); + expect(messages, isEmpty); + + // Preparing test data + final insertedMessages = await _prepareTestData(cid, count: 30); + expect(insertedMessages, isNotEmpty); + + // Fetched message should match the inserted messages + final fetchedMessages = await messageDao.getMessagesByCid( + cid, + messagePagination: pagination, + ); + expect(fetchedMessages.length, limit); + expect(fetchedMessages.first.id, greaterThanOrEqual); + expect(fetchedMessages.last.id != lessThan, true); + }); + + test('updateMessages', () async { + const cid = 'testCid'; + + // Preparing test data + final insertedMessages = await _prepareTestData(cid); + expect(insertedMessages, isNotEmpty); + + // Modifying one of the message and also adding one new + final copyMessage = insertedMessages.first.copyWith(showInChannel: false); + final newMessage = Message( + id: 'testMessageId${cid}4', + type: 'testType', + user: User(id: 'testUserId4'), + createdAt: DateTime.now(), + shadowed: math.Random().nextBool(), + showInChannel: math.Random().nextBool(), + replyCount: 4, + updatedAt: DateTime.now(), + extraData: {'extra_test_field': 'extraTestData'}, + text: 'Dummy text #4', + pinned: math.Random().nextBool(), + pinnedAt: DateTime.now(), + pinnedBy: User(id: 'testUserId4'), + ); + + await messageDao.updateMessages(cid, [copyMessage, newMessage]); + + // Fetched messages length should be one more than inserted message. + // copyMessage `showInChannel` modified field should be false. + // Fetched messages should contain the newMessage. + final fetchedMessages = await messageDao.getMessagesByCid(cid); + expect(fetchedMessages.length, insertedMessages.length + 1); + expect( + fetchedMessages.firstWhere((it) => it.id == copyMessage.id).showInChannel, + false, + ); + expect( + fetchedMessages.map((it) => it.id).contains(newMessage.id), + true, + ); + }); + + tearDown(() async { + await database.disconnect(); + }); +} diff --git a/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart new file mode 100644 index 00000000..de7c8976 --- /dev/null +++ b/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart @@ -0,0 +1,363 @@ +import 'dart:math' as math; + +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_persistence/src/dao/dao.dart'; +import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:test/test.dart'; + +void main() { + PinnedMessageDao pinnedMessageDao; + MoorChatDatabase database; + + setUp(() { + database = MoorChatDatabase.testable('testUserId'); + pinnedMessageDao = database.pinnedMessageDao; + }); + + Future> _prepareTestData( + String cid, { + bool quoted = false, + bool threads = false, + bool mapAllThreadToFirstMessage = false, + int count = 3, + }) async { + final users = List.generate(count, (index) => User(id: 'testUserId$index')); + final messages = List.generate( + count, + (index) => Message( + id: 'testMessageId$cid$index', + type: 'testType', + user: users[index], + createdAt: DateTime.now(), + shadowed: math.Random().nextBool(), + replyCount: index, + updatedAt: DateTime.now(), + extraData: {'extra_test_field': 'extraTestData'}, + text: 'Dummy text #$index', + pinned: math.Random().nextBool(), + pinnedAt: DateTime.now(), + pinnedBy: User(id: 'testUserId$index'), + ), + ); + final quotedMessages = List.generate( + count, + (index) => Message( + id: 'testQuotedMessageId$cid$index', + type: 'testType', + user: users[index], + createdAt: DateTime.now(), + shadowed: math.Random().nextBool(), + replyCount: index, + updatedAt: DateTime.now(), + extraData: {'extra_test_field': 'extraTestData'}, + text: 'Dummy text #$index', + quotedMessageId: messages[index].id, + pinned: math.Random().nextBool(), + pinnedAt: DateTime.now(), + pinnedBy: User(id: 'testUserId$index'), + ), + ); + final threadMessages = List.generate( + count, + (index) => Message( + id: 'testThreadMessageId$cid$index', + type: 'testType', + user: users[index], + parentId: + mapAllThreadToFirstMessage ? messages[0].id : messages[index].id, + createdAt: DateTime.now(), + shadowed: math.Random().nextBool(), + replyCount: index, + updatedAt: DateTime.now(), + extraData: {'extra_test_field': 'extraTestData'}, + text: 'Dummy text #$index', + pinned: math.Random().nextBool(), + pinnedAt: DateTime.now(), + pinnedBy: User(id: 'testUserId$index'), + ), + ); + final allMessages = [ + ...messages, + if (quoted) ...quotedMessages, + if (threads) ...threadMessages + ]; + await database.userDao.updateUsers(users); + await pinnedMessageDao.updateMessages(cid, allMessages); + return allMessages; + } + + test('deleteMessageByIds', () async { + const cid = 'testCid'; + + // Preparing test data + final insertedMessages = await _prepareTestData(cid); + + // Fetched message list should match the test message list length + final messages = await pinnedMessageDao.getMessagesByCid(cid); + expect(messages.length, insertedMessages.length); + + // Deleting 2 messages from DB + await pinnedMessageDao.deleteMessageByIds( + ['testMessageId${cid}0', 'testMessageId${cid}1'], + ); + + // New fetched messages length should 2 less than the + // previous fetched messages + final newMessages = await pinnedMessageDao.getMessagesByCid(cid); + expect(newMessages.length, messages.length - 2); + }); + + group('deleteMessageByCids', () { + const cid1 = 'testCid1'; + const cid2 = 'testCid2'; + + test('should delete all the messages of first channel', () async { + // Preparing test data + final cid1InsertedMessages = await _prepareTestData(cid1); + final cid2InsertedMessages = await _prepareTestData(cid2); + + // Fetched message list should match the test message list length + final cid1Messages = await pinnedMessageDao.getMessagesByCid(cid1); + final cid2Messages = await pinnedMessageDao.getMessagesByCid(cid2); + expect(cid1Messages.length, cid1InsertedMessages.length); + expect(cid2Messages.length, cid2InsertedMessages.length); + + // Deleting all the messages of cid1 + await pinnedMessageDao.deleteMessageByCids([cid1]); + + // Fetched messages length of only cid1 should be empty + final cid1FetchedMessages = await pinnedMessageDao.getMessagesByCid(cid1); + final cid2FetchedMessages = await pinnedMessageDao.getMessagesByCid(cid2); + expect(cid1FetchedMessages, isEmpty); + expect(cid2FetchedMessages, isNotEmpty); + }); + + test('should delete all the messages of both channel', () async { + // Preparing test data + final cid1InsertedMessages = await _prepareTestData(cid1); + final cid2InsertedMessages = await _prepareTestData(cid2); + + // Fetched message list should match the test message list length + final cid1Messages = await pinnedMessageDao.getMessagesByCid(cid1); + final cid2Messages = await pinnedMessageDao.getMessagesByCid(cid2); + expect(cid1Messages.length, cid1InsertedMessages.length); + expect(cid2Messages.length, cid2InsertedMessages.length); + + // Deleting all the messages of cid1 + await pinnedMessageDao.deleteMessageByCids([cid1, cid2]); + + // Fetched messages length of both cid1 and cid2 should be empty + final cid1FetchedMessages = await pinnedMessageDao.getMessagesByCid(cid1); + final cid2FetchedMessages = await pinnedMessageDao.getMessagesByCid(cid2); + expect(cid1FetchedMessages, isEmpty); + expect(cid2FetchedMessages, isEmpty); + }); + }); + + test('getMessageById', () async { + const cid = 'testCid'; + const id = 'testMessageId${cid}0'; + + // Should be null initially + final message = await pinnedMessageDao.getMessageById(id); + expect(message, isNull); + + // Adding test message with the cid and id + final insertedMessages = await _prepareTestData(cid, count: 1); + expect(insertedMessages.first.id, id); + + // Fetched message id should match the inserted message id + final fetchedMessage = await pinnedMessageDao.getMessageById(id); + expect(fetchedMessage.id, insertedMessages.first.id); + }); + + test('getThreadMessages', () async { + const cid = 'testCid'; + + // Messages should be empty initially + final messages = await pinnedMessageDao.getThreadMessages(cid); + expect(messages, isEmpty); + + // Preparing test data + final insertedMessages = await _prepareTestData(cid, threads: true); + expect(insertedMessages, isNotEmpty); + + // Should fetch all the thread messages of cid + final threadMessages = await pinnedMessageDao.getThreadMessages(cid); + expect(threadMessages, isNotEmpty); + for (final message in threadMessages) { + expect(message.parentId, isNotNull); + } + }); + + test('getThreadMessagesByParentId', () async { + const cid = 'testCid'; + const parentId = 'testMessageId${cid}0'; + + // Messages should be empty initially + final messages = + await pinnedMessageDao.getThreadMessagesByParentId(parentId); + expect(messages, isEmpty); + + // Preparing test data + final insertedMessages = await _prepareTestData(cid, threads: true); + expect(insertedMessages, isNotEmpty); + + // Should fetch all the thread messages of parentId + final threadMessages = + await pinnedMessageDao.getThreadMessagesByParentId(parentId); + expect(threadMessages.length, 1); + expect(threadMessages.first.parentId, parentId); + }); + + test('getThreadMessagesByParentId along with pagination', () async { + const cid = 'testCid'; + const parentId = 'testMessageId${cid}0'; + const options = PaginationParams( + limit: 15, + lessThan: 'testThreadMessageId${cid}25', + greaterThanOrEqual: 'testThreadMessageId${cid}5', + ); + + // Messages should be empty initially + final messages = await pinnedMessageDao.getThreadMessagesByParentId( + parentId, + options: options, + ); + expect(messages, isEmpty); + + // Preparing test data + final insertedMessages = await _prepareTestData( + cid, + threads: true, + mapAllThreadToFirstMessage: true, + count: 30, + ); + expect(insertedMessages, isNotEmpty); + + // Should fetch all the thread messages of parentId and apply the pagination + final threadMessages = await pinnedMessageDao.getThreadMessagesByParentId( + parentId, + options: options, + ); + expect(threadMessages.length, 15); + expect(threadMessages.first.parentId, parentId); + }); + + test('getMessagesByCid', () async { + const cid = 'testCid'; + + // Should be empty initially + final messages = await pinnedMessageDao.getMessagesByCid(cid); + expect(messages, isEmpty); + + // Preparing test data + final insertedMessages = await _prepareTestData(cid); + expect(insertedMessages, isNotEmpty); + + // Fetched message should match the inserted messages + final fetchedMessages = await pinnedMessageDao.getMessagesByCid(cid); + expect(fetchedMessages.length, insertedMessages.length); + for (var i = 0; i < fetchedMessages.length; i++) { + final fetchedMessage = fetchedMessages[i]; + final insertedMessage = insertedMessages[i]; + expect(fetchedMessage.id, insertedMessage.id); + } + }); + + test('getMessagesByCid along with quotedMessage', () async { + const cid = 'testCid'; + + // Should be empty initially + final messages = await pinnedMessageDao.getMessagesByCid(cid); + expect(messages, isEmpty); + + // Preparing test data + final insertedMessages = await _prepareTestData(cid, quoted: true); + expect(insertedMessages, isNotEmpty); + + // Fetched message should match the inserted messages + final fetchedMessages = await pinnedMessageDao.getMessagesByCid(cid); + expect(fetchedMessages.length, insertedMessages.length); + final quoted = fetchedMessages.where((it) => it.quotedMessage != null); + expect(quoted.length, insertedMessages.length / 2); + }); + + test('getMessagesByCid along with pagination', () async { + const cid = 'testCid'; + const limit = 15; + const lessThan = 'testMessageId${cid}25'; + const greaterThanOrEqual = 'testMessageId${cid}5'; + const pagination = PaginationParams( + limit: limit, + lessThan: lessThan, + greaterThanOrEqual: greaterThanOrEqual, + ); + + // Should be empty initially + final messages = await pinnedMessageDao.getMessagesByCid( + cid, + messagePagination: pagination, + ); + expect(messages, isEmpty); + + // Preparing test data + final insertedMessages = await _prepareTestData(cid, count: 30); + expect(insertedMessages, isNotEmpty); + + // Fetched message should match the inserted messages + final fetchedMessages = await pinnedMessageDao.getMessagesByCid( + cid, + messagePagination: pagination, + ); + expect(fetchedMessages.length, limit); + expect(fetchedMessages.first.id, greaterThanOrEqual); + expect(fetchedMessages.last.id != lessThan, true); + }); + + test('updateMessages', () async { + const cid = 'testCid'; + + // Preparing test data + final insertedMessages = await _prepareTestData(cid); + expect(insertedMessages, isNotEmpty); + + // Modifying one of the message and also adding one new + final copyMessage = insertedMessages.first.copyWith(showInChannel: false); + final newMessage = Message( + id: 'testMessageId${cid}4', + type: 'testType', + user: User(id: 'testUserId4'), + createdAt: DateTime.now(), + shadowed: math.Random().nextBool(), + showInChannel: math.Random().nextBool(), + replyCount: 4, + updatedAt: DateTime.now(), + extraData: {'extra_test_field': 'extraTestData'}, + text: 'Dummy text #4', + pinned: math.Random().nextBool(), + pinnedAt: DateTime.now(), + pinnedBy: User(id: 'testUserId4'), + ); + + await pinnedMessageDao.updateMessages(cid, [copyMessage, newMessage]); + + // Fetched messages length should be one more than inserted message. + // copyMessage `showInChannel` modified field should be false. + // Fetched messages should contain the newMessage. + final fetchedMessages = await pinnedMessageDao.getMessagesByCid(cid); + expect(fetchedMessages.length, insertedMessages.length + 1); + expect( + fetchedMessages.firstWhere((it) => it.id == copyMessage.id).showInChannel, + false, + ); + expect( + fetchedMessages.map((it) => it.id).contains(newMessage.id), + true, + ); + }); + + tearDown(() async { + await database.disconnect(); + }); +} diff --git a/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart new file mode 100644 index 00000000..94f8ebdd --- /dev/null +++ b/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart @@ -0,0 +1,188 @@ +import 'dart:math' as math; + +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_persistence/src/dao/reaction_dao.dart'; +import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:test/test.dart'; + +void main() { + ReactionDao reactionDao; + MoorChatDatabase database; + + setUp(() { + database = MoorChatDatabase.testable('testUserId'); + reactionDao = database.reactionDao; + }); + + Future> _prepareReactionData( + String messageId, { + String userId, + int count = 3, + }) async { + final users = List.generate(count, (index) => User(id: 'testUserId$index')); + final message = Message( + id: messageId, + type: 'testType', + user: users.first, + createdAt: DateTime.now(), + shadowed: math.Random().nextBool(), + showInChannel: math.Random().nextBool(), + replyCount: 3, + updatedAt: DateTime.now(), + extraData: {'extra_test_field': 'extraTestData'}, + text: 'Dummy text', + pinned: math.Random().nextBool(), + pinnedAt: DateTime.now(), + pinnedBy: users.first, + ); + final reactions = List.generate( + count, + (index) => Reaction( + type: 'testType$index', + createdAt: DateTime.now(), + userId: userId ?? users[index].id, + messageId: message.id, + score: count + 3, + extraData: {'extra_test_field': 'extraTestData'}, + ), + ); + + await database.userDao.updateUsers(users); + await database.messageDao.updateMessages('testCid', [message]); + await reactionDao.updateReactions(reactions); + + return reactions; + } + + test('getReactions', () async { + const messageId = 'testMessageId'; + + // Should be empty initially + final reactions = await reactionDao.getReactions(messageId); + expect(reactions, isEmpty); + + // Adding sample reactions + final insertedReactions = await _prepareReactionData(messageId); + expect(insertedReactions, isNotEmpty); + + // Fetched reaction length should match inserted reactions length. + // Every reaction messageId should match the provided messageId. + final fetchedReactions = await reactionDao.getReactions(messageId); + expect(fetchedReactions.length, insertedReactions.length); + expect(fetchedReactions.every((it) => it.messageId == messageId), true); + }); + + test('getReactionsByUserId', () async { + const messageId = 'testMessageId'; + const userId = 'testUserId'; + + // Should be empty initially + final reactions = await reactionDao.getReactionsByUserId(messageId, userId); + expect(reactions, isEmpty); + + // Adding sample reactions + final insertedReactions = + await _prepareReactionData(messageId, userId: userId); + expect(insertedReactions, isNotEmpty); + + // Fetched reaction length should match inserted reactions length. + // Every reaction messageId should match the provided messageId. + // Every reaction userId should match the provided userId. + final fetchedReactions = + await reactionDao.getReactionsByUserId(messageId, userId); + expect(fetchedReactions.length, insertedReactions.length); + expect(fetchedReactions.every((it) => it.messageId == messageId), true); + expect(fetchedReactions.every((it) => it.userId == userId), true); + }); + + test('updateReactions', () async { + const messageId = 'testMessageId'; + + // Preparing test data + final reactions = await _prepareReactionData(messageId); + + // Modifying one of the reaction and also adding one new + final copyReaction = reactions.first.copyWith(score: 33); + final newReaction = Reaction( + type: 'testType3', + createdAt: DateTime.now(), + userId: 'testUserId3', + messageId: messageId, + score: 30, + extraData: {'extra_test_field': 'extraTestData'}, + ); + + await reactionDao.updateReactions([copyReaction, newReaction]); + + // Fetched reaction length should be one more than inserted reactions. + // copyReaction `score` modified field should be 33. + // Fetched reactions should contain the newReaction. + final fetchedReactions = await reactionDao.getReactions(messageId); + expect(fetchedReactions.length, reactions.length + 1); + expect( + fetchedReactions + .firstWhere((it) => + it.userId == copyReaction.userId && it.type == copyReaction.type) + .score, + 33, + ); + expect( + fetchedReactions + .where((it) => + it.userId == newReaction.userId && it.type == newReaction.type) + .isNotEmpty, + true, + ); + }); + + group('deleteReactionsByMessageIds', () { + const messageId1 = 'testMessageId1'; + const messageId2 = 'testMessageId2'; + test('should delete all the reactions of first message', () async { + // Preparing test data + final insertedReactions1 = await _prepareReactionData(messageId1); + final insertedReactions2 = await _prepareReactionData(messageId2); + + // Fetched reaction list length should match + // the inserted reactions list length + final reactions1 = await reactionDao.getReactions(messageId1); + final reactions2 = await reactionDao.getReactions(messageId2); + expect(reactions1.length, insertedReactions1.length); + expect(reactions2.length, insertedReactions2.length); + + // Deleting all the reactions of messageId1 + await reactionDao.deleteReactionsByMessageIds([messageId1]); + + // Fetched reactions length of only messageId1 should be empty + final fetchedReactions1 = await reactionDao.getReactions(messageId1); + final fetchedReactions2 = await reactionDao.getReactions(messageId2); + expect(fetchedReactions1, isEmpty); + expect(fetchedReactions2, isNotEmpty); + }); + test('should delete all the messages of both message', () async { + // Preparing test data + final insertedReactions1 = await _prepareReactionData(messageId1); + final insertedReactions2 = await _prepareReactionData(messageId2); + + // Fetched reaction list length should match + // the inserted reactions list length + final reactions1 = await reactionDao.getReactions(messageId1); + final reactions2 = await reactionDao.getReactions(messageId2); + expect(reactions1.length, insertedReactions1.length); + expect(reactions2.length, insertedReactions2.length); + + // Deleting all the reactions of messageId1 and messageId2 + await reactionDao.deleteReactionsByMessageIds([messageId1, messageId2]); + + // Fetched reactions length of both messages should be empty + final fetchedReactions1 = await reactionDao.getReactions(messageId1); + final fetchedReactions2 = await reactionDao.getReactions(messageId2); + expect(fetchedReactions1, isEmpty); + expect(fetchedReactions2, isEmpty); + }); + }); + + tearDown(() async { + await database.disconnect(); + }); +} diff --git a/packages/stream_chat_persistence/test/src/dao/read_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/read_dao_test.dart new file mode 100644 index 00000000..52207f68 --- /dev/null +++ b/packages/stream_chat_persistence/test/src/dao/read_dao_test.dart @@ -0,0 +1,97 @@ +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_persistence/src/dao/dao.dart'; +import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:test/test.dart'; + +import '../utils/date_matcher.dart'; + +void main() { + ReadDao readDao; + MoorChatDatabase database; + + setUp(() { + database = MoorChatDatabase.testable('testUserId'); + readDao = database.readDao; + }); + + Future> _prepareReadData(String cid, {int count = 3}) async { + final users = List.generate(count, (index) => User(id: 'testUserId$index')); + final reads = List.generate( + count, + (index) => Read( + lastRead: DateTime.now(), + user: users[index], + unreadMessages: index + 10, + ), + ); + + await database.userDao.updateUsers(users); + await readDao.updateReads(cid, reads); + return reads; + } + + test('getReadsByCid', () async { + const cid = 'testCid'; + + // Should be empty initially + final reads = await readDao.getReadsByCid(cid); + expect(reads, isEmpty); + + // Preparing test data + final insertedReads = await _prepareReadData(cid); + expect(insertedReads, isNotEmpty); + + // Fetched reads should be equal to inserted reads + final fetchedReads = await readDao.getReadsByCid(cid); + expect(fetchedReads.length, insertedReads.length); + for (var i = 0; i < fetchedReads.length; i++) { + final fetchedRead = fetchedReads[i]; + final insertedRead = insertedReads[i]; + expect(fetchedRead.user.id, insertedRead.user.id); + expect(fetchedRead.lastRead, isSameDateAs(insertedRead.lastRead)); + expect(fetchedRead.unreadMessages, insertedRead.unreadMessages); + } + }); + + test('updateReads', () async { + const cid = 'testCid'; + + // Preparing test data + final insertedReads = await _prepareReadData(cid); + + // Modifying one of the read and also adding one new + final copyRead = insertedReads.first.copyWith(unreadMessages: 33); + final newUser = User(id: 'testUserId3'); + final newRead = Read( + lastRead: DateTime.now(), + user: newUser, + unreadMessages: 30, + ); + await database.userDao.updateUsers([newUser]); + await readDao.updateReads(cid, [copyRead, newRead]); + + // Fetched reads length should be one more than inserted reads. + // copyRead `unreadMessages` modified field should be 33. + // Fetched reads should contain the newRead. + final fetchedReads = await readDao.getReadsByCid(cid); + expect(fetchedReads.length, insertedReads.length + 1); + expect( + fetchedReads + .firstWhere((it) => it.user.id == copyRead.user.id) + .unreadMessages, + 33, + ); + expect( + fetchedReads + .where((it) => + it.user.id == newRead.user.id && + it.unreadMessages == newRead.unreadMessages) + .isNotEmpty, + true, + ); + }); + + tearDown(() async { + await database.disconnect(); + }); +} diff --git a/packages/stream_chat_persistence/test/src/dao/user_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/user_dao_test.dart new file mode 100644 index 00000000..8c5898a0 --- /dev/null +++ b/packages/stream_chat_persistence/test/src/dao/user_dao_test.dart @@ -0,0 +1,77 @@ +import 'dart:math' as math; + +import 'package:stream_chat_persistence/src/dao/dao.dart'; +import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:test/test.dart'; +import 'package:stream_chat/stream_chat.dart'; + +void main() { + UserDao userDao; + MoorChatDatabase database; + + setUp(() { + database = MoorChatDatabase.testable('testUserId'); + userDao = database.userDao; + }); + + Future> _prepareUserData({int count = 3}) async { + final users = List.generate( + count, + (index) => User( + id: 'testUserId$index', + role: 'testRole', + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + lastActive: DateTime.now(), + online: math.Random().nextBool(), + banned: math.Random().nextBool(), + ), + ); + await userDao.updateUsers(users); + return users; + } + + test('updateUsers', () async { + // Preparing test data + final insertedUsers = await _prepareUserData(); + + // Modifying one of the user and also adding one new + final copyUser = insertedUsers.first.copyWith(online: false); + final newUser = User( + id: 'testUserId3', + role: 'testRole', + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + lastActive: DateTime.now(), + online: math.Random().nextBool(), + banned: math.Random().nextBool(), + ); + await userDao.updateUsers([copyUser, newUser]); + + // Fetched users length should be one more than inserted users. + // copyUser `online` modified field should be `false`. + // Fetched users should contain the newUser. + final fetchedUsers = await userDao.getUsers(); + expect(fetchedUsers.length, insertedUsers.length + 1); + expect(fetchedUsers.firstWhere((it) => it.id == copyUser.id).online, false); + expect(fetchedUsers.contains(newUser), true); + }); + + test('getUsers', () async { + // Should be empty initially + final users = await userDao.getUsers(); + expect(users, isEmpty); + + // Preparing test data + final insertedUsers = await _prepareUserData(); + expect(insertedUsers, isNotEmpty); + + // Fetched user list should match inserted user list length + final fetchedUsers = await userDao.getUsers(); + expect(fetchedUsers.length, insertedUsers.length); + }); + + tearDown(() async { + await database.disconnect(); + }); +} diff --git a/packages/stream_chat_persistence/test/src/db/moor_chat_database_test.dart b/packages/stream_chat_persistence/test/src/db/moor_chat_database_test.dart new file mode 100644 index 00000000..2ec4c76b --- /dev/null +++ b/packages/stream_chat_persistence/test/src/db/moor_chat_database_test.dart @@ -0,0 +1,43 @@ +import 'package:test/test.dart'; +import 'package:moor/ffi.dart'; +import 'package:moor/isolate.dart'; +import 'package:moor/moor.dart' hide isNotNull; +import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; + +DatabaseConnection _backgroundConnection() => + DatabaseConnection.fromExecutor(VmDatabase.memory()); + +void main() { + test( + 'default constructor should create a new instance of MoorChatDatabase', + () async { + const userId = 'testUserId'; + final executor = VmDatabase.memory(); + final database = MoorChatDatabase(userId, executor); + expect(database, isNotNull); + expect(database.userId, userId); + + addTearDown(() async { + await database.disconnect(); + }); + }, + ); + + test( + 'connect constructor should create a new instance of MoorChatDatabase', + () async { + const userId = 'testUserId'; + final isolate = await MoorIsolate.spawn(_backgroundConnection); + final connection = DatabaseConnection.delayed(isolate.connect()); + + final database = MoorChatDatabase.connect(userId, connection); + expect(database, isNotNull); + expect(database.userId, userId); + + addTearDown(() async { + await database.disconnect(); + await isolate.shutdownAll(); + }); + }, + ); +} diff --git a/packages/stream_chat_persistence/test/src/mapper/channel_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/channel_mapper_test.dart new file mode 100644 index 00000000..dc02ee4b --- /dev/null +++ b/packages/stream_chat_persistence/test/src/mapper/channel_mapper_test.dart @@ -0,0 +1,111 @@ +import 'dart:math' as math; + +import 'package:test/test.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/mapper/channel_mapper.dart'; +import '../utils/date_matcher.dart'; + +void main() { + group('ChannelEntity', () { + final user = User(id: 'testUserId'); + final entity = ChannelEntity( + id: 'testId', + type: 'testType', + cid: 'testCid', + config: {'max_message_length': 33}, + frozen: math.Random().nextBool(), + lastMessageAt: DateTime.now(), + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + deletedAt: DateTime.now(), + memberCount: 33, + createdById: user.id, + extraData: {'test_extra_data': 'testData'}, + ); + + test('toChannelModel should map entity into ChannelModel', () { + final channelModel = entity.toChannelModel(createdBy: user); + expect(channelModel, isA()); + expect(channelModel.id, entity.id); + expect(channelModel.config.toJson()['max_message_length'], 33); + expect(channelModel.frozen, entity.frozen); + expect(channelModel.createdAt, isSameDateAs(entity.createdAt)); + expect(channelModel.updatedAt, isSameDateAs(entity.updatedAt)); + expect(channelModel.memberCount, entity.memberCount); + expect(channelModel.cid, entity.cid); + expect(channelModel.lastMessageAt, isSameDateAs(entity.lastMessageAt)); + expect(channelModel.deletedAt, isSameDateAs(entity.deletedAt)); + expect(channelModel.extraData, entity.extraData); + expect(channelModel.createdBy.id, entity.createdById); + }); + + test('toChannelState should map entity into ChannelState ', () { + final members = List.generate(3, (index) => Member()); + final reads = List.generate(3, (index) => Read()); + final messages = List.generate(3, (index) => Message()); + + final channelState = entity.toChannelState( + createdBy: user, + members: members, + reads: reads, + messages: messages, + pinnedMessages: messages, + ); + + expect(channelState, isA()); + expect(channelState.members.length, members.length); + expect(channelState.read.length, reads.length); + expect(channelState.messages.length, messages.length); + expect(channelState.pinnedMessages.length, messages.length); + + final channelModel = channelState.channel; + expect(channelModel.id, entity.id); + expect(channelModel.config.toJson()['max_message_length'], 33); + expect(channelModel.frozen, entity.frozen); + expect(channelModel.createdAt, isSameDateAs(entity.createdAt)); + expect(channelModel.updatedAt, isSameDateAs(entity.updatedAt)); + expect(channelModel.memberCount, entity.memberCount); + expect(channelModel.cid, entity.cid); + expect(channelModel.lastMessageAt, isSameDateAs(entity.lastMessageAt)); + expect(channelModel.deletedAt, isSameDateAs(entity.deletedAt)); + expect(channelModel.extraData, entity.extraData); + expect(channelModel.createdBy.id, entity.createdById); + }); + }); + + test('toEntity should map model into ChannelEntity', () { + final createdBy = User(id: 'testUserId'); + final model = ChannelModel( + id: 'testId', + type: 'testType', + cid: 'testCid', + config: ChannelConfig(maxMessageLength: 33), + frozen: math.Random().nextBool(), + lastMessageAt: DateTime.now(), + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + deletedAt: DateTime.now(), + memberCount: 33, + createdBy: createdBy, + extraData: {'test_extra_data': 'testData'}, + ); + + final channelEntity = model.toEntity(); + expect(channelEntity, isA()); + expect(channelEntity.id, model.id); + expect( + channelEntity.config['max_message_length'], + model.config.maxMessageLength, + ); + expect(channelEntity.frozen, model.frozen); + expect(channelEntity.createdAt, isSameDateAs(model.createdAt)); + expect(channelEntity.updatedAt, isSameDateAs(model.updatedAt)); + expect(channelEntity.memberCount, model.memberCount); + expect(channelEntity.cid, model.cid); + expect(channelEntity.lastMessageAt, isSameDateAs(model.lastMessageAt)); + expect(channelEntity.deletedAt, isSameDateAs(model.deletedAt)); + expect(channelEntity.extraData, model.extraData); + expect(channelEntity.createdById, model.createdBy.id); + }); +} diff --git a/packages/stream_chat_persistence/test/src/mapper/event_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/event_mapper_test.dart new file mode 100644 index 00000000..240ce1a7 --- /dev/null +++ b/packages/stream_chat_persistence/test/src/mapper/event_mapper_test.dart @@ -0,0 +1,23 @@ +import 'package:test/test.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/mapper/event_mapper.dart'; + +void main() { + test('toEvent should map entity into Event', () { + final ownUser = OwnUser(id: 'testUserId'); + final entity = ConnectionEventEntity( + id: 3, + ownUser: ownUser.toJson(), + totalUnreadCount: 33, + unreadChannels: 33, + lastSyncAt: DateTime.now(), + lastEventAt: DateTime.now(), + ); + final event = entity.toEvent(); + expect(event, isA()); + expect(event.me.id, ownUser.id); + expect(event.totalUnreadCount, entity.totalUnreadCount); + expect(event.unreadChannels, entity.unreadChannels); + }); +} diff --git a/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart new file mode 100644 index 00000000..0f7d4f11 --- /dev/null +++ b/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart @@ -0,0 +1,69 @@ +import 'dart:math' as math; + +import 'package:test/test.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/mapper/member_mapper.dart'; + +import '../utils/date_matcher.dart'; + +void main() { + test('toMember should map entity into Member', () { + final user = User(id: 'testUserId'); + final entity = MemberEntity( + userId: user.id, + channelCid: 'testCid', + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + role: 'testRole', + inviteAcceptedAt: DateTime.now(), + inviteRejectedAt: DateTime.now(), + invited: math.Random().nextBool(), + banned: math.Random().nextBool(), + shadowBanned: math.Random().nextBool(), + isModerator: math.Random().nextBool(), + ); + final member = entity.toMember(user: user); + expect(member, isA()); + expect(member.user.id, entity.userId); + expect(member.createdAt, isSameDateAs(entity.createdAt)); + expect(member.updatedAt, isSameDateAs(entity.updatedAt)); + expect(member.role, entity.role); + expect(member.inviteAcceptedAt, isSameDateAs(entity.inviteAcceptedAt)); + expect(member.inviteRejectedAt, isSameDateAs(entity.inviteRejectedAt)); + expect(member.invited, entity.invited); + expect(member.banned, entity.banned); + expect(member.shadowBanned, entity.shadowBanned); + expect(member.isModerator, entity.isModerator); + }); + + test('toEntity show map member into MemberEntity', () { + const cid = 'testCid'; + final user = User(id: 'testUserId'); + final member = Member( + user: user, + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + role: 'testRole', + inviteAcceptedAt: DateTime.now(), + inviteRejectedAt: DateTime.now(), + invited: math.Random().nextBool(), + banned: math.Random().nextBool(), + shadowBanned: math.Random().nextBool(), + isModerator: math.Random().nextBool(), + ); + final entity = member.toEntity(cid: cid); + expect(entity, isA()); + expect(entity.channelCid, cid); + expect(entity.userId, member.user.id); + expect(entity.createdAt, isSameDateAs(member.createdAt)); + expect(entity.updatedAt, isSameDateAs(member.updatedAt)); + expect(entity.role, member.role); + expect(entity.inviteAcceptedAt, isSameDateAs(member.inviteAcceptedAt)); + expect(entity.inviteRejectedAt, isSameDateAs(member.inviteRejectedAt)); + expect(entity.invited, member.invited); + expect(entity.banned, member.banned); + expect(entity.shadowBanned, member.shadowBanned); + expect(entity.isModerator, member.isModerator); + }); +} diff --git a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart new file mode 100644 index 00000000..21a05da8 --- /dev/null +++ b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart @@ -0,0 +1,184 @@ +import 'dart:convert'; +import 'dart:math' as math; + +import 'package:test/test.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/mapper/message_mapper.dart'; + +import '../utils/date_matcher.dart'; + +void main() { + test('toMessage should map the entity into Message', () { + final user = User(id: 'testUserId'); + final quotedMessage = Message(id: 'testQuotedMessageId'); + final reactions = List.generate( + 3, + (index) => Reaction( + messageId: 'testMessageId', + createdAt: DateTime.now(), + type: 'testType$index', + user: user, + score: math.Random().nextInt(50), + ), + ); + final attachments = List.generate( + 3, + (index) => Attachment( + id: 'testAttachmentId', + type: 'testAttachmentType', + assetUrl: 'testAssetUrl', + ), + ); + final entity = MessageEntity( + id: 'testMessageId', + attachments: attachments?.map((it) => jsonEncode(it.toData()))?.toList(), + channelCid: 'testCid', + type: 'testType', + parentId: 'testParentId', + quotedMessageId: quotedMessage.id, + command: 'testCommand', + createdAt: DateTime.now(), + shadowed: math.Random().nextBool(), + showInChannel: math.Random().nextBool(), + replyCount: 33, + reactionScores: {for (final r in reactions) r.type: r.score}, + reactionCounts: reactions.fold( + {}, + (prev, curr) => + prev..update(curr.type, (value) => value + 1, ifAbsent: () => 1), + ), + status: MessageSendingStatus.sent, + updatedAt: DateTime.now(), + extraData: {'extra_test_data': 'extraData'}, + userId: user.id, + deletedAt: DateTime.now(), + messageText: 'dummy text', + pinned: true, + pinExpires: DateTime.now().toUtc(), + pinnedAt: DateTime.now(), + pinnedByUserId: user.id, + ); + final message = entity.toMessage( + user: user, + pinnedBy: user, + latestReactions: reactions, + ownReactions: reactions, + quotedMessage: quotedMessage, + ); + + expect(message, isA()); + expect(message.id, entity.id); + expect(message.type, entity.type); + expect(message.parentId, entity.parentId); + expect(message.quotedMessageId, entity.quotedMessageId); + expect(message.command, entity.command); + expect(message.createdAt, isSameDateAs(entity.createdAt)); + expect(message.shadowed, entity.shadowed); + expect(message.showInChannel, entity.showInChannel); + expect(message.replyCount, entity.replyCount); + expect(message.reactionScores, entity.reactionScores); + expect(message.reactionCounts, entity.reactionCounts); + expect(message.status, entity.status); + expect(message.updatedAt, isSameDateAs(entity.updatedAt)); + expect(message.extraData, entity.extraData); + expect(message.user.id, entity.userId); + expect(message.deletedAt, isSameDateAs(entity.deletedAt)); + expect(message.text, entity.messageText); + expect(message.pinned, entity.pinned); + expect(message.pinExpires, isSameDateAs(entity.pinExpires)); + expect(message.pinnedAt, isSameDateAs(entity.pinnedAt)); + expect(message.pinnedBy.id, entity.pinnedByUserId); + expect(message.reactionCounts, entity.reactionCounts); + expect(message.reactionScores, entity.reactionScores); + for (var i = 0; i < message.attachments.length; i++) { + final messageAttachment = message.attachments[i]; + final entityAttachmentData = jsonDecode(entity.attachments[i]); + final entityAttachment = Attachment.fromData(entityAttachmentData); + expect(messageAttachment.id, entityAttachment.id); + expect(messageAttachment.type, entityAttachment.type); + expect(messageAttachment.assetUrl, entityAttachment.assetUrl); + } + }); + + test('toEntity should map message into MessageEntity', () { + const cid = 'testCid'; + final user = User(id: 'testUserId'); + final quotedMessage = Message(id: 'testQuotedMessageId'); + final reactions = List.generate( + 3, + (index) => Reaction( + messageId: 'testMessageId', + createdAt: DateTime.now(), + type: 'testType$index', + user: user, + score: math.Random().nextInt(50), + ), + ); + final attachments = List.generate( + 3, + (index) => Attachment( + id: 'testAttachmentId', + type: 'testAttachmentType', + assetUrl: 'testAssetUrl', + ), + ); + final message = Message( + id: 'testMessageId', + attachments: attachments, + type: 'testType', + parentId: 'testParentId', + quotedMessageId: quotedMessage.id, + command: 'testCommand', + createdAt: DateTime.now(), + shadowed: math.Random().nextBool(), + showInChannel: math.Random().nextBool(), + replyCount: 33, + reactionScores: {for (final r in reactions) r.type: r.score}, + reactionCounts: reactions.fold( + {}, + (prev, curr) => + prev..update(curr.type, (value) => value + 1, ifAbsent: () => 1), + ), + status: MessageSendingStatus.sending, + updatedAt: DateTime.now(), + extraData: {'extra_test_data': 'extraData'}, + user: user, + deletedAt: DateTime.now(), + text: 'dummy text', + pinned: true, + pinExpires: DateTime.now(), + pinnedAt: DateTime.now(), + pinnedBy: user, + ); + final entity = message.toEntity(cid: cid); + expect(entity, isA()); + expect(entity.id, message.id); + expect(entity.type, message.type); + expect(entity.parentId, message.parentId); + expect(entity.quotedMessageId, message.quotedMessageId); + expect(entity.command, message.command); + expect(entity.createdAt, isSameDateAs(message.createdAt)); + expect(entity.shadowed, message.shadowed); + expect(entity.showInChannel, message.showInChannel); + expect(entity.replyCount, message.replyCount); + expect(entity.reactionScores, message.reactionScores); + expect(entity.reactionCounts, message.reactionCounts); + expect(entity.status, message.status); + expect(entity.updatedAt, isSameDateAs(message.updatedAt)); + expect(entity.extraData, message.extraData); + expect(entity.userId, message.user.id); + expect(entity.deletedAt, isSameDateAs(message.deletedAt)); + expect(entity.messageText, message.text); + expect(entity.pinned, message.pinned); + expect(entity.pinExpires, isSameDateAs(message.pinExpires)); + expect(entity.pinnedAt, isSameDateAs(message.pinnedAt)); + expect(entity.pinnedByUserId, message.pinnedBy.id); + expect(entity.reactionCounts, message.reactionCounts); + expect(entity.reactionScores, message.reactionScores); + expect( + entity.attachments, + message.attachments?.map((it) => jsonEncode(it.toData()))?.toList(), + ); + }); +} diff --git a/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart new file mode 100644 index 00000000..0aadf08a --- /dev/null +++ b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart @@ -0,0 +1,184 @@ +import 'dart:convert'; +import 'dart:math' as math; + +import 'package:test/test.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/mapper/pinned_message_mapper.dart'; + +import '../utils/date_matcher.dart'; + +void main() { + test('toMessage should map the entity into Message', () { + final user = User(id: 'testUserId'); + final quotedMessage = Message(id: 'testQuotedMessageId'); + final reactions = List.generate( + 3, + (index) => Reaction( + messageId: 'testMessageId', + createdAt: DateTime.now(), + type: 'testType$index', + user: user, + score: math.Random().nextInt(50), + ), + ); + final attachments = List.generate( + 3, + (index) => Attachment( + id: 'testAttachmentId', + type: 'testAttachmentType', + assetUrl: 'testAssetUrl', + ), + ); + final entity = PinnedMessageEntity( + id: 'testMessageId', + attachments: attachments?.map((it) => jsonEncode(it.toData()))?.toList(), + channelCid: 'testCid', + type: 'testType', + parentId: 'testParentId', + quotedMessageId: quotedMessage.id, + command: 'testCommand', + createdAt: DateTime.now(), + shadowed: math.Random().nextBool(), + showInChannel: math.Random().nextBool(), + replyCount: 33, + reactionScores: {for (final r in reactions) r.type: r.score}, + reactionCounts: reactions.fold( + {}, + (prev, curr) => + prev..update(curr.type, (value) => value + 1, ifAbsent: () => 1), + ), + status: MessageSendingStatus.sent, + updatedAt: DateTime.now(), + extraData: {'extra_test_data': 'extraData'}, + userId: user.id, + deletedAt: DateTime.now(), + messageText: 'dummy text', + pinned: true, + pinExpires: DateTime.now().toUtc(), + pinnedAt: DateTime.now(), + pinnedByUserId: user.id, + ); + final message = entity.toMessage( + user: user, + pinnedBy: user, + latestReactions: reactions, + ownReactions: reactions, + quotedMessage: quotedMessage, + ); + + expect(message, isA()); + expect(message.id, entity.id); + expect(message.type, entity.type); + expect(message.parentId, entity.parentId); + expect(message.quotedMessageId, entity.quotedMessageId); + expect(message.command, entity.command); + expect(message.createdAt, isSameDateAs(entity.createdAt)); + expect(message.shadowed, entity.shadowed); + expect(message.showInChannel, entity.showInChannel); + expect(message.replyCount, entity.replyCount); + expect(message.reactionScores, entity.reactionScores); + expect(message.reactionCounts, entity.reactionCounts); + expect(message.status, entity.status); + expect(message.updatedAt, isSameDateAs(entity.updatedAt)); + expect(message.extraData, entity.extraData); + expect(message.user.id, entity.userId); + expect(message.deletedAt, isSameDateAs(entity.deletedAt)); + expect(message.text, entity.messageText); + expect(message.pinned, entity.pinned); + expect(message.pinExpires, isSameDateAs(entity.pinExpires)); + expect(message.pinnedAt, isSameDateAs(entity.pinnedAt)); + expect(message.pinnedBy.id, entity.pinnedByUserId); + expect(message.reactionCounts, entity.reactionCounts); + expect(message.reactionScores, entity.reactionScores); + for (var i = 0; i < message.attachments.length; i++) { + final messageAttachment = message.attachments[i]; + final entityAttachmentData = jsonDecode(entity.attachments[i]); + final entityAttachment = Attachment.fromData(entityAttachmentData); + expect(messageAttachment.id, entityAttachment.id); + expect(messageAttachment.type, entityAttachment.type); + expect(messageAttachment.assetUrl, entityAttachment.assetUrl); + } + }); + + test('toPinnedEntity should map message into PinnedMessageEntity', () { + const cid = 'testCid'; + final user = User(id: 'testUserId'); + final quotedMessage = Message(id: 'testQuotedMessageId'); + final reactions = List.generate( + 3, + (index) => Reaction( + messageId: 'testMessageId', + createdAt: DateTime.now(), + type: 'testType$index', + user: user, + score: math.Random().nextInt(50), + ), + ); + final attachments = List.generate( + 3, + (index) => Attachment( + id: 'testAttachmentId', + type: 'testAttachmentType', + assetUrl: 'testAssetUrl', + ), + ); + final message = Message( + id: 'testMessageId', + attachments: attachments, + type: 'testType', + parentId: 'testParentId', + quotedMessageId: quotedMessage.id, + command: 'testCommand', + createdAt: DateTime.now(), + shadowed: math.Random().nextBool(), + showInChannel: math.Random().nextBool(), + replyCount: 33, + reactionScores: {for (final r in reactions) r.type: r.score}, + reactionCounts: reactions.fold( + {}, + (prev, curr) => + prev..update(curr.type, (value) => value + 1, ifAbsent: () => 1), + ), + status: MessageSendingStatus.sending, + updatedAt: DateTime.now(), + extraData: {'extra_test_data': 'extraData'}, + user: user, + deletedAt: DateTime.now(), + text: 'dummy text', + pinned: true, + pinExpires: DateTime.now(), + pinnedAt: DateTime.now(), + pinnedBy: user, + ); + final entity = message.toPinnedEntity(cid: cid); + expect(entity, isA()); + expect(entity.id, message.id); + expect(entity.type, message.type); + expect(entity.parentId, message.parentId); + expect(entity.quotedMessageId, message.quotedMessageId); + expect(entity.command, message.command); + expect(entity.createdAt, isSameDateAs(message.createdAt)); + expect(entity.shadowed, message.shadowed); + expect(entity.showInChannel, message.showInChannel); + expect(entity.replyCount, message.replyCount); + expect(entity.reactionScores, message.reactionScores); + expect(entity.reactionCounts, message.reactionCounts); + expect(entity.status, message.status); + expect(entity.updatedAt, isSameDateAs(message.updatedAt)); + expect(entity.extraData, message.extraData); + expect(entity.userId, message.user.id); + expect(entity.deletedAt, isSameDateAs(message.deletedAt)); + expect(entity.messageText, message.text); + expect(entity.pinned, message.pinned); + expect(entity.pinExpires, isSameDateAs(message.pinExpires)); + expect(entity.pinnedAt, isSameDateAs(message.pinnedAt)); + expect(entity.pinnedByUserId, message.pinnedBy.id); + expect(entity.reactionCounts, message.reactionCounts); + expect(entity.reactionScores, message.reactionScores); + expect( + entity.attachments, + message.attachments?.map((it) => jsonEncode(it.toData()))?.toList(), + ); + }); +} diff --git a/packages/stream_chat_persistence/test/src/mapper/reaction_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/reaction_mapper_test.dart new file mode 100644 index 00000000..cb5d231e --- /dev/null +++ b/packages/stream_chat_persistence/test/src/mapper/reaction_mapper_test.dart @@ -0,0 +1,52 @@ +import 'package:test/test.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/mapper/reaction_mapper.dart'; + +import '../utils/date_matcher.dart'; + +void main() { + test('toReaction should map the entity into Reaction', () { + final user = User(id: 'testUserId'); + final message = Message(id: 'testMessageId'); + final entity = ReactionEntity( + userId: user.id, + messageId: message.id, + type: 'haha', + score: 33, + createdAt: DateTime.now(), + extraData: {'extra_test_data': 'extraData'}, + ); + + final reaction = entity.toReaction(user: user); + expect(reaction, isA()); + expect(reaction.userId, entity.userId); + expect(reaction.messageId, entity.messageId); + expect(reaction.type, entity.type); + expect(reaction.score, entity.score); + expect(reaction.createdAt, isSameDateAs(entity.createdAt)); + expect(reaction.extraData, entity.extraData); + }); + + test('toEntity should map reaction into ReactionEntity', () { + final user = User(id: 'testUserId'); + final message = Message(id: 'testMessageId'); + final reaction = Reaction( + userId: user.id, + messageId: message.id, + type: 'haha', + score: 33, + createdAt: DateTime.now(), + extraData: {'extra_test_data': 'extraData'}, + ); + + final entity = reaction.toEntity(); + expect(entity, isA()); + expect(entity.userId, reaction.userId); + expect(entity.messageId, reaction.messageId); + expect(entity.type, reaction.type); + expect(entity.score, reaction.score); + expect(entity.createdAt, isSameDateAs(reaction.createdAt)); + expect(entity.extraData, reaction.extraData); + }); +} diff --git a/packages/stream_chat_persistence/test/src/mapper/read_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/read_mapper_test.dart new file mode 100644 index 00000000..73c87da3 --- /dev/null +++ b/packages/stream_chat_persistence/test/src/mapper/read_mapper_test.dart @@ -0,0 +1,41 @@ +import 'package:test/test.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/mapper/read_mapper.dart'; + +import '../utils/date_matcher.dart'; + +void main() { + test('toRead should map entity into Read', () { + const cid = 'testCid'; + final user = User(id: 'testUserId'); + final entity = ReadEntity( + lastRead: DateTime.now(), + userId: user.id, + channelCid: cid, + unreadMessages: 33, + ); + + final read = entity.toRead(user: user); + expect(read, isA()); + expect(read.lastRead, isSameDateAs(entity.lastRead)); + expect(read.user.id, entity.userId); + expect(read.unreadMessages, entity.unreadMessages); + }); + + test('toEntity should map read into ReadEntity', () { + const cid = 'testCid'; + final user = User(id: 'testUserId'); + final read = Read( + lastRead: DateTime.now(), + user: user, + unreadMessages: 33, + ); + + final entity = read.toEntity(cid: cid); + expect(entity, isA()); + expect(entity.lastRead, isSameDateAs(read.lastRead)); + expect(entity.userId, read.user.id); + expect(entity.unreadMessages, read.unreadMessages); + }); +} diff --git a/packages/stream_chat_persistence/test/src/mapper/user_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/user_mapper_test.dart new file mode 100644 index 00000000..5e40f237 --- /dev/null +++ b/packages/stream_chat_persistence/test/src/mapper/user_mapper_test.dart @@ -0,0 +1,55 @@ +import 'dart:math' as math; +import 'package:test/test.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/mapper/user_mapper.dart'; + +import '../utils/date_matcher.dart'; + +void main() { + test('toUser should map entity into User', () { + final entity = UserEntity( + id: 'testUserId', + role: 'testType', + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + lastActive: DateTime.now(), + online: math.Random().nextBool(), + banned: math.Random().nextBool(), + extraData: {'test_extra_data': 'extraData'}, + ); + final user = entity.toUser(); + expect(user, isA()); + expect(user.id, entity.id); + expect(user.role, entity.role); + expect(user.createdAt, isSameDateAs(entity.createdAt)); + expect(user.updatedAt, isSameDateAs(entity.updatedAt)); + expect(user.lastActive, isSameDateAs(entity.lastActive)); + expect(user.online, entity.online); + expect(user.banned, entity.banned); + expect(user.extraData, entity.extraData); + }); + + test('toEntity should map user into UserEntity', () { + final user = User( + id: 'testUserId', + role: 'testType', + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + lastActive: DateTime.now(), + online: math.Random().nextBool(), + banned: math.Random().nextBool(), + extraData: {'test_extra_data': 'extraData'}, + ); + final entity = user.toEntity(); + expect(entity, isA()); + expect(entity.id, user.id); + expect(entity.role, user.role); + expect(entity.createdAt, isSameDateAs(user.createdAt)); + expect(entity.updatedAt, isSameDateAs(user.updatedAt)); + expect(entity.lastActive, isSameDateAs(user.lastActive)); + expect(entity.online, user.online); + expect(entity.banned, user.banned); + expect(entity.extraData, user.extraData); + }); +} 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/src/utils/date_matcher.dart b/packages/stream_chat_persistence/test/src/utils/date_matcher.dart new file mode 100644 index 00000000..909e58b0 --- /dev/null +++ b/packages/stream_chat_persistence/test/src/utils/date_matcher.dart @@ -0,0 +1,26 @@ +import 'package:test/test.dart'; +import 'package:meta/meta.dart'; + +Matcher isSameDateAs(DateTime targetDate) => + _IsSameDateAs(targetDate: targetDate); + +class _IsSameDateAs extends Matcher { + const _IsSameDateAs({ + @required this.targetDate, + }) : assert(targetDate != null, ''); + + final DateTime targetDate; + + @override + bool matches(covariant DateTime date, Map matchState) => + date.year == targetDate.year && + date.month == targetDate.month && + date.day == targetDate.day && + date.hour == targetDate.hour && + date.minute == targetDate.minute && + date.second == targetDate.second; + + @override + Description describe(Description description) => + description.add('is same date as $targetDate'); +} 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..2d44ca15 --- /dev/null +++ b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart @@ -0,0 +1,558 @@ +import 'package:mocktail/mocktail.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('client constructor', () { + test('throws assertion error if null connectionMode is provided', () { + expect( + () => StreamChatPersistenceClient(connectionMode: null), + throwsA(isA()), + ); + }); + + test('throws assertion error if null logLevel is provided', () { + expect( + () => StreamChatPersistenceClient(logLevel: null), + throwsA(isA()), + ); + }); + }); + + 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); + }); + }); +}