diff --git a/CHANGELOG.md b/CHANGELOG.md index ede9a02..eceee9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,4 +32,5 @@ ## 3.0.0 -- Some parameters have been replaced by different rules, which will definitely introduce breaking changes \ No newline at end of file +- Some parameters have been replaced by different rules, which will definitely introduce breaking changes +- Full coverage test \ No newline at end of file diff --git a/README.md b/README.md index e828a67..6bc22bb 100644 --- a/README.md +++ b/README.md @@ -44,42 +44,52 @@ typedef DB = LocalShared; ![LocalShared Collection.gif](https://github.com/Nialixus/local_shared/assets/45191605/6f5be9e9-892b-4381-9691-98c7cc92c92b) -This guide illustrates fundamental CRUD (Create, Read, Update, Delete) operations for collection management. Interacting with it can be achieved through the following methods: `Shared.col(id)` or `Shared.collection(id)`. +`SharedCollection` organizes documents in a JSON map. Use `Shared.col(id)` or `Shared.collection(id)`, +then operate with the following methods. ### Create -To initiate the creation of a new collection, utilize this method: - ```dart final result = await Shared.col('myCollection').create(); -print(result); // SharedMany(success: true, message: '...', data: []) +// SharedMany(success: true, message: ..., data: []) ``` -### Read +- `replace: true` can overwrite existing collection. -To retrieve information pertaining to a collection, invoke this method: +### Read ```dart final response = await Shared.col('myCollection').read(); -print(response); // SharedMany(success: true, message: '...', data: []) +// SharedMany(success: true, message: ..., data: [ ... ]) ``` ### Update -To migrate or change collection id implement this method: +```dart +final response = await Shared.col('myCollection').update({ + 'doc1': {'k': 'v'}, + 'doc2': {'k': 'v2'}, +}); +// SharedMany(success: true, message: ..., data: [ ... ]) +``` + +- `force: true` creates missing collection if it does not exist. + +### Migrate ```dart -final response = await Shared.col('myCollection').update('myNewCollection'); -print(response); // SharedMany(success: true, message: '...', data: []) +final response = await Shared.col('myCollection').migrate('targetCollection'); +// SharedMany(success: true, message: ..., data: [ ... ]) ``` -### Delete +- `merge: true` merges existing target collection data. +- `force: true` allows migration from non-existing source. -To remove a collection, employ this method: +### Delete ```dart -final response = await Shared.col('myNewCollection').delete(); -print(response): // SharedNone(success: true, message: '....') +final response = await Shared.col('myCollection').delete(); +// SharedNone(success: true, message: ...) ``` ## Document | [View Code](https://github.com/Nialixus/local_shared/blob/main/example/lib/src/document_crud.dart) diff --git a/example/lib/main.dart b/example/lib/main.dart index 6769cc3..992e4e6 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -10,7 +10,7 @@ part 'src/many_document_crud.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - await LocalShared('MY_DB').initialize(); + await const LocalShared('MY_DB').initialize(); // ignore: avoid_print if (kDebugMode) LocalShared.stream.listen(print); diff --git a/lib/local_shared.dart b/lib/local_shared.dart index 6719001..ebcb608 100644 --- a/lib/local_shared.dart +++ b/lib/local_shared.dart @@ -1,4 +1,7 @@ -/// A SharedPreferences-based local storage, designed as an alternative to the localstore package +/// Lightweight local storage that uses Flutter Secure Storage and SharedPreferences. +/// +/// Provides a simplified API for managing collections and documents in a key/value store. +/// It is designed as an alternative to `localstore` with explicit, typed responses. library local_shared; import 'dart:async'; @@ -21,31 +24,21 @@ typedef JSON = Map; /// A shorter term for [LocalShared]. typedef Shared = LocalShared; -/// Parent of [SharedCollection], [SharedDocument] and [SharedManyDocument]. Need to be initiated before used! +/// LocalShared is the core datastore object. /// -/// ```dart -/// void main() { -/// WidgetsFlutterBinding.ensureInitialized(); -/// await LocalShared('DATABASE').initialize(); -/// } -/// ``` -/// -/// and later in app, you can access it anywhere by calling this +/// Call [initialize] once at app startup, then use [Shared.col] or [Shared.collection] +/// to access collection and document CRUD operations. /// /// ```dart -/// final response = await Shared.col(id).doc(id).read(); -/// print(response); // SharedOne(success: true, message: '..., data: JSON) */ +/// WidgetsFlutterBinding.ensureInitialized(); +/// await LocalShared('appId').initialize(); +/// final result = await Shared.col('users').doc('user1').read(); /// ``` class LocalShared { - /// Default constructor of [LocalShared] containing [id] to be used as prefix in [SharedPreferences]. - /// - /// Let [id] empty if you want to use default prefix of [SharedPreferences]. + /// Constructs a LocalShared instance. /// - /// ```dart - /// LocalShared('app.inidia.example'); - /// ``` - LocalShared(this.id); - + /// The [id] is used as a namespace prefix for all saved keys. + const LocalShared(this.id); /// The unique identifier for this [LocalShared] database instance /// which will be used as prefix for [SharedPreferences]. final String id; @@ -63,15 +56,13 @@ class LocalShared { static final StreamController _controller = StreamController.broadcast(); - /// Initializes the [LocalShared] instance. + /// Initializes internal storage instances. /// - /// Call this once inside main function after ensuring flutter initialized. + /// Must be called once before retrying any read/write operations. /// /// ```dart - /// Future main() async { - /// WidgetsFlutterBinding.ensureInitialized(); - /// await LocalShared('app.inidia.example').initialize(); - /// } + /// WidgetsFlutterBinding.ensureInitialized(); + /// await LocalShared('app.example').initialize(); /// ``` Future initialize() async { _storage = FlutterSecureStorage( @@ -82,9 +73,9 @@ class LocalShared { _db = await SharedPreferences.getInstance(); } - /// Loaded [FlutterSecureStorage] instance from awaiting [initialize]. + /// Returns the initialized secure storage instance. /// - /// This instance will be used in entire lifecycle of the app. + /// Throws [StateError] if [initialize] has not been called. static FlutterSecureStorage get storage { if (_storage != null) { return _storage!; diff --git a/lib/src/shared_collection.dart b/lib/src/shared_collection.dart index 78e5e22..411e918 100644 --- a/lib/src/shared_collection.dart +++ b/lib/src/shared_collection.dart @@ -1,33 +1,15 @@ part of '../local_shared.dart'; -/// Represents a collection of [SharedDocument] within the [LocalShared] storage. +/// A collection is a container of documents inside `LocalShared`. /// -/// Collections are used to organize and manage related pieces of data. -/// This class provides methods for creating, reading, updating, and deleting collections, -/// as well as shortcuts for interacting with documents and multiple documents. -/// --- -/// ```dart -/// // Create a new collection -/// final result = await Shared.col('myCollection').create(); -/// print(response); // SharedMany(success: true, message: '...', data: []) -/// ``` -/// --- -/// ```dart -/// // Read the contents of a collection -/// final response = await Shared.col('myCollection').read(); -/// print(response); // SharedMany(success: true, message: '...', data: []) -/// ``` -/// --- -/// ```dart -/// // Update a collection -/// final response = await Shared.col('myCollection').update(); -/// print(response); // SharedMany(success: true, message: '...', data: []) -/// ``` -/// --- +/// Use this class to manage whole collections and to access document-level operations: +/// creation, reading, updating, deleting, and migration. +/// +/// Recommended usage: /// ```dart -/// // Delete a collection -/// final response = await Shared.col('myCollection').delete(); -/// print(response): // SharedNone(success: true, message: '...') +/// final collection = Shared.col('myCollection'); +/// await collection.create(); +/// await collection.doc('item1').create({'value': 1}); /// ``` class SharedCollection { /// Creates a new instance of [SharedCollection]. diff --git a/lib/src/shared_document.dart b/lib/src/shared_document.dart index 5299008..fd23909 100644 --- a/lib/src/shared_document.dart +++ b/lib/src/shared_document.dart @@ -1,33 +1,14 @@ part of '../local_shared.dart'; -/// Represents a document within a [SharedCollection] in [LocalShared] storage. +/// A single document stored inside a collection. /// -/// Documents are individual pieces of data stored within a collection. -/// This class provides methods for creating, reading, updating, and deleting documents -/// within the context of a specific collection. -/// --- -/// ```dart -/// // Create a new document within a collection -/// final result = await Shared.col('myCollection').doc('documentId').create({'key': 'value'}); -/// print(response); // SharedOne(success: true, message: '...', data: JSON) -/// ``` -/// --- -/// ```dart -/// // Read the contents of a document within a collection -/// final response = await Shared.col('myCollection').doc('documentId').read(); -/// print(response); // SharedOne(success: true, message: '...', data: JSON) -/// ``` -/// --- -/// ```dart -/// // Update the contents of a document within a collection -/// final response = await Shared.col('myCollection').doc('documentId').update({'newKey': 'newValue'}); -/// print(response); // SharedOne(success: true, message: '...', data: JSON) -/// ``` -/// --- +/// Use this class for document-level CRUD in a collection: +/// create, read, update, delete, and migrate. +/// +/// Example: /// ```dart -/// // Delete a document within a collection -/// final response = await Shared.col('myCollection').doc('documentId').delete(); -/// print(response): // SharedNone(success: true, message: '...') +/// final doc = Shared.col('users').doc('userA'); +/// await doc.create({'name': 'Alice'}); /// ``` class SharedDocument { /// Creates a new instance of [SharedDocument]. @@ -219,16 +200,19 @@ class SharedDocument { /// final response = await Shared.col('myCollection').doc('doc1').migrate('doc2'); /// print(response); // SharedOne(success: true, message: '...', data: JSON) /// ``` - Future migrate(String id, - {bool merge = false, bool force = false,}) async { + Future migrate( + String id, { + bool merge = false, + bool force = false, + }) async { try { // [1] Get collection 📂. JSON? collection = await Shared._read(this.collection.id); // [2] Check collection existence 🔍. if (collection == null && !force) { - throw 'Unable to migrate the document. ' - 'The specified collection with ID `${this.collection.id}` does not exist.'; + throw 'Unable to migrate the document. ' + 'The specified collection with ID `${this.collection.id}` does not exist.'; } // [3] Source and destination cannot be identical. @@ -244,7 +228,7 @@ class SharedDocument { } // [5] Target existence check. - if (collection?[id] != null && !merge) { + if (collection?[id] != null && !merge && this.id != id) { throw 'Unable to migrate the document. ' 'The target document with ID `$id` already exists. ' 'To merge with existing target, set `merge` to true.'; @@ -267,7 +251,8 @@ class SharedDocument { this.collection._controller.add({ 'id': this.collection.id, 'documents': [ - for (var item in ((await Shared._read(this.collection.id)) ?? {}).entries) + for (var item + in ((await Shared._read(this.collection.id)) ?? {}).entries) {'id': item.key, 'data': item.value}, ], }); diff --git a/lib/src/shared_extension.dart b/lib/src/shared_extension.dart index 0e3739b..0ace5b9 100644 --- a/lib/src/shared_extension.dart +++ b/lib/src/shared_extension.dart @@ -1,6 +1,6 @@ part of '../local_shared.dart'; -/// Extension methods for enhancing [String] functionality. +/// Utility extensions used internally for JSON encoding/decoding and validation. extension _StringExtension on String { /// Decodes a JSON-formatted string into a [JSON] object. JSON get decode => jsonDecode(this); @@ -29,12 +29,10 @@ extension _ListExtension on List { } extension JSONExtension on JSON { - /// Merges the current [JSON] object with another [JSON] object. + /// Merge two JSON documents, giving precedence to existing target values. /// - /// The [other] parameter is the [JSON] object to merge into the current object. - /// The merge operation combines the key-value pairs from both objects. - /// If a key exists in both objects and the values are themselves [JSON] or [Map] objects, - /// the values are recursively merged. + /// - Nested maps are merged recursively. + /// - Scalar values in target replace values from source. JSON merge(JSON other) { // 1. Create a copy of the other (the target/base) final result = JSON.from(other); diff --git a/lib/src/shared_many_document.dart b/lib/src/shared_many_document.dart index 868a7d8..e0b700c 100644 --- a/lib/src/shared_many_document.dart +++ b/lib/src/shared_many_document.dart @@ -1,33 +1,9 @@ part of '../local_shared.dart'; -/// Representing a group of document within a [SharedCollection] in [LocalShared] storage. +/// Handles bulk operations for many documents in a collection. /// -/// This class provides methods for creating, reading, updating, and deleting multiple documents -/// within the context of a specific collection. -/// --- -/// ```dart -/// // Create multiple documents within a collection -/// final result = await Shared.col('myCollection').docs(['id1', 'id2', 'id3']).create((index) => {'key': 'value for $id'}); -/// print(result); // SharedMany(success: true, message: '...', data: []) -/// ``` -/// --- -/// ```dart -/// // Read the contents of multiple documents within a collection -/// final response = await Shared.col('myCollection').docs(['id1', 'id2', 'id3']).read(); -/// print(response); // SharedMany(success: true, message: '...', data: []) -/// ``` -/// --- -/// ```dart -/// // Update the contents of multiple documents within a collection -/// final response = await Shared.col('myCollection').docs(['id1', 'id2', 'id3']).update((index) => {'newKey': 'newValue for $id'}); -/// print(response); // SharedMany(success: true, message: '...', data: []) -/// ``` -/// --- -/// ```dart -/// // Delete multiple documents within a collection -/// final response = await Shared.col('myCollection').docs(['id1', 'id2', 'id3']).delete(); -/// print(response); // SharedNone(success: true, message: '...') -/// ``` +/// Use `SharedCollection.docs(ids)` to update or delete multiple documents, or +/// to merge them into a single target document via [migrate]. class SharedManyDocument { /// Creates a new instance of [SharedManyDocument]. /// diff --git a/lib/src/shared_response.dart b/lib/src/shared_response.dart index 55bfb29..d0068ff 100644 --- a/lib/src/shared_response.dart +++ b/lib/src/shared_response.dart @@ -1,16 +1,9 @@ part of '../local_shared.dart'; -/// Represents the response of an operation in [LocalShared]. +/// Base class for structured results returned by LocalShared operations. /// -/// A [SharedResponse] contains information about the success or failure of an operation, -/// along with an optional data payload. This is an abstract class, and concrete implementations -/// include [SharedOne], [SharedMany], and [SharedNone]. -/// -/// Usage example: -/// ```dart -/// final response = SharedOne(success: true, message: 'Operation successful', data: {'key': 'value'}); -/// print(response); // SharedOne(success: true, message: 'Operation successful', data: {'key': 'value'}) -/// ``` +/// Every result communicates whether the operation succeeded and carries +/// an informative message plus optional payload data. abstract class SharedResponse { /// Creates a new instance of [SharedResponse]. /// diff --git a/pubspec.yaml b/pubspec.yaml index 0dbe03e..a0ea666 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: local_shared -description: A secure wrapper for JSON file-based storage, providing an alternative to Localstore. Safely execute CRUD operations across all platforms with built in support for sensitive data handling. +description: Lightweight cross-platform secure local storage for Flutter. Native-safe JSON collections and documents with tested CRUD, batch operations, migration semantics, and stream updates. version: 3.0.0 homepage: https://inidia.app repository: https://github.com/Nialixus/local_shared.git @@ -10,11 +10,14 @@ screenshots: - description: 'Local Shared Logo' path: logo.png topics: - - utility - - storage + - flutter + - secure-storage + - shared-preferences + - local-storage + - data-sync - database - - cache - - secure + - caching + - crud platforms: android: ios: diff --git a/test/local_shared_test.dart b/test/local_shared_test.dart index 1170d48..80c9973 100644 --- a/test/local_shared_test.dart +++ b/test/local_shared_test.dart @@ -8,21 +8,74 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('Local Shared Test', () { - setUp(() { - // 2. Provide mock values for SharedPreferences before every test - // This prevents the "Binding not initialized" and plugin errors + // provide mock values for SharedPreferences so initialization works SharedPreferences.setMockInitialValues({}); FlutterSecureStorage.setMockInitialValues({}); }); + test('storage and preferences getters require initialize', () { + expect(() => LocalShared.storage, throwsStateError); + expect(() => LocalShared.preferences, throwsStateError); + }); + test('Initialization', () async { - final db = LocalShared('test_db'); + const db = LocalShared('test_db'); + await db.initialize(); + expect(db, isNotNull); + expect(Shared.col('collection'), isA()); + }); - // Now this call will succeed because the binding and mocks are ready + test('col and collection return equivalent objects', () { + final col = Shared.col('my_collection'); + final col2 = Shared.collection('my_collection'); + expect(col2, isA()); + expect(col.id, equals(col2.id)); + expect(col.toString(), contains('my_collection')); + }); + + test('create/read/delete collection operations', () async { + const db = LocalShared('test_db_2'); await db.initialize(); - expect(db, isNotNull); + final collection = Shared.col('test_collection'); + await collection.delete(); + + final createResp = await collection.create(); + expect(createResp.success, isTrue); + expect(createResp, isA()); + expect(createResp.data, isEmpty); + + final readResp = await collection.read(); + expect(readResp.success, isTrue); + expect(readResp.data, isA>()); + expect((readResp.data as List).isEmpty, isTrue); + + final deleteResp = await collection.delete(); + expect(deleteResp.success, isTrue); + expect(deleteResp, isA()); + }); + + test('stream emits events on collection mutations', () async { + final completed = []; + final sub = LocalShared.stream.listen((event) { + completed.add(event); + }); + + const db = LocalShared('test_db_stream'); + await db.initialize(); + + final coll = Shared.col('stream_collection'); + await coll.delete(); + await coll.create(); + + // allow asynchronous stream event dispatch + await Future.delayed(const Duration(milliseconds: 100)); + + expect(completed, isNotEmpty); + + await sub.cancel(); + await LocalShared.close(); }); }); } \ No newline at end of file diff --git a/test/src/shared_collection_test.dart b/test/src/shared_collection_test.dart index 551d213..069eeae 100644 --- a/test/src/shared_collection_test.dart +++ b/test/src/shared_collection_test.dart @@ -22,7 +22,7 @@ void main() { // 2. Initialize LocalShared FlutterSecureStorage.setMockInitialValues({}); SharedPreferences.setMockInitialValues({}); - await LocalShared('test_db').initialize(); + await const LocalShared('test_db').initialize(); // // 3. Setup the collection instance collection = Shared.collection(collectionId); @@ -73,6 +73,42 @@ void main() { expect(response.message, contains('has been successfully recreated')); }); + test('Read missing collection returns SharedNone', () async { + await collection.delete(); + final response = await collection.read(); + expect(response, isA()); + expect(response.success, isFalse); + expect(response.message, contains('does not exist')); + }); + + test('IDs returns all saved document keys', () async { + await collection.create(); + await document.create(data); + await collection.document('document_2').create(data2); + + final ids = await collection.ids(); + expect(ids, containsAll([documentId, 'document_2'])); + }); + + test('Update missing collection without force fails', () async { + await collection.delete(); + final response = await collection.update({ + 'document_1': {'key': 'updated'}, + }); + + expect(response, isA()); + expect(response.success, isFalse); + expect(response.message, contains('does not exist')); + }); + + test('Delete missing collection returns SharedNone', () async { + await collection.delete(); + final response = await collection.delete(); + expect(response, isA()); + expect(response.success, isFalse); + expect(response.message, contains('does not exist')); + }); + test('Read Collection', () async { // Arrange: Mock up collection await collection.create(); diff --git a/test/src/shared_document_test.dart b/test/src/shared_document_test.dart index 3368f76..ce06a6f 100644 --- a/test/src/shared_document_test.dart +++ b/test/src/shared_document_test.dart @@ -20,7 +20,7 @@ void main() { // 2. Initialize LocalShared FlutterSecureStorage.setMockInitialValues({}); SharedPreferences.setMockInitialValues({}); - await LocalShared('test_db').initialize(); + await const LocalShared('test_db').initialize(); // 3. Setup the collection and document instance collection = Shared.collection(collectionId); @@ -73,7 +73,7 @@ void main() { // Act: Create a new document with the same ID and merge: true final response = await document.create(data2, merge: true); - + // Assert: Should return these values expect(response.success, isTrue); expect(response.data, data.merge(data2)); @@ -228,6 +228,29 @@ void main() { expect(response.message, contains('cannot be the same')); }); + test('Migrate Missing Source With Force Creates Target', () async { + await collection.create(replace: true); + + final response = await document.migrate(documentId2, force: true); + expect(response.success, isTrue); + expect(response, isA()); + expect(response.data, isNotNull); + + final targetRead = await collection.doc(documentId2).read(); + expect(targetRead.success, isTrue); + expect(targetRead.data, isEmpty); + }); + + test('Migrate to Same ID with Force does nothing but succeeds', () async { + await document.create(data); + + final response = await document.migrate(documentId, force: true); + + expect(response.success, isTrue); + expect(response, isA()); + expect(response.data, data); + }); + test('Delete Document', () async { // Arrange: Mock up document await document.create(data); diff --git a/test/src/shared_extension_test.dart b/test/src/shared_extension_test.dart new file mode 100644 index 0000000..979c18c --- /dev/null +++ b/test/src/shared_extension_test.dart @@ -0,0 +1,51 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:local_shared/local_shared.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('SharedExtension Test', () { + test('JSON merge should preserve existing target values and merge nested maps', () { + final source = { + 'name': 'source', + 'meta': {'a': 1, 'b': 2}, + 'field': 's', + }; + final target = { + 'name': 'target', + 'meta': {'b': 20, 'c': 3}, + 'other': true, + }; + + final merged = source.merge(target); + + expect(merged['name'], 'target'); + expect(merged['field'], 's'); + expect(merged['other'], true); + expect(merged['meta'], isA()); + expect((merged['meta'] as Map)['a'], 1); + expect((merged['meta'] as Map)['b'], 20); + expect((merged['meta'] as Map)['c'], 3); + }); + + test('SharedResponse extension one/many with SharedOne and SharedMany', () { + const one = SharedOne(success: true, message: 'ok', data: {'k': 1}); + const many = SharedMany(success: true, message: 'ok', data: [{'k': 1}]); + + expect(one.one, {'k': 1}); + expect(one.many, isNull); + expect(many.one, isNull); + expect(many.many, isA>>()); + }); + + test('FutureSharedResponse extension handles SharedFuture correctly', () async { + final futureOne = Future.value(const SharedOne(success: true, message: 'ok', data: {'foo': 'bar'})); + final futureMany = Future.value(const SharedMany(success: true, message: 'ok', data: [{'foo': 'bar'}])); + + expect(await futureOne.one(), {'foo': 'bar'}); + expect(await futureOne.many(), isNull); + expect(await futureMany.one(), isNull); + expect(await futureMany.many(), isA>>()); + }); + }); +} diff --git a/test/src/shared_many_document_test.dart b/test/src/shared_many_document_test.dart index ff191ef..56907da 100644 --- a/test/src/shared_many_document_test.dart +++ b/test/src/shared_many_document_test.dart @@ -15,7 +15,7 @@ void main() { setUp(() async { FlutterSecureStorage.setMockInitialValues({}); SharedPreferences.setMockInitialValues({}); - await LocalShared('test_db').initialize(); + await const LocalShared('test_db').initialize(); collection = Shared.collection(collectionId); await collection.delete(); }); @@ -41,6 +41,15 @@ void main() { expect(response.success, isFalse); }); + test('Create new docs without collection and force false fails', () async { + await collection.delete(); + final response = await collection.docs(['docA', 'docB']).create((index) => index == 0 ? docA : docB, force: false); + + expect(response, isA()); + expect(response.success, isFalse); + expect(response.message, contains('does not exist')); + }); + test('Create existing docs with merge should succeed', () async { await collection.docs(['docA']).create((_) => docA); final response = await collection.docs(['docA']).create((_) => {'c': 3}, merge: true); @@ -53,6 +62,26 @@ void main() { expect(stored.first, containsPair('a', 1)); }); + test('Update missing documents without force fails', () async { + await collection.docs(['docA']).create((_) => docA); + + final response = await collection.docs(['docA', 'docB']).update((index) => index == 0 ? {'a': 10} : {'b': 20}, force: false); + + expect(response, isA()); + expect(response.success, isFalse); + expect(response.message, contains('does not exist')); + }); + + test('Delete missing documents without skip fails', () async { + await collection.docs(['docA']).create((_) => docA); + + final response = await collection.docs(['docA', 'docB']).delete(skip: false); + + expect(response, isA()); + expect(response.success, isFalse); + expect(response.message, contains('does not exist')); + }); + test('Read missing doc with skip false throws', () async { final response = await collection.docs(['docA']).read(skip: false); expect(response, isA()); diff --git a/test/src/shared_response_test.dart b/test/src/shared_response_test.dart new file mode 100644 index 0000000..5bf0cf0 --- /dev/null +++ b/test/src/shared_response_test.dart @@ -0,0 +1,60 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:local_shared/local_shared.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('SharedResponse Test', () { + test('SharedOne behavior and toString', () { + const response = SharedOne(success: true, message: 'OK', data: {'key': 'value'}); + expect(response.success, isTrue); + expect(response.message, 'OK'); + expect(response.data, {'key': 'value'}); + expect(response.one, {'key': 'value'}); + expect(response.many, isNull); + final toString = response.toString(); + expect(toString, contains('SharedOne')); + expect(toString, contains('success: true')); + }); + + test('SharedMany behavior and extension', () { + const response = SharedMany(success: true, message: 'Many', data: [{'a': 1}, {'b': 2}]); + expect(response.success, isTrue); + expect(response.message, 'Many'); + expect(response.data, hasLength(2)); + expect(response.many, isA>>()); + expect(response.one, isNull); + final toString = response.toString(); + expect(toString, contains('SharedMany')); + expect(toString, contains('success: true')); + + expect(response.many, contains(equals({'a': 1}))); + }); + + test('SharedNone behavior', () { + const response = SharedNone(success: false, message: 'None'); + expect(response.success, isFalse); + expect(response.message, 'None'); + expect(response.data, isNull); + expect(response.one, isNull); + expect(response.many, isNull); + expect(response.toString(), contains('SharedNone')); + }); + + test('FutureSharedResponse extension one/many methods', () async { + final one = Future.value(const SharedOne(success: true, message: 'OK', data: {'a': 123})).one(); + final many = Future.value(const SharedMany(success: true, message: 'Many', data: [{'name': 'x'}])).many(); + + expect(await one, {'a': 123}); + expect(await many, isA>>()); + expect(await many, hasLength(1)); + }); + + test('SharedResponse base toString includes data only when present', () { + const noData = SharedNone(success: true, message: 'No data'); + expect(noData.toString(), contains('success: true')); + expect(noData.toString(), contains('message: No data')); + expect(noData.toString(), isNot(contains('data:'))); + }); + }); +}