diff --git a/lib/app/app.dart b/lib/app/app.dart index e1cf154..da3b46f 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -18,6 +18,7 @@ class MyApp extends StatelessWidget { initialBinding: InitialBindings(), initialRoute: AppRoutes.splashScreen, getPages: AppPages.routes, + debugShowCheckedModeBanner: false, ); } } diff --git a/lib/data/services/auth_service.dart b/lib/data/services/auth_service.dart index c2bfad6..1922006 100644 --- a/lib/data/services/auth_service.dart +++ b/lib/data/services/auth_service.dart @@ -1,4 +1,3 @@ - import 'package:dio/dio.dart'; import 'package:get/get.dart'; import 'package:quiz_app/core/endpoint/api_endpoint.dart'; @@ -9,16 +8,16 @@ import 'package:quiz_app/data/models/register/register_request.dart'; import 'package:quiz_app/data/providers/dio_client.dart'; class AuthService extends GetxService { - late final Dio _dio; + late final Dio dio; @override void onInit() { - _dio = Get.find().dio; + dio = Get.find().dio; super.onInit(); } Future register(RegisterRequestModel request) async { - var data = await _dio.post( + var data = await dio.post( APIEndpoint.register, data: request.toJson(), ); @@ -31,7 +30,7 @@ class AuthService extends GetxService { Future loginWithEmail(LoginRequestModel request) async { final data = request.toJson(); - final response = await _dio.post(APIEndpoint.login, data: data); + final response = await dio.post(APIEndpoint.login, data: data); if (response.statusCode == 200) { print(response.data); @@ -46,7 +45,7 @@ class AuthService extends GetxService { } Future loginWithGoogle(String idToken) async { - final response = await _dio.post( + final response = await dio.post( APIEndpoint.loginGoogle, data: {"token_id": idToken}, ); diff --git a/lib/data/services/subject_service.dart b/lib/data/services/subject_service.dart index 6cd95ef..83579dd 100644 --- a/lib/data/services/subject_service.dart +++ b/lib/data/services/subject_service.dart @@ -1,39 +1,31 @@ import 'package:dio/dio.dart'; import 'package:get/get.dart'; import 'package:quiz_app/core/endpoint/api_endpoint.dart'; -import 'package:quiz_app/core/utils/logger.dart'; import 'package:quiz_app/data/models/base/base_model.dart'; import 'package:quiz_app/data/models/subject/subject_model.dart'; import 'package:quiz_app/data/providers/dio_client.dart'; class SubjectService extends GetxService { - late final Dio _dio; + late final Dio dio; @override void onInit() { - _dio = Get.find().dio; + dio = Get.find().dio; super.onInit(); } - Future>?> getSubject() async { - try { - final response = await _dio.get( - APIEndpoint.subject, + Future>> getSubject() async { + final response = await dio.get(APIEndpoint.subject); + + if (response.statusCode == 200) { + final parsedResponse = BaseResponseModel>.fromJson( + response.data, + (data) => (data as List).map((e) => SubjectModel.fromJson(e as Map)).toList(), ); - if (response.statusCode == 200) { - final parsedResponse = BaseResponseModel>.fromJson( - response.data, - (data) => (data as List).map((e) => SubjectModel.fromJson(e as Map)).toList(), - ); - - return parsedResponse; - } else { - return null; - } - } catch (e) { - logC.e("Quiz creation error: $e"); - return null; + return parsedResponse; + } else { + throw Exception('Failed to fetch subjects. Status code: ${response.statusCode}'); } } } diff --git a/lib/feature/home/controller/home_controller.dart b/lib/feature/home/controller/home_controller.dart index d1e3bec..ecdc870 100644 --- a/lib/feature/home/controller/home_controller.dart +++ b/lib/feature/home/controller/home_controller.dart @@ -1,6 +1,7 @@ import 'package:get/get.dart'; import 'package:quiz_app/app/const/enums/listing_type.dart'; import 'package:quiz_app/app/routes/app_pages.dart'; +import 'package:quiz_app/core/utils/logger.dart'; import 'package:quiz_app/data/controllers/user_controller.dart'; import 'package:quiz_app/data/models/base/base_model.dart'; import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart'; @@ -42,9 +43,11 @@ class HomeController extends GetxController { } void loadSubjectData() async { - BaseResponseModel>? respnse = await _subjectService.getSubject(); - if (respnse != null) { - subjects.assignAll(respnse.data!); + try { + final response = await _subjectService.getSubject(); + subjects.assignAll(response.data!); + } catch (e) { + logC.e(e); } } diff --git a/lib/feature/quiz_preview/controller/quiz_preview_controller.dart b/lib/feature/quiz_preview/controller/quiz_preview_controller.dart index e9dad1b..c6e5336 100644 --- a/lib/feature/quiz_preview/controller/quiz_preview_controller.dart +++ b/lib/feature/quiz_preview/controller/quiz_preview_controller.dart @@ -6,7 +6,6 @@ import 'package:quiz_app/core/utils/custom_floating_loading.dart'; import 'package:quiz_app/core/utils/custom_notification.dart'; import 'package:quiz_app/core/utils/logger.dart'; import 'package:quiz_app/data/controllers/user_controller.dart'; -import 'package:quiz_app/data/models/base/base_model.dart'; import 'package:quiz_app/data/models/quiz/question_create_request.dart'; import 'package:quiz_app/data/models/quiz/question_listings_model.dart'; import 'package:quiz_app/data/models/quiz/quiestion_data_model.dart'; @@ -57,10 +56,15 @@ class QuizPreviewController extends GetxController { } void loadSubjectData() async { - BaseResponseModel>? respnse = await _subjectService.getSubject(); - if (respnse != null) { - subjects.assignAll(respnse.data!); - subjectId = subjects[0].id; + try { + final response = await _subjectService.getSubject(); + subjects.assignAll(response.data!); + + if (subjects.isNotEmpty) { + subjectId = subjects[0].id; + } + } catch (e) { + logC.e(e); } } diff --git a/lib/feature/search/controller/search_controller.dart b/lib/feature/search/controller/search_controller.dart index d71062a..1759638 100644 --- a/lib/feature/search/controller/search_controller.dart +++ b/lib/feature/search/controller/search_controller.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:quiz_app/app/const/enums/listing_type.dart'; import 'package:quiz_app/app/routes/app_pages.dart'; +import 'package:quiz_app/core/utils/logger.dart'; import 'package:quiz_app/data/models/base/base_model.dart'; import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart'; import 'package:quiz_app/data/models/subject/subject_model.dart'; @@ -63,9 +64,11 @@ class SearchQuizController extends GetxController { ); void loadSubjectData() async { - BaseResponseModel>? respnse = await _subjectService.getSubject(); - if (respnse != null) { - subjects.assignAll(respnse.data!); + try { + final response = await _subjectService.getSubject(); + subjects.assignAll(response.data!); + } catch (e) { + logC.e("Failed to load subjects: $e"); } } diff --git a/pubspec.lock b/pubspec.lock index 6045306..11e8610 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,22 +1,6 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f - url: "https://pub.dev" - source: hosted - version: "82.0.0" - analyzer: - dependency: transitive - description: - name: analyzer - sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0" - url: "https://pub.dev" - source: hosted - version: "7.4.5" args: dependency: transitive description: @@ -41,30 +25,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" - build: - dependency: transitive - description: - name: build - sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - built_collection: - dependency: transitive - description: - name: built_collection - sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" - url: "https://pub.dev" - source: hosted - version: "5.1.1" - built_value: - dependency: transitive - description: - name: built_value - sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 - url: "https://pub.dev" - source: hosted - version: "8.9.5" characters: dependency: transitive description: @@ -81,14 +41,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - code_builder: - dependency: transitive - description: - name: code_builder - sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" - url: "https://pub.dev" - source: hosted - version: "4.10.1" collection: dependency: transitive description: @@ -97,14 +49,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.0" - convert: - dependency: transitive - description: - name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" - source: hosted - version: "3.1.2" crypto: dependency: transitive description: @@ -121,14 +65,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" - dart_style: - dependency: transitive - description: - name: dart_style - sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" - url: "https://pub.dev" - source: hosted - version: "3.0.1" dio: dependency: "direct main" description: @@ -185,14 +121,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -237,14 +165,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.6.6" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" google_fonts: dependency: "direct main" description: @@ -405,22 +325,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.15.0" - mockito: + mocktail: dependency: "direct dev" description: - name: mockito - sha256: f99d8d072e249f719a5531735d146d8cf04c580d93920b04de75bef6dfb2daf6 + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" url: "https://pub.dev" source: hosted - version: "5.4.5" - package_config: - dependency: transitive - description: - name: package_config - sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.dev" - source: hosted - version: "2.2.0" + version: "1.0.4" path: dependency: transitive description: @@ -501,14 +413,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" shared_preferences: dependency: "direct main" description: @@ -586,14 +490,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" - source_gen: - dependency: transitive - description: - name: source_gen - sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" - url: "https://pub.dev" - source: hosted - version: "2.0.0" source_span: dependency: transitive description: @@ -666,14 +562,6 @@ packages: url: "https://pub.dev" source: hosted version: "14.3.0" - watcher: - dependency: transitive - description: - name: watcher - sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" - url: "https://pub.dev" - source: hosted - version: "1.1.1" web: dependency: transitive description: @@ -690,14 +578,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" sdks: dart: ">=3.6.0 <4.0.0" flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index 0c07a01..56cd005 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,7 +49,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - mockito: ^5.4.4 + mocktail: ^1.0.4 # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is diff --git a/test/service/auth_service_test.dart b/test/service/auth_service_test.dart new file mode 100644 index 0000000..a4ed0d4 --- /dev/null +++ b/test/service/auth_service_test.dart @@ -0,0 +1,150 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:dio/dio.dart'; +import 'package:quiz_app/core/endpoint/api_endpoint.dart'; +import 'package:quiz_app/data/models/login/login_request_model.dart'; +import 'package:quiz_app/data/models/register/register_request.dart'; +import 'package:quiz_app/data/services/auth_service.dart'; + +class MockDio extends Mock implements Dio {} + +void main() { + late MockDio mockDio; + late AuthService authService; + + setUp(() { + mockDio = MockDio(); + authService = AuthService(); + authService.dio = mockDio; + }); + + group('AuthService with Mocktail (Using Real Data)', () { + test('Register Success', () async { + final request = RegisterRequestModel( + email: 'danakhdan@gmail.com', + password: '123456', + name: 'Akhdan Robbani', + birthDate: '12-07-2002', + ); + + when(() => mockDio.post(APIEndpoint.register, data: request.toJson())).thenAnswer((_) async => Response( + statusCode: 200, + data: {}, + requestOptions: RequestOptions(path: APIEndpoint.register), + )); + + final result = await authService.register(request); + + expect(result, true); + verify(() => mockDio.post(APIEndpoint.register, data: request.toJson())).called(1); + }); + + test('Register Failure', () async { + final request = RegisterRequestModel( + email: 'danakhdan@gmail.com', + password: '123456', + name: 'Akhdan Robbani', + birthDate: '12-07-2002', + ); + + when(() => mockDio.post(APIEndpoint.register, data: request.toJson())).thenAnswer((_) async => Response( + statusCode: 400, + data: {'message': 'Registration failed'}, + requestOptions: RequestOptions(path: APIEndpoint.register), + )); + + expect(() => authService.register(request), throwsException); + verify(() => mockDio.post(APIEndpoint.register, data: request.toJson())).called(1); + }); + + test('Login With Email Success', () async { + final request = LoginRequestModel(email: 'danakhdan@gmail.com', password: '123456'); + final responseData = { + 'message': 'Login success', + 'data': { + 'id': 'lkasjd93093j4oi234n1234', + 'email': 'danakhdan@gmail.com', + 'name': 'Akhdan Robbani', + 'birth_date': '2002-08-13 07:00:00', + 'pic_url': 'https://example.com/akhdan.png', + 'phone': '081234567890', + 'locale': 'id-ID', + }, + 'meta': null, + }; + + when(() => mockDio.post(APIEndpoint.login, data: request.toJson())).thenAnswer((_) async => Response( + statusCode: 200, + data: responseData, + requestOptions: RequestOptions(path: APIEndpoint.login), + )); + + final result = await authService.loginWithEmail(request); + + expect(result.name, 'Akhdan Robbani'); + expect(result.email, 'danakhdan@gmail.com'); + expect(result.birthDate, DateTime.parse('2002-08-13 07:00:00')); + expect(result.phone, '081234567890'); + expect(result.locale, 'id-ID'); + verify(() => mockDio.post(APIEndpoint.login, data: request.toJson())).called(1); + }); + + test('Login With Email Failure - Invalid Credentials', () async { + final request = LoginRequestModel(email: 'danakhdan@gmail.com', password: 'wrongpassword'); + + when(() => mockDio.post(APIEndpoint.login, data: request.toJson())).thenAnswer((_) async => Response( + statusCode: 401, + data: {'message': 'Invalid credentials'}, + requestOptions: RequestOptions(path: APIEndpoint.login), + )); + + expect(() => authService.loginWithEmail(request), throwsException); + verify(() => mockDio.post(APIEndpoint.login, data: request.toJson())).called(1); + }); + + test('Login With Google Success', () async { + final idToken = 'valid_google_token'; + final responseData = { + 'message': 'Login success', + 'data': { + 'id': '680e5a6d2f480bd75db17a09', + 'email': 'danakhdan@gmail.com', + 'name': 'Akhdan Robbani', + 'birth_date': '2002-08-13 07:00:00', + 'pic_url': null, + 'phone': '081234567890', + 'locale': 'id-ID' + }, + 'meta': null + }; + + when(() => mockDio.post(APIEndpoint.loginGoogle, data: {"token_id": idToken})).thenAnswer((_) async => Response( + statusCode: 200, + data: responseData, + requestOptions: RequestOptions(path: APIEndpoint.loginGoogle), + )); + + final result = await authService.loginWithGoogle(idToken); + + expect(result.name, 'Akhdan Robbani'); + expect(result.email, 'danakhdan@gmail.com'); + expect(result.birthDate, DateTime.parse('2002-08-13 07:00:00')); + expect(result.phone, '081234567890'); + expect(result.locale, 'id-ID'); + verify(() => mockDio.post(APIEndpoint.loginGoogle, data: {"token_id": idToken})).called(1); + }); + + test('Login With Google Failure - Invalid Token', () async { + final idToken = 'invalid_google_token'; + + when(() => mockDio.post(APIEndpoint.loginGoogle, data: {"token_id": idToken})).thenAnswer((_) async => Response( + statusCode: 401, + data: {'message': 'Invalid Google token'}, + requestOptions: RequestOptions(path: APIEndpoint.loginGoogle), + )); + + expect(() => authService.loginWithGoogle(idToken), throwsException); + verify(() => mockDio.post(APIEndpoint.loginGoogle, data: {"token_id": idToken})).called(1); + }); + }); +} diff --git a/test/test_helper/auth_service_test.mock.dart b/test/test_helper/auth_service_test.mock.dart new file mode 100644 index 0000000..af25380 --- /dev/null +++ b/test/test_helper/auth_service_test.mock.dart @@ -0,0 +1,8 @@ +// // Create this in a separate file, e.g., auth_service_test.mocks.dart + +// import 'package:dio/dio.dart'; +// import 'package:mockito/annotations.dart'; +// import 'package:quiz_app/data/providers/dio_client.dart'; + +// @GenerateMocks([Dio, ApiClient]) +// void main() {} diff --git a/test/test_helper/subject_service_test_mock.dart b/test/test_helper/subject_service_test_mock.dart new file mode 100644 index 0000000..a909c9e --- /dev/null +++ b/test/test_helper/subject_service_test_mock.dart @@ -0,0 +1,83 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:dio/dio.dart'; +import 'package:quiz_app/core/endpoint/api_endpoint.dart'; +import 'package:quiz_app/data/services/subject_service.dart'; + +class MockDio extends Mock implements Dio {} + +void main() { + late MockDio mockDio; + late SubjectService subjectService; + + setUp(() { + mockDio = MockDio(); + subjectService = SubjectService(); + subjectService.dio = mockDio; + }); + + group('SubjectService Tests', () { + test('getSubject - Success', () async { + final responseData = { + 'message': 'Subjects fetched successfully', + 'data': [ + { + 'id': 'subject1', + 'name': 'Mathematics', + }, + { + 'id': 'subject2', + 'name': 'Science', + }, + ], + 'meta': null, + }; + + when(() => mockDio.get(APIEndpoint.subject)).thenAnswer((_) async => Response( + statusCode: 200, + data: responseData, + requestOptions: RequestOptions(path: APIEndpoint.subject), + )); + + final result = await subjectService.getSubject(); + + expect(result.data!.length, 2); + expect(result.data![0].id, 'subject1'); + expect(result.data![0].name, 'Mathematics'); + expect(result.data![1].id, 'subject2'); + expect(result.data![1].name, 'Science'); + + verify(() => mockDio.get(APIEndpoint.subject)).called(1); + }); + + test('getSubject 400 validation issue', () async { + when(() => mockDio.get(APIEndpoint.subject)).thenAnswer((_) async => Response( + statusCode: 400, + data: {'message': 'Bad Request'}, + requestOptions: RequestOptions(path: APIEndpoint.subject), + )); + + expect( + () => subjectService.getSubject(), + throwsA(isA()), + ); + + verify(() => mockDio.get(APIEndpoint.subject)).called(1); + }); + + test('getSubject (Network Error)', () async { + when(() => mockDio.get(APIEndpoint.subject)).thenThrow(DioException( + requestOptions: RequestOptions(path: APIEndpoint.subject), + error: 'Network Error', + type: DioExceptionType.connectionError, + )); + + expect( + () => subjectService.getSubject(), + throwsA(isA()), + ); + + verify(() => mockDio.get(APIEndpoint.subject)).called(1); + }); + }); +}