From c44ee8ebbeef6a255d85ea9e2e595c8683409613 Mon Sep 17 00:00:00 2001 From: "Guilherme C. Matuella" Date: Thu, 25 Mar 2021 14:16:07 -0300 Subject: [PATCH 01/10] Adds `meta` and `equatable` package dependencies (both null-safe) --- pubspec.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pubspec.yaml b/pubspec.yaml index 6de57216..23ff4de1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,12 @@ dependencies: flutter: sdk: flutter + ### + # Core + ### + meta: ^1.3.0 + equatable: ^2.0.0 + dev_dependencies: strict: ^1.0.0 flutter_test: From b3728f333609bc4af02070a7bbc5ca57a4b3375c Mon Sep 17 00:00:00 2001 From: "Guilherme C. Matuella" Date: Thu, 25 Mar 2021 14:21:48 -0300 Subject: [PATCH 02/10] Adds `DatabaseRepository` abstract class --- lib/data/database_repository.dart | 59 +++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 lib/data/database_repository.dart diff --git a/lib/data/database_repository.dart b/lib/data/database_repository.dart new file mode 100644 index 00000000..b3feb4e3 --- /dev/null +++ b/lib/data/database_repository.dart @@ -0,0 +1,59 @@ +import 'package:equatable/equatable.dart'; + +/// Handles the local persistence to a database +/// +/// To store primitives values, use `StorageRepository`. +abstract class DatabaseRepository { + /// Adds an [object] to the [store], using a [serializer] + /// + /// If there is already an object with the same [KeyStorable.id], the default behavior should be merging all of its + /// fields + Future put({ + required T object, + required JsonSerializer serializer, + required DatabaseStore store, + }); + + /// Deletes the value with [key] from the [store] + Future removeObject({required String key, required DatabaseStore store}); + + /// Retrieves an object with [key] from the [store] + /// + /// Returns `null` if the key doesn't exist + Future getObject({ + required String key, + required JsonSerializer serializer, + required DatabaseStore store, + }); + + /// Retrieves all objects within [store] + Future> getAll({ + required JsonSerializer serializer, + required DatabaseStore store, + }); + + /// Retrieves a stream of all the [store] objects, triggered whenever any update occurs to this [store] + Future>> listenAll({ + required JsonSerializer serializer, + required DatabaseStore store, + }); +} + +enum DatabaseStore { + decks, + cards, + executions, + resources, +} + +/// Middleware that should be responsible of parsing a type [T] to/from a JSON representation +abstract class JsonSerializer { + T fromMap(Map json); + Map mapOf(T object); +} + +/// Base class that adds a key [id] to allow its implementation to be stored/identified in any database +abstract class KeyStorable extends Equatable { + const KeyStorable({required this.id}); + final String id; +} From fc6c6b77ecfb5155bf1cd1b5993540ddb85f57da Mon Sep 17 00:00:00 2001 From: "Guilherme C. Matuella" Date: Thu, 25 Mar 2021 14:25:37 -0300 Subject: [PATCH 03/10] Adds `sembast`, `uuid`, `path` and `path_provider` dependencies (all null-safe) --- pubspec.lock | 131 ++++++++++++++++++++++++++++++++++++++++++++++++++- pubspec.yaml | 14 +++++- 2 files changed, 142 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 3f05e28e..97cad2a4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -43,6 +43,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + equatable: + dependency: "direct main" + description: + name: equatable + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" fake_async: dependency: transitive description: @@ -50,6 +64,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.0" flutter: dependency: "direct main" description: flutter @@ -68,19 +96,89 @@ packages: source: hosted version: "0.12.10" meta: - dependency: transitive + dependency: "direct main" description: name: meta url: "https://pub.dartlang.org" source: hosted version: "1.3.0" path: - dependency: transitive + dependency: "direct main" description: name: path url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.11.0" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.1" + sembast: + dependency: "direct main" + description: + name: sembast + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0+4" sky_engine: dependency: transitive description: flutter @@ -121,6 +219,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" term_glyph: dependency: transitive description: @@ -142,6 +247,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0" + uuid: + dependency: "direct main" + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" vector_math: dependency: transitive description: @@ -149,5 +261,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" sdks: dart: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" diff --git a/pubspec.yaml b/pubspec.yaml index 23ff4de1..05b9f386 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,12 +19,24 @@ dependencies: ### meta: ^1.3.0 equatable: ^2.0.0 + path: ^1.8.0 + path_provider: ^2.0.0 + + ### + # Database & Storage + ### + sembast: ^3.0.0 + uuid: ^3.0.0 dev_dependencies: - strict: ^1.0.0 flutter_test: sdk: flutter + ### + # Lint + ### + strict: ^1.0.0 + flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in From de2a62db24bed965b76a11439cbb167f47b5e597 Mon Sep 17 00:00:00 2001 From: "Guilherme C. Matuella" Date: Thu, 25 Mar 2021 14:44:17 -0300 Subject: [PATCH 04/10] Adds `DatabaseRepository` implementation with `sembast` --- lib/data/database_repository.dart | 100 +++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 3 deletions(-) diff --git a/lib/data/database_repository.dart b/lib/data/database_repository.dart index b3feb4e3..245b4b01 100644 --- a/lib/data/database_repository.dart +++ b/lib/data/database_repository.dart @@ -1,8 +1,9 @@ +import 'dart:async'; + import 'package:equatable/equatable.dart'; +import 'package:sembast/sembast.dart'; -/// Handles the local persistence to a database -/// -/// To store primitives values, use `StorageRepository`. +/// Handles the local persistence to the database abstract class DatabaseRepository { /// Adds an [object] to the [store], using a [serializer] /// @@ -57,3 +58,96 @@ abstract class KeyStorable extends Equatable { const KeyStorable({required this.id}); final String id; } + +// +// DatabaseRepository implementation using `sembast` +// +class DatabaseRepositoryImpl implements DatabaseRepository { + DatabaseRepositoryImpl(this._db); + + // `sembast` database instance + final Database _db; + + @override + Future put({ + required T object, + required JsonSerializer serializer, + required DatabaseStore store, + }) async { + final storeMap = stringMapStoreFactory.store(store.key); + final deserializedObject = serializer.mapOf(object); + + await storeMap.record(object.id).put(_db, deserializedObject, merge: true); + } + + @override + Future removeObject({required String key, required DatabaseStore store}) async { + final storeMap = stringMapStoreFactory.store(store.key); + await storeMap.record(key).delete(_db); + } + + @override + Future getObject({ + required String key, + required JsonSerializer serializer, + required DatabaseStore store, + }) async { + final storeMap = stringMapStoreFactory.store(store.key); + final rawObject = await storeMap.record(key).get(_db); + + if (rawObject != null) { + return serializer.fromMap(rawObject); + } + + return null; + } + + @override + Future> getAll({ + required JsonSerializer serializer, + required DatabaseStore store, + }) async { + final storeMap = stringMapStoreFactory.store(store.key); + + final allRecords = await storeMap.find(_db); + + return allRecords.map((record) => serializer.fromMap(record.value)).toList(); + } + + @override + Future>> listenAll({ + required JsonSerializer serializer, + required DatabaseStore store, + }) async { + final storeMap = stringMapStoreFactory.store(store.key); + + final transformer = _snapshotSerializerTransformer(serializer); + return storeMap.query().onSnapshots(_db).transform(transformer); + } + + /// Transforms a list of `sembast` snapshot records into a list of objects parsed by [serializer] + StreamTransformer>>, List> + _snapshotSerializerTransformer(JsonSerializer serializer) { + return StreamTransformer.fromHandlers( + handleData: (snapshots, sink) { + final transformedRecords = snapshots.map((record) => serializer.fromMap(record.value)).toList(); + sink.add(transformedRecords); + }, + ); + } +} + +extension on DatabaseStore { + String get key { + switch (this) { + case DatabaseStore.decks: + return 'decks'; + case DatabaseStore.cards: + return 'cards'; + case DatabaseStore.executions: + return 'card_executions'; + case DatabaseStore.resources: + return 'resources'; + } + } +} From 2f1270b0f05e75692986f86db60fdc787d4badd9 Mon Sep 17 00:00:00 2001 From: "Guilherme C. Matuella" Date: Thu, 25 Mar 2021 15:22:50 -0300 Subject: [PATCH 05/10] Adds `openDatabase` (sembast) function and a placeholder for future migrations --- lib/data/sembast_database.dart | 40 ++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 lib/data/sembast_database.dart diff --git a/lib/data/sembast_database.dart b/lib/data/sembast_database.dart new file mode 100644 index 00000000..5cb81867 --- /dev/null +++ b/lib/data/sembast_database.dart @@ -0,0 +1,40 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; +import 'package:sembast/sembast.dart'; +import 'package:sembast/sembast_io.dart'; + +const _schemaVersion = 1; +const _dbName = 'memo_sembast.db'; + +/// Opens this application's [Database], creating a new one if non-existing +Future openDatabase() async { + final dir = await getApplicationDocumentsDirectory(); + // Make sure that the application documents directory exists + await dir.create(recursive: true); + + final dbPath = path.join(dir.path, _dbName); + + return databaseFactoryIo.openDatabase(dbPath, version: _schemaVersion, onVersionChanged: _applyMigrations); +} + +Future _applyMigrations(Database db, int oldVersion, int newVersion) async { + if (oldVersion == newVersion) { + return SynchronousFuture(null); + } + + // Call the necessary migrations in order +} + +// +// Migrations +// + +// Example: +// Future migrateToVersion2(Database db) async { +// final store = stringMapStoreFactory.store('storeThatNeedsMigration'); +// final updatableItemsFinder = Finder(filter: Filter.equals('myUpdatedField', 1)); +// await store.update(db, { 'myUpdatedField': 2 }, finder: updatableItemsFinder); +// } From b0ded8889e043951ba4833ca117f244f01e3d268 Mon Sep 17 00:00:00 2001 From: "Guilherme C. Matuella" Date: Fri, 26 Mar 2021 14:46:45 -0300 Subject: [PATCH 06/10] Adds `shouldMerge` to `DatabaseRepository.put` --- lib/data/database_repository.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/data/database_repository.dart b/lib/data/database_repository.dart index 245b4b01..c43dbf70 100644 --- a/lib/data/database_repository.dart +++ b/lib/data/database_repository.dart @@ -7,16 +7,17 @@ import 'package:sembast/sembast.dart'; abstract class DatabaseRepository { /// Adds an [object] to the [store], using a [serializer] /// - /// If there is already an object with the same [KeyStorable.id], the default behavior should be merging all of its - /// fields + /// If there is already an object with the same [KeyStorable.id], the default behavior is to merge all of its fields. + /// [shouldMerge] should be `false` if pre-existing fields should not be merged. Future put({ required T object, required JsonSerializer serializer, required DatabaseStore store, + bool shouldMerge = true, }); /// Deletes the value with [key] from the [store] - Future removeObject({required String key, required DatabaseStore store}); + Future removeObject({required String key, required DatabaseStore store}); /// Retrieves an object with [key] from the [store] /// @@ -73,15 +74,16 @@ class DatabaseRepositoryImpl implements DatabaseRepository { required T object, required JsonSerializer serializer, required DatabaseStore store, + bool shouldMerge = true, }) async { final storeMap = stringMapStoreFactory.store(store.key); final deserializedObject = serializer.mapOf(object); - await storeMap.record(object.id).put(_db, deserializedObject, merge: true); + await storeMap.record(object.id).put(_db, deserializedObject, merge: shouldMerge); } @override - Future removeObject({required String key, required DatabaseStore store}) async { + Future removeObject({required String key, required DatabaseStore store}) async { final storeMap = stringMapStoreFactory.store(store.key); await storeMap.record(key).delete(_db); } From 189f6dbd0bba2a96a25774e8e11319f337d6fc63 Mon Sep 17 00:00:00 2001 From: "Guilherme C. Matuella" Date: Fri, 26 Mar 2021 14:47:58 -0300 Subject: [PATCH 07/10] Exposes core private functionality in data layer, while annotating with @visibleForTesting --- lib/data/database_repository.dart | 4 +++- lib/data/sembast_database.dart | 11 ++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/data/database_repository.dart b/lib/data/database_repository.dart index c43dbf70..ba59c244 100644 --- a/lib/data/database_repository.dart +++ b/lib/data/database_repository.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; import 'package:sembast/sembast.dart'; /// Handles the local persistence to the database @@ -139,7 +140,8 @@ class DatabaseRepositoryImpl implements DatabaseRepository { } } -extension on DatabaseStore { +extension StoreKeys on DatabaseStore { + @visibleForTesting String get key { switch (this) { case DatabaseStore.decks: diff --git a/lib/data/sembast_database.dart b/lib/data/sembast_database.dart index 5cb81867..101c52bb 100644 --- a/lib/data/sembast_database.dart +++ b/lib/data/sembast_database.dart @@ -9,7 +9,7 @@ import 'package:sembast/sembast_io.dart'; const _schemaVersion = 1; const _dbName = 'memo_sembast.db'; -/// Opens this application's [Database], creating a new one if non-existing +/// Opens this application's [Database], creating a new one if nonexistent Future openDatabase() async { final dir = await getApplicationDocumentsDirectory(); // Make sure that the application documents directory exists @@ -17,14 +17,11 @@ Future openDatabase() async { final dbPath = path.join(dir.path, _dbName); - return databaseFactoryIo.openDatabase(dbPath, version: _schemaVersion, onVersionChanged: _applyMigrations); + return databaseFactoryIo.openDatabase(dbPath, version: _schemaVersion, onVersionChanged: applyMigrations); } -Future _applyMigrations(Database db, int oldVersion, int newVersion) async { - if (oldVersion == newVersion) { - return SynchronousFuture(null); - } - +@visibleForTesting +Future applyMigrations(Database db, int oldVersion, int newVersion) async { // Call the necessary migrations in order } From 080ee53b1a7f2ce8f1ebeaebe5023914d6b2972b Mon Sep 17 00:00:00 2001 From: "Guilherme C. Matuella" Date: Fri, 26 Mar 2021 14:53:53 -0300 Subject: [PATCH 08/10] Adds `mocktail` testing dependency --- pubspec.lock | 217 +++++++++++++++++++++++++++++++++++++++++++++++++++ pubspec.yaml | 5 ++ 2 files changed, 222 insertions(+) diff --git a/pubspec.lock b/pubspec.lock index 97cad2a4..174fcc77 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,27 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "19.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" async: dependency: transitive description: @@ -29,6 +50,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" clock: dependency: transitive description: @@ -43,6 +71,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + coverage: + dependency: transitive + description: + name: coverage + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" crypto: dependency: transitive description: @@ -88,6 +130,48 @@ packages: description: flutter source: sdk version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.3" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" matcher: dependency: transitive description: @@ -102,6 +186,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + mocktail: + dependency: "direct dev" + description: + name: mocktail + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1" + node_preamble: + dependency: transitive + description: + name: node_preamble + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.13" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" path: dependency: "direct main" description: @@ -165,6 +277,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" process: dependency: transitive description: @@ -172,6 +291,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.2.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" sembast: dependency: "direct main" description: @@ -179,11 +305,53 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.0+4" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + shelf_static: + dependency: transitive + description: + name: shelf_static + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.10" source_span: dependency: transitive description: @@ -233,6 +401,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + test: + dependency: transitive + description: + name: test + url: "https://pub.dartlang.org" + source: hosted + version: "1.16.5" test_api: dependency: transitive description: @@ -240,6 +415,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.19" + test_core: + dependency: transitive + description: + name: test_core + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.15" typed_data: dependency: transitive description: @@ -261,6 +443,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + vm_service: + dependency: transitive + description: + name: vm_service + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.0+1" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" win32: dependency: transitive description: @@ -275,6 +485,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.0" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" sdks: dart: ">=2.12.0 <3.0.0" flutter: ">=1.20.0" diff --git a/pubspec.yaml b/pubspec.yaml index 05b9f386..96e77f38 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,11 @@ dev_dependencies: ### strict: ^1.0.0 + ### + # Testing + ### + mocktail: ^0.1.0 + flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in From c47a092b866b2302537c4982b781a2c7ce1bb2c3 Mon Sep 17 00:00:00 2001 From: "Guilherme C. Matuella" Date: Fri, 26 Mar 2021 14:49:12 -0300 Subject: [PATCH 09/10] Adds `DatabaseRepositoryImpl` tests --- test/data/database_repository_test.dart | 136 ++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 test/data/database_repository_test.dart diff --git a/test/data/database_repository_test.dart b/test/data/database_repository_test.dart new file mode 100644 index 00000000..05fb1c38 --- /dev/null +++ b/test/data/database_repository_test.dart @@ -0,0 +1,136 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:memo/data/database_repository.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:sembast/sembast.dart'; +import 'package:sembast/sembast_memory.dart'; + +class MockSerializer extends Mock implements JsonSerializer {} + +// ignore: avoid_implementing_value_types +class MockSerializable extends Mock implements KeyStorable {} + +void main() { + final serializer = MockSerializer(); + final serializable = MockSerializable(); + + final fakeRawSerializable = {'fake': 'fake'}; + const fakeSerializableId = 'a-fake-id'; + const fakeStore = DatabaseStore.cards; + final fakeRecord = stringMapStoreFactory.store(fakeStore.key).record(fakeSerializableId); + + late Database memorySembast; + late DatabaseRepository db; + + setUpAll(() { + registerFallbackValue(MockSerializable()); + }); + + setUp(() async { + await databaseFactoryMemory.deleteDatabase('test.db'); + memorySembast = await databaseFactoryMemory.openDatabase('test.db'); + db = DatabaseRepositoryImpl(memorySembast); + + reset(serializer); + reset(serializable); + + when(() => serializer.mapOf(any())).thenReturn(fakeRawSerializable); + when(() => serializer.fromMap(any())).thenReturn(serializable); + when(() => serializable.id).thenReturn(fakeSerializableId); + }); + + test('DatabaseRepositoryImpl should put a new object', () async { + expect(await fakeRecord.get(memorySembast), null); + + await db.put(object: serializable, serializer: serializer, store: fakeStore); + + expect(await fakeRecord.get(memorySembast), fakeRawSerializable); + verify(() => serializer.mapOf(serializable)).called(1); + }); + + test('DatabaseRepositoryImpl should update an existing object', () async { + await fakeRecord.put(memorySembast, fakeRawSerializable); + + final fakeUpdatedRawSerializable = {'fake': 'fakeUpdated', 'newFake': 'fake'}; + when(() => serializer.mapOf(any())).thenReturn(fakeUpdatedRawSerializable); + + await db.put(object: serializable, serializer: serializer, store: fakeStore); + + expect(await fakeRecord.get(memorySembast), fakeUpdatedRawSerializable); + verify(() => serializer.mapOf(serializable)).called(1); + }); + + test('DatabaseRepositoryImpl should remove pre-existing fields in an update without merge', () async { + await fakeRecord.put(memorySembast, fakeRawSerializable); + + final fakeUpdatedRawSerializable = {'newFake': 'fake'}; + when(() => serializer.mapOf(any())).thenReturn(fakeUpdatedRawSerializable); + + await db.put(object: serializable, serializer: serializer, store: fakeStore, shouldMerge: false); + + expect(await fakeRecord.get(memorySembast), fakeUpdatedRawSerializable); + verify(() => serializer.mapOf(serializable)).called(1); + }); + + test('DatabaseRepositoryImpl should maintain pre-existing fields in an update with merge', () async { + await fakeRecord.put(memorySembast, fakeRawSerializable); + + final fakeUpdatedRawSerializable = {'newFake': 'fake'}; + when(() => serializer.mapOf(any())).thenReturn(fakeUpdatedRawSerializable); + + await db.put(object: serializable, serializer: serializer, store: fakeStore); + + fakeUpdatedRawSerializable.addAll(fakeRawSerializable); + expect(await fakeRecord.get(memorySembast), fakeUpdatedRawSerializable); + verify(() => serializer.mapOf(serializable)).called(1); + }); + + test('DatabaseRepositoryImpl should remove an existing object', () async { + await fakeRecord.put(memorySembast, fakeRawSerializable); + + expect(await fakeRecord.get(memorySembast), fakeRawSerializable); + await db.removeObject(key: fakeSerializableId, store: fakeStore); + expect(await fakeRecord.get(memorySembast), null); + }); + + test('DatabaseRepositoryImpl should do nothing when removing a nonexistent object', () async { + await fakeRecord.put(memorySembast, fakeRawSerializable); + + await db.removeObject(key: fakeSerializableId, store: fakeStore); + expect(await fakeRecord.get(memorySembast), null); + }); + + test('DatabaseRepositoryImpl should retrieve a single existing object', () async { + await fakeRecord.put(memorySembast, fakeRawSerializable); + + final object = await db.getObject( + key: fakeSerializableId, + serializer: serializer, + store: fakeStore, + ); + expect(object, isNotNull); + }); + + test('DatabaseRepositoryImpl should get null when retrieving a single nonexistent object', () async { + final object = await db.getObject( + key: fakeSerializableId, + serializer: serializer, + store: fakeStore, + ); + expect(object, null); + }); + + test('DatabaseRepositoryImpl should retrieve multiple objects', () async { + await fakeRecord.put(memorySembast, fakeRawSerializable); + await stringMapStoreFactory.store(fakeStore.key).record('2').put(memorySembast, fakeRawSerializable); + + final objects = await db.getAll(serializer: serializer, store: fakeStore); + expect(objects.length, 2); + }); + + test('DatabaseRepositoryImpl should retrieve an empty list if there is no objects in the store', () async { + final objects = await db.getAll(serializer: serializer, store: fakeStore); + expect(objects.isEmpty, true); + }); + + // TODO(matuella): Find a way to test the listenAll +} From 83fc9b13b69f45797bec8732cef79c856a4d33a0 Mon Sep 17 00:00:00 2001 From: "Guilherme C. Matuella" Date: Fri, 26 Mar 2021 14:49:31 -0300 Subject: [PATCH 10/10] Adds `applyMigration` placeholder test --- test/data/sembast_database_test.dart | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 test/data/sembast_database_test.dart diff --git a/test/data/sembast_database_test.dart b/test/data/sembast_database_test.dart new file mode 100644 index 00000000..75a12c43 --- /dev/null +++ b/test/data/sembast_database_test.dart @@ -0,0 +1,3 @@ +void main() { + // `applyMigrations` tests should go here when new versions are added +}