diff --git a/.github/workflows/android.yaml b/.github/workflows/android.yaml index a9d49403..4199cf28 100644 --- a/.github/workflows/android.yaml +++ b/.github/workflows/android.yaml @@ -6,9 +6,6 @@ on: push: branches: - master - pull_request: - branches: - - master jobs: diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..f3ae7c75 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,70 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + branches: + - master + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INVENTREE_DB_ENGINE: django.db.backends.sqlite3 + INVENTREE_DB_NAME: ../inventree_unit_test_db.sqlite3 + INVENTREE_MEDIA_ROOT: ../test_inventree_media + INVENTREE_STATIC_ROOT: ../test_inventree_static + INVENTREE_ADMIN_USER: testuser + INVENTREE_ADMIN_PASSWORD: testpassword + INVENTREE_ADMIN_EMAIL: test@test.com + INVENTREE_PYTHON_TEST_SERVER: http://localhost:12345 + INVENTREE_PYTHON_TEST_USERNAME: testuser + INVENTREE_PYTHON_TEST_PASSWORD: testpassword +jobs: + + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + submodules: recursive + - name: Setup Java + uses: actions/setup-java@v1 + with: + java-version: '12.x' + - name: Setup Flutter + uses: subosito/flutter-action@v1 + with: + flutter-version: '2.10.3' + - name: Collect Translation Files + run: | + cd lib/l10n + python3 collect_translations.py + - name: Static Analysis Tests + run: | + cp lib/dummy_dsn.dart lib/dsn.dart + python3 find_dart_files.py + flutter pub get + flutter analyze + + - name: Start InvenTree Server + run: | + sudo apt-get install python3-dev python3-pip python3-venv python3-wheel g++ + pip3 install invoke + git clone --depth 1 https://github.com/inventree/inventree ./inventree_server + cd inventree_server + invoke install + invoke migrate + invoke import-fixtures + invoke server -a 127.0.0.1:12345 & + invoke wait + - name: Unit Tests + run: | + flutter test --coverage + + - name: Coveralls + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ios.yaml b/.github/workflows/ios.yaml index 039a6e9a..3dc6338c 100644 --- a/.github/workflows/ios.yaml +++ b/.github/workflows/ios.yaml @@ -6,9 +6,6 @@ on: push: branches: - master - pull_request: - branches: - - master jobs: diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml deleted file mode 100644 index 44b1ed3b..00000000 --- a/.github/workflows/lint.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# Run flutter linting checks - -name: lint - -on: - push: - branches: - - master - pull_request: - branches: - - master - -jobs: - - lint: - runs-on: ubuntu-latest - - env: - SENTRY_DSN: ${{ secrets.SENTRY_DSN }} - - steps: - - name: Checkout code - uses: actions/checkout@v2 - with: - submodules: recursive - - name: Setup Java - uses: actions/setup-java@v1 - with: - java-version: '12.x' - - name: Setup Flutter - uses: subosito/flutter-action@v1 - with: - flutter-version: '2.10.3' - - name: Collect Translation Files - run: | - cd lib/l10n - python3 collect_translations.py - - run: flutter pub get - - run: cp lib/dummy_dsn.dart lib/dsn.dart - - run: flutter analyze - - run: flutter test --coverage diff --git a/.gitignore b/.gitignore index 2f258989..07101c9b 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ coverage/* +# This file is auto-generated as part of the CI process +test/coverage_helper_test.dart + # Sentry API key lib/dsn.dart diff --git a/find_dart_files.py b/find_dart_files.py new file mode 100644 index 00000000..a182daaa --- /dev/null +++ b/find_dart_files.py @@ -0,0 +1,46 @@ +""" +This script recursively finds any '.dart' files in the ./lib directory, +and generates a 'test' file which includes all these files. + +This is to ensure that *all* .dart files are included in test coverage. +By default, source files which are not touched by the unit tests are not included! + +Ref: https://github.com/flutter/flutter/issues/27997 +""" + +from pathlib import Path + +if __name__ == '__main__': + + dart_files = Path('lib').rglob('*.dart') + + with open("test/coverage_helper_test.dart", "w") as f: + + f.write("// ignore_for_file: unused_import\n\n") + + skips = [ + 'generated', + 'l10n', + 'dsn.dart', + ] + + for path in dart_files: + path = str(path) + + if any([s in path for s in skips]): + continue + + # Remove leading 'lib\' text + path = path[4:] + path = path.replace('\\', '/') + f.write(f'import "package:inventree/{path}";\n') + + f.write("\n\n") + + f.write("// DO NOT EDIT THIS FILE - it has been auto-generated by 'find_dart_files.py'\n") + f.write("// It has been created to ensure that *all* source file are included in coverage data\n") + + f.write('import "package:test/test.dart";\n\n'); + + f.write("// Do not actually test anything!\n") + f.write("void main() {}\n") diff --git a/lib/api.dart b/lib/api.dart index 9994b655..52bd29a1 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -6,7 +6,7 @@ import "package:flutter/foundation.dart"; import "package:http/http.dart" as http; import "package:intl/intl.dart"; import "package:inventree/app_colors.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; import "package:open_file/open_file.dart"; import "package:cached_network_image/cached_network_image.dart"; @@ -16,6 +16,7 @@ import "package:flutter_cache_manager/flutter_cache_manager.dart"; import "package:inventree/widget/dialogs.dart"; import "package:inventree/l10.dart"; +import "package:inventree/helpers.dart"; import "package:inventree/inventree/sentry.dart"; import "package:inventree/inventree/model.dart"; import "package:inventree/user_profile.dart"; @@ -201,11 +202,13 @@ class InvenTreeAPI { // Authentication token (initially empty, must be requested) String _token = ""; + bool get hasToken => _token.isNotEmpty; + /* * Check server connection and display messages if not connected. * Useful as a precursor check before performing operations. */ - bool checkConnection(BuildContext context) { + bool checkConnection() { // Firstly, is the server connected? if (!isConnected()) { @@ -278,7 +281,7 @@ class InvenTreeAPI { bool _connecting = false; bool isConnected() { - return profile != null && _connected && baseUrl.isNotEmpty && _token.isNotEmpty; + return profile != null && _connected && baseUrl.isNotEmpty && hasToken; } bool isConnecting() { @@ -289,14 +292,10 @@ class InvenTreeAPI { static final InvenTreeAPI _api = InvenTreeAPI._internal(); // API endpoint for receiving purchase order line items was introduced in v12 - bool supportPoReceive() { - return apiVersion >= 12; - } + bool get supportsPoReceive => apiVersion >= 12; // "Modern" API transactions were implemented in API v14 - bool supportModernStockTransactions() { - return apiVersion >= 14; - } + bool get supportsModernStockTransactions => apiVersion >= 14; /* * Connect to the remote InvenTree server: @@ -338,7 +337,7 @@ class InvenTreeAPI { // Clear the list of available plugins _plugins.clear(); - print("Connecting to ${apiUrl} -> username=${username}"); + debug("Connecting to ${apiUrl} -> username=${username}"); APIResponse response; @@ -431,7 +430,7 @@ class InvenTreeAPI { // Return the received token _token = (data["token"] ?? "") as String; - print("Received token - $_token"); + debug("Received token from server"); // Request user role information (async) getUserRoles(); @@ -445,7 +444,7 @@ class InvenTreeAPI { } void disconnectFromServer() { - print("InvenTreeAPI().disconnectFromServer()"); + debug("API : disconnectFromServer()"); _connected = false; _connecting = false; @@ -501,7 +500,7 @@ class InvenTreeAPI { roles.clear(); - print("Requesting user role data"); + debug("API: Requesting user role data"); // Next we request the permissions assigned to the current user // Note: 2021-02-27 this "roles" feature for the API was just introduced. @@ -531,7 +530,7 @@ class InvenTreeAPI { return; } - print("Requesting plugin information"); + debug("API: getPluginInformation()"); // Request a list of plugins from the server final List results = await InvenTreePlugin().list(); @@ -661,7 +660,7 @@ class InvenTreeAPI { _request.headers.set(HttpHeaders.acceptLanguageHeader, Intl.getCurrentLocale()); } on SocketException catch (error) { - print("SocketException at ${url}: ${error.toString()}"); + debug("SocketException at ${url}: ${error.toString()}"); showServerError(L10().connectionRefused, error.toString()); return; } on TimeoutException { @@ -670,7 +669,7 @@ class InvenTreeAPI { return; } on HandshakeException catch (error) { print("HandshakeException at ${url}:"); - print(error.toString()); + debug(error.toString()); showServerError(L10().serverCertificateError, error.toString()); return; } catch (error, stackTrace) { @@ -1233,8 +1232,6 @@ class InvenTreeAPI { var plugins = getPlugins(mixin: "locate"); - print("locateItemOrLocation"); - if (plugins.isEmpty) { // TODO: Error message return; diff --git a/lib/api_form.dart b/lib/api_form.dart index 07221a1e..588658bc 100644 --- a/lib/api_form.dart +++ b/lib/api_form.dart @@ -335,7 +335,7 @@ class APIFormField { controller.text = hash; data["value"] = hash; - successTone(); + barcodeSuccessTone(); showSnackIcon( L10().barcodeAssigned, diff --git a/lib/app_colors.dart b/lib/app_colors.dart index 99d81384..2f0395f6 100644 --- a/lib/app_colors.dart +++ b/lib/app_colors.dart @@ -1,5 +1,3 @@ - - import "dart:ui"; const Color COLOR_GRAY = Color.fromRGBO(50, 50, 50, 1); diff --git a/lib/app_settings.dart b/lib/app_settings.dart deleted file mode 100644 index e009b784..00000000 --- a/lib/app_settings.dart +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Class for managing app-level configuration options - */ - -import "package:sembast/sembast.dart"; -import "package:inventree/preferences.dart"; - -// Settings key values -const String INV_HOME_SHOW_SUBSCRIBED = "homeShowSubscribed"; -const String INV_HOME_SHOW_PO = "homeShowPo"; -const String INV_HOME_SHOW_MANUFACTURERS = "homeShowManufacturers"; -const String INV_HOME_SHOW_CUSTOMERS = "homeShowCustomers"; -const String INV_HOME_SHOW_SUPPLIERS = "homeShowSuppliers"; - -const String INV_SOUNDS_BARCODE = "barcodeSounds"; -const String INV_SOUNDS_SERVER = "serverSounds"; - -const String INV_PART_SUBCATEGORY = "partSubcategory"; - -const String INV_STOCK_SUBLOCATION = "stockSublocation"; -const String INV_STOCK_SHOW_HISTORY = "stockShowHistory"; - -const String INV_REPORT_ERRORS = "reportErrors"; - -const String INV_STRICT_HTTPS = "strictHttps"; - -class InvenTreeSettingsManager { - - factory InvenTreeSettingsManager() { - return _manager; - } - - InvenTreeSettingsManager._internal(); - - final store = StoreRef("settings"); - - Future get _db async => InvenTreePreferencesDB.instance.database; - - Future getValue(String key, dynamic backup) async { - - final value = await store.record(key).get(await _db); - - if (value == null) { - return backup; - } - - return value; - } - - // Load a boolean setting - Future getBool(String key, bool backup) async { - final dynamic value = await getValue(key, backup); - - if (value is bool) { - return value; - } else { - return backup; - } - } - - Future setValue(String key, dynamic value) async { - - await store.record(key).put(await _db, value); - } - - // Ensure we only ever create a single instance of this class - static final InvenTreeSettingsManager _manager = InvenTreeSettingsManager._internal(); -} diff --git a/lib/barcode.dart b/lib/barcode.dart index 8781f2d1..85702e0e 100644 --- a/lib/barcode.dart +++ b/lib/barcode.dart @@ -11,15 +11,38 @@ import "package:qr_code_scanner/qr_code_scanner.dart"; import "package:inventree/inventree/stock.dart"; import "package:inventree/inventree/part.dart"; -import "package:inventree/l10.dart"; -import "package:inventree/helpers.dart"; import "package:inventree/api.dart"; +import "package:inventree/helpers.dart"; +import "package:inventree/l10.dart"; +import "package:inventree/preferences.dart"; import "package:inventree/widget/location_display.dart"; import "package:inventree/widget/part_detail.dart"; import "package:inventree/widget/stock_detail.dart"; +/* + * Play an audible 'success' alert to the user. + */ +Future barcodeSuccessTone() async { + + final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool; + + if (en) { + playAudioFile("sounds/barcode_scan.mp3"); + } +} + +Future barcodeFailureTone() async { + + final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool; + + if (en) { + playAudioFile("sounds/barcode_error.mp3"); + } +} + + class BarcodeHandler { /* * Class which "handles" a barcode, by communicating with the InvenTree server, @@ -44,7 +67,7 @@ class BarcodeHandler { // Called when the server does not know about a barcode // Override this function - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().barcodeNoMatch, @@ -55,7 +78,7 @@ class BarcodeHandler { Future onBarcodeUnhandled(BuildContext context, Map data) async { - failureTone(); + barcodeFailureTone(); // Called when the server returns an unhandled response showServerError(L10().responseUnknown, data.toString()); @@ -125,7 +148,7 @@ class BarcodeScanHandler extends BarcodeHandler { @override Future onBarcodeUnknown(BuildContext context, Map data) async { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().barcodeNoMatch, @@ -146,7 +169,7 @@ class BarcodeScanHandler extends BarcodeHandler { if (pk > 0) { - successTone(); + barcodeSuccessTone(); InvenTreeStockLocation().get(pk).then((var loc) { if (loc is InvenTreeStockLocation) { @@ -156,7 +179,7 @@ class BarcodeScanHandler extends BarcodeHandler { }); } else { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().invalidStockLocation, @@ -170,7 +193,7 @@ class BarcodeScanHandler extends BarcodeHandler { if (pk > 0) { - successTone(); + barcodeSuccessTone(); InvenTreeStockItem().get(pk).then((var item) { @@ -183,7 +206,7 @@ class BarcodeScanHandler extends BarcodeHandler { }); } else { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().invalidStockItem, @@ -196,7 +219,7 @@ class BarcodeScanHandler extends BarcodeHandler { if (pk > 0) { - successTone(); + barcodeSuccessTone(); InvenTreePart().get(pk).then((var part) { @@ -209,7 +232,7 @@ class BarcodeScanHandler extends BarcodeHandler { }); } else { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().invalidPart, @@ -218,7 +241,7 @@ class BarcodeScanHandler extends BarcodeHandler { } } else { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().barcodeUnknown, @@ -275,7 +298,7 @@ class StockItemScanIntoLocationHandler extends BarcodeHandler { if (result) { - successTone(); + barcodeSuccessTone(); Navigator.of(context).pop(); @@ -285,7 +308,7 @@ class StockItemScanIntoLocationHandler extends BarcodeHandler { ); } else { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().barcodeScanIntoLocationFailure, @@ -294,7 +317,7 @@ class StockItemScanIntoLocationHandler extends BarcodeHandler { } } else { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().invalidStockLocation, @@ -329,14 +352,14 @@ class StockLocationScanInItemsHandler extends BarcodeHandler { if (item == null) { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().invalidStockItem, success: false, ); } else if (item.locationId == location.pk) { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().itemInLocation, @@ -347,7 +370,7 @@ class StockLocationScanInItemsHandler extends BarcodeHandler { if (result) { - successTone(); + barcodeSuccessTone(); showSnackIcon( L10().barcodeScanIntoLocationSuccess, @@ -355,7 +378,7 @@ class StockLocationScanInItemsHandler extends BarcodeHandler { ); } else { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().barcodeScanIntoLocationFailure, @@ -365,7 +388,7 @@ class StockLocationScanInItemsHandler extends BarcodeHandler { } } else { - failureTone(); + barcodeFailureTone(); // Does not match a valid stock item! showSnackIcon( @@ -401,7 +424,7 @@ class UniqueBarcodeHandler extends BarcodeHandler { @override Future onBarcodeMatched(BuildContext context, Map data) async { - failureTone(); + barcodeFailureTone(); // If the barcode is known, we can"t assign it to the stock item! showSnackIcon( @@ -424,7 +447,7 @@ class UniqueBarcodeHandler extends BarcodeHandler { String hash = (data["hash"] ?? "") as String; if (hash.isEmpty) { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().barcodeError, @@ -432,7 +455,7 @@ class UniqueBarcodeHandler extends BarcodeHandler { ); } else { - successTone(); + barcodeSuccessTone(); // Close the barcode scanner Navigator.of(context).pop(); diff --git a/lib/helpers.dart b/lib/helpers.dart index 524cd92a..24dc6e84 100644 --- a/lib/helpers.dart +++ b/lib/helpers.dart @@ -7,8 +7,22 @@ * supressing trailing zeroes */ +import "dart:io"; + import "package:audioplayers/audioplayers.dart"; -import "package:inventree/app_settings.dart"; +import "package:one_context/one_context.dart"; + + +/* + * Display a debug message if we are in testing mode, or running in debug mode + */ +void debug(dynamic msg) { + + if (Platform.environment.containsKey("FLUTTER_TEST")) { + print("DEBUG: ${msg.toString()}"); + } +} + String simpleNumberString(double number) { // Ref: https://stackoverflow.com/questions/55152175/how-to-remove-trailing-zeros-using-dart @@ -16,22 +30,19 @@ String simpleNumberString(double number) { return number.toStringAsFixed(number.truncateToDouble() == number ? 0 : 1); } -Future successTone() async { +/* + * Play an audio file from the requested path. + * + * Note: If OneContext module fails the 'hasContext' check, + * we will not attempt to play the sound + */ +Future playAudioFile(String path) async { - final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool; - - if (en) { - final player = AudioCache(); - player.play("sounds/barcode_scan.mp3"); + if (!OneContext.hasContext) { + return; } + + final player = AudioCache(); + player.play(path); + } - -Future failureTone() async { - - final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool; - - if (en) { - final player = AudioCache(); - player.play("sounds/barcode_error.mp3"); - } -} \ No newline at end of file diff --git a/lib/inventree/sentry.dart b/lib/inventree/sentry.dart index 429a50af..cd05a14f 100644 --- a/lib/inventree/sentry.dart +++ b/lib/inventree/sentry.dart @@ -1,7 +1,7 @@ import "dart:io"; import "package:device_info_plus/device_info_plus.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; import "package:package_info_plus/package_info_plus.dart"; import "package:sentry_flutter/sentry_flutter.dart"; diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart index 3dc4f2b1..2bb741f2 100644 --- a/lib/inventree/stock.dart +++ b/lib/inventree/stock.dart @@ -533,7 +533,7 @@ class InvenTreeStockItem extends InvenTreeModel { Map data = {}; // Note: Format of adjustment API was updated in API v14 - if (api.supportModernStockTransactions()) { + if (api.supportsModernStockTransactions) { // Modern (> 14) API data = { "items": [ @@ -560,7 +560,7 @@ class InvenTreeStockItem extends InvenTreeModel { } // Expected API return code depends on server API version - final int expected_response = api.supportModernStockTransactions() ? 201 : 200; + final int expected_response = api.supportsModernStockTransactions ? 201 : 200; var response = await api.post( endpoint, diff --git a/lib/l10.dart b/lib/l10.dart index ce9f3199..2b44f962 100644 --- a/lib/l10.dart +++ b/lib/l10.dart @@ -7,16 +7,18 @@ import "package:flutter/material.dart"; // Shortcut function to reduce boilerplate! I18N L10() { - BuildContext? _ctx = OneContext().context; + if (OneContext.hasContext) { + BuildContext? _ctx = OneContext().context; - if (_ctx != null) { - I18N? i18n = I18N.of(_ctx); + if (_ctx != null) { + I18N? i18n = I18N.of(_ctx); - if (i18n != null) { - return i18n; + if (i18n != null) { + return i18n; + } } } // Fallback for "null" context - return I18NEn(); + return I18NEn(); } \ No newline at end of file diff --git a/lib/preferences.dart b/lib/preferences.dart index 2c52a88d..b804cbd9 100644 --- a/lib/preferences.dart +++ b/lib/preferences.dart @@ -6,6 +6,26 @@ import "package:sembast/sembast_io.dart"; import "package:path/path.dart"; +// Settings key values +const String INV_HOME_SHOW_SUBSCRIBED = "homeShowSubscribed"; +const String INV_HOME_SHOW_PO = "homeShowPo"; +const String INV_HOME_SHOW_MANUFACTURERS = "homeShowManufacturers"; +const String INV_HOME_SHOW_CUSTOMERS = "homeShowCustomers"; +const String INV_HOME_SHOW_SUPPLIERS = "homeShowSuppliers"; + +const String INV_SOUNDS_BARCODE = "barcodeSounds"; +const String INV_SOUNDS_SERVER = "serverSounds"; + +const String INV_PART_SUBCATEGORY = "partSubcategory"; + +const String INV_STOCK_SUBLOCATION = "stockSublocation"; +const String INV_STOCK_SHOW_HISTORY = "stockShowHistory"; + +const String INV_REPORT_ERRORS = "reportErrors"; + +const String INV_STRICT_HTTPS = "strictHttps"; + + /* * Class for storing InvenTree preferences in a NoSql DB */ @@ -40,46 +60,59 @@ class InvenTreePreferencesDB { // Get a platform-specific directory where persistent app data can be stored final appDocumentDir = await getApplicationDocumentsDirectory(); - print("Documents Dir: ${appDocumentDir.toString()}"); - - print("Path: ${appDocumentDir.path}"); - // Path with the form: /platform-specific-directory/demo.db final dbPath = join(appDocumentDir.path, "InvenTreeSettings.db"); final database = await databaseFactoryIo.openDatabase(dbPath); - // Any code awaiting the Completer's future will now start executing _dbOpenCompleter.complete(database); } } -class InvenTreePreferences { - factory InvenTreePreferences() { - return _api; +/* + * InvenTree setings manager class. + * Provides functions for loading and saving settings, with provision for default values + */ +class InvenTreeSettingsManager { + + factory InvenTreeSettingsManager() { + return _manager; } - InvenTreePreferences._internal(); + InvenTreeSettingsManager._internal(); - /* The following settings are not stored to persistent storage, - * instead they are only used as "session preferences". - * They are kept here as a convenience only. - */ + final store = StoreRef("settings"); - // Expand subcategory list in PartCategory view - bool expandCategoryList = false; + Future get _db async => InvenTreePreferencesDB.instance.database; - // Expand part list in PartCategory view - bool expandPartList = true; + Future getValue(String key, dynamic backup) async { - // Expand sublocation list in StockLocation view - bool expandLocationList = false; + final value = await store.record(key).get(await _db); - // Expand item list in StockLocation view - bool expandStockList = true; + if (value == null) { + return backup; + } - // Ensure we only ever create a single instance of the preferences class - static final InvenTreePreferences _api = InvenTreePreferences._internal(); + return value; + } -} \ No newline at end of file + // Load a boolean setting + Future getBool(String key, bool backup) async { + final dynamic value = await getValue(key, backup); + + if (value is bool) { + return value; + } else { + return backup; + } + } + + Future setValue(String key, dynamic value) async { + + await store.record(key).put(await _db, value); + } + + // Ensure we only ever create a single instance of this class + static final InvenTreeSettingsManager _manager = InvenTreeSettingsManager._internal(); +} diff --git a/lib/settings/app_settings.dart b/lib/settings/app_settings.dart index 124246d1..f322bf7c 100644 --- a/lib/settings/app_settings.dart +++ b/lib/settings/app_settings.dart @@ -3,7 +3,7 @@ import "package:flutter/material.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:inventree/l10.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; class InvenTreeAppSettingsWidget extends StatefulWidget { diff --git a/lib/settings/home_settings.dart b/lib/settings/home_settings.dart index c5776c65..13e19cd2 100644 --- a/lib/settings/home_settings.dart +++ b/lib/settings/home_settings.dart @@ -5,7 +5,7 @@ import "package:inventree/l10.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; class HomeScreenSettingsWidget extends StatefulWidget { @override diff --git a/lib/settings/login.dart b/lib/settings/login.dart index a998af41..e61c08b4 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -344,7 +344,6 @@ class _ProfileEditState extends State { Uri uri = Uri.parse(value); if (uri.hasScheme) { - print("Scheme: ${uri.scheme}"); if (!["http", "https"].contains(uri.scheme.toLowerCase())) { return L10().serverStart; } diff --git a/lib/user_profile.dart b/lib/user_profile.dart index 251aa309..9eff1edf 100644 --- a/lib/user_profile.dart +++ b/lib/user_profile.dart @@ -1,6 +1,7 @@ import "package:sembast/sembast.dart"; +import "package:inventree/helpers.dart"; import "package:inventree/preferences.dart"; class UserProfile { @@ -62,75 +63,94 @@ class UserProfileDBManager { Future get _db async => InvenTreePreferencesDB.instance.database; + /* + * Check if a profile with the specified name exists in the database + */ Future profileNameExists(String name) async { - final finder = Finder(filter: Filter.equals("name", name)); + final profiles = await getAllProfiles(); - final profiles = await store.find(await _db, finder: finder); + for (var prf in profiles) { + if (name == prf.name) { + return true; + } + } - return profiles.isNotEmpty; + // No match found! + return false; } - Future addProfile(UserProfile profile) async { + /* + * Add a new UserProfile to the profiles database. + */ + Future addProfile(UserProfile profile) async { + + if (profile.name.isEmpty || profile.username.isEmpty || profile.password.isEmpty) { + debug("addProfile() : Profile missing required values - not adding to database"); + return false; + } // Check if a profile already exists with the name final bool exists = await profileNameExists(profile.name); if (exists) { - print("UserProfile '${profile.name}' already exists"); - return; + debug("addProfile() : UserProfile '${profile.name}' already exists"); + return false; } int key = await store.add(await _db, profile.toJson()) as int; - print("Added user profile <${key}> - '${profile.name}'"); - // Record the key profile.key = key; + + return true; } - Future selectProfile(int key) async { - /* - * Mark the particular profile as selected - */ + /* + * Update the selected profile in the database. + * The unique integer is used to determine if the profile already exists. + */ + Future updateProfile(UserProfile profile) async { - final result = await store.record("selected").put(await _db, key); - - return result; - } - - Future updateProfile(UserProfile profile) async { - - if (profile.key == null) { - await addProfile(profile); - return; + // Prevent invalid profile data from being updated + if (profile.name.isEmpty || profile.username.isEmpty || profile.password.isEmpty) { + debug("updateProfile() : Profile missing required values - not updating"); + return false; } - final result = await store.record(profile.key).update(await _db, profile.toJson()); + if (profile.key == null) { + bool result = await addProfile(profile); + return result; + } - print("Updated user profile <${profile.key}> - '${profile.name}'"); + await store.record(profile.key).update(await _db, profile.toJson()); - return result; + return true; } + /* + * Remove a user profile from the database + */ Future deleteProfile(UserProfile profile) async { await store.record(profile.key).delete(await _db); - print("Deleted user profile <${profile.key}> - '${profile.name}'"); } + /* + * Return the currently selected profile. + * The key of the UserProfile should match the "selected" property + */ Future getSelectedProfile() async { - /* - * Return the currently selected profile. - * - * key should match the "selected" property - */ final selected = await store.record("selected").get(await _db); final profiles = await store.find(await _db); + debug("getSelectedProfile() : ${profiles.length} profiles available - selected = ${selected}"); + for (int idx = 0; idx < profiles.length; idx++) { + debug("- Checking ${idx} - key = ${profiles[idx].key} - ${profiles[idx].value.toString()}"); + if (profiles[idx].key is int && profiles[idx].key == selected) { return UserProfile.fromJson( profiles[idx].key as int, @@ -158,14 +178,43 @@ class UserProfileDBManager { if (profiles[idx].key is int) { profileList.add( - UserProfile.fromJson( - profiles[idx].key as int, - profiles[idx].value as Map, - profiles[idx].key == selected, - )); + UserProfile.fromJson( + profiles[idx].key as int, + profiles[idx].value as Map, + profiles[idx].key == selected, + ) + ); } } return profileList; } + + /* + * Mark the particular profile as selected + */ + Future selectProfile(int key) async { + await store.record("selected").put(await _db, key); + } + + /* + * Look-up and select a profile by name. + * Return true if the profile was selected + */ + Future selectProfileByName(String name) async { + var profiles = await getAllProfiles(); + + for (var prf in profiles) { + if (prf.name == name) { + int key = prf.key ?? -1; + + if (key >= 0) { + await selectProfile(key); + return true; + } + } + } + + return false; + } } diff --git a/lib/widget/dialogs.dart b/lib/widget/dialogs.dart index 357983b2..5fbe20b1 100644 --- a/lib/widget/dialogs.dart +++ b/lib/widget/dialogs.dart @@ -1,12 +1,12 @@ - -import "package:inventree/app_settings.dart"; -import "package:inventree/widget/snacks.dart"; -import "package:audioplayers/audioplayers.dart"; import "package:flutter/material.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; -import "package:inventree/l10.dart"; +import "package:inventree/helpers.dart"; import "package:one_context/one_context.dart"; +import "package:inventree/l10.dart"; +import "package:inventree/preferences.dart"; +import "package:inventree/widget/snacks.dart"; + Future confirmationDialog(String title, String text, {IconData icon = FontAwesomeIcons.questionCircle, String? acceptText, String? rejectText, Function? onAccept, Function? onReject}) async { String _accept = acceptText ?? L10().ok; @@ -108,8 +108,7 @@ Future showServerError(String title, String description) async { final bool tones = await InvenTreeSettingsManager().getValue(INV_SOUNDS_SERVER, true) as bool; if (tones) { - final player = AudioCache(); - player.play("sounds/server_error.mp3"); + playAudioFile("sounds/server_error.mp3"); } showSnackIcon( diff --git a/lib/widget/drawer.dart b/lib/widget/drawer.dart index 85a30259..cff24de8 100644 --- a/lib/widget/drawer.dart +++ b/lib/widget/drawer.dart @@ -34,7 +34,7 @@ class InvenTreeDrawer extends StatelessWidget { void _search() { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; _closeDrawer(); @@ -51,7 +51,7 @@ class InvenTreeDrawer extends StatelessWidget { * Upon successful scan, data are passed off to be decoded. */ Future _scan() async { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; _closeDrawer(); scanQrCode(context); diff --git a/lib/widget/home.dart b/lib/widget/home.dart index 64c566de..19f1c953 100644 --- a/lib/widget/home.dart +++ b/lib/widget/home.dart @@ -6,7 +6,7 @@ import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:inventree/api.dart"; import "package:inventree/app_colors.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; import "package:inventree/barcode.dart"; import "package:inventree/l10.dart"; import "package:inventree/settings/login.dart"; @@ -71,13 +71,13 @@ class _InvenTreeHomePageState extends State { UserProfile? _profile; void _scan(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; scanQrCode(context); } void _showParts(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null))); } @@ -87,7 +87,7 @@ class _InvenTreeHomePageState extends State { } void _showStarredParts(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; Navigator.push( context, @@ -100,13 +100,13 @@ class _InvenTreeHomePageState extends State { } void _showStock(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null))); } void _showPurchaseOrders(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; Navigator.push( context, @@ -118,19 +118,19 @@ class _InvenTreeHomePageState extends State { /* void _showSuppliers(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().suppliers, {"is_supplier": "true"}))); } void _showManufacturers(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().manufacturers, {"is_manufacturer": "true"}))); } void _showCustomers(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().customers, {"is_customer": "true"}))); } diff --git a/lib/widget/part_list.dart b/lib/widget/part_list.dart index ca4d158e..528273f7 100644 --- a/lib/widget/part_list.dart +++ b/lib/widget/part_list.dart @@ -6,7 +6,7 @@ import "package:inventree/widget/paginator.dart"; import "package:inventree/widget/part_detail.dart"; import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/api.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; import "package:inventree/l10.dart"; diff --git a/lib/widget/purchase_order_detail.dart b/lib/widget/purchase_order_detail.dart index 27486969..2abe56c6 100644 --- a/lib/widget/purchase_order_detail.dart +++ b/lib/widget/purchase_order_detail.dart @@ -247,7 +247,7 @@ class _PurchaseOrderDetailState extends RefreshableState | - */ - import "package:flutter/material.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:one_context/one_context.dart"; import "package:inventree/l10.dart"; +/* + * Display a configurable 'snackbar' at the bottom of the screen + */ void showSnackIcon(String text, {IconData? icon, Function()? onAction, bool? success, String? actionText}) { + // Escape quickly if we do not have context + if (!OneContext.hasContext) { + return; + } + BuildContext? context = OneContext().context; if (context != null) { diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index b0b3cdab..e4773755 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -19,10 +19,9 @@ import "package:inventree/widget/stock_item_history.dart"; import "package:inventree/widget/stock_item_test_results.dart"; import "package:inventree/widget/stock_notes.dart"; import "package:inventree/l10.dart"; -import "package:inventree/helpers.dart"; import "package:inventree/api.dart"; import "package:inventree/api_form.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; class StockDetailWidget extends StatefulWidget { @@ -312,7 +311,7 @@ class _StockItemDisplayState extends RefreshableState { Future _addStockDialog() async { // TODO: In future, deprecate support for older API - if (InvenTreeAPI().supportModernStockTransactions()) { + if (InvenTreeAPI().supportsModernStockTransactions) { Map fields = { "pk": { @@ -392,7 +391,7 @@ class _StockItemDisplayState extends RefreshableState { void _removeStockDialog() { // TODO: In future, deprecate support for the older API - if (InvenTreeAPI().supportModernStockTransactions()) { + if (InvenTreeAPI().supportsModernStockTransactions) { Map fields = { "pk": { "parent": "items", @@ -464,7 +463,7 @@ class _StockItemDisplayState extends RefreshableState { Future _countStockDialog() async { // TODO: In future, deprecate support for older API - if (InvenTreeAPI().supportModernStockTransactions()) { + if (InvenTreeAPI().supportsModernStockTransactions) { Map fields = { "pk": { @@ -567,7 +566,7 @@ class _StockItemDisplayState extends RefreshableState { Future _transferStockDialog(BuildContext context) async { // TODO: In future, deprecate support for older API - if (InvenTreeAPI().supportModernStockTransactions()) { + if (InvenTreeAPI().supportsModernStockTransactions) { Map fields = { "pk": { @@ -1008,7 +1007,7 @@ class _StockItemDisplayState extends RefreshableState { } ).then((result) { if (result) { - successTone(); + barcodeSuccessTone(); showSnackIcon( L10().barcodeAssigned, diff --git a/lib/widget/stock_list.dart b/lib/widget/stock_list.dart index 09d0dc3a..90481c78 100644 --- a/lib/widget/stock_list.dart +++ b/lib/widget/stock_list.dart @@ -5,7 +5,7 @@ import "package:inventree/inventree/stock.dart"; import "package:inventree/widget/paginator.dart"; import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/l10.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; import "package:inventree/widget/stock_detail.dart"; import "package:inventree/api.dart"; diff --git a/pubspec.lock b/pubspec.lock index 4fb3ba92..0f7c12f7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,20 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "31.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.0" archive: dependency: transitive description: @@ -99,6 +113,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" clock: dependency: transitive description: @@ -113,6 +134,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + coverage: + dependency: transitive + description: + name: coverage + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" cross_file: dependency: transitive description: @@ -280,6 +315,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "9.2.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" http: dependency: "direct main" description: @@ -287,6 +336,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.13.4" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" http_parser: dependency: transitive description: @@ -336,6 +392,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.17.0" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" js: dependency: transitive description: @@ -350,6 +413,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.2" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" markdown: dependency: transitive description: @@ -378,6 +448,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.7.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + node_preamble: + dependency: transitive + description: + name: node_preamble + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" octo_image: dependency: transitive description: @@ -399,6 +483,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.2.1" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" package_info_plus: dependency: "direct main" description: @@ -525,6 +616,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" process: dependency: transitive description: @@ -532,6 +630,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.2.4" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" qr_code_scanner: dependency: "direct main" description: @@ -630,6 +735,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + shelf_static: + dependency: transitive + description: + name: shelf_static + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" sky_engine: dependency: transitive description: flutter @@ -642,6 +775,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.5" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.10" source_span: dependency: transitive description: @@ -705,6 +852,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + test: + dependency: "direct dev" + description: + name: test + url: "https://pub.dartlang.org" + source: hosted + version: "1.19.5" test_api: dependency: transitive description: @@ -712,6 +866,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.8" + test_core: + dependency: transitive + description: + name: test_core + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.9" typed_data: dependency: transitive description: @@ -789,6 +950,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.1" + vm_service: + dependency: transitive + description: + name: vm_service + url: "https://pub.dartlang.org" + source: hosted + version: "7.5.0" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9031ea5d..33932d52 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,10 +38,11 @@ dependencies: url_launcher: ^6.0.9 # Open link in system browser dev_dependencies: - flutter_launcher_icons: + flutter_launcher_icons: ^0.9.0 flutter_test: sdk: flutter - lint: ^1.0.0 + lint: ^1.8.0 + test: ^1.19.0 flutter_icons: android: true @@ -64,29 +65,3 @@ flutter: - assets/sounds/barcode_scan.mp3 - assets/sounds/barcode_error.mp3 - assets/sounds/server_error.mp3 - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..94f990c7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +# Python requirements for devops + +coverage==5.3 # Unit test coverage +coveralls==2.1.2 # Coveralls linking (for code coverage reporting) \ No newline at end of file diff --git a/test/api_test.dart b/test/api_test.dart new file mode 100644 index 00000000..b46cab64 --- /dev/null +++ b/test/api_test.dart @@ -0,0 +1,141 @@ +/* + * Unit tests for the InvenTree API code + */ + +import "package:test/test.dart"; + +import "package:inventree/api.dart"; +import "package:inventree/user_profile.dart"; + + + +void main() { + + setUp(() async { + + if (! await UserProfileDBManager().profileNameExists("Test Profile")) { + // Create and select a profile to user + + print("TEST: Creating profile for user 'testuser'"); + + await UserProfileDBManager().addProfile(UserProfile( + name: "Test Profile", + server: "http://localhost:12345", + username: "testuser", + password: "testpassword", + selected: true, + )); + } + + var prf = await UserProfileDBManager().getSelectedProfile(); + + // Ensure that the server settings are correct by default, + // as they can get overwritten by subsequent tests + + if (prf != null) { + prf.name = "Test Profile"; + prf.server = "http://localhost:12345"; + prf.username = "testuser"; + prf.password = "testpassword"; + + await UserProfileDBManager().updateProfile(prf); + } + + // Ensure the profile is selected + assert(! await UserProfileDBManager().selectProfileByName("Missing Profile")); + assert(await UserProfileDBManager().selectProfileByName("Test Profile")); + + }); + + group("Login Tests:", () { + + test("Disconnected", () async { + // Test that calling disconnect() does the right thing + var api = InvenTreeAPI(); + + api.disconnectFromServer(); + + // Check expected values + expect(api.isConnected(), equals(false)); + expect(api.isConnecting(), equals(false)); + expect(api.hasToken, equals(false)); + + }); + + test("Login Failure", () async { + // Tests for various types of login failures + var api = InvenTreeAPI(); + + // Incorrect server address + var profile = await UserProfileDBManager().getSelectedProfile(); + + assert(profile != null); + + if (profile != null) { + profile.server = "http://localhost:5555"; + await UserProfileDBManager().updateProfile(profile); + + bool result = await api.connectToServer(); + assert(!result); + + // TODO: Test the the right 'error message' is returned + // TODO: The request above should throw a 'SockeException' + + // Test incorrect login details + profile.server = "http://localhost:12345"; + profile.username = "invalidusername"; + + await UserProfileDBManager().updateProfile(profile); + + await api.connectToServer(); + assert(!result); + + // TODO: Test that the connection attempt above throws an authentication error + + assert(!api.checkConnection()); + + } else { + assert(false); + } + + }); + + test("Login Success", () async { + // Test that we can login to the server successfully + var api = InvenTreeAPI(); + + // Attempt to connect + final bool result = await api.connectToServer(); + + // Check expected values + assert(result); + assert(api.hasToken); + expect(api.baseUrl, equals("http://localhost:12345/")); + + assert(api.isConnected()); + assert(!api.isConnecting()); + assert(api.checkConnection()); + }); + + test("Version Checks", () async { + // Test server version information + var api = InvenTreeAPI(); + + assert(await api.connectToServer()); + + // Check supported functions + assert(api.apiVersion >= 50); + assert(api.supportsSettings); + assert(api.supportsNotifications); + assert(api.supportsModernStockTransactions); + assert(api.supportsPoReceive); + + // Check available permissions + assert(api.checkPermission("part", "change")); + assert(api.checkPermission("stocklocation", "delete")); + assert(api.checkPermission("part", "weirdpermission")); + assert(api.checkPermission("blah", "bloo")); + }); + + }); +} \ No newline at end of file diff --git a/test/preferences_test.dart b/test/preferences_test.dart new file mode 100644 index 00000000..b402b7a9 --- /dev/null +++ b/test/preferences_test.dart @@ -0,0 +1,30 @@ +/* + * Unit tests for the preferences manager + */ + +import "package:test/test.dart"; +import "package:inventree/preferences.dart"; + +void main() { + + setUp(() async { + + }); + + group("Settings Tests:", () { + test("Default Values", () async { + // Boolean values + expect(await InvenTreeSettingsManager().getBool("test", false), equals(false)); + expect(await InvenTreeSettingsManager().getBool("test", true), equals(true)); + + // String values + expect(await InvenTreeSettingsManager().getValue("test", "x"), equals("x")); + }); + + test("Set value", () async { + await InvenTreeSettingsManager().setValue("abc", "xyz"); + + expect(await InvenTreeSettingsManager().getValue("abc", "123"), equals("xyz")); + }); + }); +} \ No newline at end of file diff --git a/test/user_profile_test.dart b/test/user_profile_test.dart new file mode 100644 index 00000000..9b672ad5 --- /dev/null +++ b/test/user_profile_test.dart @@ -0,0 +1,125 @@ +/* + * Unit tests for the API class + */ + +import "package:test/test.dart"; +import "package:inventree/user_profile.dart"; + +void main() { + + setUp(() async { + // Ensure we have a user profile available + // This profile will match the dockerized InvenTree setup, running locally + + // To start with, there should not be *any* profiles available + var profiles = await UserProfileDBManager().getAllProfiles(); + + for (var prf in profiles) { + await UserProfileDBManager().deleteProfile(prf); + } + + // Check that there are *no* profiles in the database + profiles = await UserProfileDBManager().getAllProfiles(); + expect(profiles.length, equals(0)); + + // Now, create one! + bool result = await UserProfileDBManager().addProfile(UserProfile( + name: "Test Profile", + username: "testuser", + password: "testpassword""", + server: "http://localhost:12345", + selected: true, + )); + + expect(result, equals(true)); + + // Ensure we have one profile available + // expect(profiles.length, equals(1)); + profiles = await UserProfileDBManager().getAllProfiles(); + + expect(profiles.length, equals(1)); + + int key = -1; + + // Find the first available profile + for (var p in profiles) { + if (p.key != null) { + key = p.key ?? key; + break; + } + } + + // Select the profile + await UserProfileDBManager().selectProfile(key); + }); + + // Run a set of tests for user profile functionality + group("Profile Tests:", () { + + test("Add Invalid Profiles", () async { + // Add a profile with missing data + bool result = await UserProfileDBManager().addProfile( + UserProfile( + username: "what", + password: "why", + ) + ); + + expect(result, equals(false)); + + // Add a profile with a name that already exists + result = await UserProfileDBManager().addProfile( + UserProfile( + name: "Test Profile", + username: "xyz", + password: "hunter42", + ) + ); + + expect(result, equals(false)); + + // Check that the number of protocols available is still the same + var profiles = await UserProfileDBManager().getAllProfiles(); + + expect(profiles.length, equals(1)); + }); + + test("Profile Name Check", () async { + bool result = await UserProfileDBManager().profileNameExists("doesnotexist"); + expect(result, equals(false)); + + result = await UserProfileDBManager().profileNameExists("Test Profile"); + expect(result, equals(true)); + }); + + test("Select Profile", () async { + // Ensure that we can select a user profile + final prf = await UserProfileDBManager().getSelectedProfile(); + + expect(prf, isNot(null)); + + if (prf != null) { + UserProfile p = prf; + + expect(p.name, equals("Test Profile")); + expect(p.username, equals("testuser")); + expect(p.password, equals("testpassword")); + expect(p.server, equals("http://localhost:12345")); + + expect(p.toString(), equals("<${p.key}> Test Profile : http://localhost:12345 - testuser:testpassword")); + + // Test that we can update the profile + p.name = "different name"; + + bool result = await UserProfileDBManager().updateProfile(p); + expect(result, equals(true)); + + // Trying to update with an invalid value will fail! + p.password = ""; + result = await UserProfileDBManager().updateProfile(p); + expect(result, equals(false)); + } + }); + }); + +} \ No newline at end of file diff --git a/test/widget_test.dart b/test/widget_test.dart index 3c73102a..4e838ac5 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -5,9 +5,9 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. -import "package:flutter_test/flutter_test.dart"; +// import "package:flutter_test/flutter_test.dart"; void main() { - testWidgets("Counter increments smoke test", (WidgetTester tester) async { - }); + // testWidgets("Counter increments smoke test", (WidgetTester tester) async { + // }); }