From 89f989003d12598c45636cec76bcc7554c57db51 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 8 Jun 2021 16:09:17 +0530 Subject: [PATCH] add core/api tests Signed-off-by: Sahil Kumar --- .../stream_chat/lib/src/client/client.dart | 6 - .../core/api/attachment_file_uploader.dart | 51 +- .../lib/src/core/api/channel_api.dart | 2 - .../lib/src/core/api/device_api.dart | 3 +- .../lib/src/core/models/attachment_file.dart | 35 +- packages/stream_chat/test/assets/example.pdf | 57 ++ .../stream_chat/test/assets/test_image.jpeg | Bin 0 -> 4429 bytes .../api/attachment_file_uploader_test.dart | 136 +++++ .../test/src/core/api/channel_api_test.dart | 492 ++++++++++++++++++ .../test/src/core/api/device_api_test.dart | 84 +++ .../test/src/core/api/general_api_test.dart | 211 ++++++++ .../test/src/core/api/guest_api_test.dart | 44 ++ .../test/src/core/api/message_api_test.dart | 291 +++++++++++ .../src/core/api/moderation_api_test.dart | 190 +++++++ .../src/core/api/stream_chat_api_test.dart | 49 ++ .../test/src/core/api/user_api_test.dart | 77 +++ ...rt => connection_id_interceptor_test.dart} | 0 .../src/{ => core}/models/action_test.dart | 0 .../{ => core}/models/attachment_test.dart | 0 .../{ => core}/models/channel_state_test.dart | 0 .../src/{ => core}/models/channel_test.dart | 0 .../src/{ => core}/models/command_test.dart | 0 .../src/{ => core}/models/device_test.dart | 0 .../src/{ => core}/models/event_test.dart | 0 .../src/{ => core}/models/filter_test.dart | 0 .../src/{ => core}/models/member_test.dart | 0 .../src/{ => core}/models/message_test.dart | 0 .../src/{ => core}/models/reaction_test.dart | 0 .../test/src/{ => core}/models/read_test.dart | 0 .../{ => core}/models/serialization_test.dart | 0 .../test/src/{ => core}/models/user_test.dart | 0 packages/stream_chat/test/src/fakes.dart | 57 ++ packages/stream_chat/test/src/matchers.dart | 19 + packages/stream_chat/test/src/mocks.dart | 25 + packages/stream_chat/test/src/utils.dart | 15 + 35 files changed, 1788 insertions(+), 56 deletions(-) create mode 100644 packages/stream_chat/test/assets/example.pdf create mode 100644 packages/stream_chat/test/assets/test_image.jpeg create mode 100644 packages/stream_chat/test/src/core/api/attachment_file_uploader_test.dart create mode 100644 packages/stream_chat/test/src/core/api/channel_api_test.dart create mode 100644 packages/stream_chat/test/src/core/api/device_api_test.dart create mode 100644 packages/stream_chat/test/src/core/api/general_api_test.dart create mode 100644 packages/stream_chat/test/src/core/api/guest_api_test.dart create mode 100644 packages/stream_chat/test/src/core/api/message_api_test.dart create mode 100644 packages/stream_chat/test/src/core/api/moderation_api_test.dart create mode 100644 packages/stream_chat/test/src/core/api/stream_chat_api_test.dart create mode 100644 packages/stream_chat/test/src/core/api/user_api_test.dart rename packages/stream_chat/test/src/core/http/interceptor/{connection_id_interceptor.dart => connection_id_interceptor_test.dart} (100%) rename packages/stream_chat/test/src/{ => core}/models/action_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/attachment_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/channel_state_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/channel_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/command_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/device_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/event_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/filter_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/member_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/message_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/reaction_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/read_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/serialization_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/user_test.dart (100%) create mode 100644 packages/stream_chat/test/src/matchers.dart create mode 100644 packages/stream_chat/test/src/utils.dart diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index ddc6e335..9ecc780b 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -38,10 +38,6 @@ import 'package:stream_chat/src/core/models/member.dart'; /// [LogRecord] as the only parameter. typedef LogHandlerFunction = void Function(LogRecord record); -/// A function which can be used to request a Stream Chat API token from your -/// own backend server. Function requires a single [userId]. -typedef TokenProvider = Future Function(String userId); - final _levelEmojiMapper = { Level.INFO: 'ℹ️', Level.WARNING: '⚠️', @@ -583,7 +579,6 @@ class StreamChatClient { } logger.info('Query channel start'); - print('Query Channel Started : ${DateTime.now()}'); final res = await _chatApi.channel.queryChannels( filter: filter, sort: sort, @@ -594,7 +589,6 @@ class StreamChatClient { messageLimit: messageLimit, paginationParams: paginationParams, ); - print('Query Channel Completed : ${DateTime.now()}'); if (res.channels.isEmpty && paginationParams.offset == 0) { logger.warning(''' diff --git a/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart b/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart index 8eacf716..d6dc249f 100644 --- a/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart +++ b/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart @@ -1,7 +1,6 @@ import 'package:dio/dio.dart'; import 'package:stream_chat/src/core/api/responses.dart'; import 'package:stream_chat/src/core/http/stream_http_client.dart'; -import 'package:stream_chat/src/core/util/extension.dart'; import 'package:stream_chat/src/core/models/attachment_file.dart'; /// Class responsible for uploading images and files to a given channel @@ -70,29 +69,10 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader { ProgressCallback? onSendProgress, CancelToken? cancelToken, }) async { - final filename = file.path?.split('/').last ?? file.name; - final mimeType = filename?.mimeType; - - MultipartFile? multiPartFile; - if (file.path != null) { - multiPartFile = await MultipartFile.fromFile( - file.path!, - filename: filename, - contentType: mimeType, - ); - } else if (file.bytes != null) { - multiPartFile = MultipartFile.fromBytes( - file.bytes!, - filename: filename, - contentType: mimeType, - ); - } - - final response = await _client.post( + final multiPartFile = await file.toMultipartFile(); + final response = await _client.postFile( '/channels/$channelType/$channelId/image', - data: FormData.fromMap({ - 'file': multiPartFile, - }), + multiPartFile, onSendProgress: onSendProgress, cancelToken: cancelToken, ); @@ -107,29 +87,10 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader { ProgressCallback? onSendProgress, CancelToken? cancelToken, }) async { - final filename = file.path?.split('/').last ?? file.name; - final mimeType = filename?.mimeType; - - MultipartFile? multiPartFile; - if (file.path != null) { - multiPartFile = await MultipartFile.fromFile( - file.path!, - filename: filename, - contentType: mimeType, - ); - } else if (file.bytes != null) { - multiPartFile = MultipartFile.fromBytes( - file.bytes!, - filename: filename, - contentType: mimeType, - ); - } - - final response = await _client.post( + final multiPartFile = await file.toMultipartFile(); + final response = await _client.postFile( '/channels/$channelType/$channelId/file', - data: FormData.fromMap({ - 'file': multiPartFile, - }), + multiPartFile, onSendProgress: onSendProgress, cancelToken: cancelToken, ); diff --git a/packages/stream_chat/lib/src/core/api/channel_api.dart b/packages/stream_chat/lib/src/core/api/channel_api.dart index 7407bcb5..761f10da 100644 --- a/packages/stream_chat/lib/src/core/api/channel_api.dart +++ b/packages/stream_chat/lib/src/core/api/channel_api.dart @@ -59,7 +59,6 @@ class ChannelApi { bool presence = false, PaginationParams paginationParams = const PaginationParams(), }) async { - print('Query Channel Started 2 : ${DateTime.now()}'); final response = await _client.get( '/channels', queryParameters: { @@ -80,7 +79,6 @@ class ChannelApi { }) }, ); - print('Query Channel Completed 2 : ${DateTime.now()}'); return QueryChannelsResponse.fromJson(response.data); } diff --git a/packages/stream_chat/lib/src/core/api/device_api.dart b/packages/stream_chat/lib/src/core/api/device_api.dart index 2d5e0fea..69f509f8 100644 --- a/packages/stream_chat/lib/src/core/api/device_api.dart +++ b/packages/stream_chat/lib/src/core/api/device_api.dart @@ -10,7 +10,8 @@ enum PushProvider { apn } -extension on PushProvider { +/// Helper extension for [PushProvider] +extension PushProviderX on PushProvider { /// Returns the string notion for [PushProvider]. String get name => { PushProvider.apn: 'apn', diff --git a/packages/stream_chat/lib/src/core/models/attachment_file.dart b/packages/stream_chat/lib/src/core/models/attachment_file.dart index 8dbb70ad..7bff5806 100644 --- a/packages/stream_chat/lib/src/core/models/attachment_file.dart +++ b/packages/stream_chat/lib/src/core/models/attachment_file.dart @@ -1,9 +1,13 @@ import 'dart:typed_data'; +import 'package:dio/dio.dart' show MultipartFile; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:meta/meta.dart'; +import 'package:stream_chat/src/core/platform_detector/platform_detector.dart'; +import 'package:stream_chat/src/core/util/extension.dart'; part 'attachment_file.freezed.dart'; + part 'attachment_file.g.dart'; /// Union class to hold various [UploadState] of a attachment. @@ -58,14 +62,18 @@ String? _toString(Uint8List? bytes) { @JsonSerializable() class AttachmentFile { /// Creates a new [AttachmentFile] instance. - const AttachmentFile({ + AttachmentFile({ required this.size, this.path, this.name, this.bytes, - }) : assert( + }) : assert( path != null || bytes != null, 'Either path or bytes should be != null', + ), + assert( + !CurrentPlatform.isWeb || bytes != null, + 'File by path is not supported in web, Please provide bytes', ); /// Create a new instance from a json @@ -95,4 +103,27 @@ class AttachmentFile { /// Serialize to json Map toJson() => _$AttachmentFileToJson(this); + + /// Converts this into a [MultipartFile] + Future toMultipartFile() async { + final filename = path?.split('/').last ?? name; + final mimeType = filename?.mimeType; + + late MultipartFile multiPartFile; + + if (CurrentPlatform.isWeb) { + multiPartFile = MultipartFile.fromBytes( + bytes!, + filename: filename, + contentType: mimeType, + ); + } else { + multiPartFile = await MultipartFile.fromFile( + path!, + filename: filename, + contentType: mimeType, + ); + } + return multiPartFile; + } } diff --git a/packages/stream_chat/test/assets/example.pdf b/packages/stream_chat/test/assets/example.pdf new file mode 100644 index 00000000..d736dedc --- /dev/null +++ b/packages/stream_chat/test/assets/example.pdf @@ -0,0 +1,57 @@ +%PDF-1.7 +% +3 0 obj +<< /Length 4 0 R >> +stream +/DeviceRGB cs /DeviceRGB CS +0 0 0.972549 SC +21.68 194 136.64 26 re +10 10 m 20 20 l S +BT +/F0 24 Tf +25.68 200 Td +(Hello World!) Tj +ET +endstream +endobj +4 0 obj +132 +endobj +5 0 obj +<< /Type /Font /Subtype /Type1 /BaseFont /Times-Roman /Encoding /WinAnsiEncoding >> +endobj +6 0 obj +<< /Type /Page + /Parent 2 0 R + /Resources << /Font << /F0 5 0 R >> >> + /MediaBox [ 0 0 180 240 ] + /Contents 3 0 R +>> +endobj +2 0 obj +<< /Type /Pages + /Count 1 + /Kids [ 6 0 R ] +>> +endobj +1 0 obj +<< /Type /Catalog + /Pages 2 0 R +>> +endobj +xref +0 7 +0000000000 65535 f +0000000522 00000 n +0000000457 00000 n +0000000015 00000 n +0000000199 00000 n +0000000218 00000 n +0000000317 00000 n +trailer +<< /Size 7 + /Root 1 0 R +>> +startxref +574 +%%EOF diff --git a/packages/stream_chat/test/assets/test_image.jpeg b/packages/stream_chat/test/assets/test_image.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..aeaccec269c66219d2abb3352e50eb959a1961ce GIT binary patch literal 4429 zcmeH|c{J4h9>;%UjKPEkgT^w}OtKV)WP6aw@E}XJETIW`>@;L6Q642F@{B!1i4a4n zWErwF${uPkD6-91Ct~h&?>)~w=l*q<|L*PcJ@3yy-{rhtpU>xbMmqzt+Z2FU^(a1a;{ zVvzw90DvHDw?7R3JwO~_PPR*K9$pyx0BJwK0Rn?L*#6l7?9;LAeSi}V5tKP<#3f|o z36%{IR=;xRKDV54O$)-dk1ns_6`H`qDhBCubM$bNKT6zKjU*_h&GFI2tH#WDJ-?o2nfdKH| zu>K_bCoVXfi-X-3PUsIV5C?%RFq{)2bCgTa$Oh^eA|$JRgIZo zk5@!qb4r2n1MRP5{~B1re?;~t*gv?&fPG*PyLn(ZUPqkLVWP)28`jP&E{E!!LsxnfBVBT3rsZ0|3)(`C_{SgDy;Jzbz!X0^B%3t{K08x zkJU%2<6hI!MVXP(lHyqRHzhW%Y3&p6ulXW$)80o#e9~Xte+wCZ@+d;`vjC z<*3!qBlGSR`=>WWR4gc+ixAJalid-n)S`_h1<7X6Ht}PJMF!fbLN5oe5&dYff;47* zUjX8}EdI?L}(3M5({s@<&e#8|D7x@M z7EJbIw~!h-HCg3Fuwt+~hs4N(c@JnD((h8gzlgg~-K|v}&SZ-Gt>oqz1lH>+v=oX6 z>hOSlhfMQX>{yTuBokKGeC>|+$Xhjscco?>lL+%LaZH)FMjUu#ks0l{p9G;ShftgM!P4;&|*TI$tsYrlT^ zrKIeH<4vdXfcjS`gHd*6{;WIy&#= zH@vdwF_f+rRI&8%ySHJB28uA3RK;V@EitinDXA6XnZpeiw;ENmw2BjRm9Vz8Pa6V) zqL=ngX2%Vb?GV~u3&ilgK7IjN{p`85*b-OZVV zs7*0F{mB*n$au%0jaWG=t*x=QO#$CKq9nRUy$guR?rzUq1v>4g*0ztiO8D8@+NJ84 z>Kh!Q=;%^e0548|r7uuta@Mvuqw@T~H-$1<3=5!pw8jZD6vnjn5ZLXc!0 zF)6M)*yGD8qi)Hxlk;p;W-5KcwLeCI4kPa!9M*JJQ_(Z26h~_^ef-1m+)jM+-DTU& zAtQmuk=qt+`Cxk4^%zY26h+*8N0s>S0Nd6|oY(xf4M`B|Sb>P_#< zxwfRcbxm8VV-xf@J@G>8ZpMR5!?xG--`UyGajQn$dXbpzW@IE|-=n5u>$)E&pE*hQ zf3L@bFN&O)-Xgq!I z)=9{S(vmwAuF5by##b(56Jb9y>88+%V(Cy|61kOjnFR>Q(+%6Q-jnVJL=fUNvqTwJ zn$+@)zs02e5pYUCtV_R~nn@>2m1b2Qtiey|nytv)HoMtxHg}WX$o@R~$fdI(&a}u{ z0SmaZUBDQw19vD4-EOK#kb3lD;l=ylo-b(ZF6oTR@@_$yNjbgp!S3~>^~aJ9vwUlf&j*ng7}x~lhrWv`X>qa_9W*`gFBE@2x};HQ4p;O7kdf6qWX GYxFNZ3vhY> literal 0 HcmV?d00001 diff --git a/packages/stream_chat/test/src/core/api/attachment_file_uploader_test.dart b/packages/stream_chat/test/src/core/api/attachment_file_uploader_test.dart new file mode 100644 index 00000000..27f4ce6a --- /dev/null +++ b/packages/stream_chat/test/src/core/api/attachment_file_uploader_test.dart @@ -0,0 +1,136 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; +import 'package:stream_chat/src/core/models/attachment_file.dart'; +import 'package:test/test.dart'; + +import '../../fakes.dart'; +import '../../matchers.dart'; +import '../../mocks.dart'; +import '../../utils.dart'; + +void main() { + late final client = MockHttpClient(); + late StreamAttachmentFileUploader fileUploader; + + setUp(() { + fileUploader = StreamAttachmentFileUploader(client); + registerFallbackValue(FakeMultiPartFile()); + }); + + Response successResponse(String path, {Object? data}) => Response( + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); + + test('sendImage', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + + const path = '/channels/$channelType/$channelId/image'; + final file = assetFile('test_image.jpeg'); + final attachmentFile = AttachmentFile( + size: 333, + path: file.path, + bytes: file.readAsBytesSync(), + ); + final multipartFile = await attachmentFile.toMultipartFile(); + + when(() => client.postFile( + path, + any(that: isSameMultipartFileAs(multipartFile)), + )).thenAnswer((_) async => successResponse(path, data: { + 'file': 'test-file-url', + })); + + final res = await fileUploader.sendImage( + attachmentFile, + channelId, + channelType, + ); + + expect(res, isNotNull); + expect(res.file, isNotNull); + expect(res.file, isNotEmpty); + + verify(() => client.postFile( + path, + any(that: isSameMultipartFileAs(multipartFile)), + )).called(1); + verifyNoMoreInteractions(client); + }); + + test('sendFile', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + + const path = '/channels/$channelType/$channelId/file'; + final file = assetFile('example.pdf'); + final attachmentFile = AttachmentFile( + size: 333, + path: file.path, + bytes: file.readAsBytesSync(), + ); + final multipartFile = await attachmentFile.toMultipartFile(); + + when(() => client.postFile( + path, + any(that: isSameMultipartFileAs(multipartFile)), + )).thenAnswer((_) async => successResponse(path, data: { + 'file': 'test-file-url', + })); + + final res = await fileUploader.sendFile( + attachmentFile, + channelId, + channelType, + ); + + expect(res, isNotNull); + expect(res.file, isNotNull); + expect(res.file, isNotEmpty); + + verify(() => client.postFile( + path, + any(that: isSameMultipartFileAs(multipartFile)), + )).called(1); + verifyNoMoreInteractions(client); + }); + + test('deleteImage', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const path = '/channels/$channelType/$channelId/image'; + + const url = 'test-image-url'; + + when(() => client.delete(path, queryParameters: {'url': url})).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await fileUploader.deleteImage(url, channelId, channelType); + + expect(res, isNotNull); + + verify(() => client.delete(path, queryParameters: {'url': url})).called(1); + verifyNoMoreInteractions(client); + }); + + test('deleteFile', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const path = '/channels/$channelType/$channelId/file'; + + const url = 'test-file-url'; + + when(() => client.delete(path, queryParameters: {'url': url})).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await fileUploader.deleteFile(url, channelId, channelType); + + expect(res, isNotNull); + + verify(() => client.delete(path, queryParameters: {'url': url})).called(1); + verifyNoMoreInteractions(client); + }); +} diff --git a/packages/stream_chat/test/src/core/api/channel_api_test.dart b/packages/stream_chat/test/src/core/api/channel_api_test.dart new file mode 100644 index 00000000..8d5f5e76 --- /dev/null +++ b/packages/stream_chat/test/src/core/api/channel_api_test.dart @@ -0,0 +1,492 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/channel_api.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/channel_state.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; + +import '../../mocks.dart'; + +void main() { + String _getChannelUrl(String channelId, String channelType) => + '/channels/$channelType/$channelId'; + + ChannelState _generateChannelState( + String channelId, + String channelType, + ) { + final channel = ChannelModel(id: channelId, type: channelType); + final messages = List.generate( + 3, + (index) => Message( + id: 'test-message-id-$index', + text: 'test-message-text-$index', + ), + ); + final members = List.generate( + 3, + (index) => Member(userId: 'test-user-id-$index'), + ); + final reads = List.generate( + 3, + (index) => Read( + lastRead: DateTime.now(), + user: User(id: 'test-user-id-$index'), + ), + ); + final watchers = List.generate( + 3, + (index) => User(id: 'test-user-id-$index'), + ); + final state = ChannelState( + channel: channel, + messages: messages, + pinnedMessages: messages, + members: members, + read: reads, + watchers: watchers, + watcherCount: watchers.length, + ); + return state; + } + + Response successResponse(String path, {Object? data}) => Response( + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); + + late final client = MockHttpClient(); + late ChannelApi channelApi; + + setUp(() { + channelApi = ChannelApi(client); + }); + + test('queryChannel', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const channelData = {'name': 'test-channel'}; + const messagePagination = PaginationParams(); + const membersPagination = PaginationParams(); + const watchersPagination = PaginationParams(); + + const channelPath = '/channels/$channelType/$channelId'; + const path = '$channelPath/query'; + + final channelState = _generateChannelState(channelId, channelType); + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse( + path, + data: channelState.toJson(), + )); + + final res = await channelApi.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + messagesPagination: messagePagination, + membersPagination: membersPagination, + watchersPagination: watchersPagination, + ); + + expect(res, isNotNull); + expect(res.messages.length, channelState.messages.length); + expect(res.pinnedMessages.length, channelState.pinnedMessages.length); + expect(res.members.length, channelState.members.length); + expect(res.read.length, channelState.read.length); + expect(res.watchers.length, channelState.watchers.length); + expect(res.watcherCount, channelState.watcherCount); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('queryChannels', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + + final filter = Filter.in_('cid', const ['test-cid']); + const sort = [SortOption('test-field')]; + const memberLimit = 33; + const messageLimit = 33; + + const path = '/channels'; + + final channelState = _generateChannelState(channelId, channelType); + when(() => client.get(path, queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => successResponse( + path, + data: { + 'channels': [channelState.toJson()] + }, + )); + + final res = await channelApi.queryChannels( + filter: filter, + sort: sort, + memberLimit: memberLimit, + messageLimit: messageLimit, + ); + + expect(res, isNotNull); + expect(res.channels, isNotEmpty); + + verify( + () => client.get(path, queryParameters: any(named: 'queryParameters')), + ).called(1); + verifyNoMoreInteractions(client); + }); + + test('markAllRead', () async { + const path = 'channels/read'; + when(() => client.post(path)).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await channelApi.markAllRead(); + + expect(res, isNotNull); + + verify(() => client.post(path)).called(1); + verifyNoMoreInteractions(client); + }); + + test('updateChannel', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const data = {'name': 'test-channel-name'}; + final message = Message(id: 'test-message-id', text: 'channel-updated'); + + final path = _getChannelUrl(channelId, channelType); + + final channelModel = ChannelModel( + id: channelId, + type: channelType, + extraData: data, + ); + + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'channel': channelModel.toJson(), + 'message': message.toJson(), + })); + + final res = await channelApi.updateChannel( + channelId, + channelType, + data, + message: message, + ); + + expect(res, isNotNull); + expect(res.channel.cid, channelModel.cid); + expect(res.message?.id, message.id); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('updateChannelPartial', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const data = {'name': 'test-channel-name'}; + + final path = _getChannelUrl(channelId, channelType); + + final channelModel = ChannelModel( + id: channelId, + type: channelType, + extraData: data, + ); + + when(() => client.patch(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'channel': channelModel.toJson(), + })); + + final res = await channelApi.updateChannelPartial( + channelId, + channelType, + data, + ); + + expect(res, isNotNull); + + verify(() => client.patch(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('acceptChannelInvite', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + final message = Message(id: 'test-message-id', text: 'channel-accepted'); + + final channelModel = ChannelModel(id: channelId, type: channelType); + + final path = _getChannelUrl(channelId, channelType); + + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'channel': channelModel.toJson(), + 'message': message.toJson(), + })); + + final res = await channelApi.acceptChannelInvite( + channelId, + channelType, + message: message, + ); + + expect(res, isNotNull); + expect(res.channel.cid, channelModel.cid); + expect(res.message?.id, message.id); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('rejectChannelInvite', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + final message = Message(id: 'test-message-id', text: 'channel-rejected'); + + final channelModel = ChannelModel(id: channelId, type: channelType); + + final path = _getChannelUrl(channelId, channelType); + + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'channel': channelModel.toJson(), + 'message': message.toJson(), + })); + + final res = await channelApi.rejectChannelInvite( + channelId, + channelType, + message: message, + ); + + expect(res, isNotNull); + expect(res.channel.cid, channelModel.cid); + expect(res.message?.id, message.id); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('inviteChannelMembers', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const memberIds = ['test-member-id-1', 'test-member-id-2']; + final channelModel = ChannelModel(id: channelId, type: channelType); + final message = Message(id: 'test-message-id', text: 'members-invited'); + + final path = _getChannelUrl(channelId, channelType); + + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'channel': channelModel.toJson(), + 'message': message.toJson(), + })); + + final res = await channelApi.inviteChannelMembers( + channelId, + channelType, + memberIds, + message: message, + ); + + expect(res, isNotNull); + expect(res.channel.cid, channelModel.cid); + expect(res.message?.id, message.id); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('addMembers', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const memberIds = ['test-member-id-1', 'test-member-id-2']; + final channelModel = ChannelModel(id: channelId, type: channelType); + final message = Message(id: 'test-message-id', text: 'members-added'); + + final path = _getChannelUrl(channelId, channelType); + + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'channel': channelModel.toJson(), + 'message': message.toJson(), + })); + + final res = await channelApi.addMembers( + channelId, + channelType, + memberIds, + message: message, + ); + + expect(res, isNotNull); + expect(res.channel.cid, channelModel.cid); + expect(res.message?.id, message.id); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('removeMembers', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const memberIds = ['test-member-id-1', 'test-member-id-2']; + final channelModel = ChannelModel(id: channelId, type: channelType); + final message = Message(id: 'test-message-id', text: 'members-removed'); + + final path = _getChannelUrl(channelId, channelType); + + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'channel': channelModel.toJson(), + 'message': message.toJson(), + })); + + final res = await channelApi.removeMembers( + channelId, + channelType, + memberIds, + message: message, + ); + + expect(res, isNotNull); + expect(res.channel.cid, channelModel.cid); + expect(res.message?.id, message.id); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('sendEvent', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + final event = Event(type: 'event.test'); + + final path = '${_getChannelUrl(channelId, channelType)}/event'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await channelApi.sendEvent(channelId, channelType, event); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('deleteChannel', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + + final path = _getChannelUrl(channelId, channelType); + + when(() => client.delete(path)).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await channelApi.deleteChannel(channelId, channelType); + + expect(res, isNotNull); + + verify(() => client.delete(path)).called(1); + verifyNoMoreInteractions(client); + }); + + test('truncateChannel', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + + final path = '${_getChannelUrl(channelId, channelType)}/truncate'; + + when(() => client.post(path)).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await channelApi.truncateChannel(channelId, channelType); + + expect(res, isNotNull); + + verify(() => client.post(path)).called(1); + verifyNoMoreInteractions(client); + }); + + test('hideChannel', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + + final path = '${_getChannelUrl(channelId, channelType)}/hide'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await channelApi.hideChannel(channelId, channelType); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('showChannel', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + + final path = '${_getChannelUrl(channelId, channelType)}/show'; + + when(() => client.post(path)).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await channelApi.showChannel(channelId, channelType); + + expect(res, isNotNull); + + verify(() => client.post(path)).called(1); + verifyNoMoreInteractions(client); + }); + + test('markRead', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const messageId = 'test-message-id'; + + final path = '${_getChannelUrl(channelId, channelType)}/read'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await channelApi.markRead( + channelId, + channelType, + messageId: messageId, + ); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('stopWatching', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + + final path = '${_getChannelUrl(channelId, channelType)}/stop-watching'; + + when(() => client.post(path)).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await channelApi.stopWatching(channelId, channelType); + + expect(res, isNotNull); + + verify(() => client.post(path)).called(1); + verifyNoMoreInteractions(client); + }); +} diff --git a/packages/stream_chat/test/src/core/api/device_api_test.dart b/packages/stream_chat/test/src/core/api/device_api_test.dart new file mode 100644 index 00000000..f12f04f5 --- /dev/null +++ b/packages/stream_chat/test/src/core/api/device_api_test.dart @@ -0,0 +1,84 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/device_api.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; + +import '../../mocks.dart'; + +void main() { + Response successResponse(String path, {Object? data}) => Response( + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); + + late final client = MockHttpClient(); + late DeviceApi deviceApi; + + setUp(() { + deviceApi = DeviceApi(client); + }); + + test('addDevice', () async { + const deviceId = 'test-device-id'; + const pushProvider = PushProvider.firebase; + + const path = '/devices'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await deviceApi.addDevice(deviceId, pushProvider); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('getDevices', () async { + const path = '/devices'; + + final devices = List.generate( + 3, + (index) => Device( + id: 'test-device-id-$index', + pushProvider: PushProvider.firebase.name, + ), + ); + + when(() => client.get(path)).thenAnswer( + (_) async => successResponse(path, data: { + 'devices': [...devices.map((it) => it.toJson())] + }), + ); + + final res = await deviceApi.getDevices(); + + expect(res, isNotNull); + expect(res.devices.length, devices.length); + + verify(() => client.get(path)).called(1); + verifyNoMoreInteractions(client); + }); + + test('removeDevice', () async { + const deviceId = 'test-device-id'; + + const path = '/devices'; + + when( + () => client.delete(path, queryParameters: any(named: 'queryParameters')), + ).thenAnswer((_) async => successResponse(path, data: {})); + + final res = await deviceApi.removeDevice(deviceId); + + expect(res, isNotNull); + + verify( + () => client.delete(path, queryParameters: any(named: 'queryParameters')), + ).called(1); + verifyNoMoreInteractions(client); + }); +} diff --git a/packages/stream_chat/test/src/core/api/general_api_test.dart b/packages/stream_chat/test/src/core/api/general_api_test.dart new file mode 100644 index 00000000..8844262f --- /dev/null +++ b/packages/stream_chat/test/src/core/api/general_api_test.dart @@ -0,0 +1,211 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/general_api.dart'; +import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/filter.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; + +import '../../mocks.dart'; + +void main() { + Response successResponse(String path, {Object? data}) => Response( + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); + + late final client = MockHttpClient(); + late GeneralApi generalApi; + + setUp(() { + generalApi = GeneralApi(client); + }); + + test('sync', () async { + const cids = ['test-cid-1', 'test-cid-2', 'test-cid-3']; + final lastSyncAt = DateTime.now(); + + const path = '/sync'; + + final events = + List.generate(3, (index) => Event(type: 'test-event-type-$index')); + + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'events': [...events.map((it) => it.toJson())] + })); + + final res = await generalApi.sync(cids, lastSyncAt); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + group('searchMessages', () { + test( + 'should throw if `query` and `messageFilters` is not provided', + () async { + final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); + try { + await generalApi.searchMessages(filter); + } catch (e) { + expect(e, isA()); + } + }, + ); + + test( + 'should throw if `query` and `messageFilters` both are provided', + () async { + final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); + const query = 'test-query'; + final messageFilter = Filter.query('key', 'text'); + try { + await generalApi.searchMessages( + filter, + query: query, + messageFilters: messageFilter, + ); + } catch (e) { + expect(e, isA()); + } + }, + ); + + test('should run successfully with `query`', () async { + final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); + const query = 'test-query'; + const sort = [SortOption('test-field')]; + const pagination = PaginationParams(); + + const path = '/search'; + + when( + () => client.get(path, queryParameters: any(named: 'queryParameters')), + ).thenAnswer((_) async => successResponse(path, data: {'results': []})); + + final res = await generalApi.searchMessages( + filter, + query: query, + sort: sort, + pagination: pagination, + ); + + expect(res, isNotNull); + expect(res.results, isEmpty); + + verify( + () => client.get(path, queryParameters: any(named: 'queryParameters')), + ).called(1); + verifyNoMoreInteractions(client); + }); + + test('should run successfully with `messageFilter`', () async { + final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); + const sort = [SortOption('test-field')]; + final messageFilter = Filter.query('key', 'text'); + const pagination = PaginationParams(); + + const path = '/search'; + + when( + () => client.get(path, queryParameters: any(named: 'queryParameters')), + ).thenAnswer((_) async => successResponse(path, data: {'results': []})); + + final res = await generalApi.searchMessages( + filter, + messageFilters: messageFilter, + sort: sort, + pagination: pagination, + ); + + expect(res, isNotNull); + expect(res.results, isEmpty); + + verify( + () => client.get(path, queryParameters: any(named: 'queryParameters')), + ).called(1); + verifyNoMoreInteractions(client); + }); + }); + + group('queryMembers', () { + test('with `channelId`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); + const pagination = PaginationParams(); + const sort = [SortOption('test-field')]; + + const path = '/members'; + + final members = List.generate( + 3, + (index) => Member(userId: 'test-user-id=$index'), + ); + + when(() => + client.get(path, queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => successResponse(path, data: { + 'members': [...members.map((it) => it.toJson())] + })); + + final res = await generalApi.queryMembers( + channelType, + channelId: channelId, + filter: filter, + pagination: pagination, + sort: sort, + ); + + expect(res, isNotNull); + expect(res.members.length, members.length); + + verify( + () => client.get(path, queryParameters: any(named: 'queryParameters')), + ).called(1); + verifyNoMoreInteractions(client); + }); + + test('with `members`', () async { + const channelType = 'test-channel-type'; + final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); + const pagination = PaginationParams(); + const sort = [SortOption('test-field')]; + + const path = '/members'; + + final members = List.generate( + 3, + (index) => Member(userId: 'test-user-id=$index'), + ); + + when(() => + client.get(path, queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => successResponse(path, data: { + 'members': [...members.map((it) => it.toJson())] + })); + + final res = await generalApi.queryMembers( + channelType, + filter: filter, + pagination: pagination, + sort: sort, + members: members, + ); + + expect(res, isNotNull); + expect(res.members.length, members.length); + + verify( + () => client.get(path, queryParameters: any(named: 'queryParameters')), + ).called(1); + verifyNoMoreInteractions(client); + }); + }); +} diff --git a/packages/stream_chat/test/src/core/api/guest_api_test.dart b/packages/stream_chat/test/src/core/api/guest_api_test.dart new file mode 100644 index 00000000..6c6386fa --- /dev/null +++ b/packages/stream_chat/test/src/core/api/guest_api_test.dart @@ -0,0 +1,44 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/guest_api.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; + +import '../../mocks.dart'; + +void main() { + Response successResponse(String path, {Object? data}) => Response( + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); + + late final client = MockHttpClient(); + late GuestApi guestApi; + + setUp(() { + guestApi = GuestApi(client); + }); + + test('getGuestUser', () async { + const accessToken = 'test-guest-token'; + final user = User(id: 'test-user-id'); + + const path = '/guest'; + + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'access_token': accessToken, + 'user': user.toJson(), + })); + + final res = await guestApi.getGuestUser(user); + + expect(res, isNotNull); + expect(res.accessToken, accessToken); + expect(res.user.id, user.id); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); +} diff --git a/packages/stream_chat/test/src/core/api/message_api_test.dart b/packages/stream_chat/test/src/core/api/message_api_test.dart new file mode 100644 index 00000000..59319eca --- /dev/null +++ b/packages/stream_chat/test/src/core/api/message_api_test.dart @@ -0,0 +1,291 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/message_api.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; + +import '../../mocks.dart'; + +void main() { + Response successResponse(String path, {Object? data}) => Response( + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); + + late final client = MockHttpClient(); + late MessageApi messageApi; + + setUp(() { + messageApi = MessageApi(client); + }); + + test('sendMessage', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + final message = Message(id: 'test-message-id', text: 'test-message-text'); + + const path = '/channels/$channelType/$channelId/message'; + + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'message': message.toJson(), + })); + + final res = await messageApi.sendMessage(channelId, channelType, message); + + expect(res, isNotNull); + expect(res.message.id, message.id); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('getMessagesById', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const messageIds = ['test-message-id-1', 'test-message-id-2']; + + const path = '/channels/$channelType/$channelId/messages'; + + final messages = List.generate( + 3, + (index) => Message(id: 'test-message-id-$index'), + ); + + when(() => client.get(path, queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => successResponse(path, data: { + 'messages': [...messages.map((it) => it.toJson())], + })); + + final res = await messageApi.getMessagesById( + channelId, + channelType, + messageIds, + ); + + expect(res, isNotNull); + expect(res.messages.length, messages.length); + + verify( + () => client.get(path, queryParameters: any(named: 'queryParameters')), + ).called(1); + verifyNoMoreInteractions(client); + }); + + test('getMessage', () async { + const messageId = 'test-message-id'; + + const path = '/messages/$messageId'; + + final message = Message(id: messageId); + + when(() => client.get(path)).thenAnswer((_) async => + successResponse(path, data: {'message': message.toJson()})); + + final res = await messageApi.getMessage(messageId); + + expect(res, isNotNull); + expect(res.message.id, messageId); + + verify(() => client.get(path)).called(1); + verifyNoMoreInteractions(client); + }); + + test('updateMessage', () async { + final message = Message(id: 'test-message-id'); + + final path = '/messages/${message.id}'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {'message': message.toJson()}), + ); + + final res = await messageApi.updateMessage(message); + + expect(res, isNotNull); + expect(res.message.id, message.id); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('deleteMessage', () async { + const messageId = 'test-message-id'; + + const path = '/messages/$messageId'; + + when(() => client.delete(path)).thenAnswer( + (_) async => successResponse(path, data: {}), + ); + + final res = await messageApi.deleteMessage(messageId); + + expect(res, isNotNull); + + verify(() => client.delete(path)).called(1); + verifyNoMoreInteractions(client); + }); + + test('sendAction', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const messageId = 'test-message-id'; + const formData = {'test-key': 'test-data'}; + + const path = '/messages/$messageId/action'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await messageApi.sendAction( + channelId, + channelType, + messageId, + formData, + ); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('sendReaction', () async { + const messageId = 'test-message-id'; + const reactionType = 'test-reaction-type'; + const extraData = {'test-key': 'test-data'}; + + const path = '/messages/$messageId/reaction'; + + final message = Message(id: messageId); + final reaction = Reaction(type: reactionType, messageId: messageId); + + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'message': message.toJson(), + 'reaction': reaction.toJson(), + })); + + final res = await messageApi.sendReaction( + messageId, + reactionType, + extraData: extraData, + ); + + expect(res, isNotNull); + expect(res.message.id, messageId); + expect(res.reaction.messageId, messageId); + expect(res.reaction.type, reactionType); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('deleteReaction', () async { + const messageId = 'test-message-id'; + const reactionType = 'test-reaction-type'; + + const path = '/messages/$messageId/reaction/$reactionType'; + + when(() => client.delete(path)).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await messageApi.deleteReaction(messageId, reactionType); + + expect(res, isNotNull); + + verify(() => client.delete(path)).called(1); + verifyNoMoreInteractions(client); + }); + + test('getReactions', () async { + const messageId = 'test-message-id'; + const options = PaginationParams(); + + const path = '/messages/$messageId/reactions'; + + final reactions = List.generate( + 3, + (index) => Reaction( + type: 'test-reaction-type-$index', + messageId: messageId, + ), + ); + + when(() => client.get(path, queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => successResponse(path, data: { + 'reactions': [...reactions.map((it) => it.toJson())] + })); + + final res = await messageApi.getReactions(messageId, options); + + expect(res, isNotNull); + expect(res.reactions.length, reactions.length); + expect(res.reactions.every((it) => it.messageId == messageId), isTrue); + + verify( + () => client.get(path, queryParameters: any(named: 'queryParameters')), + ).called(1); + verifyNoMoreInteractions(client); + }); + + test('translateMessage', () async { + const messageId = 'test-message-id'; + const messageText = 'hello'; + const language = 'hi'; // Hindi + final message = Message(id: messageId, text: messageText); + + final path = '/messages/${message.id}/translate'; + + const translatedMessageText = 'नमस्ते'; + final translatedMessage = TranslatedMessage(const { + language: translatedMessageText, + }); + + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'message': translatedMessage.toJson(), + })); + + final res = await messageApi.translateMessage(messageId, language); + + expect(res, isNotNull); + expect(res.message.i18n?.containsKey(language), isTrue); + expect(res.message.i18n?[language], translatedMessageText); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('getReplies', () async { + const parentId = 'test-parent-id'; + const options = PaginationParams(); + + const path = '/messages/$parentId/replies'; + + final messages = List.generate( + 3, + (index) => Message( + id: 'test-message-id-$index', + parentId: parentId, + ), + ); + + when(() => client.get(path, queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => successResponse(path, data: { + 'messages': [...messages.map((it) => it.toJson())] + })); + + final res = await messageApi.getReplies(parentId, options); + + expect(res, isNotNull); + expect(res.messages.length, messages.length); + expect(res.messages.every((it) => it.parentId == parentId), isTrue); + + verify( + () => client.get(path, queryParameters: any(named: 'queryParameters')), + ).called(1); + verifyNoMoreInteractions(client); + }); +} diff --git a/packages/stream_chat/test/src/core/api/moderation_api_test.dart b/packages/stream_chat/test/src/core/api/moderation_api_test.dart new file mode 100644 index 00000000..fb2a1f29 --- /dev/null +++ b/packages/stream_chat/test/src/core/api/moderation_api_test.dart @@ -0,0 +1,190 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/moderation_api.dart'; +import 'package:test/test.dart'; + +import '../../mocks.dart'; + +void main() { + Response successResponse(String path, {Object? data}) => Response( + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); + + late final client = MockHttpClient(); + late ModerationApi moderationApi; + + setUp(() { + moderationApi = ModerationApi(client); + }); + + test('muteUser', () async { + const userId = 'test-user-id'; + + const path = '/moderation/mute'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await moderationApi.muteUser(userId); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('unmuteUser', () async { + const userId = 'test-user-id'; + + const path = '/moderation/unmute'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await moderationApi.unmuteUser(userId); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('muteChannel', () async { + const channelCid = 'test-channel-cid'; + const expiration = Duration(days: 3); + + const path = '/moderation/mute/channel'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await moderationApi.muteChannel( + channelCid, + expiration: expiration, + ); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('unmuteChannel', () async { + const channelCid = 'test-channel-cid'; + + const path = '/moderation/unmute/channel'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await moderationApi.unmuteChannel(channelCid); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('flagMessage', () async { + const messageId = 'test-message-id'; + + const path = '/moderation/flag'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await moderationApi.flagMessage(messageId); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('unflagMessage', () async { + const messageId = 'test-message-id'; + + const path = '/moderation/unflag'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await moderationApi.unflagMessage(messageId); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('flagUser', () async { + const userId = 'test-message-id'; + + const path = '/moderation/flag'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await moderationApi.flagUser(userId); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('unflagUser', () async { + const userId = 'test-message-id'; + + const path = '/moderation/unflag'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await moderationApi.unflagUser(userId); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('banUser', () async { + const targetUserId = 'test-target-user-id'; + const options = {'key': 'value'}; + + const path = '/moderation/ban'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await moderationApi.banUser(targetUserId, options: options); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('unbanUser', () async { + const targetUserId = 'test-target-user-id'; + const options = {'key': 'value'}; + + const path = '/moderation/ban'; + + when( + () => client.delete(path, queryParameters: any(named: 'queryParameters')), + ).thenAnswer((_) async => successResponse(path, data: {})); + + final res = await moderationApi.unbanUser(targetUserId, options: options); + + expect(res, isNotNull); + + verify( + () => client.delete(path, queryParameters: any(named: 'queryParameters')), + ).called(1); + verifyNoMoreInteractions(client); + }); +} diff --git a/packages/stream_chat/test/src/core/api/stream_chat_api_test.dart b/packages/stream_chat/test/src/core/api/stream_chat_api_test.dart new file mode 100644 index 00000000..f61746cd --- /dev/null +++ b/packages/stream_chat/test/src/core/api/stream_chat_api_test.dart @@ -0,0 +1,49 @@ +import 'package:stream_chat/src/core/api/stream_chat_api.dart'; +import 'package:test/test.dart'; + +import '../../mocks.dart'; + +void main() { + const apiKey = 'test-api-key'; + late final client = MockHttpClient(); + late StreamChatApi streamChatApi; + + setUp(() { + streamChatApi = StreamChatApi( + apiKey, + client: client, + ); + }); + + test('`.user`', () { + expect(streamChatApi.user, isNotNull); + }); + + test('`.guest`', () { + expect(streamChatApi.guest, isNotNull); + }); + + test('`.message`', () { + expect(streamChatApi.message, isNotNull); + }); + + test('`.channel`', () { + expect(streamChatApi.channel, isNotNull); + }); + + test('`.device`', () { + expect(streamChatApi.device, isNotNull); + }); + + test('`.moderation`', () { + expect(streamChatApi.moderation, isNotNull); + }); + + test('`.general`', () { + expect(streamChatApi.general, isNotNull); + }); + + test('`.fileUploader`', () { + expect(streamChatApi.fileUploader, isNotNull); + }); +} diff --git a/packages/stream_chat/test/src/core/api/user_api_test.dart b/packages/stream_chat/test/src/core/api/user_api_test.dart new file mode 100644 index 00000000..ebf0e343 --- /dev/null +++ b/packages/stream_chat/test/src/core/api/user_api_test.dart @@ -0,0 +1,77 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/api/user_api.dart'; +import 'package:stream_chat/src/core/models/filter.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; + +import '../../mocks.dart'; + +void main() { + Response successResponse(String path, {Object? data}) => Response( + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); + + late final client = MockHttpClient(); + late UserApi userApi; + + setUp(() { + userApi = UserApi(client); + }); + + test('queryUsers', () async { + const presence = true; + final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); + const sort = [SortOption('test-field')]; + const pagination = PaginationParams(); + + const path = '/users'; + + final users = List.generate(3, (index) => User(id: 'test-user-id-$index')); + + when(() => client.get(path, queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => successResponse(path, data: { + 'users': [...users.map((it) => it.toJson())] + })); + + final res = await userApi.queryUsers( + presence: presence, + filter: filter, + sort: sort, + pagination: pagination, + ); + + expect(res, isNotNull); + expect(res.users.length, users.length); + + verify( + () => client.get(path, queryParameters: any(named: 'queryParameters')), + ).called(1); + verifyNoMoreInteractions(client); + }); + + test('updateUsers', () async { + final users = List.generate(3, (index) => User(id: 'test-user-id-$index')); + + const path = '/users'; + + final updatedUsers = {for (final user in users) user.id: user}; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: { + 'users': updatedUsers + .map((key, value) => MapEntry(key, value.toJson())) + })); + + final res = await userApi.updateUsers(users); + + expect(res, isNotNull); + expect(res.users.length, updatedUsers.length); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); +} diff --git a/packages/stream_chat/test/src/core/http/interceptor/connection_id_interceptor.dart b/packages/stream_chat/test/src/core/http/interceptor/connection_id_interceptor_test.dart similarity index 100% rename from packages/stream_chat/test/src/core/http/interceptor/connection_id_interceptor.dart rename to packages/stream_chat/test/src/core/http/interceptor/connection_id_interceptor_test.dart diff --git a/packages/stream_chat/test/src/models/action_test.dart b/packages/stream_chat/test/src/core/models/action_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/action_test.dart rename to packages/stream_chat/test/src/core/models/action_test.dart diff --git a/packages/stream_chat/test/src/models/attachment_test.dart b/packages/stream_chat/test/src/core/models/attachment_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/attachment_test.dart rename to packages/stream_chat/test/src/core/models/attachment_test.dart diff --git a/packages/stream_chat/test/src/models/channel_state_test.dart b/packages/stream_chat/test/src/core/models/channel_state_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/channel_state_test.dart rename to packages/stream_chat/test/src/core/models/channel_state_test.dart diff --git a/packages/stream_chat/test/src/models/channel_test.dart b/packages/stream_chat/test/src/core/models/channel_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/channel_test.dart rename to packages/stream_chat/test/src/core/models/channel_test.dart diff --git a/packages/stream_chat/test/src/models/command_test.dart b/packages/stream_chat/test/src/core/models/command_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/command_test.dart rename to packages/stream_chat/test/src/core/models/command_test.dart diff --git a/packages/stream_chat/test/src/models/device_test.dart b/packages/stream_chat/test/src/core/models/device_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/device_test.dart rename to packages/stream_chat/test/src/core/models/device_test.dart diff --git a/packages/stream_chat/test/src/models/event_test.dart b/packages/stream_chat/test/src/core/models/event_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/event_test.dart rename to packages/stream_chat/test/src/core/models/event_test.dart diff --git a/packages/stream_chat/test/src/models/filter_test.dart b/packages/stream_chat/test/src/core/models/filter_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/filter_test.dart rename to packages/stream_chat/test/src/core/models/filter_test.dart diff --git a/packages/stream_chat/test/src/models/member_test.dart b/packages/stream_chat/test/src/core/models/member_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/member_test.dart rename to packages/stream_chat/test/src/core/models/member_test.dart diff --git a/packages/stream_chat/test/src/models/message_test.dart b/packages/stream_chat/test/src/core/models/message_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/message_test.dart rename to packages/stream_chat/test/src/core/models/message_test.dart diff --git a/packages/stream_chat/test/src/models/reaction_test.dart b/packages/stream_chat/test/src/core/models/reaction_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/reaction_test.dart rename to packages/stream_chat/test/src/core/models/reaction_test.dart diff --git a/packages/stream_chat/test/src/models/read_test.dart b/packages/stream_chat/test/src/core/models/read_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/read_test.dart rename to packages/stream_chat/test/src/core/models/read_test.dart diff --git a/packages/stream_chat/test/src/models/serialization_test.dart b/packages/stream_chat/test/src/core/models/serialization_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/serialization_test.dart rename to packages/stream_chat/test/src/core/models/serialization_test.dart diff --git a/packages/stream_chat/test/src/models/user_test.dart b/packages/stream_chat/test/src/core/models/user_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/user_test.dart rename to packages/stream_chat/test/src/core/models/user_test.dart diff --git a/packages/stream_chat/test/src/fakes.dart b/packages/stream_chat/test/src/fakes.dart index e96cbd12..b7e431b1 100644 --- a/packages/stream_chat/test/src/fakes.dart +++ b/packages/stream_chat/test/src/fakes.dart @@ -1,6 +1,18 @@ +import 'package:dio/dio.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/channel_api.dart'; +import 'package:stream_chat/src/core/api/device_api.dart'; +import 'package:stream_chat/src/core/api/general_api.dart'; +import 'package:stream_chat/src/core/api/message_api.dart'; +import 'package:stream_chat/src/core/api/moderation_api.dart'; +import 'package:stream_chat/src/core/api/stream_chat_api.dart'; +import 'package:stream_chat/src/core/api/user_api.dart'; +import 'package:stream_chat/src/core/api/guest_api.dart'; import 'package:stream_chat/src/core/http/token.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; +import 'package:stream_chat/stream_chat.dart'; + +import 'mocks.dart'; class FakeTokenManager extends Fake implements TokenManager { final token = Token.development('test-user-id'); @@ -25,3 +37,48 @@ class FakeTokenManager extends Fake implements TokenManager { @override void reset() {} } + +class FakeMultiPartFile extends Fake implements MultipartFile {} + +class FakeChatApi extends Fake implements StreamChatApi { + UserApi? _user; + + @override + UserApi get user => _user ??= MockUserApi(); + + GuestApi? _guest; + + @override + GuestApi get guest => _guest ??= MockGuestApi(); + + MessageApi? _message; + + @override + MessageApi get message => _message ??= MockMessageApi(); + + ChannelApi? _channel; + + @override + ChannelApi get channel => _channel ??= MockChannelApi(); + + DeviceApi? _device; + + @override + DeviceApi get device => _device ??= MockDeviceApi(); + + ModerationApi? _moderation; + + @override + ModerationApi get moderation => _moderation ??= MockModerationApi(); + + GeneralApi? _general; + + @override + GeneralApi get general => _general ??= MockGeneralApi(); + + AttachmentFileUploader? _fileUploader; + + @override + AttachmentFileUploader get fileUploader => + _fileUploader ??= MockAttachmentFileUploader(); +} diff --git a/packages/stream_chat/test/src/matchers.dart b/packages/stream_chat/test/src/matchers.dart new file mode 100644 index 00000000..a2be680f --- /dev/null +++ b/packages/stream_chat/test/src/matchers.dart @@ -0,0 +1,19 @@ +import 'package:dio/dio.dart' show MultipartFile; +import 'package:test/test.dart'; + +Matcher isSameMultipartFileAs(MultipartFile targetFile) => + _IsSameMultipartFileAs(targetFile: targetFile); + +class _IsSameMultipartFileAs extends Matcher { + const _IsSameMultipartFileAs({required this.targetFile}); + + final MultipartFile targetFile; + + @override + Description describe(Description description) => + description.add('is same multipartFile as $targetFile'); + + @override + bool matches(covariant MultipartFile file, Map matchState) => + file.length == targetFile.length; +} diff --git a/packages/stream_chat/test/src/mocks.dart b/packages/stream_chat/test/src/mocks.dart index be4f15e5..411f06a3 100644 --- a/packages/stream_chat/test/src/mocks.dart +++ b/packages/stream_chat/test/src/mocks.dart @@ -1,6 +1,14 @@ import 'package:dio/dio.dart'; import 'package:logging/logging.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; +import 'package:stream_chat/src/core/api/channel_api.dart'; +import 'package:stream_chat/src/core/api/device_api.dart'; +import 'package:stream_chat/src/core/api/general_api.dart'; +import 'package:stream_chat/src/core/api/guest_api.dart'; +import 'package:stream_chat/src/core/api/message_api.dart'; +import 'package:stream_chat/src/core/api/moderation_api.dart'; +import 'package:stream_chat/src/core/api/user_api.dart'; import 'package:stream_chat/src/core/http/connection_id_manager.dart'; import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; @@ -32,3 +40,20 @@ class MockHttpClient extends Mock implements StreamHttpClient {} class MockTokenManager extends Mock implements TokenManager {} class MockConnectionIdManager extends Mock implements ConnectionIdManager {} + +class MockUserApi extends Mock implements UserApi {} + +class MockGuestApi extends Mock implements GuestApi {} + +class MockMessageApi extends Mock implements MessageApi {} + +class MockChannelApi extends Mock implements ChannelApi {} + +class MockDeviceApi extends Mock implements DeviceApi {} + +class MockModerationApi extends Mock implements ModerationApi {} + +class MockGeneralApi extends Mock implements GeneralApi {} + +class MockAttachmentFileUploader extends Mock + implements AttachmentFileUploader {} diff --git a/packages/stream_chat/test/src/utils.dart b/packages/stream_chat/test/src/utils.dart new file mode 100644 index 00000000..b17e35a9 --- /dev/null +++ b/packages/stream_chat/test/src/utils.dart @@ -0,0 +1,15 @@ +import 'dart:io'; + +File assetFile(String name) { + final dir = currentDirectory.path; + return File('$dir/test/assets/$name'); +} + +// https://github.com/flutter/flutter/issues/20907 +Directory get currentDirectory { + var directory = Directory.current; + if (directory.path.endsWith('/test')) { + directory = directory.parent; + } + return directory; +}