Merge pull request #332 from GetStream/persistence/add-tests

test: add tests in persistence client
This commit is contained in:
Salvatore Giordano
2021-03-19 16:13:05 +01:00
committed by GitHub
56 changed files with 5488 additions and 222 deletions
+6
View File
@@ -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$'
+10 -5
View File
@@ -33,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
@@ -69,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/[email protected]
with:
path: packages/stream_chat/lcov.info
min_coverage: 50
path: packages/stream_chat/coverage/lcov.info
min_coverage: 40
- uses: VeryGoodOpenSource/[email protected]
with:
path: packages/stream_chat_persistence/coverage/lcov.info
min_coverage: 0.2
min_coverage: 95
- uses: VeryGoodOpenSource/[email protected]
with:
path: packages/stream_chat_flutter_core/coverage/lcov.info
+2 -1
View File
@@ -738,7 +738,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;
@@ -100,10 +100,9 @@ abstract class ChatPersistenceClient {
/// the list of matching rows will be deleted
Future<void> updateChannelQueries(
Map<String, dynamic> filter,
List<String> cids,
// ignore: avoid_positional_boolean_parameters
bool clearQueryCache,
);
List<String> cids, {
bool clearQueryCache = false,
});
/// Remove a message by [messageId]
Future<void> deleteMessageById(String messageId) =>
@@ -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<String, dynamic> 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
@@ -27,4 +27,16 @@ class Read {
/// Serialize to json
Map<String, dynamic> 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,
);
}
@@ -100,4 +100,28 @@ class User {
/// Serialize to json
Map<String, dynamic> 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<String, dynamic> extraData,
bool banned,
List<String> 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,
);
}
@@ -32,7 +32,7 @@ class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
Future<void> updateChannelQueries(
Map<String, dynamic> filter,
List<String> cids, {
bool clearQueryCache,
bool clearQueryCache = false,
}) async =>
transaction(() async {
final hash = _computeHash(filter);
@@ -57,6 +57,14 @@ class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
});
});
///
Future<List<String>> getCachedChannelCids(Map<String, dynamic> 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<List<ChannelModel>> getChannels({
Map<String, dynamic> filter,
@@ -72,12 +80,7 @@ class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
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([
@@ -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<MoorChatDatabase>
with _$ConnectionEventDaoMixin {
/// Creates a new connection event dao instance
@@ -9,5 +9,4 @@ part of 'connection_event_dao.dart';
mixin _$ConnectionEventDaoMixin on DatabaseAccessor<MoorChatDatabase> {
$ConnectionEventsTable get connectionEvents =>
attachedDatabase.connectionEvents;
$UsersTable get users => attachedDatabase.users;
}
@@ -72,7 +72,7 @@ class MessageDao extends DatabaseAccessor<MoorChatDatabase>
/// [Messages.channelCid] with [cid]
Future<List<Message>> 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<MoorChatDatabase>
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<MoorChatDatabase>
.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;
}
@@ -89,18 +89,36 @@ class PinnedMessageDao extends DatabaseAccessor<MoorChatDatabase>
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<MoorChatDatabase>
.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;
}
@@ -20,4 +20,10 @@ class UserDao extends DatabaseAccessor<MoorChatDatabase> with _$UserDaoMixin {
mode: InsertMode.insertOrReplace,
),
);
/// Returns the list of all the users stored in db
Future<List<User>> getUsers() =>
(select(users)..orderBy([(u) => OrderingTerm.desc(u.createdAt)]))
.map((it) => it.toUser())
.get();
}
@@ -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
@@ -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<VmDatabase> 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<VmDatabase> _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 {
@@ -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<DelegatedDatabase> 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',
);
}
}
@@ -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<WebDatabase> 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);
}
}
@@ -1,3 +1,4 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
/// Represents a [ChannelQueries] table in [MoorChatDatabase].
@@ -1,3 +1,4 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
@@ -1,3 +1,4 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
@@ -1,3 +1,4 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
/// Represents a [Members] table in [MoorChatDatabase].
@@ -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';
@@ -1,3 +1,4 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
import 'package:stream_chat_persistence/src/entity/messages.dart';
@@ -1,3 +1,4 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
@@ -1,3 +1,4 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
/// Represents a [Reads] table in [MoorChatDatabase].
@@ -1,3 +1,4 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
import 'package:stream_chat_persistence/src/converter/map_converter.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,
);
}
@@ -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<void> connect(String userId) async {
Future<void> 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<void> updateChannelQueries(
Map<String, dynamic> filter,
List<String> cids,
bool clearQueryCache,
) =>
List<String> cids, {
bool clearQueryCache = false,
}) =>
_readProtected(() async {
_logger.info('updateChannelQueries');
return db.channelQueryDao.updateChannelQueries(
@@ -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<Object>()",
"dart_type_name": "Map<String, Object>"
}
},
{
"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<Object>()",
"dart_type_name": "Map<String, Object>"
}
}
],
"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<String>()",
"dart_type_name": "List<String>"
}
},
{
"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<String>()",
"dart_type_name": "List<String>"
}
},
{
"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<int>()",
"dart_type_name": "Map<String, int>"
}
},
{
"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<int>()",
"dart_type_name": "Map<String, int>"
}
},
{
"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<Object>()",
"dart_type_name": "Map<String, Object>"
}
}
],
"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<Object>()",
"dart_type_name": "Map<String, Object>"
}
}
],
"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<Object>()",
"dart_type_name": "Map<String, Object>"
}
}
],
"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<Object>()",
"dart_type_name": "Map<String, Object>"
}
},
{
"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"
]
}
}
]
}
File diff suppressed because it is too large Load Diff
@@ -20,7 +20,12 @@ dependencies:
sqlite3_flutter_libs: ^0.4.0+1
stream_chat: ^1.5.0
dependency_overrides:
stream_chat:
path: ../stream_chat
dev_dependencies:
build_runner: ^1.11.0
mockito: ^4.1.3
moor_generator: ^3.4.1
test: ^1.15.7
@@ -0,0 +1,71 @@
import 'package:mockito/mockito.dart';
import 'package:stream_chat_persistence/src/dao/dao.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
class MockChatDatabase extends Mock implements MoorChatDatabase {
UserDao _userDao;
@override
UserDao get userDao => _userDao ??= MockUserDao();
ChannelDao _channelDao;
@override
ChannelDao get channelDao => _channelDao ??= MockChannelDao();
MessageDao _messageDao;
@override
MessageDao get messageDao => _messageDao ??= MockMessageDao();
PinnedMessageDao _pinnedMessageDao;
@override
PinnedMessageDao get pinnedMessageDao =>
_pinnedMessageDao ??= MockPinnedMessageDao();
MemberDao _memberDao;
@override
MemberDao get memberDao => _memberDao ??= MockMemberDao();
ReactionDao _reactionDao;
@override
ReactionDao get reactionDao => _reactionDao ??= MockReactionDao();
ReadDao _readDao;
@override
ReadDao get readDao => _readDao ??= MockReadDao();
ChannelQueryDao _channelQueryDao;
@override
ChannelQueryDao get channelQueryDao =>
_channelQueryDao ??= MockChannelQueryDao();
ConnectionEventDao _connectionEventDao;
@override
ConnectionEventDao get connectionEventDao =>
_connectionEventDao ??= MockConnectionEventDao();
}
class MockUserDao extends Mock implements UserDao {}
class MockChannelDao extends Mock implements ChannelDao {}
class MockMessageDao extends Mock implements MessageDao {}
class MockPinnedMessageDao extends Mock implements PinnedMessageDao {}
class MockMemberDao extends Mock implements MemberDao {}
class MockReactionDao extends Mock implements ReactionDao {}
class MockReadDao extends Mock implements ReadDao {}
class MockChannelQueryDao extends Mock implements ChannelQueryDao {}
class MockConnectionEventDao extends Mock implements ConnectionEventDao {}
@@ -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<String>();
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<TypeError>()),
);
});
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<TypeError>()),
);
});
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<String>();
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));
});
});
}
@@ -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<String>();
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<TypeError>()),
);
});
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<TypeError>()),
);
},
);
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<String>();
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));
});
});
}
@@ -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);
});
});
}
@@ -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();
});
}
@@ -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<List<ChannelModel>> _insertTestDataForGetChannel(
Map<String, Object> 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();
});
}
@@ -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();
});
}
@@ -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<List<Member>> _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();
});
}
@@ -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<List<Message>> _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();
});
}
@@ -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<List<Message>> _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();
});
}
@@ -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<List<Reaction>> _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();
});
}
@@ -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<List<Read>> _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();
});
}
@@ -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<List<User>> _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();
});
}
@@ -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();
});
},
);
}
@@ -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<ChannelModel>());
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<ChannelState>());
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<ChannelEntity>());
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);
});
}
@@ -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<Event>());
expect(event.me.id, ownUser.id);
expect(event.totalUnreadCount, entity.totalUnreadCount);
expect(event.unreadChannels, entity.unreadChannels);
});
}
@@ -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<Member>());
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<MemberEntity>());
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);
});
}
@@ -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<Message>());
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<MessageEntity>());
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(),
);
});
}
@@ -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<Message>());
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<PinnedMessageEntity>());
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(),
);
});
}
@@ -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<Reaction>());
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<ReactionEntity>());
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);
});
}
@@ -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<Read>());
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<ReadEntity>());
expect(entity.lastRead, isSameDateAs(read.lastRead));
expect(entity.userId, read.user.id);
expect(entity.unreadMessages, read.unreadMessages);
});
}
@@ -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<User>());
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<UserEntity>());
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);
});
}
@@ -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.';
}))),
);
});
});
}
@@ -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');
}
@@ -0,0 +1,540 @@
import 'package:mockito/mockito.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/stream_chat_persistence_client.dart';
import 'package:test/test.dart';
import 'mock_chat_database.dart';
import 'src/utils/date_matcher.dart';
MoorChatDatabase _testDatabaseProvider(String userId, ConnectionMode mode) =>
MoorChatDatabase.testable(userId);
void main() {
group('client constructor', () {
test('throws assertion error if null connectionMode is provided', () {
expect(
() => StreamChatPersistenceClient(connectionMode: null),
throwsA(isA<AssertionError>()),
);
});
test('throws assertion error if null logLevel is provided', () {
expect(
() => StreamChatPersistenceClient(logLevel: null),
throwsA(isA<AssertionError>()),
);
});
});
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<MoorChatDatabase>());
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<MoorChatDatabase>());
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 = <String, dynamic>{};
const cids = <String>[];
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 = <String>[];
when(mockDatabase.messageDao.deleteMessageByIds(messageIds))
.thenAnswer((_) async {
return;
});
await client.deleteMessageByIds(messageIds);
verify(mockDatabase.messageDao.deleteMessageByIds(messageIds)).called(1);
});
test('deletePinnedMessageByIds', () async {
const messageIds = <String>[];
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 = <String>[];
when(mockDatabase.messageDao.deleteMessageByCids(cids))
.thenAnswer((_) async {
return;
});
await client.deleteMessageByCids(cids);
verify(mockDatabase.messageDao.deleteMessageByCids(cids)).called(1);
});
test('deletePinnedMessageByCids', () async {
const cids = <String>[];
when(mockDatabase.pinnedMessageDao.deleteMessageByCids(cids))
.thenAnswer((_) async {
return;
});
await client.deletePinnedMessageByCids(cids);
verify(mockDatabase.pinnedMessageDao.deleteMessageByCids(cids)).called(1);
});
test('deleteChannels', () async {
const cids = <String>[];
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<Map<String, List<Message>>>(
{},
(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 = <String>[];
when(mockDatabase.reactionDao.deleteReactionsByMessageIds(messageIds))
.thenAnswer((_) async {
return;
});
await client.deleteReactionsByMessageId(messageIds);
verify(mockDatabase.reactionDao.deleteReactionsByMessageIds(messageIds))
.called(1);
});
test('deleteMembersByCids', () async {
final cids = <String>[];
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);
});
});
}