19 changed files with 568 additions and 199 deletions
			
			
		@ -0,0 +1,40 @@
					 | 
				
			||||
import 'dart:async'; | 
				
			||||
 | 
				
			||||
import 'package:collection/collection.dart'; | 
				
			||||
import 'package:neon/src/settings/models/option.dart'; | 
				
			||||
 | 
				
			||||
/// Exportable data interface. | 
				
			||||
abstract interface class Exportable { | 
				
			||||
  /// Exports into a json map entry. | 
				
			||||
  MapEntry<String, Object?> export(); | 
				
			||||
 | 
				
			||||
  /// Imports [data] from an export. | 
				
			||||
  FutureOr<void> import(final Map<String, Object?> data); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/// Serialization helpers for a collection of [Option]s. | 
				
			||||
extension SerializeOptions on Iterable<Option<dynamic>> { | 
				
			||||
  /// Serializes into an [Iterable<JsonEntry>]. | 
				
			||||
  /// | 
				
			||||
  /// Use [Map.fromEntries] to get a json Map. | 
				
			||||
  Iterable<MapEntry<String, Object?>> serialize() sync* { | 
				
			||||
    for (final option in this) { | 
				
			||||
      if (option.enabled) { | 
				
			||||
        yield MapEntry(option.key.value, option.serialize()); | 
				
			||||
      } | 
				
			||||
    } | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  /// Deserializes [data] and updates the [Option]s. | 
				
			||||
  void deserialize(final Map<String, Object?> data) { | 
				
			||||
    for (final entry in data.entries) { | 
				
			||||
      final option = firstWhereOrNull((final option) => option.key.value == entry.key); | 
				
			||||
 | 
				
			||||
      if (entry.value != null) { | 
				
			||||
        option?.load(entry.value); | 
				
			||||
      } else { | 
				
			||||
        option?.reset(); | 
				
			||||
      } | 
				
			||||
    } | 
				
			||||
  } | 
				
			||||
} | 
				
			||||
@ -1,23 +0,0 @@
					 | 
				
			||||
import 'package:neon/src/settings/models/option.dart'; | 
				
			||||
import 'package:neon/src/settings/models/options_category.dart'; | 
				
			||||
import 'package:neon/src/settings/models/storage.dart'; | 
				
			||||
 | 
				
			||||
abstract class NextcloudAppOptions { | 
				
			||||
  NextcloudAppOptions(this.storage); | 
				
			||||
 | 
				
			||||
  final AppStorage storage; | 
				
			||||
  late final List<OptionsCategory> categories; | 
				
			||||
  late final List<Option> options; | 
				
			||||
 | 
				
			||||
  void reset() { | 
				
			||||
    for (final option in options) { | 
				
			||||
      option.reset(); | 
				
			||||
    } | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  void dispose() { | 
				
			||||
    for (final option in options) { | 
				
			||||
      option.dispose(); | 
				
			||||
    } | 
				
			||||
  } | 
				
			||||
} | 
				
			||||
@ -0,0 +1,65 @@
					 | 
				
			||||
import 'package:meta/meta.dart'; | 
				
			||||
import 'package:neon/src/settings/models/exportable.dart'; | 
				
			||||
import 'package:neon/src/settings/models/option.dart'; | 
				
			||||
import 'package:neon/src/settings/models/options_category.dart'; | 
				
			||||
import 'package:neon/src/settings/models/storage.dart'; | 
				
			||||
 | 
				
			||||
/// Collection of [Option]s. | 
				
			||||
abstract class OptionsCollection implements Exportable { | 
				
			||||
  OptionsCollection(this.storage); | 
				
			||||
 | 
				
			||||
  /// Storage backend to use. | 
				
			||||
  @protected | 
				
			||||
  final AppStorage storage; | 
				
			||||
 | 
				
			||||
  /// Collection of options. | 
				
			||||
  @protected | 
				
			||||
  Iterable<Option> get options; | 
				
			||||
 | 
				
			||||
  /// Resets all [options]. | 
				
