From 214d1bcf1dae9702d16a4716ab7803d8217a89ca Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 4 Jun 2021 16:24:52 +0530 Subject: [PATCH] add core/http tests Signed-off-by: Sahil Kumar --- .../http/interceptor/auth_interceptor.dart | 3 +- .../lib/src/core/http/stream_http_client.dart | 4 +- .../core/http/connection_id_manager_test.dart | 38 ++ .../interceptor/auth_interceptor_test.dart | 270 +++++++++ .../connection_id_interceptor.dart | 67 +++ .../core/http/stream_chat_dio_error_test.dart | 20 + .../http/stream_http_client_options_test.dart | 53 ++ .../core/http/stream_http_client_test.dart | 528 ++++++++++++++++++ .../src/core/http/token_manager_test.dart | 141 +++++ .../test/src/core/http/token_test.dart | 57 ++ 10 files changed, 1177 insertions(+), 4 deletions(-) create mode 100644 packages/stream_chat/test/src/core/http/connection_id_manager_test.dart create mode 100644 packages/stream_chat/test/src/core/http/interceptor/auth_interceptor_test.dart create mode 100644 packages/stream_chat/test/src/core/http/interceptor/connection_id_interceptor.dart create mode 100644 packages/stream_chat/test/src/core/http/stream_chat_dio_error_test.dart create mode 100644 packages/stream_chat/test/src/core/http/stream_http_client_options_test.dart create mode 100644 packages/stream_chat/test/src/core/http/stream_http_client_test.dart create mode 100644 packages/stream_chat/test/src/core/http/token_manager_test.dart create mode 100644 packages/stream_chat/test/src/core/http/token_test.dart diff --git a/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart b/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart index d689cce1..3097c2b1 100644 --- a/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart +++ b/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart @@ -7,7 +7,6 @@ import 'package:stream_chat/src/core/http/token.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; import 'package:stream_chat/src/core/error/error.dart'; - /// class AuthInterceptor extends Interceptor { /// @@ -84,7 +83,7 @@ class AuthInterceptor extends Interceptor { ); return handler.resolve(response); } on DioError catch (error) { - return handler.reject(error); + return handler.next(error); } } return handler.next(err); diff --git a/packages/stream_chat/lib/src/core/http/stream_http_client.dart b/packages/stream_chat/lib/src/core/http/stream_http_client.dart index 3eacd11e..4cf5b0e0 100644 --- a/packages/stream_chat/lib/src/core/http/stream_http_client.dart +++ b/packages/stream_chat/lib/src/core/http/stream_http_client.dart @@ -86,8 +86,8 @@ class StreamHttpClient { /// [StreamHttpClient] instance dequeue the request task。 void unlock() => httpClient.unlock(); - ///Clear the current [StreamHttpClient] instance waiting queue. - void clear() => httpClient.close(); + /// Clear the current [StreamHttpClient] instance waiting queue. + void clear() => httpClient.clear(); /// Shuts down the [StreamHttpClient]. /// diff --git a/packages/stream_chat/test/src/core/http/connection_id_manager_test.dart b/packages/stream_chat/test/src/core/http/connection_id_manager_test.dart new file mode 100644 index 00000000..ad70283f --- /dev/null +++ b/packages/stream_chat/test/src/core/http/connection_id_manager_test.dart @@ -0,0 +1,38 @@ +import 'package:stream_chat/src/core/http/connection_id_manager.dart'; +import 'package:test/test.dart'; + +void main() { + late ConnectionIdManager connectionIdManager; + + setUp(() { + connectionIdManager = ConnectionIdManager(); + }); + + tearDown(() { + connectionIdManager.reset(); + }); + + test('`setConnectionId` should set connectionId', () { + expect(connectionIdManager.connectionId, isNull); + expect(connectionIdManager.hasConnectionId, isFalse); + + const connectionId = 'test-connection-id'; + connectionIdManager.setConnectionId(connectionId); + + expect(connectionIdManager.connectionId, connectionId); + expect(connectionIdManager.hasConnectionId, isTrue); + }); + + test('`reset` should clear the connectionId', () { + const connectionId = 'test-connection-id'; + connectionIdManager.setConnectionId(connectionId); + + expect(connectionIdManager.connectionId, connectionId); + expect(connectionIdManager.hasConnectionId, isTrue); + + connectionIdManager.reset(); + + expect(connectionIdManager.connectionId, isNull); + expect(connectionIdManager.hasConnectionId, isFalse); + }); +} diff --git a/packages/stream_chat/test/src/core/http/interceptor/auth_interceptor_test.dart b/packages/stream_chat/test/src/core/http/interceptor/auth_interceptor_test.dart new file mode 100644 index 00000000..82dcdfb9 --- /dev/null +++ b/packages/stream_chat/test/src/core/http/interceptor/auth_interceptor_test.dart @@ -0,0 +1,270 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/http/interceptor/auth_interceptor.dart'; +import 'package:stream_chat/src/core/http/stream_chat_dio_error.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.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 'package:test/test.dart'; + +import '../../../mocks.dart'; + +void main() { + late StreamHttpClient client; + late TokenManager tokenManager; + late AuthInterceptor authInterceptor; + + setUp(() { + client = MockHttpClient(); + tokenManager = MockTokenManager(); + authInterceptor = AuthInterceptor(client, tokenManager); + }); + + test( + '`onRequest` should add userId, authToken, authType in the request', + () async { + final options = RequestOptions(path: 'test-path'); + final handler = RequestInterceptorHandler(); + + final headers = options.headers; + final queryParams = options.queryParameters; + expect(headers.containsKey('Authorization'), isFalse); + expect(headers.containsKey('stream-auth-type'), isFalse); + expect(queryParams.containsKey('user_id'), isFalse); + + final token = Token.development('test-user-id'); + when(() => tokenManager.loadToken(refresh: any(named: 'refresh'))) + .thenAnswer((_) async => token); + + authInterceptor.onRequest(options, handler); + + final updatedOptions = (await handler.future).data as RequestOptions; + final updateHeaders = updatedOptions.headers; + final updatedQueryParams = updatedOptions.queryParameters; + + expect(updateHeaders.containsKey('Authorization'), isTrue); + expect(updateHeaders['Authorization'], token.rawValue); + expect(updateHeaders.containsKey('stream-auth-type'), isTrue); + expect(updateHeaders['stream-auth-type'], token.authType.raw); + expect(updatedQueryParams.containsKey('user_id'), isTrue); + expect(updatedQueryParams['user_id'], token.userId); + + verify(() => tokenManager.loadToken(refresh: any(named: 'refresh'))) + .called(1); + verifyNoMoreInteractions(tokenManager); + }, + ); + + test( + '`onRequest` should reject with error if `tokenManager.loadToken` throws', + () async { + final options = RequestOptions(path: 'test-path'); + final handler = RequestInterceptorHandler(); + + authInterceptor.onRequest(options, handler); + + try { + await handler.future; + } catch (e) { + // need to cast it as the type is private in dio + var error = (e as dynamic).data; + expect(error, isA()); + error = (error as StreamChatDioError).error; + expect(error.code, ChatErrorCode.undefinedToken.code); + expect(error.message, ChatErrorCode.undefinedToken.message); + } + }, + ); + + test('`onError` should retry the request with refreshed token', () async { + const path = 'test-request-path'; + final options = RequestOptions(path: path); + const code = ChatErrorCode.tokenExpired; + final errorResponse = ErrorResponse() + ..code = code.code + ..message = code.message; + final response = Response( + requestOptions: options, + data: errorResponse.toJson(), + ); + final err = DioError(requestOptions: options, response: response); + final handler = ErrorInterceptorHandler(); + + when(() => tokenManager.isStatic).thenReturn(false); + + when(() => client.lock()).thenReturn(() {}); + + final token = Token.development('test-user-id'); + when(() => tokenManager.loadToken(refresh: true)) + .thenAnswer((_) async => token); + + when(() => client.unlock()).thenReturn(() {}); + + when(() => client.request( + path, + data: options.data, + onReceiveProgress: options.onReceiveProgress, + onSendProgress: options.onSendProgress, + queryParameters: options.queryParameters, + cancelToken: options.cancelToken, + options: any(named: 'options'), + )).thenAnswer((_) async => Response( + requestOptions: options, + statusCode: 200, + )); + + authInterceptor.onError(err, handler); + + final res = await handler.future; + + var data = res.data; + expect(data, isA()); + data = data as Response; + expect(data, isNotNull); + expect(data.statusCode, 200); + expect(data.requestOptions.path, path); + + verify(() => tokenManager.isStatic).called(1); + + verify(() => client.lock()).called(1); + + verify(() => tokenManager.loadToken(refresh: true)).called(1); + verifyNoMoreInteractions(tokenManager); + + verify(() => client.unlock()).called(1); + verify(() => client.request( + path, + data: options.data, + onReceiveProgress: options.onReceiveProgress, + onSendProgress: options.onSendProgress, + queryParameters: options.queryParameters, + cancelToken: options.cancelToken, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(client); + }); + + test( + '`onError` should reject with error if retried request throws', + () async { + const path = 'test-request-path'; + final options = RequestOptions(path: path); + const code = ChatErrorCode.tokenExpired; + final errorResponse = ErrorResponse() + ..code = code.code + ..message = code.message; + final response = Response( + requestOptions: options, + data: errorResponse.toJson(), + ); + final err = DioError(requestOptions: options, response: response); + final handler = ErrorInterceptorHandler(); + + when(() => tokenManager.isStatic).thenReturn(false); + + when(() => client.lock()).thenReturn(() {}); + + final token = Token.development('test-user-id'); + when(() => tokenManager.loadToken(refresh: true)) + .thenAnswer((_) async => token); + + when(() => client.unlock()).thenReturn(() {}); + + when(() => client.request( + path, + data: options.data, + onReceiveProgress: options.onReceiveProgress, + onSendProgress: options.onSendProgress, + queryParameters: options.queryParameters, + cancelToken: options.cancelToken, + options: any(named: 'options'), + )).thenThrow(err); + + authInterceptor.onError(err, handler); + + try { + await handler.future; + } catch (e) { + // need to cast it as the type is private in dio + final error = (e as dynamic).data; + expect(error, isA()); + } + + verify(() => tokenManager.isStatic).called(1); + + verify(() => client.lock()).called(1); + + verify(() => tokenManager.loadToken(refresh: true)).called(1); + verifyNoMoreInteractions(tokenManager); + + verify(() => client.unlock()).called(1); + verify(() => client.request( + path, + data: options.data, + onReceiveProgress: options.onReceiveProgress, + onSendProgress: options.onSendProgress, + queryParameters: options.queryParameters, + cancelToken: options.cancelToken, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(client); + }, + ); + + test( + '`onError` should reject with error if `tokenManager.isStatic` is true', + () async { + const path = 'test-request-path'; + final options = RequestOptions(path: path); + const code = ChatErrorCode.tokenExpired; + final errorResponse = ErrorResponse() + ..code = code.code + ..message = code.message; + final response = Response( + requestOptions: options, + data: errorResponse.toJson(), + ); + final err = DioError(requestOptions: options, response: response); + final handler = ErrorInterceptorHandler(); + + when(() => tokenManager.isStatic).thenReturn(true); + + authInterceptor.onError(err, handler); + + try { + await handler.future; + } catch (e) { + // need to cast it as the type is private in dio + final error = (e as dynamic).data; + expect(error, isA()); + final response = StreamChatNetworkError.fromDioError(error); + expect(response.errorCode, code); + } + + verify(() => tokenManager.isStatic).called(1); + verifyNoMoreInteractions(tokenManager); + }, + ); + + test( + '`onError` should reject with error if error is not a `tokenExpired error`', + () async { + const path = 'test-request-path'; + final options = RequestOptions(path: path); + final response = Response(requestOptions: options); + final err = DioError(requestOptions: options, response: response); + final handler = ErrorInterceptorHandler(); + + authInterceptor.onError(err, handler); + + try { + await handler.future; + } catch (e) { + // need to cast it as the type is private in dio + final error = (e as dynamic).data; + expect(error, isA()); + } + }, + ); +} 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.dart new file mode 100644 index 00000000..9acabccf --- /dev/null +++ b/packages/stream_chat/test/src/core/http/interceptor/connection_id_interceptor.dart @@ -0,0 +1,67 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/http/connection_id_manager.dart'; +import 'package:stream_chat/src/core/http/interceptor/connection_id_interceptor.dart'; +import 'package:test/test.dart'; + +import '../../../mocks.dart'; + +void main() { + late ConnectionIdManager connectionIdManager; + late ConnectionIdInterceptor connectionIdInterceptor; + + setUp(() { + connectionIdManager = MockConnectionIdManager(); + connectionIdInterceptor = ConnectionIdInterceptor(connectionIdManager); + }); + + test( + '`onRequest` should add connectionId in the request', + () async { + final options = RequestOptions(path: 'test-path'); + final handler = RequestInterceptorHandler(); + + final queryParams = options.queryParameters; + expect(queryParams.containsKey('connection_id'), isFalse); + + const connectionId = 'test-connection-id'; + when(() => connectionIdManager.hasConnectionId).thenReturn(true); + when(() => connectionIdManager.connectionId).thenReturn(connectionId); + + connectionIdInterceptor.onRequest(options, handler); + + final updatedOptions = (await handler.future).data as RequestOptions; + final updatedQueryParams = updatedOptions.queryParameters; + + expect(updatedQueryParams.containsKey('connection_id'), isTrue); + expect(updatedQueryParams['connection_id'], connectionId); + + verify(() => connectionIdManager.hasConnectionId).called(1); + verify(() => connectionIdManager.connectionId).called(1); + verifyNoMoreInteractions(connectionIdManager); + }, + ); + + test( + '`onRequest` should not add connectionId if `hasConnectionId` is false', + () async { + final options = RequestOptions(path: 'test-path'); + final handler = RequestInterceptorHandler(); + + final queryParams = options.queryParameters; + expect(queryParams.containsKey('connection_id'), isFalse); + + when(() => connectionIdManager.hasConnectionId).thenReturn(false); + + connectionIdInterceptor.onRequest(options, handler); + + final updatedOptions = (await handler.future).data as RequestOptions; + final updatedQueryParams = updatedOptions.queryParameters; + + expect(updatedQueryParams.containsKey('connection_id'), isFalse); + + verify(() => connectionIdManager.hasConnectionId).called(1); + verifyNoMoreInteractions(connectionIdManager); + }, + ); +} diff --git a/packages/stream_chat/test/src/core/http/stream_chat_dio_error_test.dart b/packages/stream_chat/test/src/core/http/stream_chat_dio_error_test.dart new file mode 100644 index 00000000..f13cf50d --- /dev/null +++ b/packages/stream_chat/test/src/core/http/stream_chat_dio_error_test.dart @@ -0,0 +1,20 @@ +import 'package:dio/dio.dart'; +import 'package:stream_chat/src/core/error/error.dart'; +import 'package:stream_chat/src/core/http/stream_chat_dio_error.dart'; +import 'package:test/test.dart'; + +void main() { + test('should create a new instance of StreamChatDioError', () { + final error = StreamChatNetworkError(ChatErrorCode.inputError); + final options = RequestOptions(path: 'test-path'); + final dioError = StreamChatDioError( + error: error, + requestOptions: options, + ); + + expect(dioError, isA()); + expect(dioError, isNotNull); + expect(dioError.error, error); + expect(dioError.requestOptions, options); + }); +} diff --git a/packages/stream_chat/test/src/core/http/stream_http_client_options_test.dart b/packages/stream_chat/test/src/core/http/stream_http_client_options_test.dart new file mode 100644 index 00000000..a03434fa --- /dev/null +++ b/packages/stream_chat/test/src/core/http/stream_http_client_options_test.dart @@ -0,0 +1,53 @@ +import 'package:stream_chat/src/core/http/stream_http_client.dart'; +import 'package:stream_chat/src/location.dart'; +import 'package:test/test.dart'; + +void main() { + test('should return the all default set params', () { + const options = StreamHttpClientOptions(); + expect(options.location, isNull); + expect(options.baseUrl, 'https://chat-us-east-1.stream-io-api.com'); + expect(options.connectTimeout, const Duration(seconds: 6)); + expect(options.receiveTimeout, const Duration(seconds: 6)); + }); + + test('should override all the default set params', () { + const options = StreamHttpClientOptions( + baseUrl: 'base-url', + connectTimeout: Duration(seconds: 3), + receiveTimeout: Duration(seconds: 3), + ); + expect(options.location, isNull); + expect(options.baseUrl, 'base-url'); + expect(options.connectTimeout, const Duration(seconds: 3)); + expect(options.receiveTimeout, const Duration(seconds: 3)); + }); + + group('should create baseUrl according to provided location', () { + test('us-east', () { + const options = StreamHttpClientOptions(location: Location.usEast); + expect(options.location, isNotNull); + expect(options.baseUrl, 'https://chat-proxy-us-east.stream-io-api.com'); + }); + test('eu-west', () { + const options = StreamHttpClientOptions(location: Location.euWest); + expect(options.location, isNotNull); + expect(options.baseUrl, 'https://chat-proxy-dublin.stream-io-api.com'); + }); + test('mumbai', () { + const options = StreamHttpClientOptions(location: Location.mumbai); + expect(options.location, isNotNull); + expect(options.baseUrl, 'https://chat-proxy-mumbai.stream-io-api.com'); + }); + test('sydney', () { + const options = StreamHttpClientOptions(location: Location.sydney); + expect(options.location, isNotNull); + expect(options.baseUrl, 'https://chat-proxy-sydney.stream-io-api.com'); + }); + test('singapore', () { + const options = StreamHttpClientOptions(location: Location.singapore); + expect(options.location, isNotNull); + expect(options.baseUrl, 'https://chat-proxy-singapore.stream-io-api.com'); + }); + }); +} diff --git a/packages/stream_chat/test/src/core/http/stream_http_client_test.dart b/packages/stream_chat/test/src/core/http/stream_http_client_test.dart new file mode 100644 index 00000000..ece00efd --- /dev/null +++ b/packages/stream_chat/test/src/core/http/stream_http_client_test.dart @@ -0,0 +1,528 @@ +import 'package:dio/dio.dart'; +import 'package:logging/logging.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/error/error.dart'; +import 'package:stream_chat/src/core/http/connection_id_manager.dart'; +import 'package:stream_chat/src/core/http/interceptor/auth_interceptor.dart'; +import 'package:stream_chat/src/core/http/interceptor/connection_id_interceptor.dart'; +import 'package:stream_chat/src/core/http/interceptor/logging_interceptor.dart'; +import 'package:stream_chat/src/core/http/stream_chat_dio_error.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; +import 'package:stream_chat/src/core/http/token_manager.dart'; +import 'package:test/test.dart'; + +import '../../mocks.dart'; + +void main() { + Response successResponse(String path) => Response( + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); + + DioError throwableError( + String path, { + StreamChatNetworkError? error, + bool streamChatDioError = false, + }) { + if (streamChatDioError) assert(error != null, ''); + final options = RequestOptions(path: path); + final data = ErrorResponse() + ..code = error?.code + ..statusCode = error?.statusCode + ..message = error?.message; + DioError? dioError; + if (streamChatDioError) { + dioError = StreamChatDioError(error: error!, requestOptions: options); + } else { + dioError = DioError( + error: error, + requestOptions: options, + response: Response( + requestOptions: options, + statusCode: data.statusCode, + data: data.toJson(), + ), + ); + } + return dioError; + } + + test('AuthInterceptor should be added if tokenManager is provided', () { + const apiKey = 'api-key'; + final client = StreamHttpClient(apiKey, tokenManager: TokenManager()); + + expect(client.httpClient.interceptors.length, 1); + expect(client.httpClient.interceptors.first, isA()); + }); + + test( + 'connectionIdInterceptor should be added if connectionIdManager is provided', + () { + const apiKey = 'api-key'; + final client = StreamHttpClient( + apiKey, + connectionIdManager: ConnectionIdManager(), + ); + + expect(client.httpClient.interceptors.length, 1); + expect( + client.httpClient.interceptors.first, + isA(), + ); + }, + ); + + test('loggingInterceptor should be added if logger is provided', () { + const apiKey = 'api-key'; + final client = StreamHttpClient( + apiKey, + logger: Logger('test-logger'), + ); + + expect(client.httpClient.interceptors.length, 1); + expect( + client.httpClient.interceptors.first, + isA(), + ); + }); + + test('loggingInterceptor should log requests', () async { + const apiKey = 'api-key'; + final logger = MockLogger(); + final client = StreamHttpClient(apiKey, logger: logger); + + try { + await client.get('path'); + } catch (_) {} + + verify(() => logger.info(any())).called(16); + }); + + test('loggingInterceptor should log error', () async { + const apiKey = 'api-key'; + final logger = MockLogger(); + final client = StreamHttpClient(apiKey, logger: logger); + + try { + await client.get('path'); + } catch (_) {} + + verify(() => logger.severe(any())).called(8); + }); + + test('`.lock` should lock the dio client', () async { + final client = StreamHttpClient('api-key'); + expect(client.httpClient.interceptors.requestLock.locked, isFalse); + client.lock(); + expect(client.httpClient.interceptors.requestLock.locked, isTrue); + }); + + test('`.unlock` should unlock the dio client', () async { + final client = StreamHttpClient('api-key'); + expect(client.httpClient.interceptors.requestLock.locked, isFalse); + client.lock(); + expect(client.httpClient.interceptors.requestLock.locked, isTrue); + client.unlock(); + expect(client.httpClient.interceptors.requestLock.locked, isFalse); + }); + + test('`.clear` should clear and unlock the dio client', () async { + final client = StreamHttpClient('api-key')..clear(); + expect(client.httpClient.interceptors.requestLock.locked, isFalse); + }); + + test('`.close` should close the dio client', () async { + final client = StreamHttpClient('api-key')..close(force: true); + try { + await client.get('path'); + } on StreamChatNetworkError catch (e) { + expect(e, isA()); + expect(e.message, "Dio can't establish new connection after closed."); + } + }); + + test('`.get` should return response successfully', () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-get-api-path'; + when(() => dio.get( + path, + options: any(named: 'options'), + )).thenAnswer((_) async => successResponse(path)); + + final res = await client.get(path); + + expect(res, isNotNull); + expect(res.statusCode, 200); + expect(res.requestOptions.path, path); + + verify(() => dio.get( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }); + + test('`.get` should throw an instance of `StreamChatNetworkError`', () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-get-api-path'; + final error = throwableError( + path, + error: StreamChatNetworkError(ChatErrorCode.internalSystemError), + ); + when(() => dio.get( + path, + options: any(named: 'options'), + )).thenThrow(error); + + try { + await client.get(path); + } catch (e) { + expect(e, isA()); + expect(e, StreamChatNetworkError.fromDioError(error)); + } + + verify(() => dio.get( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }); + + test('`.post` should return response successfully', () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-post-api-path'; + when(() => dio.post( + path, + options: any(named: 'options'), + )).thenAnswer((_) async => successResponse(path)); + + final res = await client.post(path); + + expect(res, isNotNull); + expect(res.statusCode, 200); + expect(res.requestOptions.path, path); + + verify(() => dio.post( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }); + + test( + '`.post` should throw an instance of `StreamChatNetworkError`', + () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-post-api-path'; + final error = throwableError( + path, + error: StreamChatNetworkError(ChatErrorCode.internalSystemError), + ); + when(() => dio.post( + path, + options: any(named: 'options'), + )).thenThrow(error); + + try { + await client.post(path); + } catch (e) { + expect(e, isA()); + expect(e, StreamChatNetworkError.fromDioError(error)); + } + + verify(() => dio.post( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }, + ); + + test('`.delete` should return response successfully', () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-delete-api-path'; + when(() => dio.delete( + path, + options: any(named: 'options'), + )).thenAnswer((_) async => successResponse(path)); + + final res = await client.delete(path); + + expect(res, isNotNull); + expect(res.statusCode, 200); + expect(res.requestOptions.path, path); + + verify(() => dio.delete( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }); + + test( + '`.delete` should throw an instance of `StreamChatNetworkError`', + () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-delete-api-path'; + final error = throwableError( + path, + error: StreamChatNetworkError(ChatErrorCode.internalSystemError), + ); + when(() => dio.delete( + path, + options: any(named: 'options'), + )).thenThrow(error); + + try { + await client.delete(path); + } catch (e) { + expect(e, isA()); + expect(e, StreamChatNetworkError.fromDioError(error)); + } + + verify(() => dio.delete( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }, + ); + + test('`.patch` should return response successfully', () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-patch-api-path'; + when(() => dio.patch( + path, + options: any(named: 'options'), + )).thenAnswer((_) async => successResponse(path)); + + final res = await client.patch(path); + + expect(res, isNotNull); + expect(res.statusCode, 200); + expect(res.requestOptions.path, path); + + verify(() => dio.patch( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }); + + test( + '`.patch` should throw an instance of `StreamChatNetworkError`', + () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-patch-api-path'; + final error = throwableError( + path, + error: StreamChatNetworkError(ChatErrorCode.internalSystemError), + ); + when(() => dio.patch( + path, + options: any(named: 'options'), + )).thenThrow(error); + + try { + await client.patch(path); + } catch (e) { + expect(e, isA()); + expect(e, StreamChatNetworkError.fromDioError(error)); + } + + verify(() => dio.patch( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }, + ); + + test('`.put` should return response successfully', () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-put-api-path'; + when(() => dio.put( + path, + options: any(named: 'options'), + )).thenAnswer((_) async => successResponse(path)); + + final res = await client.put(path); + + expect(res, isNotNull); + expect(res.statusCode, 200); + expect(res.requestOptions.path, path); + + verify(() => dio.put( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }); + + test( + '`.put` should throw an instance of `StreamChatNetworkError`', + () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-put-api-path'; + final error = throwableError( + path, + error: StreamChatNetworkError(ChatErrorCode.internalSystemError), + ); + when(() => dio.put( + path, + options: any(named: 'options'), + )).thenThrow(error); + + try { + await client.put(path); + } catch (e) { + expect(e, isA()); + expect(e, StreamChatNetworkError.fromDioError(error)); + } + + verify(() => dio.put( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }, + ); + + test('`.postFile` should return response successfully', () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-delete-api-path'; + final file = MultipartFile.fromBytes([]); + + when(() => dio.post( + path, + data: any(named: 'data'), + options: any(named: 'options'), + )).thenAnswer((_) async => successResponse(path)); + + final res = await client.postFile(path, file); + + expect(res, isNotNull); + expect(res.statusCode, 200); + expect(res.requestOptions.path, path); + + verify(() => dio.post( + path, + data: any(named: 'data'), + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }); + + test( + '`.postFile` should throw an instance of `StreamChatNetworkError`', + () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-post-file-api-path'; + final file = MultipartFile.fromBytes([]); + + final error = throwableError( + path, + error: StreamChatNetworkError(ChatErrorCode.internalSystemError), + ); + when(() => dio.post( + path, + data: any(named: 'data'), + options: any(named: 'options'), + )).thenThrow(error); + + try { + await client.postFile(path, file); + } catch (e) { + expect(e, isA()); + expect(e, StreamChatNetworkError.fromDioError(error)); + } + + verify(() => dio.post( + path, + data: any(named: 'data'), + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }, + ); + + test('`.request` should return response successfully', () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-request-api-path'; + when(() => dio.request( + path, + options: any(named: 'options'), + )).thenAnswer((_) async => successResponse(path)); + + final res = await client.request(path); + + expect(res, isNotNull); + expect(res.statusCode, 200); + expect(res.requestOptions.path, path); + + verify(() => dio.request( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }); + + test( + '`.request` should throw an instance of `StreamChatNetworkError`', + () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-put-api-path'; + final error = throwableError( + path, + streamChatDioError: true, + error: StreamChatNetworkError(ChatErrorCode.internalSystemError), + ); + when(() => dio.request( + path, + options: any(named: 'options'), + )).thenThrow(error); + + try { + await client.request(path); + } catch (e) { + expect(e, isA()); + expect(e, error.error); + } + + verify(() => dio.request( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }, + ); +} diff --git a/packages/stream_chat/test/src/core/http/token_manager_test.dart b/packages/stream_chat/test/src/core/http/token_manager_test.dart new file mode 100644 index 00000000..a1a38fa5 --- /dev/null +++ b/packages/stream_chat/test/src/core/http/token_manager_test.dart @@ -0,0 +1,141 @@ +import 'package:stream_chat/src/core/http/token.dart'; +import 'package:stream_chat/src/core/http/token_manager.dart'; +import 'package:test/test.dart'; + +void main() { + late TokenManager tokenManager; + + setUp(() { + tokenManager = TokenManager(); + }); + + tearDown(() { + tokenManager.reset(); + }); + + test('`setTokenOrProvider` should set token', () async { + expect(tokenManager.userId, isNull); + + const userId = 'test-user-id'; + final token = Token.development(userId); + final returnedToken = await tokenManager.setTokenOrProvider( + userId, + token: token, + ); + + expect(returnedToken, token); + expect(tokenManager.userId, userId); + expect(tokenManager.isStatic, isTrue); + }); + + test('`setTokenOrProvider` should set tokenProvider', () async { + expect(tokenManager.userId, isNull); + + const userId = 'test-user-id'; + Future tokenProvider(String userId) async => + Token.development(userId).rawValue; + final returnedToken = await tokenManager.setTokenOrProvider( + userId, + provider: tokenProvider, + ); + + expect(returnedToken, isNotNull); + expect(tokenManager.userId, userId); + expect(tokenManager.isStatic, isFalse); + }); + + test( + '`setTokenOrProvider` should throw if both token and provider is not provided', + () async { + expect(tokenManager.userId, isNull); + + const userId = 'test-user-id'; + try { + await tokenManager.setTokenOrProvider(userId); + } catch (e) { + expect(e, isA()); + } + }, + ); + + test( + '`setTokenOrProvider` should throw if both token and provider is provided', + () async { + expect(tokenManager.userId, isNull); + + const userId = 'test-user-id'; + final token = Token.development(userId); + Future tokenProvider(String userId) async => + Token.development(userId).rawValue; + try { + await tokenManager.setTokenOrProvider( + userId, + token: token, + provider: tokenProvider, + ); + } catch (e) { + expect(e, isA()); + } + }, + ); + + test( + '`.loadToken` should return token set via `setToken`', + () async { + const userId = 'test-user-id'; + final token = Token.development(userId); + await tokenManager.setTokenOrProvider(userId, token: token); + + final returnedToken = await tokenManager.loadToken(); + expect(returnedToken, token); + }, + ); + + test( + '`.loadToken` should return token set via `setProvider`', + () async { + const userId = 'test-user-id'; + final token = Token.development(userId); + Future tokenProvider(String userId) async => token.rawValue; + await tokenManager.setTokenOrProvider(userId, provider: tokenProvider); + + final returnedToken = await tokenManager.loadToken(); + expect(returnedToken, token); + }, + ); + + test( + '`.loadToken` should return refreshed token set via `setProvider`', + () async { + const userId = 'test-user-id'; + final token = Token.development(userId); + final refreshToken = Token.development(userId); + + var refresh = false; + + Future tokenProvider(String userId) async { + if (refresh) return refreshToken.rawValue; + return token.rawValue; + } + + await tokenManager.setTokenOrProvider(userId, provider: tokenProvider); + + final returnedToken = await tokenManager.loadToken(); + expect(returnedToken, token); + + refresh = true; + final returnedRefreshToken = await tokenManager.loadToken(refresh: true); + expect(returnedRefreshToken, refreshToken); + }, + ); + + test('`.reset` should reset the tokenManager', () async { + const userId = 'test-user-id'; + final token = Token.development(userId); + await tokenManager.setTokenOrProvider(userId, token: token); + expect(tokenManager.userId, userId); + + tokenManager.reset(); + expect(tokenManager.userId, isNull); + }); +} diff --git a/packages/stream_chat/test/src/core/http/token_test.dart b/packages/stream_chat/test/src/core/http/token_test.dart new file mode 100644 index 00000000..b448884e --- /dev/null +++ b/packages/stream_chat/test/src/core/http/token_test.dart @@ -0,0 +1,57 @@ +import 'package:stream_chat/src/core/http/token.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; + +void main() { + test('`.anonymous` should create anonymous token with passed userId', () { + const userId = 'test-user-id'; + final token = Token.anonymous(userId: userId); + expect(token, isNotNull); + expect(token.userId, userId); + expect(token.rawValue, isEmpty); + expect(token.authType, AuthType.anonymous); + expect(token.authType.raw, AuthType.anonymous.raw); + }); + + test('`.fromRawValue` should create token from rawValue', () { + const userId = 'test-user-id'; + final devToken = Token.development(userId); + final token = Token.fromRawValue(devToken.rawValue); + expect(token, devToken); + }); + + test('`.fromRawValue` should throw if does not contain `user_id`', () { + const badToken = 'bad-token-without-a-user-id'; + try { + Token.fromRawValue(badToken); + } catch (e) { + expect(e, isA()); + } + }); + + test('`.development` should create a dev-token with provided user-id', () { + const userId = 'test-user-id'; + final token = Token.development(userId); + expect(token, isNotNull); + expect(token.userId, userId); + expect(token.rawValue, isNotEmpty); + expect(token.authType, AuthType.jwt); + expect(token.authType.raw, AuthType.jwt.raw); + }); + + test( + '`.guest` should create a guest-token with provided user and provider', + () async { + final user = User(id: 'test-user-id'); + Future provider(User user) async => + Token.development(user.id).rawValue; + + final token = await Token.guest(user, provider); + expect(token, isNotNull); + expect(token.userId, user.id); + expect(token.rawValue, isNotEmpty); + expect(token.authType, AuthType.jwt); + expect(token.authType.raw, AuthType.jwt.raw); + }, + ); +}