diff --git a/.env b/.env new file mode 100644 index 0000000..b0c15c7 --- /dev/null +++ b/.env @@ -0,0 +1,7 @@ +apiKey= +appId= +messagingSenderId= +projectId= +storageBucket= +iosClientId= +iosBundleId= diff --git a/.gitignore b/.gitignore index 4cafd2e..16cfb20 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ .buildlog/ .history .svn/ +*.env # IntelliJ related *.iml @@ -42,3 +43,9 @@ app.*.map.json # Exceptions to above rules. !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages + +# Firebase +android/app/google-services.json +ios/firebase_app_id_file.json +ios/Runner/GoogleService-Info.plist + diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..0316c22 --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,70 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static FirebaseOptions android = FirebaseOptions( + apiKey: dotenv.env['apiKey'] ?? "", + appId: dotenv.env['appId'] ?? "", + messagingSenderId: dotenv.env['messagingSenderId'] ?? "", + projectId: dotenv.env['projectId'] ?? "", + storageBucket: dotenv.env['storageBucket'] ?? "", + ); + + static FirebaseOptions ios = FirebaseOptions( + apiKey: dotenv.env['apiKey'] ?? "", + appId: dotenv.env['appId'] ?? "", + messagingSenderId: dotenv.env['messagingSenderId'] ?? "", + projectId: dotenv.env['projectId'] ?? "", + storageBucket: dotenv.env['storageBucket'] ?? "", + iosClientId: dotenv.env['iosClientId'] ?? "", + iosBundleId: dotenv.env['iosBundleId'] ?? "", + ); +} diff --git a/lib/main.dart b/lib/main.dart index 9be3d34..06b8634 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,10 @@ // ignore_for_file: import_of_legacy_library_into_null_safe +import 'dart:async'; + +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:hive/hive.dart'; import 'package:injectable/injectable.dart'; import 'package:path_provider/path_provider.dart'; @@ -11,23 +15,39 @@ import 'package:rutorrentflutter/services/state_services/user_preferences_servic import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/theme/app_theme.dart'; import 'package:rutorrentflutter/ui/widgets/smart_widgets/bottom_sheets/bottom_sheet_setup.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'firebase_options.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; void main() async { - WidgetsFlutterBinding.ensureInitialized(); - //Setting up Hive DB - final appDir = await getApplicationDocumentsDirectory(); - Hive.init(appDir.path); - await Hive.openBox('DB'); - //To work in development environment, simply change the environment to Environment.dev below - setupLocator(environment: Environment.prod); - //Setting custom Bottom Sheet - setUpBottomSheetUi(); - //Setting up Services - locator().init(); - await locator().init(); - runApp(MyApp()); + //Zone guarded for Firebase Crashlytics to catch errors + runZonedGuarded>(() async { + WidgetsFlutterBinding.ensureInitialized(); + //Setting up Hive DB + final appDir = await getApplicationDocumentsDirectory(); + Hive.init(appDir.path); + await Hive.openBox('DB'); + //To work in development environment, simply change the environment to Environment.dev below + setupLocator(environment: Environment.prod); + //Setting custom Bottom Sheet + setUpBottomSheetUi(); + //Setting up Services + locator().init(); + await locator().init(); + //Setting up Firebase + await dotenv.load(fileName: ".env"); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + + // Make sure to comment out this line in development to see Errors + // FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError; + + runApp(MyApp()); + }, + (error, stack) => + FirebaseCrashlytics.instance.recordError(error, stack, fatal: true)); } class MyApp extends StatelessWidget { diff --git a/pubspec.lock b/pubspec.lock index bfe0fbe..54e58b5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -239,6 +239,62 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + firebase_analytics: + dependency: "direct main" + description: + name: firebase_analytics + url: "https://pub.dartlang.org" + source: hosted + version: "9.3.3" + firebase_analytics_platform_interface: + dependency: transitive + description: + name: firebase_analytics_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.3" + firebase_analytics_web: + dependency: transitive + description: + name: firebase_analytics_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.2+3" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + url: "https://pub.dartlang.org" + source: hosted + version: "1.21.1" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "4.5.1" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.2" + firebase_crashlytics: + dependency: "direct main" + description: + name: firebase_crashlytics + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.9" + firebase_crashlytics_platform_interface: + dependency: transitive + description: + name: firebase_crashlytics_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.15" fixnum: dependency: transitive description: @@ -272,6 +328,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.2" flutter_launcher_icons: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7c16aa3..962e46c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: rutorrentflutter description: A rutorrent-based client in flutter publish_to: "none" -version: 1.0.0-alpha+4 +version: 1.0.0-alpha+6 environment: sdk: ">=2.12.0-0 <3.0.0" @@ -45,6 +45,10 @@ dependencies: webview_flutter_plus: ^0.3.0+2 percent_indicator: ^4.2.2 logger: + firebase_core: ^1.21.1 + firebase_analytics: ^9.3.3 + firebase_crashlytics: ^2.8.9 + flutter_dotenv: ^5.0.2 # webviewx: ^0.2.1 dev_dependencies: @@ -66,6 +70,7 @@ flutter: assets: - assets/logo/ - assets/animation/ + - .env fonts: - family: SFUIDisplay diff --git a/test/helpers/mock_ioclient.dart b/test/helpers/mock_ioclient.dart index fe5ed88..6720c29 100644 --- a/test/helpers/mock_ioclient.dart +++ b/test/helpers/mock_ioclient.dart @@ -1,5 +1,4 @@ import 'dart:convert'; - import 'package:http/http.dart'; import 'package:logger/logger.dart'; import 'package:rutorrentflutter/app/app.logger.dart'; @@ -11,26 +10,37 @@ Logger log = getLogger("MockIOClientExtention"); /// The [MockIOClientExtention] class helps in mocking API calls /// and returning the proper response. class MockIOClientExtention extends MockIOClient { + @override post(Uri? url, {Map? headers, Object? body, Encoding? encoding}) async { - log.e(url.toString()); - log.e(TestData.historyPluginUrl); - log.e(TestData.diskSpacePluginUrl); - log.e(headers); - log.e(body); - switch (url.toString()) { case (TestData.historyPluginUrl): return Response(TestData.updateHistoryJSONReponse, 200); case (TestData.httpRpcPluginUrl): - log.e(body, TestData.getAllAccountsTorrentListBody); - log.e(body.toString() == - TestData.getAllAccountsTorrentListBody.toString()); - return body.toString() == - TestData.getAllAccountsTorrentListBody.toString() - ? Response(TestData.getAllAccountsTorrentListResponse, 200) - : Response("", 404); + if (body.toString() == + TestData.getAllAccountsTorrentListBody.toString()) + return Response(TestData.getAllAccountsTorrentListResponse, 200); + else if (body.toString() == TestData.getFilesBody.toString()) + return Response(jsonEncode(TestData.getFilesResponse), 200); + else if (body.toString() == TestData.getTrackersBody.toString()) + return Response(jsonEncode(TestData.getTrackersResponse), 200); + + return Response("", 404); + + case (TestData.explorerPluginUrl): + if (body.toString() == TestData.getDiskFilesBody.toString()) + return Response(TestData.getDiskFilesResponse.toString(), 200); + + return Response("", 404); + + case (TestData.rssPluginUrl): + if (body == null) + return Response(jsonEncode(TestData.loadRSSResponse), 200); + else if (body.toString() == TestData.getRSSFiltersBody.toString()) + return Response(jsonEncode(TestData.getRSSFiltersResponse), 200); + + return Response("", 404); case "": return Response("", 200); @@ -42,11 +52,6 @@ class MockIOClientExtention extends MockIOClient { @override Future get(Uri? url, {Map? headers}) async { - log.e(url.toString()); - log.e(TestData.historyPluginUrl); - log.e(TestData.diskSpacePluginUrl); - log.e(headers); - switch (url.toString()) { case (TestData.diskSpacePluginUrl): return Response(TestData.updateDiskSpaceResponse, 200); diff --git a/test/helpers/test_data.dart b/test/helpers/test_data.dart index 192f87d..cc67f2d 100644 --- a/test/helpers/test_data.dart +++ b/test/helpers/test_data.dart @@ -2,12 +2,14 @@ import 'package:flutter/foundation.dart'; import 'package:rutorrentflutter/models/account.dart'; import 'package:rutorrentflutter/models/history_item.dart'; +import 'package:rutorrentflutter/models/rss_filter.dart'; import 'package:rutorrentflutter/models/torrent.dart'; import 'package:rutorrentflutter/services/services_info.dart'; /// This class contains the test data used in tests to remove non-deterministic behaviour class TestData { static const url = "http://localhost:8080"; + static const String hash = "EB25F7EC8FDE4DA888C197DB0975FFF549C9D7FB"; static ValueNotifier> accounts = ValueNotifier([ Account(username: "test", password: "test", url: "http://localhost:8080") @@ -24,6 +26,7 @@ class TestData { ]; static Torrent get torrent => Torrent("dddd"); + static RSSFilter get rssFilter => RSSFilter("", 0, "", "", "", ""); static const httpRpcPluginUrl = url + '/plugins/httprpc/action.php'; diff --git a/test/service_tests/api_service_test.dart b/test/service_tests/api_service_test.dart index 121031b..f41fe66 100644 --- a/test/service_tests/api_service_test.dart +++ b/test/service_tests/api_service_test.dart @@ -1,6 +1,12 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:rutorrentflutter/models/disk_file.dart'; +import 'package:rutorrentflutter/models/rss.dart'; +import 'package:rutorrentflutter/models/torrent_file.dart'; import 'package:rutorrentflutter/services/api/prod_api_service.dart'; +import 'package:rutorrentflutter/services/functional_services/disk_space_service.dart'; +import 'package:rutorrentflutter/services/state_services/disk_file_service.dart'; import 'package:rutorrentflutter/services/state_services/history_service.dart'; +import 'package:rutorrentflutter/services/state_services/torrent_service.dart'; import '../helpers/test_data.dart'; import '../helpers/test_helpers.dart'; @@ -11,6 +17,25 @@ void main() { setUp(() => registerServices()); tearDown(() => unregisterServices()); + group('Get History -', () { + test( + 'When get history network call made, should populate history items', + () async { + HistoryService historyservice = + getAndRegisterHistoryService(mock: false); + ProdApiService api = ProdApiService(); + + //Mock Api Call + await api.getHistory(); + + expect(historyservice.historyList, isNotNull); + + //Expect [historyservice.historyList.runtimeType] to be same + expect(historyservice.historyList[0].runtimeType, + TestData.historyItems[0].runtimeType); + }); + }); + group('Update History -', () { test( 'When update history network call made, should populate history items', @@ -29,6 +54,104 @@ void main() { TestData.historyItems[0].runtimeType); }); }); + + group('Update Disk Space -', () { + test( + 'When update diskspace call made, should populate diskspace object', + () async { + DiskSpaceService _diskSpaceService = + getAndRegisterDiskSpaceService(mock: false); + ProdApiService api = ProdApiService(); + + //Mock Api Call + await api.updateDiskSpace(); + + expect(_diskSpaceService.diskSpace, isNotNull); + + bool isTotalUpdated = _diskSpaceService.diskSpace.total != -1; + expect(isTotalUpdated, true); + }); + }); + group('Get All Accounts Torrent List -', () { + test( + 'When update all accounts torrent list call made, should populate torrent list', + () async { + TorrentService _torrentService = + getAndRegisterTorrentService(mock: false); + ProdApiService api = ProdApiService(); + + //Mock Api Call + await api.getAllAccountsTorrentList().listen((event) {}).cancel(); + + expect(_torrentService.torrentsList.value, isNotEmpty); + expect(_torrentService.torrentsList.value[0].runtimeType, + TestData.torrent.runtimeType); + }); + }); + group('Get Disk Files -', () { + test('When get disk files call made, should populate disk file list', + () async { + DiskFileService _diskFileService = + getAndRegisterDiskFileService(mock: false); + ProdApiService api = ProdApiService(); + + //Mock Api Call + await api.getDiskFiles("/"); + + expect(_diskFileService.diskFileList.value, isNotEmpty); + expect(_diskFileService.diskFileList.value[0].runtimeType, + DiskFile().runtimeType); + }); + }); + group('Get Files -', () { + test( + 'When files from Torrent call made, should return torrent file list', + () async { + ProdApiService api = ProdApiService(); + + //Mock Api Call + var response = await api.getFiles(TestData.hash); + + expect(response, isNotEmpty); + expect( + response.runtimeType, [TorrentFile("", "", "", "")].runtimeType); + }); + }); + group('Get Trackers -', () { + test('When getTrackers call made, should return trackers list', + () async { + ProdApiService api = ProdApiService(); + + //Mock Api Call + var response = await api.getTrackers(TestData.hash); + + expect(response, isNotEmpty); + expect(response.runtimeType, [""].runtimeType); + }); + }); + group('Load RSS -', () { + test('When loadRSS call made, should return RSS Label list', () async { + ProdApiService api = ProdApiService(); + + //Mock Api Call + var response = await api.loadRSS(); + + expect(response, isNotEmpty); + expect( + response.runtimeType, [RSSLabel(TestData.hash, "")].runtimeType); + }); + }); + group('Get RSS Filters -', () { + test('When loadRSS call made, should return RSS Label list', () async { + ProdApiService api = ProdApiService(); + + //Mock Api Call + var response = await api.getRSSFilters(); + + expect(response, isNotEmpty); + expect(response.runtimeType, [TestData.rssFilter].runtimeType); + }); + }); }); }); }