			||||
  /// | 
				
			||||
  /// Implementers extending this must call super. | 
				
			||||
  @mustCallSuper | 
				
			||||
  void reset() { | 
				
			||||
    for (final option in options) { | 
				
			||||
      option.reset(); | 
				
			||||
    } | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  /// Disposes all [options]. | 
				
			||||
  /// | 
				
			||||
  /// Implementers extending this must call super. | 
				
			||||
  @mustCallSuper | 
				
			||||
  void dispose() { | 
				
			||||
    for (final option in options) { | 
				
			||||
      option.dispose(); | 
				
			||||
    } | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  @override | 
				
			||||
  MapEntry<String, Object?> export() { | 
				
			||||
    final data = Map.fromEntries(options.serialize()); | 
				
			||||
 | 
				
			||||
    return MapEntry(storage.id, data); | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  @override | 
				
			||||
  void import(final Map<String, Object?> data) { | 
				
			||||
    final values = data[storage.id] as Map<String, Object?>?; | 
				
			||||
 | 
				
			||||
    if (values != null) { | 
				
			||||
      options.deserialize(values); | 
				
			||||
    } | 
				
			||||
  } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/// OpptionsCollection for a neon app. | 
				
			||||
abstract class NextcloudAppOptions extends OptionsCollection { | 
				
			||||
  NextcloudAppOptions(super.storage); | 
				
			||||
 | 
				
			||||
  /// Collection of categories to display the options in the settings. | 
				
			||||
  late final Iterable<OptionsCategory> categories; | 
				
			||||
 | 
				
			||||
  @override | 
				
			||||
  late final Iterable<Option> options; | 
				
			||||
} | 
				
			||||
@ -0,0 +1,147 @@
					 | 
				
			||||
import 'dart:async'; | 
				
			||||
import 'dart:convert'; | 
				
			||||
import 'dart:typed_data'; | 
				
			||||
 | 
				
			||||
import 'package:meta/meta.dart'; | 
				
			||||
import 'package:neon/src/blocs/accounts.dart'; | 
				
			||||
import 'package:neon/src/models/account.dart' show Account, AccountFind; | 
				
			||||
import 'package:neon/src/models/app_implementation.dart'; | 
				
			||||
import 'package:neon/src/settings/models/exportable.dart'; | 
				
			||||
import 'package:neon/src/settings/models/option.dart'; | 
				
			||||
import 'package:neon/src/settings/models/storage.dart'; | 
				
			||||
 | 
				
			||||
/// Helper class to export all [Option]s. | 
				
			||||
/// | 
				
			||||
/// Json based operations: | 
				
			||||
/// * [exportToJson] | 
				
			||||
/// * [applyFromJson] | 
				
			||||
/// | 
				
			||||
/// [Uint8List] based operations: | 
				
			||||
/// * [exportToFile] | 
				
			||||
/// * [applyFromFile] | 
				
			||||
@internal | 
				
			||||
@immutable | 
				
			||||
class SettingsExportHelper { | 
				
			||||
  const SettingsExportHelper({ | 
				
			||||
    required this.exportables, | 
				
			||||
  }); | 
				
			||||
 | 
				
			||||
  /// Collections of elements to export. | 
				
			||||
  final Set<Exportable> exportables; | 
				
			||||
 | 
				
			||||
  /// Imports [file] and applies the stored [Option]s. | 
				
			||||
  /// | 
				
			||||
  /// See: | 
				
			||||
  /// * [applyFromJson] to import a json map. | 
				
			||||
  Future<void> applyFromFile(final Stream<List<int>>? file) async { | 
				
			||||
    final transformer = const Utf8Decoder().fuse(const JsonDecoder()); | 
				
			||||
 | 
				
			||||
    final data = await file?.transform(transformer).single; | 
				
			||||
 | 
				
			||||
    if (data == null) { | 
				
			||||
      return; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    await applyFromJson(data as Map<String, Object?>); | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  /// Imports the [json] data and applies the stored [Option]s. | 
				
			||||
  /// | 
				
			||||
  /// See: | 
				
			||||
  /// * [applyFromFile] to import data from a [Stream<Uint8List>]. | 
				
			||||
  Future<void> applyFromJson(final Map<String, Object?> json) async { | 
				
			||||
    for (final exportable in exportables) { | 
				
			||||
      await exportable.import(json); | 
				
			||||
    } | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  /// Exports the stored [Option]s into a [Uint8List]. | 
				
			||||
  /// | 
				
			||||
  /// See: | 
				
			||||
  /// * [exportToJson] to export to a json map. | 
				
			||||
  Uint8List exportToFile() { | 
				
			||||
    final transformer = JsonUtf8Encoder(); | 
				
			||||
 | 
				
			||||
    final json = exportToJson(); | 
				
			||||
    return transformer.convert(json) as Uint8List; | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  /// Exports the stored [Option]s into a json map. | 
				
			||||
  /// | 
				
			||||
  /// See: | 
				
			||||
  /// * [exportToFile] to export data to a [Uint8List]. | 
				
			||||
  Map<String, Object?> exportToJson() => Map.fromEntries(exportables.map((final e) => e.export())); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/// Helper class to export [AppImplementation]s implementing the [Exportable] interface. | 
				
			||||
@immutable | 
				
			||||
class AppImplementationsExporter implements Exportable { | 
				
			||||
  const AppImplementationsExporter(this.appImplementations); | 
				
			||||
 | 
				
			||||
  /// List of apps to export. | 
				
			||||
  final Iterable<AppImplementation> appImplementations; | 
				
			||||
 | 
				
			||||
  /// Key the exported value will be stored at. | 
				
			||||
  static final _key = StorageKeys.apps.value; | 
				
			||||
 | 
				
			||||
  @override | 
				
			||||
  MapEntry<String, Object?> export() => MapEntry( | 
				
			||||
        _key, | 
				
			||||
        Map.fromEntries(appImplementations.map((final app) => app.options.export())), | 
				
			||||
      ); | 
				
			||||
 | 
				
			||||
  @override | 
				
			||||
  void import(final Map<String, Object?> data) { | 
				
			||||
    final values = data[_key] as Map<String, Object?>?; | 
				
			||||
 | 
				
			||||
    if (values == null) { | 
				
			||||
      return; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    for (final element in values.entries) { | 
				
			||||
      final app = appImplementations.tryFind(element.key); | 
				
			||||
 | 
				
			||||
      if (app != null) { | 
				
			||||
        app.options.import(values); | 
				
			||||
      } | 
				
			||||
    } | 
				
			||||
  } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/// Helper class to export [Account]s implementing the [Exportable] interface. | 
				
			||||
@immutable | 
				
			||||
class AccountsBlocExporter implements Exportable { | 
				
			||||
  const AccountsBlocExporter(this.accountsBloc); | 
				
			||||
 | 
				
			||||
  /// AccountsBloc containing the accounts to export. | 
				
			||||
  final AccountsBloc accountsBloc; | 
				
			||||
 | 
				
			||||
  /// Key the exported value will be stored at. | 
				
			||||
  static final _key = StorageKeys.accounts.value; | 
				
			||||
 | 
				
			||||
  @override | 
				
			||||
  MapEntry<String, Object?> export() => MapEntry(_key, Map.fromEntries(_serialize())); | 
				
			||||
 | 
				
			||||
  Iterable<MapEntry<String, Object?>> _serialize() sync* { | 
				
			||||
    for (final account in accountsBloc.accounts.value) { | 
				
			||||
      yield accountsBloc.getOptionsFor(account).export(); | 
				
			||||
    } | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  @override | 
				
			||||
  void import(final Map<String, Object?> data) { | 
				
			||||
    final values = data[_key] as Map<String, Object?>?; | 
				
			||||
 | 
				
			||||
    if (values == null) { | 
				
			||||
      return; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    for (final element in values.entries) { | 
				
			||||
      final account = accountsBloc.accounts.value.tryFind(element.key); | 
				
			||||
 | 
				
			||||
      if (account != null) { | 
				
			||||
        accountsBloc.getOptionsFor(account).import(values); | 
				
			||||
      } | 
				
			||||
    } | 
				
			||||
  } | 
				
			||||
} | 
				
			||||
@ -1,98 +0,0 @@
					 | 
				
			||||
import 'package:meta/meta.dart'; | 
				
			||||
import 'package:neon/src/models/account.dart'; | 
				
			||||
import 'package:neon/src/models/app_implementation.dart'; | 
				
			||||
import 'package:neon/src/settings/models/option.dart'; | 
				
			||||
import 'package:neon/src/utils/global_options.dart'; | 
				
			||||
 | 
				
			||||
@internal | 
				
			||||
class SettingsExportHelper { | 
				
			||||
  SettingsExportHelper({ | 
				
			||||
    required this.globalOptions, | 
				
			||||
    required this.appImplementations, | 
				
			||||
    required this.accountSpecificOptions, | 
				
			||||
  }); | 
				
			||||
 | 
				
			||||
  final GlobalOptions globalOptions; | 
				
			||||
  final Iterable<AppImplementation> appImplementations; | 
				
			||||
  final Map<Account, List<Option>> accountSpecificOptions; | 
				
			||||
 | 
				
			||||
  Future applyFromJson(final Map<String, dynamic> data) async { | 
				
			||||
    final globalOptionsData = data['global'] as Map<String, dynamic>; | 
				
			||||
    await _applyOptionsMapToOptions( | 
				
			||||
      globalOptions.options, | 
				
			||||
      globalOptionsData, | 
				
			||||
    ); | 
				
			||||
 | 
				
			||||
    final appImplementationsData = data['apps'] as Map<String, dynamic>; | 
				
			||||
    for (final appId in appImplementationsData.keys) { | 
				
			||||
      final app = appImplementations.tryFind(appId); | 
				
			||||
      if (app == null) { | 
				
			||||
        return; | 
				
			||||
      } | 
				
			||||
      final appImplementationData = appImplementationsData[appId]! as Map<String, dynamic>; | 
				
			||||
      await _applyOptionsMapToOptions( | 
				
			||||
        app.options.options, | 
				
			||||
        appImplementationData, | 
				
			||||
      ); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    final accountsData = data['accounts'] as Map<String, dynamic>; | 
				
			||||
    for (final accountId in accountsData.keys) { | 
				
			||||
      final account = accountSpecificOptions.keys.tryFind(accountId); | 
				
			||||
      if (account == null) { | 
				
			||||
        return; | 
				
			||||
      } | 
				
			||||
      final accountData = accountsData[accountId]! as Map<String, dynamic>; | 
				
			||||
      await _applyOptionsMapToOptions( | 
				
			||||
        accountSpecificOptions[account]!, | 
				
			||||
        accountData, | 
				
			||||
      ); | 
				
			||||
    } | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  Future _applyOptionsMapToOptions(final List<Option> options, final Map<String, dynamic> data) async { | 
				
			||||
    for (final optionKey in data.keys) { | 
				
			||||
      for (final option in options) { | 
				
			||||
        if (option.key.value == optionKey) { | 
				
			||||
          final Object? value = data[optionKey]; | 
				
			||||
 | 
				
			||||
          if (value != null) { | 
				
			||||
            option.value = await option.deserialize(value); | 
				
			||||
          } | 
				
			||||
        } | 
				
			||||
      } | 
				
			||||
    } | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  Map<String, dynamic> toJsonExport() => { | 
				
			||||
        'global': { | 
				
			||||
          for (final option in globalOptions.options) ...{ | 
				
			||||
            if (option.enabled) ...{ | 
				
			||||
              option.key: option.serialize(), | 
				
			||||
            }, | 
				
			||||
          }, | 
				
			||||
        }, | 
				
			||||
        'apps': { | 
				
			||||
          for (final appImplementation in appImplementations) ...{ | 
				
			||||
            appImplementation.id: { | 
				
			||||
              for (final option in appImplementation.options.options) ...{ | 
				
			||||
                if (option.enabled) ...{ | 
				
			||||
                  option.key: option.serialize(), | 
				
			||||
                }, | 
				
			||||
              }, | 
				
			||||
            }, | 
				
			||||
          }, | 
				
			||||
        }, | 
				
			||||
        'accounts': { | 
				
			||||
          for (final account in accountSpecificOptions.keys) ...{ | 
				
			||||
            account.id: { | 
				
			||||
              for (final option in accountSpecificOptions[account]!) ...{ | 
				
			||||
                if (option.enabled) ...{ | 
				
			||||
                  option.key: option.serialize(), | 
				
			||||
                }, | 
				
			||||
              }, | 
				
			||||
            }, | 
				
			||||
          }, | 
				
			||||
        }, | 
				
			||||
      }; | 
				
			||||
} | 
				
			||||
@ -0,0 +1,84 @@
					 | 
				
			||||
import 'package:mocktail/mocktail.dart'; | 
				
			||||
import 'package:neon/settings.dart'; | 
				
			||||
import 'package:neon/src/settings/models/option.dart'; | 
				
			||||
import 'package:neon/src/settings/models/storage.dart'; | 
				
			||||
import 'package:test/test.dart'; | 
				
			||||
 | 
				
			||||
// ignore: missing_override_of_must_be_overridden | 
				
			||||
class OptionMock extends Mock implements Option {} | 
				
			||||
 | 
				
			||||
class Collection extends NextcloudAppOptions { | 
				
			||||
  Collection(final List<Option> options) : super(const AppStorage(StorageKeys.apps)) { | 
				
			||||
    super.options = options; | 
				
			||||
  } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
enum Keys implements Storable { | 
				
			||||
  key1._('key1'), | 
				
			||||
  key2._('key2'); | 
				
			||||
 | 
				
			||||
  const Keys._(this.value); | 
				
			||||
 | 
				
			||||
  @override | 
				
			||||
  final String value; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
void main() { | 
				
			||||
  group('OptionsCollection', () { | 
				
			||||
    final option1 = OptionMock(); | 
				
			||||
    final option2 = OptionMock(); | 
				
			||||
    final collection = Collection([ | 
				
			||||
      option1, | 
				
			||||
      option2, | 
				
			||||
    ]); | 
				
			||||
 | 
				
			||||
    test('reset', () { | 
				
			||||
      collection.reset(); | 
				
			||||
 | 
				
			||||
      verify(option1.reset).called(1); | 
				
			||||
      verify(option2.reset).called(1); | 
				
			||||
    }); | 
				
			||||
 | 
				
			||||
    test('dispose', () { | 
				
			||||
      collection.dispose(); | 
				
			||||
 | 
				
			||||
      verify(option1.dispose).called(1); | 
				
			||||
      verify(option2.dispose).called(1); | 
				
			||||
    }); | 
				
			||||
 | 
				
			||||
    test('export', () { | 
				
			||||
      when(() => option1.key).thenReturn(Keys.key1); | 
				
			||||
      when(option1.serialize).thenReturn('value1'); | 
				
			||||
      when(() => option1.enabled).thenReturn(true); | 
				
			||||
 | 
				
			||||
      when(() => option2.key).thenReturn(Keys.key2); | 
				
			||||
      when(option2.serialize).thenReturn('value2'); | 
				
			||||
      when(() => option2.enabled).thenReturn(false); | 
				
			||||
 | 
				
			||||
      const json = { | 
				
			||||
        'app': {'key1': 'value1'}, | 
				
			||||
      }; | 
				
			||||
 | 
				
			||||
      final export = collection.export(); | 
				
			||||
 | 
				
			||||
      expect(Map.fromEntries([export]), equals(json)); | 
				
			||||
    }); | 
				
			||||
 | 
				
			||||
    test('import', () { | 
				
			||||
      when(() => option1.key).thenReturn(Keys.key1); | 
				
			||||
      when(() => option2.key).thenReturn(Keys.key2); | 
				
			||||
 | 
				
			||||
      const json = { | 
				
			||||
        'app': { | 
				
			||||
          'key1': 'value1', | 
				
			||||
          'key2': null, | 
				
			||||
        }, | 
				
			||||
      }; | 
				
			||||
 | 
				
			||||
      collection.import(json); | 
				
			||||
 | 
				
			||||
      verify(() => option1.load('value1')).called(1); | 
				
			||||
      verify(option2.reset).called(1); | 
				
			||||
    }); | 
				
			||||
  }); | 
				
			||||
} | 
				
			||||
@ -0,0 +1,130 @@
					 | 
				
			||||
// ignore_for_file: avoid_implementing_value_types | 
				
			||||
 | 
				
			||||
import 'dart:convert'; | 
				
			||||
import 'dart:typed_data'; | 
				
			||||
 | 
				
			||||
import 'package:mocktail/mocktail.dart'; | 
				
			||||
import 'package:neon/blocs.dart'; | 
				
			||||
import 'package:neon/src/models/account.dart'; | 
				
			||||
import 'package:neon/src/models/app_implementation.dart'; | 
				
			||||
import 'package:neon/src/settings/models/exportable.dart'; | 
				
			||||
import 'package:neon/src/settings/models/options_collection.dart'; | 
				
			||||
import 'package:neon/src/settings/utils/settings_export_helper.dart'; | 
				
			||||
import 'package:neon/src/utils/account_options.dart'; | 
				
			||||
import 'package:rxdart/rxdart.dart'; | 
				
			||||
import 'package:test/test.dart'; | 
				
			||||
 | 
				
			||||
// ignore: missing_override_of_must_be_overridden | 
				
			||||
class FakeAppImplementation extends Mock implements AppImplementation {} | 
				
			||||
 | 
				
			||||
class NextcloudAppOptionsMock extends Mock implements NextcloudAppOptions {} | 
				
			||||
 | 
				
			||||
class AccountsBlocMock extends Mock implements AccountsBloc {} | 
				
			||||
 | 
				
			||||
class FakeAccount extends Mock implements Account {} | 
				
			||||
 | 
				
			||||
class AccountSpecificOptionsMock extends Mock implements AccountSpecificOptions {} | 
				
			||||
 | 
				
			||||
class ExporterMock extends Mock implements Exportable {} | 
				
			||||
 | 
				
			||||
void main() { | 
				
			||||
  group('Exporter', () { | 
				
			||||
    test('AccountsBlocExporter', () { | 
				
			||||
      var exporter = const AppImplementationsExporter([]); | 
				
			||||
 | 
				
			||||
      var export = exporter.export(); | 
				
			||||
      expect(Map.fromEntries([export]), {'app': {}}); | 
				
			||||
 | 
				
			||||
      final fakeApp = FakeAppImplementation(); | 
				
			||||
      final fakeOptions = NextcloudAppOptionsMock(); | 
				
			||||
      exporter = AppImplementationsExporter([fakeApp]); | 
				
			||||
 | 
				
			||||
      const appValue = MapEntry('appID', 'value'); | 
				
			||||
      const appExport = { | 
				
			||||
        'app': {'appID': 'value'}, | 
				
			||||
      }; | 
				
			||||
 | 
				
			||||
      when(() => fakeApp.options).thenReturn(fakeOptions); | 
				
			||||
      when(fakeOptions.export).thenReturn(appValue); | 
				
			||||
      when(() => fakeApp.id).thenReturn('appID'); | 
				
			||||
 | 
				
			||||
      export = exporter.export(); | 
				
			||||
      expect(Map.fromEntries([export]), appExport); | 
				
			||||
 | 
				
			||||
      exporter.import(Map.fromEntries([export])); | 
				
			||||
      verify(() => fakeOptions.import(Map.fromEntries([appValue]))).called(1); | 
				
			||||
    }); | 
				
			||||
 | 
				
			||||
    test('AccountsBlocExporter', () { | 
				
			||||
      final bloc = AccountsBlocMock(); | 
				
			||||
      final exporter = AccountsBlocExporter(bloc); | 
				
			||||
 | 
				
			||||
      const accountValue = MapEntry('accountID', 'value'); | 
				
			||||
      const accountExport = { | 
				
			||||
        'accounts': {'accountID': 'value'}, | 
				
			||||
      }; | 
				
			||||
 | 
				
			||||
      when(() => bloc.accounts).thenAnswer((final _) => BehaviorSubject.seeded([])); | 
				
			||||
      var export = exporter.export(); | 
				
			||||
      expect(Map.fromEntries([export]), {'accounts': {}}); | 
				
			||||
 | 
				
			||||
      final fakeAccount = FakeAccount(); | 
				
			||||
      final fakeOptions = AccountSpecificOptionsMock(); | 
				
			||||
      when(() => bloc.accounts).thenAnswer((final _) => BehaviorSubject.seeded([fakeAccount])); | 
				
			||||
      when(() => bloc.getOptionsFor(fakeAccount)).thenReturn(fakeOptions); | 
				
			||||
      when(fakeOptions.export).thenReturn(accountValue); | 
				
			||||
      when(() => fakeAccount.id).thenReturn('accountID'); | 
				
			||||
 | 
				
			||||
      export = exporter.export(); | 
				
			||||
      expect(Map.fromEntries([export]), accountExport); | 
				
			||||
 | 
				
			||||
      exporter.import(Map.fromEntries([export])); | 
				
			||||
      verify(() => fakeOptions.import(Map.fromEntries([accountValue]))).called(1); | 
				
			||||
    }); | 
				
			||||
  }); | 
				
			||||
 | 
				
			||||
  group('SettingsExportHelper', () { | 
				
			||||
    test('SettingsExportHelper.json', () async { | 
				
			||||
      final exportable = ExporterMock(); | 
				
			||||
      final settingsExporter = SettingsExportHelper( | 
				
			||||
        exportables: { | 
				
			||||
          exportable, | 
				
			||||
        }, | 
				
			||||
      ); | 
				
			||||
 | 
				
			||||
      const value = MapEntry('sxportableKey', 'value'); | 
				
			||||
      const export = {'sxportableKey': 'value'}; | 
				
			||||
 | 
				
			||||
      when(exportable.export).thenAnswer((final _) => value); | 
				
			||||
 | 
				
			||||
      expect(settingsExporter.exportToJson(), equals(export)); | 
				
			||||
 | 
				
			||||
      await settingsExporter.applyFromJson(export); | 
				
			||||
      verify(() => exportable.import(Map.fromEntries([value]))).called(1); | 
				
			||||
    }); | 
				
			||||
 | 
				
			||||
    test('SettingsExportHelper.file', () async { | 
				
			||||
      final exportable = ExporterMock(); | 
				
			||||
      final settingsExporter = SettingsExportHelper( | 
				
			||||
        exportables: { | 
				
			||||
          exportable, | 
				
			||||
        }, | 
				
			||||
      ); | 
				
			||||
 | 
				
			||||
      const value = MapEntry('sxportableKey', 'value'); | 
				
			||||
      const jsonExport = {'sxportableKey': 'value'}; | 
				
			||||
      final export = JsonUtf8Encoder().convert(jsonExport) as Uint8List; | 
				
			||||
 | 
				
			||||
      when(exportable.export).thenAnswer((final _) => value); | 
				
			||||
 | 
				
			||||
      expect(settingsExporter.exportToFile(), equals(export)); | 
				
			||||
 | 
				
			||||
      await settingsExporter.applyFromFile(_streamValue(export)); | 
				
			||||
      verify(() => exportable.import(Map.fromEntries([value]))).called(1); | 
				
			||||
    }); | 
				
			||||
  }); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
Stream<T> _streamValue<T>(final T value) async* { | 
				
			||||
  yield value; | 
				
			||||
} | 
				
			||||
					Loading…
					
					
				
		Reference in new issue