From 6640b6e3b165147bef1d4a66dd5733a88e01624f Mon Sep 17 00:00:00 2001 From: Nialixus Date: Fri, 20 Mar 2026 10:18:22 +0700 Subject: [PATCH 1/2] [3.0.0] Add some coverage tests --- devtools_options.yaml | 3 ++ lib/local_shared.dart | 6 ++-- lib/src/shared_collection.dart | 13 ++++--- lib/src/shared_document.dart | 34 +++++++++--------- lib/src/shared_many_document.dart | 46 ++++++++++++------------- test/src/shared_collection_test.dart | 25 ++++++++++---- test/src/shared_document_test.dart | 39 +++++++++++++++++++++ test/src/shared_extension_test.dart | 46 ++++++++++++++++++++++--- test/src/shared_many_document_test.dart | 30 ++++++++++++++++ 9 files changed, 181 insertions(+), 61 deletions(-) create mode 100644 devtools_options.yaml diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/local_shared.dart b/lib/local_shared.dart index ebcb608..88ab2ab 100644 --- a/lib/local_shared.dart +++ b/lib/local_shared.dart @@ -6,7 +6,6 @@ library local_shared; import 'dart:async'; import 'dart:convert'; -import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -39,6 +38,7 @@ class LocalShared { /// /// 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; @@ -130,7 +130,7 @@ class LocalShared { /// Shared.col(collectionID)... /// ``` static SharedCollection col(String id) { - return SharedCollection(id, controller: _controller); + return LocalShared.collection(id); } /// Another shortcut that not so short compare to [col] @@ -144,7 +144,7 @@ class LocalShared { /// Shared.collection(collectionID)... /// ``` static SharedCollection collection(String id) { - return LocalShared.col(id); + return SharedCollection(id, controller: _controller); } /// A stream that listens for any changes when you're interacting with collections or documents diff --git a/lib/src/shared_collection.dart b/lib/src/shared_collection.dart index 411e918..398910d 100644 --- a/lib/src/shared_collection.dart +++ b/lib/src/shared_collection.dart @@ -37,7 +37,6 @@ class SharedCollection { return result; } catch (e) { - debugPrint("Failed to get ids, reason: $e"); return result; } } @@ -145,7 +144,8 @@ class SharedCollection { } // [3] Updating the collection πŸŽ‰. - final bool result = await Shared._create(id, (collection ?? {}).merge(document)); + final bool result = + await Shared._create(id, (collection ?? {}).merge(document)); // [4] Notify the stream about the change in the collection πŸ“£. _controller.add({ @@ -242,8 +242,8 @@ class SharedCollection { success: delete, message: delete ? 'Successfully migrated the collection from ID `${this.id}` to ID `$id`.' - : 'Failed to clear the old collection after migrating to the new ID. ' - 'Please try deleting the collection with ID `${this.id}` manually.', + : '''Failed to clear the old collection after migrating to the new ID. + Please try deleting the collection with ID `${this.id}` manually.''', data: [ for (var item in ((await Shared._read(delete ? id : this.id)) ?? {}).entries) @@ -260,7 +260,6 @@ class SharedCollection { } } - /// Deletes the collection. /// /// Returns a [SharedResponse] of [SharedNone] indicating the success or failure of the deletion. @@ -329,7 +328,7 @@ class SharedCollection { /// await Shared.collection(id).docs(id)... /// ``` SharedManyDocument docs(Iterable ids) { - return SharedManyDocument(ids, collection: this); + return documents(ids); } /// Another shortcut to interact with [SharedManyDocument] that not so short compared to [docs]. @@ -339,7 +338,7 @@ class SharedCollection { /// await Shared.collection(id).documents(id)... /// ``` SharedManyDocument documents(Iterable ids) { - return docs(ids); + return SharedManyDocument(ids, collection: this); } @override diff --git a/lib/src/shared_document.dart b/lib/src/shared_document.dart index fd23909..cb0f983 100644 --- a/lib/src/shared_document.dart +++ b/lib/src/shared_document.dart @@ -144,25 +144,25 @@ class SharedDocument { // [2] Check if collection exists or not πŸ‘». if (collection == null && !force) { - throw 'Unable to update the document. ' - 'The specified collection with ID `${this.collection.id}` does not exist. ' - 'To forcibly continue, ' - 'set the `force` parameter to true. ' - 'This action will create a new collection and a new document.'; + throw '''Unable to update the document. + The specified collection with ID `${this.collection.id}` does not exist. + To forcibly continue, + set the `force` parameter to true. + This action will create a new collection and a new document.'''; } // [3] Check if document exist or not πŸ•Š. if (collection?[id] == null && !force) { - throw 'Unable to update the document. ' - 'The specified document with ID `$id` does not exist. ' - 'To forcibly continue, ' - 'set the `force` parameter to true. ' - 'This action will create a new document.'; + throw '''Unable to update the document. + The specified document with ID `$id` does not exist. + To forcibly continue, + set the `force` parameter to true. + This action will create a new document.'''; } // [4] Updating the document πŸ’Ό. bool result = await Shared._create(this.collection.id, { - ...collection ?? {}, + ...(collection ?? {}), id: (collection?[id] as JSON? ?? {}).merge(document), }); @@ -211,8 +211,8 @@ class SharedDocument { // [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. @@ -223,8 +223,8 @@ class SharedDocument { // [4] Source document existence. if (collection?[this.id] == null && !force) { - throw 'Unable to migrate the document. ' - 'The source document with ID `${this.id}` does not exist.'; + throw '''Unable to migrate the document. + The source document with ID `${this.id}` does not exist.'''; } // [5] Target existence check. @@ -288,8 +288,8 @@ class SharedDocument { // [2] Check if collection exists or not πŸ‘». if (collection == null) { - throw 'Unable to delete the document. ' - 'The specified collection with ID `${this.collection.id}` does not exist.'; + throw '''Unable to delete the document. + The specified collection with ID `${this.collection.id}` does not exist.'''; } // [3] Check if document exists or not πŸ•Š. diff --git a/lib/src/shared_many_document.dart b/lib/src/shared_many_document.dart index e0b700c..c4055b3 100644 --- a/lib/src/shared_many_document.dart +++ b/lib/src/shared_many_document.dart @@ -62,10 +62,13 @@ class SharedManyDocument { this.collection.id, ({ ...collection, - for (int i = 0; i < ids.length; i++)...(){ - final id = ids.elementAt(i); - return {id:(collection?[id] as JSON? ?? {}).merge( document(i))}; - }(), + for (int i = 0; i < ids.length; i++) + ...() { + final id = ids.elementAt(i); + return { + id: (collection?[id] as JSON? ?? {}).merge(document(i)) + }; + }(), }), ); @@ -120,9 +123,9 @@ class SharedManyDocument { if (!skip) { for (String id in ids) { if (collection[id] == null) { - throw 'Unable to read document. ' - 'The specified document with ID `$id` does not exist. ' - 'To skip checking document existence, set parameter `skip` to true.'; + throw '''Unable to read document. + The specified document with ID `$id` does not exist. + To skip checking document existence, set parameter `skip` to true.'''; } } } @@ -166,19 +169,16 @@ class SharedManyDocument { // [2] Check if collection exists or not πŸ‘». if (collection == null && !force) { - throw 'Unable to update documents. ' - 'The specified collection with ID `${this.collection.id}` does not exist. ' - 'To forcibly continue, ' - 'set the `force` parameter to true. ' - 'This action will create a new collection and create new documents within it.'; + throw '''Unable to update documents. + The specified collection with ID `${this.collection.id}` does not exist. + To forcibly continue, + set the `force` parameter to true. + This action will create a new collection and create new documents within it.'''; } - // [3] Make the collection null safety β›‘. - collection = collection ?? {}; - // [4] Check if document exist or not πŸ•Š. for (String id in ids) { - if (collection[id] == null && !force) { + if (collection?[id] == null && !force) { throw 'Unable to update the document. ' 'The specified document with ID `$id` does not exist. ' 'To forcibly continue, ' @@ -189,9 +189,10 @@ class SharedManyDocument { // [5] Updating the document πŸ’Ό. bool result = await Shared._create(this.collection.id, { - ...collection, + ...(collection ?? {}), for (int i = 0; i < ids.length; i++) - ids.elementAt(i): (collection[ids.elementAt(i)] as JSON? ?? {}).merge( + ids.elementAt(i): + (collection?[ids.elementAt(i)] as JSON? ?? {}).merge( document(i), ), }); @@ -238,8 +239,8 @@ class SharedManyDocument { // [2] Check if collection exists or not πŸ‘». if (collection == null) { - throw 'Unable to delete documents. ' - 'The specified collection with ID `${this.collection.id}` does not exist.'; + throw '''Unable to delete documents. + The specified collection with ID `${this.collection.id}` does not exist.'''; } // [3] Watch initial length of collection @@ -293,7 +294,6 @@ class SharedManyDocument { } } - Future migrate( String id, { bool merge = false, @@ -305,8 +305,8 @@ class SharedManyDocument { // [2] Source collection existence. if (source.isEmpty && !force) { - throw 'Unable to migrate documents. ' - 'The collection with ID `${collection.id}` does not exist.'; + throw '''Unable to migrate documents. + The collection with ID `${collection.id}` does not exist.'''; } // [3] Source and destination document cannot be identical unless forced. diff --git a/test/src/shared_collection_test.dart b/test/src/shared_collection_test.dart index 069eeae..814bc20 100644 --- a/test/src/shared_collection_test.dart +++ b/test/src/shared_collection_test.dart @@ -141,7 +141,8 @@ void main() { // Assert: Should return these values expect(response.success, isTrue); expect(response.data, isA()); - expect(response.data, contains(equals({'key': 'updated', 'newField': 'yes', '1': '1'}))); + expect(response.data, + contains(equals({'key': 'updated', 'newField': 'yes', '1': '1'}))); expect(response, isA()); expect(response.message, contains('has been successfully updated')); }); @@ -303,14 +304,16 @@ void main() { expect(response, isA()); }); - test('Migrate Documents from Collection Using SharedManyDocument', () async { + test('Migrate Documents from Collection Using SharedManyDocument', + () async { // Arrange: Setup source collection and documents await collection.create(); await document.create(data); await collection.document('document_2').create({'x': 'y'}); // Act: Migrate specific documents from source into a single target document - final response = await collection.docs([documentId, 'document_2']).migrate('merged_document'); + final response = await collection + .docs([documentId, 'document_2']).migrate('merged_document'); final targetRead = await collection.doc('merged_document').read(); final sourceRead = await collection.read(); @@ -322,7 +325,13 @@ void main() { expect(targetRead.one, containsPair('x', 'y')); expect(sourceRead.success, isTrue); expect(sourceRead.data, isA>()); - expect(sourceRead.data, contains(equals({'x': 'y', 'key': 'value1', '1': '1'}))); // merged doc should be new target not in source list + expect( + sourceRead.data, + contains(equals({ + 'x': 'y', + 'key': 'value1', + '1': '1' + }))); // merged doc should be new target not in source list }); test('Migrate Documents to Target With Merge', () async { @@ -332,14 +341,18 @@ void main() { await collection.doc('merged_document').create({'1': '2'}); // Act: Migrate with merge true into the existing target document - final response = await collection.docs([documentId]).migrate('merged_document', merge: true); + final response = await collection + .docs([documentId]).migrate('merged_document', merge: true); final targetRead = await collection.doc('merged_document').read(); // Assert: Should merge existing target document expect(response.success, isTrue); expect(targetRead.success, isTrue); expect(targetRead.one, containsPair('key', 'value1')); - expect(targetRead.one, containsPair('1', '1')); // existing value preserved by target precedence + expect( + targetRead.one, + containsPair( + '1', '1')); // existing value preserved by target precedence }); test('Delete Collection', () async { diff --git a/test/src/shared_document_test.dart b/test/src/shared_document_test.dart index ce06a6f..bcc60ab 100644 --- a/test/src/shared_document_test.dart +++ b/test/src/shared_document_test.dart @@ -228,6 +228,26 @@ void main() { expect(response.message, contains('cannot be the same')); }); + test('Migrate on Missing Collection without Force fails', () async { + await collection.delete(); + + final response = await document.migrate(documentId2, force: false); + + expect(response.success, isFalse); + expect(response, isA()); + expect(response.message, contains('does not exist')); + }); + + test('Migrate Missing Source without Force fails', () async { + await collection.create(replace: true); + + final response = await document.migrate(documentId2, force: false); + + expect(response.success, isFalse); + expect(response, isA()); + expect(response.message, contains('source document with ID')); + }); + test('Migrate Missing Source With Force Creates Target', () async { await collection.create(replace: true); @@ -269,6 +289,25 @@ void main() { expect(readResponse.success, isFalse); }); + test('Delete Document in Non-Existing Collection', () async { + // Arrange: Remove the collection entirely + await collection.delete(); + + // Act: Delete a document when collection is missing + final response = await document.delete(); + + // Assert: Should fail + expect(response.success, isFalse); + expect(response, isA()); + expect(response.message, contains('does not exist')); + }); + + test('SharedDocument.toString includes id and collection', () async { + final str = document.toString(); + expect(str, contains('id: $documentId')); + expect(str, contains('collection:')); // at least mentions collection + }); + test('Delete Non-Existing Document', () async { // Arrange: Clear the collection await collection.create(replace: true); diff --git a/test/src/shared_extension_test.dart b/test/src/shared_extension_test.dart index 979c18c..222a2ea 100644 --- a/test/src/shared_extension_test.dart +++ b/test/src/shared_extension_test.dart @@ -5,7 +5,9 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('SharedExtension Test', () { - test('JSON merge should preserve existing target values and merge nested maps', () { + test( + 'JSON merge should preserve existing target values and merge nested maps', + () { final source = { 'name': 'source', 'meta': {'a': 1, 'b': 2}, @@ -30,7 +32,9 @@ void main() { 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}]); + const many = SharedMany(success: true, message: 'ok', data: [ + {'k': 1} + ]); expect(one.one, {'k': 1}); expect(one.many, isNull); @@ -38,14 +42,46 @@ void main() { 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'}])); + 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>>()); }); + + // test('JSON validate should throw on invalid nested type in list', () { + // final list = ['a', 1, {'b': true}, [2.3]]; + // expect(() => list.validate([String, int, double, bool, List, JSON, Null], key: 'root'), returnsNormally); + + // final invalidList = ['a', 1, DateTime.now()]; + // expect( + // () => invalidList.validate([String, int, double, bool, List, JSON, Null], key: 'root'), + // throwsA(isA().having((e) => e.message, 'message', contains('Invalid type for key "root"'))), + // ); + // }); + + // test('JSON map validate should throw on invalid nested type in map', () { + // final json = { + // 'valid': {'a': 'b'}, + // 'validArray': [1, 2, 3], + // }; + // expect(() => json.validate([String, int, double, bool, List, JSON, Null], key: 'root'), returnsNormally); + + // final invalidJson = { + // 'invalid': {'x': DateTime(2022)}, + // }; + // expect( + // () => invalidJson.validate([String, int, double, bool, List, JSON, Null], key: 'root'), + // throwsA(isA().having((e) => e.message, 'message', contains('Invalid type for key "root.invalid"'))), + // ); + // }); }); } diff --git a/test/src/shared_many_document_test.dart b/test/src/shared_many_document_test.dart index 56907da..1f638ad 100644 --- a/test/src/shared_many_document_test.dart +++ b/test/src/shared_many_document_test.dart @@ -82,6 +82,25 @@ void main() { expect(response.message, contains('does not exist')); }); + test('Delete documents when collection is missing fails', () async { + await collection.delete(); + + final response = await collection.docs(['docA', 'docB']).delete(); + + expect(response, isA()); + expect(response.success, isFalse); + expect(response.message, contains('does not exist')); + }); + + test('Read missing docs with default skip returns no results message', () async { + final response = await collection.docs(['docA']).read(); + + expect(response, isA()); + expect(response.success, isFalse); + expect(response.message, contains("There's no single document")); + expect(response.data, isEmpty); + }); + test('Read missing doc with skip false throws', () async { final response = await collection.docs(['docA']).read(skip: false); expect(response, isA()); @@ -111,6 +130,17 @@ void main() { expect(rest, hasLength(1)); }); + test('Delete docs not found returns no single document message', () async { + await collection.docs(['docA', 'docB']).create((index) { + return index == 0 ? docA : docB; + }); + + final response = await collection.docs(['docC']).delete(); + expect(response, isA()); + expect(response.success, isFalse); + expect(response.message, contains("There's no single document with ID `docC` found")); + }); + test('Migrate many documents into one target document', () async { await collection.docs(['docA', 'docB']).create((index) { return index == 0 ? docA : docB; From 0fa698d73d37d90e4336367f2fbaf0c043805eec Mon Sep 17 00:00:00 2001 From: Nialixus Date: Fri, 20 Mar 2026 10:41:22 +0700 Subject: [PATCH 2/2] [3.0.0] Full Test --- lib/src/shared_many_document.dart | 4 +- test/src/shared_many_document_test.dart | 142 +++++++++++++++++++++--- 2 files changed, 131 insertions(+), 15 deletions(-) diff --git a/lib/src/shared_many_document.dart b/lib/src/shared_many_document.dart index c4055b3..6ea54d0 100644 --- a/lib/src/shared_many_document.dart +++ b/lib/src/shared_many_document.dart @@ -339,8 +339,8 @@ class SharedManyDocument { } if (doc is! JSON) { - throw 'Unable to migrate documents. ' - 'Source document with ID `$docId` has invalid type.'; + throw '''Unable to migrate documents. + Source document with ID `$docId` has invalid type.'''; } migratedData = hasData ? migratedData.merge(doc) : JSON.from(doc); diff --git a/test/src/shared_many_document_test.dart b/test/src/shared_many_document_test.dart index 1f638ad..bb309c2 100644 --- a/test/src/shared_many_document_test.dart +++ b/test/src/shared_many_document_test.dart @@ -1,4 +1,5 @@ -ο»Ώimport 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'dart:convert'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:local_shared/local_shared.dart'; @@ -36,14 +37,17 @@ void main() { return index == 0 ? docA : docB; }); - final response = await collection.docs(['docA']).create((_) => {'a': 9}, merge: false); + final response = + await collection.docs(['docA']).create((_) => {'a': 9}, merge: false); expect(response, isA()); 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); + final response = await collection.docs(['docA', 'docB']).create( + (index) => index == 0 ? docA : docB, + force: false); expect(response, isA()); expect(response.success, isFalse); @@ -52,7 +56,8 @@ void main() { 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); + final response = + await collection.docs(['docA']).create((_) => {'c': 3}, merge: true); expect(response, isA()); expect(response.success, isTrue); @@ -65,7 +70,9 @@ void main() { 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); + final response = await collection.docs(['docA', 'docB']).update( + (index) => index == 0 ? {'a': 10} : {'b': 20}, + force: false); expect(response, isA()); expect(response.success, isFalse); @@ -75,7 +82,8 @@ void main() { test('Delete missing documents without skip fails', () async { await collection.docs(['docA']).create((_) => docA); - final response = await collection.docs(['docA', 'docB']).delete(skip: false); + final response = + await collection.docs(['docA', 'docB']).delete(skip: false); expect(response, isA()); expect(response.success, isFalse); @@ -92,7 +100,11 @@ void main() { expect(response.message, contains('does not exist')); }); - test('Read missing docs with default skip returns no results message', () async { + test('Read missing docs with default skip returns no results message', + () async { + // Ensure the collection exists first by creating a different document + await collection.doc('dummy').create({'foo': 'bar'}); + final response = await collection.docs(['docA']).read(); expect(response, isA()); @@ -115,7 +127,8 @@ void main() { expect(response, isA()); expect(response.success, isTrue); - expect((await collection.docs(['docA', 'docB']).read()).many, hasLength(2)); + expect( + (await collection.docs(['docA', 'docB']).read()).many, hasLength(2)); }); test('Delete existing docs', () async { @@ -138,7 +151,8 @@ void main() { final response = await collection.docs(['docC']).delete(); expect(response, isA()); expect(response.success, isFalse); - expect(response.message, contains("There's no single document with ID `docC` found")); + expect(response.message, + contains("There's no single document with ID `docC` found")); }); test('Migrate many documents into one target document', () async { @@ -146,7 +160,8 @@ void main() { return index == 0 ? docA : docB; }); - final response = await collection.docs(['docA', 'docB']).migrate('target'); + final response = + await collection.docs(['docA', 'docB']).migrate('target'); expect(response, isA()); expect(response.success, isTrue); @@ -166,7 +181,8 @@ void main() { }); await collection.doc('target').create({'existing': true}); - final response = await collection.docs(['docA']).migrate('target', merge: false); + final response = + await collection.docs(['docA']).migrate('target', merge: false); expect(response, isA()); expect(response.success, isFalse); }); @@ -177,10 +193,110 @@ void main() { }); await collection.doc('target').create({'existing': true}); - final response = await collection.docs(['docA']).migrate('target', merge: true); + final response = + await collection.docs(['docA']).migrate('target', merge: true); + expect(response, isA()); + expect(response.success, isTrue); + expect((await collection.doc('target').read()).one, + containsPair('existing', true)); + }); + + test('Migrate fails when source collection is empty and force is false', + () async { + await collection.delete(); + final response = + await collection.docs(['docA']).migrate('target', force: false); + expect(response, isA()); + expect(response.success, isFalse); + expect(response.message, contains('does not exist')); + }); + + test('Migrate fails when target ID is in source IDs and force is false', + () async { + await collection.docs(['docA']).create((_) => docA); + final response = await collection + .docs(['docA', 'target']).migrate('target', force: false); + expect(response, isA()); + expect(response.success, isFalse); + expect(response.message, + contains('Source and destination document IDs cannot be the same')); + }); + + test( + 'Migrate fails if target document exists and neither merge nor force is true', + () async { + await collection.docs(['docA']).create((_) => docA); + await collection.doc('target').create({'existing': true}); + final response = await collection + .docs(['docA']).migrate('target', merge: false, force: false); + expect(response, isA()); + expect(response.success, isFalse); + expect(response.message, contains('already exists')); + }); + + test( + 'Migrate fails if a requested source document is missing and force is false', + () async { + await collection.docs(['docA']).create((_) => docA); + final response = await collection + .docs(['docA', 'docB']).migrate('target', force: false); + expect(response, isA()); + expect(response.success, isFalse); + expect(response.message, + contains('Source document with ID `docB` does not exist')); + }); + + test( + 'Migrate fails if no source documents are found and target doesn\'t exist, unless force is true', + () async { + final response = + await collection.docs(['docB']).migrate('target', force: true); expect(response, isA()); expect(response.success, isTrue); - expect((await collection.doc('target').read()).one, containsPair('existing', true)); + + final response2 = + await collection.docs(['docB']).migrate('target2', force: false); + expect(response2, isA()); + expect(response2.success, isFalse); + expect(response2.message, contains('does not exist')); + }); + + test('Migrate fails if a source document has invalid type', () async { + await collection.docs(['docA']).create((_) => docA); + // Inject invalid data directly into storage + await LocalShared.storage.write( + key: collectionId, + value: jsonEncode({ + 'docA': ['not', 'a', 'map'], + 'other': {} + }), + ); + + final response = await collection.docs(['docA']).migrate('target'); + expect(response, isA()); + expect(response.success, isFalse); + expect(response.message, contains('has invalid type')); + }); + + test('Migrate with no data and missing target fails without force', + () async { + // Ensure the collection exists first + await collection.doc('dummy').create({'foo': 'bar'}); + + final emptyIdsCollection = SharedManyDocument([], collection: collection); + final response = await emptyIdsCollection.migrate('target', force: false); + expect(response, isA()); + expect(response.success, isFalse); + expect(response.message, + contains('No source documents were available for migration')); + }); + + test('toString returns expected format', () { + final many = collection.docs(['docA', 'docB']); + expect( + many.toString(), + contains( + 'SharedManyDocument(ids: [docA, docB], collection: $collection)')); }); }); }