diff --git a/packages/neon_framework/example/pubspec.lock b/packages/neon_framework/example/pubspec.lock index 978081b4dae..ba6abcea90c 100644 --- a/packages/neon_framework/example/pubspec.lock +++ b/packages/neon_framework/example/pubspec.lock @@ -881,6 +881,13 @@ packages: relative: true source: path version: "1.0.0" + neon_storage: + dependency: "direct overridden" + description: + path: "../packages/neon_storage" + relative: true + source: path + version: "0.1.0" nested: dependency: transitive description: @@ -1337,14 +1344,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" - sqflite: - dependency: transitive - description: - name: sqflite - sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d - url: "https://pub.dev" - source: hosted - version: "2.3.3+1" sqflite_common: dependency: transitive description: diff --git a/packages/neon_framework/example/pubspec_overrides.yaml b/packages/neon_framework/example/pubspec_overrides.yaml index 3839f558d53..35c14ed7402 100644 --- a/packages/neon_framework/example/pubspec_overrides.yaml +++ b/packages/neon_framework/example/pubspec_overrides.yaml @@ -1,4 +1,4 @@ -# melos_managed_dependency_overrides: account_repository,cookie_store,dashboard_app,dynamite_runtime,files_app,interceptor_http_client,neon_framework,neon_http_client,neon_lints,news_app,nextcloud,notes_app,notifications_app,sort_box,talk_app +# melos_managed_dependency_overrides: account_repository,cookie_store,dashboard_app,dynamite_runtime,files_app,interceptor_http_client,neon_framework,neon_http_client,neon_lints,neon_storage,news_app,nextcloud,notes_app,notifications_app,sort_box,talk_app dependency_overrides: account_repository: path: ../packages/account_repository @@ -18,6 +18,8 @@ dependency_overrides: path: ../packages/neon_http_client neon_lints: path: ../../neon_lints + neon_storage: + path: ../packages/neon_storage news_app: path: ../packages/news_app nextcloud: diff --git a/packages/neon_framework/lib/neon.dart b/packages/neon_framework/lib/neon.dart index c141bd6a412..0f291c2de93 100644 --- a/packages/neon_framework/lib/neon.dart +++ b/packages/neon_framework/lib/neon.dart @@ -16,13 +16,12 @@ import 'package:neon_framework/src/blocs/push_notifications.dart'; import 'package:neon_framework/src/models/app_implementation.dart'; import 'package:neon_framework/src/models/disposable.dart'; import 'package:neon_framework/src/platform/platform.dart'; -import 'package:neon_framework/src/storage/keys.dart'; +import 'package:neon_framework/src/storage/storage.dart'; import 'package:neon_framework/src/theme/neon.dart'; import 'package:neon_framework/src/utils/global_options.dart'; import 'package:neon_framework/src/utils/provider.dart'; import 'package:neon_framework/src/utils/timezone.dart'; import 'package:neon_framework/src/utils/user_agent.dart'; -import 'package:neon_framework/storage.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:provider/provider.dart'; import 'package:timezone/data/latest.dart' as tzdata; diff --git a/packages/neon_framework/lib/src/blocs/accounts.dart b/packages/neon_framework/lib/src/blocs/accounts.dart index 541b8e34b15..12a251a943d 100644 --- a/packages/neon_framework/lib/src/blocs/accounts.dart +++ b/packages/neon_framework/lib/src/blocs/accounts.dart @@ -10,7 +10,7 @@ import 'package:neon_framework/src/blocs/maintenance_mode.dart'; import 'package:neon_framework/src/blocs/unified_search.dart'; import 'package:neon_framework/src/models/account_cache.dart'; import 'package:neon_framework/src/models/disposable.dart'; -import 'package:neon_framework/src/storage/keys.dart'; +import 'package:neon_framework/src/storage/storage_keys.dart'; import 'package:neon_framework/src/utils/account_options.dart'; import 'package:neon_framework/storage.dart'; import 'package:rxdart/rxdart.dart'; diff --git a/packages/neon_framework/lib/src/blocs/first_launch.dart b/packages/neon_framework/lib/src/blocs/first_launch.dart index 34a98a8127e..cff4b79bf83 100644 --- a/packages/neon_framework/lib/src/blocs/first_launch.dart +++ b/packages/neon_framework/lib/src/blocs/first_launch.dart @@ -4,7 +4,7 @@ import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; import 'package:neon_framework/src/bloc/bloc.dart'; import 'package:neon_framework/src/models/disposable.dart'; -import 'package:neon_framework/src/storage/keys.dart'; +import 'package:neon_framework/src/storage/storage_keys.dart'; import 'package:neon_framework/storage.dart'; import 'package:rxdart/rxdart.dart'; diff --git a/packages/neon_framework/lib/src/blocs/push_notifications.dart b/packages/neon_framework/lib/src/blocs/push_notifications.dart index 3b6967e2e72..faf96eda311 100644 --- a/packages/neon_framework/lib/src/blocs/push_notifications.dart +++ b/packages/neon_framework/lib/src/blocs/push_notifications.dart @@ -7,7 +7,7 @@ import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; import 'package:neon_framework/src/bloc/bloc.dart'; import 'package:neon_framework/src/platform/platform.dart'; -import 'package:neon_framework/src/storage/keys.dart'; +import 'package:neon_framework/src/storage/storage_keys.dart'; import 'package:neon_framework/src/utils/global_options.dart'; import 'package:neon_framework/src/utils/push_utils.dart'; import 'package:neon_framework/storage.dart'; diff --git a/packages/neon_framework/lib/src/models/app_implementation.dart b/packages/neon_framework/lib/src/models/app_implementation.dart index c9f7cf1af34..0a7e2f70b79 100644 --- a/packages/neon_framework/lib/src/models/app_implementation.dart +++ b/packages/neon_framework/lib/src/models/app_implementation.dart @@ -10,7 +10,7 @@ import 'package:neon_framework/src/bloc/bloc.dart'; import 'package:neon_framework/src/models/account_cache.dart'; import 'package:neon_framework/src/models/disposable.dart'; import 'package:neon_framework/src/settings/models/options_collection.dart'; -import 'package:neon_framework/src/storage/keys.dart'; +import 'package:neon_framework/src/storage/storage_keys.dart'; import 'package:neon_framework/src/utils/findable.dart'; import 'package:neon_framework/src/utils/provider.dart'; import 'package:neon_framework/src/widgets/drawer_destination.dart'; diff --git a/packages/neon_framework/lib/src/platform/linux.dart b/packages/neon_framework/lib/src/platform/linux.dart index 73a52bcbfb1..9bd4d983e20 100644 --- a/packages/neon_framework/lib/src/platform/linux.dart +++ b/packages/neon_framework/lib/src/platform/linux.dart @@ -3,7 +3,6 @@ import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; import 'package:meta/meta.dart'; import 'package:neon_framework/src/platform/platform.dart'; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:universal_io/io.dart'; /// Linux specific platform information. @@ -43,10 +42,7 @@ class LinuxNeonPlatform implements NeonPlatform { bool get canUsePaths => true; @override - void init() { - sqfliteFfiInit(); - databaseFactory = databaseFactoryFfi; - } + void init() {} @override Future saveFileWithPickDialog(String fileName, String mimeType, Uint8List data) async { diff --git a/packages/neon_framework/lib/src/platform/web.dart b/packages/neon_framework/lib/src/platform/web.dart index 14dd819dd32..48869d7abf0 100644 --- a/packages/neon_framework/lib/src/platform/web.dart +++ b/packages/neon_framework/lib/src/platform/web.dart @@ -4,8 +4,6 @@ import 'dart:typed_data'; import 'package:meta/meta.dart'; import 'package:neon_framework/src/platform/platform.dart'; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; -import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart'; import 'package:web/web.dart'; @immutable @@ -39,9 +37,7 @@ class WebNeonPlatform implements NeonPlatform { bool get canUsePaths => false; @override - Future init() async { - databaseFactory = databaseFactoryFfiWeb; - } + void init() {} @override Future saveFileWithPickDialog(String fileName, String mimeType, Uint8List data) async { diff --git a/packages/neon_framework/lib/src/settings/utils/settings_export_helper.dart b/packages/neon_framework/lib/src/settings/utils/settings_export_helper.dart index 3ee5408043c..fd21a6e2a18 100644 --- a/packages/neon_framework/lib/src/settings/utils/settings_export_helper.dart +++ b/packages/neon_framework/lib/src/settings/utils/settings_export_helper.dart @@ -8,7 +8,7 @@ import 'package:neon_framework/models.dart'; import 'package:neon_framework/src/blocs/accounts.dart'; import 'package:neon_framework/src/settings/models/exportable.dart'; import 'package:neon_framework/src/settings/models/option.dart'; -import 'package:neon_framework/src/storage/keys.dart'; +import 'package:neon_framework/src/storage/storage_keys.dart'; import 'package:neon_framework/src/utils/findable.dart'; /// Helper class to export all [Option]s. diff --git a/packages/neon_framework/lib/src/storage/neon_cache_db.dart b/packages/neon_framework/lib/src/storage/neon_cache_db.dart new file mode 100644 index 00000000000..0dafb6afecd --- /dev/null +++ b/packages/neon_framework/lib/src/storage/neon_cache_db.dart @@ -0,0 +1,67 @@ +import 'dart:async'; + +import 'package:cookie_store/cookie_store.dart'; +import 'package:flutter/foundation.dart'; +import 'package:neon_storage/neon_sqlite.dart'; +import 'package:neon_storage/neon_storage.dart'; + +import 'package:path_provider/path_provider.dart'; + +/// Database holding the neon cache. +final class NeonCacheDB extends MultiTableDatabase { + /// Creates a new database with the given [tables]. + factory NeonCacheDB({ + Iterable? tables, + }) { + return NeonCacheDB._( + tables: [ + ...?tables, + if (!kIsWeb) SQLiteRequestCache.table, + if (!kIsWeb) SQLiteCookiePersistence.table, + ], + ); + } + + NeonCacheDB._({ + required super.tables, + }); + + @override + String get name => 'cache'; + + @override + Future get path async { + final cacheDir = await getApplicationCacheDirectory(); + + return buildDatabasePath(cacheDir.path, name); + } + + /// The current request cache if available. + RequestCache? get requestCache { + if (kIsWeb) { + return null; + } + + assertInitialized(); + + return const SQLiteRequestCache(); + } + + /// Creates a new `CookieStore` scoped to the given [accountID] and [serverURL]. + /// + /// Cookies will only be sent to cookies matching the [serverURL]. + CookieStore? cookieStore({required String accountID, required Uri serverURL}) { + if (kIsWeb) { + return null; + } + + assertInitialized(); + + final persistence = SQLiteCookiePersistence( + accountID: accountID, + allowedBaseUri: serverURL, + ); + + return DefaultCookieStore(persistence); + } +} diff --git a/packages/neon_framework/lib/src/storage/neon_data_db.dart b/packages/neon_framework/lib/src/storage/neon_data_db.dart new file mode 100644 index 00000000000..73b769cf117 --- /dev/null +++ b/packages/neon_framework/lib/src/storage/neon_data_db.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:neon_storage/neon_sqlite.dart'; +import 'package:neon_storage/neon_storage.dart'; +import 'package:path_provider/path_provider.dart'; + +/// Database holding the neon data. +final class NeonDataDB extends MultiTableDatabase { + /// Creates a new database with the given [tables]. + factory NeonDataDB({ + Iterable
? tables, + }) { + return NeonDataDB._( + tables: [ + ...?tables, + SQLiteCachedPersistence.table, + ], + ); + } + + NeonDataDB._({ + required super.tables, + }); + + @override + String get name => 'preferences'; + + @override + Future get path async { + final cacheDir = await getApplicationSupportDirectory(); + + return buildDatabasePath(cacheDir.path, name); + } +} diff --git a/packages/neon_framework/lib/src/storage/persistence.dart b/packages/neon_framework/lib/src/storage/persistence.dart deleted file mode 100644 index e42bc218477..00000000000 --- a/packages/neon_framework/lib/src/storage/persistence.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'dart:async'; - -import 'package:meta/meta.dart' show protected; - -/// A persistent key value storage. -abstract interface class Persistence { - /// Whether a value exists at the given [key]. - FutureOr containsKey(String key); - - /// Clears all values from persistent storage. - FutureOr clear(); - - /// Removes an entry from persistent storage. - FutureOr remove(String key); - - /// Saves a [value] to persistent storage. - FutureOr setValue(String key, T value); - - /// Fetches the value persisted at the given [key] from the persistent - /// storage. - FutureOr getValue(String key); -} - -/// A key value persistence that caches read values to be accessed -/// synchronously. -/// -/// Mutating values is asynchronous. -abstract class CachedPersistence implements Persistence { - /// Fetches the latest values from the host platform. - /// - /// Use this method to observe modifications that were made in the background - /// like another isolate or native code while the app is already running. - Future reload(); - - /// The cache that holds all values. - /// - /// It is instantiated to the current state of the backing database and then - /// kept in sync via setter methods in this class. - /// - /// It is NOT guaranteed that this cache and the backing database will remain - /// in sync since the setter method might fail for any reason. - @protected - final Map cache = {}; - - @override - T? getValue(String key) => cache[key]; - - /// Saves a [value] to the cached storage. - /// - /// Use this method to cache type conversions of the value that do not change - /// the meaning of the actual value like a `BuiltList` to `List` conversion. - /// Changes will not be persisted to the backing storage and will be lost - /// when the app is restarted. - void setCache(String key, T value) => cache[key] = value; - - @override - bool containsKey(String key) => cache.containsKey(key); - - @override - Future clear(); - - @override - Future remove(String key); - - @override - Future setValue(String key, T value); -} diff --git a/packages/neon_framework/lib/src/storage/request_cache.dart b/packages/neon_framework/lib/src/storage/request_cache.dart deleted file mode 100644 index 24f7a49db10..00000000000 --- a/packages/neon_framework/lib/src/storage/request_cache.dart +++ /dev/null @@ -1,251 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:http/http.dart' as http; -import 'package:logging/logging.dart'; -import 'package:meta/meta.dart'; -import 'package:neon_framework/models.dart'; -import 'package:neon_framework/src/platform/platform.dart'; -import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart'; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; - -final _log = Logger('RequestCache'); - -/// A storage used to cache HTTP requests. -abstract interface class RequestCache { - /// Gets the cached status code, body and headers for the [request]. - Future get(Account account, http.Request request); - - /// Sets the cached [response] for the [request]. - /// - /// If a request is already present it will be updated with the new one. - Future set(Account account, http.Request request, http.Response response); - - /// Updates the cache [headers] for the [request] without modifying anything else. - Future updateHeaders(Account account, http.Request request, Map headers); -} - -/// Default implementation of the [RequestCache]. -/// -/// Values are persisted locally in an SQLite database in the application cache -/// directory. -/// -/// The database must be initialized with by calling `DefaultRequestCache().init()` -/// and awaiting it's completion. If the database is not yet initialized a -/// `StateError` will be thrown. -@internal -final class DefaultRequestCache implements RequestCache { - /// Creates a new request cache instance. - /// - /// There should be no need to create multiple instances. - DefaultRequestCache(); - - @visibleForTesting - Database? database; - - /// Initializes this request cache by setting up the backing SQLite database. - /// - /// This must called and completed before accessing other methods of the cache. - Future init() async { - if (database != null) { - return; - } - - assert( - NeonPlatform.instance.canUsePaths, - 'Tried to initialize DefaultRequestCache on a platform without support for paths', - ); - final cacheDir = await getApplicationCacheDirectory(); - database = await openDatabase( - p.join(cacheDir.path, 'cache.db'), - version: 7, - onCreate: onCreate, - onUpgrade: (db, oldVersion, newVersion) async { - // We can safely drop the table as it only contains cached data. - // Non breaking migrations should not drop the cache. The next - // breaking change should remove all non breaking migrations before it. - await db.transaction((txn) async { - if (oldVersion <= 6) { - await txn.execute('DROP TABLE cache'); - await onCreate(txn); - } - }); - }, - ); - } - - @visibleForTesting - static Future onCreate(DatabaseExecutor db, [int? version]) async { - await db.execute(''' -CREATE TABLE "cache" ( - "account" TEXT NOT NULL, - "request_method" TEXT NOT NULL, - "request_url" TEXT NOT NULL, - "request_headers" TEXT NOT NULL, - "request_body" BLOB NOT NULL, - "response_status_code" INTEGER NOT NULL, - "response_headers" TEXT NOT NULL, - "response_body" BLOB NOT NULL, - PRIMARY KEY("account", "request_method", "request_url", "request_headers", "request_body") -); -'''); - } - - Database get _requireDatabase { - final database = this.database; - if (database == null) { - throw StateError( - 'Cache has not been set up yet. Please make sure DefaultRequestCache.init() has been called before and completed.', - ); - } - - return database; - } - - @override - Future get(Account account, http.Request request) async { - List>? result; - try { - result = await _requireDatabase.rawQuery( - ''' -SELECT response_status_code, - response_headers, - response_body -FROM cache -WHERE account = ? - AND request_method = ? - AND request_url = ? - AND request_headers = ? - AND request_body = ? -''', - [ - account.id, - request.method.toUpperCase(), - request.url.toString(), - json.encode(sanitizeRequestHeaders(request.headers)), - request.bodyBytes, - ], - ); - } on DatabaseException catch (error, stackTrace) { - _log.severe( - 'Error while getting `$request` from cache.', - error, - stackTrace, - ); - } - - final row = result?.singleOrNull; - if (row == null) { - return null; - } - - return http.Response.bytes( - row['response_body']! as Uint8List, - row['response_status_code']! as int, - headers: (json.decode(row['response_headers']! as String) as Map).cast(), - ); - } - - @override - Future set(Account account, http.Request request, http.Response response) async { - final encodedRequestHeaders = json.encode(sanitizeRequestHeaders(request.headers)); - final encodedResponseHeaders = json.encode(response.headers); - - try { - // UPSERT is only available since SQLite 3.24.0 (June 4, 2018). - // Using a manual solution from https://stackoverflow.com/a/38463024 - final batch = _requireDatabase.batch() - ..update( - 'cache', - { - 'account': account.id, - 'request_method': request.method.toUpperCase(), - 'request_url': request.url.toString(), - 'request_headers': encodedRequestHeaders, - 'request_body': request.bodyBytes, - 'response_status_code': response.statusCode, - 'response_headers': encodedResponseHeaders, - 'response_body': response.bodyBytes, - }, - where: 'account = ? AND request_method = ? AND request_url = ? AND request_headers = ? AND request_body = ?', - whereArgs: [ - account.id, - request.method.toUpperCase(), - request.url.toString(), - encodedRequestHeaders, - request.bodyBytes, - ], - ) - ..rawInsert( - ''' -INSERT INTO cache ( - account, - request_method, - request_url, - request_headers, - request_body, - response_status_code, - response_headers, - response_body -) -SELECT ?, ?, ?, ?, ?, ?, ?, ? -WHERE (SELECT changes() = 0) -''', - [ - account.id, - request.method.toUpperCase(), - request.url.toString(), - encodedRequestHeaders, - request.bodyBytes, - response.statusCode, - encodedResponseHeaders, - response.bodyBytes, - ], - ); - await batch.commit(noResult: true); - } on DatabaseException catch (error, stackTrace) { - _log.severe( - 'Error while setting `$request` in the cache.', - error, - stackTrace, - ); - } - } - - @override - Future updateHeaders(Account account, http.Request request, Map headers) async { - try { - await _requireDatabase.update( - 'cache', - { - 'account': account.id, - 'response_headers': json.encode(headers), - }, - where: 'account = ? AND request_method = ? AND request_url = ? AND request_headers = ? AND request_body = ?', - whereArgs: [ - account.id, - request.method.toUpperCase(), - request.url.toString(), - json.encode(sanitizeRequestHeaders(request.headers)), - request.bodyBytes, - ], - ); - } on DatabaseException catch (error, stackTrace) { - _log.severe( - 'Error while updating headers at `$request`.', - error, - stackTrace, - ); - } - } - - /// Ensures header names are case-insensitive and removes headers that would potentially prevent caching. - Map sanitizeRequestHeaders(Map headers) { - return headers.map((key, value) => MapEntry(key.toLowerCase(), value)) - ..remove('authorization') - ..remove('cookie') - ..remove('user-agent'); - } -} diff --git a/packages/neon_framework/lib/src/storage/settings_store.dart b/packages/neon_framework/lib/src/storage/settings_store.dart index 0297904a439..0df34e3046f 100644 --- a/packages/neon_framework/lib/src/storage/settings_store.dart +++ b/packages/neon_framework/lib/src/storage/settings_store.dart @@ -1,38 +1,7 @@ // ignore_for_file: avoid_positional_boolean_parameters import 'package:meta/meta.dart'; -import 'package:neon_framework/src/storage/persistence.dart'; - -/// A storage that can save a group of values primarily used by `Option`s. -/// -/// Mimics a subset of the `SharedPreferences` interface to synchronously -/// access data while persisting changes in the background. -/// -/// See: -/// * `NeonStorage` to initialize and manage different storage backends. -abstract interface class SettingsStore { - /// The id that uniquely identifies this app storage. - /// - /// Used in `Exportable` classes. - String get id; - - /// Reads a value from persistent storage, throwing an `Exception` if it is - /// not a `String`. - String? getString(String key); - - /// Saves a `String` [value] to persistent storage in the background. - Future setString(String key, String value); - - /// Reads a value from persistent storage, throwing an `Exception` if it is - /// not a `bool`. - bool? getBool(String key); - - /// Saves a `bool` [value] to persistent storage in the background. - Future setBool(String key, bool value); - - /// Removes an entry from persistent storage. - Future remove(String key); -} +import 'package:neon_framework/src/storage/storage.dart'; /// Default implementation of the [SettingsStore] backed by the given [persistence]. @immutable diff --git a/packages/neon_framework/lib/src/storage/single_value_store.dart b/packages/neon_framework/lib/src/storage/single_value_store.dart index ae3830e91e4..316a27bddad 100644 --- a/packages/neon_framework/lib/src/storage/single_value_store.dart +++ b/packages/neon_framework/lib/src/storage/single_value_store.dart @@ -2,47 +2,7 @@ import 'package:built_collection/built_collection.dart'; import 'package:meta/meta.dart'; -import 'package:neon_framework/src/storage/keys.dart'; -import 'package:neon_framework/src/storage/persistence.dart'; - -/// A storage that itself is a single entry of a key value store. -/// -/// Mimics a subset of the `SharedPreferences` interface to synchronously -/// access a single value while persisting changes in the background. -/// -/// See: -/// * `NeonStorage` to initialize and manage different storage backends. -abstract interface class SingleValueStore { - /// The key used by the storage backend. - StorageKeys get key; - - /// Returns true if the persistent storage contains a value at the given [key]. - bool hasValue(); - - /// Reads a value from persistent storage, throwing an `Exception` if it is - /// not a `String`. - String? getString(); - - /// Saves a `String` [value] to persistent storage in the background. - Future setString(String value); - - /// Reads a value from persistent storage, throwing an `Exception` if it is - /// not a `bool`. - bool? getBool(); - - /// Saves a `bool` [value] to persistent storage in the background. - Future setBool(bool value); - - /// Removes an entry from persistent storage. - Future remove(); - - /// Reads a set of string values from persistent storage, throwing an - /// `Exception` if it's not a `String` collection. - BuiltList? getStringList(); - - /// Saves a list of strings [value] to persistent storage in the background. - Future setStringList(BuiltList value); -} +import 'package:neon_framework/src/storage/storage.dart'; /// Default implementation of the [SingleValueStore] backed by the given [persistence]. @immutable diff --git a/packages/neon_framework/lib/src/storage/sqlite_cookie_persistence.dart b/packages/neon_framework/lib/src/storage/sqlite_cookie_persistence.dart deleted file mode 100644 index 0cc54189f32..00000000000 --- a/packages/neon_framework/lib/src/storage/sqlite_cookie_persistence.dart +++ /dev/null @@ -1,380 +0,0 @@ -import 'dart:async'; - -import 'package:cookie_store/cookie_store.dart'; -import 'package:logging/logging.dart'; -import 'package:meta/meta.dart'; -import 'package:neon_framework/platform.dart'; -import 'package:nextcloud/utils.dart'; -import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart'; -import 'package:sqflite/sqflite.dart'; -import 'package:universal_io/io.dart' show Cookie; - -const String _unsupportedCookieManagementMessage = - 'The neon project does not plan to support individual cookie management.'; - -const String _lastAccessMaxDuration = "STRFTIME('%s', 'now', '-1 years')"; - -/// An SQLite backed cookie persistence. -/// -/// No maximum cookie age is set as mentioned in [RFC6265 section 7.3](https://httpwg.org/specs/rfc6265.html#rfc.section.7.3). -/// -/// This persistence does not support cookie management through the [deleteAll] -/// and [deleteWhere] methods. Calling them will throw a [UnsupportedError]. -@internal -final class SQLiteCookiePersistence implements CookiePersistence { - /// Creates a new SQLite backed cookie persistence for the given account. - /// - /// Optionally [allowedBaseUri] can be used to restrict storage and loading of cookies to a certain domain and path. - SQLiteCookiePersistence({ - required this.accountID, - Uri? allowedBaseUri, - }) : allowedBaseUri = switch (allowedBaseUri) { - null => null, - Uri(:final path) when path.endsWith('/') => Uri( - host: allowedBaseUri.host, - path: path.substring(0, path.length - 1), - ), - _ => allowedBaseUri, - }; - - final _log = Logger('SQLiteCookiePersistence'); - - /// The allowed cookie base uri. - /// - /// When not null, only cookies domain and path matching this uri will be - /// persisted. - /// - /// See: - /// * domain matching [RFC6265 section 5.1.3](https://httpwg.org/specs/rfc6265.html#rfc.section.5.1.3) - /// * path matching [RFC6265 section 5.1.4](https://httpwg.org/specs/rfc6265.html#rfc.section.5.1.4) - final Uri? allowedBaseUri; - - /// The id of the requesting account. - final String accountID; - - @visibleForTesting - static Database? database; - - /// Initializes this cookie persistence by setting up the backing SQLite database. - /// - /// This must called and completed before accessing other methods of the cache. - static Future init() async { - if (database != null) { - return; - } - - assert( - NeonPlatform.instance.canUsePaths, - 'Tried to initialize SQLiteCookiePersistence on a platform without support for paths', - ); - final cacheDir = await getApplicationCacheDirectory(); - database = await openDatabase( - p.join(cacheDir.path, 'cookies.db'), - version: 1, - onCreate: onCreate, - ); - } - - @visibleForTesting - static Future onCreate(Database db, int version) async { - // https://httpwg.org/specs/rfc6265.html#storage-model with an extra account key. - await db.execute(''' -CREATE TABLE "cookies" ( - "account" TEXT NOT NULL, - "name" TEXT NOT NULL, - "value" TEXT NOT NULL, - "expiry-time" INTEGER NOT NULL, - "domain" TEXT NOT NULL, - "path" TEXT NOT NULL, - "creation-time" INTEGER NOT NULL, - "last-access-time" INTEGER NOT NULL, - "persistent-flag" BOOLEAN NOT NULL, - "host-only-flag" BOOLEAN NOT NULL, - "secure-only-flag" BOOLEAN NOT NULL, - "http-only-flag" BOOLEAN NOT NULL, - PRIMARY KEY("account", "name", "domain", "path") -); - -CREATE TRIGGER delete_outdated_cookies - AFTER UPDATE - ON cookies -BEGIN - DELETE FROM cookies - WHERE - "expiry-time" <= STRFTIME('%s') - OR "last-access-time" <= $_lastAccessMaxDuration; -END; -'''); - } - - Database get _requireDatabase { - if (database == null) { - throw StateError( - 'Cookie persistence has not been set up yet. Please make sure SQLiteCookiePersistence.init() has been called before and completed.', - ); - } - - return database!; - } - - @override - Future> loadForRequest(Uri uri) async { - final requestHost = uri.host; - var requestPath = uri.path; - if (requestPath.isEmpty) { - requestPath = '/'; - } - - if (!_isAllowedUri(requestHost, requestPath)) { - return []; - } - - final isHttpRequest = isHttpUri(uri); - final isSecureRequest = isSecureUri(uri); - - // domain and patch matching is error prone in SQL; - // use the dart helpers for now. - try { - final results = await _requireDatabase.rawQuery( - ''' -SELECT "name", "domain", "path", "value" -FROM "cookies" -WHERE "account" = ? - AND ("domain" = ? OR "host-only-flag" = 0) - AND ("secure-only-flag" = 0 OR ?) - AND ("http-only-flag" = 0 OR ?) - AND "expiry-time" >= STRFTIME('%s') - AND "last-access-time" >= $_lastAccessMaxDuration -ORDER BY - length("path") DESC, - "creation-time" ASC -''', - [ - accountID, - requestHost, - if (isSecureRequest) 1 else 0, - if (isHttpRequest) 1 else 0, - ], - ); - - final list = []; - final batch = _requireDatabase.batch(); - - for (final result in results) { - final name = result['name']! as String; - final domain = result['domain']! as String; - final path = result['path']! as String; - final value = result['value']! as String; - - if (!isDomainMatch(requestHost, domain) || !isPathMatch(requestPath, path)) { - continue; - } - - final cookie = Cookie(name, value); - list.add(cookie); - - batch.rawUpdate( - ''' -UPDATE "cookies" -SET "last-access-time" = STRFTIME('%s') -WHERE "account" = ? - AND "name" = ? - AND "domain" = ? - AND "path" = ? -''', - [ - accountID, - name, - domain, - path, - ], - ); - } - - if (batch.length >= 1) { - await batch.commit(noResult: true); - } - - return list; - } on DatabaseException catch (error, stackTrace) { - _log.warning( - 'Error loading cookies.', - error, - stackTrace, - ); - } - - return []; - } - - @override - Future saveFromResponse(Set cookies, {required bool isHttpRequest}) async { - if (cookies.isEmpty) { - return true; - } - - final batch = _requireDatabase.batch(); - - for (final cookie in cookies) { - batch - ..update( - 'cookies', - { - '"name"': cookie.name, - '"value"': cookie.value, - '"expiry-time"': cookie.expiryTime.secondsSinceEpoch, - '"domain"': cookie.domain, - '"path"': cookie.path, - // DO NOT update the creation time - '"last-access-time"': cookie.lastAccessTime.secondsSinceEpoch, - '"persistent-flag"': cookie.persistentFlag ? 1 : 0, - '"host-only-flag"': cookie.hostOnlyFlag ? 1 : 0, - '"secure-only-flag"': cookie.secureOnlyFlag ? 1 : 0, - '"http-only-flag"': cookie.httpOnlyFlag ? 1 : 0, - }, - where: ''' -"account" = ? -AND "name" = ? -AND "domain" = ? -AND "path" = ? -AND ("http-only-flag" = 0 OR ? = 1) -''', - whereArgs: [ - accountID, - cookie.name, - cookie.domain, - cookie.path, - if (isHttpRequest) 1 else 0, - ], - ) - // Ignore already present records. - // This happens when the cookie was not updated because of the http-only-flag - ..rawInsert( - ''' -INSERT OR IGNORE INTO cookies ("account", "name", "value", "expiry-time", "domain", "path", "creation-time", "last-access-time", "persistent-flag", "host-only-flag", "secure-only-flag", "http-only-flag") -SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? -WHERE (SELECT changes() = 0) -''', - [ - accountID, - cookie.name, - cookie.value, - cookie.expiryTime.secondsSinceEpoch, - cookie.domain, - cookie.path, - cookie.creationTime.secondsSinceEpoch, - cookie.lastAccessTime.secondsSinceEpoch, - if (cookie.persistentFlag) 1 else 0, - if (cookie.hostOnlyFlag) 1 else 0, - if (cookie.secureOnlyFlag) 1 else 0, - if (cookie.httpOnlyFlag) 1 else 0, - ], - ); - } - - try { - await batch.commit(noResult: true); - } on DatabaseException catch (error, stackTrace) { - _log.warning( - 'Error persisting cookies.', - error, - stackTrace, - ); - - return false; - } - - return true; - } - - @override - Future endSession() async { - try { - await _requireDatabase.execute( - ''' -DELETE FROM cookies -WHERE - "account" = ? - AND ("persistent-flag" = 0 - OR "expiry-time" <= STRFTIME('%s')) -''', - [accountID], - ); - - return true; - } on DatabaseException catch (error, stackTrace) { - _log.warning( - 'Error removing session cookies.', - error, - stackTrace, - ); - - return false; - } - } - - /// Not implemented by this persistence. - @override - bool deleteWhere(bool Function(Cookie cookie) test) { - throw UnsupportedError(_unsupportedCookieManagementMessage); - } - - /// Not implemented by this persistence. - @override - bool deleteAll() { - throw UnsupportedError(_unsupportedCookieManagementMessage); - } - - /// This method is only meant for testing. - @visibleForTesting - @override - Future> loadAll() async { - final list = []; - - try { - final results = await _requireDatabase.rawQuery( - ''' -SELECT "name", "value" -FROM "cookies" -WHERE "account" = ? -''', - [accountID], - ); - - for (final result in results) { - final cookie = Cookie( - result['name']! as String, - result['value']! as String, - ); - - list.add(cookie); - } - } on DatabaseException catch (error, stackTrace) { - _log.warning( - 'Error loading cookies.', - error, - stackTrace, - ); - } - - return list; - } - - bool _isAllowedUri(String domain, String path) { - final allowedBaseUri = this.allowedBaseUri; - if (allowedBaseUri == null) { - return true; - } - - if (allowedBaseUri.host != domain) { - return false; - } - - if (allowedBaseUri.path.isNotEmpty && !isPathMatch(path, allowedBaseUri.path)) { - return false; - } - - return true; - } -} diff --git a/packages/neon_framework/lib/src/storage/sqlite_persistence.dart b/packages/neon_framework/lib/src/storage/sqlite_persistence.dart deleted file mode 100644 index 636a91bb4cb..00000000000 --- a/packages/neon_framework/lib/src/storage/sqlite_persistence.dart +++ /dev/null @@ -1,264 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:built_collection/built_collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:logging/logging.dart'; -import 'package:meta/meta.dart' show internal; -import 'package:neon_framework/platform.dart'; -import 'package:neon_framework/src/storage/persistence.dart'; -import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart'; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; - -final _log = Logger('SQLitePersistence'); - -/// An SQLite backed cached persistence for preferences. -/// -/// There is only one cache backing all `SQLitePersistence` instances. -/// Use the [prefix] to separate different storages. -/// Keys within a storage must be unique. -/// -/// The persistence must be initialized with by calling `SQLitePersistence().init()` -/// and awaiting it's completion. If it has not yet initialized a `StateError` -/// will be thrown. -@internal -final class SQLiteCachedPersistence extends CachedPersistence { - /// Creates a new sqlite persistence. - SQLiteCachedPersistence({this.prefix = ''}); - - /// The prefix of this persistence. - /// - /// Keys within it must be unique. - final String prefix; - - @override - Map get cache => globalCache[prefix] ??= {}; - - @visibleForTesting - static final Map> globalCache = {}; - - @visibleForTesting - static Database? database; - - /// Initializes all persistences by setting up the backing SQLite database - /// and priming the global cache. - /// - /// This must be called and completed before accessing any other methods of - /// the sqlite persistence. - static Future init() async { - if (database != null) { - return; - } - - var path = 'preferences.db'; - if (NeonPlatform.instance.canUsePaths) { - final appDir = await getApplicationSupportDirectory(); - path = p.join(appDir.path, path); - } - - database = await openDatabase( - path, - version: 1, - onCreate: onCreate, - ); - - await getAll(); - } - - @visibleForTesting - static Future onCreate(Database db, int version) async { - await db.execute(''' -CREATE TABLE "preferences" ( - "prefix" TEXT NOT NULL, - "key" TEXT NOT NULL, - "value" TEXT NOT NULL, - PRIMARY KEY("prefix","key"), - UNIQUE("key","prefix") -); -'''); - } - - static Database get _requireDatabase { - if (database == null) { - throw StateError( - 'Persistence has not been set up yet. Please make sure SQLitePersistence.init() has been called before and completed.', - ); - } - - return database!; - } - - @visibleForTesting - static Future getAll() async { - try { - final results = await _requireDatabase.rawQuery(''' -SELECT prefix, key, value -FROM preferences -'''); - - globalCache.clear(); - for (final result in results) { - final prefix = result['prefix']! as String; - final key = result['key']! as String; - final value = result['value']! as String; - - final cache = globalCache[prefix] ??= {}; - cache[key] = _decode(value) as Object; - } - } on DatabaseException catch (error) { - _log.warning( - 'Error fetching all values from the SQLite persistence.', - error, - ); - } - } - - @override - Future clear() async { - try { - await _requireDatabase.rawDelete( - ''' -DELETE FROM preferences -WHERE prefix = ? -''', - [prefix], - ); - cache.clear(); - } on DatabaseException catch (error) { - _log.warning( - 'Error clearing the SQLite persistence.', - error, - ); - - return false; - } - - return true; - } - - @override - Future reload() async { - try { - final fromSystem = {}; - - final results = await _requireDatabase.rawQuery( - ''' -SELECT key, value -FROM preferences -WHERE prefix = ? -''', - [prefix], - ); - for (final result in results) { - final key = result['key']! as String; - final value = result['value']! as String; - - fromSystem[key] = _decode(value) as Object; - } - - cache - ..clear() - ..addAll(fromSystem); - } on DatabaseException catch (error) { - _log.warning( - 'Error reloading the SQLite persistence.', - error, - ); - } - } - - @override - Future remove(String key) async { - try { - await _requireDatabase.rawDelete( - ''' -DELETE FROM preferences -WHERE - prefix = ? - AND key = ? -''', - [prefix, key], - ); - - cache.remove(key); - } on DatabaseException catch (error) { - _log.warning( - 'Error removing the value from the SQLite persistence.', - error, - ); - return false; - } - - return true; - } - - @override - Future setValue(String key, Object value) async { - final serialized = _encode(value); - - try { - // UPSERT is only available since SQLite 3.24.0 (June 4, 2018). - // Using a manual solution from https://stackoverflow.com/a/38463024 - final batch = _requireDatabase.batch() - ..update( - 'preferences', - { - 'prefix': prefix, - 'key': key, - 'value': serialized, - }, - where: 'prefix = ? AND key = ?', - whereArgs: [prefix, key], - ) - ..rawInsert( - ''' -INSERT INTO preferences (prefix, key, value) -SELECT ?, ?, ? -WHERE (SELECT changes() = 0) -''', - [prefix, key, serialized], - ); - await batch.commit(noResult: true); - - cache[key] = value; - } on DatabaseException catch (error) { - _log.warning( - 'Error updating the storage value.', - error, - ); - - return false; - } - - return true; - } - - static dynamic _decode(String source) => json.decode( - source, - reviver: (key, value) { - switch (value) { - case List(): - return BuiltList.from(value); - case Map(): - return BuiltMap.from(value); - case _: - return value; - } - }, - ); - - static String _encode(dynamic object) => json.encode( - object, - toEncodable: (nonEncodable) { - switch (nonEncodable) { - case BuiltList(): - return nonEncodable.toList(); - case BuiltMap(): - return nonEncodable.toMap(); - case _: - return nonEncodable; - } - }, - ); -} diff --git a/packages/neon_framework/lib/src/storage/storage.dart b/packages/neon_framework/lib/src/storage/storage.dart new file mode 100644 index 00000000000..7e9c2e64767 --- /dev/null +++ b/packages/neon_framework/lib/src/storage/storage.dart @@ -0,0 +1,7 @@ +export 'package:neon_framework/src/storage/neon_cache_db.dart'; +export 'package:neon_framework/src/storage/neon_data_db.dart'; +export 'package:neon_framework/src/storage/settings_store.dart'; +export 'package:neon_framework/src/storage/single_value_store.dart'; +export 'package:neon_framework/src/storage/storage_keys.dart'; +export 'package:neon_framework/src/storage/storage_manager.dart'; +export 'package:neon_framework/storage.dart'; diff --git a/packages/neon_framework/lib/src/storage/keys.dart b/packages/neon_framework/lib/src/storage/storage_keys.dart similarity index 81% rename from packages/neon_framework/lib/src/storage/keys.dart rename to packages/neon_framework/lib/src/storage/storage_keys.dart index cf80c4c48bd..0384cefa78e 100644 --- a/packages/neon_framework/lib/src/storage/keys.dart +++ b/packages/neon_framework/lib/src/storage/storage_keys.dart @@ -1,12 +1,6 @@ import 'package:meta/meta.dart'; -/// Interface of a storable element. -/// -/// Usually used in enhanced enums to ensure uniqueness of the storage keys. -abstract interface class Storable { - /// The key of this storage element. - String get value; -} +import 'package:neon_framework/src/storage/storage.dart'; /// Unique storage keys. /// diff --git a/packages/neon_framework/lib/src/storage/storage_manager.dart b/packages/neon_framework/lib/src/storage/storage_manager.dart index 10844c2e22e..52a6425e519 100644 --- a/packages/neon_framework/lib/src/storage/storage_manager.dart +++ b/packages/neon_framework/lib/src/storage/storage_manager.dart @@ -1,14 +1,8 @@ import 'dart:async'; import 'package:cookie_store/cookie_store.dart'; -import 'package:flutter/foundation.dart'; import 'package:meta/meta.dart'; -import 'package:neon_framework/src/storage/keys.dart'; -import 'package:neon_framework/src/storage/request_cache.dart'; -import 'package:neon_framework/src/storage/settings_store.dart'; -import 'package:neon_framework/src/storage/single_value_store.dart'; -import 'package:neon_framework/src/storage/sqlite_cookie_persistence.dart'; -import 'package:neon_framework/src/storage/sqlite_persistence.dart'; +import 'package:neon_framework/src/storage/storage.dart'; /// Neon storage that manages the storage backend. /// @@ -45,27 +39,23 @@ class NeonStorage { return; } - if (!kIsWeb) { - final requestCache = DefaultRequestCache(); - await requestCache.init(); - _requestCache = requestCache; + _neonCache = NeonCacheDB(); + _neonData = NeonDataDB(); - await SQLiteCookiePersistence.init(); - } - - await SQLiteCachedPersistence.init(); + await _neonCache.init(); + await _neonData.init(); _initialized = true; } - /// Request cache instance. - RequestCache? _requestCache; + late NeonCacheDB _neonCache; + late NeonDataDB _neonData; /// The current request cache if available. RequestCache? get requestCache { _assertInitialized(); - return _requestCache; + return _neonCache.requestCache; } /// Initializes a new `SettingsStorage`. @@ -101,16 +91,9 @@ class NeonStorage { /// /// Cookies will only be sent to cookies matching the [serverURL]. CookieStore? cookieStore({required String accountID, required Uri serverURL}) { - if (kIsWeb) { - return null; - } - - final persistence = SQLiteCookiePersistence( - accountID: accountID, - allowedBaseUri: serverURL, - ); + _assertInitialized(); - return DefaultCookieStore(persistence); + return _neonCache.cookieStore(accountID: accountID, serverURL: serverURL); } void _assertInitialized() { diff --git a/packages/neon_framework/lib/src/testing/mocks.dart b/packages/neon_framework/lib/src/testing/mocks.dart index 131fbd0269e..2937b8a3874 100644 --- a/packages/neon_framework/lib/src/testing/mocks.dart +++ b/packages/neon_framework/lib/src/testing/mocks.dart @@ -14,7 +14,6 @@ import 'package:neon_framework/src/blocs/accounts.dart'; import 'package:neon_framework/src/models/account_cache.dart'; import 'package:neon_framework/src/models/disposable.dart'; import 'package:neon_framework/src/settings/models/exportable.dart'; -import 'package:neon_framework/src/storage/persistence.dart'; import 'package:neon_framework/src/utils/account_options.dart'; import 'package:neon_framework/storage.dart'; import 'package:neon_framework/testing.dart'; @@ -90,10 +89,6 @@ class FakeNeonStorage extends Fake implements NeonStorage { Null cookieStore({required String accountID, required Uri serverURL}) => null; } -class MockCachedPersistence extends Mock implements CachedPersistence {} - -class MockPersistence extends Mock implements Persistence {} - class MockSettingsStore extends Mock implements SettingsStore {} class MockSharedPreferencesPlatform extends Mock implements SharedPreferencesStorePlatform { diff --git a/packages/neon_framework/lib/src/utils/global_options.dart b/packages/neon_framework/lib/src/utils/global_options.dart index a844099185a..3fe5213e044 100644 --- a/packages/neon_framework/lib/src/utils/global_options.dart +++ b/packages/neon_framework/lib/src/utils/global_options.dart @@ -5,7 +5,7 @@ import 'package:neon_framework/l10n/localizations.dart'; import 'package:neon_framework/models.dart'; import 'package:neon_framework/src/settings/models/option.dart'; import 'package:neon_framework/src/settings/models/options_collection.dart'; -import 'package:neon_framework/src/storage/keys.dart'; +import 'package:neon_framework/src/storage/storage_keys.dart'; import 'package:neon_framework/storage.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:permission_handler/permission_handler.dart'; diff --git a/packages/neon_framework/lib/src/utils/push_utils.dart b/packages/neon_framework/lib/src/utils/push_utils.dart index 9ee530d57d4..2f8f3261bce 100644 --- a/packages/neon_framework/lib/src/utils/push_utils.dart +++ b/packages/neon_framework/lib/src/utils/push_utils.dart @@ -15,7 +15,7 @@ import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; import 'package:neon_framework/src/bloc/result.dart'; import 'package:neon_framework/src/models/push_notification.dart'; -import 'package:neon_framework/src/storage/keys.dart'; +import 'package:neon_framework/src/storage/storage_keys.dart'; import 'package:neon_framework/src/theme/colors.dart'; import 'package:neon_framework/src/utils/account_client_extension.dart'; import 'package:neon_framework/src/utils/image_utils.dart'; diff --git a/packages/neon_framework/lib/src/utils/request_manager.dart b/packages/neon_framework/lib/src/utils/request_manager.dart index e39e7101fe5..f5d2c17bbd3 100644 --- a/packages/neon_framework/lib/src/utils/request_manager.dart +++ b/packages/neon_framework/lib/src/utils/request_manager.dart @@ -85,7 +85,7 @@ class RequestManager { var request = getRequest(); - final cachedResponse = await _cache?.get(account, request); + final cachedResponse = await _cache?.get(account.id, request); if (subject.isClosed) { return; } @@ -126,7 +126,7 @@ class RequestManager { if (newParameters.etag == etag) { unawaited( _cache?.updateHeaders( - account, + account.id, request, newHeaders, ), @@ -173,7 +173,7 @@ class RequestManager { subject.add(Result.success(unwrap(converter.convert(response)))); await _cache?.set( - account, + account.id, request, response, ); diff --git a/packages/neon_framework/lib/storage.dart b/packages/neon_framework/lib/storage.dart index 6cf00b16351..fb5d80bb6d0 100644 --- a/packages/neon_framework/lib/storage.dart +++ b/packages/neon_framework/lib/storage.dart @@ -3,8 +3,5 @@ /// The `NeonStorage` manages all storage backends. library; -export 'package:neon_framework/src/storage/keys.dart' show Storable; -export 'package:neon_framework/src/storage/request_cache.dart' show RequestCache; -export 'package:neon_framework/src/storage/settings_store.dart' show SettingsStore; -export 'package:neon_framework/src/storage/single_value_store.dart' show SingleValueStore; -export 'package:neon_framework/src/storage/storage_manager.dart'; +export 'package:neon_framework/src/storage/storage.dart' show NeonStorage; +export 'package:neon_storage/neon_storage.dart'; diff --git a/packages/neon_framework/packages/account_repository/lib/src/testing/testing_account.dart b/packages/neon_framework/packages/account_repository/lib/src/testing/testing_account.dart index f9546990689..c4868cb8366 100644 --- a/packages/neon_framework/packages/account_repository/lib/src/testing/testing_account.dart +++ b/packages/neon_framework/packages/account_repository/lib/src/testing/testing_account.dart @@ -4,6 +4,7 @@ import 'package:account_repository/testing.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart' as http; import 'package:meta/meta.dart'; +import 'package:neon_framework/testing.dart' show MockNeonStorage; final _mockClient = http.MockClient((request) async { throw UnsupportedError('The fake account client can not be used in tests.'); @@ -19,6 +20,7 @@ Account createAccount({ http.Client? httpClient, }) { credentials ??= createCredentials(); + MockNeonStorage(); return Account((b) { b diff --git a/packages/neon_framework/packages/account_repository/pubspec_overrides.yaml b/packages/neon_framework/packages/account_repository/pubspec_overrides.yaml index 5c2f161f46d..4b435bdc1a4 100644 --- a/packages/neon_framework/packages/account_repository/pubspec_overrides.yaml +++ b/packages/neon_framework/packages/account_repository/pubspec_overrides.yaml @@ -1,4 +1,4 @@ -# melos_managed_dependency_overrides: cookie_store,dynamite_runtime,interceptor_http_client,neon_framework,neon_http_client,neon_lints,nextcloud,sort_box +# melos_managed_dependency_overrides: cookie_store,dynamite_runtime,interceptor_http_client,neon_framework,neon_http_client,neon_lints,neon_storage,nextcloud,sort_box dependency_overrides: cookie_store: path: ../../../cookie_store @@ -12,6 +12,8 @@ dependency_overrides: path: ../neon_http_client neon_lints: path: ../../../neon_lints + neon_storage: + path: ../neon_storage nextcloud: path: ../../../nextcloud sort_box: diff --git a/packages/neon_framework/packages/dashboard_app/pubspec_overrides.yaml b/packages/neon_framework/packages/dashboard_app/pubspec_overrides.yaml index 0abe46d2bdd..c24ede0eb8c 100644 --- a/packages/neon_framework/packages/dashboard_app/pubspec_overrides.yaml +++ b/packages/neon_framework/packages/dashboard_app/pubspec_overrides.yaml @@ -1,4 +1,4 @@ -# melos_managed_dependency_overrides: account_repository,cookie_store,dynamite_runtime,interceptor_http_client,neon_framework,neon_http_client,neon_lints,nextcloud,sort_box +# melos_managed_dependency_overrides: account_repository,cookie_store,dynamite_runtime,interceptor_http_client,neon_framework,neon_http_client,neon_lints,neon_storage,nextcloud,sort_box dependency_overrides: account_repository: path: ../account_repository @@ -14,6 +14,8 @@ dependency_overrides: path: ../neon_http_client neon_lints: path: ../../../neon_lints + neon_storage: + path: ../neon_storage nextcloud: path: ../../../nextcloud sort_box: diff --git a/packages/neon_framework/packages/files_app/pubspec_overrides.yaml b/packages/neon_framework/packages/files_app/pubspec_overrides.yaml index 0abe46d2bdd..c24ede0eb8c 100644 --- a/packages/neon_framework/packages/files_app/pubspec_overrides.yaml +++ b/packages/neon_framework/packages/files_app/pubspec_overrides.yaml @@ -1,4 +1,4 @@ -# melos_managed_dependency_overrides: account_repository,cookie_store,dynamite_runtime,interceptor_http_client,neon_framework,neon_http_client,neon_lints,nextcloud,sort_box +# melos_managed_dependency_overrides: account_repository,cookie_store,dynamite_runtime,interceptor_http_client,neon_framework,neon_http_client,neon_lints,neon_storage,nextcloud,sort_box dependency_overrides: account_repository: path: ../account_repository @@ -14,6 +14,8 @@ dependency_overrides: path: ../neon_http_client neon_lints: path: ../../../neon_lints + neon_storage: + path: ../neon_storage nextcloud: path: ../../../nextcloud sort_box: diff --git a/packages/neon_framework/packages/neon_storage/pubspec_overrides.yaml b/packages/neon_framework/packages/neon_storage/pubspec_overrides.yaml index d4b2d2ff99a..fbf277d4fc5 100644 --- a/packages/neon_framework/packages/neon_storage/pubspec_overrides.yaml +++ b/packages/neon_framework/packages/neon_storage/pubspec_overrides.yaml @@ -1,4 +1,4 @@ -# melos_managed_dependency_overrides: neon_lints,cookie_store,cookie_store_conformance_tests,dynamite_runtime,nextcloud +# melos_managed_dependency_overrides: cookie_store,cookie_store_conformance_tests,dynamite_runtime,neon_lints,nextcloud dependency_overrides: cookie_store: path: ../../../cookie_store diff --git a/packages/neon_framework/packages/news_app/pubspec_overrides.yaml b/packages/neon_framework/packages/news_app/pubspec_overrides.yaml index 0abe46d2bdd..c24ede0eb8c 100644 --- a/packages/neon_framework/packages/news_app/pubspec_overrides.yaml +++ b/packages/neon_framework/packages/news_app/pubspec_overrides.yaml @@ -1,4 +1,4 @@ -# melos_managed_dependency_overrides: account_repository,cookie_store,dynamite_runtime,interceptor_http_client,neon_framework,neon_http_client,neon_lints,nextcloud,sort_box +# melos_managed_dependency_overrides: account_repository,cookie_store,dynamite_runtime,interceptor_http_client,neon_framework,neon_http_client,neon_lints,neon_storage,nextcloud,sort_box dependency_overrides: account_repository: path: ../account_repository @@ -14,6 +14,8 @@ dependency_overrides: path: ../neon_http_client neon_lints: path: ../../../neon_lints + neon_storage: + path: ../neon_storage nextcloud: path: ../../../nextcloud sort_box: diff --git a/packages/neon_framework/packages/notes_app/pubspec_overrides.yaml b/packages/neon_framework/packages/notes_app/pubspec_overrides.yaml index 0abe46d2bdd..c24ede0eb8c 100644 --- a/packages/neon_framework/packages/notes_app/pubspec_overrides.yaml +++ b/packages/neon_framework/packages/notes_app/pubspec_overrides.yaml @@ -1,4 +1,4 @@ -# melos_managed_dependency_overrides: account_repository,cookie_store,dynamite_runtime,interceptor_http_client,neon_framework,neon_http_client,neon_lints,nextcloud,sort_box +# melos_managed_dependency_overrides: account_repository,cookie_store,dynamite_runtime,interceptor_http_client,neon_framework,neon_http_client,neon_lints,neon_storage,nextcloud,sort_box dependency_overrides: account_repository: path: ../account_repository @@ -14,6 +14,8 @@ dependency_overrides: path: ../neon_http_client neon_lints: path: ../../../neon_lints + neon_storage: + path: ../neon_storage nextcloud: path: ../../../nextcloud sort_box: diff --git a/packages/neon_framework/packages/notifications_app/pubspec_overrides.yaml b/packages/neon_framework/packages/notifications_app/pubspec_overrides.yaml index 0abe46d2bdd..c24ede0eb8c 100644 --- a/packages/neon_framework/packages/notifications_app/pubspec_overrides.yaml +++ b/packages/neon_framework/packages/notifications_app/pubspec_overrides.yaml @@ -1,4 +1,4 @@ -# melos_managed_dependency_overrides: account_repository,cookie_store,dynamite_runtime,interceptor_http_client,neon_framework,neon_http_client,neon_lints,nextcloud,sort_box +# melos_managed_dependency_overrides: account_repository,cookie_store,dynamite_runtime,interceptor_http_client,neon_framework,neon_http_client,neon_lints,neon_storage,nextcloud,sort_box dependency_overrides: account_repository: path: ../account_repository @@ -14,6 +14,8 @@ dependency_overrides: path: ../neon_http_client neon_lints: path: ../../../neon_lints + neon_storage: + path: ../neon_storage nextcloud: path: ../../../nextcloud sort_box: diff --git a/packages/neon_framework/packages/talk_app/pubspec_overrides.yaml b/packages/neon_framework/packages/talk_app/pubspec_overrides.yaml index 0abe46d2bdd..c24ede0eb8c 100644 --- a/packages/neon_framework/packages/talk_app/pubspec_overrides.yaml +++ b/packages/neon_framework/packages/talk_app/pubspec_overrides.yaml @@ -1,4 +1,4 @@ -# melos_managed_dependency_overrides: account_repository,cookie_store,dynamite_runtime,interceptor_http_client,neon_framework,neon_http_client,neon_lints,nextcloud,sort_box +# melos_managed_dependency_overrides: account_repository,cookie_store,dynamite_runtime,interceptor_http_client,neon_framework,neon_http_client,neon_lints,neon_storage,nextcloud,sort_box dependency_overrides: account_repository: path: ../account_repository @@ -14,6 +14,8 @@ dependency_overrides: path: ../neon_http_client neon_lints: path: ../../../neon_lints + neon_storage: + path: ../neon_storage nextcloud: path: ../../../nextcloud sort_box: diff --git a/packages/neon_framework/pubspec.yaml b/packages/neon_framework/pubspec.yaml index a06c793f4da..784fe17208f 100644 --- a/packages/neon_framework/pubspec.yaml +++ b/packages/neon_framework/pubspec.yaml @@ -50,10 +50,13 @@ dependencies: neon_http_client: git: url: https://github.com/nextcloud/neon - path: packages/neon_http_client + path: packages/neon_framework/packages/neon_http_client + neon_storage: + git: + url: https://github.com/nextcloud/neon + path: packages/neon_framework/packages/neon_storage nextcloud: ^7.0.0 package_info_plus: ^8.0.0 - path: ^1.0.0 path_provider: ^2.1.0 permission_handler: ^11.0.0 provider: ^6.0.0 @@ -65,9 +68,6 @@ dependencies: git: url: https://github.com/nextcloud/neon path: packages/neon_framework/packages/sort_box - sqflite: ^2.3.0 - sqflite_common_ffi: ^2.3.2 - sqflite_common_ffi_web: ^0.4.2+3 timezone: ^0.9.4 unifiedpush: ^5.0.0 unifiedpush_android: ^2.0.0 diff --git a/packages/neon_framework/pubspec_overrides.yaml b/packages/neon_framework/pubspec_overrides.yaml index 73b6a2ad440..f858cdf6e64 100644 --- a/packages/neon_framework/pubspec_overrides.yaml +++ b/packages/neon_framework/pubspec_overrides.yaml @@ -1,4 +1,4 @@ -# melos_managed_dependency_overrides: account_repository,cookie_store,cookie_store_conformance_tests,dynamite_runtime,interceptor_http_client,neon_http_client,neon_lints,nextcloud,sort_box +# melos_managed_dependency_overrides: account_repository,cookie_store,cookie_store_conformance_tests,dynamite_runtime,interceptor_http_client,neon_http_client,neon_lints,neon_storage,nextcloud,sort_box dependency_overrides: account_repository: path: packages/account_repository @@ -14,6 +14,8 @@ dependency_overrides: path: packages/neon_http_client neon_lints: path: ../neon_lints + neon_storage: + path: packages/neon_storage nextcloud: path: ../nextcloud sort_box: diff --git a/packages/neon_framework/test/options_collection_test.dart b/packages/neon_framework/test/options_collection_test.dart index bc3696fa866..a2496e9ca36 100644 --- a/packages/neon_framework/test/options_collection_test.dart +++ b/packages/neon_framework/test/options_collection_test.dart @@ -1,7 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:neon_framework/settings.dart'; -import 'package:neon_framework/src/storage/keys.dart'; +import 'package:neon_framework/storage.dart'; import 'package:neon_framework/testing.dart'; class Collection extends AppImplementationOptions { diff --git a/packages/neon_framework/test/persistence_test.dart b/packages/neon_framework/test/persistence_test.dart deleted file mode 100644 index 7f5108ac976..00000000000 --- a/packages/neon_framework/test/persistence_test.dart +++ /dev/null @@ -1,302 +0,0 @@ -// ignore_for_file: inference_failure_on_instance_creation, strict_raw_type, inference_failure_on_collection_literal - -import 'package:built_collection/built_collection.dart'; -import 'package:cookie_store/cookie_store.dart'; -import 'package:cookie_store_conformance_tests/cookie_store_conformance_tests.dart' as cookie_jar_conformance; -import 'package:flutter_test/flutter_test.dart'; -import 'package:http/http.dart' as http; -import 'package:neon_framework/src/storage/request_cache.dart'; -import 'package:neon_framework/src/storage/sqlite_cookie_persistence.dart'; -import 'package:neon_framework/src/storage/sqlite_persistence.dart'; -import 'package:neon_framework/testing.dart'; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; -import 'package:universal_io/io.dart' show Cookie; - -void main() { - group('Persistences', () { - group('RequestCache', () { - final account = MockAccount(); - - final cache = DefaultRequestCache(); - - setUpAll(() { - sqfliteFfiInit(); - databaseFactory = databaseFactoryFfi; - }); - - setUp(() async { - cache.database = await openDatabase( - inMemoryDatabasePath, - version: 1, - onCreate: DefaultRequestCache.onCreate, - singleInstance: false, - ); - }); - - test('init', () { - cache.database = null; - expect(() async => cache.get(account, http.Request('GET', Uri())), throwsA(isA())); - }); - - test('RequestCache', () async { - final request = http.Request('GET', Uri(host: 'example.com')) - ..headers.addAll({'a': 'b'}) - ..body = 'c'; - - var result = await cache.get(account, request); - expect(result, isNull); - - await cache.set(account, request, http.Response('value', 200, headers: {'key': 'value'})); - result = await cache.get(account, request); - expect(result?.statusCode, 200); - expect(result?.body, 'value'); - expect(result?.headers, {'key': 'value'}); - - for (final modifiedRequest in [ - http.Request('POST', Uri(host: 'example.com')) - ..headers.addAll({'a': 'b'}) - ..body = 'c', - http.Request('GET', Uri(host: 'example.org')) - ..headers.addAll({'a': 'b'}) - ..body = 'c', - http.Request('GET', Uri(host: 'example.com')) - ..headers.addAll({'a': 'd'}) - ..body = 'c', - http.Request('GET', Uri(host: 'example.com')) - ..headers.addAll({'a': 'b'}) - ..body = 'e', - ]) { - result = await cache.get(account, modifiedRequest); - expect(result, isNull); - } - - await cache.set(account, request, http.Response('upsert', 201, headers: {'key': 'updated'})); - result = await cache.get(account, request); - expect(result?.statusCode, 201); - expect(result?.body, 'upsert'); - expect(result?.headers, {'key': 'updated'}); - - await cache.set(account, request, http.Response('value', 200, headers: {'key': 'value'})); - result = await cache.get(account, request); - expect(result?.statusCode, 200); - expect(result?.body, 'value'); - expect(result?.headers, {'key': 'value'}); - - await cache.updateHeaders(account, request, {'key': 'updated'}); - result = await cache.get(account, request); - expect(result?.statusCode, 200); - expect(result?.body, 'value'); - expect(result?.headers, {'key': 'updated'}); - }); - }); - - group('SQLitePersistence', () { - setUpAll(() { - sqfliteFfiInit(); - databaseFactory = databaseFactoryFfi; - }); - - setUp(() async { - SQLiteCachedPersistence.database = await openDatabase( - inMemoryDatabasePath, - version: 1, - onCreate: SQLiteCachedPersistence.onCreate, - singleInstance: false, - ); - }); - - tearDown(() { - SQLiteCachedPersistence.globalCache.clear(); - SQLiteCachedPersistence.database = null; - }); - - test('Uninitialized', () { - SQLiteCachedPersistence.database = null; - - expect(() async => SQLiteCachedPersistence().clear(), throwsA(isA())); - }); - - test('init reloads all values', () async { - await SQLiteCachedPersistence().setValue('key', 'value'); - - SQLiteCachedPersistence(prefix: 'garbage').setCache('key2', 'value2'); - SQLiteCachedPersistence(prefix: 'garbage2').setCache('key2', 'value2'); - - await SQLiteCachedPersistence.getAll(); - expect( - SQLiteCachedPersistence.globalCache, - equals( - { - '': {'key': 'value'}, - }, - ), - ); - }); - - test('reload unrelated persistence', () async { - await SQLiteCachedPersistence().setValue('key', 'value'); - - SQLiteCachedPersistence().setCache('key2', 'value2'); - SQLiteCachedPersistence(prefix: 'garbage').setCache('key2', 'value2'); - - await SQLiteCachedPersistence().reload(); - expect( - SQLiteCachedPersistence.globalCache, - equals( - { - '': {'key': 'value'}, - 'garbage': {'key2': 'value2'}, - }, - ), - ); - }); - - test('remove unrelated persistence', () async { - await SQLiteCachedPersistence().setValue('key', 'value'); - await SQLiteCachedPersistence(prefix: 'prefix').setValue('pKey', 'pValue'); - - await SQLiteCachedPersistence().remove('key'); - expect( - SQLiteCachedPersistence.globalCache, - equals( - { - '': {}, - 'prefix': {'pKey': 'pValue'}, - }, - ), - ); - - await SQLiteCachedPersistence().reload(); - expect( - SQLiteCachedPersistence.globalCache, - equals( - { - '': {}, - 'prefix': {'pKey': 'pValue'}, - }, - ), - ); - }); - - test('persist built_collection', () async { - await SQLiteCachedPersistence().setValue('string-key', 'value'); - await SQLiteCachedPersistence().setValue('num-key', 4); - await SQLiteCachedPersistence().setValue('bool-key', false); - await SQLiteCachedPersistence().setValue('built-list-key', BuiltList(['hi', 'there'])); - await SQLiteCachedPersistence().setValue('built-map-key', BuiltMap({'hi': 'again'})); - - await SQLiteCachedPersistence().reload(); - expect( - SQLiteCachedPersistence().cache, - equals({ - 'bool-key': false, - 'built-list-key': BuiltList(['hi', 'there']), - 'built-map-key': BuiltMap({'hi': 'again'}), - 'num-key': 4, - 'string-key': 'value', - }), - ); - expect(SQLiteCachedPersistence().getValue('built-list-key'), isA()); - expect(SQLiteCachedPersistence().getValue('built-map-key'), isA()); - }); - - test('clear unrelated persistence', () async { - await SQLiteCachedPersistence().setValue('key', 'value'); - await SQLiteCachedPersistence(prefix: 'prefix').setValue('pKey', 'pValue'); - - await SQLiteCachedPersistence(prefix: 'prefix').clear(); - expect(SQLiteCachedPersistence(prefix: 'prefix').cache, isEmpty); - expect(SQLiteCachedPersistence().cache, isNotEmpty); - - await SQLiteCachedPersistence(prefix: 'prefix').reload(); - expect(SQLiteCachedPersistence(prefix: 'prefix').cache, isEmpty); - expect(SQLiteCachedPersistence().cache, isNotEmpty); - }); - - test('contains key', () async { - SQLiteCachedPersistence().setCache('key', 'value'); - expect(SQLiteCachedPersistence().containsKey('key'), isTrue); - expect(SQLiteCachedPersistence(prefix: 'prefix').containsKey('key'), isFalse); - }); - }); - - group(SQLiteCookiePersistence, () { - setUpAll(() { - sqfliteFfiInit(); - databaseFactory = databaseFactoryFfi; - }); - - tearDown(() { - SQLiteCookiePersistence.database = null; - }); - - test('Uninitialized', () { - SQLiteCookiePersistence.database = null; - - final persistence = SQLiteCookiePersistence(accountID: 'accountID'); - expect(() async => persistence.loadAll(), throwsA(isA())); - }); - - cookie_jar_conformance.testAll( - () async { - SQLiteCookiePersistence.database = await openDatabase( - inMemoryDatabasePath, - version: 1, - onCreate: SQLiteCookiePersistence.onCreate, - singleInstance: false, - ); - - final persistence = SQLiteCookiePersistence(accountID: 'accountID'); - return DefaultCookieStore(persistence); - }, - canDeleteAll: false, - canDeleteByTest: false, - ); - - group('Restrict allowedBaseUri', () { - late CookieStore cookieStore; - - setUpAll(() async { - SQLiteCookiePersistence.database = await openDatabase( - inMemoryDatabasePath, - version: 1, - onCreate: SQLiteCookiePersistence.onCreate, - singleInstance: false, - ); - - cookieStore = DefaultCookieStore( - SQLiteCookiePersistence( - accountID: 'accountID', - allowedBaseUri: Uri(host: 'example.com', path: '/subpath/'), - ), - ); - }); - - for (final element in [ - (Uri(host: 'example.com', path: '/subpath/other'), isNotEmpty), - (Uri(host: 'example.com', path: '/subpath'), isNotEmpty), - (Uri(host: 'example.com', path: '/subpathTest'), isEmpty), - (Uri(host: 'example.com', path: '/'), isEmpty), - (Uri(host: 'test.com', path: '/subpath/other'), isEmpty), - ]) { - final uri = element.$1; - test(uri, () async { - final cookies = [ - Cookie(uri.host, 'value') - ..domain = uri.host - ..path = uri.path - ..httpOnly = false - ..secure = false, - ]; - - await cookieStore.saveFromResponse(uri, cookies); - expect( - await cookieStore.loadForRequest(uri), - element.$2, - ); - }); - } - }); - }); - }); -} diff --git a/packages/neon_framework/test/request_manager_test.dart b/packages/neon_framework/test/request_manager_test.dart index a775c85cf89..20edbf91dac 100644 --- a/packages/neon_framework/test/request_manager_test.dart +++ b/packages/neon_framework/test/request_manager_test.dart @@ -264,10 +264,10 @@ void main() { ); await subject.close(); - verify(() => cache.get(account, any())).called(1); + verify(() => cache.get(account.id, any())).called(1); verify( () => cache.set( - account, + account.id, any(), any( that: isA() @@ -299,10 +299,10 @@ void main() { ); await subject.close(); - verify(() => cache.get(account, any())).called(1); + verify(() => cache.get(account.id, any())).called(1); verify( () => cache.set( - account, + account.id, any(), any( that: isA() @@ -336,7 +336,7 @@ void main() { ); await subject.close(); - verify(() => cache.get(account, any())).called(1); + verify(() => cache.get(account.id, any())).called(1); verifyNever(() => cache.set(any(), any(), any())); subject = BehaviorSubject>.seeded(Result.success('Seed value')); @@ -360,7 +360,7 @@ void main() { ); await subject.close(); - verify(() => cache.get(account, any())).called(1); + verify(() => cache.get(account.id, any())).called(1); verifyNever(() => cache.set(any(), any(), any())); }); @@ -397,7 +397,7 @@ void main() { ); await subject.close(); - verify(() => cache.get(account, any())).called(1); + verify(() => cache.get(account.id, any())).called(1); verifyNever(() => cache.set(any(), any(), any())); when(() => cache.get(any(), any())).thenAnswer( @@ -433,7 +433,7 @@ void main() { ); await subject.close(); - verify(() => cache.get(account, any())).called(1); + verify(() => cache.get(account.id, any())).called(1); verify(() => cache.set(any(), any(), any())).called(1); }); @@ -486,9 +486,9 @@ void main() { ); await subject.close(); - verify(() => cache.get(account, any())).called(1); + verify(() => cache.get(account.id, any())).called(1); verify(callback.call).called(1); - verify(() => cache.updateHeaders(account, any(), {'etag': 'a', 'expires': newExpires})).called(1); + verify(() => cache.updateHeaders(account.id, any(), {'etag': 'a', 'expires': newExpires})).called(1); verifyNever(() => cache.set(any(), any(), any())); when(() => cache.get(any(), any())).thenAnswer( @@ -531,11 +531,11 @@ void main() { ); await subject.close(); - verify(() => cache.get(account, any())).called(1); + verify(() => cache.get(account.id, any())).called(1); verify(callback.call).called(1); verify( () => cache.set( - account, + account.id, any(), any( that: isA() @@ -589,10 +589,10 @@ void main() { ); await subject.close(); - verify(() => cache.get(account, any())).called(1); + verify(() => cache.get(account.id, any())).called(1); verify( () => cache.set( - account, + account.id, any(), any( that: isA() diff --git a/packages/neon_framework/test/storage/settings_store_test.dart b/packages/neon_framework/test/storage/settings_store_test.dart new file mode 100644 index 00000000000..6b106da8215 --- /dev/null +++ b/packages/neon_framework/test/storage/settings_store_test.dart @@ -0,0 +1,49 @@ +// ignore_for_file: inference_failure_on_instance_creation + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:neon_framework/src/storage/settings_store.dart'; +import 'package:neon_framework/src/storage/storage_keys.dart'; +import 'package:neon_framework/storage.dart'; + +class _MockCachedPersistence extends Mock implements CachedPersistence {} + +void main() { + group('Storages', () { + late CachedPersistence persistence; + + setUp(() { + persistence = _MockCachedPersistence(); + }); + + test('DefaultSettingsStore', () async { + final appStorage = DefaultSettingsStore(persistence, StorageKeys.accountOptions.value); + const key = 'key'; + + when(() => persistence.remove(key)).thenAnswer((_) => Future.value(false)); + dynamic result = await appStorage.remove(key); + expect(result, equals(false)); + verify(() => persistence.remove(key)).called(1); + + when(() => persistence.getValue(key)).thenReturn(null); + result = appStorage.getString(key); + expect(result, isNull); + verify(() => persistence.getValue(key)).called(1); + + when(() => persistence.setValue(key, 'value')).thenAnswer((_) => Future.value(false)); + result = await appStorage.setString(key, 'value'); + expect(result, false); + verify(() => persistence.setValue(key, 'value')).called(1); + + when(() => persistence.getValue(key)).thenReturn(true); + result = appStorage.getBool(key); + expect(result, equals(true)); + verify(() => persistence.getValue(key)).called(1); + + when(() => persistence.setValue(key, true)).thenAnswer((_) => Future.value(true)); + result = await appStorage.setBool(key, true); + expect(result, true); + verify(() => persistence.setValue(key, true)).called(1); + }); + }); +} diff --git a/packages/neon_framework/test/storage_test.dart b/packages/neon_framework/test/storage/single_value_store_test.dart similarity index 59% rename from packages/neon_framework/test/storage_test.dart rename to packages/neon_framework/test/storage/single_value_store_test.dart index 492552982d9..9fee1af3ef3 100644 --- a/packages/neon_framework/test/storage_test.dart +++ b/packages/neon_framework/test/storage/single_value_store_test.dart @@ -3,50 +3,21 @@ import 'package:built_collection/built_collection.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:neon_framework/src/storage/keys.dart'; -import 'package:neon_framework/src/storage/settings_store.dart'; import 'package:neon_framework/src/storage/single_value_store.dart'; -import 'package:neon_framework/testing.dart'; +import 'package:neon_framework/src/storage/storage_keys.dart'; +import 'package:neon_framework/storage.dart'; + +class _MockCachedPersistence extends Mock implements CachedPersistence {} void main() { group('Storages', () { - late MockCachedPersistence persistence; + late CachedPersistence persistence; setUp(() { - persistence = MockCachedPersistence(); - }); - - test('AppStorage interface', () async { - final appStorage = DefaultSettingsStore(persistence, StorageKeys.accountOptions.value); - const key = 'key'; - - when(() => persistence.remove(key)).thenAnswer((_) => Future.value(false)); - dynamic result = await appStorage.remove(key); - expect(result, equals(false)); - verify(() => persistence.remove(key)).called(1); - - when(() => persistence.getValue(key)).thenReturn(null); - result = appStorage.getString(key); - expect(result, isNull); - verify(() => persistence.getValue(key)).called(1); - - when(() => persistence.setValue(key, 'value')).thenAnswer((_) => Future.value(false)); - result = await appStorage.setString(key, 'value'); - expect(result, false); - verify(() => persistence.setValue(key, 'value')).called(1); - - when(() => persistence.getValue(key)).thenReturn(true); - result = appStorage.getBool(key); - expect(result, equals(true)); - verify(() => persistence.getValue(key)).called(1); - - when(() => persistence.setValue(key, true)).thenAnswer((_) => Future.value(true)); - result = await appStorage.setBool(key, true); - expect(result, true); - verify(() => persistence.setValue(key, true)).called(1); + persistence = _MockCachedPersistence(); }); - test('SingleValueStorage', () async { + test('DefaultSingleValueStore', () async { final storage = DefaultSingleValueStore(persistence, StorageKeys.global); final key = StorageKeys.global.value;