diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..7e7e7f6 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1 @@ +extensions: diff --git a/lib/feature/data/datasource/general/general_remote_data_source.dart b/lib/feature/data/datasource/general/general_remote_data_source.dart new file mode 100644 index 0000000..0a98915 --- /dev/null +++ b/lib/feature/data/datasource/general/general_remote_data_source.dart @@ -0,0 +1,36 @@ +import 'package:dio/dio.dart'; +import 'package:dipantau_desktop_client/config/flavor_config.dart'; +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; + +abstract class GeneralRemoteDataSource { + /// Panggil endpoint [host]/api/ping + /// + /// Throws [DioException] untuk semua error kode + late String pathPing; + + Future ping(String baseUrl); +} + +class GeneralRemoteDataSourceImpl implements GeneralRemoteDataSource { + final Dio dio; + + GeneralRemoteDataSourceImpl({ + required this.dio, + }); + + final baseUrl = FlavorConfig.instance.values.baseUrl; + + @override + String pathPing = ''; + + @override + Future ping(String baseUrl) async { + pathPing = '$baseUrl/api/ping'; + final response = await dio.get(pathPing); + if (response.statusCode.toString().startsWith('2')) { + return GeneralResponse.fromJson(response.data); + } else { + throw DioException(requestOptions: RequestOptions(path: pathPing)); + } + } +} diff --git a/lib/feature/data/repository/general/general_repository_impl.dart b/lib/feature/data/repository/general/general_repository_impl.dart new file mode 100644 index 0000000..be2ad2e --- /dev/null +++ b/lib/feature/data/repository/general/general_repository_impl.dart @@ -0,0 +1,56 @@ +import 'package:dio/dio.dart'; +import 'package:dipantau_desktop_client/core/error/failure.dart'; +import 'package:dipantau_desktop_client/core/network/network_info.dart'; +import 'package:dipantau_desktop_client/feature/data/datasource/general/general_remote_data_source.dart'; +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:dipantau_desktop_client/feature/domain/repository/general/general_repository.dart'; + +class GeneralRepositoryImpl implements GeneralRepository { + final GeneralRemoteDataSource remoteDataSource; + final NetworkInfo networkInfo; + + GeneralRepositoryImpl({ + required this.remoteDataSource, + required this.networkInfo, + }); + + String getErrorMessageFromEndpoint(dynamic dynamicErrorMessage, String httpErrorMessage, int? statusCode) { + if (dynamicErrorMessage is Map && dynamicErrorMessage.containsKey('message')) { + return '$statusCode ${dynamicErrorMessage['message']}'; + } else if (dynamicErrorMessage is String) { + return httpErrorMessage; + } else { + return httpErrorMessage; + } + } + + @override + Future<({Failure? failure, GeneralResponse? response})> ping(String baseUrl) async { + Failure? failure; + GeneralResponse? response; + final isConnected = await networkInfo.isConnected; + if (isConnected) { + try { + response = await remoteDataSource.ping(baseUrl); + } on DioException catch (error) { + final message = error.message ?? error.toString(); + if (error.response == null) { + failure = ServerFailure(message); + } else { + final errorMessage = getErrorMessageFromEndpoint( + error.response?.data, + message, + error.response?.statusCode, + ); + failure = ServerFailure(errorMessage); + } + } on TypeError catch (error) { + final errorMessage = error.toString(); + failure = ParsingFailure(errorMessage); + } + } else { + failure = ConnectionFailure(); + } + return (failure: failure, response: response); + } +} diff --git a/lib/feature/domain/repository/general/general_repository.dart b/lib/feature/domain/repository/general/general_repository.dart new file mode 100644 index 0000000..3b26c36 --- /dev/null +++ b/lib/feature/domain/repository/general/general_repository.dart @@ -0,0 +1,6 @@ +import 'package:dipantau_desktop_client/core/error/failure.dart'; +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; + +abstract class GeneralRepository { + Future<({Failure? failure, GeneralResponse? response})> ping(String baseUrl); +} \ No newline at end of file diff --git a/lib/feature/domain/usecase/ping/ping.dart b/lib/feature/domain/usecase/ping/ping.dart new file mode 100644 index 0000000..3c28a00 --- /dev/null +++ b/lib/feature/domain/usecase/ping/ping.dart @@ -0,0 +1,34 @@ +import 'package:dipantau_desktop_client/core/error/failure.dart'; +import 'package:dipantau_desktop_client/core/usecase/usecase.dart'; +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:dipantau_desktop_client/feature/domain/repository/general/general_repository.dart'; +import 'package:equatable/equatable.dart'; + +class Ping implements UseCaseRecords { + final GeneralRepository repository; + + Ping({required this.repository}); + + @override + Future<({Failure? failure, GeneralResponse? response})> call(ParamsPing params) { + return repository.ping(params.baseUrl); + } +} + +class ParamsPing extends Equatable { + final String baseUrl; + + ParamsPing({ + required this.baseUrl, + }); + + @override + List get props => [ + baseUrl, + ]; + + @override + String toString() { + return 'ParamsPing{baseUrl: $baseUrl}'; + } +} diff --git a/lib/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart b/lib/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart new file mode 100644 index 0000000..e089437 --- /dev/null +++ b/lib/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart @@ -0,0 +1,43 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:dipantau_desktop_client/core/util/helper.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/ping/ping.dart'; + +part 'setup_credential_event.dart'; + +part 'setup_credential_state.dart'; + +class SetupCredentialBloc extends Bloc { + final Helper helper; + final Ping ping; + + SetupCredentialBloc({ + required this.helper, + required this.ping, + }) : super(InitialSetupCredentialState()) { + on(_onPingSetupCredentialEvent); + } + + FutureOr _onPingSetupCredentialEvent( + PingSetupCredentialEvent event, + Emitter emit, + ) async { + final baseUrl = event.baseUrl; + emit(LoadingSetupCredentialState()); + final result = await ping( + ParamsPing( + baseUrl: baseUrl, + ), + ); + final response = result.response; + final failure = result.failure; + if (response != null) { + emit(SuccessPingSetupCredentialState(baseUrl: baseUrl)); + return; + } + + final errorMessage = helper.getErrorMessageFromFailure(failure); + emit(FailureSetupCredentialState(errorMessage: errorMessage)); + } +} diff --git a/lib/feature/presentation/bloc/setup_credential/setup_credential_event.dart b/lib/feature/presentation/bloc/setup_credential/setup_credential_event.dart new file mode 100644 index 0000000..07219b2 --- /dev/null +++ b/lib/feature/presentation/bloc/setup_credential/setup_credential_event.dart @@ -0,0 +1,16 @@ +part of 'setup_credential_bloc.dart'; + +abstract class SetupCredentialEvent {} + +class PingSetupCredentialEvent extends SetupCredentialEvent { + final String baseUrl; + + PingSetupCredentialEvent({ + required this.baseUrl, + }); + + @override + String toString() { + return 'PingSetupCredentialEvent{baseUrl: $baseUrl}'; + } +} diff --git a/lib/feature/presentation/bloc/setup_credential/setup_credential_state.dart b/lib/feature/presentation/bloc/setup_credential/setup_credential_state.dart new file mode 100644 index 0000000..f2127f6 --- /dev/null +++ b/lib/feature/presentation/bloc/setup_credential/setup_credential_state.dart @@ -0,0 +1,33 @@ +part of 'setup_credential_bloc.dart'; + +abstract class SetupCredentialState {} + +class InitialSetupCredentialState extends SetupCredentialState {} + +class LoadingSetupCredentialState extends SetupCredentialState {} + +class FailureSetupCredentialState extends SetupCredentialState { + final String errorMessage; + + FailureSetupCredentialState({ + required this.errorMessage, + }); + + @override + String toString() { + return 'FailureSetupCredentialState{errorMessage: $errorMessage}'; + } +} + +class SuccessPingSetupCredentialState extends SetupCredentialState { + final String baseUrl; + + SuccessPingSetupCredentialState({ + required this.baseUrl, + }); + + @override + String toString() { + return 'SuccessPingSetupCredentialState{baseUrl: $baseUrl}'; + } +} diff --git a/lib/feature/presentation/page/setup_credential/setup_credential_page.dart b/lib/feature/presentation/page/setup_credential/setup_credential_page.dart index dd76164..4a7ec98 100644 --- a/lib/feature/presentation/page/setup_credential/setup_credential_page.dart +++ b/lib/feature/presentation/page/setup_credential/setup_credential_page.dart @@ -2,10 +2,14 @@ import 'package:dipantau_desktop_client/core/util/enum/global_variable.dart'; import 'package:dipantau_desktop_client/core/util/helper.dart'; import 'package:dipantau_desktop_client/core/util/shared_preferences_manager.dart'; import 'package:dipantau_desktop_client/core/util/widget_helper.dart'; +import 'package:dipantau_desktop_client/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart'; +import 'package:dipantau_desktop_client/feature/presentation/widget/widget_loading_center_full_screen.dart'; import 'package:dipantau_desktop_client/feature/presentation/widget/widget_primary_button.dart'; import 'package:dipantau_desktop_client/injection_container.dart' as di; +import 'package:dipantau_desktop_client/injection_container.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; class SetupCredentialPage extends StatefulWidget { @@ -32,6 +36,7 @@ class _SetupCredentialPageState extends State { final controllerHostname = TextEditingController(); final widgetHelper = WidgetHelper(); final formState = GlobalKey(); + final setupCredentialBloc = sl(); var isLogin = false; var defaultDomainApi = ''; @@ -52,37 +57,81 @@ class _SetupCredentialPageState extends State { appBar: AppBar( automaticallyImplyLeading: widget.isFromSplashScreen ? false : true, ), - body: Padding( - padding: EdgeInsets.only( - left: helper.getDefaultPaddingLayout, - top: helper.getDefaultPaddingLayoutTop, - right: helper.getDefaultPaddingLayout, - bottom: helper.getDefaultPaddingLayout, + body: BlocProvider( + create: (context) => setupCredentialBloc, + child: BlocListener( + listener: (context, state) { + if (state is FailureSetupCredentialState) { + final errorMessage = 'invalid_hostname'.tr(); + widgetHelper.showDialogMessage( + context, + 'info'.tr(), + errorMessage, + ); + } else if (state is SuccessPingSetupCredentialState) { + final hostname = state.baseUrl; + sharedPreferencesManager.putString(SharedPreferencesManager.keyDomainApi, hostname).then((value) { + helper.setDomainApiToFlavor(hostname); + di.init(); + if (mounted) { + context.go('/'); + } + }); + } + }, + child: Stack( + children: [ + buildWidgetBody(), + buildWidgetLoadingOverlay(), + ], + ), ), - child: SizedBox( - width: double.infinity, - child: Form( - key: formState, - autovalidateMode: AutovalidateMode.onUserInteraction, - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - buildWidgetTitle(), - const SizedBox(height: 8), - Text( - 'subtitle_set_hostname'.tr(), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.grey, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 24), - buildWidgetTextFieldHostname(), - const SizedBox(height: 24), - buildWidgetButtonSave(), - ], - ), + ), + ); + } + + Widget buildWidgetLoadingOverlay() { + return BlocBuilder( + builder: (context, state) { + if (state is LoadingSetupCredentialState) { + return const WidgetLoadingCenterFullScreen(); + } + return Container(); + }, + ); + } + + Widget buildWidgetBody() { + return Padding( + padding: EdgeInsets.only( + left: helper.getDefaultPaddingLayout, + top: helper.getDefaultPaddingLayoutTop, + right: helper.getDefaultPaddingLayout, + bottom: helper.getDefaultPaddingLayout, + ), + child: SizedBox( + width: double.infinity, + child: Form( + key: formState, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + buildWidgetTitle(), + const SizedBox(height: 8), + Text( + 'subtitle_set_hostname'.tr(), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.grey, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + buildWidgetTextFieldHostname(), + const SizedBox(height: 24), + buildWidgetButtonSave(), + ], ), ), ), @@ -149,12 +198,11 @@ class _SetupCredentialPageState extends State { } if (isContinue != null && isContinue) { final hostname = helper.removeTrailingSlash(controllerHostname.text.trim()).trim(); - await sharedPreferencesManager.putString(SharedPreferencesManager.keyDomainApi, hostname); - helper.setDomainApiToFlavor(hostname); - di.init(); - if (mounted) { - context.go('/'); - } + setupCredentialBloc.add( + PingSetupCredentialEvent( + baseUrl: hostname, + ), + ); } } } diff --git a/lib/injection_container.dart b/lib/injection_container.dart index 9d581a2..a72e1a5 100644 --- a/lib/injection_container.dart +++ b/lib/injection_container.dart @@ -7,17 +7,20 @@ import 'package:dipantau_desktop_client/core/util/notification_helper.dart'; import 'package:dipantau_desktop_client/core/util/shared_preferences_manager.dart'; import 'package:dipantau_desktop_client/core/util/widget_helper.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/auth/auth_remote_data_source.dart'; +import 'package:dipantau_desktop_client/feature/data/datasource/general/general_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/project/project_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/setting/setting_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/track/track_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/user/user_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/repository/auth/auth_repository_impl.dart'; +import 'package:dipantau_desktop_client/feature/data/repository/general/general_repository_impl.dart'; import 'package:dipantau_desktop_client/feature/data/repository/project/project_repository_impl.dart'; import 'package:dipantau_desktop_client/feature/data/repository/setting/setting_repository_impl.dart'; import 'package:dipantau_desktop_client/feature/data/repository/track/track_repository_impl.dart'; import 'package:dipantau_desktop_client/feature/data/repository/user/user_repository_impl.dart'; import 'package:dipantau_desktop_client/feature/database/app_database.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/auth/auth_repository.dart'; +import 'package:dipantau_desktop_client/feature/domain/repository/general/general_repository.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/project/project_repository.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/setting/setting_repository.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/track/track_repository.dart'; @@ -38,6 +41,7 @@ import 'package:dipantau_desktop_client/feature/domain/usecase/get_track_user/ge import 'package:dipantau_desktop_client/feature/domain/usecase/get_track_user_lite/get_track_user_lite.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/get_user_setting/get_user_setting.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/login/login.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/ping/ping.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/refresh_token/refresh_token.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/reset_password/reset_password.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/send_app_version/send_app_version.dart'; @@ -57,6 +61,7 @@ import 'package:dipantau_desktop_client/feature/presentation/bloc/project/projec import 'package:dipantau_desktop_client/feature/presentation/bloc/report_screenshot/report_screenshot_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/reset_password/reset_password_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/setting/setting_bloc.dart'; +import 'package:dipantau_desktop_client/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/sign_up/sign_up_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/sync_manual/sync_manual_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/tracking/tracking_bloc.dart'; @@ -169,6 +174,12 @@ void init() { getProjectTaskByUserId: sl(), ), ); + sl.registerFactory( + () => SetupCredentialBloc( + helper: sl(), + ping: sl(), + ), + ); // use case sl.registerLazySingleton(() => GetProject(repository: sl())); @@ -195,6 +206,7 @@ void init() { sl.registerLazySingleton(() => GetAllUserSetting(repository: sl())); sl.registerLazySingleton(() => GetUserSetting(repository: sl())); sl.registerLazySingleton(() => UpdateUserSetting(repository: sl())); + sl.registerLazySingleton(() => Ping(repository: sl())); // repository sl.registerLazySingleton( @@ -227,6 +239,12 @@ void init() { networkInfo: sl(), ), ); + sl.registerLazySingleton( + () => GeneralRepositoryImpl( + remoteDataSource: sl(), + networkInfo: sl(), + ), + ); // data source sl.registerLazySingleton( @@ -254,6 +272,11 @@ void init() { dio: sl(instanceName: dioRefreshToken), ), ); + sl.registerLazySingleton( + () => GeneralRemoteDataSourceImpl( + dio: sl(instanceName: dioLogging), + ), + ); // core sl.registerLazySingleton(() => NetworkInfoImpl(sl())); diff --git a/pubspec.yaml b/pubspec.yaml index 9dfe644..95e0e96 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -168,6 +168,9 @@ dev_dependencies: # This package provides a library that performs static analysis of Dart code. analyzer: ^6.4.1 + # DevTools extension for Flutter: Manage SharedPreferences efficiently. Edit, search, and view keys. + shared_preferences_tools: 1.0.3 + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/test/feature/data/datasource/general/general_remote_data_source_test.dart b/test/feature/data/datasource/general/general_remote_data_source_test.dart new file mode 100644 index 0000000..ad0373b --- /dev/null +++ b/test/feature/data/datasource/general/general_remote_data_source_test.dart @@ -0,0 +1,109 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:dipantau_desktop_client/config/flavor_config.dart'; +import 'package:dipantau_desktop_client/feature/data/datasource/general/general_remote_data_source.dart'; +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../../../../fixture/fixture_reader.dart'; +import '../../../../helper/mock_helper.mocks.dart'; + +void main() { + late GeneralRemoteDataSource remoteDataSource; + late MockDio mockDio; + late MockHttpClientAdapter mockDioAdapter; + + const baseUrl = 'https://example.com'; + + setUp(() { + FlavorConfig( + values: FlavorValues( + baseUrl: baseUrl, + baseUrlAuth: '', + baseUrlUser: '', + baseUrlTrack: '', + baseUrlProject: '', + baseUrlSetting: '', + ), + ); + mockDio = MockDio(); + mockDioAdapter = MockHttpClientAdapter(); + mockDio.httpClientAdapter = mockDioAdapter; + remoteDataSource = GeneralRemoteDataSourceImpl(dio: mockDio); + }); + + final tRequestOptions = RequestOptions(path: ''); + + group('ping', () { + const tPathResponse = 'general_response.json'; + const hostname = baseUrl; + final tResponse = GeneralResponse.fromJson( + json.decode( + fixture(tPathResponse), + ), + ); + + void setUpMockDioSuccess() { + final responsePayload = json.decode(fixture(tPathResponse)); + final response = Response( + requestOptions: tRequestOptions, + data: responsePayload, + statusCode: 200, + headers: Headers.fromMap({ + Headers.contentTypeHeader: [Headers.jsonContentType], + }), + ); + when(mockDio.get(any)).thenAnswer((_) async => response); + } + + test( + 'pastikan endpoint ping benar-benar terpanggil dengan method GET', + () async { + // arrange + setUpMockDioSuccess(); + + // act + await remoteDataSource.ping(hostname); + + // assert + verify(mockDio.get('$hostname/api/ping')); + }, + ); + + test( + 'pastikan mengembalikan objek class model GeneralResponse ketika menerima respon sukses ' + 'dari endpoint', + () async { + // arrange + setUpMockDioSuccess(); + + // act + final result = await remoteDataSource.ping(hostname); + + // assert + expect(result, tResponse); + }, + ); + + test( + 'pastikan akan menerima exception DioError ketika menerima respon kegagalan dari endpoint', + () async { + // arrange + final response = Response( + requestOptions: tRequestOptions, + data: 'Bad Request', + statusCode: 400, + ); + when(mockDio.get(any)).thenAnswer((_) async => response); + + // act + final call = remoteDataSource.ping(hostname); + + // assert + expect(() => call, throwsA(const TypeMatcher())); + }, + ); + }); +} diff --git a/test/feature/data/repository/general/general_repository_impl_test.dart b/test/feature/data/repository/general/general_repository_impl_test.dart new file mode 100644 index 0000000..82c1d33 --- /dev/null +++ b/test/feature/data/repository/general/general_repository_impl_test.dart @@ -0,0 +1,189 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:dipantau_desktop_client/core/error/failure.dart'; +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:dipantau_desktop_client/feature/data/repository/general/general_repository_impl.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../../../../fixture/fixture_reader.dart'; +import '../../../../helper/mock_helper.mocks.dart'; + +void main() { + late GeneralRepositoryImpl repository; + late MockGeneralRemoteDataSource mockRemoteDataSource; + late MockNetworkInfo mockNetworkInfo; + + setUp(() { + mockRemoteDataSource = MockGeneralRemoteDataSource(); + mockNetworkInfo = MockNetworkInfo(); + repository = GeneralRepositoryImpl( + remoteDataSource: mockRemoteDataSource, + networkInfo: mockNetworkInfo, + ); + }); + + final tRequestOptions = RequestOptions(path: ''); + + void setUpMockNetworkConnected() { + when(mockNetworkInfo.isConnected).thenAnswer((_) async => true); + } + + void setUpMockNetworkDisconnected() { + when(mockNetworkInfo.isConnected).thenAnswer((_) async => false); + } + + void testDisconnected(Function endpointInvoke) { + test( + 'pastikan mengembalikan objek ConnectionFailure ketika device tidak terhubung ke internet', + () async { + // arrange + setUpMockNetworkDisconnected(); + + // act + final result = await endpointInvoke.call(); + + // assert + verify(mockNetworkInfo.isConnected); + expect(result.failure, ConnectionFailure()); + }, + ); + } + + void testServerFailureString(Function whenInvoke, Function actInvoke, Function verifyInvoke) { + test( + 'pastikan mengembalikan objek ServerFailure ketika repository menerima respon kegagalan ' + 'dari endpoint dengan respon data html atau string', + () async { + // arrange + setUpMockNetworkConnected(); + when(whenInvoke.call()).thenThrow( + DioException( + requestOptions: tRequestOptions, + message: 'testError', + response: Response( + requestOptions: tRequestOptions, + data: 'testDataError', + statusCode: 400, + ), + ), + ); + + // act + final result = await actInvoke.call(); + + // assert + verify(verifyInvoke.call()); + expect(result.failure, ServerFailure('testError')); + }, + ); + } + + void testParsingFailure(Function whenInvoke, Function actInvoke, Function verifyInvoke) { + test( + 'pastikan mengembalikan objek ParsingFailure ketika RemoteDataSource menerima respon kegagalan ' + 'dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(whenInvoke.call()).thenThrow(TypeError()); + + // act + final result = await actInvoke.call(); + + // assert + verify(verifyInvoke.call()); + expect(result.failure, ParsingFailure(TypeError().toString())); + }, + ); + } + + group('ping', () { + final tResponse = GeneralResponse.fromJson( + json.decode( + fixture('general_response.json'), + ), + ); + const hostname = 'https://example.com'; + + test( + 'pastikan mengembalikan objek model GeneralResponse ketika RemoteDataSource berhasil menerima ' + 'respon sukses dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.ping(any)).thenAnswer((_) async => tResponse); + + // act + final result = await repository.ping(hostname); + + // assert + verify(mockRemoteDataSource.ping(hostname)); + expect(result.response, tResponse); + }, + ); + + test( + 'pastikan mengembalikan objek ServerFailure ketika RemoteDataSource berhasil menerima ' + 'respon timeout dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.ping(any)) + .thenThrow(DioException(requestOptions: tRequestOptions, message: 'testError')); + + // act + final result = await repository.ping(hostname); + + // assert + verify(mockRemoteDataSource.ping(hostname)); + expect(result.failure, ServerFailure('testError')); + }, + ); + + test( + 'pastikan mengembalikan objek ServerFailure ketika RemoteDataSource menerima respon kegagaln ' + 'dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.ping(any)).thenThrow( + DioException( + requestOptions: tRequestOptions, + message: 'testError', + response: Response( + requestOptions: tRequestOptions, + data: { + 'title': 'testTitleError', + 'message': 'testMessageError', + }, + statusCode: 400, + ), + ), + ); + + // act + final result = await repository.ping(hostname); + + // assert + verify(mockRemoteDataSource.ping(hostname)); + expect(result.failure, ServerFailure('400 testMessageError')); + }, + ); + + testServerFailureString( + () => mockRemoteDataSource.ping(any), + () => repository.ping(hostname), + () => mockRemoteDataSource.ping(hostname), + ); + + testParsingFailure( + () => mockRemoteDataSource.ping(any), + () => repository.ping(hostname), + () => mockRemoteDataSource.ping(hostname), + ); + + testDisconnected(() => repository.ping(hostname)); + }); +} diff --git a/test/feature/domain/usecase/ping/ping_test.dart b/test/feature/domain/usecase/ping/ping_test.dart new file mode 100644 index 0000000..609071e --- /dev/null +++ b/test/feature/domain/usecase/ping/ping_test.dart @@ -0,0 +1,68 @@ +import 'dart:convert'; + +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/ping/ping.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../../../../fixture/fixture_reader.dart'; +import '../../../../helper/mock_helper.mocks.dart'; + +void main() { + late Ping useCase; + late MockGeneralRepository mockRepository; + + setUp(() { + mockRepository = MockGeneralRepository(); + useCase = Ping(repository: mockRepository); + }); + + const hostname = 'https://example.com'; + final tParams = ParamsPing(baseUrl: hostname); + + test( + 'pastikan objek repository berhasil menerima respon sukses atau gagal dari endpoint', + () async { + // arrange + final tResponse = GeneralResponse.fromJson( + json.decode( + fixture('general_response.json'), + ), + ); + final tResult = (failure: null, response: tResponse); + when(mockRepository.ping(any)).thenAnswer((_) async => tResult); + + // act + final result = await useCase(tParams); + + // assert + expect(result, tResult); + verify(mockRepository.ping(hostname)); + verifyNoMoreInteractions(mockRepository); + }, + ); + + test( + 'pastikan output dari nilai props', + () async { + // assert + expect( + tParams.props, + [ + tParams.baseUrl, + ], + ); + }, + ); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + tParams.toString(), + 'ParamsPing{baseUrl: ${tParams.baseUrl}}', + ); + }, + ); +} diff --git a/test/feature/presentation/bloc/setup_credential/setup_credential_bloc_test.dart b/test/feature/presentation/bloc/setup_credential/setup_credential_bloc_test.dart new file mode 100644 index 0000000..7da245a --- /dev/null +++ b/test/feature/presentation/bloc/setup_credential/setup_credential_bloc_test.dart @@ -0,0 +1,131 @@ +import 'dart:convert'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:dipantau_desktop_client/core/error/failure.dart'; +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/ping/ping.dart'; +import 'package:dipantau_desktop_client/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../../../../fixture/fixture_reader.dart'; +import '../../../../helper/mock_helper.mocks.dart'; + +void main() { + late SetupCredentialBloc bloc; + late MockHelper mockHelper; + late MockPing mockPing; + + const errorMessage = 'testErrorMessage'; + + setUp(() { + mockHelper = MockHelper(); + mockPing = MockPing(); + bloc = SetupCredentialBloc( + helper: mockHelper, + ping: mockPing, + ); + }); + + test( + 'pastikan output dari initial state', + () async { + // assert + expect( + bloc.state, + isA(), + ); + }, + ); + + group('ping setup credential', () { + const baseUrl = 'https://example.com'; + final params = ParamsPing(baseUrl: baseUrl); + final event = PingSetupCredentialEvent(baseUrl: baseUrl); + + blocTest( + 'pastikan emit [LoadingSetupCredentialState, SuccessPingSetupCredentialState] ketika terima event ' + 'PingSetupCredentialEvent dengan proses berhasil', + build: () { + final response = GeneralResponse.fromJson( + json.decode( + fixture('general_response.json'), + ), + ); + final result = (failure: null, response: response); + when(mockPing(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SetupCredentialBloc bloc) { + return bloc.add(event); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockPing(params)); + }, + ); + + blocTest( + 'pastikan emit [LoadingSetupCredentialState, FailureSetupCredentialState] ketika terima event ' + 'PingSetupCredentialEvent dengan proses gagal dari endpoint', + build: () { + final result = (failure: ServerFailure(errorMessage), response: null); + when(mockPing(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SetupCredentialBloc bloc) { + return bloc.add(event); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockPing(params)); + }, + ); + + blocTest( + 'pastikan emit [LoadingSetupCredentialState, FailureSetupCredentialState] ketika terima event ' + 'PingSetupCredentialEvent dengan kondisi internet tidak terhubung ketika hit endpoint', + build: () { + final result = (failure: ConnectionFailure(), response: null); + when(mockPing(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SetupCredentialBloc bloc) { + return bloc.add(event); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockPing(params)); + }, + ); + + blocTest( + 'pastikan emit [LoadingSetupCredentialState, FailureSetupCredentialState] ketika terima event ' + 'PingSetupCredentialEvent dengan proses gagal parsing respon JSON endpoint', + build: () { + final result = (failure: ParsingFailure(errorMessage), response: null); + when(mockPing(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SetupCredentialBloc bloc) { + return bloc.add(event); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockPing(params)); + }, + ); + }); +} diff --git a/test/feature/presentation/bloc/setup_credential/setup_credential_event_test.dart b/test/feature/presentation/bloc/setup_credential/setup_credential_event_test.dart new file mode 100644 index 0000000..576022b --- /dev/null +++ b/test/feature/presentation/bloc/setup_credential/setup_credential_event_test.dart @@ -0,0 +1,19 @@ +import 'package:dipantau_desktop_client/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('PingSetupCredentialEvent', () { + final event = PingSetupCredentialEvent(baseUrl: 'https://example.com'); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + event.toString(), + 'PingSetupCredentialEvent{baseUrl: ${event.baseUrl}}', + ); + }, + ); + }); +} diff --git a/test/feature/presentation/bloc/setup_credential/setup_credential_state_test.dart b/test/feature/presentation/bloc/setup_credential/setup_credential_state_test.dart new file mode 100644 index 0000000..f20f2ef --- /dev/null +++ b/test/feature/presentation/bloc/setup_credential/setup_credential_state_test.dart @@ -0,0 +1,34 @@ +import 'package:dipantau_desktop_client/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('FailureSetupCredentialState', () { + final state = FailureSetupCredentialState(errorMessage: 'errorMessage'); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + state.toString(), + 'FailureSetupCredentialState{errorMessage: ${state.errorMessage}}', + ); + }, + ); + }); + + group('SuccessPingSetupCredentialState', () { + final state = SuccessPingSetupCredentialState(baseUrl: 'https://example.com'); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + state.toString(), + 'SuccessPingSetupCredentialState{baseUrl: ${state.baseUrl}}', + ); + }, + ); + }); +} diff --git a/test/helper/mock_helper.dart b/test/helper/mock_helper.dart index ab3f6c1..1913f43 100644 --- a/test/helper/mock_helper.dart +++ b/test/helper/mock_helper.dart @@ -4,11 +4,13 @@ import 'package:dipantau_desktop_client/core/network/network_info.dart'; import 'package:dipantau_desktop_client/core/util/helper.dart'; import 'package:dipantau_desktop_client/core/util/shared_preferences_manager.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/auth/auth_remote_data_source.dart'; +import 'package:dipantau_desktop_client/feature/data/datasource/general/general_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/project/project_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/setting/setting_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/track/track_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/user/user_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/auth/auth_repository.dart'; +import 'package:dipantau_desktop_client/feature/domain/repository/general/general_repository.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/project/project_repository.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/setting/setting_repository.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/track/track_repository.dart'; @@ -29,6 +31,7 @@ import 'package:dipantau_desktop_client/feature/domain/usecase/get_track_user/ge import 'package:dipantau_desktop_client/feature/domain/usecase/get_track_user_lite/get_track_user_lite.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/get_user_setting/get_user_setting.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/login/login.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/ping/ping.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/refresh_token/refresh_token.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/reset_password/reset_password.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/send_app_version/send_app_version.dart'; @@ -53,11 +56,13 @@ import 'package:shared_preferences/shared_preferences.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), MockSpec(), MockSpec(), MockSpec(), MockSpec(), MockSpec(), + MockSpec(), MockSpec(), MockSpec(), MockSpec(), @@ -82,5 +87,6 @@ import 'package:shared_preferences/shared_preferences.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), ]) void main() {}