diff options
author | Gil <mcg@google.com> | 2017-10-03 08:55:22 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-10-03 08:55:22 -0700 |
commit | bde743ed25166a0b320ae157bfb1d68064f531c9 (patch) | |
tree | 4dd7525d9df32fa5dbdb721d4b0d4f9b87f5e884 /Firestore/Example | |
parent | bf550507ffa8beee149383a5bf1e2363bccefbb4 (diff) |
Release 4.3.0 (#327)
Initial release of Firestore at 0.8.0
Bump FirebaseCommunity to 0.1.3
Diffstat (limited to 'Firestore/Example')
112 files changed, 31139 insertions, 0 deletions
diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj new file mode 100644 index 0000000..979050f --- /dev/null +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -0,0 +1,1700 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXAggregateTarget section */ + DE29E7F51F2174B000909613 /* AllTests */ = { + isa = PBXAggregateTarget; + buildConfigurationList = DE29E7F81F2174B000909613 /* Build configuration list for PBXAggregateTarget "AllTests" */; + buildPhases = ( + ); + dependencies = ( + DE0761FA1F2FEE7E003233AF /* PBXTargetDependency */, + DE29E7FA1F2174DD00909613 /* PBXTargetDependency */, + DE29E7FC1F2174DD00909613 /* PBXTargetDependency */, + ); + name = AllTests; + productName = AllTests; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 3B843E4C1F3A182900548890 /* remote_store_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */; }; + 54DA12A61F315EE100DD57A1 /* collection_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129C1F315EE100DD57A1 /* collection_spec_test.json */; }; + 54DA12A71F315EE100DD57A1 /* existence_filter_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129D1F315EE100DD57A1 /* existence_filter_spec_test.json */; }; + 54DA12A81F315EE100DD57A1 /* limbo_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129E1F315EE100DD57A1 /* limbo_spec_test.json */; }; + 54DA12A91F315EE100DD57A1 /* limit_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129F1F315EE100DD57A1 /* limit_spec_test.json */; }; + 54DA12AA1F315EE100DD57A1 /* listen_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A01F315EE100DD57A1 /* listen_spec_test.json */; }; + 54DA12AB1F315EE100DD57A1 /* offline_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A11F315EE100DD57A1 /* offline_spec_test.json */; }; + 54DA12AC1F315EE100DD57A1 /* orderby_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A21F315EE100DD57A1 /* orderby_spec_test.json */; }; + 54DA12AD1F315EE100DD57A1 /* persistence_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A31F315EE100DD57A1 /* persistence_spec_test.json */; }; + 54DA12AE1F315EE100DD57A1 /* resume_token_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A41F315EE100DD57A1 /* resume_token_spec_test.json */; }; + 54DA12AF1F315EE100DD57A1 /* write_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A51F315EE100DD57A1 /* write_spec_test.json */; }; + 54DA12B11F315F3800DD57A1 /* FIRValidationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 54DA12B01F315F3800DD57A1 /* FIRValidationTests.m */; }; + 54E928221F33952900C1953E /* FSTIntegrationTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9281F1F33950B00C1953E /* FSTIntegrationTestCase.m */; }; + 54E928231F33952D00C1953E /* FSTIntegrationTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9281F1F33950B00C1953E /* FSTIntegrationTestCase.m */; }; + 54E928241F33953300C1953E /* FSTEventAccumulator.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9281D1F33950B00C1953E /* FSTEventAccumulator.m */; }; + 54E928251F33953400C1953E /* FSTEventAccumulator.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9281D1F33950B00C1953E /* FSTEventAccumulator.m */; }; + 54E9282C1F339CAD00C1953E /* XCTestCase+Await.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9282B1F339CAD00C1953E /* XCTestCase+Await.m */; }; + 54E9282D1F339CAD00C1953E /* XCTestCase+Await.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9282B1F339CAD00C1953E /* XCTestCase+Await.m */; }; + 6003F58E195388D20070C39A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; }; + 6003F590195388D20070C39A /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58F195388D20070C39A /* CoreGraphics.framework */; }; + 6003F592195388D20070C39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; + 6003F598195388D20070C39A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F596195388D20070C39A /* InfoPlist.strings */; }; + 6003F59A195388D20070C39A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 6003F599195388D20070C39A /* main.m */; }; + 6003F59E195388D20070C39A /* FIRAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6003F59D195388D20070C39A /* FIRAppDelegate.m */; }; + 6003F5A7195388D20070C39A /* FIRViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6003F5A6195388D20070C39A /* FIRViewController.m */; }; + 6003F5A9195388D20070C39A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5A8195388D20070C39A /* Images.xcassets */; }; + 6003F5B0195388D20070C39A /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F5AF195388D20070C39A /* XCTest.framework */; }; + 6003F5B1195388D20070C39A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; }; + 6003F5B2195388D20070C39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; + 6003F5BA195388D20070C39A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5B8195388D20070C39A /* InfoPlist.strings */; }; + 6ED54761B845349D43DB6B78 /* Pods_Firestore_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 75A6FE51C1A02DF38F62FAAD /* Pods_Firestore_Example.framework */; }; + 71719F9F1E33DC2100824A3D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */; }; + 873B8AEB1B1F5CCA007FD442 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */; }; + AFE6114F0D4DAECBA7B7C089 /* Pods_Firestore_IntegrationTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */; }; + C4E749275AD0FBDF9F4716A8 /* Pods_SwiftBuildTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 32AD40BF6B0E849B07FFD05E /* Pods_SwiftBuildTest.framework */; }; + DE03B2C91F2149D600A30B9C /* FSTTransactionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C61F0D48AC0013853F /* FSTTransactionTests.m */; }; + DE03B2D41F2149D600A30B9C /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F5AF195388D20070C39A /* XCTest.framework */; }; + DE03B2D51F2149D600A30B9C /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; + DE03B2D61F2149D600A30B9C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; }; + DE03B2D71F2149D600A30B9C /* Pods_Firestore_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */; }; + DE03B2DD1F2149D600A30B9C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5B8195388D20070C39A /* InfoPlist.strings */; }; + DE03B2EC1F214BA200A30B9C /* FSTDatastoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C41F0D48AC0013853F /* FSTDatastoreTests.m */; }; + DE03B2ED1F214BA200A30B9C /* FSTSmokeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C51F0D48AC0013853F /* FSTSmokeTests.m */; }; + DE03B2EE1F214BAA00A30B9C /* FIRWriteBatchTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DEFE0F471F1F960A0071599A /* FIRWriteBatchTests.m */; }; + DE03B2EF1F214BAA00A30B9C /* FIRCursorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1BD1F0D48AC0013853F /* FIRCursorTests.m */; }; + DE03B2F01F214BAA00A30B9C /* FIRDatabaseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1BE1F0D48AC0013853F /* FIRDatabaseTests.m */; }; + DE03B2F11F214BAA00A30B9C /* FIRFieldsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1BF1F0D48AC0013853F /* FIRFieldsTests.m */; }; + DE03B2F21F214BAA00A30B9C /* FIRListenerRegistrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C01F0D48AC0013853F /* FIRListenerRegistrationTests.m */; }; + DE03B2F31F214BAA00A30B9C /* FIRQueryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C11F0D48AC0013853F /* FIRQueryTests.m */; }; + DE03B2F41F214BAA00A30B9C /* FIRServerTimestampTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C21F0D48AC0013853F /* FIRServerTimestampTests.m */; }; + DE03B2F51F214BAA00A30B9C /* FIRTypeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C31F0D48AC0013853F /* FIRTypeTests.m */; }; + DE03B35E1F21586C00A30B9C /* FSTHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1891F0D48AC0013853F /* FSTHelpers.m */; }; + DE03B3631F215E1A00A30B9C /* CAcert.pem in Resources */ = {isa = PBXBuildFile; fileRef = DE03B3621F215E1600A30B9C /* CAcert.pem */; }; + DE0761F81F2FE68D003233AF /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE0761F61F2FE68D003233AF /* main.swift */; }; + DE2EF0851F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF07E1F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m */; }; + DE2EF0861F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF0801F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m */; }; + DE2EF0871F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF0821F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m */; }; + DE2EF0881F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF0841F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m */; }; + DE51B1CC1F0D48C00013853F /* FIRGeoPointTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1841F0D48AC0013853F /* FIRGeoPointTests.m */; }; + DE51B1CD1F0D48CD0013853F /* FSTDatabaseInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1A91F0D48AC0013853F /* FSTDatabaseInfoTests.m */; }; + DE51B1CE1F0D48CD0013853F /* FSTEventManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AA1F0D48AC0013853F /* FSTEventManagerTests.m */; }; + DE51B1CF1F0D48CD0013853F /* FSTQueryListenerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AB1F0D48AC0013853F /* FSTQueryListenerTests.m */; }; + DE51B1D01F0D48CD0013853F /* FSTQueryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AC1F0D48AC0013853F /* FSTQueryTests.m */; }; + DE51B1D11F0D48CD0013853F /* FSTTargetIDGeneratorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AE1F0D48AC0013853F /* FSTTargetIDGeneratorTests.m */; }; + DE51B1D21F0D48CD0013853F /* FSTTimestampTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AF1F0D48AC0013853F /* FSTTimestampTests.m */; }; + DE51B1D31F0D48CD0013853F /* FSTViewSnapshotTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B01F0D48AC0013853F /* FSTViewSnapshotTest.m */; }; + DE51B1D41F0D48CD0013853F /* FSTViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B11F0D48AC0013853F /* FSTViewTests.m */; }; + DE51B1D91F0D490D0013853F /* FSTEagerGarbageCollectorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1631F0D48AC0013853F /* FSTEagerGarbageCollectorTests.m */; }; + DE51B1DA1F0D490D0013853F /* FSTLevelDBLocalStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1651F0D48AC0013853F /* FSTLevelDBLocalStoreTests.m */; }; + DE51B1DB1F0D490D0013853F /* FSTLevelDBQueryCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1671F0D48AC0013853F /* FSTLevelDBQueryCacheTests.m */; }; + DE51B1DC1F0D490D0013853F /* FSTLocalSerializerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1691F0D48AC0013853F /* FSTLocalSerializerTests.m */; }; + DE51B1DD1F0D490D0013853F /* FSTLocalStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16B1F0D48AC0013853F /* FSTLocalStoreTests.m */; }; + DE51B1DE1F0D490D0013853F /* FSTMemoryLocalStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16C1F0D48AC0013853F /* FSTMemoryLocalStoreTests.m */; }; + DE51B1DF1F0D490D0013853F /* FSTMemoryMutationQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16D1F0D48AC0013853F /* FSTMemoryMutationQueueTests.m */; }; + DE51B1E01F0D490D0013853F /* FSTMemoryQueryCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16E1F0D48AC0013853F /* FSTMemoryQueryCacheTests.m */; }; + DE51B1E11F0D490D0013853F /* FSTMemoryRemoteDocumentCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16F1F0D48AC0013853F /* FSTMemoryRemoteDocumentCacheTests.m */; }; + DE51B1E21F0D490D0013853F /* FSTMutationQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1711F0D48AC0013853F /* FSTMutationQueueTests.m */; }; + DE51B1E31F0D490D0013853F /* FSTPersistenceTestHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1731F0D48AC0013853F /* FSTPersistenceTestHelpers.m */; }; + DE51B1E41F0D490D0013853F /* FSTQueryCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1751F0D48AC0013853F /* FSTQueryCacheTests.m */; }; + DE51B1E51F0D490D0013853F /* FSTReferenceSetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1761F0D48AC0013853F /* FSTReferenceSetTests.m */; }; + DE51B1E61F0D490D0013853F /* FSTRemoteDocumentCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1781F0D48AC0013853F /* FSTRemoteDocumentCacheTests.m */; }; + DE51B1E71F0D490D0013853F /* FSTRemoteDocumentChangeBufferTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1791F0D48AC0013853F /* FSTRemoteDocumentChangeBufferTests.m */; }; + DE51B1E81F0D490D0013853F /* FSTLevelDBKeyTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1641F0D48AC0013853F /* FSTLevelDBKeyTests.mm */; }; + DE51B1E91F0D490D0013853F /* FSTLevelDBMutationQueueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1661F0D48AC0013853F /* FSTLevelDBMutationQueueTests.mm */; }; + DE51B1EA1F0D490D0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1681F0D48AC0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm */; }; + DE51B1EB1F0D490D0013853F /* FSTWriteGroupTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17A1F0D48AC0013853F /* FSTWriteGroupTests.mm */; }; + DE51B1EC1F0D49140013853F /* FSTDatabaseIDTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17C1F0D48AC0013853F /* FSTDatabaseIDTests.m */; }; + DE51B1ED1F0D49140013853F /* FSTDocumentKeyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17D1F0D48AC0013853F /* FSTDocumentKeyTests.m */; }; + DE51B1EE1F0D49140013853F /* FSTDocumentSetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17E1F0D48AC0013853F /* FSTDocumentSetTests.m */; }; + DE51B1EF1F0D49140013853F /* FSTDocumentTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17F1F0D48AC0013853F /* FSTDocumentTests.m */; }; + DE51B1F01F0D49140013853F /* FSTFieldValueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1801F0D48AC0013853F /* FSTFieldValueTests.m */; }; + DE51B1F11F0D49140013853F /* FSTMutationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1811F0D48AC0013853F /* FSTMutationTests.m */; }; + DE51B1F21F0D49140013853F /* FSTPathTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1821F0D48AC0013853F /* FSTPathTests.m */; }; + DE51B1F31F0D491B0013853F /* FSTDatastoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B31F0D48AC0013853F /* FSTDatastoreTests.m */; }; + DE51B1F41F0D491B0013853F /* FSTRemoteEventTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B41F0D48AC0013853F /* FSTRemoteEventTests.m */; }; + DE51B1F61F0D491B0013853F /* FSTSerializerBetaTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B61F0D48AC0013853F /* FSTSerializerBetaTests.m */; }; + DE51B1F71F0D491B0013853F /* FSTStreamTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B71F0D48AC0013853F /* FSTStreamTests.m */; }; + DE51B1F81F0D491F0013853F /* FSTWatchChange+Testing.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B91F0D48AC0013853F /* FSTWatchChange+Testing.m */; }; + DE51B1F91F0D491F0013853F /* FSTWatchChangeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1BA1F0D48AC0013853F /* FSTWatchChangeTests.m */; }; + DE51B1FA1F0D492C0013853F /* FSTLevelDBSpecTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1941F0D48AC0013853F /* FSTLevelDBSpecTests.m */; }; + DE51B1FB1F0D492C0013853F /* FSTMemorySpecTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1951F0D48AC0013853F /* FSTMemorySpecTests.m */; }; + DE51B1FC1F0D492C0013853F /* FSTMockDatastore.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1971F0D48AC0013853F /* FSTMockDatastore.m */; }; + DE51B1FD1F0D492C0013853F /* FSTSpecTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1991F0D48AC0013853F /* FSTSpecTests.m */; }; + DE51B1FE1F0D492C0013853F /* FSTSyncEngineTestDriver.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B19B1F0D48AC0013853F /* FSTSyncEngineTestDriver.m */; }; + DE51B1FF1F0D493A0013853F /* FSTAssertTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1861F0D48AC0013853F /* FSTAssertTests.m */; }; + DE51B2001F0D493A0013853F /* FSTComparisonTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1871F0D48AC0013853F /* FSTComparisonTests.m */; }; + DE51B2011F0D493E0013853F /* FSTHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1891F0D48AC0013853F /* FSTHelpers.m */; }; + DE51B2021F0D493E0013853F /* FSTUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B18A1F0D48AC0013853F /* FSTUtilTests.m */; }; + F104BBD69BC3F0796E3A77C1 /* Pods_Firestore_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 6003F5B3195388D20070C39A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6003F582195388D10070C39A /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6003F589195388D20070C39A; + remoteInfo = Firestore; + }; + DE03B2961F2149D600A30B9C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6003F582195388D10070C39A /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6003F589195388D20070C39A; + remoteInfo = Firestore; + }; + DE0761F91F2FEE7E003233AF /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6003F582195388D10070C39A /* Project object */; + proxyType = 1; + remoteGlobalIDString = DE0761E31F2FE611003233AF; + remoteInfo = SwiftBuildTest; + }; + DE29E7F91F2174DD00909613 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6003F582195388D10070C39A /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6003F5AD195388D20070C39A; + remoteInfo = Firestore_Tests; + }; + DE29E7FB1F2174DD00909613 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6003F582195388D10070C39A /* Project object */; + proxyType = 1; + remoteGlobalIDString = DE03B2941F2149D600A30B9C; + remoteInfo = Firestore_IntegrationTests; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 04DF37A117F88A9891379ED6 /* Pods-Firestore_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests.release.xcconfig"; sourceTree = "<group>"; }; + 12F4357299652983A615F886 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = "<group>"; }; + 32AD40BF6B0E849B07FFD05E /* Pods_SwiftBuildTest.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftBuildTest.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = remote_store_spec_test.json; sourceTree = "<group>"; }; + 42491D7DC8C8CD245CC22B93 /* Pods-SwiftBuildTest.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftBuildTest.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest.debug.xcconfig"; sourceTree = "<group>"; }; + 4EBC5F5ABE1FD097EFE5E224 /* Pods-Firestore_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example/Pods-Firestore_Example.release.xcconfig"; sourceTree = "<group>"; }; + 54DA129C1F315EE100DD57A1 /* collection_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = collection_spec_test.json; sourceTree = "<group>"; }; + 54DA129D1F315EE100DD57A1 /* existence_filter_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = existence_filter_spec_test.json; sourceTree = "<group>"; }; + 54DA129E1F315EE100DD57A1 /* limbo_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = limbo_spec_test.json; sourceTree = "<group>"; }; + 54DA129F1F315EE100DD57A1 /* limit_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = limit_spec_test.json; sourceTree = "<group>"; }; + 54DA12A01F315EE100DD57A1 /* listen_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = listen_spec_test.json; sourceTree = "<group>"; }; + 54DA12A11F315EE100DD57A1 /* offline_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = offline_spec_test.json; sourceTree = "<group>"; }; + 54DA12A21F315EE100DD57A1 /* orderby_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = orderby_spec_test.json; sourceTree = "<group>"; }; + 54DA12A31F315EE100DD57A1 /* persistence_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = persistence_spec_test.json; sourceTree = "<group>"; }; + 54DA12A41F315EE100DD57A1 /* resume_token_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = resume_token_spec_test.json; sourceTree = "<group>"; }; + 54DA12A51F315EE100DD57A1 /* write_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = write_spec_test.json; sourceTree = "<group>"; }; + 54DA12B01F315F3800DD57A1 /* FIRValidationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRValidationTests.m; sourceTree = "<group>"; }; + 54E9281C1F33950B00C1953E /* FSTEventAccumulator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSTEventAccumulator.h; sourceTree = "<group>"; }; + 54E9281D1F33950B00C1953E /* FSTEventAccumulator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FSTEventAccumulator.m; sourceTree = "<group>"; }; + 54E9281E1F33950B00C1953E /* FSTIntegrationTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSTIntegrationTestCase.h; sourceTree = "<group>"; }; + 54E9281F1F33950B00C1953E /* FSTIntegrationTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FSTIntegrationTestCase.m; sourceTree = "<group>"; }; + 54E9282A1F339CAD00C1953E /* XCTestCase+Await.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCTestCase+Await.h"; sourceTree = "<group>"; }; + 54E9282B1F339CAD00C1953E /* XCTestCase+Await.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCTestCase+Await.m"; sourceTree = "<group>"; }; + 6003F58A195388D20070C39A /* Firestore_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Firestore_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 6003F58D195388D20070C39A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 6003F58F195388D20070C39A /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 6003F591195388D20070C39A /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + 6003F595195388D20070C39A /* Firestore-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Firestore-Info.plist"; sourceTree = "<group>"; }; + 6003F597195388D20070C39A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + 6003F599195388D20070C39A /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; }; + 6003F59C195388D20070C39A /* FIRAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FIRAppDelegate.h; sourceTree = "<group>"; }; + 6003F59D195388D20070C39A /* FIRAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRAppDelegate.m; sourceTree = "<group>"; }; + 6003F5A5195388D20070C39A /* FIRViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FIRViewController.h; sourceTree = "<group>"; }; + 6003F5A6195388D20070C39A /* FIRViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRViewController.m; sourceTree = "<group>"; }; + 6003F5A8195388D20070C39A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; }; + 6003F5AE195388D20070C39A /* Firestore_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Firestore_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 6003F5AF195388D20070C39A /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + 6003F5B7195388D20070C39A /* Tests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Tests-Info.plist"; sourceTree = "<group>"; }; + 6003F5B9195388D20070C39A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + 69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 71719F9E1E33DC2100824A3D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; + 75A6FE51C1A02DF38F62FAAD /* Pods_Firestore_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; }; + 8E002F4AD5D9B6197C940847 /* Firestore.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = Firestore.podspec; path = ../Firestore.podspec; sourceTree = "<group>"; }; + 9D52E67EE96AA7E5D6F69748 /* Pods-Firestore_IntegrationTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_IntegrationTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests.debug.xcconfig"; sourceTree = "<group>"; }; + 9EF477AD4B2B643FD320867A /* Pods-Firestore_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example/Pods-Firestore_Example.debug.xcconfig"; sourceTree = "<group>"; }; + B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_IntegrationTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + CE00BABB5A3AAB44A4C209E2 /* Pods-Firestore_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests.debug.xcconfig"; sourceTree = "<group>"; }; + D3CC3DC5338DCAF43A211155 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; }; + DB17FEDFB80770611A935A60 /* Pods-Firestore_IntegrationTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_IntegrationTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests.release.xcconfig"; sourceTree = "<group>"; }; + DE03B2E91F2149D600A30B9C /* Firestore_IntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Firestore_IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + DE03B3621F215E1600A30B9C /* CAcert.pem */ = {isa = PBXFileReference; lastKnownFileType = text; path = CAcert.pem; sourceTree = "<group>"; }; + DE0761E41F2FE611003233AF /* SwiftBuildTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftBuildTest.app; sourceTree = BUILT_PRODUCTS_DIR; }; + DE0761F61F2FE68D003233AF /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; }; + DE2EF07E1F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FSTArraySortedDictionaryTests.m; path = ../../third_party/Immutable/Tests/FSTArraySortedDictionaryTests.m; sourceTree = "<group>"; }; + DE2EF07F1F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "FSTImmutableSortedDictionary+Testing.h"; path = "../../third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.h"; sourceTree = "<group>"; }; + DE2EF0801F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "FSTImmutableSortedDictionary+Testing.m"; path = "../../third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.m"; sourceTree = "<group>"; }; + DE2EF0811F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "FSTImmutableSortedSet+Testing.h"; path = "../../third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.h"; sourceTree = "<group>"; }; + DE2EF0821F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "FSTImmutableSortedSet+Testing.m"; path = "../../third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.m"; sourceTree = "<group>"; }; + DE2EF0831F3D0B6E003D0CDC /* FSTLLRBValueNode+Test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "FSTLLRBValueNode+Test.h"; path = "../../third_party/Immutable/Tests/FSTLLRBValueNode+Test.h"; sourceTree = "<group>"; }; + DE2EF0841F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FSTTreeSortedDictionaryTests.m; path = ../../third_party/Immutable/Tests/FSTTreeSortedDictionaryTests.m; sourceTree = "<group>"; }; + DE51B1631F0D48AC0013853F /* FSTEagerGarbageCollectorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTEagerGarbageCollectorTests.m; sourceTree = "<group>"; }; + DE51B1641F0D48AC0013853F /* FSTLevelDBKeyTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBKeyTests.mm; sourceTree = "<group>"; }; + DE51B1651F0D48AC0013853F /* FSTLevelDBLocalStoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLevelDBLocalStoreTests.m; sourceTree = "<group>"; }; + DE51B1661F0D48AC0013853F /* FSTLevelDBMutationQueueTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBMutationQueueTests.mm; sourceTree = "<group>"; }; + DE51B1671F0D48AC0013853F /* FSTLevelDBQueryCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLevelDBQueryCacheTests.m; sourceTree = "<group>"; }; + DE51B1681F0D48AC0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBRemoteDocumentCacheTests.mm; sourceTree = "<group>"; }; + DE51B1691F0D48AC0013853F /* FSTLocalSerializerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLocalSerializerTests.m; sourceTree = "<group>"; }; + DE51B16A1F0D48AC0013853F /* FSTLocalStoreTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTLocalStoreTests.h; sourceTree = "<group>"; }; + DE51B16B1F0D48AC0013853F /* FSTLocalStoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLocalStoreTests.m; sourceTree = "<group>"; }; + DE51B16C1F0D48AC0013853F /* FSTMemoryLocalStoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemoryLocalStoreTests.m; sourceTree = "<group>"; }; + DE51B16D1F0D48AC0013853F /* FSTMemoryMutationQueueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemoryMutationQueueTests.m; sourceTree = "<group>"; }; + DE51B16E1F0D48AC0013853F /* FSTMemoryQueryCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemoryQueryCacheTests.m; sourceTree = "<group>"; }; + DE51B16F1F0D48AC0013853F /* FSTMemoryRemoteDocumentCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemoryRemoteDocumentCacheTests.m; sourceTree = "<group>"; }; + DE51B1701F0D48AC0013853F /* FSTMutationQueueTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTMutationQueueTests.h; sourceTree = "<group>"; }; + DE51B1711F0D48AC0013853F /* FSTMutationQueueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMutationQueueTests.m; sourceTree = "<group>"; }; + DE51B1721F0D48AC0013853F /* FSTPersistenceTestHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTPersistenceTestHelpers.h; sourceTree = "<group>"; }; + DE51B1731F0D48AC0013853F /* FSTPersistenceTestHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTPersistenceTestHelpers.m; sourceTree = "<group>"; }; + DE51B1741F0D48AC0013853F /* FSTQueryCacheTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTQueryCacheTests.h; sourceTree = "<group>"; }; + DE51B1751F0D48AC0013853F /* FSTQueryCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTQueryCacheTests.m; sourceTree = "<group>"; }; + DE51B1761F0D48AC0013853F /* FSTReferenceSetTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTReferenceSetTests.m; sourceTree = "<group>"; }; + DE51B1771F0D48AC0013853F /* FSTRemoteDocumentCacheTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTRemoteDocumentCacheTests.h; sourceTree = "<group>"; }; + DE51B1781F0D48AC0013853F /* FSTRemoteDocumentCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTRemoteDocumentCacheTests.m; sourceTree = "<group>"; }; + DE51B1791F0D48AC0013853F /* FSTRemoteDocumentChangeBufferTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTRemoteDocumentChangeBufferTests.m; sourceTree = "<group>"; }; + DE51B17A1F0D48AC0013853F /* FSTWriteGroupTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTWriteGroupTests.mm; sourceTree = "<group>"; }; + DE51B17C1F0D48AC0013853F /* FSTDatabaseIDTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDatabaseIDTests.m; sourceTree = "<group>"; }; + DE51B17D1F0D48AC0013853F /* FSTDocumentKeyTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDocumentKeyTests.m; sourceTree = "<group>"; }; + DE51B17E1F0D48AC0013853F /* FSTDocumentSetTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDocumentSetTests.m; sourceTree = "<group>"; }; + DE51B17F1F0D48AC0013853F /* FSTDocumentTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDocumentTests.m; sourceTree = "<group>"; }; + DE51B1801F0D48AC0013853F /* FSTFieldValueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTFieldValueTests.m; sourceTree = "<group>"; }; + DE51B1811F0D48AC0013853F /* FSTMutationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMutationTests.m; sourceTree = "<group>"; }; + DE51B1821F0D48AC0013853F /* FSTPathTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTPathTests.m; sourceTree = "<group>"; }; + DE51B1841F0D48AC0013853F /* FIRGeoPointTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRGeoPointTests.m; sourceTree = "<group>"; }; + DE51B1861F0D48AC0013853F /* FSTAssertTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTAssertTests.m; sourceTree = "<group>"; }; + DE51B1871F0D48AC0013853F /* FSTComparisonTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTComparisonTests.m; sourceTree = "<group>"; }; + DE51B1881F0D48AC0013853F /* FSTHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTHelpers.h; sourceTree = "<group>"; }; + DE51B1891F0D48AC0013853F /* FSTHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTHelpers.m; sourceTree = "<group>"; }; + DE51B18A1F0D48AC0013853F /* FSTUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTUtilTests.m; sourceTree = "<group>"; }; + DE51B1941F0D48AC0013853F /* FSTLevelDBSpecTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLevelDBSpecTests.m; sourceTree = "<group>"; }; + DE51B1951F0D48AC0013853F /* FSTMemorySpecTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemorySpecTests.m; sourceTree = "<group>"; }; + DE51B1961F0D48AC0013853F /* FSTMockDatastore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTMockDatastore.h; sourceTree = "<group>"; }; + DE51B1971F0D48AC0013853F /* FSTMockDatastore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMockDatastore.m; sourceTree = "<group>"; }; + DE51B1981F0D48AC0013853F /* FSTSpecTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTSpecTests.h; sourceTree = "<group>"; }; + DE51B1991F0D48AC0013853F /* FSTSpecTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTSpecTests.m; sourceTree = "<group>"; }; + DE51B19A1F0D48AC0013853F /* FSTSyncEngineTestDriver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTSyncEngineTestDriver.h; sourceTree = "<group>"; }; + DE51B19B1F0D48AC0013853F /* FSTSyncEngineTestDriver.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTSyncEngineTestDriver.m; sourceTree = "<group>"; }; + DE51B1A71F0D48AC0013853F /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; }; + DE51B1A91F0D48AC0013853F /* FSTDatabaseInfoTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDatabaseInfoTests.m; sourceTree = "<group>"; }; + DE51B1AA1F0D48AC0013853F /* FSTEventManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTEventManagerTests.m; sourceTree = "<group>"; }; + DE51B1AB1F0D48AC0013853F /* FSTQueryListenerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTQueryListenerTests.m; sourceTree = "<group>"; }; + DE51B1AC1F0D48AC0013853F /* FSTQueryTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTQueryTests.m; sourceTree = "<group>"; }; + DE51B1AD1F0D48AC0013853F /* FSTSyncEngine+Testing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FSTSyncEngine+Testing.h"; sourceTree = "<group>"; }; + DE51B1AE1F0D48AC0013853F /* FSTTargetIDGeneratorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTTargetIDGeneratorTests.m; sourceTree = "<group>"; }; + DE51B1AF1F0D48AC0013853F /* FSTTimestampTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTTimestampTests.m; sourceTree = "<group>"; }; + DE51B1B01F0D48AC0013853F /* FSTViewSnapshotTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTViewSnapshotTest.m; sourceTree = "<group>"; }; + DE51B1B11F0D48AC0013853F /* FSTViewTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTViewTests.m; sourceTree = "<group>"; }; + DE51B1B31F0D48AC0013853F /* FSTDatastoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDatastoreTests.m; sourceTree = "<group>"; }; + DE51B1B41F0D48AC0013853F /* FSTRemoteEventTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTRemoteEventTests.m; sourceTree = "<group>"; }; + DE51B1B61F0D48AC0013853F /* FSTSerializerBetaTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTSerializerBetaTests.m; sourceTree = "<group>"; }; + DE51B1B71F0D48AC0013853F /* FSTStreamTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTStreamTests.m; sourceTree = "<group>"; }; + DE51B1B81F0D48AC0013853F /* FSTWatchChange+Testing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FSTWatchChange+Testing.h"; sourceTree = "<group>"; }; + DE51B1B91F0D48AC0013853F /* FSTWatchChange+Testing.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "FSTWatchChange+Testing.m"; sourceTree = "<group>"; }; + DE51B1BA1F0D48AC0013853F /* FSTWatchChangeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTWatchChangeTests.m; sourceTree = "<group>"; }; + DE51B1BD1F0D48AC0013853F /* FIRCursorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRCursorTests.m; sourceTree = "<group>"; }; + DE51B1BE1F0D48AC0013853F /* FIRDatabaseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRDatabaseTests.m; sourceTree = "<group>"; }; + DE51B1BF1F0D48AC0013853F /* FIRFieldsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRFieldsTests.m; sourceTree = "<group>"; }; + DE51B1C01F0D48AC0013853F /* FIRListenerRegistrationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRListenerRegistrationTests.m; sourceTree = "<group>"; }; + DE51B1C11F0D48AC0013853F /* FIRQueryTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRQueryTests.m; sourceTree = "<group>"; }; + DE51B1C21F0D48AC0013853F /* FIRServerTimestampTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRServerTimestampTests.m; sourceTree = "<group>"; }; + DE51B1C31F0D48AC0013853F /* FIRTypeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRTypeTests.m; sourceTree = "<group>"; }; + DE51B1C41F0D48AC0013853F /* FSTDatastoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDatastoreTests.m; sourceTree = "<group>"; }; + DE51B1C51F0D48AC0013853F /* FSTSmokeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTSmokeTests.m; sourceTree = "<group>"; }; + DE51B1C61F0D48AC0013853F /* FSTTransactionTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTTransactionTests.m; sourceTree = "<group>"; }; + DEFE0F471F1F960A0071599A /* FIRWriteBatchTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRWriteBatchTests.m; sourceTree = "<group>"; }; + F23325524BEAF8D24F78AC88 /* Pods-SwiftBuildTest.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftBuildTest.release.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest.release.xcconfig"; sourceTree = "<group>"; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 6003F587195388D20070C39A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6003F590195388D20070C39A /* CoreGraphics.framework in Frameworks */, + 6003F592195388D20070C39A /* UIKit.framework in Frameworks */, + 6003F58E195388D20070C39A /* Foundation.framework in Frameworks */, + 6ED54761B845349D43DB6B78 /* Pods_Firestore_Example.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6003F5AB195388D20070C39A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6003F5B0195388D20070C39A /* XCTest.framework in Frameworks */, + 6003F5B2195388D20070C39A /* UIKit.framework in Frameworks */, + 6003F5B1195388D20070C39A /* Foundation.framework in Frameworks */, + F104BBD69BC3F0796E3A77C1 /* Pods_Firestore_Tests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DE03B2D31F2149D600A30B9C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DE03B2D41F2149D600A30B9C /* XCTest.framework in Frameworks */, + DE03B2D51F2149D600A30B9C /* UIKit.framework in Frameworks */, + DE03B2D61F2149D600A30B9C /* Foundation.framework in Frameworks */, + DE03B2D71F2149D600A30B9C /* Pods_Firestore_Tests.framework in Frameworks */, + AFE6114F0D4DAECBA7B7C089 /* Pods_Firestore_IntegrationTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DE0761E11F2FE611003233AF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C4E749275AD0FBDF9F4716A8 /* Pods_SwiftBuildTest.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 6003F581195388D10070C39A = { + isa = PBXGroup; + children = ( + 60FF7A9C1954A5C5007DD14C /* Podspec Metadata */, + 6003F593195388D20070C39A /* Example for Firestore */, + 6003F5B5195388D20070C39A /* Tests */, + DE0761E51F2FE611003233AF /* SwiftBuildTest */, + 6003F58C195388D20070C39A /* Frameworks */, + 6003F58B195388D20070C39A /* Products */, + A47A1BF74A48BCAEAFBCBF1E /* Pods */, + ); + sourceTree = "<group>"; + }; + 6003F58B195388D20070C39A /* Products */ = { + isa = PBXGroup; + children = ( + 6003F58A195388D20070C39A /* Firestore_Example.app */, + 6003F5AE195388D20070C39A /* Firestore_Tests.xctest */, + DE03B2E91F2149D600A30B9C /* Firestore_IntegrationTests.xctest */, + DE0761E41F2FE611003233AF /* SwiftBuildTest.app */, + ); + name = Products; + sourceTree = "<group>"; + }; + 6003F58C195388D20070C39A /* Frameworks */ = { + isa = PBXGroup; + children = ( + 6003F58D195388D20070C39A /* Foundation.framework */, + 6003F58F195388D20070C39A /* CoreGraphics.framework */, + 6003F591195388D20070C39A /* UIKit.framework */, + 6003F5AF195388D20070C39A /* XCTest.framework */, + 75A6FE51C1A02DF38F62FAAD /* Pods_Firestore_Example.framework */, + 69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */, + B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */, + 32AD40BF6B0E849B07FFD05E /* Pods_SwiftBuildTest.framework */, + ); + name = Frameworks; + sourceTree = "<group>"; + }; + 6003F593195388D20070C39A /* Example for Firestore */ = { + isa = PBXGroup; + children = ( + 6003F59C195388D20070C39A /* FIRAppDelegate.h */, + 6003F59D195388D20070C39A /* FIRAppDelegate.m */, + 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */, + 6003F5A5195388D20070C39A /* FIRViewController.h */, + 6003F5A6195388D20070C39A /* FIRViewController.m */, + 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */, + 6003F5A8195388D20070C39A /* Images.xcassets */, + 6003F594195388D20070C39A /* Supporting Files */, + ); + name = "Example for Firestore"; + path = Firestore; + sourceTree = "<group>"; + }; + 6003F594195388D20070C39A /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 6003F595195388D20070C39A /* Firestore-Info.plist */, + 6003F596195388D20070C39A /* InfoPlist.strings */, + 6003F599195388D20070C39A /* main.m */, + ); + name = "Supporting Files"; + sourceTree = "<group>"; + }; + 6003F5B5195388D20070C39A /* Tests */ = { + isa = PBXGroup; + children = ( + DE51B1831F0D48AC0013853F /* API */, + DE51B1A81F0D48AC0013853F /* Core */, + DE2EF06E1F3D07D7003D0CDC /* Immutable */, + DE51B1BB1F0D48AC0013853F /* Integration */, + DE51B1621F0D48AC0013853F /* Local */, + DE51B17B1F0D48AC0013853F /* Model */, + DE51B1B21F0D48AC0013853F /* Remote */, + DE51B1931F0D48AC0013853F /* SpecTests */, + DE51B1851F0D48AC0013853F /* Util */, + 6003F5B6195388D20070C39A /* Supporting Files */, + ); + path = Tests; + sourceTree = "<group>"; + }; + 6003F5B6195388D20070C39A /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 6003F5B7195388D20070C39A /* Tests-Info.plist */, + 6003F5B8195388D20070C39A /* InfoPlist.strings */, + ); + name = "Supporting Files"; + sourceTree = "<group>"; + }; + 60FF7A9C1954A5C5007DD14C /* Podspec Metadata */ = { + isa = PBXGroup; + children = ( + 8E002F4AD5D9B6197C940847 /* Firestore.podspec */, + D3CC3DC5338DCAF43A211155 /* README.md */, + 12F4357299652983A615F886 /* LICENSE */, + ); + name = "Podspec Metadata"; + sourceTree = "<group>"; + }; + A47A1BF74A48BCAEAFBCBF1E /* Pods */ = { + isa = PBXGroup; + children = ( + 9EF477AD4B2B643FD320867A /* Pods-Firestore_Example.debug.xcconfig */, + 4EBC5F5ABE1FD097EFE5E224 /* Pods-Firestore_Example.release.xcconfig */, + 9D52E67EE96AA7E5D6F69748 /* Pods-Firestore_IntegrationTests.debug.xcconfig */, + DB17FEDFB80770611A935A60 /* Pods-Firestore_IntegrationTests.release.xcconfig */, + CE00BABB5A3AAB44A4C209E2 /* Pods-Firestore_Tests.debug.xcconfig */, + 04DF37A117F88A9891379ED6 /* Pods-Firestore_Tests.release.xcconfig */, + 42491D7DC8C8CD245CC22B93 /* Pods-SwiftBuildTest.debug.xcconfig */, + F23325524BEAF8D24F78AC88 /* Pods-SwiftBuildTest.release.xcconfig */, + ); + name = Pods; + sourceTree = "<group>"; + }; + DE0761E51F2FE611003233AF /* SwiftBuildTest */ = { + isa = PBXGroup; + children = ( + DE0761F61F2FE68D003233AF /* main.swift */, + ); + path = SwiftBuildTest; + sourceTree = "<group>"; + }; + DE2EF06E1F3D07D7003D0CDC /* Immutable */ = { + isa = PBXGroup; + children = ( + DE2EF07E1F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m */, + DE2EF07F1F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.h */, + DE2EF0801F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m */, + DE2EF0811F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.h */, + DE2EF0821F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m */, + DE2EF0831F3D0B6E003D0CDC /* FSTLLRBValueNode+Test.h */, + DE2EF0841F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m */, + ); + name = Immutable; + sourceTree = "<group>"; + }; + DE51B1621F0D48AC0013853F /* Local */ = { + isa = PBXGroup; + children = ( + DE51B16A1F0D48AC0013853F /* FSTLocalStoreTests.h */, + DE51B1701F0D48AC0013853F /* FSTMutationQueueTests.h */, + DE51B1721F0D48AC0013853F /* FSTPersistenceTestHelpers.h */, + DE51B1741F0D48AC0013853F /* FSTQueryCacheTests.h */, + DE51B1771F0D48AC0013853F /* FSTRemoteDocumentCacheTests.h */, + DE51B1631F0D48AC0013853F /* FSTEagerGarbageCollectorTests.m */, + DE51B1651F0D48AC0013853F /* FSTLevelDBLocalStoreTests.m */, + DE51B1671F0D48AC0013853F /* FSTLevelDBQueryCacheTests.m */, + DE51B1691F0D48AC0013853F /* FSTLocalSerializerTests.m */, + DE51B16B1F0D48AC0013853F /* FSTLocalStoreTests.m */, + DE51B16C1F0D48AC0013853F /* FSTMemoryLocalStoreTests.m */, + DE51B16D1F0D48AC0013853F /* FSTMemoryMutationQueueTests.m */, + DE51B16E1F0D48AC0013853F /* FSTMemoryQueryCacheTests.m */, + DE51B16F1F0D48AC0013853F /* FSTMemoryRemoteDocumentCacheTests.m */, + DE51B1711F0D48AC0013853F /* FSTMutationQueueTests.m */, + DE51B1731F0D48AC0013853F /* FSTPersistenceTestHelpers.m */, + DE51B1751F0D48AC0013853F /* FSTQueryCacheTests.m */, + DE51B1761F0D48AC0013853F /* FSTReferenceSetTests.m */, + DE51B1781F0D48AC0013853F /* FSTRemoteDocumentCacheTests.m */, + DE51B1791F0D48AC0013853F /* FSTRemoteDocumentChangeBufferTests.m */, + DE51B1641F0D48AC0013853F /* FSTLevelDBKeyTests.mm */, + DE51B1661F0D48AC0013853F /* FSTLevelDBMutationQueueTests.mm */, + DE51B1681F0D48AC0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm */, + DE51B17A1F0D48AC0013853F /* FSTWriteGroupTests.mm */, + ); + path = Local; + sourceTree = "<group>"; + }; + DE51B17B1F0D48AC0013853F /* Model */ = { + isa = PBXGroup; + children = ( + DE51B17C1F0D48AC0013853F /* FSTDatabaseIDTests.m */, + DE51B17D1F0D48AC0013853F /* FSTDocumentKeyTests.m */, + DE51B17E1F0D48AC0013853F /* FSTDocumentSetTests.m */, + DE51B17F1F0D48AC0013853F /* FSTDocumentTests.m */, + DE51B1801F0D48AC0013853F /* FSTFieldValueTests.m */, + DE51B1811F0D48AC0013853F /* FSTMutationTests.m */, + DE51B1821F0D48AC0013853F /* FSTPathTests.m */, + ); + path = Model; + sourceTree = "<group>"; + }; + DE51B1831F0D48AC0013853F /* API */ = { + isa = PBXGroup; + children = ( + DE51B1841F0D48AC0013853F /* FIRGeoPointTests.m */, + ); + path = API; + sourceTree = "<group>"; + }; + DE51B1851F0D48AC0013853F /* Util */ = { + isa = PBXGroup; + children = ( + 54E9281C1F33950B00C1953E /* FSTEventAccumulator.h */, + 54E9281D1F33950B00C1953E /* FSTEventAccumulator.m */, + 54E9281E1F33950B00C1953E /* FSTIntegrationTestCase.h */, + 54E9281F1F33950B00C1953E /* FSTIntegrationTestCase.m */, + DE51B1861F0D48AC0013853F /* FSTAssertTests.m */, + DE51B1871F0D48AC0013853F /* FSTComparisonTests.m */, + DE51B1881F0D48AC0013853F /* FSTHelpers.h */, + DE51B1891F0D48AC0013853F /* FSTHelpers.m */, + DE51B18A1F0D48AC0013853F /* FSTUtilTests.m */, + 54E9282A1F339CAD00C1953E /* XCTestCase+Await.h */, + 54E9282B1F339CAD00C1953E /* XCTestCase+Await.m */, + ); + path = Util; + sourceTree = "<group>"; + }; + DE51B1931F0D48AC0013853F /* SpecTests */ = { + isa = PBXGroup; + children = ( + DE51B1961F0D48AC0013853F /* FSTMockDatastore.h */, + DE51B1981F0D48AC0013853F /* FSTSpecTests.h */, + DE51B19A1F0D48AC0013853F /* FSTSyncEngineTestDriver.h */, + DE51B1941F0D48AC0013853F /* FSTLevelDBSpecTests.m */, + DE51B1951F0D48AC0013853F /* FSTMemorySpecTests.m */, + DE51B1971F0D48AC0013853F /* FSTMockDatastore.m */, + DE51B1991F0D48AC0013853F /* FSTSpecTests.m */, + DE51B19B1F0D48AC0013853F /* FSTSyncEngineTestDriver.m */, + DE51B19C1F0D48AC0013853F /* json */, + ); + path = SpecTests; + sourceTree = "<group>"; + }; + DE51B19C1F0D48AC0013853F /* json */ = { + isa = PBXGroup; + children = ( + 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */, + 54DA129C1F315EE100DD57A1 /* collection_spec_test.json */, + 54DA129D1F315EE100DD57A1 /* existence_filter_spec_test.json */, + 54DA129E1F315EE100DD57A1 /* limbo_spec_test.json */, + 54DA129F1F315EE100DD57A1 /* limit_spec_test.json */, + 54DA12A01F315EE100DD57A1 /* listen_spec_test.json */, + 54DA12A11F315EE100DD57A1 /* offline_spec_test.json */, + 54DA12A21F315EE100DD57A1 /* orderby_spec_test.json */, + 54DA12A31F315EE100DD57A1 /* persistence_spec_test.json */, + 54DA12A41F315EE100DD57A1 /* resume_token_spec_test.json */, + 54DA12A51F315EE100DD57A1 /* write_spec_test.json */, + DE51B1A71F0D48AC0013853F /* README.md */, + ); + path = json; + sourceTree = "<group>"; + }; + DE51B1A81F0D48AC0013853F /* Core */ = { + isa = PBXGroup; + children = ( + DE51B1AD1F0D48AC0013853F /* FSTSyncEngine+Testing.h */, + DE51B1A91F0D48AC0013853F /* FSTDatabaseInfoTests.m */, + DE51B1AA1F0D48AC0013853F /* FSTEventManagerTests.m */, + DE51B1AB1F0D48AC0013853F /* FSTQueryListenerTests.m */, + DE51B1AC1F0D48AC0013853F /* FSTQueryTests.m */, + DE51B1AE1F0D48AC0013853F /* FSTTargetIDGeneratorTests.m */, + DE51B1AF1F0D48AC0013853F /* FSTTimestampTests.m */, + DE51B1B01F0D48AC0013853F /* FSTViewSnapshotTest.m */, + DE51B1B11F0D48AC0013853F /* FSTViewTests.m */, + ); + path = Core; + sourceTree = "<group>"; + }; + DE51B1B21F0D48AC0013853F /* Remote */ = { + isa = PBXGroup; + children = ( + DE51B1B31F0D48AC0013853F /* FSTDatastoreTests.m */, + DE51B1B41F0D48AC0013853F /* FSTRemoteEventTests.m */, + DE51B1B61F0D48AC0013853F /* FSTSerializerBetaTests.m */, + DE51B1B71F0D48AC0013853F /* FSTStreamTests.m */, + DE51B1B81F0D48AC0013853F /* FSTWatchChange+Testing.h */, + DE51B1B91F0D48AC0013853F /* FSTWatchChange+Testing.m */, + DE51B1BA1F0D48AC0013853F /* FSTWatchChangeTests.m */, + ); + path = Remote; + sourceTree = "<group>"; + }; + DE51B1BB1F0D48AC0013853F /* Integration */ = { + isa = PBXGroup; + children = ( + DE03B3621F215E1600A30B9C /* CAcert.pem */, + DE51B1BC1F0D48AC0013853F /* API */, + DE51B1C41F0D48AC0013853F /* FSTDatastoreTests.m */, + DE51B1C51F0D48AC0013853F /* FSTSmokeTests.m */, + DE51B1C61F0D48AC0013853F /* FSTTransactionTests.m */, + DE51B1C71F0D48AC0013853F /* Util */, + ); + path = Integration; + sourceTree = "<group>"; + }; + DE51B1BC1F0D48AC0013853F /* API */ = { + isa = PBXGroup; + children = ( + DE51B1BD1F0D48AC0013853F /* FIRCursorTests.m */, + DE51B1BE1F0D48AC0013853F /* FIRDatabaseTests.m */, + DE51B1BF1F0D48AC0013853F /* FIRFieldsTests.m */, + DE51B1C01F0D48AC0013853F /* FIRListenerRegistrationTests.m */, + DE51B1C11F0D48AC0013853F /* FIRQueryTests.m */, + DE51B1C21F0D48AC0013853F /* FIRServerTimestampTests.m */, + DE51B1C31F0D48AC0013853F /* FIRTypeTests.m */, + 54DA12B01F315F3800DD57A1 /* FIRValidationTests.m */, + DEFE0F471F1F960A0071599A /* FIRWriteBatchTests.m */, + ); + path = API; + sourceTree = "<group>"; + }; + DE51B1C71F0D48AC0013853F /* Util */ = { + isa = PBXGroup; + children = ( + ); + path = Util; + sourceTree = "<group>"; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 6003F589195388D20070C39A /* Firestore_Example */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6003F5BF195388D20070C39A /* Build configuration list for PBXNativeTarget "Firestore_Example" */; + buildPhases = ( + FAB3416C6DD87D45081EC3E8 /* [CP] Check Pods Manifest.lock */, + 6003F586195388D20070C39A /* Sources */, + 6003F587195388D20070C39A /* Frameworks */, + 6003F588195388D20070C39A /* Resources */, + 7C5123A9C345ECE100DA21BD /* [CP] Embed Pods Frameworks */, + DEB4B96019F51073F0553ABC /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Firestore_Example; + productName = Firestore; + productReference = 6003F58A195388D20070C39A /* Firestore_Example.app */; + productType = "com.apple.product-type.application"; + }; + 6003F5AD195388D20070C39A /* Firestore_Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6003F5C2195388D20070C39A /* Build configuration list for PBXNativeTarget "Firestore_Tests" */; + buildPhases = ( + 8D94B6319191CD7344A4D1B9 /* [CP] Check Pods Manifest.lock */, + 6003F5AA195388D20070C39A /* Sources */, + 6003F5AB195388D20070C39A /* Frameworks */, + 6003F5AC195388D20070C39A /* Resources */, + BB3FE78ABF533BFC38839A0E /* [CP] Embed Pods Frameworks */, + AB3F19DA92555D3399DB07CE /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + 6003F5B4195388D20070C39A /* PBXTargetDependency */, + ); + name = Firestore_Tests; + productName = FirestoreTests; + productReference = 6003F5AE195388D20070C39A /* Firestore_Tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + DE03B2941F2149D600A30B9C /* Firestore_IntegrationTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = DE03B2E61F2149D600A30B9C /* Build configuration list for PBXNativeTarget "Firestore_IntegrationTests" */; + buildPhases = ( + DE03B2971F2149D600A30B9C /* [CP] Check Pods Manifest.lock */, + DE03B2981F2149D600A30B9C /* Sources */, + DE03B2D31F2149D600A30B9C /* Frameworks */, + DE03B2D81F2149D600A30B9C /* Resources */, + DE03B2E41F2149D600A30B9C /* [CP] Embed Pods Frameworks */, + DE03B2E51F2149D600A30B9C /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + DE03B2951F2149D600A30B9C /* PBXTargetDependency */, + ); + name = Firestore_IntegrationTests; + productName = FirestoreTests; + productReference = DE03B2E91F2149D600A30B9C /* Firestore_IntegrationTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + DE0761E31F2FE611003233AF /* SwiftBuildTest */ = { + isa = PBXNativeTarget; + buildConfigurationList = DE0761F51F2FE611003233AF /* Build configuration list for PBXNativeTarget "SwiftBuildTest" */; + buildPhases = ( + 8F34C5E63ACEBD784CF82A45 /* [CP] Check Pods Manifest.lock */, + DE0761E01F2FE611003233AF /* Sources */, + DE0761E11F2FE611003233AF /* Frameworks */, + DE0761E21F2FE611003233AF /* Resources */, + 125BDFEB177CFD41D7A40928 /* [CP] Embed Pods Frameworks */, + 04C27A4B1FAE812E8153B724 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SwiftBuildTest; + productName = SwiftBuildTest; + productReference = DE0761E41F2FE611003233AF /* SwiftBuildTest.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 6003F582195388D10070C39A /* Project object */ = { + isa = PBXProject; + attributes = { + CLASSPREFIX = FIR; + LastSwiftUpdateCheck = 0830; + LastUpgradeCheck = 0720; + ORGANIZATIONNAME = Google; + TargetAttributes = { + 6003F5AD195388D20070C39A = { + DevelopmentTeam = EQHXZ8M8AV; + TestTargetID = 6003F589195388D20070C39A; + }; + DE03B2941F2149D600A30B9C = { + DevelopmentTeam = EQHXZ8M8AV; + }; + DE0761E31F2FE611003233AF = { + CreatedOnToolsVersion = 8.3.3; + DevelopmentTeam = EQHXZ8M8AV; + ProvisioningStyle = Automatic; + }; + DE29E7F51F2174B000909613 = { + CreatedOnToolsVersion = 9.0; + DevelopmentTeam = EQHXZ8M8AV; + }; + }; + }; + buildConfigurationList = 6003F585195388D10070C39A /* Build configuration list for PBXProject "Firestore" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 6003F581195388D10070C39A; + productRefGroup = 6003F58B195388D20070C39A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 6003F589195388D20070C39A /* Firestore_Example */, + 6003F5AD195388D20070C39A /* Firestore_Tests */, + DE03B2941F2149D600A30B9C /* Firestore_IntegrationTests */, + DE29E7F51F2174B000909613 /* AllTests */, + DE0761E31F2FE611003233AF /* SwiftBuildTest */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 6003F588195388D20070C39A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 873B8AEB1B1F5CCA007FD442 /* Main.storyboard in Resources */, + 71719F9F1E33DC2100824A3D /* LaunchScreen.storyboard in Resources */, + 6003F5A9195388D20070C39A /* Images.xcassets in Resources */, + 6003F598195388D20070C39A /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6003F5AC195388D20070C39A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3B843E4C1F3A182900548890 /* remote_store_spec_test.json in Resources */, + 54DA12A81F315EE100DD57A1 /* limbo_spec_test.json in Resources */, + 54DA12AA1F315EE100DD57A1 /* listen_spec_test.json in Resources */, + 54DA12A61F315EE100DD57A1 /* collection_spec_test.json in Resources */, + 54DA12AE1F315EE100DD57A1 /* resume_token_spec_test.json in Resources */, + 6003F5BA195388D20070C39A /* InfoPlist.strings in Resources */, + 54DA12AF1F315EE100DD57A1 /* write_spec_test.json in Resources */, + 54DA12AD1F315EE100DD57A1 /* persistence_spec_test.json in Resources */, + 54DA12AB1F315EE100DD57A1 /* offline_spec_test.json in Resources */, + 54DA12A71F315EE100DD57A1 /* existence_filter_spec_test.json in Resources */, + 54DA12AC1F315EE100DD57A1 /* orderby_spec_test.json in Resources */, + 54DA12A91F315EE100DD57A1 /* limit_spec_test.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DE03B2D81F2149D600A30B9C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DE03B2DD1F2149D600A30B9C /* InfoPlist.strings in Resources */, + DE03B3631F215E1A00A30B9C /* CAcert.pem in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DE0761E21F2FE611003233AF /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 04C27A4B1FAE812E8153B724 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 125BDFEB177CFD41D7A40928 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/BoringSSL/openssl.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseCommunity/FirebaseCommunity.framework", + "${BUILT_PRODUCTS_DIR}/Firestore/Firestore.framework", + "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", + "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", + "${BUILT_PRODUCTS_DIR}/Protobuf/Protobuf.framework", + "${BUILT_PRODUCTS_DIR}/gRPC/GRPCClient.framework", + "${BUILT_PRODUCTS_DIR}/gRPC-Core/grpc.framework", + "${BUILT_PRODUCTS_DIR}/gRPC-ProtoRPC/ProtoRPC.framework", + "${BUILT_PRODUCTS_DIR}/gRPC-RxLibrary/RxLibrary.framework", + "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework", + "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCommunity.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Firestore.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Protobuf.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRPCClient.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/grpc.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ProtoRPC.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxLibrary.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 7C5123A9C345ECE100DA21BD /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example/Pods-Firestore_Example-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/BoringSSL/openssl.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseCommunity/FirebaseCommunity.framework", + "${BUILT_PRODUCTS_DIR}/Firestore/Firestore.framework", + "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", + "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", + "${BUILT_PRODUCTS_DIR}/Protobuf/Protobuf.framework", + "${BUILT_PRODUCTS_DIR}/gRPC/GRPCClient.framework", + "${BUILT_PRODUCTS_DIR}/gRPC-Core/grpc.framework", + "${BUILT_PRODUCTS_DIR}/gRPC-ProtoRPC/ProtoRPC.framework", + "${BUILT_PRODUCTS_DIR}/gRPC-RxLibrary/RxLibrary.framework", + "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework", + "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCommunity.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Firestore.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Protobuf.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRPCClient.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/grpc.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ProtoRPC.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxLibrary.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example/Pods-Firestore_Example-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 8D94B6319191CD7344A4D1B9 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Firestore_Tests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 8F34C5E63ACEBD784CF82A45 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-SwiftBuildTest-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + AB3F19DA92555D3399DB07CE /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + BB3FE78ABF533BFC38839A0E /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework", + "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OCMock.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + DE03B2971F2149D600A30B9C /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Firestore_IntegrationTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + DE03B2E41F2149D600A30B9C /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OCMock.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + DE03B2E51F2149D600A30B9C /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + DEB4B96019F51073F0553ABC /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example/Pods-Firestore_Example-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + FAB3416C6DD87D45081EC3E8 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Firestore_Example-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 6003F586195388D20070C39A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6003F59E195388D20070C39A /* FIRAppDelegate.m in Sources */, + 6003F5A7195388D20070C39A /* FIRViewController.m in Sources */, + 6003F59A195388D20070C39A /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6003F5AA195388D20070C39A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DE2EF0881F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m in Sources */, + DE51B1FD1F0D492C0013853F /* FSTSpecTests.m in Sources */, + DE51B2001F0D493A0013853F /* FSTComparisonTests.m in Sources */, + DE51B1CC1F0D48C00013853F /* FIRGeoPointTests.m in Sources */, + DE51B1E11F0D490D0013853F /* FSTMemoryRemoteDocumentCacheTests.m in Sources */, + DE51B1FF1F0D493A0013853F /* FSTAssertTests.m in Sources */, + DE51B1D31F0D48CD0013853F /* FSTViewSnapshotTest.m in Sources */, + DE51B2021F0D493E0013853F /* FSTUtilTests.m in Sources */, + DE51B1F91F0D491F0013853F /* FSTWatchChangeTests.m in Sources */, + DE51B1F81F0D491F0013853F /* FSTWatchChange+Testing.m in Sources */, + DE51B1EB1F0D490D0013853F /* FSTWriteGroupTests.mm in Sources */, + DE51B2011F0D493E0013853F /* FSTHelpers.m in Sources */, + DE51B1F61F0D491B0013853F /* FSTSerializerBetaTests.m in Sources */, + DE51B1F01F0D49140013853F /* FSTFieldValueTests.m in Sources */, + DE2EF0861F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m in Sources */, + DE51B1DE1F0D490D0013853F /* FSTMemoryLocalStoreTests.m in Sources */, + DE51B1EC1F0D49140013853F /* FSTDatabaseIDTests.m in Sources */, + 54E928221F33952900C1953E /* FSTIntegrationTestCase.m in Sources */, + DE51B1ED1F0D49140013853F /* FSTDocumentKeyTests.m in Sources */, + DE51B1D41F0D48CD0013853F /* FSTViewTests.m in Sources */, + DE51B1F41F0D491B0013853F /* FSTRemoteEventTests.m in Sources */, + 54E928241F33953300C1953E /* FSTEventAccumulator.m in Sources */, + DE51B1D11F0D48CD0013853F /* FSTTargetIDGeneratorTests.m in Sources */, + DE51B1EF1F0D49140013853F /* FSTDocumentTests.m in Sources */, + DE51B1DC1F0D490D0013853F /* FSTLocalSerializerTests.m in Sources */, + DE51B1E71F0D490D0013853F /* FSTRemoteDocumentChangeBufferTests.m in Sources */, + DE51B1E51F0D490D0013853F /* FSTReferenceSetTests.m in Sources */, + DE51B1EA1F0D490D0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm in Sources */, + DE51B1D21F0D48CD0013853F /* FSTTimestampTests.m in Sources */, + DE51B1EE1F0D49140013853F /* FSTDocumentSetTests.m in Sources */, + DE2EF0851F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m in Sources */, + DE51B1F11F0D49140013853F /* FSTMutationTests.m in Sources */, + DE51B1FB1F0D492C0013853F /* FSTMemorySpecTests.m in Sources */, + DE51B1DB1F0D490D0013853F /* FSTLevelDBQueryCacheTests.m in Sources */, + 54E9282C1F339CAD00C1953E /* XCTestCase+Await.m in Sources */, + DE51B1DF1F0D490D0013853F /* FSTMemoryMutationQueueTests.m in Sources */, + DE51B1F31F0D491B0013853F /* FSTDatastoreTests.m in Sources */, + DE51B1D01F0D48CD0013853F /* FSTQueryTests.m in Sources */, + DE2EF0871F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m in Sources */, + DE51B1E01F0D490D0013853F /* FSTMemoryQueryCacheTests.m in Sources */, + DE51B1E91F0D490D0013853F /* FSTLevelDBMutationQueueTests.mm in Sources */, + DE51B1E61F0D490D0013853F /* FSTRemoteDocumentCacheTests.m in Sources */, + DE51B1D91F0D490D0013853F /* FSTEagerGarbageCollectorTests.m in Sources */, + DE51B1F71F0D491B0013853F /* FSTStreamTests.m in Sources */, + DE51B1E21F0D490D0013853F /* FSTMutationQueueTests.m in Sources */, + DE51B1E81F0D490D0013853F /* FSTLevelDBKeyTests.mm in Sources */, + DE51B1E31F0D490D0013853F /* FSTPersistenceTestHelpers.m in Sources */, + DE51B1CF1F0D48CD0013853F /* FSTQueryListenerTests.m in Sources */, + DE51B1DA1F0D490D0013853F /* FSTLevelDBLocalStoreTests.m in Sources */, + DE51B1FA1F0D492C0013853F /* FSTLevelDBSpecTests.m in Sources */, + DE51B1FE1F0D492C0013853F /* FSTSyncEngineTestDriver.m in Sources */, + DE51B1FC1F0D492C0013853F /* FSTMockDatastore.m in Sources */, + DE51B1CE1F0D48CD0013853F /* FSTEventManagerTests.m in Sources */, + DE51B1E41F0D490D0013853F /* FSTQueryCacheTests.m in Sources */, + DE51B1CD1F0D48CD0013853F /* FSTDatabaseInfoTests.m in Sources */, + DE51B1F21F0D49140013853F /* FSTPathTests.m in Sources */, + DE51B1DD1F0D490D0013853F /* FSTLocalStoreTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DE03B2981F2149D600A30B9C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DE03B2EE1F214BAA00A30B9C /* FIRWriteBatchTests.m in Sources */, + DE03B2F01F214BAA00A30B9C /* FIRDatabaseTests.m in Sources */, + 54E928231F33952D00C1953E /* FSTIntegrationTestCase.m in Sources */, + DE03B2F41F214BAA00A30B9C /* FIRServerTimestampTests.m in Sources */, + DE03B2F11F214BAA00A30B9C /* FIRFieldsTests.m in Sources */, + 54E9282D1F339CAD00C1953E /* XCTestCase+Await.m in Sources */, + DE03B2EC1F214BA200A30B9C /* FSTDatastoreTests.m in Sources */, + 54E928251F33953400C1953E /* FSTEventAccumulator.m in Sources */, + DE03B2ED1F214BA200A30B9C /* FSTSmokeTests.m in Sources */, + DE03B2F31F214BAA00A30B9C /* FIRQueryTests.m in Sources */, + DE03B35E1F21586C00A30B9C /* FSTHelpers.m in Sources */, + DE03B2F51F214BAA00A30B9C /* FIRTypeTests.m in Sources */, + DE03B2EF1F214BAA00A30B9C /* FIRCursorTests.m in Sources */, + DE03B2F21F214BAA00A30B9C /* FIRListenerRegistrationTests.m in Sources */, + DE03B2C91F2149D600A30B9C /* FSTTransactionTests.m in Sources */, + 54DA12B11F315F3800DD57A1 /* FIRValidationTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DE0761E01F2FE611003233AF /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DE0761F81F2FE68D003233AF /* main.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 6003F5B4195388D20070C39A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6003F589195388D20070C39A /* Firestore_Example */; + targetProxy = 6003F5B3195388D20070C39A /* PBXContainerItemProxy */; + }; + DE03B2951F2149D600A30B9C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6003F589195388D20070C39A /* Firestore_Example */; + targetProxy = DE03B2961F2149D600A30B9C /* PBXContainerItemProxy */; + }; + DE0761FA1F2FEE7E003233AF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DE0761E31F2FE611003233AF /* SwiftBuildTest */; + targetProxy = DE0761F91F2FEE7E003233AF /* PBXContainerItemProxy */; + }; + DE29E7FA1F2174DD00909613 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6003F5AD195388D20070C39A /* Firestore_Tests */; + targetProxy = DE29E7F91F2174DD00909613 /* PBXContainerItemProxy */; + }; + DE29E7FC1F2174DD00909613 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DE03B2941F2149D600A30B9C /* Firestore_IntegrationTests */; + targetProxy = DE29E7FB1F2174DD00909613 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 6003F596195388D20070C39A /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 6003F597195388D20070C39A /* en */, + ); + name = InfoPlist.strings; + sourceTree = "<group>"; + }; + 6003F5B8195388D20070C39A /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 6003F5B9195388D20070C39A /* en */, + ); + name = InfoPlist.strings; + sourceTree = "<group>"; + }; + 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 71719F9E1E33DC2100824A3D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = "<group>"; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 6003F5BD195388D20070C39A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 6003F5BE195388D20070C39A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 6003F5C0195388D20070C39A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9EF477AD4B2B643FD320867A /* Pods-Firestore_Example.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = ""; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/Firebase/Firebase/Firebase\"", + "\"${PODS_ROOT}/leveldb-library/\"", + "\"${PODS_ROOT}/leveldb-library/include\"", + ); + INFOPLIST_FILE = "Firestore/Firestore-Info.plist"; + MODULE_NAME = ExampleApp; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = app; + }; + name = Debug; + }; + 6003F5C1195388D20070C39A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4EBC5F5ABE1FD097EFE5E224 /* Pods-Firestore_Example.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = ""; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/Firebase/Firebase/Firebase\"", + "\"${PODS_ROOT}/leveldb-library/\"", + "\"${PODS_ROOT}/leveldb-library/include\"", + ); + INFOPLIST_FILE = "Firestore/Firestore-Info.plist"; + MODULE_NAME = ExampleApp; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; + 6003F5C3195388D20070C39A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CE00BABB5A3AAB44A4C209E2 /* Pods-Firestore_Tests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + DEVELOPMENT_TEAM = EQHXZ8M8AV; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = ""; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1", + ); + HEADER_SEARCH_PATHS = ( + "\"$(inherited)\"", + "\"${PODS_ROOT}/../../Source\"", + "\"${PODS_ROOT}/../../Source/API\"", + "\"${PODS_ROOT}/../../Source/Core\"", + "\"${PODS_ROOT}/../../Source/Remote\"", + "\"${PODS_ROOT}/../../Source/Model\"", + "\"${PODS_ROOT}/../../third_party\"", + "\"${PODS_ROOT}/../../third_party/Immutable\"", + "\"${PODS_ROOT}/../../\"", + "\"${PODS_ROOT}/../../Protos/objc/firestore/local\"", + "\"${PODS_ROOT}/../../Protos/objc/google/firestore/v1beta1\"", + "\"${PODS_ROOT}/../../Protos/objc/google/api\"", + "\"${PODS_ROOT}/../../Protos/objc/google/rpc\"", + "\"${PODS_ROOT}/../../Protos/objc/google/type\"", + ); + INFOPLIST_FILE = "Tests/Tests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Firestore_Example.app/Firestore_Example"; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + 6003F5C4195388D20070C39A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 04DF37A117F88A9891379ED6 /* Pods-Firestore_Tests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + DEVELOPMENT_TEAM = EQHXZ8M8AV; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = ""; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1", + ); + HEADER_SEARCH_PATHS = ( + "\"$(inherited)\"", + "\"${PODS_ROOT}/../../Source\"", + "\"${PODS_ROOT}/../../Source/API\"", + "\"${PODS_ROOT}/../../Source/Core\"", + "\"${PODS_ROOT}/../../Source/Remote\"", + "\"${PODS_ROOT}/../../Source/Model\"", + "\"${PODS_ROOT}/../../third_party\"", + "\"${PODS_ROOT}/../../third_party/Immutable\"", + "\"${PODS_ROOT}/../../Protos/objc/firebase/datastore/clients/proto\"", + "\"${PODS_ROOT}/../../Protos/objc/google/firestore/v1beta1\"", + "\"${PODS_ROOT}/../../Protos/objc/google/api\"", + "\"${PODS_ROOT}/../../Protos/objc/google/rpc\"", + "\"${PODS_ROOT}/../../Protos/objc/google/type\"", + "\"${PODS_ROOT}/../../\"", + ); + INFOPLIST_FILE = "Tests/Tests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Firestore_Example.app/Firestore_Example"; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; + DE03B2E71F2149D600A30B9C /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9D52E67EE96AA7E5D6F69748 /* Pods-Firestore_IntegrationTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + DEVELOPMENT_TEAM = EQHXZ8M8AV; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = ""; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1", + ); + HEADER_SEARCH_PATHS = ( + "\"$(inherited)\"", + "\"${PODS_ROOT}/../../Source\"", + "\"${PODS_ROOT}/../../Source/API\"", + "\"${PODS_ROOT}/../../Source/Core\"", + "\"${PODS_ROOT}/../../Source/Remote\"", + "\"${PODS_ROOT}/../../Source/Model\"", + "\"${PODS_ROOT}/../../third_party\"", + "\"${PODS_ROOT}/../../third_party/Immutable\"", + "\"${PODS_ROOT}/../../Protos/objc/firestore/local\"", + "\"${PODS_ROOT}/../../Protos/objc/google/firestore/v1beta1\"", + "\"${PODS_ROOT}/../../Protos/objc/google/api\"", + "\"${PODS_ROOT}/../../Protos/objc/google/rpc\"", + "\"${PODS_ROOT}/../../Protos/objc/google/type\"", + "\"${PODS_ROOT}/../../\"", + ); + INFOPLIST_FILE = "Tests/Tests-Info.plist"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-l\"c++\"", + "-framework", + "\"OCMock\"", + "-framework", + "\"leveldb\"", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Firestore_Example.app/Firestore_Example"; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + DE03B2E81F2149D600A30B9C /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DB17FEDFB80770611A935A60 /* Pods-Firestore_IntegrationTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + DEVELOPMENT_TEAM = EQHXZ8M8AV; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = ""; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1", + ); + HEADER_SEARCH_PATHS = ( + "\"$(inherited)\"", + "\"${PODS_ROOT}/../../Source\"", + "\"${PODS_ROOT}/../../Source/API\"", + "\"${PODS_ROOT}/../../Source/Core\"", + "\"${PODS_ROOT}/../../Source/Remote\"", + "\"${PODS_ROOT}/../../Source/Model\"", + "\"${PODS_ROOT}/../../third_party\"", + "\"${PODS_ROOT}/../../third_party/Immutable\"", + "\"${PODS_ROOT}/../../Protos/objc/firestore/local\"", + "\"${PODS_ROOT}/../../Protos/objc/google/firestore/v1beta1\"", + "\"${PODS_ROOT}/../../Protos/objc/google/api\"", + "\"${PODS_ROOT}/../../Protos/objc/google/rpc\"", + "\"${PODS_ROOT}/../../Protos/objc/google/type\"", + "\"${PODS_ROOT}/../../\"", + ); + INFOPLIST_FILE = "Tests/Tests-Info.plist"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-l\"c++\"", + "-framework", + "\"OCMock\"", + "-framework", + "\"leveldb\"", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Firestore_Example.app/Firestore_Example"; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; + DE0761F31F2FE611003233AF /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 42491D7DC8C8CD245CC22B93 /* Pods-SwiftBuildTest.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = EQHXZ8M8AV; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = "Firestore/Firestore-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.google.SwiftBuildTest; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + DE0761F41F2FE611003233AF /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F23325524BEAF8D24F78AC88 /* Pods-SwiftBuildTest.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = EQHXZ8M8AV; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = "Firestore/Firestore-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = com.google.SwiftBuildTest; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + DE29E7F61F2174B000909613 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEVELOPMENT_TEAM = EQHXZ8M8AV; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + DE29E7F71F2174B000909613 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEVELOPMENT_TEAM = EQHXZ8M8AV; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 6003F585195388D10070C39A /* Build configuration list for PBXProject "Firestore" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6003F5BD195388D20070C39A /* Debug */, + 6003F5BE195388D20070C39A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6003F5BF195388D20070C39A /* Build configuration list for PBXNativeTarget "Firestore_Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6003F5C0195388D20070C39A /* Debug */, + 6003F5C1195388D20070C39A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6003F5C2195388D20070C39A /* Build configuration list for PBXNativeTarget "Firestore_Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6003F5C3195388D20070C39A /* Debug */, + 6003F5C4195388D20070C39A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DE03B2E61F2149D600A30B9C /* Build configuration list for PBXNativeTarget "Firestore_IntegrationTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DE03B2E71F2149D600A30B9C /* Debug */, + DE03B2E81F2149D600A30B9C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DE0761F51F2FE611003233AF /* Build configuration list for PBXNativeTarget "SwiftBuildTest" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DE0761F31F2FE611003233AF /* Debug */, + DE0761F41F2FE611003233AF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DE29E7F81F2174B000909613 /* Build configuration list for PBXAggregateTarget "AllTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DE29E7F61F2174B000909613 /* Debug */, + DE29E7F71F2174B000909613 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 6003F582195388D10070C39A /* Project object */; +} diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme new file mode 100644 index 0000000..aacb70e --- /dev/null +++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme @@ -0,0 +1,111 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "0900" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "DE29E7F51F2174B000909613" + BuildableName = "AllTests" + BlueprintName = "AllTests" + ReferencedContainer = "container:Firestore.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" + shouldUseLaunchSchemeArgsEnv = "YES"> + <Testables> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "DE03B2941F2149D600A30B9C" + BuildableName = "Firestore_IntegrationTests.xctest" + BlueprintName = "Firestore_IntegrationTests" + ReferencedContainer = "container:Firestore.xcodeproj"> + </BuildableReference> + </TestableReference> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "6003F5AD195388D20070C39A" + BuildableName = "Firestore_Tests.xctest" + BlueprintName = "Firestore_Tests" + ReferencedContainer = "container:Firestore.xcodeproj"> + </BuildableReference> + </TestableReference> + </Testables> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "DE29E7F51F2174B000909613" + BuildableName = "AllTests" + BlueprintName = "AllTests" + ReferencedContainer = "container:Firestore.xcodeproj"> + </BuildableReference> + </MacroExpansion> + <AdditionalOptions> + </AdditionalOptions> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "DE29E7F51F2174B000909613" + BuildableName = "AllTests" + BlueprintName = "AllTests" + ReferencedContainer = "container:Firestore.xcodeproj"> + </BuildableReference> + </MacroExpansion> + <AdditionalOptions> + </AdditionalOptions> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "DE29E7F51F2174B000909613" + BuildableName = "AllTests" + BlueprintName = "AllTests" + ReferencedContainer = "container:Firestore.xcodeproj"> + </BuildableReference> + </MacroExpansion> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore-Example.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore-Example.xcscheme new file mode 100644 index 0000000..a8538f5 --- /dev/null +++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore-Example.xcscheme @@ -0,0 +1,113 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "0720" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "6003F589195388D20070C39A" + BuildableName = "Firestore_Example.app" + BlueprintName = "Firestore_Example" + ReferencedContainer = "container:Firestore.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" + shouldUseLaunchSchemeArgsEnv = "YES"> + <Testables> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "6003F5AD195388D20070C39A" + BuildableName = "Firestore_Tests.xctest" + BlueprintName = "Firestore_Tests" + ReferencedContainer = "container:Firestore.xcodeproj"> + </BuildableReference> + </TestableReference> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "DE03B2941F2149D600A30B9C" + BuildableName = "Firestore_IntegrationTests.xctest" + BlueprintName = "Firestore_IntegrationTests" + ReferencedContainer = "container:Firestore.xcodeproj"> + </BuildableReference> + </TestableReference> + </Testables> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "6003F589195388D20070C39A" + BuildableName = "Firestore_Example.app" + BlueprintName = "Firestore_Example" + ReferencedContainer = "container:Firestore.xcodeproj"> + </BuildableReference> + </MacroExpansion> + <AdditionalOptions> + </AdditionalOptions> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + <BuildableProductRunnable + runnableDebuggingMode = "0"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "6003F589195388D20070C39A" + BuildableName = "Firestore_Example.app" + BlueprintName = "Firestore_Example" + ReferencedContainer = "container:Firestore.xcodeproj"> + </BuildableReference> + </BuildableProductRunnable> + <AdditionalOptions> + </AdditionalOptions> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + <BuildableProductRunnable + runnableDebuggingMode = "0"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "6003F589195388D20070C39A" + BuildableName = "Firestore_Example.app" + BlueprintName = "Firestore_Example" + ReferencedContainer = "container:Firestore.xcodeproj"> + </BuildableReference> + </BuildableProductRunnable> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_IntegrationTests.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_IntegrationTests.xcscheme new file mode 100644 index 0000000..ccd099f --- /dev/null +++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_IntegrationTests.xcscheme @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "0900" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForRunning = "YES" + buildForTesting = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "DE03B2941F2149D600A30B9C" + BuildableName = "Firestore_IntegrationTests.xctest" + BlueprintName = "Firestore_IntegrationTests" + ReferencedContainer = "container:Firestore.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" + shouldUseLaunchSchemeArgsEnv = "YES"> + <Testables> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "DE03B2941F2149D600A30B9C" + BuildableName = "Firestore_IntegrationTests.xctest" + BlueprintName = "Firestore_IntegrationTests" + ReferencedContainer = "container:Firestore.xcodeproj"> + </BuildableReference> + </TestableReference> + </Testables> + <AdditionalOptions> + </AdditionalOptions> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + <AdditionalOptions> + </AdditionalOptions> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Tests.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Tests.xcscheme new file mode 100644 index 0000000..920e1f3 --- /dev/null +++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Tests.xcscheme @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "0900" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForRunning = "YES" + buildForTesting = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "6003F5AD195388D20070C39A" + BuildableName = "Firestore_Tests.xctest" + BlueprintName = "Firestore_Tests" + ReferencedContainer = "container:Firestore.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" + shouldUseLaunchSchemeArgsEnv = "YES"> + <Testables> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "6003F5AD195388D20070C39A" + BuildableName = "Firestore_Tests.xctest" + BlueprintName = "Firestore_Tests" + ReferencedContainer = "container:Firestore.xcodeproj"> + </BuildableReference> + </TestableReference> + </Testables> + <AdditionalOptions> + </AdditionalOptions> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + <AdditionalOptions> + </AdditionalOptions> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/SwiftBuildTest.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/SwiftBuildTest.xcscheme new file mode 100644 index 0000000..112a0d0 --- /dev/null +++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/SwiftBuildTest.xcscheme @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "0830" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "DE0761E31F2FE611003233AF" + BuildableName = "SwiftBuildTest.app" + BlueprintName = "SwiftBuildTest" + ReferencedContainer = "container:Firestore.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES"> + <Testables> + </Testables> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "DE0761E31F2FE611003233AF" + BuildableName = "SwiftBuildTest.app" + BlueprintName = "SwiftBuildTest" + ReferencedContainer = "container:Firestore.xcodeproj"> + </BuildableReference> + </MacroExpansion> + <AdditionalOptions> + </AdditionalOptions> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + <BuildableProductRunnable + runnableDebuggingMode = "0"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "DE0761E31F2FE611003233AF" + BuildableName = "SwiftBuildTest.app" + BlueprintName = "SwiftBuildTest" + ReferencedContainer = "container:Firestore.xcodeproj"> + </BuildableReference> + </BuildableProductRunnable> + <AdditionalOptions> + </AdditionalOptions> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + <BuildableProductRunnable + runnableDebuggingMode = "0"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "DE0761E31F2FE611003233AF" + BuildableName = "SwiftBuildTest.app" + BlueprintName = "SwiftBuildTest" + ReferencedContainer = "container:Firestore.xcodeproj"> + </BuildableReference> + </BuildableProductRunnable> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/Firestore/Example/Firestore/Base.lproj/LaunchScreen.storyboard b/Firestore/Example/Firestore/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..66a7681 --- /dev/null +++ b/Firestore/Example/Firestore/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM"> + <dependencies> + <deployment identifier="iOS"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/> + </dependencies> + <scenes> + <!--View Controller--> + <scene sceneID="EHf-IW-A2E"> + <objects> + <viewController id="01J-lp-oVM" sceneMemberID="viewController"> + <layoutGuides> + <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/> + <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/> + </layoutGuides> + <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3"> + <rect key="frame" x="0.0" y="0.0" width="600" height="600"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/> + </view> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="53" y="375"/> + </scene> + </scenes> +</document> diff --git a/Firestore/Example/Firestore/Base.lproj/Main.storyboard b/Firestore/Example/Firestore/Base.lproj/Main.storyboard new file mode 100644 index 0000000..d164a23 --- /dev/null +++ b/Firestore/Example/Firestore/Base.lproj/Main.storyboard @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="7706" systemVersion="14D136" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="whP-gf-Uak"> + <dependencies> + <deployment identifier="iOS"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="7703"/> + </dependencies> + <scenes> + <!--View Controller--> + <scene sceneID="wQg-tq-qST"> + <objects> + <viewController id="whP-gf-Uak" customClass="FIRViewController" sceneMemberID="viewController"> + <layoutGuides> + <viewControllerLayoutGuide type="top" id="uEw-UM-LJ8"/> + <viewControllerLayoutGuide type="bottom" id="Mvr-aV-6Um"/> + </layoutGuides> + <view key="view" contentMode="scaleToFill" id="TpU-gO-2f1"> + <rect key="frame" x="0.0" y="0.0" width="600" height="600"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + </view> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="tc2-Qw-aMS" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="305" y="433"/> + </scene> + </scenes> +</document> diff --git a/Firestore/Example/Firestore/FIRAppDelegate.h b/Firestore/Example/Firestore/FIRAppDelegate.h new file mode 100644 index 0000000..1eb5040 --- /dev/null +++ b/Firestore/Example/Firestore/FIRAppDelegate.h @@ -0,0 +1,23 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import UIKit; + +@interface FIRAppDelegate : UIResponder <UIApplicationDelegate> + +@property(strong, nonatomic) UIWindow *window; + +@end diff --git a/Firestore/Example/Firestore/FIRAppDelegate.m b/Firestore/Example/Firestore/FIRAppDelegate.m new file mode 100644 index 0000000..12ca249 --- /dev/null +++ b/Firestore/Example/Firestore/FIRAppDelegate.m @@ -0,0 +1,57 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FIRAppDelegate.h" + +@implementation FIRAppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + return YES; +} + +- (void)applicationWillResignActive:(UIApplication *)application { + // Sent when the application is about to move from active to inactive state. This can occur for + // certain types of temporary interruptions (such as an incoming phone call or SMS message) or + // when the user quits the application and it begins the transition to the background state. Use + // this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. + // Games should use this method to pause the game. +} + +- (void)applicationDidEnterBackground:(UIApplication *)application { + // Use this method to release shared resources, save user data, invalidate timers, and store + // enough application state information to restore your application to its current state in case + // it is terminated later. If your application supports background execution, this method is + // called instead of applicationWillTerminate: when the user quits. +} + +- (void)applicationWillEnterForeground:(UIApplication *)application { + // Called as part of the transition from the background to the inactive state; here you can undo + // many of the changes made on entering the background. +} + +- (void)applicationDidBecomeActive:(UIApplication *)application { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If + // the application was previously in the background, optionally refresh the user interface. +} + +- (void)applicationWillTerminate:(UIApplication *)application { + // Called when the application is about to terminate. Save data if appropriate. See also + // applicationDidEnterBackground:. +} + +@end diff --git a/Firestore/Example/Firestore/FIRViewController.h b/Firestore/Example/Firestore/FIRViewController.h new file mode 100644 index 0000000..64b4b74 --- /dev/null +++ b/Firestore/Example/Firestore/FIRViewController.h @@ -0,0 +1,21 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import UIKit; + +@interface FIRViewController : UIViewController + +@end diff --git a/Firestore/Example/Firestore/FIRViewController.m b/Firestore/Example/Firestore/FIRViewController.m new file mode 100644 index 0000000..cdad545 --- /dev/null +++ b/Firestore/Example/Firestore/FIRViewController.m @@ -0,0 +1,35 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FIRViewController.h" + +@interface FIRViewController () + +@end + +@implementation FIRViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. +} + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +@end diff --git a/Firestore/Example/Firestore/Firestore-Info.plist b/Firestore/Example/Firestore/Firestore-Info.plist new file mode 100644 index 0000000..7576a0d --- /dev/null +++ b/Firestore/Example/Firestore/Firestore-Info.plist @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleDisplayName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>1.0</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>1.0</string> + <key>LSRequiresIPhoneOS</key> + <true/> + <key>UILaunchStoryboardName</key> + <string>LaunchScreen</string> + <key>UIMainStoryboardFile</key> + <string>Main</string> + <key>UIRequiredDeviceCapabilities</key> + <array> + <string>armv7</string> + </array> + <key>UISupportedInterfaceOrientations</key> + <array> + <string>UIInterfaceOrientationPortrait</string> + <string>UIInterfaceOrientationLandscapeLeft</string> + <string>UIInterfaceOrientationLandscapeRight</string> + </array> + <key>UISupportedInterfaceOrientations~ipad</key> + <array> + <string>UIInterfaceOrientationPortrait</string> + <string>UIInterfaceOrientationPortraitUpsideDown</string> + <string>UIInterfaceOrientationLandscapeLeft</string> + <string>UIInterfaceOrientationLandscapeRight</string> + </array> +</dict> +</plist> diff --git a/Firestore/Example/Firestore/Images.xcassets/AppIcon.appiconset/Contents.json b/Firestore/Example/Firestore/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d7070bc --- /dev/null +++ b/Firestore/Example/Firestore/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,93 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/Firestore/Example/Firestore/en.lproj/InfoPlist.strings b/Firestore/Example/Firestore/en.lproj/InfoPlist.strings new file mode 100644 index 0000000..477b28f --- /dev/null +++ b/Firestore/Example/Firestore/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/Firestore/Example/Firestore/main.m b/Firestore/Example/Firestore/main.m new file mode 100644 index 0000000..724fccf --- /dev/null +++ b/Firestore/Example/Firestore/main.m @@ -0,0 +1,24 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import UIKit; +#import "FIRAppDelegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([FIRAppDelegate class])); + } +} diff --git a/Firestore/Example/Podfile b/Firestore/Example/Podfile new file mode 100644 index 0000000..4008af7 --- /dev/null +++ b/Firestore/Example/Podfile @@ -0,0 +1,22 @@ +use_frameworks! +platform :ios, '8.0' + +target 'Firestore_Example' do + pod 'FirebaseCommunity/Core', :path => '../../' + pod 'Firestore', :path => '../' + + target 'Firestore_Tests' do + inherit! :search_paths + pod 'OCMock' + pod 'leveldb-library' + end + + target 'Firestore_IntegrationTests' do + inherit! :search_paths + pod 'OCMock' + end +end + +target 'SwiftBuildTest' do + pod 'Firestore', :path => '../' +end diff --git a/Firestore/Example/SwiftBuildTest/main.swift b/Firestore/Example/SwiftBuildTest/main.swift new file mode 100644 index 0000000..ff6c0dd --- /dev/null +++ b/Firestore/Example/SwiftBuildTest/main.swift @@ -0,0 +1,284 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +import Firestore + +func main() { + let db = initializeDb(); + + let (collectionRef, documentRef) = makeRefs(database: db); + + let query = makeQuery(collection: collectionRef); + + writeDocument(at: documentRef); + + addDocument(to: collectionRef); + + readDocument(at: documentRef); + + readDocuments(matching: query); + + listenToDocument(at: documentRef); + + listenToDocuments(matching: query); + + types(); +} + +func initializeDb() -> Firestore { + + // Initialize with ProjectID. + let firestore = Firestore.firestore() + + // Apply settings + let settings = FirestoreSettings() + settings.host = "localhost" + settings.isPersistenceEnabled = true + firestore.settings = settings + + return firestore; +} + +func makeRefs(database db: Firestore) -> (CollectionReference, DocumentReference) { + + var collectionRef = db.collection("my-collection") + + var documentRef: DocumentReference; + documentRef = collectionRef.document("my-doc") + // or + documentRef = db.document("my-collection/my-doc") + + // deeper collection (my-collection/my-doc/some/deep/collection) + collectionRef = documentRef.collection("some/deep/collection") + + // parent doc (my-collection/my-doc/some/deep) + documentRef = collectionRef.parent! + + // print paths. + print("Collection: \(collectionRef.path), document: \(documentRef.path)") + + return (collectionRef, documentRef); +} + +func makeQuery(collection collectionRef: CollectionReference) -> Query { + + let query = collectionRef.whereField(FieldPath(["name"]), isEqualTo: "Fred") + .whereField("age", isGreaterThanOrEqualTo: 24) + .whereField(FieldPath.documentID(), isEqualTo: "fred") + .order(by: FieldPath(["age"])) + .order(by: "name", descending: true) + .limit(to: 10) + + return query; +} + +func writeDocument(at docRef: DocumentReference) { + + let setData = [ + "foo": 42, + "bar": [ + "baz": "Hello world!" + ] + ] as [String : Any]; + + let updateData = [ + "bar.baz": 42, + FieldPath(["foobar"]) : 42 + ] as [AnyHashable : Any]; + + docRef.setData(setData) + + // Completion callback (via trailing closure syntax). + docRef.setData(setData) { error in + if let error = error { + print("Uh oh! \(error)") + return + } + + print("Set complete!") + } + + // SetOptions + docRef.setData(setData, options:SetOptions.merge()) + + docRef.updateData(updateData) + docRef.delete(); + + docRef.delete() { error in + if let error = error { + print("Uh oh! \(error)") + return + } + + print("Set complete!") + } +} + +func addDocument(to collectionRef: CollectionReference) { + + collectionRef.addDocument(data: ["foo": 42]); + //or + collectionRef.document().setData(["foo": 42]); +} + +func readDocument(at docRef: DocumentReference) { + + // Trailing closure syntax. + docRef.getDocument() { document, error in + if let document = document { + // NOTE that document is nullable. + let data = document.data(); + print("Read document: \(data)") + + // Fields are read via subscript notation. + if let foo = document["foo"] { + print("Field: \(foo)") + } + } else { + // TODO(mikelehen): There may be a better way to do this, but it at least demonstrates + // the swift error domain / enum codes are renamed appropriately. + if let errorCode = error.flatMap({ + ($0._domain == FirestoreErrorDomain) ? FirestoreErrorCode (rawValue: $0._code) : nil + }) { + switch errorCode { + case .unavailable: + print("Can't read document due to being offline!") + case _: + print("Failed to read.") + } + } else { + print("Unknown error!") + } + } + + } +} + +func readDocuments(matching query: Query) { + query.getDocuments() { querySnapshot, error in + // TODO(mikelehen): Figure out how to make "for..in" syntax work + // directly on documentSet. + for document in querySnapshot!.documents { + print(document.data()) + } + } +} + +func listenToDocument(at docRef: DocumentReference) { + + let listener = docRef.addSnapshotListener() { document, error in + if let error = error { + print("Uh oh! Listen canceled: \(error)") + return + } + + if let document = document { + print("Current document: \(document.data())"); + if (document.metadata.isFromCache) { + print("From Cache") + } else { + print("From Server") + } + } + } + + // Unsubscribe. + listener.remove(); +} + +func listenToDocuments(matching query: Query) { + + let listener = query.addSnapshotListener() { snap, error in + if let error = error { + print("Uh oh! Listen canceled: \(error)") + return + } + + if let snap = snap { + print("NEW SNAPSHOT (empty=\(snap.isEmpty) count=\(snap.count)") + + // TODO(mikelehen): Figure out how to make "for..in" syntax work + // directly on documentSet. + for document in snap.documents { + print("Doc: ", document.data()) + } + } + } + + // Unsubscribe + listener.remove(); +} + +func listenToQueryDiffs(onQuery query: Query) { + + let listener = query.addSnapshotListener() { snap, error in + if let snap = snap { + for change in snap.documentChanges { + switch (change.type) { + case .added: + print("New document: \(change.document.data())") + case .modified: + print("Modified document: \(change.document.data())") + case .removed: + print("Removed document: \(change.document.data())") + } + } + } + } + + // Unsubscribe + listener.remove(); +} + +func transactions() { + let db = Firestore.firestore() + + let collectionRef = db.collection("cities") + let accA = collectionRef.document("accountA") + let accB = collectionRef.document("accountB") + let amount = 20.0 + + db.runTransaction({ (transaction, errorPointer) -> Any? in + do { + let balanceA = try transaction.getDocument(accA)["balance"] as! Double + let balanceB = try transaction.getDocument(accB)["balance"] as! Double + + if (balanceA < amount) { + errorPointer?.pointee = NSError(domain: "Foo", code: 123, userInfo: nil) + return nil + } + transaction.updateData(["balance": balanceA - amount], forDocument:accA) + transaction.updateData(["balance": balanceB + amount], forDocument:accB) + } catch let error as NSError { + print("Uh oh! \(error)") + } + return 0 + }) { (result, error) in + // handle result. + } +} + +func types() { + // Just highlighting the types of everything, though devs can/will often omit them. + let _: Firestore; + let _: CollectionReference; + let _: DocumentReference; + let _: Query; + let _: DocumentSnapshot; + let _: QuerySnapshot; +} diff --git a/Firestore/Example/Tests/API/FIRGeoPointTests.m b/Firestore/Example/Tests/API/FIRGeoPointTests.m new file mode 100644 index 0000000..ca7d24c --- /dev/null +++ b/Firestore/Example/Tests/API/FIRGeoPointTests.m @@ -0,0 +1,67 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Firestore/FIRGeoPoint.h" + +#import <XCTest/XCTest.h> + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRGeoPointTests : XCTestCase +@end + +@implementation FIRGeoPointTests + +- (void)testEquals { + XCTAssertEqualObjects([[FIRGeoPoint alloc] initWithLatitude:0 longitude:0], + [[FIRGeoPoint alloc] initWithLatitude:0 longitude:0]); + XCTAssertEqualObjects([[FIRGeoPoint alloc] initWithLatitude:1.23 longitude:4.56], + [[FIRGeoPoint alloc] initWithLatitude:1.23 longitude:4.56]); + XCTAssertNotEqualObjects([[FIRGeoPoint alloc] initWithLatitude:0 longitude:0], + [[FIRGeoPoint alloc] initWithLatitude:1 longitude:0]); + XCTAssertNotEqualObjects([[FIRGeoPoint alloc] initWithLatitude:0 longitude:0], + [[FIRGeoPoint alloc] initWithLatitude:0 longitude:1]); + XCTAssertNotEqualObjects([[FIRGeoPoint alloc] initWithLatitude:0 longitude:0], + [[NSObject alloc] init]); +} + +- (void)testComparison { + NSArray *values = @[ + @[ [[FIRGeoPoint alloc] initWithLatitude:-90 longitude:-180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:-90 longitude:0] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:-90 longitude:180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:-89 longitude:-180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:-89 longitude:0] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:-89 longitude:180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:0 longitude:-180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:0 longitude:0] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:0 longitude:180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:89 longitude:-180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:89 longitude:0] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:89 longitude:180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:90 longitude:-180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:90 longitude:0] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:90 longitude:180] ], + ]; + + FSTAssertComparisons(values); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTDatabaseInfoTests.m b/Firestore/Example/Tests/Core/FSTDatabaseInfoTests.m new file mode 100644 index 0000000..931a59f --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTDatabaseInfoTests.m @@ -0,0 +1,59 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Core/FSTDatabaseInfo.h" + +#import <XCTest/XCTest.h> + +#import "Model/FSTDatabaseID.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTDatabaseInfoTests : XCTestCase +@end + +@implementation FSTDatabaseInfoTests + +- (void)testConstructor { + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"]; + FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID + persistenceKey:@"pk" + host:@"h" + sslEnabled:YES]; + XCTAssertEqualObjects(databaseInfo.databaseID.projectID, @"p"); + XCTAssertEqualObjects(databaseInfo.databaseID.databaseID, @"d"); + XCTAssertEqualObjects(databaseInfo.persistenceKey, @"pk"); + XCTAssertEqualObjects(databaseInfo.host, @"h"); + XCTAssertEqual(databaseInfo.sslEnabled, YES); +} + +- (void)testDefaultDatabase { + FSTDatabaseID *databaseID = + [FSTDatabaseID databaseIDWithProject:@"p" database:kDefaultDatabaseID]; + FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID + persistenceKey:@"pk" + host:@"h" + sslEnabled:YES]; + XCTAssertEqualObjects(databaseInfo.databaseID.projectID, @"p"); + XCTAssertEqualObjects(databaseInfo.databaseID.databaseID, @"(default)"); + XCTAssertEqualObjects(databaseInfo.persistenceKey, @"pk"); + XCTAssertEqualObjects(databaseInfo.host, @"h"); + XCTAssertEqual(databaseInfo.sslEnabled, YES); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTEventManagerTests.m b/Firestore/Example/Tests/Core/FSTEventManagerTests.m new file mode 100644 index 0000000..85afe58 --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTEventManagerTests.m @@ -0,0 +1,163 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Core/FSTEventManager.h" + +#import <OCMock/OCMock.h> +#import <XCTest/XCTest.h> + +#import "Core/FSTQuery.h" +#import "Core/FSTSyncEngine.h" +#import "Model/FSTDocumentSet.h" +#import "Util/FSTDispatchQueue.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +// FSTEventManager implements this delegate privately +@interface FSTEventManager () <FSTSyncEngineDelegate> +@end + +@interface FSTEventManagerTests : XCTestCase +@end + +@implementation FSTEventManagerTests { + FSTDispatchQueue *_testUserQueue; +} + +- (void)setUp { + _testUserQueue = [FSTDispatchQueue queueWith:dispatch_get_main_queue()]; +} + +- (FSTQueryListener *)noopListenerForQuery:(FSTQuery *)query { + return [[FSTQueryListener alloc] + initWithQuery:query + options:[FSTListenOptions defaultOptions] + viewSnapshotHandler:^(FSTViewSnapshot *_Nullable snapshot, NSError *_Nullable error){ + }]; +} + +- (void)testHandlesManyListenersPerQuery { + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"foo/bar")]; + FSTQueryListener *listener1 = [self noopListenerForQuery:query]; + FSTQueryListener *listener2 = [self noopListenerForQuery:query]; + + FSTSyncEngine *syncEngineMock = OCMStrictClassMock([FSTSyncEngine class]); + OCMExpect([syncEngineMock setDelegate:[OCMArg any]]); + FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock]; + + OCMExpect([syncEngineMock listenToQuery:query]); + [eventManager addListener:listener1]; + OCMVerifyAll((id)syncEngineMock); + + [eventManager addListener:listener2]; + [eventManager removeListener:listener2]; + + OCMExpect([syncEngineMock stopListeningToQuery:query]); + [eventManager removeListener:listener1]; + OCMVerifyAll((id)syncEngineMock); +} + +- (void)testHandlesUnlistenOnUnknownListenerGracefully { + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"foo/bar")]; + FSTQueryListener *listener = [self noopListenerForQuery:query]; + + FSTSyncEngine *syncEngineMock = OCMStrictClassMock([FSTSyncEngine class]); + OCMExpect([syncEngineMock setDelegate:[OCMArg any]]); + FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock]; + + [eventManager removeListener:listener]; + OCMVerifyAll((id)syncEngineMock); +} + +- (FSTQueryListener *)makeMockListenerForQuery:(FSTQuery *)query + viewSnapshotHandler:(void (^)())handler { + FSTQueryListener *listener = OCMClassMock([FSTQueryListener class]); + OCMStub([listener query]).andReturn(query); + OCMStub([listener queryDidChangeViewSnapshot:[OCMArg any]]).andDo(^(NSInvocation *invocation) { + handler(); + }); + return listener; +} + +- (void)testNotifiesListenersInTheRightOrder { + FSTQuery *query1 = [FSTQuery queryWithPath:FSTTestPath(@"foo/bar")]; + FSTQuery *query2 = [FSTQuery queryWithPath:FSTTestPath(@"bar/baz")]; + NSMutableArray *eventOrder = [NSMutableArray array]; + + FSTQueryListener *listener1 = [self makeMockListenerForQuery:query1 + viewSnapshotHandler:^{ + [eventOrder addObject:@"listener1"]; + }]; + + FSTQueryListener *listener2 = [self makeMockListenerForQuery:query2 + viewSnapshotHandler:^{ + [eventOrder addObject:@"listener2"]; + }]; + + FSTQueryListener *listener3 = [self makeMockListenerForQuery:query1 + viewSnapshotHandler:^{ + [eventOrder addObject:@"listener3"]; + }]; + + FSTSyncEngine *syncEngineMock = OCMClassMock([FSTSyncEngine class]); + FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock]; + + [eventManager addListener:listener1]; + [eventManager addListener:listener2]; + [eventManager addListener:listener3]; + OCMVerify([syncEngineMock listenToQuery:query1]); + OCMVerify([syncEngineMock listenToQuery:query2]); + + FSTViewSnapshot *snapshot1 = OCMClassMock([FSTViewSnapshot class]); + OCMStub([snapshot1 query]).andReturn(query1); + FSTViewSnapshot *snapshot2 = OCMClassMock([FSTViewSnapshot class]); + OCMStub([snapshot2 query]).andReturn(query2); + + [eventManager handleViewSnapshots:@[ snapshot1, snapshot2 ]]; + + NSArray *expected = @[ @"listener1", @"listener3", @"listener2" ]; + XCTAssertEqualObjects(eventOrder, expected); +} + +- (void)testWillForwardOnlineStateChanges { + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"foo/bar")]; + FSTQueryListener *fakeListener = OCMClassMock([FSTQueryListener class]); + NSMutableArray *events = [NSMutableArray array]; + OCMStub([fakeListener query]).andReturn(query); + OCMStub([fakeListener clientDidChangeOnlineState:FSTOnlineStateUnknown]) + .andDo(^(NSInvocation *invocation) { + [events addObject:@(FSTOnlineStateUnknown)]; + }); + OCMStub([fakeListener clientDidChangeOnlineState:FSTOnlineStateHealthy]) + .andDo(^(NSInvocation *invocation) { + [events addObject:@(FSTOnlineStateHealthy)]; + }); + + FSTSyncEngine *syncEngineMock = OCMClassMock([FSTSyncEngine class]); + OCMExpect([syncEngineMock setDelegate:[OCMArg any]]); + FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock]; + + [eventManager addListener:fakeListener]; + XCTAssertEqualObjects(events, @[ @(FSTOnlineStateUnknown) ]); + [eventManager watchStreamDidChangeOnlineState:FSTOnlineStateHealthy]; + XCTAssertEqualObjects(events, (@[ @(FSTOnlineStateUnknown), @(FSTOnlineStateHealthy) ])); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTQueryListenerTests.m b/Firestore/Example/Tests/Core/FSTQueryListenerTests.m new file mode 100644 index 0000000..155eb00 --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTQueryListenerTests.m @@ -0,0 +1,487 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Core/FSTEventManager.h" + +#import <XCTest/XCTest.h> + +#import "Core/FSTQuery.h" +#import "Core/FSTView.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentSet.h" +#import "Remote/FSTRemoteEvent.h" +#import "Util/FSTAsyncQueryListener.h" +#import "Util/FSTDispatchQueue.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTQueryListenerTests : XCTestCase +@property(nonatomic, strong, readonly) FSTDispatchQueue *asyncQueue; +@end + +@implementation FSTQueryListenerTests + +- (void)setUp { + _asyncQueue = [FSTDispatchQueue + queueWith:dispatch_queue_create("FSTQueryListenerTests Queue", DISPATCH_QUEUE_SERIAL)]; +} + +- (void)testRaisesCollectionEvents { + NSMutableArray<FSTViewSnapshot *> *accum = [NSMutableArray array]; + NSMutableArray<FSTViewSnapshot *> *otherAccum = [NSMutableArray array]; + + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); + FSTDocument *doc2prime = + FSTTestDoc(@"rooms/Hades", 3, @{@"name" : @"Hades", @"owner" : @"Jonny"}, NO); + + FSTQueryListener *listener = [self listenToQuery:query accumulatingSnapshots:accum]; + FSTQueryListener *otherListener = [self listenToQuery:query accumulatingSnapshots:otherAccum]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2prime ], nil); + + FSTDocumentViewChange *change1 = + [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded]; + FSTDocumentViewChange *change2 = + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded]; + FSTDocumentViewChange *change3 = + [FSTDocumentViewChange changeWithDocument:doc2prime type:FSTDocumentViewChangeTypeModified]; + FSTDocumentViewChange *change4 = + [FSTDocumentViewChange changeWithDocument:doc2prime type:FSTDocumentViewChangeTypeAdded]; + + [listener queryDidChangeViewSnapshot:snap1]; + [listener queryDidChangeViewSnapshot:snap2]; + [otherListener queryDidChangeViewSnapshot:snap2]; + + XCTAssertEqualObjects(accum, (@[ snap1, snap2 ])); + XCTAssertEqualObjects(accum[0].documentChanges, (@[ change1, change2 ])); + XCTAssertEqualObjects(accum[1].documentChanges, (@[ change3 ])); + + FSTViewSnapshot *expectedSnap2 = [[FSTViewSnapshot alloc] + initWithQuery:snap2.query + documents:snap2.documents + oldDocuments:[FSTDocumentSet documentSetWithComparator:snap2.query.comparator] + documentChanges:@[ change1, change4 ] + fromCache:snap2.fromCache + hasPendingWrites:snap2.hasPendingWrites + syncStateChanged:YES]; + XCTAssertEqualObjects(otherAccum, (@[ expectedSnap2 ])); +} + +- (void)testRaisesErrorEvent { + NSMutableArray<NSError *> *accum = [NSMutableArray array]; + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms/Eros")]; + + FSTQueryListener *listener = [self listenToQuery:query + handler:^(FSTViewSnapshot *snapshot, NSError *error) { + [accum addObject:error]; + }]; + + NSError *testError = + [NSError errorWithDomain:@"com.google.firestore.test" code:42 userInfo:@{@"some" : @"info"}]; + [listener queryDidError:testError]; + + XCTAssertEqualObjects(accum, @[ testError ]); +} + +- (void)testRaisesEventForEmptyCollectionAfterSync { + NSMutableArray<FSTViewSnapshot *> *accum = [NSMutableArray array]; + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + + FSTQueryListener *listener = [self listenToQuery:query accumulatingSnapshots:accum]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); + + FSTTargetChange *ackTarget = + [FSTTargetChange changeWithDocuments:@[] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]; + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], ackTarget); + + [listener queryDidChangeViewSnapshot:snap1]; + XCTAssertEqualObjects(accum, @[]); + + [listener queryDidChangeViewSnapshot:snap2]; + XCTAssertEqualObjects(accum, @[ snap2 ]); +} + +- (void)testMutingAsyncListenerPreventsAllSubsequentEvents { + NSMutableArray<FSTViewSnapshot *> *accum = [NSMutableArray array]; + + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms/Eros")]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 3, @{@"name" : @"Eros"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Eros", 4, @{@"name" : @"Eros2"}, NO); + + __block FSTAsyncQueryListener *listener = [[FSTAsyncQueryListener alloc] + initWithDispatchQueue:self.asyncQueue + snapshotHandler:^(FSTViewSnapshot *snapshot, NSError *error) { + [accum addObject:snapshot]; + [listener mute]; + }]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *viewSnapshot1 = FSTTestApplyChanges(view, @[ doc1 ], nil); + FSTViewSnapshot *viewSnapshot2 = FSTTestApplyChanges(view, @[ doc2 ], nil); + + FSTViewSnapshotHandler handler = listener.asyncSnapshotHandler; + handler(viewSnapshot1, nil); + handler(viewSnapshot2, nil); + + // Drain queue + XCTestExpectation *expectation = [self expectationWithDescription:@"Queue drained"]; + [self.asyncQueue dispatchAsync:^{ + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:4.0 + handler:^(NSError *_Nullable expectationError) { + if (expectationError) { + XCTFail(@"Error waiting for timeout: %@", expectationError); + } + }]; + + // We should get the first snapshot but not the second. + XCTAssertEqualObjects(accum, @[ viewSnapshot1 ]); +} + +- (void)testDoesNotRaiseEventsForMetadataChangesUnlessSpecified { + NSMutableArray<FSTViewSnapshot *> *filteredAccum = [NSMutableArray array]; + NSMutableArray<FSTViewSnapshot *> *fullAccum = [NSMutableArray array]; + + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); + + FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES + includeDocumentMetadataChanges:NO + waitForSyncWhenOnline:NO]; + + FSTQueryListener *filteredListener = + [self listenToQuery:query accumulatingSnapshots:filteredAccum]; + FSTQueryListener *fullListener = + [self listenToQuery:query options:options accumulatingSnapshots:fullAccum]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); + + FSTTargetChange *ackTarget = + [FSTTargetChange changeWithDocuments:@[ doc1 ] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]; + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], ackTarget); + FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc2 ], nil); + + [filteredListener queryDidChangeViewSnapshot:snap1]; // local event + [filteredListener queryDidChangeViewSnapshot:snap2]; // no event + [filteredListener queryDidChangeViewSnapshot:snap3]; // doc2 update + + [fullListener queryDidChangeViewSnapshot:snap1]; // local event + [fullListener queryDidChangeViewSnapshot:snap2]; // state change event + [fullListener queryDidChangeViewSnapshot:snap3]; // doc2 update + + XCTAssertEqualObjects(filteredAccum, (@[ snap1, snap3 ])); + XCTAssertEqualObjects(fullAccum, (@[ snap1, snap2, snap3 ])); +} + +- (void)testRaisesDocumentMetadataEventsOnlyWhenSpecified { + NSMutableArray<FSTViewSnapshot *> *filteredAccum = [NSMutableArray array]; + NSMutableArray<FSTViewSnapshot *> *fullAccum = [NSMutableArray array]; + + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, YES); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); + FSTDocument *doc1Prime = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/Other", 3, @{@"name" : @"Other"}, NO); + + FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO + includeDocumentMetadataChanges:YES + waitForSyncWhenOnline:NO]; + + FSTQueryListener *filteredListener = + [self listenToQuery:query accumulatingSnapshots:filteredAccum]; + FSTQueryListener *fullListener = + [self listenToQuery:query options:options accumulatingSnapshots:fullAccum]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], nil); + FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc3 ], nil); + + FSTDocumentViewChange *change1 = + [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded]; + FSTDocumentViewChange *change2 = + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded]; + FSTDocumentViewChange *change3 = + [FSTDocumentViewChange changeWithDocument:doc1Prime type:FSTDocumentViewChangeTypeMetadata]; + FSTDocumentViewChange *change4 = + [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded]; + + [filteredListener queryDidChangeViewSnapshot:snap1]; + [filteredListener queryDidChangeViewSnapshot:snap2]; + [filteredListener queryDidChangeViewSnapshot:snap3]; + [fullListener queryDidChangeViewSnapshot:snap1]; + [fullListener queryDidChangeViewSnapshot:snap2]; + [fullListener queryDidChangeViewSnapshot:snap3]; + + XCTAssertEqualObjects(filteredAccum, (@[ snap1, snap3 ])); + XCTAssertEqualObjects(filteredAccum[0].documentChanges, (@[ change1, change2 ])); + XCTAssertEqualObjects(filteredAccum[1].documentChanges, (@[ change4 ])); + + XCTAssertEqualObjects(fullAccum, (@[ snap1, snap2, snap3 ])); + XCTAssertEqualObjects(fullAccum[0].documentChanges, (@[ change1, change2 ])); + XCTAssertEqualObjects(fullAccum[1].documentChanges, (@[ change3 ])); + XCTAssertEqualObjects(fullAccum[2].documentChanges, (@[ change4 ])); +} + +- (void)testRaisesQueryMetadataEventsOnlyWhenHasPendingWritesOnTheQueryChanges { + NSMutableArray<FSTViewSnapshot *> *fullAccum = [NSMutableArray array]; + + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, YES); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, YES); + FSTDocument *doc1Prime = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); + FSTDocument *doc2Prime = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/Other", 3, @{@"name" : @"Other"}, NO); + + FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES + includeDocumentMetadataChanges:NO + waitForSyncWhenOnline:NO]; + FSTQueryListener *fullListener = + [self listenToQuery:query options:options accumulatingSnapshots:fullAccum]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], nil); + FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc3 ], nil); + FSTViewSnapshot *snap4 = FSTTestApplyChanges(view, @[ doc2Prime ], nil); + + [fullListener queryDidChangeViewSnapshot:snap1]; + [fullListener queryDidChangeViewSnapshot:snap2]; // Emits no events. + [fullListener queryDidChangeViewSnapshot:snap3]; + [fullListener queryDidChangeViewSnapshot:snap4]; // Metadata change event. + + FSTViewSnapshot *expectedSnap4 = [[FSTViewSnapshot alloc] initWithQuery:snap4.query + documents:snap4.documents + oldDocuments:snap3.documents + documentChanges:@[] + fromCache:snap4.fromCache + hasPendingWrites:NO + syncStateChanged:snap4.syncStateChanged]; + XCTAssertEqualObjects(fullAccum, (@[ snap1, snap3, expectedSnap4 ])); +} + +- (void)testMetadataOnlyDocumentChangesAreFilteredOutWhenIncludeDocumentMetadataChangesIsFalse { + NSMutableArray<FSTViewSnapshot *> *filteredAccum = [NSMutableArray array]; + + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, YES); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); + FSTDocument *doc1Prime = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/Other", 3, @{@"name" : @"Other"}, NO); + + FSTQueryListener *filteredListener = + [self listenToQuery:query accumulatingSnapshots:filteredAccum]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime, doc3 ], nil); + + FSTDocumentViewChange *change3 = + [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded]; + + [filteredListener queryDidChangeViewSnapshot:snap1]; + [filteredListener queryDidChangeViewSnapshot:snap2]; + + FSTViewSnapshot *expectedSnap2 = [[FSTViewSnapshot alloc] initWithQuery:snap2.query + documents:snap2.documents + oldDocuments:snap1.documents + documentChanges:@[ change3 ] + fromCache:snap2.isFromCache + hasPendingWrites:snap2.hasPendingWrites + syncStateChanged:snap2.syncStateChanged]; + XCTAssertEqualObjects(filteredAccum, (@[ snap1, expectedSnap2 ])); +} + +- (void)testWillWaitForSyncIfOnline { + NSMutableArray<FSTViewSnapshot *> *events = [NSMutableArray array]; + + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); + FSTQueryListener *listener = + [self listenToQuery:query + options:[[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO + includeDocumentMetadataChanges:NO + waitForSyncWhenOnline:YES] + accumulatingSnapshots:events]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil); + FSTViewSnapshot *snap3 = + FSTTestApplyChanges(view, @[], + [FSTTargetChange changeWithDocuments:@[ doc1, doc2 ] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); + + [listener clientDidChangeOnlineState:FSTOnlineStateHealthy]; // no event + [listener queryDidChangeViewSnapshot:snap1]; + [listener clientDidChangeOnlineState:FSTOnlineStateUnknown]; + [listener clientDidChangeOnlineState:FSTOnlineStateHealthy]; + [listener queryDidChangeViewSnapshot:snap2]; + [listener queryDidChangeViewSnapshot:snap3]; + + FSTDocumentViewChange *change1 = + [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded]; + FSTDocumentViewChange *change2 = + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded]; + FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc] + initWithQuery:snap3.query + documents:snap3.documents + oldDocuments:[FSTDocumentSet documentSetWithComparator:snap3.query.comparator] + documentChanges:@[ change1, change2 ] + fromCache:NO + hasPendingWrites:NO + syncStateChanged:YES]; + XCTAssertEqualObjects(events, (@[ expectedSnap ])); +} + +- (void)testWillRaiseInitialEventWhenGoingOffline { + NSMutableArray<FSTViewSnapshot *> *events = [NSMutableArray array]; + + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); + FSTQueryListener *listener = + [self listenToQuery:query + options:[[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO + includeDocumentMetadataChanges:NO + waitForSyncWhenOnline:YES] + accumulatingSnapshots:events]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil); + + [listener clientDidChangeOnlineState:FSTOnlineStateHealthy]; // no event + [listener queryDidChangeViewSnapshot:snap1]; // no event + [listener clientDidChangeOnlineState:FSTOnlineStateFailed]; // event + [listener clientDidChangeOnlineState:FSTOnlineStateUnknown]; // no event + [listener clientDidChangeOnlineState:FSTOnlineStateFailed]; // no event + [listener queryDidChangeViewSnapshot:snap2]; // another event + + FSTDocumentViewChange *change1 = + [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded]; + FSTDocumentViewChange *change2 = + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded]; + FSTViewSnapshot *expectedSnap1 = [[FSTViewSnapshot alloc] + initWithQuery:query + documents:snap1.documents + oldDocuments:[FSTDocumentSet documentSetWithComparator:snap1.query.comparator] + documentChanges:@[ change1 ] + fromCache:YES + hasPendingWrites:NO + syncStateChanged:YES]; + FSTViewSnapshot *expectedSnap2 = [[FSTViewSnapshot alloc] initWithQuery:query + documents:snap2.documents + oldDocuments:snap1.documents + documentChanges:@[ change2 ] + fromCache:YES + hasPendingWrites:NO + syncStateChanged:NO]; + XCTAssertEqualObjects(events, (@[ expectedSnap1, expectedSnap2 ])); +} + +- (void)testWillRaiseInitialEventWhenGoingOfflineAndThereAreNoDocs { + NSMutableArray<FSTViewSnapshot *> *events = [NSMutableArray array]; + + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTQueryListener *listener = [self listenToQuery:query + options:[FSTListenOptions defaultOptions] + accumulatingSnapshots:events]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); + + [listener clientDidChangeOnlineState:FSTOnlineStateHealthy]; // no event + [listener queryDidChangeViewSnapshot:snap1]; // no event + [listener clientDidChangeOnlineState:FSTOnlineStateFailed]; // event + + FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc] + initWithQuery:query + documents:snap1.documents + oldDocuments:[FSTDocumentSet documentSetWithComparator:snap1.query.comparator] + documentChanges:@[] + fromCache:YES + hasPendingWrites:NO + syncStateChanged:YES]; + XCTAssertEqualObjects(events, (@[ expectedSnap ])); +} + +- (void)testWillRaiseInitialEventWhenStartingOfflineAndThereAreNoDocs { + NSMutableArray<FSTViewSnapshot *> *events = [NSMutableArray array]; + + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTQueryListener *listener = [self listenToQuery:query + options:[FSTListenOptions defaultOptions] + accumulatingSnapshots:events]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); + + [listener clientDidChangeOnlineState:FSTOnlineStateFailed]; // no event + [listener queryDidChangeViewSnapshot:snap1]; // event + + FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc] + initWithQuery:query + documents:snap1.documents + oldDocuments:[FSTDocumentSet documentSetWithComparator:snap1.query.comparator] + documentChanges:@[] + fromCache:YES + hasPendingWrites:NO + syncStateChanged:YES]; + XCTAssertEqualObjects(events, (@[ expectedSnap ])); +} + +- (FSTQueryListener *)listenToQuery:(FSTQuery *)query handler:(FSTViewSnapshotHandler)handler { + return [[FSTQueryListener alloc] initWithQuery:query + options:[FSTListenOptions defaultOptions] + viewSnapshotHandler:handler]; +} + +- (FSTQueryListener *)listenToQuery:(FSTQuery *)query + options:(FSTListenOptions *)options + accumulatingSnapshots:(NSMutableArray<FSTViewSnapshot *> *)values { + return [[FSTQueryListener alloc] initWithQuery:query + options:options + viewSnapshotHandler:^(FSTViewSnapshot *snapshot, NSError *error) { + [values addObject:snapshot]; + }]; +} + +- (FSTQueryListener *)listenToQuery:(FSTQuery *)query + accumulatingSnapshots:(NSMutableArray<FSTViewSnapshot *> *)values { + return [self listenToQuery:query + options:[FSTListenOptions defaultOptions] + accumulatingSnapshots:values]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTQueryTests.m b/Firestore/Example/Tests/Core/FSTQueryTests.m new file mode 100644 index 0000000..051f10d --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTQueryTests.m @@ -0,0 +1,577 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Core/FSTQuery.h" + +#import <XCTest/XCTest.h> + +#import "API/FIRFirestore+Internal.h" +#import "Model/FSTDatabaseID.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentKey.h" +#import "Model/FSTPath.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +/** Convenience methods for building test queries. */ +@interface FSTQuery (Tests) +- (FSTQuery *)queryByAddingSortBy:(NSString *)key ascending:(BOOL)ascending; +@end + +@implementation FSTQuery (Tests) + +- (FSTQuery *)queryByAddingSortBy:(NSString *)key ascending:(BOOL)ascending { + return [self queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(key) + ascending:ascending]]; +} + +@end + +@interface FSTQueryTests : XCTestCase +@end + +@implementation FSTQueryTests + +- (void)testConstructor { + FSTResourcePath *path = + [FSTResourcePath pathWithSegments:@[ @"rooms", @"Firestore", @"messages", @"0001" ]]; + FSTQuery *query = [FSTQuery queryWithPath:path]; + XCTAssertNotNil(query); + + XCTAssertEqual(query.sortOrders.count, 1); + XCTAssertEqualObjects(query.sortOrders[0].field.canonicalString, kDocumentKeyPath); + XCTAssertEqual(query.sortOrders[0].ascending, YES); + + XCTAssertEqual(query.explicitSortOrders.count, 0); +} + +- (void)testOrderBy { + FSTResourcePath *path = + [FSTResourcePath pathWithSegments:@[ @"rooms", @"Firestore", @"messages" ]]; + FSTQuery *query = [FSTQuery queryWithPath:path]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"length") + ascending:NO]]; + + XCTAssertEqual(query.sortOrders.count, 2); + XCTAssertEqualObjects(query.sortOrders[0].field.canonicalString, @"length"); + XCTAssertEqual(query.sortOrders[0].ascending, NO); + XCTAssertEqualObjects(query.sortOrders[1].field.canonicalString, kDocumentKeyPath); + XCTAssertEqual(query.sortOrders[1].ascending, NO); + + XCTAssertEqual(query.explicitSortOrders.count, 1); + XCTAssertEqualObjects(query.explicitSortOrders[0].field.canonicalString, @"length"); + XCTAssertEqual(query.explicitSortOrders[0].ascending, NO); +} + +- (void)testMatchesBasedOnDocumentKey { + FSTResourcePath *queryKey = + [FSTResourcePath pathWithSegments:@[ @"rooms", @"eros", @"messages", @"1" ]]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/other/messages/1", 0, @{@"text" : @"msg3"}, NO); + + // document query + FSTQuery *query = [FSTQuery queryWithPath:queryKey]; + XCTAssertTrue([query matchesDocument:doc1]); + XCTAssertFalse([query matchesDocument:doc2]); + XCTAssertFalse([query matchesDocument:doc3]); +} + +- (void)testMatchesCorrectlyForShallowAncestorQuery { + FSTResourcePath *queryPath = + [FSTResourcePath pathWithSegments:@[ @"rooms", @"eros", @"messages" ]]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc1Meta = FSTTestDoc(@"rooms/eros/messages/1/meta/1", 0, @{@"meta" : @"mv"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/other/messages/1", 0, @{@"text" : @"msg3"}, NO); + + // shallow ancestor query + FSTQuery *query = [FSTQuery queryWithPath:queryPath]; + XCTAssertTrue([query matchesDocument:doc1]); + XCTAssertFalse([query matchesDocument:doc1Meta]); + XCTAssertTrue([query matchesDocument:doc2]); + XCTAssertFalse([query matchesDocument:doc3]); +} + +- (void)testEmptyFieldsAreAllowedForQueries { + FSTResourcePath *queryPath = [FSTResourcePath pathWithString:@"rooms/eros/messages"]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO); + + FSTQuery *query = [[FSTQuery queryWithPath:queryPath] + queryByAddingFilter:FSTTestFilter(@"text", @"==", @"msg1")]; + XCTAssertTrue([query matchesDocument:doc1]); + XCTAssertFalse([query matchesDocument:doc2]); +} + +- (void)testMatchesPrimitiveValuesForFilters { + FSTQuery *query1 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]] + queryByAddingFilter:FSTTestFilter(@"sort", @">=", @(2))]; + FSTQuery *query2 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]] + queryByAddingFilter:FSTTestFilter(@"sort", @"<=", @(2))]; + + FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"collection/3", 0, @{ @"sort" : @3 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @NO }, NO); + FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{@"sort" : @"string"}, NO); + FSTDocument *doc6 = FSTTestDoc(@"collection/6", 0, @{}, NO); + + XCTAssertFalse([query1 matchesDocument:doc1]); + XCTAssertTrue([query1 matchesDocument:doc2]); + XCTAssertTrue([query1 matchesDocument:doc3]); + XCTAssertFalse([query1 matchesDocument:doc4]); + XCTAssertFalse([query1 matchesDocument:doc5]); + XCTAssertFalse([query1 matchesDocument:doc6]); + + XCTAssertTrue([query2 matchesDocument:doc1]); + XCTAssertTrue([query2 matchesDocument:doc2]); + XCTAssertFalse([query2 matchesDocument:doc3]); + XCTAssertFalse([query2 matchesDocument:doc4]); + XCTAssertFalse([query2 matchesDocument:doc5]); + XCTAssertFalse([query2 matchesDocument:doc6]); +} + +- (void)testNullFilter { + FSTQuery *query = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]] + queryByAddingFilter:FSTTestFilter(@"sort", @"==", [NSNull null])]; + FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{@"sort" : [NSNull null]}, NO); + FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @3.1 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @NO }, NO); + FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{@"sort" : @"string"}, NO); + + XCTAssertTrue([query matchesDocument:doc1]); + XCTAssertFalse([query matchesDocument:doc2]); + XCTAssertFalse([query matchesDocument:doc3]); + XCTAssertFalse([query matchesDocument:doc4]); + XCTAssertFalse([query matchesDocument:doc5]); +} + +- (void)testNanFilter { + FSTQuery *query = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]] + queryByAddingFilter:FSTTestFilter(@"sort", @"==", @(NAN))]; + FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @(NAN) }, NO); + FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @3.1 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @NO }, NO); + FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{@"sort" : @"string"}, NO); + + XCTAssertTrue([query matchesDocument:doc1]); + XCTAssertFalse([query matchesDocument:doc2]); + XCTAssertFalse([query matchesDocument:doc3]); + XCTAssertFalse([query matchesDocument:doc4]); + XCTAssertFalse([query matchesDocument:doc5]); +} + +- (void)testDoesNotMatchComplexObjectsForFilters { + FSTQuery *query1 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]] + queryByAddingFilter:FSTTestFilter(@"sort", @"<=", @(2))]; + FSTQuery *query2 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]] + queryByAddingFilter:FSTTestFilter(@"sort", @">=", @(2))]; + + FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @2 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @[] }, NO); + FSTDocument *doc3 = FSTTestDoc(@"collection/3", 0, @{ @"sort" : @[ @1 ] }, NO); + FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @{@"foo" : @2} }, NO); + FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{ @"sort" : @{@"foo" : @"bar"} }, NO); + FSTDocument *doc6 = FSTTestDoc(@"collection/6", 0, @{ @"sort" : @{} }, NO); // no sort field + FSTDocument *doc7 = FSTTestDoc(@"collection/7", 0, @{ @"sort" : @[ @3, @1 ] }, NO); + + XCTAssertTrue([query1 matchesDocument:doc1]); + XCTAssertFalse([query1 matchesDocument:doc2]); + XCTAssertFalse([query1 matchesDocument:doc3]); + XCTAssertFalse([query1 matchesDocument:doc4]); + XCTAssertFalse([query1 matchesDocument:doc5]); + XCTAssertFalse([query1 matchesDocument:doc6]); + XCTAssertFalse([query1 matchesDocument:doc7]); + + XCTAssertTrue([query2 matchesDocument:doc1]); + XCTAssertFalse([query2 matchesDocument:doc2]); + XCTAssertFalse([query2 matchesDocument:doc3]); + XCTAssertFalse([query2 matchesDocument:doc4]); + XCTAssertFalse([query2 matchesDocument:doc5]); + XCTAssertFalse([query2 matchesDocument:doc6]); + XCTAssertFalse([query2 matchesDocument:doc7]); +} + +- (void)testDoesntRemoveComplexObjectsWithOrderBy { + FSTQuery *query1 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]] + queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort") + ascending:YES]]; + + FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @2 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @[] }, NO); + FSTDocument *doc3 = FSTTestDoc(@"collection/3", 0, @{ @"sort" : @[ @1 ] }, NO); + FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @{@"foo" : @2} }, NO); + FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{ @"sort" : @{@"foo" : @"bar"} }, NO); + FSTDocument *doc6 = FSTTestDoc(@"collection/6", 0, @{}, NO); + + XCTAssertTrue([query1 matchesDocument:doc1]); + XCTAssertTrue([query1 matchesDocument:doc2]); + XCTAssertTrue([query1 matchesDocument:doc3]); + XCTAssertTrue([query1 matchesDocument:doc4]); + XCTAssertTrue([query1 matchesDocument:doc5]); + XCTAssertFalse([query1 matchesDocument:doc6]); +} + +- (void)testFiltersBasedOnArrayValue { + FSTQuery *baseQuery = + [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]; + + FSTDocument *doc1 = FSTTestDoc(@"collection/doc", 0, @{ @"tags" : @[ @"foo", @1, @YES ] }, NO); + + NSArray<id<FSTFilter>> *matchingFilters = + @[ FSTTestFilter(@"tags", @"==", @[ @"foo", @1, @YES ]) ]; + + NSArray<id<FSTFilter>> *nonMatchingFilters = @[ + FSTTestFilter(@"tags", @"==", @"foo"), + FSTTestFilter(@"tags", @"==", @[ @"foo", @1 ]), + FSTTestFilter(@"tags", @"==", @[ @"foo", @YES, @1 ]), + ]; + + for (id<FSTFilter> filter in matchingFilters) { + XCTAssertTrue([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]); + } + + for (id<FSTFilter> filter in nonMatchingFilters) { + XCTAssertFalse([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]); + } +} + +- (void)testFiltersBasedOnObjectValue { + FSTQuery *baseQuery = + [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]; + + FSTDocument *doc1 = + FSTTestDoc(@"collection/doc", 0, + @{ @"tags" : @{@"foo" : @"foo", @"a" : @0, @"b" : @YES, @"c" : @(NAN)} }, NO); + + NSArray<id<FSTFilter>> *matchingFilters = @[ + FSTTestFilter(@"tags", @"==", + @{ @"foo" : @"foo", + @"a" : @0, + @"b" : @YES, + @"c" : @(NAN) }), + FSTTestFilter(@"tags", @"==", + @{ @"b" : @YES, + @"a" : @0, + @"foo" : @"foo", + @"c" : @(NAN) }), + FSTTestFilter(@"tags.foo", @"==", @"foo") + ]; + + NSArray<id<FSTFilter>> *nonMatchingFilters = @[ + FSTTestFilter(@"tags", @"==", @"foo"), FSTTestFilter(@"tags", @"==", @{ + @"foo" : @"foo", + @"a" : @0, + @"b" : @YES, + }) + ]; + + for (id<FSTFilter> filter in matchingFilters) { + XCTAssertTrue([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]); + } + + for (id<FSTFilter> filter in nonMatchingFilters) { + XCTAssertFalse([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]); + } +} + +/** + * Checks that an ordered array of elements yields the correct pair-wise comparison result for the + * supplied comparator. + */ +- (void)assertCorrectComparisonsWithArray:(NSArray *)array comparator:(NSComparator)comp { + [array enumerateObjectsUsingBlock:^(id iObj, NSUInteger i, BOOL *outerStop) { + [array enumerateObjectsUsingBlock:^(id _Nonnull jObj, NSUInteger j, BOOL *innerStop) { + NSComparisonResult expected = [@(i) compare:@(j)]; + NSComparisonResult actual = comp(iObj, jObj); + XCTAssertEqual(actual, expected, @"Compared %@ to %@ at (%lu, %lu).", iObj, jObj, + (unsigned long)i, (unsigned long)j); + }]; + }]; +} + +- (void)testSortsDocumentsInTheCorrectOrder { + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort") + ascending:YES]]; + + // clang-format off + NSArray<FSTDocument *> *docs = @[ + FSTTestDoc(@"collection/1", 0, @{@"sort": [NSNull null]}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @NO}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @YES}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @1}, NO), + FSTTestDoc(@"collection/2", 0, @{@"sort": @1}, NO), // by key + FSTTestDoc(@"collection/3", 0, @{@"sort": @1}, NO), // by key + FSTTestDoc(@"collection/1", 0, @{@"sort": @1.9}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @2}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @2.1}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @""}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @"a"}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @"ab"}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @"b"}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": + FSTTestRef(@"project", kDefaultDatabaseID, @"collection/id1")}, NO), + ]; + // clang-format on + + [self assertCorrectComparisonsWithArray:docs comparator:query.comparator]; +} + +- (void)testSortsDocumentsUsingMultipleFields { + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort1") + ascending:YES]]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort2") + ascending:YES]]; + + // clang-format off + NSArray<FSTDocument *> *docs = + @[FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @1}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @2}, NO), + FSTTestDoc(@"collection/2", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/3", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @3}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @1}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @2}, NO), + FSTTestDoc(@"collection/2", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/3", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @3}, NO), + ]; + // clang-format on + + [self assertCorrectComparisonsWithArray:docs comparator:query.comparator]; +} + +- (void)testSortsDocumentsWithDescendingToo { + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort1") + ascending:NO]]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort2") + ascending:NO]]; + + // clang-format off + NSArray<FSTDocument *> *docs = + @[FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @3}, NO), + FSTTestDoc(@"collection/3", 0, @{@"sort1": @2, @"sort2": @2}, NO), + FSTTestDoc(@"collection/2", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @1}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @3}, NO), + FSTTestDoc(@"collection/3", 0, @{@"sort1": @1, @"sort2": @2}, NO), + FSTTestDoc(@"collection/2", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @1}, NO), + ]; + // clang-format on + + [self assertCorrectComparisonsWithArray:docs comparator:query.comparator]; +} + +- (void)testEquality { + FSTQuery *q11 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))]; + q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))]; + FSTQuery *q12 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))]; + q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))]; + + FSTQuery *q21 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q22 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + + FSTQuery *q31 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]]; + FSTQuery *q32 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]]; + + FSTQuery *q41 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q41 = [q41 queryByAddingSortBy:@"foo" ascending:YES]; + q41 = [q41 queryByAddingSortBy:@"bar" ascending:YES]; + FSTQuery *q42 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q42 = [q42 queryByAddingSortBy:@"foo" ascending:YES]; + q42 = [q42 queryByAddingSortBy:@"bar" ascending:YES]; + FSTQuery *q43Diff = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q43Diff = [q43Diff queryByAddingSortBy:@"bar" ascending:YES]; + q43Diff = [q43Diff queryByAddingSortBy:@"foo" ascending:YES]; + + FSTQuery *q51 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q51 = [q51 queryByAddingSortBy:@"foo" ascending:YES]; + q51 = [q51 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))]; + FSTQuery *q52 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q52 = [q52 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))]; + q52 = [q52 queryByAddingSortBy:@"foo" ascending:YES]; + FSTQuery *q53Diff = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q53Diff = [q53Diff queryByAddingFilter:FSTTestFilter(@"bar", @">", @(2))]; + q53Diff = [q53Diff queryByAddingSortBy:@"bar" ascending:YES]; + + FSTQuery *q61 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q61 = [q61 queryBySettingLimit:10]; + + // XCTAssertEqualObjects(q11, q12); // TODO(klimt): not canonical yet + XCTAssertNotEqualObjects(q11, q21); + XCTAssertNotEqualObjects(q11, q31); + XCTAssertNotEqualObjects(q11, q41); + XCTAssertNotEqualObjects(q11, q51); + XCTAssertNotEqualObjects(q11, q61); + + XCTAssertEqualObjects(q21, q22); + XCTAssertNotEqualObjects(q21, q31); + XCTAssertNotEqualObjects(q21, q41); + XCTAssertNotEqualObjects(q21, q51); + XCTAssertNotEqualObjects(q21, q61); + + XCTAssertEqualObjects(q31, q32); + XCTAssertNotEqualObjects(q31, q41); + XCTAssertNotEqualObjects(q31, q51); + XCTAssertNotEqualObjects(q31, q61); + + XCTAssertEqualObjects(q41, q42); + XCTAssertNotEqualObjects(q41, q43Diff); + XCTAssertNotEqualObjects(q41, q51); + XCTAssertNotEqualObjects(q41, q61); + + XCTAssertEqualObjects(q51, q52); + XCTAssertNotEqualObjects(q51, q53Diff); + XCTAssertNotEqualObjects(q51, q61); +} + +- (void)testUniqueIds { + FSTQuery *q11 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))]; + q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))]; + FSTQuery *q12 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))]; + q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))]; + + FSTQuery *q21 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q22 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + + FSTQuery *q31 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]]; + FSTQuery *q32 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]]; + + FSTQuery *q41 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q41 = [q41 queryByAddingSortBy:@"foo" ascending:YES]; + q41 = [q41 queryByAddingSortBy:@"bar" ascending:YES]; + FSTQuery *q42 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q42 = [q42 queryByAddingSortBy:@"foo" ascending:YES]; + q42 = [q42 queryByAddingSortBy:@"bar" ascending:YES]; + FSTQuery *q43Diff = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q43Diff = [q43Diff queryByAddingSortBy:@"bar" ascending:YES]; + q43Diff = [q43Diff queryByAddingSortBy:@"foo" ascending:YES]; + + FSTQuery *q51 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q51 = [q51 queryByAddingSortBy:@"foo" ascending:YES]; + q51 = [q51 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))]; + FSTQuery *q52 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q52 = [q52 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))]; + q52 = [q52 queryByAddingSortBy:@"foo" ascending:YES]; + FSTQuery *q53Diff = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q53Diff = [q53Diff queryByAddingFilter:FSTTestFilter(@"bar", @">", @(2))]; + q53Diff = [q53Diff queryByAddingSortBy:@"bar" ascending:YES]; + + FSTQuery *q61 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q61 = [q61 queryBySettingLimit:10]; + + // XCTAssertEqual(q11.hash, q12.hash); // TODO(klimt): not canonical yet + XCTAssertNotEqual(q11.hash, q21.hash); + XCTAssertNotEqual(q11.hash, q31.hash); + XCTAssertNotEqual(q11.hash, q41.hash); + XCTAssertNotEqual(q11.hash, q51.hash); + XCTAssertNotEqual(q11.hash, q61.hash); + + XCTAssertEqual(q21.hash, q22.hash); + XCTAssertNotEqual(q21.hash, q31.hash); + XCTAssertNotEqual(q21.hash, q41.hash); + XCTAssertNotEqual(q21.hash, q51.hash); + XCTAssertNotEqual(q21.hash, q61.hash); + + XCTAssertEqual(q31.hash, q32.hash); + XCTAssertNotEqual(q31.hash, q41.hash); + XCTAssertNotEqual(q31.hash, q51.hash); + XCTAssertNotEqual(q31.hash, q61.hash); + + XCTAssertEqual(q41.hash, q42.hash); + XCTAssertNotEqual(q41.hash, q43Diff.hash); + XCTAssertNotEqual(q41.hash, q51.hash); + XCTAssertNotEqual(q41.hash, q61.hash); + + XCTAssertEqual(q51.hash, q52.hash); + XCTAssertNotEqual(q51.hash, q53Diff.hash); + XCTAssertNotEqual(q51.hash, q61.hash); +} + +- (void)testImplicitOrderBy { + FSTQuery *baseQuery = FSTTestQuery(@"foo"); + // Default is ascending + XCTAssertEqualObjects(baseQuery.sortOrders, @[ FSTTestOrderBy(kDocumentKeyPath, @"asc") ]); + + // Explicit key ordering is respected + XCTAssertEqualObjects( + [baseQuery queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"asc")].sortOrders, + @[ FSTTestOrderBy(kDocumentKeyPath, @"asc") ]); + XCTAssertEqualObjects( + [baseQuery queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"desc")].sortOrders, + @[ FSTTestOrderBy(kDocumentKeyPath, @"desc") ]); + + XCTAssertEqualObjects( + [[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"asc")] + queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"asc")] + .sortOrders, + (@[ FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(kDocumentKeyPath, @"asc") ])); + + XCTAssertEqualObjects( + [[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"asc")] + queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"desc")] + .sortOrders, + (@[ FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(kDocumentKeyPath, @"desc") ])); + + // Inequality filters add order bys + XCTAssertEqualObjects( + [baseQuery queryByAddingFilter:FSTTestFilter(@"foo", @"<", @5)].sortOrders, + (@[ FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(kDocumentKeyPath, @"asc") ])); + + // Descending order by applies to implicit key ordering + XCTAssertEqualObjects( + [baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"desc")].sortOrders, + (@[ FSTTestOrderBy(@"foo", @"desc"), FSTTestOrderBy(kDocumentKeyPath, @"desc") ])); + XCTAssertEqualObjects([[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"asc")] + queryByAddingSortOrder:FSTTestOrderBy(@"bar", @"desc")] + .sortOrders, + (@[ + FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(@"bar", @"desc"), + FSTTestOrderBy(kDocumentKeyPath, @"desc") + ])); + XCTAssertEqualObjects([[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"desc")] + queryByAddingSortOrder:FSTTestOrderBy(@"bar", @"asc")] + .sortOrders, + (@[ + FSTTestOrderBy(@"foo", @"desc"), FSTTestOrderBy(@"bar", @"asc"), + FSTTestOrderBy(kDocumentKeyPath, @"asc") + ])); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTSyncEngine+Testing.h b/Firestore/Example/Tests/Core/FSTSyncEngine+Testing.h new file mode 100644 index 0000000..ff98e74 --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTSyncEngine+Testing.h @@ -0,0 +1,32 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +#import "Core/FSTSyncEngine.h" + +@class FSTDocumentKey; + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTSyncEngine (Testing) + +/** Returns the current set of limbo document keys and their associated target IDs. */ +- (NSDictionary<FSTDocumentKey *, FSTBoxedTargetID *> *)currentLimboDocuments; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTTargetIDGeneratorTests.m b/Firestore/Example/Tests/Core/FSTTargetIDGeneratorTests.m new file mode 100644 index 0000000..11a7f46 --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTTargetIDGeneratorTests.m @@ -0,0 +1,94 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Core/FSTTargetIDGenerator.h" + +#import <XCTest/XCTest.h> + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTTargetIDGenerator () +- (instancetype)initWithGeneratorID:(NSInteger)generatorID startingAfterID:(FSTTargetID)after; +@end + +@interface FSTTargetIDGeneratorTests : XCTestCase +@end + +@implementation FSTTargetIDGeneratorTests + +- (void)testConstructor { + XCTAssertEqual([[[FSTTargetIDGenerator alloc] initWithGeneratorID:0 startingAfterID:0] nextID], + 2); + XCTAssertEqual([[[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:0] nextID], + 1); + + XCTAssertEqual([[FSTTargetIDGenerator generatorForLocalStoreStartingAfterID:0] nextID], 2); + XCTAssertEqual([[FSTTargetIDGenerator generatorForSyncEngineStartingAfterID:0] nextID], 1); +} + +- (void)testSkipPast { + FSTTargetIDGenerator *gen = + [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:-1]; + XCTAssertEqual([gen nextID], 1); + + gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:2]; + XCTAssertEqual([gen nextID], 3); + + gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:4]; + XCTAssertEqual([gen nextID], 5); + + for (int i = 4; i < 12; ++i) { + FSTTargetIDGenerator *gen0 = + [[FSTTargetIDGenerator alloc] initWithGeneratorID:0 startingAfterID:i]; + FSTTargetIDGenerator *gen1 = + [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:i]; + XCTAssertEqual([gen0 nextID], i + 2 & ~1, @"Skip failed for index %d", i); + XCTAssertEqual([gen1 nextID], i + 1 | 1, @"Skip failed for index %d", i); + } + + gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:12]; + XCTAssertEqual([gen nextID], 13); + + gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:0 startingAfterID:22]; + XCTAssertEqual([gen nextID], 24); +} + +- (void)testIncrement { + FSTTargetIDGenerator *gen = + [[FSTTargetIDGenerator alloc] initWithGeneratorID:0 startingAfterID:0]; + XCTAssertEqual([gen nextID], 2); + XCTAssertEqual([gen nextID], 4); + XCTAssertEqual([gen nextID], 6); + gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:0 startingAfterID:46]; + XCTAssertEqual([gen nextID], 48); + XCTAssertEqual([gen nextID], 50); + XCTAssertEqual([gen nextID], 52); + XCTAssertEqual([gen nextID], 54); + + gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:0]; + XCTAssertEqual([gen nextID], 1); + XCTAssertEqual([gen nextID], 3); + XCTAssertEqual([gen nextID], 5); + gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:46]; + XCTAssertEqual([gen nextID], 47); + XCTAssertEqual([gen nextID], 49); + XCTAssertEqual([gen nextID], 51); + XCTAssertEqual([gen nextID], 53); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTTimestampTests.m b/Firestore/Example/Tests/Core/FSTTimestampTests.m new file mode 100644 index 0000000..8538078 --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTTimestampTests.m @@ -0,0 +1,88 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Core/FSTTimestamp.h" + +#import <XCTest/XCTest.h> + +#import "Util/FSTAssert.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTTimestampTests : XCTestCase +@end + +@implementation FSTTimestampTests + +- (void)testFromDate { + // Very carefully construct an NSDate that won't lose precision with its milliseconds. + NSDate *input = [NSDate dateWithTimeIntervalSinceReferenceDate:1.5]; + + FSTTimestamp *actual = [FSTTimestamp timestampWithDate:input]; + static const int64_t kSecondsFromEpochToReferenceDate = 978307200; + XCTAssertEqual(kSecondsFromEpochToReferenceDate + 1, actual.seconds); + XCTAssertEqual(500000000, actual.nanos); + + FSTTimestamp *expected = + [[FSTTimestamp alloc] initWithSeconds:(kSecondsFromEpochToReferenceDate + 1) nanos:500000000]; + XCTAssertEqualObjects(expected, actual); +} + +- (void)testSO8601String { + NSDate *date = FSTTestDate(1912, 4, 14, 23, 40, 0); + FSTTimestamp *timestamp = + [[FSTTimestamp alloc] initWithSeconds:(int64_t)date.timeIntervalSince1970 nanos:543000000]; + XCTAssertEqualObjects(timestamp.ISO8601String, @"1912-04-14T23:40:00.543000000Z"); +} + +- (void)testISO8601String_withLowMilliseconds { + NSDate *date = FSTTestDate(1912, 4, 14, 23, 40, 0); + FSTTimestamp *timestamp = + [[FSTTimestamp alloc] initWithSeconds:(int64_t)date.timeIntervalSince1970 nanos:7000000]; + XCTAssertEqualObjects(timestamp.ISO8601String, @"1912-04-14T23:40:00.007000000Z"); +} + +- (void)testISO8601String_withLowNanos { + FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:0 nanos:1]; + XCTAssertEqualObjects(timestamp.ISO8601String, @"1970-01-01T00:00:00.000000001Z"); +} + +- (void)testISO8601String_withNegativeSeconds { + FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:-1 nanos:999999999]; + XCTAssertEqualObjects(timestamp.ISO8601String, @"1969-12-31T23:59:59.999999999Z"); +} + +- (void)testCompare { + NSArray<FSTTimestamp *> *timestamps = @[ + [[FSTTimestamp alloc] initWithSeconds:12344 nanos:999999999], + [[FSTTimestamp alloc] initWithSeconds:12345 nanos:0], + [[FSTTimestamp alloc] initWithSeconds:12345 nanos:000000001], + [[FSTTimestamp alloc] initWithSeconds:12345 nanos:99999999], + [[FSTTimestamp alloc] initWithSeconds:12345 nanos:100000000], + [[FSTTimestamp alloc] initWithSeconds:12345 nanos:100000001], + [[FSTTimestamp alloc] initWithSeconds:12346 nanos:0], + ]; + for (int i = 0; i < timestamps.count - 1; ++i) { + XCTAssertEqual(NSOrderedAscending, [timestamps[i] compare:timestamps[i + 1]]); + XCTAssertEqual(NSOrderedDescending, [timestamps[i + 1] compare:timestamps[i]]); + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTViewSnapshotTest.m b/Firestore/Example/Tests/Core/FSTViewSnapshotTest.m new file mode 100644 index 0000000..202fae8 --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTViewSnapshotTest.m @@ -0,0 +1,141 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Core/FSTViewSnapshot.h" + +#import <XCTest/XCTest.h> + +#import "Core/FSTQuery.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentSet.h" +#import "Model/FSTPath.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTViewSnapshotTests : XCTestCase +@end + +@implementation FSTViewSnapshotTests + +- (void)testDocumentChangeConstructor { + FSTDocument *doc = FSTTestDoc(@"a/b", 0, @{}, NO); + FSTDocumentViewChangeType type = FSTDocumentViewChangeTypeModified; + FSTDocumentViewChange *change = [FSTDocumentViewChange changeWithDocument:doc type:type]; + XCTAssertEqual(change.document, doc); + XCTAssertEqual(change.type, type); +} + +- (void)testTrack { + FSTDocumentViewChangeSet *set = [FSTDocumentViewChangeSet changeSet]; + + FSTDocument *docAdded = FSTTestDoc(@"a/1", 0, @{}, NO); + FSTDocument *docRemoved = FSTTestDoc(@"a/2", 0, @{}, NO); + FSTDocument *docModified = FSTTestDoc(@"a/3", 0, @{}, NO); + + FSTDocument *docAddedThenModified = FSTTestDoc(@"b/1", 0, @{}, NO); + FSTDocument *docAddedThenRemoved = FSTTestDoc(@"b/2", 0, @{}, NO); + FSTDocument *docRemovedThenAdded = FSTTestDoc(@"b/3", 0, @{}, NO); + FSTDocument *docModifiedThenRemoved = FSTTestDoc(@"b/4", 0, @{}, NO); + FSTDocument *docModifiedThenModified = FSTTestDoc(@"b/5", 0, @{}, NO); + + [set addChange:[FSTDocumentViewChange changeWithDocument:docAdded + type:FSTDocumentViewChangeTypeAdded]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docRemoved + type:FSTDocumentViewChangeTypeRemoved]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docModified + type:FSTDocumentViewChangeTypeModified]]; + + [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenModified + type:FSTDocumentViewChangeTypeAdded]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenModified + type:FSTDocumentViewChangeTypeModified]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenRemoved + type:FSTDocumentViewChangeTypeAdded]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenRemoved + type:FSTDocumentViewChangeTypeRemoved]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docRemovedThenAdded + type:FSTDocumentViewChangeTypeRemoved]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docRemovedThenAdded + type:FSTDocumentViewChangeTypeAdded]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenRemoved + type:FSTDocumentViewChangeTypeModified]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenRemoved + type:FSTDocumentViewChangeTypeRemoved]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenModified + type:FSTDocumentViewChangeTypeModified]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenModified + type:FSTDocumentViewChangeTypeModified]]; + + NSArray<FSTDocumentViewChange *> *changes = [set changes]; + XCTAssertEqual(changes.count, 7); + + XCTAssertEqual(changes[0].document, docAdded); + XCTAssertEqual(changes[0].type, FSTDocumentViewChangeTypeAdded); + + XCTAssertEqual(changes[1].document, docRemoved); + XCTAssertEqual(changes[1].type, FSTDocumentViewChangeTypeRemoved); + + XCTAssertEqual(changes[2].document, docModified); + XCTAssertEqual(changes[2].type, FSTDocumentViewChangeTypeModified); + + XCTAssertEqual(changes[3].document, docAddedThenModified); + XCTAssertEqual(changes[3].type, FSTDocumentViewChangeTypeAdded); + + XCTAssertEqual(changes[4].document, docRemovedThenAdded); + XCTAssertEqual(changes[4].type, FSTDocumentViewChangeTypeModified); + + XCTAssertEqual(changes[5].document, docModifiedThenRemoved); + XCTAssertEqual(changes[5].type, FSTDocumentViewChangeTypeRemoved); + + XCTAssertEqual(changes[6].document, docModifiedThenModified); + XCTAssertEqual(changes[6].type, FSTDocumentViewChangeTypeModified); +} + +- (void)testViewSnapshotConstructor { + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"a" ]]]; + FSTDocumentSet *documents = [FSTDocumentSet documentSetWithComparator:FSTDocumentComparatorByKey]; + FSTDocumentSet *oldDocuments = documents; + documents = [documents documentSetByAddingDocument:FSTTestDoc(@"c/a", 1, @{}, NO)]; + NSArray<FSTDocumentViewChange *> *documentChanges = + @[ [FSTDocumentViewChange changeWithDocument:FSTTestDoc(@"c/a", 1, @{}, NO) + type:FSTDocumentViewChangeTypeAdded] ]; + + BOOL fromCache = YES; + BOOL hasPendingWrites = NO; + BOOL syncStateChanged = YES; + + FSTViewSnapshot *snapshot = [[FSTViewSnapshot alloc] initWithQuery:query + documents:documents + oldDocuments:oldDocuments + documentChanges:documentChanges + fromCache:fromCache + hasPendingWrites:hasPendingWrites + syncStateChanged:syncStateChanged]; + + XCTAssertEqual(snapshot.query, query); + XCTAssertEqual(snapshot.documents, documents); + XCTAssertEqual(snapshot.oldDocuments, oldDocuments); + XCTAssertEqual(snapshot.documentChanges, documentChanges); + XCTAssertEqual(snapshot.fromCache, fromCache); + XCTAssertEqual(snapshot.hasPendingWrites, hasPendingWrites); + XCTAssertEqual(snapshot.syncStateChanged, syncStateChanged); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTViewTests.m b/Firestore/Example/Tests/Core/FSTViewTests.m new file mode 100644 index 0000000..d939d62 --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTViewTests.m @@ -0,0 +1,618 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Core/FSTView.h" + +#import <XCTest/XCTest.h> + +#import "API/FIRFirestore+Internal.h" +#import "Core/FSTQuery.h" +#import "Core/FSTViewSnapshot.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentKey.h" +#import "Model/FSTDocumentSet.h" +#import "Model/FSTFieldValue.h" +#import "Model/FSTPath.h" +#import "Remote/FSTRemoteEvent.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTViewTests : XCTestCase +@end + +@implementation FSTViewTests + +/** Returns a new empty query to use for testing. */ +- (FSTQuery *)queryForMessages { + return [FSTQuery + queryWithPath:[FSTResourcePath pathWithSegments:@[ @"rooms", @"eros", @"messages" ]]]; +} + +- (void)testAddsDocumentsBasedOnQuery { + FSTQuery *query = [self queryForMessages]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/other/messages/1", 0, @{@"text" : @"msg3"}, NO); + + FSTViewSnapshot *_Nullable snapshot = + FSTTestApplyChanges(view, @[ doc1, doc2, doc3 ], + [FSTTargetChange changeWithDocuments:@[ doc1, doc2, doc3 ] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc2 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded], + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertFalse(snapshot.isFromCache); + XCTAssertFalse(snapshot.hasPendingWrites); + XCTAssertTrue(snapshot.syncStateChanged); +} + +- (void)testRemovesDocuments { + FSTQuery *query = [self queryForMessages]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{@"text" : @"msg3"}, NO); + + // initial state + FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + + // delete doc2, add doc3 + FSTViewSnapshot *snapshot = + FSTTestApplyChanges(view, @[ FSTTestDeletedDoc(@"rooms/eros/messages/2", 0), doc3 ], + [FSTTargetChange changeWithDocuments:@[ doc1, doc3 ] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeRemoved], + [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertFalse(snapshot.isFromCache); + XCTAssertTrue(snapshot.syncStateChanged); +} + +- (void)testReturnsNilIfThereAreNoChanges { + FSTQuery *query = [self queryForMessages]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); + + // initial state + FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + + // reapply same docs, no changes + FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + XCTAssertNil(snapshot); +} + +- (void)testDoesNotReturnNilForFirstChanges { + FSTQuery *query = [self queryForMessages]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[], nil); + XCTAssertNotNil(snapshot); +} + +- (void)testFiltersDocumentsBasedOnQueryWithFilter { + FSTQuery *query = [self queryForMessages]; + FSTRelationFilter *filter = + [FSTRelationFilter filterWithField:FSTTestFieldPath(@"sort") + filterOperator:FSTRelationFilterOperatorLessThanOrEqual + value:[FSTDoubleValue doubleValue:2]]; + query = [query queryByAddingFilter:filter]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"sort" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"sort" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"sort" : @3 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{}, NO); // no sort, no match + FSTDocument *doc5 = FSTTestDoc(@"rooms/eros/messages/5", 0, @{ @"sort" : @1 }, NO); + + FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2, doc3, doc4, doc5 ], nil); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc5, doc2 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded], + [FSTDocumentViewChange changeWithDocument:doc5 type:FSTDocumentViewChangeTypeAdded], + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertTrue(snapshot.isFromCache); + XCTAssertTrue(snapshot.syncStateChanged); +} + +- (void)testUpdatesDocumentsBasedOnQueryWithFilter { + FSTQuery *query = [self queryForMessages]; + FSTRelationFilter *filter = + [FSTRelationFilter filterWithField:FSTTestFieldPath(@"sort") + filterOperator:FSTRelationFilterOperatorLessThanOrEqual + value:[FSTDoubleValue doubleValue:2]]; + query = [query queryByAddingFilter:filter]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"sort" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"sort" : @3 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"sort" : @2 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{}, NO); + + FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2, doc3, doc4 ], nil); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ])); + + FSTDocument *newDoc2 = FSTTestDoc(@"rooms/eros/messages/2", 1, @{ @"sort" : @2 }, NO); + FSTDocument *newDoc3 = FSTTestDoc(@"rooms/eros/messages/3", 1, @{ @"sort" : @3 }, NO); + FSTDocument *newDoc4 = FSTTestDoc(@"rooms/eros/messages/4", 1, @{ @"sort" : @0 }, NO); + + snapshot = FSTTestApplyChanges(view, @[ newDoc2, newDoc3, newDoc4 ], nil); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ newDoc4, doc1, newDoc2 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeRemoved], + [FSTDocumentViewChange changeWithDocument:newDoc4 type:FSTDocumentViewChangeTypeAdded], + [FSTDocumentViewChange changeWithDocument:newDoc2 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertTrue(snapshot.isFromCache); + XCTAssertFalse(snapshot.syncStateChanged); +} + +- (void)testRemovesDocumentsForQueryWithLimit { + FSTQuery *query = [self queryForMessages]; + query = [query queryBySettingLimit:2]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{@"text" : @"msg3"}, NO); + + // initial state + FSTTestApplyChanges(view, @[ doc1, doc3 ], nil); + + // add doc2, which should push out doc3 + FSTViewSnapshot *snapshot = + FSTTestApplyChanges(view, @[ doc2 ], + [FSTTargetChange changeWithDocuments:@[ doc1, doc2, doc3 ] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc2 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeRemoved], + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertFalse(snapshot.isFromCache); + XCTAssertTrue(snapshot.syncStateChanged); +} + +- (void)testDoesntReportChangesForDocumentBeyondLimitOfQuery { + FSTQuery *query = [self queryForMessages]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"num") + ascending:YES]]; + query = [query queryBySettingLimit:2]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"num" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"num" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"num" : @3 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{ @"num" : @4 }, NO); + + // initial state + FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + + // change doc2 to 5, and add doc3 and doc4. + // doc2 will be modified + removed = removed + // doc3 will be added + // doc4 will be added + removed = nothing + doc2 = FSTTestDoc(@"rooms/eros/messages/2", 1, @{ @"num" : @5 }, NO); + FSTViewDocumentChanges *viewDocChanges = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2, doc3, doc4 ])]; + XCTAssertTrue(viewDocChanges.needsRefill); + // Verify that all the docs still match. + viewDocChanges = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4 ]) + previousChanges:viewDocChanges]; + FSTViewSnapshot *snapshot = + [view applyChangesToDocuments:viewDocChanges + targetChange:[FSTTargetChange + changeWithDocuments:@[ doc1, doc2, doc3, doc4 ] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]] + .snapshot; + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeRemoved], + [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertFalse(snapshot.isFromCache); + XCTAssertTrue(snapshot.syncStateChanged); +} + +- (void)testKeepsTrackOfLimboDocuments { + FSTQuery *query = [self queryForMessages]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO); + + FSTViewChange *change = [view + applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1 ])]]; + XCTAssertEqualObjects(change.limboChanges, @[]); + + change = + [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])] + targetChange:[FSTTargetChange + changeWithDocuments:@[] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]]; + XCTAssertEqualObjects( + change.limboChanges, + @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded key:doc1.key] ]); + + change = [view + applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])] + targetChange:[FSTTargetChange changeWithDocuments:@[ doc1 ] + currentStatusUpdate:FSTCurrentStatusUpdateNone]]; + XCTAssertEqualObjects( + change.limboChanges, + @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved key:doc1.key] ]); + + change = [view + applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ])] + targetChange:[FSTTargetChange changeWithDocuments:@[ doc2 ] + currentStatusUpdate:FSTCurrentStatusUpdateNone]]; + XCTAssertEqualObjects(change.limboChanges, @[]); + + change = [view + applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]]; + XCTAssertEqualObjects( + change.limboChanges, + @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded key:doc3.key] ]); + + change = [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ + FSTTestDeletedDoc(@"rooms/eros/messages/2", + 1) + ])]]; // remove + XCTAssertEqualObjects( + change.limboChanges, + @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved key:doc3.key] ]); +} + +- (void)testResumingQueryCreatesNoLimbos { + FSTQuery *query = [self queryForMessages]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + + // Unlike other cases, here the view is initialized with a set of previously synced documents + // which happens when listening to a previously listened-to query. + FSTView *view = [[FSTView alloc] initWithQuery:query + remoteDocuments:FSTTestDocKeySet(@[ doc1.key, doc2.key ])]; + + FSTTargetChange *markCurrent = + [FSTTargetChange changeWithDocuments:@[] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]; + FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[])]; + FSTViewChange *change = [view applyChangesToDocuments:changes targetChange:markCurrent]; + XCTAssertEqualObjects(change.limboChanges, @[]); +} + +- (void)assertDocSet:(FSTDocumentSet *)docSet containsDocs:(NSArray<FSTDocument *> *)docs { + XCTAssertEqual(docs.count, docSet.count); + for (FSTDocument *doc in docs) { + XCTAssertTrue([docSet containsKey:doc.key]); + } +} + +- (void)testReturnsNeedsRefillOnDeleteInLimitQuery { + FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Remove one of the docs. + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc( + @"rooms/eros/messages/0", 0) ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc2 ]]; + XCTAssertTrue(changes.needsRefill); + XCTAssertEqual(1, [changes.changeSet changes].count); + // Refill it with just the one doc remaining. + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ]) previousChanges:changes]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(1, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testReturnsNeedsRefillOnReorderInLimitQuery { + FSTQuery *query = [self queryForMessages]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"order") + ascending:YES]]; + query = [query queryBySettingLimit:2]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{ @"order" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"order" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Move one of the docs. + doc2 = FSTTestDoc(@"rooms/eros/messages/1", 1, @{ @"order" : @2000 }, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertTrue(changes.needsRefill); + XCTAssertEqual(1, [changes.changeSet changes].count); + // Refill it with all three current docs. + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3 ]) + previousChanges:changes]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc3 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testDoesntNeedRefillOnReorderWithinLimit { + FSTQuery *query = [self queryForMessages]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"order") + ascending:YES]]; + query = [query queryBySettingLimit:3]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{ @"order" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"order" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"order" : @4 }, NO); + FSTDocument *doc5 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{ @"order" : @5 }, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4, doc5 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(3, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Move one of the docs. + doc1 = FSTTestDoc(@"rooms/eros/messages/0", 1, @{ @"order" : @3 }, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc2, doc3, doc1 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(1, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testDoesntNeedRefillOnReorderAfterLimitQuery { + FSTQuery *query = [self queryForMessages]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"order") + ascending:YES]]; + query = [query queryBySettingLimit:3]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{ @"order" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"order" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"order" : @4 }, NO); + FSTDocument *doc5 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{ @"order" : @5 }, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4, doc5 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(3, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Move one of the docs. + doc4 = FSTTestDoc(@"rooms/eros/messages/3", 1, @{ @"order" : @6 }, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc4 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(0, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testDoesntNeedRefillForAdditionAfterTheLimit { + FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Add a doc that is past the limit. + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 1, @{}, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(0, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testDoesntNeedRefillForDeletionsWhenNotNearTheLimit { + FSTQuery *query = [[self queryForMessages] queryBySettingLimit:20]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Remove one of the docs. + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc( + @"rooms/eros/messages/1", 0) ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(1, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testHandlesApplyingIrrelevantDocs { + FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Remove a doc that isn't even in the results. + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc( + @"rooms/eros/messages/2", 0) ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(0, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testComputesMutatedKeys { + FSTQuery *query = [self queryForMessages]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [view applyChangesToDocuments:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[])); + + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, YES); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc3.key ])); +} + +- (void)testRemovesKeysFromMutatedKeysWhenNewDocHasNoLocalChanges { + FSTQuery *query = [self queryForMessages]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, YES); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [view applyChangesToDocuments:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); + + FSTDocument *doc2Prime = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2Prime ])]; + [view applyChangesToDocuments:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[])); +} + +- (void)testRemembersLocalMutationsFromPreviousSnapshot { + FSTQuery *query = [self queryForMessages]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, YES); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [view applyChangesToDocuments:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); + + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]; + [view applyChangesToDocuments:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); +} + +- (void)testRemembersLocalMutationsFromPreviousCallToComputeChangesWithDocuments { + FSTQuery *query = [self queryForMessages]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, YES); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); + + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ]) previousChanges:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Integration/API/FIRCursorTests.m b/Firestore/Example/Tests/Integration/API/FIRCursorTests.m new file mode 100644 index 0000000..2f8babd --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRCursorTests.m @@ -0,0 +1,195 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import Firestore; + +#import <XCTest/XCTest.h> + +#import "FSTIntegrationTestCase.h" + +@interface FIRCursorTests : FSTIntegrationTestCase +@end + +@implementation FIRCursorTests + +- (void)testCanPageThroughItems { + FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ + @"a" : @{@"v" : @"a"}, + @"b" : @{@"v" : @"b"}, + @"c" : @{@"v" : @"c"}, + @"d" : @{@"v" : @"d"}, + @"e" : @{@"v" : @"e"}, + @"f" : @{@"v" : @"f"} + }]; + + FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[testCollection queryLimitedTo:2]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ @{@"v" : @"a"}, @{@"v" : @"b"} ])); + + FIRDocumentSnapshot *lastDoc = snapshot.documents.lastObject; + snapshot = [self + readDocumentSetForRef:[[testCollection queryLimitedTo:3] queryStartingAfterDocument:lastDoc]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), + (@[ @{@"v" : @"c"}, @{@"v" : @"d"}, @{@"v" : @"e"} ])); + + lastDoc = snapshot.documents.lastObject; + snapshot = [self + readDocumentSetForRef:[[testCollection queryLimitedTo:1] queryStartingAfterDocument:lastDoc]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), @[ @{@"v" : @"f"} ]); + + lastDoc = snapshot.documents.lastObject; + snapshot = [self + readDocumentSetForRef:[[testCollection queryLimitedTo:3] queryStartingAfterDocument:lastDoc]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), @[]); +} + +- (void)testCanBeCreatedFromDocuments { + FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ + @"a" : @{@"v" : @"a", @"sort" : @1.0}, + @"b" : @{@"v" : @"b", @"sort" : @2.0}, + @"c" : @{@"v" : @"c", @"sort" : @2.0}, + @"d" : @{@"v" : @"d", @"sort" : @2.0}, + @"e" : @{@"v" : @"e", @"sort" : @0.0}, + @"f" : @{@"v" : @"f", @"nosort" : @1.0} // should not show up + }]; + + FIRQuery *query = [testCollection queryOrderedByField:@"sort"]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:[testCollection documentWithPath:@"c"]]; + + XCTAssertTrue(snapshot.exists); + FIRQuerySnapshot *querySnapshot = + [self readDocumentSetForRef:[query queryStartingAtDocument:snapshot]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[ + @{ @"v" : @"c", + @"sort" : @2.0 }, + @{ @"v" : @"d", + @"sort" : @2.0 } + ])); + + querySnapshot = [self readDocumentSetForRef:[query queryEndingBeforeDocument:snapshot]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[ + @{ @"v" : @"e", + @"sort" : @0.0 }, + @{ @"v" : @"a", + @"sort" : @1.0 }, + @{ @"v" : @"b", + @"sort" : @2.0 } + ])); +} + +- (void)testCanBeCreatedFromValues { + FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ + @"a" : @{@"v" : @"a", @"sort" : @1.0}, + @"b" : @{@"v" : @"b", @"sort" : @2.0}, + @"c" : @{@"v" : @"c", @"sort" : @2.0}, + @"d" : @{@"v" : @"d", @"sort" : @2.0}, + @"e" : @{@"v" : @"e", @"sort" : @0.0}, + @"f" : @{@"v" : @"f", @"nosort" : @1.0} // should not show up + }]; + + FIRQuery *query = [testCollection queryOrderedByField:@"sort"]; + FIRQuerySnapshot *querySnapshot = + [self readDocumentSetForRef:[query queryStartingAtValues:@[ @2.0 ]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[ + @{ @"v" : @"b", + @"sort" : @2.0 }, + @{ @"v" : @"c", + @"sort" : @2.0 }, + @{ @"v" : @"d", + @"sort" : @2.0 } + ])); + + querySnapshot = [self readDocumentSetForRef:[query queryEndingBeforeValues:@[ @2.0 ]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[ + @{ @"v" : @"e", + @"sort" : @0.0 }, + @{ @"v" : @"a", + @"sort" : @1.0 } + ])); +} + +- (void)testCanBeCreatedUsingDocumentId { + NSDictionary *testDocs = @{ + @"a" : @{@"k" : @"a"}, + @"b" : @{@"k" : @"b"}, + @"c" : @{@"k" : @"c"}, + @"d" : @{@"k" : @"d"}, + @"e" : @{@"k" : @"e"} + }; + FIRCollectionReference *writer = [[[[self firestore] collectionWithPath:@"parent-collection"] + documentWithAutoID] collectionWithPath:@"sub-collection"]; + [self writeAllDocuments:testDocs toCollection:writer]; + + FIRCollectionReference *reader = [[self firestore] collectionWithPath:writer.path]; + FIRQuerySnapshot *querySnapshot = + [self readDocumentSetForRef:[[[reader queryOrderedByFieldPath:[FIRFieldPath documentID]] + queryStartingAtValues:@[ @"b" ]] + queryEndingBeforeValues:@[ @"d" ]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), + (@[ @{@"k" : @"b"}, @{@"k" : @"c"} ])); +} + +- (void)testCanBeUsedWithReferenceValues { + FIRFirestore *db = [self firestore]; + + FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ + @"a" : @{@"k" : @"1a", @"ref" : [db documentWithPath:@"1/a"]}, + @"b" : @{@"k" : @"1b", @"ref" : [db documentWithPath:@"1/b"]}, + @"c" : @{@"k" : @"2a", @"ref" : [db documentWithPath:@"2/a"]}, + @"d" : @{@"k" : @"2b", @"ref" : [db documentWithPath:@"2/b"]}, + @"e" : @{@"k" : @"3a", @"ref" : [db documentWithPath:@"3/a"]}, + }]; + FIRQuery *query = [testCollection queryOrderedByField:@"ref"]; + FIRQuerySnapshot *querySnapshot = [self + readDocumentSetForRef:[[query queryStartingAfterValues:@[ [db documentWithPath:@"1/a"] ]] + queryEndingAtValues:@[ [db documentWithPath:@"2/b"] ]]]; + NSMutableArray<NSString *> *actual = [NSMutableArray array]; + [querySnapshot.documents enumerateObjectsUsingBlock:^(FIRDocumentSnapshot *_Nonnull doc, + NSUInteger idx, BOOL *_Nonnull stop) { + [actual addObject:doc.data[@"k"]]; + }]; + XCTAssertEqualObjects(actual, (@[ @"1b", @"2a", @"2b" ])); +} + +- (void)testCanBeUsedInDescendingQueries { + FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ + @"a" : @{@"v" : @"a", @"sort" : @1.0}, + @"b" : @{@"v" : @"b", @"sort" : @2.0}, + @"c" : @{@"v" : @"c", @"sort" : @2.0}, + @"d" : @{@"v" : @"d", @"sort" : @3.0}, + @"e" : @{@"v" : @"e", @"sort" : @0.0}, + @"f" : @{@"v" : @"f", @"nosort" : @1.0} // should not show up + }]; + FIRQuery *query = [[testCollection queryOrderedByField:@"sort" descending:YES] + queryOrderedByFieldPath:[FIRFieldPath documentID] + descending:YES]; + + FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[query queryStartingAtValues:@[ @2.0 ]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ + @{ @"v" : @"c", + @"sort" : @2.0 }, + @{ @"v" : @"b", + @"sort" : @2.0 }, + @{ @"v" : @"a", + @"sort" : @1.0 }, + @{ @"v" : @"e", + @"sort" : @0.0 } + ])); + + snapshot = [self readDocumentSetForRef:[query queryEndingBeforeValues:@[ @2.0 ]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ @{ @"v" : @"d", @"sort" : @3.0 } ])); +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m new file mode 100644 index 0000000..d5558cc --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m @@ -0,0 +1,741 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import Firestore; + +#import <XCTest/XCTest.h> + +#import "FSTIntegrationTestCase.h" + +@interface FIRDatabaseTests : FSTIntegrationTestCase +@end + +@implementation FIRDatabaseTests + +- (void)testCanUpdateAnExistingDocument { + FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"]; + NSDictionary<NSString *, id> *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + NSDictionary<NSString *, id> *updateData = + @{@"desc" : @"NewDescription", @"owner.email" : @"new@xyz.com"}; + NSDictionary<NSString *, id> *finalData = + @{ @"desc" : @"NewDescription", + @"owner" : @{@"name" : @"Jonny", @"email" : @"new@xyz.com"} }; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *updateCompletion = [self expectationWithDescription:@"updateData"]; + [doc updateData:updateData + completion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [updateCompletion fulfill]; + }]; + [self awaitExpectations]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertTrue(result.exists); + XCTAssertEqualObjects(result.data, finalData); +} + +- (void)testCanDeleteAFieldWithAnUpdate { + FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"]; + NSDictionary<NSString *, id> *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + NSDictionary<NSString *, id> *updateData = + @{@"owner.email" : [FIRFieldValue fieldValueForDelete]}; + NSDictionary<NSString *, id> *finalData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny"} }; + + [self writeDocumentRef:doc data:initialData]; + [self updateDocumentRef:doc data:updateData]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertTrue(result.exists); + XCTAssertEqualObjects(result.data, finalData); +} + +- (void)testDeleteDocument { + FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"]; + NSDictionary<NSString *, id> *data = @{@"value" : @"foo"}; + [self writeDocumentRef:doc data:data]; + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(result.data, data); + [self deleteDocumentRef:doc]; + result = [self readDocumentForRef:doc]; + XCTAssertFalse(result.exists); +} + +- (void)testCannotUpdateNonexistentDocument { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + XCTestExpectation *setCompletion = [self expectationWithDescription:@"setData"]; + [doc updateData:@{@"owner" : @"abc"} + completion:^(NSError *_Nullable error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound); + [setCompletion fulfill]; + }]; + [self awaitExpectations]; + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertFalse(result.exists); +} + +- (void)testCanOverwriteDataAnExistingDocumentUsingSet { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + NSDictionary<NSString *, id> *udpateData = @{@"desc" : @"NewDescription"}; + + [self writeDocumentRef:doc data:initialData]; + [self writeDocumentRef:doc data:udpateData]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(document.data, udpateData); +} + +- (void)testCanMergeDataWithAnExistingDocumentUsingSet { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = @{ + @"desc" : @"Description", + @"owner.data" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} + }; + NSDictionary<NSString *, id> *updateData = + @{ @"updated" : @YES, + @"owner.data" : @{@"name" : @"Sebastian"} }; + NSDictionary<NSString *, id> *finalData = @{ + @"desc" : @"Description", + @"updated" : @YES, + @"owner.data" : @{@"name" : @"Sebastian", @"email" : @"abc@xyz.com"} + }; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanMergeDataWithAnExistingDocumentUsingSet"]; + + [doc setData:updateData + options:[FIRSetOptions merge] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(document.data, finalData); +} + +- (void)testMergeReplacesArrays { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = @{ + @"untouched" : @YES, + @"data" : @"old", + @"topLevel" : @[ @"old", @"old" ], + @"mapInArray" : @[ @{@"data" : @"old"} ] + }; + NSDictionary<NSString *, id> *updateData = + @{ @"data" : @"new", + @"topLevel" : @[ @"new" ], + @"mapInArray" : @[ @{@"data" : @"new"} ] }; + NSDictionary<NSString *, id> *finalData = @{ + @"untouched" : @YES, + @"data" : @"new", + @"topLevel" : @[ @"new" ], + @"mapInArray" : @[ @{@"data" : @"new"} ] + }; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanMergeDataWithAnExistingDocumentUsingSet"]; + + [doc setData:updateData + options:[FIRSetOptions merge] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(document.data, finalData); +} + +- (void)testAddingToACollectionYieldsTheCorrectDocumentReference { + FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"]; + FIRDocumentReference *ref = [coll addDocumentWithData:@{ @"foo" : @1 }]; + + XCTestExpectation *getCompletion = [self expectationWithDescription:@"getData"]; + [ref getDocumentWithCompletion:^(FIRDocumentSnapshot *_Nullable document, + NSError *_Nullable error) { + XCTAssertNil(error); + XCTAssertEqualObjects(document.data, (@{ @"foo" : @1 })); + + [getCompletion fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testListenCanBeCalledMultipleTimes { + FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"]; + FIRDocumentReference *doc = [coll documentWithAutoID]; + + XCTestExpectation *completed = [self expectationWithDescription:@"multiple addSnapshotListeners"]; + + __block NSDictionary<NSString *, id> *resultingData; + + // Shut the compiler up about strong references to doc. + FIRDocumentReference *__weak weakDoc = doc; + + [doc setData:@{@"foo" : @"bar"} + completion:^(NSError *error1) { + XCTAssertNil(error1); + FIRDocumentReference *strongDoc = weakDoc; + + [strongDoc addSnapshotListener:^(FIRDocumentSnapshot *snapshot2, NSError *error2) { + XCTAssertNil(error2); + + FIRDocumentReference *strongDoc2 = weakDoc; + [strongDoc2 addSnapshotListener:^(FIRDocumentSnapshot *snapshot3, NSError *error3) { + XCTAssertNil(error3); + resultingData = snapshot3.data; + [completed fulfill]; + }]; + }]; + }]; + + [self awaitExpectations]; + XCTAssertEqualObjects(resultingData, @{@"foo" : @"bar"}); +} + +- (void)testDocumentSnapshotEvents_nonExistent { + FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + XCTestExpectation *snapshotCompletion = [self expectationWithDescription:@"snapshot"]; + __block int callbacks = 0; + + id<FIRListenerRegistration> listenerRegistration = + [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertNotNil(doc); + XCTAssertFalse(doc.exists); + [snapshotCompletion fulfill]; + + } else if (callbacks == 2) { + XCTFail("Should not have received this callback"); + } + }]; + + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testDocumentSnapshotEvents_forAdd { + FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + XCTestExpectation *emptyCompletion = [self expectationWithDescription:@"empty snapshot"]; + __block XCTestExpectation *dataCompletion; + __block int callbacks = 0; + + id<FIRListenerRegistration> listenerRegistration = + [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertNotNil(doc); + XCTAssertFalse(doc.exists); + [emptyCompletion fulfill]; + + } else if (callbacks == 2) { + XCTAssertEqualObjects(doc.data, (@{ @"a" : @1 })); + XCTAssertEqual(doc.metadata.hasPendingWrites, YES); + [dataCompletion fulfill]; + + } else if (callbacks == 3) { + XCTFail("Should not have received this callback"); + } + }]; + + [self awaitExpectations]; + dataCompletion = [self expectationWithDescription:@"data snapshot"]; + + [docRef setData:@{ @"a" : @1 }]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testDocumentSnapshotEvents_forAddIncludingMetadata { + FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + XCTestExpectation *emptyCompletion = [self expectationWithDescription:@"empty snapshot"]; + __block XCTestExpectation *dataCompletion; + __block int callbacks = 0; + + FIRDocumentListenOptions *options = + [[FIRDocumentListenOptions options] includeMetadataChanges:YES]; + + id<FIRListenerRegistration> listenerRegistration = + [docRef addSnapshotListenerWithOptions:options + listener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertNotNil(doc); + XCTAssertFalse(doc.exists); + [emptyCompletion fulfill]; + + } else if (callbacks == 2) { + XCTAssertEqualObjects(doc.data, (@{ @"a" : @1 })); + XCTAssertEqual(doc.metadata.hasPendingWrites, YES); + + } else if (callbacks == 3) { + XCTAssertEqualObjects(doc.data, (@{ @"a" : @1 })); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + [dataCompletion fulfill]; + + } else if (callbacks == 4) { + XCTFail("Should not have received this callback"); + } + }]; + + [self awaitExpectations]; + dataCompletion = [self expectationWithDescription:@"data snapshot"]; + + [docRef setData:@{ @"a" : @1 }]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testDocumentSnapshotEvents_forChange { + FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 }; + NSDictionary<NSString *, id> *changedData = @{ @"b" : @2 }; + + [self writeDocumentRef:docRef data:initialData]; + + XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; + __block XCTestExpectation *changeCompletion; + __block int callbacks = 0; + + id<FIRListenerRegistration> listenerRegistration = + [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertEqualObjects(doc.data, initialData); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + [initialCompletion fulfill]; + + } else if (callbacks == 2) { + XCTAssertEqualObjects(doc.data, changedData); + XCTAssertEqual(doc.metadata.hasPendingWrites, YES); + [changeCompletion fulfill]; + + } else if (callbacks == 3) { + XCTFail("Should not have received this callback"); + } + }]; + + [self awaitExpectations]; + changeCompletion = [self expectationWithDescription:@"listen for changed data"]; + + [docRef setData:changedData]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testDocumentSnapshotEvents_forChangeIncludingMetadata { + FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 }; + NSDictionary<NSString *, id> *changedData = @{ @"b" : @2 }; + + [self writeDocumentRef:docRef data:initialData]; + + XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; + __block XCTestExpectation *changeCompletion; + __block int callbacks = 0; + + FIRDocumentListenOptions *options = + [[FIRDocumentListenOptions options] includeMetadataChanges:YES]; + + id<FIRListenerRegistration> listenerRegistration = + [docRef addSnapshotListenerWithOptions:options + listener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertEqualObjects(doc.data, initialData); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + XCTAssertEqual(doc.metadata.isFromCache, YES); + + } else if (callbacks == 2) { + XCTAssertEqualObjects(doc.data, initialData); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + XCTAssertEqual(doc.metadata.isFromCache, NO); + [initialCompletion fulfill]; + + } else if (callbacks == 3) { + XCTAssertEqualObjects(doc.data, changedData); + XCTAssertEqual(doc.metadata.hasPendingWrites, YES); + XCTAssertEqual(doc.metadata.isFromCache, NO); + + } else if (callbacks == 4) { + XCTAssertEqualObjects(doc.data, changedData); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + XCTAssertEqual(doc.metadata.isFromCache, NO); + [changeCompletion fulfill]; + + } else if (callbacks == 5) { + XCTFail("Should not have received this callback"); + } + }]; + + [self awaitExpectations]; + changeCompletion = [self expectationWithDescription:@"listen for changed data"]; + + [docRef setData:changedData]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testDocumentSnapshotEvents_forDelete { + FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 }; + + [self writeDocumentRef:docRef data:initialData]; + + XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; + __block XCTestExpectation *changeCompletion; + __block int callbacks = 0; + + id<FIRListenerRegistration> listenerRegistration = + [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertEqualObjects(doc.data, initialData); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + XCTAssertEqual(doc.metadata.isFromCache, YES); + [initialCompletion fulfill]; + + } else if (callbacks == 2) { + XCTAssertFalse(doc.exists); + [changeCompletion fulfill]; + + } else if (callbacks == 3) { + XCTFail("Should not have received this callback"); + } + }]; + + [self awaitExpectations]; + changeCompletion = [self expectationWithDescription:@"listen for changed data"]; + + [docRef deleteDocument]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testDocumentSnapshotEvents_forDeleteIncludingMetadata { + FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 }; + + [self writeDocumentRef:docRef data:initialData]; + + FIRDocumentListenOptions *options = + [[FIRDocumentListenOptions options] includeMetadataChanges:YES]; + + XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; + __block XCTestExpectation *changeCompletion; + __block int callbacks = 0; + + id<FIRListenerRegistration> listenerRegistration = + [docRef addSnapshotListenerWithOptions:options + listener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertEqualObjects(doc.data, initialData); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + XCTAssertEqual(doc.metadata.isFromCache, YES); + + } else if (callbacks == 2) { + XCTAssertEqualObjects(doc.data, initialData); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + XCTAssertEqual(doc.metadata.isFromCache, NO); + [initialCompletion fulfill]; + + } else if (callbacks == 3) { + XCTAssertFalse(doc.exists); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + XCTAssertEqual(doc.metadata.isFromCache, NO); + [changeCompletion fulfill]; + + } else if (callbacks == 4) { + XCTFail("Should not have received this callback"); + } + }]; + + [self awaitExpectations]; + changeCompletion = [self expectationWithDescription:@"listen for changed data"]; + + [docRef deleteDocument]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testQuerySnapshotEvents_forAdd { + FIRCollectionReference *roomsRef = [self collectionRef]; + FIRDocumentReference *docRef = [roomsRef documentWithAutoID]; + + NSDictionary<NSString *, id> *newData = @{ @"a" : @1 }; + + XCTestExpectation *emptyCompletion = [self expectationWithDescription:@"empty snapshot"]; + __block XCTestExpectation *changeCompletion; + __block int callbacks = 0; + + id<FIRListenerRegistration> listenerRegistration = + [roomsRef addSnapshotListener:^(FIRQuerySnapshot *_Nullable docSet, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertEqual(docSet.count, 0); + [emptyCompletion fulfill]; + + } else if (callbacks == 2) { + XCTAssertEqual(docSet.count, 1); + XCTAssertEqualObjects(docSet.documents[0].data, newData); + XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, YES); + [changeCompletion fulfill]; + + } else if (callbacks == 3) { + XCTFail("Should not have received a third callback"); + } + }]; + + [self awaitExpectations]; + changeCompletion = [self expectationWithDescription:@"changed snapshot"]; + + [docRef setData:newData]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testQuerySnapshotEvents_forChange { + FIRCollectionReference *roomsRef = [self collectionRef]; + FIRDocumentReference *docRef = [roomsRef documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 }; + NSDictionary<NSString *, id> *changedData = @{ @"b" : @2 }; + + [self writeDocumentRef:docRef data:initialData]; + + XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; + __block XCTestExpectation *changeCompletion; + __block int callbacks = 0; + + id<FIRListenerRegistration> listenerRegistration = + [roomsRef addSnapshotListener:^(FIRQuerySnapshot *_Nullable docSet, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertEqual(docSet.count, 1); + XCTAssertEqualObjects(docSet.documents[0].data, initialData); + XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, NO); + [initialCompletion fulfill]; + + } else if (callbacks == 2) { + XCTAssertEqual(docSet.count, 1); + XCTAssertEqualObjects(docSet.documents[0].data, changedData); + XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, YES); + [changeCompletion fulfill]; + + } else if (callbacks == 3) { + XCTFail("Should not have received a third callback"); + } + }]; + + [self awaitExpectations]; + changeCompletion = [self expectationWithDescription:@"listen for changed data"]; + + [docRef setData:changedData]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testQuerySnapshotEvents_forDelete { + FIRCollectionReference *roomsRef = [self collectionRef]; + FIRDocumentReference *docRef = [roomsRef documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 }; + + [self writeDocumentRef:docRef data:initialData]; + + XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; + __block XCTestExpectation *changeCompletion; + __block int callbacks = 0; + + id<FIRListenerRegistration> listenerRegistration = + [roomsRef addSnapshotListener:^(FIRQuerySnapshot *_Nullable docSet, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertEqual(docSet.count, 1); + XCTAssertEqualObjects(docSet.documents[0].data, initialData); + XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, NO); + [initialCompletion fulfill]; + + } else if (callbacks == 2) { + XCTAssertEqual(docSet.count, 0); + [changeCompletion fulfill]; + + } else if (callbacks == 4) { + XCTFail("Should not have received a third callback"); + } + }]; + + [self awaitExpectations]; + changeCompletion = [self expectationWithDescription:@"listen for changed data"]; + + [docRef deleteDocument]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testExposesFirestoreOnDocumentReferences { + FIRDocumentReference *doc = [self.db documentWithPath:@"foo/bar"]; + XCTAssertEqual(doc.firestore, self.db); +} + +- (void)testExposesFirestoreOnQueries { + FIRQuery *q = [[self.db collectionWithPath:@"foo"] queryLimitedTo:5]; + XCTAssertEqual(q.firestore, self.db); +} + +- (void)testCanTraverseCollectionsAndDocuments { + NSString *expected = @"a/b/c/d"; + // doc path from root Firestore. + XCTAssertEqualObjects([self.db documentWithPath:@"a/b/c/d"].path, expected); + // collection path from root Firestore. + XCTAssertEqualObjects([[self.db collectionWithPath:@"a/b/c"] documentWithPath:@"d"].path, + expected); + // doc path from CollectionReference. + XCTAssertEqualObjects([[self.db collectionWithPath:@"a"] documentWithPath:@"b/c/d"].path, + expected); + // collection path from DocumentReference. + XCTAssertEqualObjects([[self.db documentWithPath:@"a/b"] collectionWithPath:@"c/d/e"].path, + @"a/b/c/d/e"); +} + +- (void)testCanTraverseCollectionAndDocumentParents { + FIRCollectionReference *collection = [self.db collectionWithPath:@"a/b/c"]; + XCTAssertEqualObjects(collection.path, @"a/b/c"); + + FIRDocumentReference *doc = collection.parent; + XCTAssertEqualObjects(doc.path, @"a/b"); + + collection = doc.parent; + XCTAssertEqualObjects(collection.path, @"a"); + + FIRDocumentReference *nilDoc = collection.parent; + XCTAssertNil(nilDoc); +} + +- (void)testUpdateFieldsWithDots { + FIRDocumentReference *doc = [self documentRef]; + + [self writeDocumentRef:doc data:@{@"a.b" : @"old", @"c.d" : @"old"}]; + + [self updateDocumentRef:doc data:@{ [[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new" }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateFieldsWithDots"]; + + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"})); + [expectation fulfill]; + }]; + + [self awaitExpectations]; +} + +- (void)testUpdateNestedFields { + FIRDocumentReference *doc = [self documentRef]; + + [self writeDocumentRef:doc + data:@{ + @"a" : @{@"b" : @"old"}, + @"c" : @{@"d" : @"old"}, + @"e" : @{@"f" : @"old"} + }]; + + [self updateDocumentRef:doc + data:@{ + @"a.b" : @"new", + [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new" + }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateNestedFields"]; + + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(snapshot.data, (@{ + @"a" : @{@"b" : @"new"}, + @"c" : @{@"d" : @"new"}, + @"e" : @{@"f" : @"old"} + })); + [expectation fulfill]; + }]; + + [self awaitExpectations]; +} + +- (void)testCollectionID { + XCTAssertEqualObjects([self.db collectionWithPath:@"foo"].collectionID, @"foo"); + XCTAssertEqualObjects([self.db collectionWithPath:@"foo/bar/baz"].collectionID, @"baz"); +} + +- (void)testDocumentID { + XCTAssertEqualObjects([self.db documentWithPath:@"foo/bar"].documentID, @"bar"); + XCTAssertEqualObjects([self.db documentWithPath:@"foo/bar/baz/qux"].documentID, @"qux"); +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRFieldsTests.m b/Firestore/Example/Tests/Integration/API/FIRFieldsTests.m new file mode 100644 index 0000000..d851556 --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRFieldsTests.m @@ -0,0 +1,223 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import Firestore; + +#import <XCTest/XCTest.h> + +#import "Core/FSTFirestoreClient.h" + +#import "FSTIntegrationTestCase.h" + +@interface FIRFieldsTests : FSTIntegrationTestCase +@end + +@implementation FIRFieldsTests + +- (NSDictionary<NSString *, id> *)testNestedDataNumbered:(int)number { + return @{ + @"name" : [NSString stringWithFormat:@"room %d", number], + @"metadata" : @{ + @"createdAt" : @(number), + @"deep" : @{@"field" : [NSString stringWithFormat:@"deep-field-%d", number]} + } + }; +} + +- (void)testNestedFieldsCanBeWrittenWithSet { + NSDictionary<NSString *, id> *testData = [self testNestedDataNumbered:1]; + + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:testData]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(result.data, testData); +} + +- (void)testNestedFieldsCanBeReadDirectly { + NSDictionary<NSString *, id> *testData = [self testNestedDataNumbered:1]; + + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:testData]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(result[@"name"], testData[@"name"]); + XCTAssertEqualObjects(result[@"metadata"], testData[@"metadata"]); + XCTAssertEqualObjects(result[@"metadata.deep.field"], testData[@"metadata"][@"deep"][@"field"]); + XCTAssertNil(result[@"metadata.nofield"]); + XCTAssertNil(result[@"nometadata.nofield"]); +} + +- (void)testNestedFieldsCanBeUpdated { + NSDictionary<NSString *, id> *testData = [self testNestedDataNumbered:1]; + + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:testData]; + [self updateDocumentRef:doc data:@{ @"metadata.deep.field" : @100, @"metadata.added" : @200 }]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertEqualObjects( + result.data, (@{ + @"name" : @"room 1", + @"metadata" : @{@"createdAt" : @1, @"deep" : @{@"field" : @100}, @"added" : @200} + })); +} + +- (void)testNestedFieldsCanBeUsedInQueryFilters { + NSDictionary<NSString *, NSDictionary<NSString *, id> *> *testDocs = @{ + @"1" : [self testNestedDataNumbered:300], + @"2" : [self testNestedDataNumbered:100], + @"3" : [self testNestedDataNumbered:200] + }; + + // inequality adds implicit sort on field + NSArray<NSDictionary<NSString *, id> *> *expected = + @[ [self testNestedDataNumbered:200], [self testNestedDataNumbered:300] ]; + FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs]; + + FIRQuery *q = [coll queryWhereField:@"metadata.createdAt" isGreaterThanOrEqualTo:@200]; + FIRQuerySnapshot *results = [self readDocumentSetForRef:q]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (expected)); +} + +- (void)testNestedFieldsCanBeUsedInOrderBy { + NSDictionary<NSString *, NSDictionary<NSString *, id> *> *testDocs = @{ + @"1" : [self testNestedDataNumbered:300], + @"2" : [self testNestedDataNumbered:100], + @"3" : [self testNestedDataNumbered:200] + }; + FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs]; + + XCTestExpectation *queryCompletion = [self expectationWithDescription:@"query"]; + FIRQuery *q = [coll queryOrderedByField:@"metadata.createdAt"]; + [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (@[ + [self testNestedDataNumbered:100], [self testNestedDataNumbered:200], + [self testNestedDataNumbered:300] + ])); + [queryCompletion fulfill]; + }]; + [self awaitExpectations]; +} + +/** + * Creates test data with special characters in field names. Datastore currently prohibits mixing + * nested data with special characters so tests that use this data must be separate. + */ +- (NSDictionary<NSString *, id> *)testDottedDataNumbered:(int)number { + return @{ + @"a" : [NSString stringWithFormat:@"field %d", number], + @"b.dot" : @(number), + @"c\\slash" : @(number) + }; +} + +- (void)testFieldsWithSpecialCharsCanBeWrittenWithSet { + NSDictionary<NSString *, id> *testData = [self testDottedDataNumbered:1]; + + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:testData]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(result.data, testData); +} + +- (void)testFieldsWithSpecialCharsCanBeReadDirectly { + NSDictionary<NSString *, id> *testData = [self testDottedDataNumbered:1]; + + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:testData]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(result[@"a"], testData[@"a"]); + XCTAssertEqualObjects(result[[[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]]], + testData[@"b.dot"]); + XCTAssertEqualObjects(result[@"c\\slash"], testData[@"c\\slash"]); +} + +- (void)testFieldsWithSpecialCharsCanBeUpdated { + NSDictionary<NSString *, id> *testData = [self testDottedDataNumbered:1]; + + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:testData]; + [self updateDocumentRef:doc + data:@{ + [[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]] : @100, + @"c\\slash" : @200 + }]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(result.data, (@{ @"a" : @"field 1", @"b.dot" : @100, @"c\\slash" : @200 })); +} + +- (void)testFieldsWithSpecialCharsCanBeUsedInQueryFilters { + NSDictionary<NSString *, NSDictionary<NSString *, id> *> *testDocs = @{ + @"1" : [self testDottedDataNumbered:300], + @"2" : [self testDottedDataNumbered:100], + @"3" : [self testDottedDataNumbered:200] + }; + + // inequality adds implicit sort on field + NSArray<NSDictionary<NSString *, id> *> *expected = + @[ [self testDottedDataNumbered:200], [self testDottedDataNumbered:300] ]; + FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs]; + + XCTestExpectation *queryCompletion = [self expectationWithDescription:@"query"]; + FIRQuery *q = [coll queryWhereFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]] + isGreaterThanOrEqualTo:@200]; + [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), expected); + [queryCompletion fulfill]; + }]; + + [self awaitExpectations]; +} + +- (void)testFieldsWithSpecialCharsCanBeUsedInOrderBy { + NSDictionary<NSString *, NSDictionary<NSString *, id> *> *testDocs = @{ + @"1" : [self testDottedDataNumbered:300], + @"2" : [self testDottedDataNumbered:100], + @"3" : [self testDottedDataNumbered:200] + }; + + NSArray<NSDictionary<NSString *, id> *> *expected = @[ + [self testDottedDataNumbered:100], [self testDottedDataNumbered:200], + [self testDottedDataNumbered:300] + ]; + FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs]; + + FIRQuery *q = [coll queryOrderedByFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]]]; + XCTestExpectation *queryDot = [self expectationWithDescription:@"query dot"]; + [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), expected); + [queryDot fulfill]; + }]; + [self awaitExpectations]; + + XCTestExpectation *querySlash = [self expectationWithDescription:@"query slash"]; + q = [coll queryOrderedByField:@"c\\slash"]; + [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), expected); + [querySlash fulfill]; + }]; + [self awaitExpectations]; +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m b/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m new file mode 100644 index 0000000..19771ff --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m @@ -0,0 +1,129 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import Firestore; + +#import <XCTest/XCTest.h> + +#import "FSTIntegrationTestCase.h" + +@interface FIRListenerRegistrationTests : FSTIntegrationTestCase +@end + +@implementation FIRListenerRegistrationTests + +- (void)testCanBeRemoved { + FIRCollectionReference *collectionRef = [self collectionRef]; + FIRDocumentReference *docRef = [collectionRef documentWithAutoID]; + + __block int callbacks = 0; + id<FIRListenerRegistration> one = [collectionRef + addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) { + XCTAssertNil(error); + callbacks++; + }]; + + id<FIRListenerRegistration> two = [collectionRef + addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) { + XCTAssertNil(error); + callbacks++; + }]; + + // Wait for initial events + [self waitUntil:^BOOL { + return callbacks == 2; + }]; + + // Trigger new events + [self writeDocumentRef:docRef data:@{@"foo" : @"bar"}]; + + // Write events should have triggered + XCTAssertEqual(4, callbacks); + + // No more events should occur + [one remove]; + [two remove]; + + [self writeDocumentRef:docRef data:@{@"foo" : @"new-bar"}]; + + // Assert no further events occurred + XCTAssertEqual(4, callbacks); +} + +- (void)testCanBeRemovedTwice { + FIRCollectionReference *collectionRef = [self collectionRef]; + FIRDocumentReference *docRef = [collectionRef documentWithAutoID]; + + id<FIRListenerRegistration> one = [collectionRef + addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error){ + }]; + id<FIRListenerRegistration> two = [docRef + addSnapshotListener:^(FIRDocumentSnapshot *_Nullable snapshot, NSError *_Nullable error){ + }]; + + [one remove]; + [one remove]; + + [two remove]; + [two remove]; +} + +- (void)testCanBeRemovedIndependently { + FIRCollectionReference *collectionRef = [self collectionRef]; + FIRDocumentReference *docRef = [collectionRef documentWithAutoID]; + + __block int callbacksOne = 0; + __block int callbacksTwo = 0; + id<FIRListenerRegistration> one = [collectionRef + addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) { + XCTAssertNil(error); + callbacksOne++; + }]; + + id<FIRListenerRegistration> two = [collectionRef + addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) { + XCTAssertNil(error); + callbacksTwo++; + }]; + + // Wait for initial events + [self waitUntil:^BOOL { + return callbacksOne == 1 && callbacksTwo == 1; + }]; + + // Trigger new events + [self writeDocumentRef:docRef data:@{@"foo" : @"bar"}]; + + // Write events should have triggered + XCTAssertEqual(2, callbacksOne); + XCTAssertEqual(2, callbacksTwo); + + // Should leave "two" unaffected + [one remove]; + + [self writeDocumentRef:docRef data:@{@"foo" : @"new-bar"}]; + + // Assert only events for "two" actually occurred + XCTAssertEqual(2, callbacksOne); + XCTAssertEqual(3, callbacksTwo); + + [self writeDocumentRef:docRef data:@{@"foo" : @"new-bar"}]; + + // No more events should occur + [two remove]; +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRQueryTests.m b/Firestore/Example/Tests/Integration/API/FIRQueryTests.m new file mode 100644 index 0000000..f08df33 --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRQueryTests.m @@ -0,0 +1,197 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import Firestore; + +#import <XCTest/XCTest.h> + +#import "Core/FSTFirestoreClient.h" + +#import "FSTIntegrationTestCase.h" + +@interface FIRQueryTests : FSTIntegrationTestCase +@end + +@implementation FIRQueryTests + +- (void)testLimitQueries { + FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ + @"a" : @{@"k" : @"a"}, + @"b" : @{@"k" : @"b"}, + @"c" : @{@"k" : @"c"} + + }]; + FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[collRef queryLimitedTo:2]]; + + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ @{@"k" : @"a"}, @{@"k" : @"b"} ])); +} + +- (void)testLimitQueriesWithDescendingSortOrder { + FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ + @"a" : @{@"k" : @"a", @"sort" : @0}, + @"b" : @{@"k" : @"b", @"sort" : @1}, + @"c" : @{@"k" : @"c", @"sort" : @1}, + @"d" : @{@"k" : @"d", @"sort" : @2}, + + }]; + FIRQuerySnapshot *snapshot = + [self readDocumentSetForRef:[[collRef queryOrderedByField:@"sort" descending:YES] + queryLimitedTo:2]]; + + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ + @{ @"k" : @"d", + @"sort" : @2 }, + @{ @"k" : @"c", + @"sort" : @1 } + ])); +} + +- (void)testKeyOrderIsDescendingForDescendingInequality { + FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ + @"a" : @{@"foo" : @42}, + @"b" : @{@"foo" : @42.0}, + @"c" : @{@"foo" : @42}, + @"d" : @{@"foo" : @21}, + @"e" : @{@"foo" : @21.0}, + @"f" : @{@"foo" : @66}, + @"g" : @{@"foo" : @66.0}, + }]; + FIRQuerySnapshot *snapshot = + [self readDocumentSetForRef:[[collRef queryWhereField:@"foo" isGreaterThan:@21] + queryOrderedByField:@"foo" + descending:YES]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetIDs(snapshot), (@[ @"g", @"f", @"c", @"b", @"a" ])); +} + +- (void)testUnaryFilterQueries { + FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ + @"a" : @{@"null" : [NSNull null], @"nan" : @(NAN)}, + @"b" : @{@"null" : [NSNull null], @"nan" : @0}, + @"c" : @{@"null" : @NO, @"nan" : @(NAN)} + }]; + + FIRQuerySnapshot *results = + [self readDocumentSetForRef:[[collRef queryWhereField:@"null" isEqualTo:[NSNull null]] + queryWhereField:@"nan" + isEqualTo:@(NAN)]]; + + XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (@[ + @{ @"null" : [NSNull null], + @"nan" : @(NAN) } + ])); +} + +- (void)testQueryWithFieldPaths { + FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ + @"a" : @{@"a" : @1}, + @"b" : @{@"a" : @2}, + @"c" : @{@"a" : @3} + }]; + + FIRQuery *query = + [collRef queryWhereFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"a" ]] isLessThan:@3]; + query = [query queryOrderedByFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"a" ]] + descending:YES]; + + FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:query]; + + XCTAssertEqualObjects(FIRQuerySnapshotGetIDs(snapshot), (@[ @"b", @"a" ])); +} + +- (void)testFilterOnInfinity { + FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ + @"a" : @{@"inf" : @(INFINITY)}, + @"b" : @{@"inf" : @(-INFINITY)} + }]; + + FIRQuerySnapshot *results = + [self readDocumentSetForRef:[collRef queryWhereField:@"inf" isEqualTo:@(INFINITY)]]; + + XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (@[ @{ @"inf" : @(INFINITY) } ])); +} + +- (void)testCanExplicitlySortByDocumentID { + NSDictionary *testDocs = @{ + @"a" : @{@"key" : @"a"}, + @"b" : @{@"key" : @"b"}, + @"c" : @{@"key" : @"c"}, + }; + FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs]; + + // Ideally this would be descending to validate it's different than + // the default, but that requires an extra index + FIRQuerySnapshot *docs = + [self readDocumentSetForRef:[collection queryOrderedByFieldPath:[FIRFieldPath documentID]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), + (@[ testDocs[@"a"], testDocs[@"b"], testDocs[@"c"] ])); +} + +- (void)testCanQueryByDocumentID { + NSDictionary *testDocs = @{ + @"aa" : @{@"key" : @"aa"}, + @"ab" : @{@"key" : @"ab"}, + @"ba" : @{@"key" : @"ba"}, + @"bb" : @{@"key" : @"bb"}, + }; + FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs]; + FIRQuerySnapshot *docs = + [self readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID] + isEqualTo:@"ab"]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"] ])); +} + +- (void)testCanQueryByDocumentIDs { + NSDictionary *testDocs = @{ + @"aa" : @{@"key" : @"aa"}, + @"ab" : @{@"key" : @"ab"}, + @"ba" : @{@"key" : @"ba"}, + @"bb" : @{@"key" : @"bb"}, + }; + FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs]; + FIRQuerySnapshot *docs = + [self readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID] + isEqualTo:@"ab"]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"] ])); + + docs = [self readDocumentSetForRef:[[collection queryWhereFieldPath:[FIRFieldPath documentID] + isGreaterThan:@"aa"] + queryWhereFieldPath:[FIRFieldPath documentID] + isLessThanOrEqualTo:@"ba"]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"], testDocs[@"ba"] ])); +} + +- (void)testCanQueryByDocumentIDsUsingRefs { + NSDictionary *testDocs = @{ + @"aa" : @{@"key" : @"aa"}, + @"ab" : @{@"key" : @"ab"}, + @"ba" : @{@"key" : @"ba"}, + @"bb" : @{@"key" : @"bb"}, + }; + FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs]; + FIRQuerySnapshot *docs = [self + readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID] + isEqualTo:[collection documentWithPath:@"ab"]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"] ])); + + docs = [self + readDocumentSetForRef:[[collection queryWhereFieldPath:[FIRFieldPath documentID] + isGreaterThan:[collection documentWithPath:@"aa"]] + queryWhereFieldPath:[FIRFieldPath documentID] + isLessThanOrEqualTo:[collection documentWithPath:@"ba"]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"], testDocs[@"ba"] ])); +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m new file mode 100644 index 0000000..1d77e16 --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m @@ -0,0 +1,183 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import Firestore; + +#import <XCTest/XCTest.h> + +#import "Core/FSTFirestoreClient.h" + +#import "FSTEventAccumulator.h" +#import "FSTIntegrationTestCase.h" + +@interface FIRServerTimestampTests : FSTIntegrationTestCase +@end + +@implementation FIRServerTimestampTests { + // Data written in tests via set. + NSDictionary *_setData; + + // Base and update data used for update tests. + NSDictionary *_initialData; + NSDictionary *_updateData; + + // A document reference to read and write to. + FIRDocumentReference *_docRef; + + // Accumulator used to capture events during the test. + FSTEventAccumulator *_accumulator; + + // Listener registration for a listener maintained during the course of the test. + id<FIRListenerRegistration> _listenerRegistration; +} + +- (void)setUp { + [super setUp]; + + // Data written in tests via set. + _setData = @{ + @"a" : @42, + @"when" : [FIRFieldValue fieldValueForServerTimestamp], + @"deep" : @{@"when" : [FIRFieldValue fieldValueForServerTimestamp]} + }; + + // Base and update data used for update tests. + _initialData = @{ @"a" : @42 }; + _updateData = @{ + @"when" : [FIRFieldValue fieldValueForServerTimestamp], + @"deep" : @{@"when" : [FIRFieldValue fieldValueForServerTimestamp]} + }; + + _docRef = [self documentRef]; + _accumulator = [FSTEventAccumulator accumulatorForTest:self]; + _listenerRegistration = [_docRef addSnapshotListener:_accumulator.handler]; + + // Wait for initial nil snapshot to avoid potential races. + FIRDocumentSnapshot *initialSnapshot = [_accumulator awaitEventWithName:@"initial event"]; + XCTAssertFalse(initialSnapshot.exists); +} + +- (void)tearDown { + [_listenerRegistration remove]; + + [super tearDown]; +} + +// Returns the expected data, with an arbitrary timestamp substituted in. +- (NSDictionary *)expectedDataWithTimestamp:(id _Nullable)timestamp { + return @{ @"a" : @42, @"when" : timestamp, @"deep" : @{@"when" : timestamp} }; +} + +/** Writes _initialData and waits for the corresponding snapshot. */ +- (void)writeInitialData { + [self writeDocumentRef:_docRef data:_initialData]; + FIRDocumentSnapshot *initialDataSnap = [_accumulator awaitEventWithName:@"Initial data event."]; + XCTAssertEqualObjects(initialDataSnap.data, _initialData); +} + +/** Waits for a snapshot containing _setData but with NSNull for the timestamps. */ +- (void)waitForLocalEvent { + FIRDocumentSnapshot *localSnap = [_accumulator awaitEventWithName:@"Local event."]; + XCTAssertEqualObjects(localSnap.data, [self expectedDataWithTimestamp:[NSNull null]]); +} + +/** Waits for a snapshot containing _setData but with resolved server timestamps. */ +- (void)waitForRemoteEvent { + // server event should have a resolved timestamp; verify it. + FIRDocumentSnapshot *remoteSnap = [_accumulator awaitEventWithName:@"Remote event"]; + XCTAssertTrue(remoteSnap.exists); + NSDate *when = remoteSnap[@"when"]; + XCTAssertTrue([when isKindOfClass:[NSDate class]]); + // Tolerate up to 10 seconds of clock skew between client and server. + XCTAssertEqualWithAccuracy(when.timeIntervalSinceNow, 0, 10); + + // Validate the rest of the document. + XCTAssertEqualObjects(remoteSnap.data, [self expectedDataWithTimestamp:when]); +} + +- (void)runTransactionBlock:(void (^)(FIRTransaction *transaction))transactionBlock { + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction complete"]; + [_docRef.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) { + transactionBlock(transaction); + return nil; + } + completion:^(id result, NSError *error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testServerTimestampsWorkViaSet { + [self writeDocumentRef:_docRef data:_setData]; + [self waitForLocalEvent]; + [self waitForRemoteEvent]; +} + +- (void)testServerTimestampsWorkViaUpdate { + [self writeInitialData]; + [self updateDocumentRef:_docRef data:_updateData]; + [self waitForLocalEvent]; + [self waitForRemoteEvent]; +} + +- (void)testServerTimestampsWorkViaTransactionSet { + [self runTransactionBlock:^(FIRTransaction *transaction) { + [transaction setData:_setData forDocument:_docRef]; + }]; + + [self waitForRemoteEvent]; +} + +- (void)testServerTimestampsWorkViaTransactionUpdate { + [self writeInitialData]; + [self runTransactionBlock:^(FIRTransaction *transaction) { + [transaction updateData:_updateData forDocument:_docRef]; + }]; + [self waitForRemoteEvent]; +} + +- (void)testServerTimestampsFailViaUpdateOnNonexistentDocument { + XCTestExpectation *expectation = [self expectationWithDescription:@"update complete"]; + [_docRef updateData:_updateData + completion:^(NSError *error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testServerTimestampsFailViaTransactionUpdateOnNonexistentDocument { + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction complete"]; + [_docRef.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) { + [transaction updateData:_updateData forDocument:_docRef]; + return nil; + } + completion:^(id result, NSError *error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + // TODO(b/35201829): This should be NotFound, but right now we retry transactions on any + // error and so this turns into Aborted instead. + // TODO(mikelehen): Actually it's FailedPrecondition, unlike Android. What do we want??? + XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRTypeTests.m b/Firestore/Example/Tests/Integration/API/FIRTypeTests.m new file mode 100644 index 0000000..f3021dd --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRTypeTests.m @@ -0,0 +1,79 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import Firestore; + +#import <XCTest/XCTest.h> + +#import "FSTIntegrationTestCase.h" + +@interface FIRTypeTests : FSTIntegrationTestCase +@end + +@implementation FIRTypeTests + +- (void)assertSuccessfulRoundtrip:(NSDictionary *)data { + FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"]; + + [self writeDocumentRef:doc data:data]; + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertTrue(document.exists); + XCTAssertEqualObjects(document.data, data); +} + +- (void)testCanReadAndWriteNullFields { + [self assertSuccessfulRoundtrip:@{ @"a" : @1, @"b" : [NSNull null] }]; +} + +- (void)testCanReadAndWriteArrayFields { + [self assertSuccessfulRoundtrip:@{ + @"array" : @[ @1, @"foo", @{@"deep" : @YES}, [NSNull null] ] + }]; +} + +- (void)testCanReadAndWriteBlobFields { + NSData *data = [NSData dataWithBytes:"\0\1\2" length:3]; + [self assertSuccessfulRoundtrip:@{@"blob" : data}]; +} + +- (void)testCanReadAndWriteGeoPointFields { + [self assertSuccessfulRoundtrip:@{ + @"geoPoint" : [[FIRGeoPoint alloc] initWithLatitude:1.23 longitude:4.56] + }]; +} + +- (void)testCanReadAndWriteTimestampFields { + // Choose a value that can be converted losslessly between fixed point and double + NSDate *timestamp = [NSDate dateWithTimeIntervalSince1970:1491847082.125]; + [self assertSuccessfulRoundtrip:@{@"timestamp" : timestamp}]; +} + +- (void)testCanReadAndWriteDocumentReferences { + // We can't use assertSuccessfulRoundtrip since FIRDocumentReference doesn't implement isEqual. + FIRDocumentReference *docRef = [self.db documentWithPath:@"rooms/eros"]; + id data = @{ @"a" : @42, @"ref" : docRef }; + [self writeDocumentRef:docRef data:data]; + + FIRDocumentSnapshot *readDoc = [self readDocumentForRef:docRef]; + XCTAssertTrue(readDoc.exists); + + XCTAssertEqualObjects(readDoc[@"a"], data[@"a"]); + FIRDocumentReference *readDocRef = readDoc[@"ref"]; + XCTAssertTrue([readDocRef isKindOfClass:[FIRDocumentReference class]]); + XCTAssertEqualObjects(readDocRef.path, docRef.path); +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRValidationTests.m b/Firestore/Example/Tests/Integration/API/FIRValidationTests.m new file mode 100644 index 0000000..1ba1d7a --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRValidationTests.m @@ -0,0 +1,560 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import Firestore; + +#import <XCTest/XCTest.h> + +#import "FSTHelpers.h" +#import "FSTIntegrationTestCase.h" + +// We have tests for passing nil when nil is not supposed to be allowed. So suppress the warnings. +#pragma clang diagnostic ignored "-Wnonnull" + +@interface FIRValidationTests : FSTIntegrationTestCase +@end + +@implementation FIRValidationTests + +#pragma mark - FIRFirestoreSettings Validation + +- (void)testNilHostFails { + FIRFirestoreSettings *settings = self.db.settings; + FSTAssertThrows(settings.host = nil, + @"host setting may not be nil. You should generally just use the default value " + "(which is firestore.googleapis.com)"); +} + +- (void)testNilDispatchQueueFails { + FIRFirestoreSettings *settings = self.db.settings; + FSTAssertThrows(settings.dispatchQueue = nil, + @"dispatch queue setting may not be nil. Create a new dispatch queue with " + "dispatch_queue_create(\"com.example.MyQueue\", NULL) or just use the default " + "(which is the main queue, returned from dispatch_get_main_queue())"); +} + +- (void)testChangingSettingsAfterUseFails { + FIRFirestoreSettings *settings = self.db.settings; + [[self.db documentWithPath:@"foo/bar"] setData:@{ @"a" : @42 }]; + settings.host = @"example.com"; + FSTAssertThrows(self.db.settings = settings, + @"Firestore instance has already been started and its settings can no longer be " + @"changed. You can only set settings before calling any other methods on " + @"a Firestore instance."); +} + +#pragma mark - FIRFirestore Validation + +- (void)testNilFIRAppFails { + FSTAssertThrows( + [FIRFirestore firestoreForApp:nil], + @"FirebaseApp instance may not be nil. Use FirebaseApp.app() if you'd like to use the " + "default FirebaseApp instance."); +} + +// TODO(b/62410906): Test for firestoreForApp:database: with nil DatabaseID. + +- (void)testNilTransactionBlocksFail { + FSTAssertThrows([self.db runTransactionWithBlock:nil + completion:^(id result, NSError *error) { + XCTFail(@"Completion shouldn't run."); + }], + @"Transaction block cannot be nil."); + + FSTAssertThrows( + [self.db runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) { + XCTFail(@"Transaction block shouldn't run."); + return nil; + } + completion:nil], + @"Transaction completion block cannot be nil."); +} + +#pragma mark - Collection and Document Path Validation + +- (void)testNilCollectionPathsFail { + FIRDocumentReference *baseDocRef = [self.db documentWithPath:@"foo/bar"]; + NSString *nilError = @"Collection path cannot be nil."; + FSTAssertThrows([self.db collectionWithPath:nil], nilError); + FSTAssertThrows([baseDocRef collectionWithPath:nil], nilError); +} + +- (void)testWrongLengthCollectionPathsFail { + FIRDocumentReference *baseDocRef = [self.db documentWithPath:@"foo/bar"]; + NSArray *badAbsolutePaths = @[ @"foo/bar", @"foo/bar/baz/quu" ]; + NSArray *badRelativePaths = @[ @"", @"baz/quu" ]; + NSArray *badPathLengths = @[ @2, @4 ]; + NSString *errorFormat = + @"Invalid collection reference. Collection references must have an odd " + @"number of segments, but %@ has %@"; + for (NSUInteger i = 0; i < badAbsolutePaths.count; i++) { + NSString *error = + [NSString stringWithFormat:errorFormat, badAbsolutePaths[i], badPathLengths[i]]; + FSTAssertThrows([self.db collectionWithPath:badAbsolutePaths[i]], error); + FSTAssertThrows([baseDocRef collectionWithPath:badRelativePaths[i]], error); + } +} + +- (void)testNilDocumentPathsFail { + FIRCollectionReference *baseCollectionRef = [self.db collectionWithPath:@"foo"]; + NSString *nilError = @"Document path cannot be nil."; + FSTAssertThrows([self.db documentWithPath:nil], nilError); + FSTAssertThrows([baseCollectionRef documentWithPath:nil], nilError); +} + +- (void)testWrongLengthDocumentPathsFail { + FIRCollectionReference *baseCollectionRef = [self.db collectionWithPath:@"foo"]; + NSArray *badAbsolutePaths = @[ @"foo", @"foo/bar/baz" ]; + NSArray *badRelativePaths = @[ @"", @"bar/baz" ]; + NSArray *badPathLengths = @[ @1, @3 ]; + NSString *errorFormat = + @"Invalid document reference. Document references must have an even " + @"number of segments, but %@ has %@"; + for (NSUInteger i = 0; i < badAbsolutePaths.count; i++) { + NSString *error = + [NSString stringWithFormat:errorFormat, badAbsolutePaths[i], badPathLengths[i]]; + FSTAssertThrows([self.db documentWithPath:badAbsolutePaths[i]], error); + FSTAssertThrows([baseCollectionRef documentWithPath:badRelativePaths[i]], error); + } +} + +- (void)testPathsWithEmptySegmentsFail { + // We're only testing using collectionWithPath since the validation happens in FSTPath which is + // shared by all methods that accept paths. + + // leading / trailing slashes are okay. + [self.db collectionWithPath:@"/foo/"]; + [self.db collectionWithPath:@"/foo"]; + [self.db collectionWithPath:@"foo/"]; + + FSTAssertThrows([self.db collectionWithPath:@"foo//bar/baz"], + @"Invalid path (foo//bar/baz). Paths must not contain // in them."); + FSTAssertThrows([self.db collectionWithPath:@"//foo"], + @"Invalid path (//foo). Paths must not contain // in them."); + FSTAssertThrows([self.db collectionWithPath:@"foo//"], + @"Invalid path (foo//). Paths must not contain // in them."); +} + +#pragma mark - Write Validation + +- (void)testWritesWithNonDictionaryValuesFail { + NSArray *badData = @[ + @42, @"test", @[ @1 ], [NSDate date], [NSNull null], [FIRFieldValue fieldValueForDelete], + [FIRFieldValue fieldValueForServerTimestamp] + ]; + + for (id data in badData) { + [self expectWrite:data toFailWithReason:@"Data to be written must be an NSDictionary."]; + } +} + +- (void)testWritesWithNestedArraysFail { + [self expectWrite:@{ + @"nested-array" : @[ @1, @[ @2 ] ] + } + toFailWithReason:@"Nested arrays are not supported"]; +} + +- (void)testWritesWithInvalidTypesFail { + [self expectWrite:@{ + @"foo" : @{@"bar" : self} + } + toFailWithReason:@"Unsupported type: FIRValidationTests (found in field foo.bar)"]; +} + +- (void)testWritesWithLargeNumbersFail { + NSNumber *num = @((unsigned long long)LONG_MAX + 1); + NSString *reason = + [NSString stringWithFormat:@"NSNumber (%@) is too large (found in field num)", num]; + [self expectWrite:@{@"num" : num} toFailWithReason:reason]; +} + +- (void)testWritesWithReferencesToADifferentDatabaseFail { + FIRDocumentReference *ref = + [[self firestoreWithProjectID:@"different-db"] documentWithPath:@"baz/quu"]; + id data = @{@"foo" : ref}; + [self expectWrite:data + toFailWithReason: + [NSString + stringWithFormat:@"Document Reference is for database different-db/(default) but " + "should be for database %@/(default) (found in field foo)", + [FSTIntegrationTestCase projectID]]]; +} + +- (void)testWritesWithReservedFieldsFail { + [self expectWrite:@{ + @"__baz__" : @1 + } + toFailWithReason:@"Document fields cannot begin and end with __ (found in field __baz__)"]; + [self expectWrite:@{ + @"foo" : @{@"__baz__" : @1} + } + toFailWithReason: + @"Document fields cannot begin and end with __ (found in field foo.__baz__)"]; + [self expectWrite:@{ + @"__baz__" : @{@"foo" : @1} + } + toFailWithReason:@"Document fields cannot begin and end with __ (found in field __baz__)"]; + + [self expectUpdate:@{ + @"foo.__baz__" : @1 + } + toFailWithReason: + @"Document fields cannot begin and end with __ (found in field foo.__baz__)"]; + [self expectUpdate:@{ + @"__baz__.foo" : @1 + } + toFailWithReason: + @"Document fields cannot begin and end with __ (found in field __baz__.foo)"]; + [self expectUpdate:@{ + @1 : @1 + } + toFailWithReason:@"Dictionary keys in updateData: must be NSStrings or FIRFieldPaths."]; +} + +- (void)testSetsWithFieldValueDeleteFail { + [self expectSet:@{@"foo" : [FIRFieldValue fieldValueForDelete]} + toFailWithReason:@"FieldValue.delete() can only be used with updateData()."]; +} + +- (void)testUpdatesWithNestedFieldValueDeleteFail { + [self expectUpdate:@{ + @"foo" : @{@"bar" : [FIRFieldValue fieldValueForDelete]} + } + toFailWithReason: + @"FieldValue.delete() can only appear at the top level of your update data " + "(found in field foo.bar)"]; +} + +- (void)testBatchWritesWithIncorrectReferencesFail { + FIRFirestore *db1 = [self firestore]; + FIRFirestore *db2 = [self firestore]; + XCTAssertNotEqual(db1, db2); + + NSString *reason = @"Provided document reference is from a different Firestore instance."; + id data = @{ @"foo" : @1 }; + FIRDocumentReference *badRef = [db2 documentWithPath:@"foo/bar"]; + FIRWriteBatch *batch = [db1 batch]; + FSTAssertThrows([batch setData:data forDocument:badRef], reason); + FSTAssertThrows([batch setData:data forDocument:badRef options:[FIRSetOptions merge]], reason); + FSTAssertThrows([batch updateData:data forDocument:badRef], reason); + FSTAssertThrows([batch deleteDocument:badRef], reason); +} + +- (void)testTransactionWritesWithIncorrectReferencesFail { + FIRFirestore *db1 = [self firestore]; + FIRFirestore *db2 = [self firestore]; + XCTAssertNotEqual(db1, db2); + + NSString *reason = @"Provided document reference is from a different Firestore instance."; + id data = @{ @"foo" : @1 }; + FIRDocumentReference *badRef = [db2 documentWithPath:@"foo/bar"]; + + XCTestExpectation *transactionDone = [self expectationWithDescription:@"transaction done"]; + [db1 runTransactionWithBlock:^id(FIRTransaction *txn, NSError **pError) { + FSTAssertThrows([txn getDocument:badRef error:nil], reason); + FSTAssertThrows([txn setData:data forDocument:badRef], reason); + FSTAssertThrows([txn setData:data forDocument:badRef options:[FIRSetOptions merge]], reason); + FSTAssertThrows([txn updateData:data forDocument:badRef], reason); + FSTAssertThrows([txn deleteDocument:badRef], reason); + return nil; + } + completion:^(id result, NSError *error) { + // ends up being a no-op transaction. + XCTAssertNil(error); + [transactionDone fulfill]; + }]; + [self awaitExpectations]; +} + +#pragma mark - Field Path validation +// TODO(b/37244157): More validation for invalid field paths. + +- (void)testFieldPathsWithEmptySegmentsFail { + NSArray *badFieldPaths = @[ @"", @"foo..baz", @".foo", @"foo." ]; + + for (NSString *fieldPath in badFieldPaths) { + NSString *reason = + [NSString stringWithFormat: + @"Invalid field path (%@). Paths must not be empty, begin with " + @"'.', end with '.', or contain '..'", + fieldPath]; + [self expectFieldPath:fieldPath toFailWithReason:reason]; + } +} + +- (void)testFieldPathsWithInvalidSegmentsFail { + NSArray *badFieldPaths = @[ @"foo~bar", @"foo*bar", @"foo/bar", @"foo[1", @"foo]1", @"foo[1]" ]; + + for (NSString *fieldPath in badFieldPaths) { + NSString *reason = + [NSString stringWithFormat: + @"Invalid field path (%@). Paths must not contain '~', '*', '/', '[', or ']'", + fieldPath]; + [self expectFieldPath:fieldPath toFailWithReason:reason]; + } +} + +#pragma mark - Query Validation + +- (void)testQueryWithNonPositiveLimitFails { + FSTAssertThrows([[self collectionRef] queryLimitedTo:0], + @"Invalid Query. Query limit (0) is invalid. Limit must be positive."); + FSTAssertThrows([[self collectionRef] queryLimitedTo:-1], + @"Invalid Query. Query limit (-1) is invalid. Limit must be positive."); +} + +- (void)testQueryInequalityOnNullOrNaNFails { + FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:nil], + @"Invalid Query. You can only perform equality comparisons on nil / NSNull."); + FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:[NSNull null]], + @"Invalid Query. You can only perform equality comparisons on nil / NSNull."); + + FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:@(NAN)], + @"Invalid Query. You can only perform equality comparisons on NaN."); +} + +- (void)testQueryCannotBeCreatedFromDocumentsMissingSortValues { + FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ + @"f" : @{@"v" : @"f", @"nosort" : @1.0} + }]; + + FIRQuery *query = [testCollection queryOrderedByField:@"sort"]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:[testCollection documentWithPath:@"f"]]; + XCTAssertTrue(snapshot.exists); + + NSString *reason = + @"Invalid query. You are trying to start or end a query using a document for " + "which the field 'sort' (used as the order by) does not exist."; + FSTAssertThrows([query queryStartingAtDocument:snapshot], reason); + FSTAssertThrows([query queryStartingAfterDocument:snapshot], reason); + FSTAssertThrows([query queryEndingBeforeDocument:snapshot], reason); + FSTAssertThrows([query queryEndingAtDocument:snapshot], reason); +} + +- (void)testQueryBoundMustNotHaveMoreComponentsThanSortOrders { + FIRCollectionReference *testCollection = [self collectionRef]; + FIRQuery *query = [testCollection queryOrderedByField:@"foo"]; + + NSString *reason = + @"Invalid query. You are trying to start or end a query using more values " + "than were specified in the order by."; + // More elements than order by + FSTAssertThrows(([query queryStartingAtValues:@[ @1, @2 ]]), reason); + FSTAssertThrows(([[query queryOrderedByField:@"bar"] queryStartingAtValues:@[ @1, @2, @3 ]]), + reason); +} + +- (void)testQueryOrderedByKeyBoundMustBeAStringWithoutSlashes { + FIRCollectionReference *testCollection = [self collectionRef]; + FIRQuery *query = [testCollection queryOrderedByFieldPath:[FIRFieldPath documentID]]; + FSTAssertThrows([query queryStartingAtValues:@[ @1 ]], + @"Invalid query. Expected a string for the document ID."); + FSTAssertThrows([query queryStartingAtValues:@[ @"foo/bar" ]], + @"Invalid query. Document ID 'foo/bar' contains a slash."); +} + +- (void)testQueryMustNotSpecifyStartingOrEndingPointAfterOrder { + FIRCollectionReference *testCollection = [self collectionRef]; + FIRQuery *query = [testCollection queryOrderedByField:@"foo"]; + NSString *reason = + @"Invalid query. You must not specify a starting point before specifying the order by."; + FSTAssertThrows([[query queryStartingAtValues:@[ @1 ]] queryOrderedByField:@"bar"], reason); + FSTAssertThrows([[query queryStartingAfterValues:@[ @1 ]] queryOrderedByField:@"bar"], reason); + reason = @"Invalid query. You must not specify an ending point before specifying the order by."; + FSTAssertThrows([[query queryEndingAtValues:@[ @1 ]] queryOrderedByField:@"bar"], reason); + FSTAssertThrows([[query queryEndingBeforeValues:@[ @1 ]] queryOrderedByField:@"bar"], reason); +} + +- (void)testQueriesFilteredByDocumentIDMustUseStringsOrDocumentReferences { + FIRCollectionReference *collection = [self collectionRef]; + NSString *reason = + @"Invalid query. When querying by document ID you must provide a valid " + "document ID, but it was an empty string."; + FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@""], reason); + + reason = + @"Invalid query. When querying by document ID you must provide a valid document ID, " + "but 'foo/bar/baz' contains a '/' character."; + FSTAssertThrows( + [collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@"foo/bar/baz"], reason); + + reason = + @"Invalid query. When querying by document ID you must provide a valid string or " + "DocumentReference, but it was of type: __NSCFNumber"; + FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@1], reason); +} + +- (void)testQueryInequalityFieldMustMatchFirstOrderByField { + FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"]; + FIRQuery *base = [coll queryWhereField:@"x" isGreaterThanOrEqualTo:@32]; + + FSTAssertThrows([base queryWhereField:@"y" isLessThan:@"cat"], + @"Invalid Query. All where filters with an inequality (lessThan, " + "lessThanOrEqual, greaterThan, or greaterThanOrEqual) must be on the same " + "field. But you have inequality filters on 'x' and 'y'"); + + NSString *reason = + @"Invalid query. You have a where filter with " + "an inequality (lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) " + "on field 'x' and so you must also use 'x' as your first queryOrderedBy field, " + "but your first queryOrderedBy is currently on field 'y' instead."; + FSTAssertThrows([base queryOrderedByField:@"y"], reason); + FSTAssertThrows([[coll queryOrderedByField:@"y"] queryWhereField:@"x" isGreaterThan:@32], reason); + FSTAssertThrows([[base queryOrderedByField:@"y"] queryOrderedByField:@"x"], reason); + FSTAssertThrows([[[coll queryOrderedByField:@"y"] queryOrderedByField:@"x"] queryWhereField:@"x" + isGreaterThan:@32], + reason); + + XCTAssertNoThrow([base queryWhereField:@"x" isLessThanOrEqualTo:@"cat"], + @"Same inequality fields work"); + + XCTAssertNoThrow([base queryWhereField:@"y" isEqualTo:@"cat"], + @"Inequality and equality on different fields works"); + + XCTAssertNoThrow([base queryOrderedByField:@"x"], @"inequality same as order by works"); + XCTAssertNoThrow([[coll queryOrderedByField:@"x"] queryWhereField:@"x" isGreaterThan:@32], + @"inequality same as order by works"); + XCTAssertNoThrow([[base queryOrderedByField:@"x"] queryOrderedByField:@"y"], + @"inequality same as first order by works."); + XCTAssertNoThrow([[[coll queryOrderedByField:@"x"] queryOrderedByField:@"y"] queryWhereField:@"x" + isGreaterThan:@32], + @"inequality same as first order by works."); +} + +#pragma mark - GeoPoint Validation + +- (void)testInvalidGeoPointParameters { + [self verifyExceptionForInvalidLatitude:NAN]; + [self verifyExceptionForInvalidLatitude:-INFINITY]; + [self verifyExceptionForInvalidLatitude:INFINITY]; + [self verifyExceptionForInvalidLatitude:-90.1]; + [self verifyExceptionForInvalidLatitude:90.1]; + + [self verifyExceptionForInvalidLongitude:NAN]; + [self verifyExceptionForInvalidLongitude:-INFINITY]; + [self verifyExceptionForInvalidLongitude:INFINITY]; + [self verifyExceptionForInvalidLongitude:-180.1]; + [self verifyExceptionForInvalidLongitude:180.1]; +} + +#pragma mark - Helpers + +/** Performs a write using each write API and makes sure it fails with the expected reason. */ +- (void)expectWrite:(id)data toFailWithReason:(NSString *)reason { + [self expectWrite:data toFailWithReason:reason includeSets:YES includeUpdates:YES]; +} + +/** Performs a write using each set API and makes sure it fails with the expected reason. */ +- (void)expectSet:(id)data toFailWithReason:(NSString *)reason { + [self expectWrite:data toFailWithReason:reason includeSets:YES includeUpdates:NO]; +} + +/** Performs a write using each update API and makes sure it fails with the expected reason. */ +- (void)expectUpdate:(id)data toFailWithReason:(NSString *)reason { + [self expectWrite:data toFailWithReason:reason includeSets:NO includeUpdates:YES]; +} + +/** + * Performs a write using each set and/or update API and makes sure it fails with the expected + * reason. + */ +- (void)expectWrite:(id)data + toFailWithReason:(NSString *)reason + includeSets:(BOOL)includeSets + includeUpdates:(BOOL)includeUpdates { + FIRDocumentReference *ref = [self documentRef]; + if (includeSets) { + FSTAssertThrows([ref setData:data], reason, @"for %@", data); + FSTAssertThrows([[ref.firestore batch] setData:data forDocument:ref], reason, @"for %@", data); + } + + if (includeUpdates) { + FSTAssertThrows([ref updateData:data], reason, @"for %@", data); + FSTAssertThrows([[ref.firestore batch] updateData:data forDocument:ref], reason, @"for %@", + data); + } + + XCTestExpectation *transactionDone = [self expectationWithDescription:@"transaction done"]; + [ref.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) { + if (includeSets) { + FSTAssertThrows([transaction setData:data forDocument:ref], reason, @"for %@", data); + } + if (includeUpdates) { + FSTAssertThrows([transaction updateData:data forDocument:ref], reason, @"for %@", data); + } + return nil; + } + completion:^(id result, NSError *error) { + // ends up being a no-op transaction. + XCTAssertNil(error); + [transactionDone fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testFieldNamesMustNotBeEmpty { + NSString *reason = @"Invalid field path. Provided names must not be empty."; + FSTAssertThrows([[FIRFieldPath alloc] initWithFields:@[]], reason); + + reason = @"Invalid field name at index 0. Field names must not be empty."; + FSTAssertThrows([[FIRFieldPath alloc] initWithFields:@[ @"" ]], reason); + + reason = @"Invalid field name at index 1. Field names must not be empty."; + FSTAssertThrows(([[FIRFieldPath alloc] initWithFields:@[ @"foo", @"" ]]), reason); +} + +/** + * Tests a field path with all of our APIs that accept field paths and ensures they fail with the + * specified reason. + */ +- (void)expectFieldPath:(NSString *)fieldPath toFailWithReason:(NSString *)reason { + // Get an arbitrary snapshot we can use for testing. + FIRDocumentReference *docRef = [self documentRef]; + [self writeDocumentRef:docRef data:@{ @"test" : @1 }]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:docRef]; + + // Update paths. + NSMutableDictionary *dict = [NSMutableDictionary dictionary]; + dict[fieldPath] = @1; + [self expectUpdate:dict toFailWithReason:reason]; + + // Snapshot fields. + FSTAssertThrows(snapshot[fieldPath], reason); + + // Query filter / order fields. + FIRCollectionReference *collection = [self collectionRef]; + FSTAssertThrows([collection queryWhereField:fieldPath isEqualTo:@1], reason); + // isLessThan, etc. omitted for brevity since the code path is trivially shared. + FSTAssertThrows([collection queryOrderedByField:fieldPath], reason); +} + +- (void)verifyExceptionForInvalidLatitude:(double)latitude { + NSString *reason = [NSString + stringWithFormat:@"GeoPoint requires a latitude value in the range of [-90, 90], but was %f", + latitude]; + FSTAssertThrows([[FIRGeoPoint alloc] initWithLatitude:latitude longitude:0], reason); +} + +- (void)verifyExceptionForInvalidLongitude:(double)longitude { + NSString *reason = + [NSString stringWithFormat: + @"GeoPoint requires a longitude value in the range of [-180, 180], but was %f", + longitude]; + FSTAssertThrows([[FIRGeoPoint alloc] initWithLatitude:0 longitude:longitude], reason); +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m new file mode 100644 index 0000000..159cbd7 --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m @@ -0,0 +1,313 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import Firestore; + +#import <XCTest/XCTest.h> + +#import "FSTEventAccumulator.h" +#import "FSTIntegrationTestCase.h" + +@interface FIRWriteBatchTests : FSTIntegrationTestCase +@end + +@implementation FIRWriteBatchTests + +- (void)testSupportEmptyBatches { + XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"]; + [[[self firestore] batch] commitWithCompletion:^(NSError *error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testSetDocuments { + FIRDocumentReference *doc = [self documentRef]; + XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch setData:@{@"a" : @"b"} forDocument:doc]; + [batch setData:@{@"c" : @"d"} forDocument:doc]; + [batch commitWithCompletion:^(NSError *error) { + XCTAssertNil(error); + [batchExpectation fulfill]; + }]; + [self awaitExpectations]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertTrue(snapshot.exists); + XCTAssertEqualObjects(snapshot.data, @{@"c" : @"d"}); +} + +- (void)testSetDocumentWithMerge { + FIRDocumentReference *doc = [self documentRef]; + XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch setData:@{ @"a" : @"b", @"nested" : @{@"a" : @"b"} } forDocument:doc]; + [batch setData:@{ + @"c" : @"d", + @"nested" : @{@"c" : @"d"} + } + forDocument:doc + options:[FIRSetOptions merge]]; + [batch commitWithCompletion:^(NSError *error) { + XCTAssertNil(error); + [batchExpectation fulfill]; + }]; + [self awaitExpectations]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertTrue(snapshot.exists); + XCTAssertEqualObjects( + snapshot.data, ( + @{ @"a" : @"b", + @"c" : @"d", + @"nested" : @{@"a" : @"b", @"c" : @"d"} })); +} + +- (void)testUpdateDocuments { + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; + XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch updateData:@{ @"baz" : @42 } forDocument:doc]; + [batch commitWithCompletion:^(NSError *error) { + XCTAssertNil(error); + [batchExpectation fulfill]; + }]; + [self awaitExpectations]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertTrue(snapshot.exists); + XCTAssertEqualObjects(snapshot.data, (@{ @"foo" : @"bar", @"baz" : @42 })); +} + +- (void)testCannotUpdateNonexistentDocuments { + FIRDocumentReference *doc = [self documentRef]; + XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch updateData:@{ @"baz" : @42 } forDocument:doc]; + [batch commitWithCompletion:^(NSError *error) { + XCTAssertNotNil(error); + [batchExpectation fulfill]; + }]; + [self awaitExpectations]; + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertFalse(result.exists); +} + +- (void)testDeleteDocuments { + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + + XCTAssertTrue(snapshot.exists); + XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch deleteDocument:doc]; + [batch commitWithCompletion:^(NSError *error) { + XCTAssertNil(error); + [batchExpectation fulfill]; + }]; + [self awaitExpectations]; + snapshot = [self readDocumentForRef:doc]; + XCTAssertFalse(snapshot.exists); +} + +- (void)testBatchesCommitAtomicallyRaisingCorrectEvents { + FIRCollectionReference *collection = [self collectionRef]; + FIRDocumentReference *docA = [collection documentWithPath:@"a"]; + FIRDocumentReference *docB = [collection documentWithPath:@"b"]; + FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self]; + [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options] + includeQueryMetadataChanges:YES] + listener:accumulator.handler]; + FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"]; + XCTAssertEqual(initialSnap.count, 0); + + // Atomically write two documents. + XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [collection.firestore batch]; + [batch setData:@{ @"a" : @1 } forDocument:docA]; + [batch setData:@{ @"b" : @2 } forDocument:docB]; + [batch commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + + FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; + XCTAssertTrue(localSnap.metadata.hasPendingWrites); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(localSnap), (@[ @{ @"a" : @1 }, @{ @"b" : @2 } ])); + + FIRQuerySnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"]; + XCTAssertFalse(serverSnap.metadata.hasPendingWrites); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(serverSnap), (@[ @{ @"a" : @1 }, @{ @"b" : @2 } ])); +} + +- (void)testBatchesFailAtomicallyRaisingCorrectEvents { + FIRCollectionReference *collection = [self collectionRef]; + FIRDocumentReference *docA = [collection documentWithPath:@"a"]; + FIRDocumentReference *docB = [collection documentWithPath:@"b"]; + FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self]; + [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options] + includeQueryMetadataChanges:YES] + listener:accumulator.handler]; + FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"]; + XCTAssertEqual(initialSnap.count, 0); + + // Atomically write 1 document and update a nonexistent document. + XCTestExpectation *expectation = [self expectationWithDescription:@"batch failed"]; + FIRWriteBatch *batch = [collection.firestore batch]; + [batch setData:@{ @"a" : @1 } forDocument:docA]; + [batch updateData:@{ @"b" : @2 } forDocument:docB]; + [batch commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound); + [expectation fulfill]; + }]; + + // Local event with the set document. + FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; + XCTAssertTrue(localSnap.metadata.hasPendingWrites); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(localSnap), (@[ @{ @"a" : @1 } ])); + + // Server event with the set reverted. + FIRQuerySnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"]; + XCTAssertFalse(serverSnap.metadata.hasPendingWrites); + XCTAssertEqual(serverSnap.count, 0); +} + +- (void)testWriteTheSameServerTimestampAcrossWrites { + FIRCollectionReference *collection = [self collectionRef]; + FIRDocumentReference *docA = [collection documentWithPath:@"a"]; + FIRDocumentReference *docB = [collection documentWithPath:@"b"]; + FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self]; + [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options] + includeQueryMetadataChanges:YES] + listener:accumulator.handler]; + FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"]; + XCTAssertEqual(initialSnap.count, 0); + + // Atomically write 2 documents with server timestamps. + XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [collection.firestore batch]; + [batch setData:@{@"when" : [FIRFieldValue fieldValueForServerTimestamp]} forDocument:docA]; + [batch setData:@{@"when" : [FIRFieldValue fieldValueForServerTimestamp]} forDocument:docB]; + [batch commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + + FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; + XCTAssertTrue(localSnap.metadata.hasPendingWrites); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(localSnap), + (@[ @{@"when" : [NSNull null]}, @{@"when" : [NSNull null]} ])); + + FIRQuerySnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"]; + XCTAssertFalse(serverSnap.metadata.hasPendingWrites); + XCTAssertEqual(serverSnap.count, 2); + NSDate *when = serverSnap.documents[0][@"when"]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(serverSnap), + (@[ @{@"when" : when}, @{@"when" : when} ])); +} + +- (void)testCanWriteTheSameDocumentMultipleTimes { + FIRDocumentReference *doc = [self documentRef]; + FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self]; + [doc + addSnapshotListenerWithOptions:[[FIRDocumentListenOptions options] includeMetadataChanges:YES] + listener:accumulator.handler]; + FIRDocumentSnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"]; + XCTAssertFalse(initialSnap.exists); + + XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch deleteDocument:doc]; + [batch setData:@{ @"a" : @1, @"b" : @1, @"when" : @"when" } forDocument:doc]; + [batch updateData:@{ + @"b" : @2, + @"when" : [FIRFieldValue fieldValueForServerTimestamp] + } + forDocument:doc]; + [batch commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + + FIRDocumentSnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; + XCTAssertTrue(localSnap.metadata.hasPendingWrites); + XCTAssertEqualObjects(localSnap.data, (@{ @"a" : @1, @"b" : @2, @"when" : [NSNull null] })); + + FIRDocumentSnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"]; + XCTAssertFalse(serverSnap.metadata.hasPendingWrites); + NSDate *when = serverSnap[@"when"]; + XCTAssertEqualObjects(serverSnap.data, (@{ @"a" : @1, @"b" : @2, @"when" : when })); +} + +- (void)testUpdateFieldsWithDots { + FIRDocumentReference *doc = [self documentRef]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateFieldsWithDots"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch setData:@{@"a.b" : @"old", @"c.d" : @"old"} forDocument:doc]; + [batch updateData:@{ + [[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new" + } + forDocument:doc]; + + [batch commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"})); + }]; + [expectation fulfill]; + }]; + + [self awaitExpectations]; +} + +- (void)testUpdateNestedFields { + FIRDocumentReference *doc = [self documentRef]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateNestedFields"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch setData:@{ + @"a" : @{@"b" : @"old"}, + @"c" : @{@"d" : @"old"}, + @"e" : @{@"f" : @"old"} + } + forDocument:doc]; + [batch updateData:@{ + @"a.b" : @"new", + [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new" + } + forDocument:doc]; + [batch commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(snapshot.data, (@{ + @"a" : @{@"b" : @"new"}, + @"c" : @{@"d" : @"new"}, + @"e" : @{@"f" : @"old"} + })); + }]; + [expectation fulfill]; + }]; + + [self awaitExpectations]; +} + +@end diff --git a/Firestore/Example/Tests/Integration/CAcert.pem b/Firestore/Example/Tests/Integration/CAcert.pem new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/Firestore/Example/Tests/Integration/CAcert.pem diff --git a/Firestore/Example/Tests/Integration/FSTDatastoreTests.m b/Firestore/Example/Tests/Integration/FSTDatastoreTests.m new file mode 100644 index 0000000..bab8f44 --- /dev/null +++ b/Firestore/Example/Tests/Integration/FSTDatastoreTests.m @@ -0,0 +1,239 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import Firestore; + +#import <GRPCClient/GRPCCall+ChannelCredentials.h> +#import <GRPCClient/GRPCCall+Tests.h> +#import <XCTest/XCTest.h> + +#import "API/FIRDocumentReference+Internal.h" +#import "API/FSTUserDataConverter.h" +#import "Auth/FSTEmptyCredentialsProvider.h" +#import "Core/FSTDatabaseInfo.h" +#import "Core/FSTFirestoreClient.h" +#import "Core/FSTQuery.h" +#import "Core/FSTSnapshotVersion.h" +#import "Core/FSTTimestamp.h" +#import "Local/FSTQueryData.h" +#import "Model/FSTDatabaseID.h" +#import "Model/FSTDocumentKey.h" +#import "Model/FSTFieldValue.h" +#import "Model/FSTMutation.h" +#import "Model/FSTMutationBatch.h" +#import "Model/FSTPath.h" +#import "Remote/FSTDatastore.h" +#import "Remote/FSTRemoteEvent.h" +#import "Remote/FSTRemoteStore.h" +#import "Util/FSTAssert.h" +#import "Util/FSTDispatchQueue.h" +#import "Util/FSTUtil.h" + +#import "FSTIntegrationTestCase.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTRemoteStore (Tests) +- (void)commitBatch:(FSTMutationBatch *)batch; +@end + +#pragma mark - FSTRemoteStoreEventCapture + +@interface FSTRemoteStoreEventCapture : NSObject <FSTRemoteSyncer> + +- (instancetype)init __attribute__((unavailable("Use initWithTestCase:"))); + +- (instancetype)initWithTestCase:(XCTestCase *_Nullable)testCase NS_DESIGNATED_INITIALIZER; + +- (void)expectWriteEventWithDescription:(NSString *)description; +- (void)expectListenEventWithDescription:(NSString *)description; + +@property(nonatomic, weak, nullable) XCTestCase *testCase; +@property(nonatomic, strong) NSMutableArray<NSObject *> *writeEvents; +@property(nonatomic, strong) NSMutableArray<NSObject *> *listenEvents; +@property(nonatomic, strong) NSMutableArray<XCTestExpectation *> *writeEventExpectations; +@property(nonatomic, strong) NSMutableArray<XCTestExpectation *> *listenEventExpectations; +@end + +@implementation FSTRemoteStoreEventCapture + +- (instancetype)initWithTestCase:(XCTestCase *_Nullable)testCase { + if (self = [super init]) { + _writeEvents = [NSMutableArray array]; + _listenEvents = [NSMutableArray array]; + _testCase = testCase; + _writeEventExpectations = [NSMutableArray array]; + _listenEventExpectations = [NSMutableArray array]; + } + return self; +} + +- (void)expectWriteEventWithDescription:(NSString *)description { + [self.writeEventExpectations + addObject:[self.testCase + expectationWithDescription:[NSString + stringWithFormat:@"write event %lu: %@", + (unsigned long) + self.writeEventExpectations + .count, + description]]]; +} + +- (void)expectListenEventWithDescription:(NSString *)description { + [self.listenEventExpectations + addObject:[self.testCase + expectationWithDescription:[NSString + stringWithFormat:@"listen event %lu: %@", + (unsigned long) + self.listenEventExpectations + .count, + description]]]; +} + +- (void)applySuccessfulWriteWithResult:(FSTMutationBatchResult *)batchResult { + [self.writeEvents addObject:batchResult]; + XCTestExpectation *expectation = [self.writeEventExpectations objectAtIndex:0]; + [self.writeEventExpectations removeObjectAtIndex:0]; + [expectation fulfill]; +} + +- (void)rejectFailedWriteWithBatchID:(FSTBatchID)batchID error:(NSError *)error { + FSTFail(@"Not implemented"); +} + +- (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { + [self.listenEvents addObject:remoteEvent]; + XCTestExpectation *expectation = [self.listenEventExpectations objectAtIndex:0]; + [self.listenEventExpectations removeObjectAtIndex:0]; + [expectation fulfill]; +} + +- (void)rejectListenWithTargetID:(FSTBoxedTargetID *)targetID error:(NSError *)error { + FSTFail(@"Not implemented"); +} + +@end + +#pragma mark - FSTDatastoreTests + +@interface FSTDatastoreTests : XCTestCase + +@end + +@implementation FSTDatastoreTests { + FSTDispatchQueue *_testWorkerQueue; + FSTLocalStore *_localStore; + id<FSTCredentialsProvider> _credentials; + + FSTDatastore *_datastore; + FSTRemoteStore *_remoteStore; +} + +- (void)setUp { + [super setUp]; + + NSString *projectID = [[NSProcessInfo processInfo] environment][@"PROJECT_ID"]; + if (!projectID) { + projectID = @"test-db"; + } + + FIRFirestoreSettings *settings = [FSTIntegrationTestCase settings]; + if (!settings.sslEnabled) { + [GRPCCall useInsecureConnectionsForHost:settings.host]; + } + + FSTDatabaseID *databaseID = + [FSTDatabaseID databaseIDWithProject:projectID database:kDefaultDatabaseID]; + + FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID + persistenceKey:@"test-key" + host:settings.host + sslEnabled:settings.sslEnabled]; + + _testWorkerQueue = [FSTDispatchQueue + queueWith:dispatch_queue_create("com.google.firestore.FSTDatastoreTestsWorkerQueue", + DISPATCH_QUEUE_SERIAL)]; + + _credentials = [[FSTEmptyCredentialsProvider alloc] init]; + + _datastore = [FSTDatastore datastoreWithDatabase:databaseInfo + workerDispatchQueue:_testWorkerQueue + credentials:_credentials]; + + _remoteStore = [FSTRemoteStore remoteStoreWithLocalStore:_localStore datastore:_datastore]; + [_remoteStore start]; +} + +- (void)tearDown { + XCTestExpectation *completion = [self expectationWithDescription:@"shutdown"]; + [_testWorkerQueue dispatchAsync:^{ + [_remoteStore shutdown]; + [completion fulfill]; + }]; + [self awaitExpectations]; + + [super tearDown]; +} + +- (void)testCommit { + XCTestExpectation *expectation = [self expectationWithDescription:@"commitWithCompletion"]; + + [_datastore commitMutations:@[] + completion:^(NSError *_Nullable error) { + XCTAssertNil(error, @"Failed to commit"); + [expectation fulfill]; + }]; + + [self awaitExpectations]; +} + +- (void)testStreamingWrite { + FSTRemoteStoreEventCapture *capture = [[FSTRemoteStoreEventCapture alloc] initWithTestCase:self]; + [capture expectWriteEventWithDescription:@"write mutations"]; + + _remoteStore.syncEngine = capture; + + FSTSetMutation *mutation = [self setMutation]; + FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:23 + localWriteTime:[FSTTimestamp timestamp] + mutations:@[ mutation ]]; + [_testWorkerQueue dispatchAsync:^{ + [_remoteStore commitBatch:batch]; + }]; + + [self awaitExpectations]; +} + +- (void)awaitExpectations { + [self waitForExpectationsWithTimeout:4.0 + handler:^(NSError *_Nullable expectationError) { + if (expectationError) { + XCTFail(@"Error waiting for timeout: %@", expectationError); + } + }]; +} + +- (FSTSetMutation *)setMutation { + return [[FSTSetMutation alloc] + initWithKey:[FSTDocumentKey keyWithPathString:@"rooms/eros"] + value:[[FSTObjectValue alloc] + initWithDictionary:@{@"name" : [FSTStringValue stringValue:@"Eros"]}] + precondition:[FSTPrecondition none]]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Integration/FSTSmokeTests.m b/Firestore/Example/Tests/Integration/FSTSmokeTests.m new file mode 100644 index 0000000..e0ad06c --- /dev/null +++ b/Firestore/Example/Tests/Integration/FSTSmokeTests.m @@ -0,0 +1,129 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import Firestore; + +#import <XCTest/XCTest.h> + +#import "FSTEventAccumulator.h" +#import "FSTIntegrationTestCase.h" + +@interface FSTSmokeTests : FSTIntegrationTestCase +@end + +@implementation FSTSmokeTests + +- (void)testCanWriteASingleDocument { + FIRDocumentReference *ref = [self documentRef]; + [self writeDocumentRef:ref data:[self chatMessage]]; +} + +- (void)testCanReadAWrittenDocument { + NSDictionary<NSString *, id> *data = [self chatMessage]; + + FIRDocumentReference *ref = [self documentRef]; + [self writeDocumentRef:ref data:data]; + + FIRDocumentSnapshot *doc = [self readDocumentForRef:ref]; + XCTAssertEqualObjects(doc.data, data); +} + +- (void)testObservesExistingDocument { + [self readerAndWriterOnDocumentRef:^(NSString *path, FIRDocumentReference *readerRef, + FIRDocumentReference *writerRef) { + NSDictionary<NSString *, id> *data = [self chatMessage]; + [self writeDocumentRef:writerRef data:data]; + + id<FIRListenerRegistration> listenerRegistration = + [readerRef addSnapshotListener:self.eventAccumulator.handler]; + + FIRDocumentSnapshot *doc = [self.eventAccumulator awaitEventWithName:@"snapshot"]; + XCTAssertEqual([doc class], [FIRDocumentSnapshot class]); + XCTAssertEqualObjects(doc.data, data); + + [listenerRegistration remove]; + }]; +} + +- (void)testObservesNewDocument { + [self readerAndWriterOnDocumentRef:^(NSString *path, FIRDocumentReference *readerRef, + FIRDocumentReference *writerRef) { + id<FIRListenerRegistration> listenerRegistration = + [readerRef addSnapshotListener:self.eventAccumulator.handler]; + + FIRDocumentSnapshot *doc1 = [self.eventAccumulator awaitEventWithName:@"null snapshot"]; + XCTAssertFalse(doc1.exists); + // TODO(b/36366944): add tests for doc1.path) + + NSDictionary<NSString *, id> *data = [self chatMessage]; + [self writeDocumentRef:writerRef data:data]; + + FIRDocumentSnapshot *doc2 = [self.eventAccumulator awaitEventWithName:@"full snapshot"]; + XCTAssertEqual([doc2 class], [FIRDocumentSnapshot class]); + XCTAssertEqualObjects(doc2.data, data); + + [listenerRegistration remove]; + }]; +} + +- (void)testWillFireValueEventsForEmptyCollections { + FIRCollectionReference *collection = [self.db collectionWithPath:@"empty-collection"]; + id<FIRListenerRegistration> listenerRegistration = + [collection addSnapshotListener:self.eventAccumulator.handler]; + + FIRQuerySnapshot *snap = [self.eventAccumulator awaitEventWithName:@"empty query snapshot"]; + XCTAssertEqual([snap class], [FIRQuerySnapshot class]); + XCTAssertEqual(snap.count, 0); + + [listenerRegistration remove]; +} + +- (void)testGetCollectionQuery { + NSDictionary<NSString *, id> *testDocs = @{ + @"1" : @{@"name" : @"Patryk", @"message" : @"Real data, yo!"}, + @"2" : @{@"name" : @"Gil", @"message" : @"Yep!"}, + @"3" : @{@"name" : @"Jonny", @"message" : @"Back to work!"}, + }; + + FIRCollectionReference *docs = [self collectionRefWithDocuments:testDocs]; + FIRQuerySnapshot *result = [self readDocumentSetForRef:docs]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(result), + (@[ testDocs[@"1"], testDocs[@"2"], testDocs[@"3"] ])); +} + +// TODO(klimt): This test is disabled because we can't create compound indexes programmatically. +- (void)xtestQueryByFieldAndUseOrderBy { + NSDictionary<NSString *, id> *testDocs = @{ + @"1" : @{@"sort" : @1, @"filter" : @YES, @"key" : @"1"}, + @"2" : @{@"sort" : @2, @"filter" : @YES, @"key" : @"2"}, + @"3" : @{@"sort" : @2, @"filter" : @YES, @"key" : @"3"}, + @"4" : @{@"sort" : @3, @"filter" : @NO, @"key" : @"4"} + }; + + FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs]; + + FIRQuery *query = + [[coll queryWhereField:@"filter" isEqualTo:@YES] queryOrderedByField:@"sort" descending:YES]; + FIRQuerySnapshot *result = [self readDocumentSetForRef:query]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(result), + (@[ testDocs[@"2"], testDocs[@"3"], testDocs[@"1"] ])); +} + +- (NSDictionary<NSString *, id> *)chatMessage { + return @{@"name" : @"Patryk", @"message" : @"We are actually writing data!"}; +} + +@end diff --git a/Firestore/Example/Tests/Integration/FSTTransactionTests.m b/Firestore/Example/Tests/Integration/FSTTransactionTests.m new file mode 100644 index 0000000..a0b5bbe --- /dev/null +++ b/Firestore/Example/Tests/Integration/FSTTransactionTests.m @@ -0,0 +1,541 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import Firestore; + +#import <XCTest/XCTest.h> +#include <libkern/OSAtomic.h> + +#import "FSTIntegrationTestCase.h" + +@interface FSTTransactionTests : FSTIntegrationTestCase +@end + +@implementation FSTTransactionTests + +// We currently require every document read to also be written. +// TODO(b/34879758): Re-enable this test once we fix it. +- (void)xtestGetDocuments { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"spaces"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{ @"foo" : @1, @"desc" : @"Stuff", @"owner" : @"Jonny" }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertNil(result); + // We currently require every document read to also be written. + // TODO(b/34879758): Fix this check once we drop that requirement. + XCTAssertNotNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testDeleteDocument { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(@"bar", snapshot[@"foo"]); + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + [transaction deleteDocument:doc]; + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertEqualObjects(@YES, result); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; + + snapshot = [self readDocumentForRef:doc]; + XCTAssertFalse(snapshot.exists); +} + +- (void)testGetNonexistentDocumentThenCreate { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + XCTAssertFalse(snapshot.exists); + [transaction setData:@{@"foo" : @"bar"} forDocument:doc]; + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertEqualObjects(@YES, result); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; + + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertTrue(snapshot.exists); + XCTAssertEqualObjects(@"bar", snapshot[@"foo"]); +} + +- (void)testGetNonexistentDocumentThenFailPatch { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + XCTAssertFalse(snapshot.exists); + [transaction updateData:@{@"foo" : @"bar"} forDocument:doc]; + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertNil(result); + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + // TODO(dimond): This is probably the wrong error code, but it's what we use today. We + // should update the code once the underlying error was fixed. + XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testDeleteDocumentAndPatch { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **error) { + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + XCTAssertTrue(snapshot.exists); + [transaction deleteDocument:doc]; + // Since we deleted the doc, the update will fail + [transaction updateData:@{@"foo" : @"bar"} forDocument:doc]; + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertNil(result); + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + // TODO(dimond): This is probably the wrong error code, but it's what we use today. We + // should update the code once the underlying error was fixed. + XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testDeleteDocumentAndSet { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **error) { + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + XCTAssertTrue(snapshot.exists); + [transaction deleteDocument:doc]; + // TODO(dimond): In theory this should work, but it's complex to make it work, so instead we + // just let the transaction fail and verify it's unsupported for now + [transaction setData:@{@"foo" : @"new-bar"} forDocument:doc]; + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertNil(result); + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + // TODO(dimond): This is probably the wrong error code, but it's what we use today. We + // should update the code once the underlying error was fixed. + XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testWriteDocumentTwice { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **error) { + [transaction setData:@{@"a" : @"b"} forDocument:doc]; + [transaction setData:@{@"c" : @"d"} forDocument:doc]; + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertEqualObjects(@YES, result); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; + + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(snapshot.data, @{@"c" : @"d"}); +} + +- (void)testSetDocumentWithMerge { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + [transaction setData:@{ @"a" : @"b", @"nested" : @{@"a" : @"b"} } forDocument:doc]; + [transaction setData:@{ + @"c" : @"d", + @"nested" : @{@"c" : @"d"} + } + forDocument:doc + options:[FIRSetOptions merge]]; + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertEqualObjects(@YES, result); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; + + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertEqualObjects( + snapshot.data, ( + @{ @"a" : @"b", + @"c" : @"d", + @"nested" : @{@"a" : @"b", @"c" : @"d"} })); +} + +- (void)testCannotUpdateNonExistentDocument { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + [transaction updateData:@{@"foo" : @"bar"} forDocument:doc]; + return nil; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertNotNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertFalse(result.exists); +} + +- (void)testIncrementTransactionally { + // A barrier to make sure every transaction reaches the same spot. + dispatch_semaphore_t writeBarrier = dispatch_semaphore_create(0); + __block volatile int32_t started = 0; + + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{ @"count" : @(5.0) }]; + + // Make 3 transactions that will all increment. + int total = 3; + for (int i = 0; i < total; i++) { + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + int32_t nowStarted = OSAtomicIncrement32(&started); + // Once all of the transactions have read, allow the first write. + if (nowStarted == total) { + dispatch_semaphore_signal(writeBarrier); + } + + dispatch_semaphore_wait(writeBarrier, DISPATCH_TIME_FOREVER); + // Refill the barrier so that the other transactions and retries succeed. + dispatch_semaphore_signal(writeBarrier); + + double newCount = ((NSNumber *)snapshot[@"count"]).doubleValue + 1.0; + [transaction setData:@{ @"count" : @(newCount) } forDocument:doc]; + return @YES; + + } + completion:^(id _Nullable result, NSError *_Nullable error) { + [expectation fulfill]; + }]; + } + + [self awaitExpectations]; + // Now all transaction should be completed, so check the result. + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(@(5.0 + total), snapshot[@"count"]); +} + +- (void)testUpdateTransactionally { + // A barrier to make sure every transaction reaches the same spot. + dispatch_semaphore_t writeBarrier = dispatch_semaphore_create(0); + __block volatile int32_t started = 0; + + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{ @"count" : @(5.0), @"other" : @"yes" }]; + + // Make 3 transactions that will all increment. + int total = 3; + for (int i = 0; i < total; i++) { + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + int32_t nowStarted = OSAtomicIncrement32(&started); + // Once all of the transactions have read, allow the first write. + if (nowStarted == total) { + dispatch_semaphore_signal(writeBarrier); + } + + dispatch_semaphore_wait(writeBarrier, DISPATCH_TIME_FOREVER); + // Refill the barrier so that the other transactions and retries succeed. + dispatch_semaphore_signal(writeBarrier); + + double newCount = ((NSNumber *)snapshot[@"count"]).doubleValue + 1.0; + [transaction updateData:@{ @"count" : @(newCount) } forDocument:doc]; + return @YES; + + } + completion:^(id _Nullable result, NSError *_Nullable error) { + [expectation fulfill]; + }]; + } + + [self awaitExpectations]; + // Now all transaction should be completed, so check the result. + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(@(5.0 + total), snapshot[@"count"]); + XCTAssertEqualObjects(@"yes", snapshot[@"other"]); +} + +// We currently require every document read to also be written. +// TODO(b/34879758): Re-enable this test once we fix it. +- (void)xtestHandleReadingOneDocAndWritingAnother { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc1 = [[firestore collectionWithPath:@"counters"] documentWithAutoID]; + FIRDocumentReference *doc2 = [[firestore collectionWithPath:@"counters"] documentWithAutoID]; + + [self writeDocumentRef:doc1 data:@{ @"count" : @(15.0) }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + // Get the first doc. + [transaction getDocument:doc1 error:error]; + XCTAssertNil(*error); + // Do a write outside of the transaction. The first time the + // transaction is tried, this will bump the version, which + // will cause the write to doc2 to fail. The second time, it + // will be a no-op and not bump the version. + dispatch_semaphore_t writeSemaphore = dispatch_semaphore_create(0); + [doc1 setData:@{ + @"count" : @(1234) + } + completion:^(NSError *_Nullable error) { + dispatch_semaphore_signal(writeSemaphore); + }]; + // We can block on it, because transactions run on a background queue. + dispatch_semaphore_wait(writeSemaphore, DISPATCH_TIME_FOREVER); + // Now try to update the other doc from within the transaction. + // This should fail once, because we read 15 earlier. + [transaction setData:@{ @"count" : @(16) } forDocument:doc2]; + return nil; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + // We currently require every document read to also be written. + // TODO(b/34879758): Add this check back once we drop that. + // NSError *error = nil; + // FIRDocument *snapshot = [transaction getDocument:doc1 error:&error]; + // XCTAssertNil(error); + // XCTAssertEquals(0, tries); + // XCTAssertEqualObjects(@(1234), snapshot[@"count"]); + // snapshot = [transaction getDocument:doc2 error:&error]; + // XCTAssertNil(error); + // XCTAssertEqualObjects(@(16), snapshot[@"count"]); + XCTAssertNotNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testReadingADocTwiceWithDifferentVersions { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{ @"count" : @(15.0) }]; + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + // Get the doc once. + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + XCTAssertEqualObjects(@(15), snapshot[@"count"]); + // Do a write outside of the transaction. + dispatch_semaphore_t writeSemaphore = dispatch_semaphore_create(0); + [doc setData:@{ + @"count" : @(1234) + } + completion:^(NSError *_Nullable error) { + dispatch_semaphore_signal(writeSemaphore); + }]; + // We can block on it, because transactions run on a background queue. + dispatch_semaphore_wait(writeSemaphore, DISPATCH_TIME_FOREVER); + // Get the doc again in the transaction with the new version. + snapshot = [transaction getDocument:doc error:error]; + // The get itself will fail, because we already read an earlier version of this document. + // TODO(klimt): Perhaps we shouldn't fail reads for this, but should wait and fail the + // whole transaction? It's an edge-case anyway, as developers shouldn't be reading the same + // do multiple times. But they need to handle read errors anyway. + XCTAssertNotNil(*error); + return nil; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + [expectation fulfill]; + }]; + [self awaitExpectations]; + + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(@(1234.0), snapshot[@"count"]); +} + +// We currently require every document read to also be written. +// TODO(b/34879758): Add this test back once we fix that. +- (void)xtestCannotHaveAGetWithoutMutations { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"foo"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertTrue(snapshot.exists); + XCTAssertNil(*error); + return nil; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertNotNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testSuccessWithNoTransactionOperations { + FIRFirestore *firestore = [self firestore]; + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + return @"yes"; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertEqualObjects(@"yes", result); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testCancellationOnError { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + __block volatile int32_t count = 0; + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + OSAtomicIncrement32(&count); + [transaction setData:@{@"foo" : @"bar"} forDocument:doc]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:35 userInfo:@{}]; + return nil; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertNil(result); + XCTAssertNotNil(error); + XCTAssertEqual(35, error.code); + [expectation fulfill]; + }]; + [self awaitExpectations]; + XCTAssertEqual(1, (int)count); + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertFalse(snapshot.exists); +} + +- (void)testUpdateFieldsWithDotsTransactionally { + FIRDocumentReference *doc = [self documentRef]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"testUpdateFieldsWithDotsTransactionally"]; + + [doc.firestore + runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + XCTAssertNil(*error); + [transaction setData:@{@"a.b" : @"old", @"c.d" : @"old"} forDocument:doc]; + [transaction updateData:@{ + [[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new" + } + forDocument:doc]; + return nil; + } + completion:^(id result, NSError *error) { + XCTAssertNil(error); + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"})); + }]; + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testUpdateNestedFieldsTransactionally { + FIRDocumentReference *doc = [self documentRef]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"testUpdateNestedFieldsTransactionally"]; + + [doc.firestore + runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + XCTAssertNil(*error); + [transaction setData:@{ + @"a" : @{@"b" : @"old"}, + @"c" : @{@"d" : @"old"}, + @"e" : @{@"f" : @"old"} + } + forDocument:doc]; + [transaction updateData:@{ + @"a.b" : @"new", + [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new" + } + forDocument:doc]; + return nil; + } + completion:^(id result, NSError *error) { + XCTAssertNil(error); + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(snapshot.data, (@{ + @"a" : @{@"b" : @"new"}, + @"c" : @{@"d" : @"new"}, + @"e" : @{@"f" : @"old"} + })); + }]; + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +@end diff --git a/Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.m b/Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.m new file mode 100644 index 0000000..34c0685 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.m @@ -0,0 +1,111 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Local/FSTEagerGarbageCollector.h" + +#import <XCTest/XCTest.h> + +#import "Local/FSTReferenceSet.h" +#import "Model/FSTDocumentKey.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTEagerGarbageCollectorTests : XCTestCase +@end + +@implementation FSTEagerGarbageCollectorTests + +- (void)testAddOrRemoveReferences { + FSTEagerGarbageCollector *gc = [[FSTEagerGarbageCollector alloc] init]; + FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init]; + [gc addGarbageSource:referenceSet]; + + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"foo/bar"]; + [referenceSet addReferenceToKey:key forID:1]; + FSTAssertEqualSets([gc collectGarbage], @[]); + XCTAssertFalse([referenceSet isEmpty]); + + [referenceSet removeReferenceToKey:key forID:1]; + FSTAssertEqualSets([gc collectGarbage], @[ key ]); + XCTAssertTrue([referenceSet isEmpty]); +} + +- (void)testRemoveAllReferencesForID { + FSTEagerGarbageCollector *gc = [[FSTEagerGarbageCollector alloc] init]; + FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init]; + [gc addGarbageSource:referenceSet]; + + FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"]; + FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"]; + FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"foo/blah"]; + [referenceSet addReferenceToKey:key1 forID:1]; + [referenceSet addReferenceToKey:key2 forID:1]; + [referenceSet addReferenceToKey:key3 forID:2]; + XCTAssertFalse([referenceSet isEmpty]); + + [referenceSet removeReferencesForID:1]; + FSTAssertEqualSets([gc collectGarbage], (@[ key1, key2 ])); + XCTAssertFalse([referenceSet isEmpty]); + + [referenceSet removeReferencesForID:2]; + FSTAssertEqualSets([gc collectGarbage], @[ key3 ]); + XCTAssertTrue([referenceSet isEmpty]); +} + +- (void)testTwoReferenceSetsAtTheSameTime { + FSTReferenceSet *remoteTargets = [[FSTReferenceSet alloc] init]; + FSTReferenceSet *localViews = [[FSTReferenceSet alloc] init]; + FSTReferenceSet *mutations = [[FSTReferenceSet alloc] init]; + + FSTEagerGarbageCollector *gc = [[FSTEagerGarbageCollector alloc] init]; + [gc addGarbageSource:remoteTargets]; + [gc addGarbageSource:localViews]; + [gc addGarbageSource:mutations]; + + FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"]; + [remoteTargets addReferenceToKey:key1 forID:1]; + [localViews addReferenceToKey:key1 forID:1]; + [mutations addReferenceToKey:key1 forID:10]; + + FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"]; + [mutations addReferenceToKey:key2 forID:10]; + + XCTAssertFalse([remoteTargets isEmpty]); + XCTAssertFalse([localViews isEmpty]); + XCTAssertFalse([mutations isEmpty]); + + [localViews removeReferencesForID:1]; + FSTAssertEqualSets([gc collectGarbage], @[]); + + [remoteTargets removeReferencesForID:1]; + FSTAssertEqualSets([gc collectGarbage], @[]); + + [mutations removeReferenceToKey:key1 forID:10]; + FSTAssertEqualSets([gc collectGarbage], @[ key1 ]); + + [mutations removeReferenceToKey:key2 forID:10]; + FSTAssertEqualSets([gc collectGarbage], @[ key2 ]); + + XCTAssertTrue([remoteTargets isEmpty]); + XCTAssertTrue([localViews isEmpty]); + XCTAssertTrue([mutations isEmpty]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLevelDBKeyTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBKeyTests.mm new file mode 100644 index 0000000..3374fcf --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTLevelDBKeyTests.mm @@ -0,0 +1,361 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Local/FSTLevelDBKey.h" + +#import <XCTest/XCTest.h> + +#import "Model/FSTPath.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTLevelDBKeyTests : XCTestCase +@end + +// I can't believe I have to write this... +bool StartsWith(const std::string &value, const std::string &prefix) { + return prefix.size() <= value.size() && std::equal(prefix.begin(), prefix.end(), value.begin()); +} + +static std::string RemoteDocKey(NSString *pathString) { + return [FSTLevelDBRemoteDocumentKey keyWithDocumentKey:FSTTestDocKey(pathString)]; +} + +static std::string RemoteDocKeyPrefix(NSString *pathString) { + return [FSTLevelDBRemoteDocumentKey keyPrefixWithResourcePath:FSTTestPath(pathString)]; +} + +static std::string DocMutationKey(NSString *userID, NSString *key, FSTBatchID batchID) { + return [FSTLevelDBDocumentMutationKey keyWithUserID:userID + documentKey:FSTTestDocKey(key) + batchID:batchID]; +} + +static std::string TargetDocKey(FSTTargetID targetID, NSString *key) { + return [FSTLevelDBTargetDocumentKey keyWithTargetID:targetID documentKey:FSTTestDocKey(key)]; +} + +static std::string DocTargetKey(NSString *key, FSTTargetID targetID) { + return [FSTLevelDBDocumentTargetKey keyWithDocumentKey:FSTTestDocKey(key) targetID:targetID]; +} + +/** + * Asserts that the description for given key is equal to the expected description. + * + * @param key A StringView of a textual key + * @param key An NSString that [FSTLevelDBKey descriptionForKey:] is expected to produce. + */ +#define FSTAssertExpectedKeyDescription(key, expectedDescription) \ + XCTAssertEqualObjects([FSTLevelDBKey descriptionForKey:(key)], (expectedDescription)) + +#define FSTAssertKeyLessThan(left, right) \ + do { \ + std::string leftKey = (left); \ + std::string rightKey = (right); \ + XCTAssertLessThan(leftKey.compare(right), 0, @"Expected %@ to be less than %@", \ + [FSTLevelDBKey descriptionForKey:leftKey], \ + [FSTLevelDBKey descriptionForKey:rightKey]); \ + } while (0) + +@implementation FSTLevelDBKeyTests + +- (void)testMutationKeyPrefixing { + auto tableKey = [FSTLevelDBMutationKey keyPrefix]; + auto emptyUserKey = [FSTLevelDBMutationKey keyPrefixWithUserID:""]; + auto fooUserKey = [FSTLevelDBMutationKey keyPrefixWithUserID:"foo"]; + + auto foo2Key = [FSTLevelDBMutationKey keyWithUserID:"foo" batchID:2]; + + XCTAssertTrue(StartsWith(emptyUserKey, tableKey)); + + // This is critical: prefixes of the a value don't convert into prefixes of the key. + XCTAssertTrue(StartsWith(fooUserKey, tableKey)); + XCTAssertFalse(StartsWith(fooUserKey, emptyUserKey)); + + // However whole segments in common are prefixes. + XCTAssertTrue(StartsWith(foo2Key, tableKey)); + XCTAssertTrue(StartsWith(foo2Key, fooUserKey)); +} + +- (void)testMutationKeyEncodeDecodeCycle { + FSTLevelDBMutationKey *key = [[FSTLevelDBMutationKey alloc] init]; + std::string user("foo"); + + NSArray<NSNumber *> *batchIds = @[ @0, @1, @100, @(INT_MAX - 1), @(INT_MAX) ]; + for (NSNumber *batchIDNumber in batchIds) { + FSTBatchID batchID = [batchIDNumber intValue]; + auto encoded = [FSTLevelDBMutationKey keyWithUserID:user batchID:batchID]; + + BOOL ok = [key decodeKey:encoded]; + XCTAssertTrue(ok); + XCTAssertEqual(key.userID, user); + XCTAssertEqual(key.batchID, batchID); + } +} + +- (void)testMutationKeyDescription { + FSTAssertExpectedKeyDescription([FSTLevelDBMutationKey keyPrefix], @"[mutation: incomplete key]"); + + FSTAssertExpectedKeyDescription([FSTLevelDBMutationKey keyPrefixWithUserID:@"user1"], + @"[mutation: userID=user1 incomplete key]"); + + auto key = [FSTLevelDBMutationKey keyWithUserID:@"user1" batchID:42]; + FSTAssertExpectedKeyDescription(key, @"[mutation: userID=user1 batchID=42]"); + + FSTAssertExpectedKeyDescription(key + " extra", + @"[mutation: userID=user1 batchID=42 invalid " + @"key=<hW11dGF0aW9uAAGNdXNlcjEAAYqqgCBleHRyYQ==>]"); + + // Truncate the key so that it's missing its terminator. + key.resize(key.size() - 1); + FSTAssertExpectedKeyDescription(key, @"[mutation: userID=user1 batchID=42 incomplete key]"); +} + +- (void)testDocumentMutationKeyPrefixing { + auto tableKey = [FSTLevelDBDocumentMutationKey keyPrefix]; + auto emptyUserKey = [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:""]; + auto fooUserKey = [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:"foo"]; + + FSTDocumentKey *documentKey = FSTTestDocKey(@"foo/bar"); + auto foo2Key = + [FSTLevelDBDocumentMutationKey keyWithUserID:"foo" documentKey:documentKey batchID:2]; + + XCTAssertTrue(StartsWith(emptyUserKey, tableKey)); + + // While we want a key with whole segments in common be considered a prefix it's vital that + // partial segments in common not be prefixes. + XCTAssertTrue(StartsWith(fooUserKey, tableKey)); + + // Here even though "" is a prefix of "foo" that prefix is within a segment so keys derived from + // those segments cannot be prefixes of each other. + XCTAssertFalse(StartsWith(fooUserKey, emptyUserKey)); + XCTAssertFalse(StartsWith(emptyUserKey, fooUserKey)); + + // However whole segments in common are prefixes. + XCTAssertTrue(StartsWith(foo2Key, tableKey)); + XCTAssertTrue(StartsWith(foo2Key, fooUserKey)); +} + +- (void)testDocumentMutationKeyEncodeDecodeCycle { + FSTLevelDBDocumentMutationKey *key = [[FSTLevelDBDocumentMutationKey alloc] init]; + std::string user("foo"); + + NSArray<FSTDocumentKey *> *documentKeys = @[ FSTTestDocKey(@"a/b"), FSTTestDocKey(@"a/b/c/d") ]; + + NSArray<NSNumber *> *batchIds = @[ @0, @1, @100, @(INT_MAX - 1), @(INT_MAX) ]; + for (NSNumber *batchIDNumber in batchIds) { + for (FSTDocumentKey *documentKey in documentKeys) { + FSTBatchID batchID = [batchIDNumber intValue]; + auto encoded = [FSTLevelDBDocumentMutationKey keyWithUserID:user + documentKey:documentKey + batchID:batchID]; + + BOOL ok = [key decodeKey:encoded]; + XCTAssertTrue(ok); + XCTAssertEqual(key.userID, user); + XCTAssertEqualObjects(key.documentKey, documentKey); + XCTAssertEqual(key.batchID, batchID); + } + } +} + +- (void)testDocumentMutationKeyOrdering { + // Different user: + FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0), DocMutationKey(@"10", @"foo/bar", 0)); + FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0), DocMutationKey(@"2", @"foo/bar", 0)); + + // Different paths: + FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0), DocMutationKey(@"1", @"foo/baz", 0)); + FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0), DocMutationKey(@"1", @"foo/bar2", 0)); + FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0), + DocMutationKey(@"1", @"foo/bar/suffix/key", 0)); + FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar/suffix/key", 0), + DocMutationKey(@"1", @"foo/bar2", 0)); + + // Different batchID: + FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0), DocMutationKey(@"1", @"foo/bar", 1)); +} + +- (void)testDocumentMutationKeyDescription { + FSTAssertExpectedKeyDescription([FSTLevelDBDocumentMutationKey keyPrefix], + @"[document_mutation: incomplete key]"); + + FSTAssertExpectedKeyDescription([FSTLevelDBDocumentMutationKey keyPrefixWithUserID:@"user1"], + @"[document_mutation: userID=user1 incomplete key]"); + + auto key = [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:@"user1" + resourcePath:FSTTestPath(@"foo/bar")]; + FSTAssertExpectedKeyDescription(key, + @"[document_mutation: userID=user1 key=foo/bar incomplete key]"); + + key = [FSTLevelDBDocumentMutationKey keyWithUserID:@"user1" + documentKey:FSTTestDocKey(@"foo/bar") + batchID:42]; + FSTAssertExpectedKeyDescription(key, @"[document_mutation: userID=user1 key=foo/bar batchID=42]"); +} + +- (void)testTargetGlobalKeyEncodeDecodeCycle { + FSTLevelDBTargetGlobalKey *key = [[FSTLevelDBTargetGlobalKey alloc] init]; + + auto encoded = [FSTLevelDBTargetGlobalKey key]; + BOOL ok = [key decodeKey:encoded]; + XCTAssertTrue(ok); +} + +- (void)testTargetGlobalKeyDescription { + FSTAssertExpectedKeyDescription([FSTLevelDBTargetGlobalKey key], @"[target_global:]"); +} + +- (void)testTargetKeyEncodeDecodeCycle { + FSTLevelDBTargetKey *key = [[FSTLevelDBTargetKey alloc] init]; + FSTTargetID targetID = 42; + + auto encoded = [FSTLevelDBTargetKey keyWithTargetID:42]; + BOOL ok = [key decodeKey:encoded]; + XCTAssertTrue(ok); + XCTAssertEqual(key.targetID, targetID); +} + +- (void)testTargetKeyDescription { + FSTAssertExpectedKeyDescription([FSTLevelDBTargetKey keyWithTargetID:42], + @"[target: targetID=42]"); +} + +- (void)testQueryTargetKeyEncodeDecodeCycle { + FSTLevelDBQueryTargetKey *key = [[FSTLevelDBQueryTargetKey alloc] init]; + std::string canonicalID("foo"); + FSTTargetID targetID = 42; + + auto encoded = [FSTLevelDBQueryTargetKey keyWithCanonicalID:canonicalID targetID:42]; + BOOL ok = [key decodeKey:encoded]; + XCTAssertTrue(ok); + XCTAssertEqual(key.canonicalID, canonicalID); + XCTAssertEqual(key.targetID, targetID); +} + +- (void)testQueryKeyDescription { + FSTAssertExpectedKeyDescription([FSTLevelDBQueryTargetKey keyWithCanonicalID:"foo" targetID:42], + @"[query_target: canonicalID=foo targetID=42]"); +} + +- (void)testTargetDocumentKeyEncodeDecodeCycle { + FSTLevelDBTargetDocumentKey *key = [[FSTLevelDBTargetDocumentKey alloc] init]; + + auto encoded = + [FSTLevelDBTargetDocumentKey keyWithTargetID:42 documentKey:FSTTestDocKey(@"foo/bar")]; + BOOL ok = [key decodeKey:encoded]; + XCTAssertTrue(ok); + XCTAssertEqual(key.targetID, 42); + XCTAssertEqualObjects(key.documentKey, FSTTestDocKey(@"foo/bar")); +} + +- (void)testTargetDocumentKeyOrdering { + // Different targetID: + FSTAssertKeyLessThan(TargetDocKey(1, @"foo/bar"), TargetDocKey(2, @"foo/bar")); + FSTAssertKeyLessThan(TargetDocKey(2, @"foo/bar"), TargetDocKey(10, @"foo/bar")); + FSTAssertKeyLessThan(TargetDocKey(10, @"foo/bar"), TargetDocKey(100, @"foo/bar")); + FSTAssertKeyLessThan(TargetDocKey(42, @"foo/bar"), TargetDocKey(100, @"foo/bar")); + + // Different paths: + FSTAssertKeyLessThan(TargetDocKey(1, @"foo/bar"), TargetDocKey(1, @"foo/baz")); + FSTAssertKeyLessThan(TargetDocKey(1, @"foo/bar"), TargetDocKey(1, @"foo/bar2")); + FSTAssertKeyLessThan(TargetDocKey(1, @"foo/bar"), TargetDocKey(1, @"foo/bar/suffix/key")); + FSTAssertKeyLessThan(TargetDocKey(1, @"foo/bar/suffix/key"), TargetDocKey(1, @"foo/bar2")); +} + +- (void)testTargetDocumentKeyDescription { + auto key = [FSTLevelDBTargetDocumentKey keyWithTargetID:42 documentKey:FSTTestDocKey(@"foo/bar")]; + XCTAssertEqualObjects([FSTLevelDBKey descriptionForKey:key], + @"[target_document: targetID=42 key=foo/bar]"); +} + +- (void)testDocumentTargetKeyEncodeDecodeCycle { + FSTLevelDBDocumentTargetKey *key = [[FSTLevelDBDocumentTargetKey alloc] init]; + + auto encoded = + [FSTLevelDBDocumentTargetKey keyWithDocumentKey:FSTTestDocKey(@"foo/bar") targetID:42]; + BOOL ok = [key decodeKey:encoded]; + XCTAssertTrue(ok); + XCTAssertEqualObjects(key.documentKey, FSTTestDocKey(@"foo/bar")); + XCTAssertEqual(key.targetID, 42); +} + +- (void)testDocumentTargetKeyDescription { + auto key = [FSTLevelDBDocumentTargetKey keyWithDocumentKey:FSTTestDocKey(@"foo/bar") targetID:42]; + XCTAssertEqualObjects([FSTLevelDBKey descriptionForKey:key], + @"[document_target: key=foo/bar targetID=42]"); +} + +- (void)testDocumentTargetKeyOrdering { + // Different paths: + FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 1), DocTargetKey(@"foo/baz", 1)); + FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 1), DocTargetKey(@"foo/bar2", 1)); + FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 1), DocTargetKey(@"foo/bar/suffix/key", 1)); + FSTAssertKeyLessThan(DocTargetKey(@"foo/bar/suffix/key", 1), DocTargetKey(@"foo/bar2", 1)); + + // Different targetID: + FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 1), DocTargetKey(@"foo/bar", 2)); + FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 2), DocTargetKey(@"foo/bar", 10)); + FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 10), DocTargetKey(@"foo/bar", 100)); + FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 42), DocTargetKey(@"foo/bar", 100)); +} + +- (void)testRemoteDocumentKeyPrefixing { + auto tableKey = [FSTLevelDBRemoteDocumentKey keyPrefix]; + + XCTAssertTrue(StartsWith(RemoteDocKey(@"foo/bar"), tableKey)); + + // This is critical: foo/bar2 should not contain foo/bar. + XCTAssertFalse(StartsWith(RemoteDocKey(@"foo/bar2"), RemoteDocKey(@"foo/bar"))); + + // Prefixes must be encoded specially + XCTAssertFalse(StartsWith(RemoteDocKey(@"foo/bar/baz/quu"), RemoteDocKey(@"foo/bar"))); + XCTAssertTrue(StartsWith(RemoteDocKey(@"foo/bar/baz/quu"), RemoteDocKeyPrefix(@"foo/bar"))); + XCTAssertTrue(StartsWith(RemoteDocKeyPrefix(@"foo/bar/baz/quu"), RemoteDocKeyPrefix(@"foo/bar"))); + XCTAssertTrue(StartsWith(RemoteDocKeyPrefix(@"foo/bar/baz"), RemoteDocKeyPrefix(@"foo/bar"))); + XCTAssertTrue(StartsWith(RemoteDocKeyPrefix(@"foo/bar"), RemoteDocKeyPrefix(@"foo"))); +} + +- (void)testRemoteDocumentKeyOrdering { + FSTAssertKeyLessThan(RemoteDocKey(@"foo/bar"), RemoteDocKey(@"foo/bar2")); + FSTAssertKeyLessThan(RemoteDocKey(@"foo/bar"), RemoteDocKey(@"foo/bar/suffix/key")); +} + +- (void)testRemoteDocumentKeyEncodeDecodeCycle { + FSTLevelDBRemoteDocumentKey *key = [[FSTLevelDBRemoteDocumentKey alloc] init]; + + NSArray<NSString *> *paths = @[ @"foo/bar", @"foo/bar2", @"foo/bar/baz/quux" ]; + for (NSString *path in paths) { + auto encoded = RemoteDocKey(path); + BOOL ok = [key decodeKey:encoded]; + XCTAssertTrue(ok); + XCTAssertEqualObjects(key.documentKey, FSTTestDocKey(path)); + } +} + +- (void)testRemoteDocumentKeyDescription { + FSTAssertExpectedKeyDescription( + [FSTLevelDBRemoteDocumentKey keyWithDocumentKey:FSTTestDocKey(@"foo/bar/baz/quux")], + @"[remote_document: key=foo/bar/baz/quux]"); +} + +@end + +#undef FSTAssertExpectedKeyDescription + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.m b/Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.m new file mode 100644 index 0000000..d426149 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.m @@ -0,0 +1,45 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Local/FSTLocalStore.h" + +#import <XCTest/XCTest.h> + +#import "Auth/FSTUser.h" +#import "Local/FSTLevelDB.h" + +#import "FSTLocalStoreTests.h" +#import "FSTPersistenceTestHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The tests for FSTLevelDBLocalStore are performed on the FSTLocalStore protocol in + * FSTLocalStoreTests. This class is merely responsible for creating a new FSTPersistence + * implementation on demand. + */ +@interface FSTLevelDBLocalStoreTests : FSTLocalStoreTests +@end + +@implementation FSTLevelDBLocalStoreTests + +- (id<FSTPersistence>)persistence { + return [FSTPersistenceTestHelpers levelDBPersistence]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm new file mode 100644 index 0000000..1e66aac --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm @@ -0,0 +1,158 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Local/FSTLevelDBMutationQueue.h" + +#import <XCTest/XCTest.h> +#include <leveldb/db.h> + +#import "Protos/objc/firestore/local/Mutation.pbobjc.h" +#import "Auth/FSTUser.h" +#import "Local/FSTLevelDB.h" +#import "Local/FSTLevelDBKey.h" +#import "Local/FSTWriteGroup.h" +#include "Port/ordered_code.h" + +#import "FSTMutationQueueTests.h" +#import "FSTPersistenceTestHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +using leveldb::DB; +using leveldb::Slice; +using leveldb::Status; +using leveldb::WriteOptions; +using Firestore::StringView; +using Firestore::OrderedCode; + +// A dummy mutation value, useful for testing code that's known to examine only mutation keys. +static const char *kDummy = "1"; + +/** + * Most of the tests for FSTLevelDBMutationQueue are performed on the FSTMutationQueue protocol in + * FSTMutationQueueTests. This class is responsible for setting up the @a mutationQueue plus any + * additional LevelDB-specific tests. + */ +@interface FSTLevelDBMutationQueueTests : FSTMutationQueueTests +@end + +/** + * Creates a key that's structurally the same as FSTLevelDBMutationKey except it allows for + * nonstandard table names. + */ +std::string MutationLikeKey(StringView table, StringView userID, FSTBatchID batchID) { + std::string key; + OrderedCode::WriteString(&key, table); + OrderedCode::WriteString(&key, userID); + OrderedCode::WriteSignedNumIncreasing(&key, batchID); + return key; +} + +@implementation FSTLevelDBMutationQueueTests { + FSTLevelDB *_db; +} + +- (void)setUp { + [super setUp]; + _db = [FSTPersistenceTestHelpers levelDBPersistence]; + self.mutationQueue = [_db mutationQueueForUser:[[FSTUser alloc] initWithUID:@"user"]]; + self.persistence = _db; + + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Start MutationQueue"]; + [self.mutationQueue startWithGroup:group]; + [self.persistence commitGroup:group]; +} + +- (void)testLoadNextBatchID_zeroWhenTotallyEmpty { + // Initial seek is invalid + XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 0); +} + +- (void)testLoadNextBatchID_zeroWhenNoMutations { + // Initial seek finds no mutations + [self setDummyValueForKey:MutationLikeKey("mutationr", "foo", 20)]; + [self setDummyValueForKey:MutationLikeKey("mutationsa", "foo", 10)]; + XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 0); +} + +- (void)testLoadNextBatchID_findsSingleRow { + // Seeks off the end of the table altogether + [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:6]]; + + XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 7); +} + +- (void)testLoadNextBatchID_findsSingleRowAmongNonMutations { + // Seeks into table following mutations. + [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:6]]; + [self setDummyValueForKey:MutationLikeKey("mutationsa", "foo", 10)]; + + XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 7); +} + +- (void)testLoadNextBatchID_findsMaxAcrossUsers { + [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"fo" batchID:5]]; + [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"food" batchID:3]]; + + [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:6]]; + [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:2]]; + [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:1]]; + + XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 7); +} + +- (void)testLoadNextBatchID_onlyFindsMutations { + // Write higher-valued batchIDs in nearby "tables" + auto tables = @[ @"mutatio", @"mutationsa", @"bears", @"zombies" ]; + FSTBatchID highBatchID = 5; + for (NSString *table in tables) { + [self setDummyValueForKey:MutationLikeKey(table, "", highBatchID++)]; + } + + [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"bar" batchID:3]]; + [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"bar" batchID:2]]; + [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:1]]; + + // None of the higher tables should match -- this is the only entry that's in the mutations + // table + XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 4); +} + +- (void)testEmptyProtoCanBeUpgraded { + // An empty protocol buffer serializes to a zero-length byte buffer. + GPBEmpty *empty = [GPBEmpty message]; + NSData *emptyData = [empty data]; + XCTAssertEqual(emptyData.length, 0); + + // Choose some other (arbitrary) proto and parse it from the empty message and it should all be + // defaults. This shows that empty proto values within the index row value don't pose any future + // liability. + NSError *error; + FSTPBMutationQueue *parsedMessage = [FSTPBMutationQueue parseFromData:emptyData error:&error]; + XCTAssertNil(error); + + FSTPBMutationQueue *defaultMessage = [FSTPBMutationQueue message]; + XCTAssertEqual(parsedMessage.lastAcknowledgedBatchId, defaultMessage.lastAcknowledgedBatchId); + XCTAssertEqualObjects(parsedMessage.lastStreamToken, defaultMessage.lastStreamToken); +} + +- (void)setDummyValueForKey:(const std::string &)key { + _db.ptr->Put(WriteOptions(), key, kDummy); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.m b/Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.m new file mode 100644 index 0000000..8149397 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.m @@ -0,0 +1,54 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Local/FSTLevelDBQueryCache.h" + +#import "Local/FSTLevelDB.h" + +#import "FSTPersistenceTestHelpers.h" +#import "FSTQueryCacheTests.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTLevelDBQueryCacheTests : FSTQueryCacheTests +@end + +/** + * The tests for FSTLevelDBQueryCache are performed on the FSTQueryCache protocol in + * FSTQueryCacheTests. This class is merely responsible for setting up and tearing down the + * @a queryCache. + */ +@implementation FSTLevelDBQueryCacheTests + +- (void)setUp { + [super setUp]; + + self.persistence = [FSTPersistenceTestHelpers levelDBPersistence]; + self.queryCache = [self.persistence queryCache]; + [self.queryCache start]; +} + +- (void)tearDown { + [self.queryCache shutdown]; + self.persistence = nil; + self.queryCache = nil; + + [super tearDown]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLevelDBRemoteDocumentCacheTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBRemoteDocumentCacheTests.mm new file mode 100644 index 0000000..a6af103 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTLevelDBRemoteDocumentCacheTests.mm @@ -0,0 +1,78 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FSTRemoteDocumentCacheTests.h" + +#include <leveldb/db.h> + +#import "Local/FSTLevelDB.h" +#import "Local/FSTLevelDBKey.h" +#include "Port/ordered_code.h" + +#import "FSTPersistenceTestHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +using leveldb::WriteOptions; +using Firestore::OrderedCode; + +// A dummy document value, useful for testing code that's known to examine only document keys. +static const char *kDummy = "1"; + +/** + * The tests for FSTLevelDBRemoteDocumentCache are performed on the FSTRemoteDocumentCache + * protocol in FSTRemoteDocumentCacheTests. This class is merely responsible for setting up and + * tearing down the @a remoteDocumentCache. + */ +@interface FSTLevelDBRemoteDocumentCacheTests : FSTRemoteDocumentCacheTests +@end + +@implementation FSTLevelDBRemoteDocumentCacheTests { + FSTLevelDB *_db; +} + +- (void)setUp { + [super setUp]; + _db = [FSTPersistenceTestHelpers levelDBPersistence]; + self.persistence = _db; + self.remoteDocumentCache = [self.persistence remoteDocumentCache]; + + // Write a couple dummy rows that should appear before/after the remote_documents table to make + // sure the tests are unaffected. + [self writeDummyRowWithSegments:@[ @"remote_documentr", @"foo", @"bar" ]]; + [self writeDummyRowWithSegments:@[ @"remote_documentsa", @"foo", @"bar" ]]; +} + +- (void)tearDown { + [self.remoteDocumentCache shutdown]; + self.remoteDocumentCache = nil; + self.persistence = nil; + _db = nil; + [super tearDown]; +} + +- (void)writeDummyRowWithSegments:(NSArray<NSString *> *)segments { + std::string key; + for (NSString *segment in segments) { + OrderedCode::WriteString(&key, segment.UTF8String); + } + + _db.ptr->Put(WriteOptions(), key, kDummy); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLocalSerializerTests.m b/Firestore/Example/Tests/Local/FSTLocalSerializerTests.m new file mode 100644 index 0000000..0080f89 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTLocalSerializerTests.m @@ -0,0 +1,181 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Local/FSTLocalSerializer.h" + +#import <XCTest/XCTest.h> + +#import "Core/FSTQuery.h" +#import "Core/FSTSnapshotVersion.h" +#import "Core/FSTTimestamp.h" +#import "Local/FSTQueryData.h" +#import "Model/FSTDatabaseID.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentKey.h" +#import "Model/FSTFieldValue.h" +#import "Model/FSTMutation.h" +#import "Model/FSTMutationBatch.h" +#import "Model/FSTPath.h" +#import "Protos/objc/firestore/local/MaybeDocument.pbobjc.h" +#import "Protos/objc/firestore/local/Mutation.pbobjc.h" +#import "Protos/objc/firestore/local/Target.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Common.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Document.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Query.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Write.pbobjc.h" +#import "Protos/objc/google/type/Latlng.pbobjc.h" +#import "Remote/FSTSerializerBeta.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTSerializerBeta (Test) +- (GCFSValue *)encodedNull; +- (GCFSValue *)encodedBool:(BOOL)value; +- (GCFSValue *)encodedDouble:(double)value; +- (GCFSValue *)encodedInteger:(int64_t)value; +- (GCFSValue *)encodedString:(NSString *)value; +@end + +@interface FSTLocalSerializerTests : XCTestCase + +@property(nonatomic, strong) FSTLocalSerializer *serializer; +@property(nonatomic, strong) FSTSerializerBeta *remoteSerializer; + +@end + +@implementation FSTLocalSerializerTests + +- (void)setUp { + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"]; + self.remoteSerializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseID]; + self.serializer = [[FSTLocalSerializer alloc] initWithRemoteSerializer:self.remoteSerializer]; +} + +- (void)testEncodesMutationBatch { + FSTMutation *set = FSTTestSetMutation(@"foo/bar", @{ @"a" : @"b", @"num" : @1 }); + FSTMutation *patch = [[FSTPatchMutation alloc] + initWithKey:[FSTDocumentKey keyWithPathString:@"bar/baz"] + fieldMask:[[FSTFieldMask alloc] initWithFields:@[ FSTTestFieldPath(@"a") ]] + value:FSTTestObjectValue( + @{ @"a" : @"b", + @"num" : @1 }) + precondition:[FSTPrecondition preconditionWithExists:YES]]; + FSTMutation *del = FSTTestDeleteMutation(@"baz/quux"); + FSTTimestamp *writeTime = [FSTTimestamp timestamp]; + FSTMutationBatch *model = [[FSTMutationBatch alloc] initWithBatchID:42 + localWriteTime:writeTime + mutations:@[ set, patch, del ]]; + + GCFSWrite *setProto = [GCFSWrite message]; + setProto.update.name = @"projects/p/databases/d/documents/foo/bar"; + [setProto.update.fields addEntriesFromDictionary:@{ + @"a" : [self.remoteSerializer encodedString:@"b"], + @"num" : [self.remoteSerializer encodedInteger:1] + }]; + + GCFSWrite *patchProto = [GCFSWrite message]; + patchProto.update.name = @"projects/p/databases/d/documents/bar/baz"; + [patchProto.update.fields addEntriesFromDictionary:@{ + @"a" : [self.remoteSerializer encodedString:@"b"], + @"num" : [self.remoteSerializer encodedInteger:1] + }]; + [patchProto.updateMask.fieldPathsArray addObjectsFromArray:@[ @"a" ]]; + patchProto.currentDocument.exists = YES; + + GCFSWrite *delProto = [GCFSWrite message]; + delProto.delete_p = @"projects/p/databases/d/documents/baz/quux"; + + GPBTimestamp *writeTimeProto = [GPBTimestamp message]; + writeTimeProto.seconds = writeTime.seconds; + writeTimeProto.nanos = writeTime.nanos; + + FSTPBWriteBatch *batchProto = [FSTPBWriteBatch message]; + batchProto.batchId = 42; + [batchProto.writesArray addObjectsFromArray:@[ setProto, patchProto, delProto ]]; + batchProto.localWriteTime = writeTimeProto; + + XCTAssertEqualObjects([self.serializer encodedMutationBatch:model], batchProto); + FSTMutationBatch *decoded = [self.serializer decodedMutationBatch:batchProto]; + XCTAssertEqual(decoded.batchID, model.batchID); + XCTAssertEqualObjects(decoded.localWriteTime, model.localWriteTime); + XCTAssertEqualObjects(decoded.mutations, model.mutations); + XCTAssertEqualObjects([decoded keys], [model keys]); +} + +- (void)testEncodesDocumentAsMaybeDocument { + FSTDocument *doc = FSTTestDoc(@"some/path", 42, @{@"foo" : @"bar"}, NO); + + FSTPBMaybeDocument *maybeDocProto = [FSTPBMaybeDocument message]; + maybeDocProto.document = [GCFSDocument message]; + maybeDocProto.document.name = @"projects/p/databases/d/documents/some/path"; + [maybeDocProto.document.fields addEntriesFromDictionary:@{ + @"foo" : [self.remoteSerializer encodedString:@"bar"], + }]; + maybeDocProto.document.updateTime.seconds = 0; + maybeDocProto.document.updateTime.nanos = 42000; + + XCTAssertEqualObjects([self.serializer encodedMaybeDocument:doc], maybeDocProto); + FSTMaybeDocument *decoded = [self.serializer decodedMaybeDocument:maybeDocProto]; + XCTAssertEqualObjects(decoded, doc); +} + +- (void)testEncodesDeletedDocumentAsMaybeDocument { + FSTDeletedDocument *deletedDoc = FSTTestDeletedDoc(@"some/path", 42); + + FSTPBMaybeDocument *maybeDocProto = [FSTPBMaybeDocument message]; + maybeDocProto.noDocument = [FSTPBNoDocument message]; + maybeDocProto.noDocument.name = @"projects/p/databases/d/documents/some/path"; + maybeDocProto.noDocument.readTime.seconds = 0; + maybeDocProto.noDocument.readTime.nanos = 42000; + + XCTAssertEqualObjects([self.serializer encodedMaybeDocument:deletedDoc], maybeDocProto); + FSTMaybeDocument *decoded = [self.serializer decodedMaybeDocument:maybeDocProto]; + XCTAssertEqualObjects(decoded, deletedDoc); +} + +- (void)testEncodesQueryData { + FSTQuery *query = FSTTestQuery(@"room"); + FSTTargetID targetID = 42; + FSTSnapshotVersion *version = FSTTestVersion(1039); + NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1039); + + FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query + targetID:targetID + purpose:FSTQueryPurposeListen + snapshotVersion:version + resumeToken:resumeToken]; + + // Let the RPC serializer test various permutations of query serialization. + GCFSTarget_QueryTarget *queryTarget = [self.remoteSerializer encodedQueryTarget:query]; + + FSTPBTarget *expected = [FSTPBTarget message]; + expected.targetId = targetID; + expected.snapshotVersion.nanos = 1039000; + expected.resumeToken = [resumeToken copy]; + expected.query.parent = queryTarget.parent; + expected.query.structuredQuery = queryTarget.structuredQuery; + + XCTAssertEqualObjects([self.serializer encodedQueryData:queryData], expected); + FSTQueryData *decoded = [self.serializer decodedQueryData:expected]; + XCTAssertEqualObjects(decoded, queryData); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.h b/Firestore/Example/Tests/Local/FSTLocalStoreTests.h new file mode 100644 index 0000000..8e06d82 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.h @@ -0,0 +1,38 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <XCTest/XCTest.h> + +@class FSTLocalStore; + +NS_ASSUME_NONNULL_BEGIN + +/** + * These are tests for any implementation of the FSTLocalStore protocol. + * + * To test a specific implementation of FSTLocalStore: + * + * + Subclass FSTLocalStoreTests + * + override -persistence, creating a new instance of FSTPersistence. + */ +@interface FSTLocalStoreTests : XCTestCase + +/** Creates and returns an appropriate id<FSTPersistence> implementation. */ +- (id<FSTPersistence>)persistence; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.m b/Firestore/Example/Tests/Local/FSTLocalStoreTests.m new file mode 100644 index 0000000..ab492a7 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.m @@ -0,0 +1,795 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Local/FSTLocalStore.h" + +#import <XCTest/XCTest.h> + +#import "Auth/FSTUser.h" +#import "Core/FSTQuery.h" +#import "Core/FSTTimestamp.h" +#import "Local/FSTEagerGarbageCollector.h" +#import "Local/FSTLocalWriteResult.h" +#import "Local/FSTNoOpGarbageCollector.h" +#import "Local/FSTPersistence.h" +#import "Local/FSTQueryData.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentKey.h" +#import "Model/FSTDocumentSet.h" +#import "Model/FSTMutation.h" +#import "Model/FSTMutationBatch.h" +#import "Model/FSTPath.h" +#import "Remote/FSTRemoteEvent.h" +#import "Remote/FSTWatchChange.h" +#import "Util/FSTClasses.h" + +#import "FSTHelpers.h" +#import "FSTImmutableSortedDictionary+Testing.h" +#import "FSTImmutableSortedSet+Testing.h" +#import "FSTLocalStoreTests.h" +#import "FSTWatchChange+Testing.h" + +NS_ASSUME_NONNULL_BEGIN + +/** Creates a document version dictionary mapping the document in @a mutation to @a version. */ +FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, + FSTTestSnapshotVersion version) { + FSTDocumentVersionDictionary *result = [FSTDocumentVersionDictionary documentVersionDictionary]; + result = [result dictionaryBySettingObject:FSTTestVersion(version) forKey:mutation.key]; + return result; +} + +@interface FSTLocalStoreTests () + +@property(nonatomic, strong, readwrite) id<FSTPersistence> localStorePersistence; +@property(nonatomic, strong, readwrite) FSTLocalStore *localStore; + +@property(nonatomic, strong, readonly) NSMutableArray<FSTMutationBatch *> *batches; +@property(nonatomic, strong, readwrite, nullable) FSTMaybeDocumentDictionary *lastChanges; +@property(nonatomic, assign, readwrite) FSTTargetID lastTargetID; + +@end + +@implementation FSTLocalStoreTests + +- (void)setUp { + [super setUp]; + + if ([self isTestBaseClass]) { + return; + } + + id<FSTPersistence> persistence = [self persistence]; + self.localStorePersistence = persistence; + id<FSTGarbageCollector> garbageCollector = [[FSTEagerGarbageCollector alloc] init]; + self.localStore = [[FSTLocalStore alloc] initWithPersistence:persistence + garbageCollector:garbageCollector + initialUser:[FSTUser unauthenticatedUser]]; + [self.localStore start]; + + _batches = [NSMutableArray array]; + _lastChanges = nil; + _lastTargetID = 0; +} + +- (void)tearDown { + [self.localStore shutdown]; + [self.localStorePersistence shutdown]; + + [super tearDown]; +} + +- (id<FSTPersistence>)persistence { + @throw FSTAbstractMethodException(); // NOLINT +} + +/** + * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for + * FSTLocalStoreTests since it is incomplete without the implementations supplied by its + * subclasses. + */ +- (BOOL)isTestBaseClass { + return [self class] == [FSTLocalStoreTests class]; +} + +/** Restarts the local store using the FSTNoOpGarbageCollector instead of the default. */ +- (void)restartWithNoopGarbageCollector { + [self.localStore shutdown]; + + id<FSTGarbageCollector> garbageCollector = [[FSTNoOpGarbageCollector alloc] init]; + self.localStore = [[FSTLocalStore alloc] initWithPersistence:self.localStorePersistence + garbageCollector:garbageCollector + initialUser:[FSTUser unauthenticatedUser]]; + [self.localStore start]; +} + +- (void)writeMutation:(FSTMutation *)mutation { + [self writeMutations:@[ mutation ]]; +} + +- (void)writeMutations:(NSArray<FSTMutation *> *)mutations { + FSTLocalWriteResult *result = [self.localStore locallyWriteMutations:mutations]; + XCTAssertNotNil(result); + [self.batches addObject:[[FSTMutationBatch alloc] initWithBatchID:result.batchID + localWriteTime:[FSTTimestamp timestamp] + mutations:mutations]]; + self.lastChanges = result.changes; +} + +- (void)applyRemoteEvent:(FSTRemoteEvent *)event { + self.lastChanges = [self.localStore applyRemoteEvent:event]; +} + +- (void)notifyLocalViewChanges:(FSTLocalViewChanges *)changes { + [self.localStore notifyLocalViewChanges:@[ changes ]]; +} + +- (void)acknowledgeMutationWithVersion:(FSTTestSnapshotVersion)documentVersion { + FSTMutationBatch *batch = [self.batches firstObject]; + [self.batches removeObjectAtIndex:0]; + XCTAssertEqual(batch.mutations.count, 1, @"Acknowledging more than one mutation not supported."); + FSTSnapshotVersion *version = FSTTestVersion(documentVersion); + FSTMutationResult *mutationResult = + [[FSTMutationResult alloc] initWithVersion:version transformResults:nil]; + FSTMutationBatchResult *result = [FSTMutationBatchResult resultWithBatch:batch + commitVersion:version + mutationResults:@[ mutationResult ] + streamToken:nil]; + self.lastChanges = [self.localStore acknowledgeBatchWithResult:result]; +} + +- (void)rejectMutation { + FSTMutationBatch *batch = [self.batches firstObject]; + [self.batches removeObjectAtIndex:0]; + self.lastChanges = [self.localStore rejectBatchID:batch.batchID]; +} + +- (void)allocateQuery:(FSTQuery *)query { + FSTQueryData *queryData = [self.localStore allocateQuery:query]; + self.lastTargetID = queryData.targetID; +} + +- (void)collectGarbage { + [self.localStore collectGarbage]; +} + +/** Asserts that the last target ID is the given number. */ +#define FSTAssertTargetID(targetID) \ + do { \ + XCTAssertEqual(self.lastTargetID, targetID); \ + } while (0) + +/** Asserts that a the lastChanges contain the docs in the given array. */ +#define FSTAssertChanged(documents) \ + XCTAssertNotNil(self.lastChanges); \ + do { \ + FSTMaybeDocumentDictionary *actual = self.lastChanges; \ + NSArray<FSTMaybeDocument *> *expected = (documents); \ + XCTAssertEqual(actual.count, expected.count); \ + NSEnumerator<FSTMaybeDocument *> *enumerator = expected.objectEnumerator; \ + [actual enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey * key, FSTMaybeDocument * value, \ + BOOL * stop) { \ + XCTAssertEqualObjects(value, [enumerator nextObject]); \ + }]; \ + self.lastChanges = nil; \ + } while (0) + +/** Asserts that the given keys were removed. */ +#define FSTAssertRemoved(keyPaths) \ + XCTAssertNotNil(self.lastChanges); \ + do { \ + FSTMaybeDocumentDictionary *actual = self.lastChanges; \ + XCTAssertEqual(actual.count, keyPaths.count); \ + NSEnumerator<NSString *> *keyPathEnumerator = keyPaths.objectEnumerator; \ + [actual enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey * actualKey, \ + FSTMaybeDocument * value, BOOL * stop) { \ + FSTDocumentKey *expectedKey = \ + [FSTDocumentKey keyWithPathString:[keyPathEnumerator nextObject]]; \ + XCTAssertEqualObjects(actualKey, expectedKey); \ + XCTAssertTrue([value isKindOfClass:[FSTDeletedDocument class]]); \ + }]; \ + self.lastChanges = nil; \ + } while (0) + +/** Asserts that the given local store contains the given document. */ +#define FSTAssertContains(document) \ + do { \ + FSTMaybeDocument *expected = (document); \ + FSTMaybeDocument *actual = [self.localStore readDocument:expected.key]; \ + XCTAssertEqualObjects(actual, expected); \ + } while (0) + +/** Asserts that the given local store does not contain the given document. */ +#define FSTAssertNotContains(keyPathString) \ + do { \ + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:keyPathString]; \ + FSTMaybeDocument *actual = [self.localStore readDocument:key]; \ + XCTAssertNil(actual); \ + } while (0) + +- (void)testMutationBatchKeys { + if ([self isTestBaseClass]) return; + + FSTMutation *set1 = FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}); + FSTMutation *set2 = FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"}); + FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:1 + localWriteTime:[FSTTimestamp timestamp] + mutations:@[ set1, set2 ]]; + FSTDocumentKeySet *keys = [batch keys]; + XCTAssertEqual(keys.count, 2); +} + +- (void)testHandlesSetMutation { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + + [self acknowledgeMutationWithVersion:0]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO)); +} + +- (void)testHandlesSetMutationThenDocument { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent( + FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO), @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES)); +} + +- (void)testHandlesAckThenRejectThenRemoteEvent { + if ([self isTestBaseClass]) return; + + // Start a query that requires acks to be held. + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + [self allocateQuery:query]; + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + + // The last seen version is zero, so this ack must be held. + [self acknowledgeMutationWithVersion:1]; + FSTAssertChanged(@[]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + + [self writeMutation:FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"})]; + FSTAssertChanged(@[ FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES)); + + [self rejectMutation]; + FSTAssertRemoved(@[ @"bar/baz" ]); + FSTAssertNotContains(@"bar/baz"); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent( + FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO), @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO)); + FSTAssertNotContains(@"bar/baz"); +} + +- (void)testHandlesDeletedDocumentThenSetMutationThenAck { + if ([self isTestBaseClass]) return; + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 2)); + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + + [self acknowledgeMutationWithVersion:3]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO)); +} + +- (void)testHandlesSetMutationThenDeletedDocument { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); +} + +- (void)testHandlesDocumentThenSetMutationThenAckThenDocument { + if ([self isTestBaseClass]) return; + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"it" : @"base"}, NO), + @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"it" : @"base"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"it" : @"base"}, NO)); + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES)); + + [self acknowledgeMutationWithVersion:3]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO)); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent( + FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO), @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO)); +} + +- (void)testHandlesPatchWithoutPriorDocument { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertNotContains(@"foo/bar"); + + [self acknowledgeMutationWithVersion:1]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertNotContains(@"foo/bar"); +} + +- (void)testHandlesPatchMutationThenDocumentThenAck { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertNotContains(@"foo/bar"); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), + @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, YES)); + + [self acknowledgeMutationWithVersion:2]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, NO)); +} + +- (void)testHandlesPatchMutationThenAckThenDocument { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertNotContains(@"foo/bar"); + + [self acknowledgeMutationWithVersion:1]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertNotContains(@"foo/bar"); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), + @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO)); +} + +- (void)testHandlesDeleteMutationThenAck { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestDeleteMutation(@"foo/bar")]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); + + [self acknowledgeMutationWithVersion:1]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); +} + +- (void)testHandlesDocumentThenDeleteMutationThenAck { + if ([self isTestBaseClass]) return; + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), + @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO)); + + [self writeMutation:FSTTestDeleteMutation(@"foo/bar")]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); + + [self acknowledgeMutationWithVersion:2]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); +} + +- (void)testHandlesDeleteMutationThenDocumentThenAck { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestDeleteMutation(@"foo/bar")]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), + @[ @1 ], @[])]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); + + [self acknowledgeMutationWithVersion:2]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); +} + +- (void)testHandlesDocumentThenDeletedDocumentThenDocument { + if ([self isTestBaseClass]) return; + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), + @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO)); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 2)); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent( + FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO), @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO)); +} + +- (void)testHandlesSetMutationThenPatchMutationThenDocumentThenAckThenAck { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"})]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, YES)); + + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), + @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES)); + + [self acknowledgeMutationWithVersion:2]; // delete mutation + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES)); + + [self acknowledgeMutationWithVersion:3]; // patch mutation + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO)); +} + +- (void)testHandlesSetMutationAndPatchMutationTogether { + if ([self isTestBaseClass]) return; + + [self writeMutations:@[ + FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"}), + FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil) + ]]; + + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); +} + +- (void)testHandlesSetMutationThenPatchMutationThenReject { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"})]; + [self acknowledgeMutationWithVersion:1]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO)); + + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + + [self rejectMutation]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO)); +} + +- (void)testHandlesSetMutationsAndPatchMutationOfJustOneTogether { + if ([self isTestBaseClass]) return; + + [self writeMutations:@[ + FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"}), + FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"}), + FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil) + ]]; + + FSTAssertChanged((@[ + FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES), + FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) + ])); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + FSTAssertContains(FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES)); +} + +- (void)testHandlesDeleteMutationThenPatchMutationThenAckThenAck { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestDeleteMutation(@"foo/bar")]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); + + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); + + [self acknowledgeMutationWithVersion:2]; // delete mutation + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); + + [self acknowledgeMutationWithVersion:3]; // patch mutation + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); +} + +- (void)testCollectsGarbageAfterChangeBatchWithNoTargetIDs { + if ([self isTestBaseClass]) return; + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])]; + FSTAssertRemoved(@[ @"foo/bar" ]); + + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO), + @[ @1 ], @[])]; + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); +} + +- (void)testCollectsGarbageAfterChangeBatch { + if ([self isTestBaseClass]) return; + + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + [self allocateQuery:query]; + FSTAssertTargetID(2); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO), + @[ @2 ], @[])]; + [self collectGarbage]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO)); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"baz"}, NO), + @[], @[ @2 ])]; + [self collectGarbage]; + + FSTAssertNotContains(@"foo/bar"); +} + +- (void)testCollectsGarbageAfterAcknowledgedMutation { + if ([self isTestBaseClass]) return; + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO), + @[ @1 ], @[])]; + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + [self writeMutation:FSTTestSetMutation(@"foo/bah", @{@"foo" : @"bah"})]; + [self writeMutation:FSTTestDeleteMutation(@"foo/baz")]; + [self collectGarbage]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES)); + FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); + + [self acknowledgeMutationWithVersion:3]; + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); + FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES)); + FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); + + [self acknowledgeMutationWithVersion:4]; + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); + FSTAssertNotContains(@"foo/bah"); + FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); + + [self acknowledgeMutationWithVersion:5]; + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); + FSTAssertNotContains(@"foo/bah"); + FSTAssertNotContains(@"foo/baz"); +} + +- (void)testCollectsGarbageAfterRejectedMutation { + if ([self isTestBaseClass]) return; + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO), + @[ @1 ], @[])]; + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + [self writeMutation:FSTTestSetMutation(@"foo/bah", @{@"foo" : @"bah"})]; + [self writeMutation:FSTTestDeleteMutation(@"foo/baz")]; + [self collectGarbage]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES)); + FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); + + [self rejectMutation]; // patch mutation + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); + FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES)); + FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); + + [self rejectMutation]; // set mutation + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); + FSTAssertNotContains(@"foo/bah"); + FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); + + [self rejectMutation]; // delete mutation + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); + FSTAssertNotContains(@"foo/bah"); + FSTAssertNotContains(@"foo/baz"); +} + +- (void)testPinsDocumentsInTheLocalView { + if ([self isTestBaseClass]) return; + + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + [self allocateQuery:query]; + FSTAssertTargetID(2); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO), + @[ @2 ], @[])]; + [self writeMutation:FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"})]; + [self collectGarbage]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO)); + FSTAssertContains(FSTTestDoc(@"foo/baz", 0, @{@"foo" : @"baz"}, YES)); + + [self notifyLocalViewChanges:FSTTestViewChanges(query, @[ @"foo/bar", @"foo/baz" ], @[])]; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO), + @[], @[ @2 ])]; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/baz", 2, @{@"foo" : @"baz"}, NO), + @[ @1 ], @[])]; + [self acknowledgeMutationWithVersion:2]; + [self collectGarbage]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO)); + FSTAssertContains(FSTTestDoc(@"foo/baz", 2, @{@"foo" : @"baz"}, NO)); + + [self notifyLocalViewChanges:FSTTestViewChanges(query, @[], @[ @"foo/bar", @"foo/baz" ])]; + [self collectGarbage]; + + FSTAssertNotContains(@"foo/bar"); + FSTAssertNotContains(@"foo/baz"); +} + +- (void)testThrowsAwayDocumentsWithUnknownTargetIDsImmediately { + if ([self isTestBaseClass]) return; + + FSTTargetID targetID = 321; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{}, NO), + @[ @(targetID) ], @[])]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{}, NO)); + + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); +} + +- (void)testCanExecuteDocumentQueries { + if ([self isTestBaseClass]) return; + + [self.localStore locallyWriteMutations:@[ + FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}), + FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}), + FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"}) + ]]; + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]]; + FSTDocumentDictionary *docs = [self.localStore executeQuery:query]; + XCTAssertEqualObjects([docs values], @[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); +} + +- (void)testCanExecuteCollectionQueries { + if ([self isTestBaseClass]) return; + + [self.localStore locallyWriteMutations:@[ + FSTTestSetMutation(@"fo/bar", @{@"fo" : @"bar"}), + FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}), + FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}), + FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"}), + FSTTestSetMutation(@"fooo/blah", @{@"fooo" : @"blah"}) + ]]; + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTDocumentDictionary *docs = [self.localStore executeQuery:query]; + XCTAssertEqualObjects([docs values], (@[ + FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES), + FSTTestDoc(@"foo/baz", 0, @{@"foo" : @"baz"}, YES) + ])); +} + +- (void)testCanExecuteMixedCollectionQueries { + if ([self isTestBaseClass]) return; + + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + [self allocateQuery:query]; + FSTAssertTargetID(2); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/baz", 10, @{@"a" : @"b"}, NO), + @[ @2 ], @[])]; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 20, @{@"a" : @"b"}, NO), + @[ @2 ], @[])]; + + [self.localStore locallyWriteMutations:@[ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) ]]; + + FSTDocumentDictionary *docs = [self.localStore executeQuery:query]; + XCTAssertEqualObjects([docs values], (@[ + FSTTestDoc(@"foo/bar", 20, @{@"a" : @"b"}, NO), + FSTTestDoc(@"foo/baz", 10, @{@"a" : @"b"}, NO), + FSTTestDoc(@"foo/bonk", 0, @{@"a" : @"b"}, YES) + ])); +} + +- (void)testPersistsResumeTokens { + if ([self isTestBaseClass]) return; + + // This test only works in the absence of the FSTEagerGarbageCollector. + [self restartWithNoopGarbageCollector]; + + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]]; + FSTQueryData *queryData = [self.localStore allocateQuery:query]; + FSTBoxedTargetID *targetID = @(queryData.targetID); + NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1000); + + FSTWatchChange *watchChange = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ targetID ] + resumeToken:resumeToken]; + NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *listens = + [NSMutableDictionary dictionary]; + listens[targetID] = queryData; + NSMutableDictionary<FSTBoxedTargetID *, NSNumber *> *pendingResponses = + [NSMutableDictionary dictionary]; + FSTWatchChangeAggregator *aggregator = + [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:FSTTestVersion(1000) + listenTargets:listens + pendingTargetResponses:pendingResponses]; + [aggregator addWatchChanges:@[ watchChange ]]; + FSTRemoteEvent *remoteEvent = [aggregator remoteEvent]; + [self applyRemoteEvent:remoteEvent]; + + // Stop listening so that the query should become inactive (but persistent) + [self.localStore releaseQuery:query]; + + // Should come back with the same resume token + FSTQueryData *queryData2 = [self.localStore allocateQuery:query]; + XCTAssertEqualObjects(queryData2.resumeToken, resumeToken); +} + +- (void)testRemoteDocumentKeysForTarget { + if ([self isTestBaseClass]) return; + [self restartWithNoopGarbageCollector]; + + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + [self allocateQuery:query]; + FSTAssertTargetID(2); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/baz", 10, @{@"a" : @"b"}, NO), + @[ @2 ], @[])]; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 20, @{@"a" : @"b"}, NO), + @[ @2 ], @[])]; + + [self.localStore locallyWriteMutations:@[ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) ]]; + + FSTDocumentKeySet *keys = [self.localStore remoteDocumentKeysForTarget:2]; + FSTAssertEqualSets(keys, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/baz") ])); + + [self restartWithNoopGarbageCollector]; + + keys = [self.localStore remoteDocumentKeysForTarget:2]; + FSTAssertEqualSets(keys, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/baz") ])); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.m b/Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.m new file mode 100644 index 0000000..e7486d0 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.m @@ -0,0 +1,44 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Local/FSTLocalStore.h" + +#import <XCTest/XCTest.h> + +#import "Local/FSTMemoryPersistence.h" + +#import "FSTLocalStoreTests.h" +#import "FSTPersistenceTestHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * This tests the FSTLocalStore with an FSTMemoryPersistence persistence implementation. The tests + * are in FSTLocalStoreTests and this class is merely responsible for creating a new FSTPersistence + * implementation on demand. + */ +@interface FSTMemoryLocalStoreTests : FSTLocalStoreTests +@end + +@implementation FSTMemoryLocalStoreTests + +- (id<FSTPersistence>)persistence { + return [FSTPersistenceTestHelpers memoryPersistence]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.m b/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.m new file mode 100644 index 0000000..4d76393 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.m @@ -0,0 +1,42 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Local/FSTMemoryMutationQueue.h" + +#import "Auth/FSTUser.h" +#import "Local/FSTMemoryPersistence.h" + +#import "FSTMutationQueueTests.h" +#import "FSTPersistenceTestHelpers.h" + +@interface FSTMemoryMutationQueueTests : FSTMutationQueueTests +@end + +/** + * The tests for FSTMemoryMutationQueue are performed on the FSTMutationQueue protocol in + * FSTMutationQueueTests. This class is merely responsible for setting up the @a mutationQueue. + */ +@implementation FSTMemoryMutationQueueTests + +- (void)setUp { + [super setUp]; + + self.persistence = [FSTPersistenceTestHelpers memoryPersistence]; + self.mutationQueue = + [self.persistence mutationQueueForUser:[[FSTUser alloc] initWithUID:@"user"]]; +} + +@end diff --git a/Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.m b/Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.m new file mode 100644 index 0000000..6574647 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.m @@ -0,0 +1,54 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Local/FSTMemoryQueryCache.h" + +#import "Local/FSTMemoryPersistence.h" + +#import "FSTPersistenceTestHelpers.h" +#import "FSTQueryCacheTests.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTMemoryQueryCacheTests : FSTQueryCacheTests +@end + +/** + * The tests for FSTMemoryQueryCache are performed on the FSTQueryCache protocol in + * FSTQueryCacheTests. This class is merely responsible for setting up and tearing down the + * @a queryCache. + */ +@implementation FSTMemoryQueryCacheTests + +- (void)setUp { + [super setUp]; + + self.persistence = [FSTPersistenceTestHelpers memoryPersistence]; + self.queryCache = [self.persistence queryCache]; + [self.queryCache start]; +} + +- (void)tearDown { + [self.queryCache shutdown]; + self.persistence = nil; + self.queryCache = nil; + + [super tearDown]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.m b/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.m new file mode 100644 index 0000000..7602134 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.m @@ -0,0 +1,49 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Local/FSTMemoryRemoteDocumentCache.h" + +#import "Local/FSTMemoryPersistence.h" + +#import "FSTPersistenceTestHelpers.h" +#import "FSTRemoteDocumentCacheTests.h" + +@interface FSTMemoryRemoteDocumentCacheTests : FSTRemoteDocumentCacheTests +@end + +/** + * The tests for FSTMemoryRemoteDocumentCache are performed on the FSTRemoteDocumentCache + * protocol in FSTRemoteDocumentCacheTests. This class is merely responsible for setting up and + * tearing down the @a remoteDocumentCache. + */ +@implementation FSTMemoryRemoteDocumentCacheTests + +- (void)setUp { + [super setUp]; + + self.persistence = [FSTPersistenceTestHelpers memoryPersistence]; + self.remoteDocumentCache = [self.persistence remoteDocumentCache]; +} + +- (void)tearDown { + [self.remoteDocumentCache shutdown]; + self.persistence = nil; + self.remoteDocumentCache = nil; + + [super tearDown]; +} + +@end diff --git a/Firestore/Example/Tests/Local/FSTMutationQueueTests.h b/Firestore/Example/Tests/Local/FSTMutationQueueTests.h new file mode 100644 index 0000000..0193c36 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTMutationQueueTests.h @@ -0,0 +1,38 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <XCTest/XCTest.h> + +@protocol FSTMutationQueue; +@protocol FSTPersistence; + +NS_ASSUME_NONNULL_BEGIN + +/** + * These are tests for any implementation of the FSTMutationQueue protocol. + * + * To test a specific implementation of FSTMutationQueue: + * + * + Subclass FSTMutationQueueTests + * + override -setUp, assigning to mutationQueue and persistence + * + override -tearDown, cleaning up mutationQueue and persistence + */ +@interface FSTMutationQueueTests : XCTestCase +@property(nonatomic, strong, nullable) id<FSTMutationQueue> mutationQueue; +@property(nonatomic, strong, nullable) id<FSTPersistence> persistence; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTMutationQueueTests.m b/Firestore/Example/Tests/Local/FSTMutationQueueTests.m new file mode 100644 index 0000000..42ba0b3 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTMutationQueueTests.m @@ -0,0 +1,511 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FSTMutationQueueTests.h" + +#import "Auth/FSTUser.h" +#import "Core/FSTQuery.h" +#import "Core/FSTTimestamp.h" +#import "Local/FSTEagerGarbageCollector.h" +#import "Local/FSTMutationQueue.h" +#import "Local/FSTPersistence.h" +#import "Local/FSTWriteGroup.h" +#import "Model/FSTMutation.h" +#import "Model/FSTMutationBatch.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTMutationQueueTests + +- (void)tearDown { + [self.mutationQueue shutdown]; + [self.persistence shutdown]; + [super tearDown]; +} + +/** + * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for + * FSTMutationQueueTests since it is incomplete without the implementations supplied by its + * subclasses. + */ +- (BOOL)isTestBaseClass { + return [self class] == [FSTMutationQueueTests class]; +} + +- (void)testCountBatches { + if ([self isTestBaseClass]) return; + + XCTAssertEqual(0, [self batchCount]); + XCTAssertTrue([self.mutationQueue isEmpty]); + + FSTMutationBatch *batch1 = [self addMutationBatch]; + XCTAssertEqual(1, [self batchCount]); + XCTAssertFalse([self.mutationQueue isEmpty]); + + FSTMutationBatch *batch2 = [self addMutationBatch]; + XCTAssertEqual(2, [self batchCount]); + + [self removeMutationBatches:@[ batch2 ]]; + XCTAssertEqual(1, [self batchCount]); + + [self removeMutationBatches:@[ batch1 ]]; + XCTAssertEqual(0, [self batchCount]); + XCTAssertTrue([self.mutationQueue isEmpty]); +} + +- (void)testAcknowledgeBatchID { + if ([self isTestBaseClass]) return; + + // Initial state of an empty queue + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown); + + // Adding mutation batches should not change the highest acked batchID. + FSTMutationBatch *batch1 = [self addMutationBatch]; + FSTMutationBatch *batch2 = [self addMutationBatch]; + FSTMutationBatch *batch3 = [self addMutationBatch]; + XCTAssertGreaterThan(batch1.batchID, kFSTBatchIDUnknown); + XCTAssertGreaterThan(batch2.batchID, batch1.batchID); + XCTAssertGreaterThan(batch3.batchID, batch2.batchID); + + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown); + + [self acknowledgeBatch:batch1]; + [self acknowledgeBatch:batch2]; + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); + + [self removeMutationBatches:@[ batch1 ]]; + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); + + [self removeMutationBatches:@[ batch2 ]]; + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); + + // Batch 3 never acknowledged. + [self removeMutationBatches:@[ batch3 ]]; + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); +} + +- (void)testAcknowledgeThenRemove { + if ([self isTestBaseClass]) return; + + FSTMutationBatch *batch1 = [self addMutationBatch]; + + FSTWriteGroup *group = [self.persistence startGroupWithAction:NSStringFromSelector(_cmd)]; + [self.mutationQueue acknowledgeBatch:batch1 streamToken:nil group:group]; + [self.mutationQueue removeMutationBatches:@[ batch1 ] group:group]; + [self.persistence commitGroup:group]; + + XCTAssertEqual([self batchCount], 0); + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch1.batchID); +} + +- (void)testHighestAcknowledgedBatchIDNeverExceedsNextBatchID { + if ([self isTestBaseClass]) return; + + FSTMutationBatch *batch1 = [self addMutationBatch]; + FSTMutationBatch *batch2 = [self addMutationBatch]; + [self acknowledgeBatch:batch1]; + [self acknowledgeBatch:batch2]; + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); + + [self removeMutationBatches:@[ batch1, batch2 ]]; + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); + + // Restart the queue so that nextBatchID will be reset. + [self.mutationQueue shutdown]; + self.mutationQueue = + [self.persistence mutationQueueForUser:[[FSTUser alloc] initWithUID:@"user"]]; + + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Start MutationQueue"]; + [self.mutationQueue startWithGroup:group]; + [self.persistence commitGroup:group]; + + // Verify that on restart with an empty queue, nextBatchID falls to a lower value. + XCTAssertLessThan(self.mutationQueue.nextBatchID, batch2.batchID); + + // As a result highestAcknowledgedBatchID must also reset lower. + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown); + + // The mutation queue will reset the next batchID after all mutations are removed so adding + // another mutation will cause a collision. + FSTMutationBatch *newBatch = [self addMutationBatch]; + XCTAssertEqual(newBatch.batchID, batch1.batchID); + + // Restart the queue with one unacknowledged batch in it. + group = [self.persistence startGroupWithAction:@"Start MutationQueue"]; + [self.mutationQueue startWithGroup:group]; + [self.persistence commitGroup:group]; + + XCTAssertEqual([self.mutationQueue nextBatchID], newBatch.batchID + 1); + + // highestAcknowledgedBatchID must still be kFSTBatchIDUnknown. + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown); +} + +- (void)testLookupMutationBatch { + if ([self isTestBaseClass]) return; + + // Searching on an empty queue should not find a non-existent batch + FSTMutationBatch *notFound = [self.mutationQueue lookupMutationBatch:42]; + XCTAssertNil(notFound); + + NSMutableArray<FSTMutationBatch *> *batches = [self createBatches:10]; + NSArray<FSTMutationBatch *> *removed = [self makeHoles:@[ @2, @6, @7 ] inBatches:batches]; + + // After removing, a batch should not be found + for (NSUInteger i = 0; i < removed.count; i++) { + notFound = [self.mutationQueue lookupMutationBatch:removed[i].batchID]; + XCTAssertNil(notFound); + } + + // Remaining entries should still be found + for (FSTMutationBatch *batch in batches) { + FSTMutationBatch *found = [self.mutationQueue lookupMutationBatch:batch.batchID]; + XCTAssertEqual(found.batchID, batch.batchID); + } + + // Even on a nonempty queue searching should not find a non-existent batch + notFound = [self.mutationQueue lookupMutationBatch:42]; + XCTAssertNil(notFound); +} + +- (void)testNextMutationBatchAfterBatchID { + if ([self isTestBaseClass]) return; + + NSMutableArray<FSTMutationBatch *> *batches = [self createBatches:10]; + + // This is an array of successors assuming the removals below will happen: + NSArray<FSTMutationBatch *> *afters = @[ batches[3], batches[8], batches[8] ]; + NSArray<FSTMutationBatch *> *removed = [self makeHoles:@[ @2, @6, @7 ] inBatches:batches]; + + for (NSUInteger i = 0; i < batches.count - 1; i++) { + FSTMutationBatch *current = batches[i]; + FSTMutationBatch *next = batches[i + 1]; + FSTMutationBatch *found = [self.mutationQueue nextMutationBatchAfterBatchID:current.batchID]; + XCTAssertEqual(found.batchID, next.batchID); + } + + for (NSUInteger i = 0; i < removed.count; i++) { + FSTMutationBatch *current = removed[i]; + FSTMutationBatch *next = afters[i]; + FSTMutationBatch *found = [self.mutationQueue nextMutationBatchAfterBatchID:current.batchID]; + XCTAssertEqual(found.batchID, next.batchID); + } + + FSTMutationBatch *first = batches[0]; + FSTMutationBatch *found = [self.mutationQueue nextMutationBatchAfterBatchID:first.batchID - 42]; + XCTAssertEqual(found.batchID, first.batchID); + + FSTMutationBatch *last = batches[batches.count - 1]; + FSTMutationBatch *notFound = [self.mutationQueue nextMutationBatchAfterBatchID:last.batchID]; + XCTAssertNil(notFound); +} + +- (void)testAllMutationBatchesThroughBatchID { + if ([self isTestBaseClass]) return; + + NSMutableArray<FSTMutationBatch *> *batches = [self createBatches:10]; + [self makeHoles:@[ @2, @6, @7 ] inBatches:batches]; + + NSArray<FSTMutationBatch *> *found, *expected; + + found = [self.mutationQueue allMutationBatchesThroughBatchID:batches[0].batchID - 1]; + XCTAssertEqualObjects(found, (@[])); + + for (NSUInteger i = 0; i < batches.count; i++) { + found = [self.mutationQueue allMutationBatchesThroughBatchID:batches[i].batchID]; + expected = [batches subarrayWithRange:NSMakeRange(0, i + 1)]; + XCTAssertEqualObjects(found, expected, @"for index %lu", (unsigned long)i); + } +} + +- (void)testAllMutationBatchesAffectingDocumentKey { + if ([self isTestBaseClass]) return; + + NSArray<FSTMutation *> *mutations = @[ + FSTTestSetMutation(@"fob/bar", + @{ @"a" : @1 }), + FSTTestSetMutation(@"foo/bar", + @{ @"a" : @1 }), + FSTTestPatchMutation(@"foo/bar", + @{ @"b" : @1 }, nil), + FSTTestSetMutation(@"foo/bar/suffix/key", + @{ @"a" : @1 }), + FSTTestSetMutation(@"foo/baz", + @{ @"a" : @1 }), + FSTTestSetMutation(@"food/bar", + @{ @"a" : @1 }) + ]; + + // Store all the mutations. + NSMutableArray<FSTMutationBatch *> *batches = [NSMutableArray array]; + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"New mutation batch"]; + for (FSTMutation *mutation in mutations) { + FSTMutationBatch *batch = + [self.mutationQueue addMutationBatchWithWriteTime:[FSTTimestamp timestamp] + mutations:@[ mutation ] + group:group]; + [batches addObject:batch]; + } + [self.persistence commitGroup:group]; + + NSArray<FSTMutationBatch *> *expected = @[ batches[1], batches[2] ]; + NSArray<FSTMutationBatch *> *matches = + [self.mutationQueue allMutationBatchesAffectingDocumentKey:FSTTestDocKey(@"foo/bar")]; + + XCTAssertEqualObjects(matches, expected); +} + +- (void)testAllMutationBatchesAffectingQuery { + if ([self isTestBaseClass]) return; + + NSArray<FSTMutation *> *mutations = @[ + FSTTestSetMutation(@"fob/bar", + @{ @"a" : @1 }), + FSTTestSetMutation(@"foo/bar", + @{ @"a" : @1 }), + FSTTestPatchMutation(@"foo/bar", + @{ @"b" : @1 }, nil), + FSTTestSetMutation(@"foo/bar/suffix/key", + @{ @"a" : @1 }), + FSTTestSetMutation(@"foo/baz", + @{ @"a" : @1 }), + FSTTestSetMutation(@"food/bar", + @{ @"a" : @1 }) + ]; + + // Store all the mutations. + NSMutableArray<FSTMutationBatch *> *batches = [NSMutableArray array]; + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"New mutation batch"]; + for (FSTMutation *mutation in mutations) { + FSTMutationBatch *batch = + [self.mutationQueue addMutationBatchWithWriteTime:[FSTTimestamp timestamp] + mutations:@[ mutation ] + group:group]; + [batches addObject:batch]; + } + [self.persistence commitGroup:group]; + + NSArray<FSTMutationBatch *> *expected = @[ batches[1], batches[2], batches[4] ]; + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"foo")]; + NSArray<FSTMutationBatch *> *matches = + [self.mutationQueue allMutationBatchesAffectingQuery:query]; + + XCTAssertEqualObjects(matches, expected); +} + +- (void)testRemoveMutationBatches { + if ([self isTestBaseClass]) return; + + NSMutableArray<FSTMutationBatch *> *batches = [self createBatches:10]; + FSTMutationBatch *last = batches[batches.count - 1]; + + [self removeMutationBatches:@[ batches[0] ]]; + [batches removeObjectAtIndex:0]; + XCTAssertEqual([self batchCount], 9); + + NSArray<FSTMutationBatch *> *found; + + found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID]; + XCTAssertEqualObjects(found, batches); + XCTAssertEqual(found.count, 9); + + [self removeMutationBatches:@[ batches[0], batches[1], batches[2] ]]; + [batches removeObjectsInRange:NSMakeRange(0, 3)]; + XCTAssertEqual([self batchCount], 6); + + found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID]; + XCTAssertEqualObjects(found, batches); + XCTAssertEqual(found.count, 6); + + [self removeMutationBatches:@[ batches[batches.count - 1] ]]; + [batches removeObjectAtIndex:batches.count - 1]; + XCTAssertEqual([self batchCount], 5); + + found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID]; + XCTAssertEqualObjects(found, batches); + XCTAssertEqual(found.count, 5); + + [self removeMutationBatches:@[ batches[3] ]]; + [batches removeObjectAtIndex:3]; + XCTAssertEqual([self batchCount], 4); + + [self removeMutationBatches:@[ batches[1] ]]; + [batches removeObjectAtIndex:1]; + XCTAssertEqual([self batchCount], 3); + + found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID]; + XCTAssertEqualObjects(found, batches); + XCTAssertEqual(found.count, 3); + XCTAssertFalse([self.mutationQueue isEmpty]); + + [self removeMutationBatches:batches]; + found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID]; + XCTAssertEqualObjects(found, @[]); + XCTAssertEqual(found.count, 0); + XCTAssertTrue([self.mutationQueue isEmpty]); +} + +- (void)testRemoveMutationBatchesEmitsGarbageEvents { + if ([self isTestBaseClass]) return; + + FSTEagerGarbageCollector *garbageCollector = [[FSTEagerGarbageCollector alloc] init]; + [garbageCollector addGarbageSource:self.mutationQueue]; + + NSMutableArray<FSTMutationBatch *> *batches = [NSMutableArray array]; + [batches addObjectsFromArray:@[ + [self addMutationBatchWithKey:@"foo/bar"], + [self addMutationBatchWithKey:@"foo/ba"], + [self addMutationBatchWithKey:@"foo/bar2"], + [self addMutationBatchWithKey:@"foo/bar"], + [self addMutationBatchWithKey:@"foo/bar/suffix/baz"], + [self addMutationBatchWithKey:@"bar/baz"], + ]]; + + [self removeMutationBatches:@[ batches[0] ]]; + NSSet<FSTDocumentKey *> *garbage = [garbageCollector collectGarbage]; + FSTAssertEqualSets(garbage, @[]); + + [self removeMutationBatches:@[ batches[1] ]]; + garbage = [garbageCollector collectGarbage]; + FSTAssertEqualSets(garbage, @[ FSTTestDocKey(@"foo/ba") ]); + + [self removeMutationBatches:@[ batches[5] ]]; + garbage = [garbageCollector collectGarbage]; + FSTAssertEqualSets(garbage, @[ FSTTestDocKey(@"bar/baz") ]); + + [self removeMutationBatches:@[ batches[2], batches[3] ]]; + garbage = [garbageCollector collectGarbage]; + FSTAssertEqualSets(garbage, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/bar2") ])); + + [batches addObject:[self addMutationBatchWithKey:@"foo/bar/suffix/baz"]]; + garbage = [garbageCollector collectGarbage]; + FSTAssertEqualSets(garbage, @[]); + + [self removeMutationBatches:@[ batches[4], batches[6] ]]; + garbage = [garbageCollector collectGarbage]; + FSTAssertEqualSets(garbage, @[ FSTTestDocKey(@"foo/bar/suffix/baz") ]); +} + +- (void)testStreamToken { + if ([self isTestBaseClass]) return; + + NSData *streamToken1 = [@"token1" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *streamToken2 = [@"token2" dataUsingEncoding:NSUTF8StringEncoding]; + + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"initial stream token"]; + [self.mutationQueue setLastStreamToken:streamToken1 group:group]; + [self.persistence commitGroup:group]; + + FSTMutationBatch *batch1 = [self addMutationBatch]; + [self addMutationBatch]; + + XCTAssertEqualObjects([self.mutationQueue lastStreamToken], streamToken1); + + group = [self.persistence startGroupWithAction:@"acknowledgeBatchID"]; + [self.mutationQueue acknowledgeBatch:batch1 streamToken:streamToken2 group:group]; + [self.persistence commitGroup:group]; + + XCTAssertEqual(self.mutationQueue.highestAcknowledgedBatchID, batch1.batchID); + XCTAssertEqualObjects([self.mutationQueue lastStreamToken], streamToken2); +} + +/** Creates a new FSTMutationBatch with the next batch ID and a set of dummy mutations. */ +- (FSTMutationBatch *)addMutationBatch { + return [self addMutationBatchWithKey:@"foo/bar"]; +} + +/** + * Creates a new FSTMutationBatch with the given key, the next batch ID and a set of dummy + * mutations. + */ +- (FSTMutationBatch *)addMutationBatchWithKey:(NSString *)key { + FSTSetMutation *mutation = FSTTestSetMutation(key, @{ @"a" : @1 }); + + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"New mutation batch"]; + FSTMutationBatch *batch = + [self.mutationQueue addMutationBatchWithWriteTime:[FSTTimestamp timestamp] + mutations:@[ mutation ] + group:group]; + [self.persistence commitGroup:group]; + return batch; +} + +/** + * Creates an array of batches containing @a number dummy FSTMutationBatches. Each has a different + * batchID. + */ +- (NSMutableArray<FSTMutationBatch *> *)createBatches:(int)number { + NSMutableArray<FSTMutationBatch *> *batches = [NSMutableArray array]; + + for (int i = 0; i < number; i++) { + FSTMutationBatch *batch = [self addMutationBatch]; + [batches addObject:batch]; + } + + return batches; +} + +/** + * Calls -acknowledgeBatch:streamToken:group: on the mutation queue in a new group and commits the + * the group. + */ +- (void)acknowledgeBatch:(FSTMutationBatch *)batch { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Ack batchID"]; + [self.mutationQueue acknowledgeBatch:batch streamToken:nil group:group]; + [self.persistence commitGroup:group]; +} + +/** + * Calls -removeMutationBatches:group: on the mutation queue in a new group and commits the group. + */ +- (void)removeMutationBatches:(NSArray<FSTMutationBatch *> *)batches { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Remove mutation batch"]; + [self.mutationQueue removeMutationBatches:batches group:group]; + [self.persistence commitGroup:group]; +} + +/** Returns the number of mutation batches in the mutation queue. */ +- (NSUInteger)batchCount { + return [self.mutationQueue allMutationBatches].count; +} + +/** + * Removes entries from from the given @a batches and returns them. + * + * @param holes An array of indexes in the batches array; in increasing order. Indexes are relative + * to the original state of the batches array, not any intermediate state that might occur. + * @param batches The array to mutate, removing entries from it. + * @return A new array containing all the entries that were removed from @a batches. + */ +- (NSArray<FSTMutationBatch *> *)makeHoles:(NSArray<NSNumber *> *)holes + inBatches:(NSMutableArray<FSTMutationBatch *> *)batches { + NSMutableArray<FSTMutationBatch *> *removed = [NSMutableArray array]; + for (NSUInteger i = 0; i < holes.count; i++) { + NSUInteger index = holes[i].unsignedIntegerValue - i; + FSTMutationBatch *batch = batches[index]; + [self removeMutationBatches:@[ batch ]]; + + [batches removeObjectAtIndex:index]; + [removed addObject:batch]; + } + return removed; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h new file mode 100644 index 0000000..936bacf --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h @@ -0,0 +1,40 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +@class FSTLevelDB; +@class FSTMemoryPersistence; + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTPersistenceTestHelpers : NSObject + +/** + * Creates and starts a new FSTLevelDB instance for testing, destroying any previous contents + * if they existed. + * + * Note that in order to avoid generating a bunch of garbage on the filesystem, the path of the + * database is reused. This prevents concurrent running of tests using this database. We may + * need to revisit this if we want to parallelize the tests. + */ ++ (FSTLevelDB *)levelDBPersistence; + +/** Creates and starts a new FSTMemoryPersistence instance for testing. */ ++ (FSTMemoryPersistence *)memoryPersistence; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.m b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.m new file mode 100644 index 0000000..f3d7914 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.m @@ -0,0 +1,72 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FSTPersistenceTestHelpers.h" + +#import "Local/FSTLevelDB.h" +#import "Local/FSTLocalSerializer.h" +#import "Local/FSTMemoryPersistence.h" +#import "Model/FSTDatabaseID.h" +#import "Remote/FSTSerializerBeta.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTPersistenceTestHelpers + ++ (FSTLevelDB *)levelDBPersistence { + NSError *error; + NSFileManager *files = [NSFileManager defaultManager]; + + NSString *dir = + [NSTemporaryDirectory() stringByAppendingPathComponent:@"FSTPersistenceTestHelpers"]; + if ([files fileExistsAtPath:dir]) { + // Delete the directory first to ensure isolation between runs. + BOOL success = [files removeItemAtPath:dir error:&error]; + if (!success) { + [NSException raise:NSInternalInconsistencyException + format:@"Failed to clean up leveldb path %@: %@", dir, error]; + } + } + + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"]; + FSTSerializerBeta *remoteSerializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseID]; + FSTLocalSerializer *serializer = + [[FSTLocalSerializer alloc] initWithRemoteSerializer:remoteSerializer]; + FSTLevelDB *db = [[FSTLevelDB alloc] initWithDirectory:dir serializer:serializer]; + BOOL success = [db start:&error]; + if (!success) { + [NSException raise:NSInternalInconsistencyException + format:@"Failed to create leveldb path %@: %@", dir, error]; + } + + return db; +} + ++ (FSTMemoryPersistence *)memoryPersistence { + NSError *error; + FSTMemoryPersistence *persistence = [FSTMemoryPersistence persistence]; + BOOL success = [persistence start:&error]; + if (!success) { + [NSException raise:NSInternalInconsistencyException + format:@"Failed to start memory persistence: %@", error]; + } + + return persistence; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTQueryCacheTests.h b/Firestore/Example/Tests/Local/FSTQueryCacheTests.h new file mode 100644 index 0000000..a615372 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTQueryCacheTests.h @@ -0,0 +1,47 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Local/FSTQueryCache.h" + +#import <XCTest/XCTest.h> + +@protocol FSTPersistence; + +NS_ASSUME_NONNULL_BEGIN + +/** + * These are tests for any implementation of the FSTQueryCache protocol. + * + * To test a specific implementation of FSTQueryCache: + * + * + Subclass FSTQueryCacheTests + * + override -setUp, assigning to queryCache and persistence + * + override -tearDown, cleaning up queryCache and persistence + */ +@interface FSTQueryCacheTests : XCTestCase + +/** The implementation of the query cache to test. */ +@property(nonatomic, strong, nullable) id<FSTQueryCache> queryCache; + +/** + * The persistence implementation to use while testing the queryCache (e.g. for committing write + * groups). + */ +@property(nonatomic, strong, nullable) id<FSTPersistence> persistence; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTQueryCacheTests.m b/Firestore/Example/Tests/Local/FSTQueryCacheTests.m new file mode 100644 index 0000000..ed409b4 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTQueryCacheTests.m @@ -0,0 +1,375 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FSTQueryCacheTests.h" + +#import "Core/FSTQuery.h" +#import "Core/FSTSnapshotVersion.h" +#import "Local/FSTEagerGarbageCollector.h" +#import "Local/FSTPersistence.h" +#import "Local/FSTQueryData.h" +#import "Local/FSTWriteGroup.h" +#import "Model/FSTDocumentKey.h" + +#import "FSTHelpers.h" +#import "FSTImmutableSortedSet+Testing.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTQueryCacheTests { + FSTQuery *_queryRooms; +} + +- (void)setUp { + [super setUp]; + + _queryRooms = FSTTestQuery(@"rooms"); +} + +/** + * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for + * FSTSpecTests since it is incomplete without the implementations supplied by its subclasses. + */ +- (BOOL)isTestBaseClass { + return [self class] == [FSTQueryCacheTests class]; +} + +- (void)testReadQueryNotInCache { + if ([self isTestBaseClass]) return; + + XCTAssertNil([self.queryCache queryDataForQuery:_queryRooms]); +} + +- (void)testSetAndReadAQuery { + if ([self isTestBaseClass]) return; + + FSTQueryData *queryData = [self queryDataWithQuery:_queryRooms targetID:1 version:1]; + [self addQueryData:queryData]; + + FSTQueryData *result = [self.queryCache queryDataForQuery:_queryRooms]; + XCTAssertEqualObjects(result.query, queryData.query); + XCTAssertEqual(result.targetID, queryData.targetID); + XCTAssertEqualObjects(result.resumeToken, queryData.resumeToken); +} + +- (void)testCanonicalIDCollision { + if ([self isTestBaseClass]) return; + + // Type information is currently lost in our canonicalID implementations so this currently an + // easy way to force colliding canonicalIDs + FSTQuery *q1 = [[FSTQuery queryWithPath:FSTTestPath(@"a")] + queryByAddingFilter:FSTTestFilter(@"foo", @"==", @(1))]; + FSTQuery *q2 = [[FSTQuery queryWithPath:FSTTestPath(@"a")] + queryByAddingFilter:FSTTestFilter(@"foo", @"==", @"1")]; + XCTAssertEqualObjects(q1.canonicalID, q2.canonicalID); + + FSTQueryData *data1 = [self queryDataWithQuery:q1 targetID:1 version:1]; + [self addQueryData:data1]; + + // Using the other query should not return the query cache entry despite equal canonicalIDs. + XCTAssertNil([self.queryCache queryDataForQuery:q2]); + XCTAssertEqualObjects([self.queryCache queryDataForQuery:q1], data1); + + FSTQueryData *data2 = [self queryDataWithQuery:q2 targetID:2 version:1]; + [self addQueryData:data2]; + + XCTAssertEqualObjects([self.queryCache queryDataForQuery:q1], data1); + XCTAssertEqualObjects([self.queryCache queryDataForQuery:q2], data2); + + [self removeQueryData:data1]; + XCTAssertNil([self.queryCache queryDataForQuery:q1]); + XCTAssertEqualObjects([self.queryCache queryDataForQuery:q2], data2); + + [self removeQueryData:data2]; + XCTAssertNil([self.queryCache queryDataForQuery:q1]); + XCTAssertNil([self.queryCache queryDataForQuery:q2]); +} + +- (void)testSetQueryToNewValue { + if ([self isTestBaseClass]) return; + + FSTQueryData *queryData1 = [self queryDataWithQuery:_queryRooms targetID:1 version:1]; + [self addQueryData:queryData1]; + + FSTQueryData *queryData2 = [self queryDataWithQuery:_queryRooms targetID:1 version:2]; + [self addQueryData:queryData2]; + + FSTQueryData *result = [self.queryCache queryDataForQuery:_queryRooms]; + XCTAssertNotEqualObjects(queryData2.resumeToken, queryData1.resumeToken); + XCTAssertNotEqualObjects(queryData2.snapshotVersion, queryData1.snapshotVersion); + XCTAssertEqualObjects(result.resumeToken, queryData2.resumeToken); + XCTAssertEqualObjects(result.snapshotVersion, queryData2.snapshotVersion); +} + +- (void)testRemoveQuery { + if ([self isTestBaseClass]) return; + + FSTQueryData *queryData1 = [self queryDataWithQuery:_queryRooms targetID:1 version:1]; + [self addQueryData:queryData1]; + + [self removeQueryData:queryData1]; + + FSTQueryData *result = [self.queryCache queryDataForQuery:_queryRooms]; + XCTAssertNil(result); +} + +- (void)testRemoveNonExistentQuery { + if ([self isTestBaseClass]) return; + + FSTQueryData *queryData = [self queryDataWithQuery:_queryRooms targetID:1 version:1]; + + // no-op, but make sure it doesn't throw. + XCTAssertNoThrow([self removeQueryData:queryData]); +} + +- (void)testRemoveQueryRemovesMatchingKeysToo { + if ([self isTestBaseClass]) return; + + FSTQueryData *rooms = [self queryDataWithQuery:_queryRooms targetID:1 version:1]; + [self addQueryData:rooms]; + + FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"rooms/foo"]; + FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"rooms/bar"]; + [self addMatchingKey:key1 forTargetID:rooms.targetID]; + [self addMatchingKey:key2 forTargetID:rooms.targetID]; + + XCTAssertTrue([self.queryCache containsKey:key1]); + XCTAssertTrue([self.queryCache containsKey:key2]); + + [self removeQueryData:rooms]; + XCTAssertFalse([self.queryCache containsKey:key1]); + XCTAssertFalse([self.queryCache containsKey:key2]); +} + +- (void)testAddOrRemoveMatchingKeys { + if ([self isTestBaseClass]) return; + + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"foo/bar"]; + + XCTAssertFalse([self.queryCache containsKey:key]); + + [self addMatchingKey:key forTargetID:1]; + XCTAssertTrue([self.queryCache containsKey:key]); + + [self addMatchingKey:key forTargetID:2]; + XCTAssertTrue([self.queryCache containsKey:key]); + + [self removeMatchingKey:key forTargetID:1]; + XCTAssertTrue([self.queryCache containsKey:key]); + + [self removeMatchingKey:key forTargetID:2]; + XCTAssertFalse([self.queryCache containsKey:key]); +} + +- (void)testRemoveMatchingKeysForTargetID { + if ([self isTestBaseClass]) return; + + FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"]; + FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"]; + FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"foo/blah"]; + + [self addMatchingKey:key1 forTargetID:1]; + [self addMatchingKey:key2 forTargetID:1]; + [self addMatchingKey:key3 forTargetID:2]; + XCTAssertTrue([self.queryCache containsKey:key1]); + XCTAssertTrue([self.queryCache containsKey:key2]); + XCTAssertTrue([self.queryCache containsKey:key3]); + + [self removeMatchingKeysForTargetID:1]; + XCTAssertFalse([self.queryCache containsKey:key1]); + XCTAssertFalse([self.queryCache containsKey:key2]); + XCTAssertTrue([self.queryCache containsKey:key3]); + + [self removeMatchingKeysForTargetID:2]; + XCTAssertFalse([self.queryCache containsKey:key1]); + XCTAssertFalse([self.queryCache containsKey:key2]); + XCTAssertFalse([self.queryCache containsKey:key3]); +} + +- (void)testRemoveEmitsGarbageEvents { + if ([self isTestBaseClass]) return; + + FSTEagerGarbageCollector *garbageCollector = [[FSTEagerGarbageCollector alloc] init]; + [garbageCollector addGarbageSource:self.queryCache]; + FSTAssertEqualSets([garbageCollector collectGarbage], @[]); + + FSTQueryData *rooms = [self queryDataWithQuery:FSTTestQuery(@"rooms") targetID:1 version:1]; + FSTDocumentKey *room1 = [FSTDocumentKey keyWithPathString:@"rooms/bar"]; + FSTDocumentKey *room2 = [FSTDocumentKey keyWithPathString:@"rooms/foo"]; + [self addQueryData:rooms]; + [self addMatchingKey:room1 forTargetID:rooms.targetID]; + [self addMatchingKey:room2 forTargetID:rooms.targetID]; + + FSTQueryData *halls = [self queryDataWithQuery:FSTTestQuery(@"halls") targetID:2 version:1]; + FSTDocumentKey *hall1 = [FSTDocumentKey keyWithPathString:@"halls/bar"]; + FSTDocumentKey *hall2 = [FSTDocumentKey keyWithPathString:@"halls/foo"]; + [self addQueryData:halls]; + [self addMatchingKey:hall1 forTargetID:halls.targetID]; + [self addMatchingKey:hall2 forTargetID:halls.targetID]; + + FSTAssertEqualSets([garbageCollector collectGarbage], @[]); + + [self removeMatchingKey:room1 forTargetID:rooms.targetID]; + FSTAssertEqualSets([garbageCollector collectGarbage], @[ room1 ]); + + [self removeQueryData:rooms]; + FSTAssertEqualSets([garbageCollector collectGarbage], @[ room2 ]); + + [self removeMatchingKeysForTargetID:halls.targetID]; + FSTAssertEqualSets([garbageCollector collectGarbage], (@[ hall1, hall2 ])); +} + +- (void)testMatchingKeysForTargetID { + if ([self isTestBaseClass]) return; + + FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"]; + FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"]; + FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"foo/blah"]; + + [self addMatchingKey:key1 forTargetID:1]; + [self addMatchingKey:key2 forTargetID:1]; + [self addMatchingKey:key3 forTargetID:2]; + + FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:1], (@[ key1, key2 ])); + FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:2], @[ key3 ]); + + [self addMatchingKey:key1 forTargetID:2]; + FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:1], (@[ key1, key2 ])); + FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:2], (@[ key1, key3 ])); +} + +- (void)testHighestTargetID { + if ([self isTestBaseClass]) return; + + XCTAssertEqual([self.queryCache highestTargetID], 0); + + FSTQueryData *query1 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"rooms") + targetID:1 + purpose:FSTQueryPurposeListen]; + FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"rooms/bar"]; + FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"rooms/foo"]; + [self addQueryData:query1]; + [self addMatchingKey:key1 forTargetID:1]; + [self addMatchingKey:key2 forTargetID:1]; + + FSTQueryData *query2 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"halls") + targetID:2 + purpose:FSTQueryPurposeListen]; + FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"halls/foo"]; + [self addQueryData:query2]; + [self addMatchingKey:key3 forTargetID:2]; + XCTAssertEqual([self.queryCache highestTargetID], 2); + + // TargetIDs never come down. + [self removeQueryData:query2]; + XCTAssertEqual([self.queryCache highestTargetID], 2); + + // A query with an empty result set still counts. + FSTQueryData *query3 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"garages") + targetID:42 + purpose:FSTQueryPurposeListen]; + [self addQueryData:query3]; + XCTAssertEqual([self.queryCache highestTargetID], 42); + + [self removeQueryData:query1]; + XCTAssertEqual([self.queryCache highestTargetID], 42); + + [self removeQueryData:query3]; + XCTAssertEqual([self.queryCache highestTargetID], 42); + + // Verify that the highestTargetID even survives restarts. + [self.queryCache shutdown]; + self.queryCache = [self.persistence queryCache]; + [self.queryCache start]; + XCTAssertEqual([self.queryCache highestTargetID], 42); +} + +- (void)testLastRemoteSnapshotVersion { + if ([self isTestBaseClass]) return; + + XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion], + [FSTSnapshotVersion noVersion]); + + // Can set the snapshot version. + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"setLastRemoteSnapshotVersion"]; + [self.queryCache setLastRemoteSnapshotVersion:FSTTestVersion(42) group:group]; + [self.persistence commitGroup:group]; + XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion], FSTTestVersion(42)); + + // Snapshot version persists restarts. + self.queryCache = [self.persistence queryCache]; + [self.queryCache start]; + XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion], FSTTestVersion(42)); +} + +#pragma mark - Helpers + +/** + * Creates a new FSTQueryData object from the given parameters, synthesizing a resume token from + * the snapshot version. + */ +- (FSTQueryData *)queryDataWithQuery:(FSTQuery *)query + targetID:(FSTTargetID)targetID + version:(FSTTestSnapshotVersion)version { + NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(version); + return [[FSTQueryData alloc] initWithQuery:query + targetID:targetID + purpose:FSTQueryPurposeListen + snapshotVersion:FSTTestVersion(version) + resumeToken:resumeToken]; +} + +/** Adds the given query data to the queryCache under test, committing immediately. */ +- (void)addQueryData:(FSTQueryData *)queryData { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"addQueryData"]; + [self.queryCache addQueryData:queryData group:group]; + [self.persistence commitGroup:group]; +} + +/** Removes the given query data from the queryCache under test, committing immediately. */ +- (void)removeQueryData:(FSTQueryData *)queryData { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeQueryData"]; + [self.queryCache removeQueryData:queryData group:group]; + [self.persistence commitGroup:group]; +} + +- (void)addMatchingKey:(FSTDocumentKey *)key forTargetID:(FSTTargetID)targetID { + FSTDocumentKeySet *keys = [FSTDocumentKeySet keySet]; + keys = [keys setByAddingObject:key]; + + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"addMatchingKeys"]; + [self.queryCache addMatchingKeys:keys forTargetID:targetID group:group]; + [self.persistence commitGroup:group]; +} + +- (void)removeMatchingKey:(FSTDocumentKey *)key forTargetID:(FSTTargetID)targetID { + FSTDocumentKeySet *keys = [FSTDocumentKeySet keySet]; + keys = [keys setByAddingObject:key]; + + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeMatchingKeys"]; + [self.queryCache removeMatchingKeys:keys forTargetID:targetID group:group]; + [self.persistence commitGroup:group]; +} + +- (void)removeMatchingKeysForTargetID:(FSTTargetID)targetID { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeMatchingKeysForTargetID"]; + [self.queryCache removeMatchingKeysForTargetID:targetID group:group]; + [self.persistence commitGroup:group]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTReferenceSetTests.m b/Firestore/Example/Tests/Local/FSTReferenceSetTests.m new file mode 100644 index 0000000..a8c783a --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTReferenceSetTests.m @@ -0,0 +1,84 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Local/FSTReferenceSet.h" + +#import <XCTest/XCTest.h> + +#import "Model/FSTDocumentKey.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTReferenceSetTests : XCTestCase +@end + +@implementation FSTReferenceSetTests + +- (void)testAddOrRemoveReferences { + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"foo/bar"]; + + FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init]; + XCTAssertTrue([referenceSet isEmpty]); + XCTAssertFalse([referenceSet containsKey:key]); + + [referenceSet addReferenceToKey:key forID:1]; + XCTAssertTrue([referenceSet containsKey:key]); + XCTAssertFalse([referenceSet isEmpty]); + + [referenceSet addReferenceToKey:key forID:2]; + XCTAssertTrue([referenceSet containsKey:key]); + + [referenceSet removeReferenceToKey:key forID:1]; + XCTAssertTrue([referenceSet containsKey:key]); + + [referenceSet removeReferenceToKey:key forID:3]; + XCTAssertTrue([referenceSet containsKey:key]); + + [referenceSet removeReferenceToKey:key forID:2]; + XCTAssertFalse([referenceSet containsKey:key]); + XCTAssertTrue([referenceSet isEmpty]); +} + +- (void)testRemoveAllReferencesForTargetID { + FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"]; + FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"]; + FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"foo/blah"]; + FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init]; + + [referenceSet addReferenceToKey:key1 forID:1]; + [referenceSet addReferenceToKey:key2 forID:1]; + [referenceSet addReferenceToKey:key3 forID:2]; + XCTAssertFalse([referenceSet isEmpty]); + XCTAssertTrue([referenceSet containsKey:key1]); + XCTAssertTrue([referenceSet containsKey:key2]); + XCTAssertTrue([referenceSet containsKey:key3]); + + [referenceSet removeReferencesForID:1]; + XCTAssertFalse([referenceSet isEmpty]); + XCTAssertFalse([referenceSet containsKey:key1]); + XCTAssertFalse([referenceSet containsKey:key2]); + XCTAssertTrue([referenceSet containsKey:key3]); + + [referenceSet removeReferencesForID:2]; + XCTAssertTrue([referenceSet isEmpty]); + XCTAssertFalse([referenceSet containsKey:key1]); + XCTAssertFalse([referenceSet containsKey:key2]); + XCTAssertFalse([referenceSet containsKey:key3]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h new file mode 100644 index 0000000..fa2a857 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h @@ -0,0 +1,39 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Local/FSTRemoteDocumentCache.h" + +#import <XCTest/XCTest.h> + +@protocol FSTPersistence; + +NS_ASSUME_NONNULL_BEGIN + +/** + * These are tests for any implementation of the FSTRemoteDocumentCache protocol. + * + * To test a specific implementation of FSTRemoteDocumentCache: + * + * + Subclass FSTRemoteDocumentCacheTests + * + override -setUp, assigning to remoteDocumentCache and persistence + * + override -tearDown, cleaning up remoteDocumentCache and persistence + */ +@interface FSTRemoteDocumentCacheTests : XCTestCase +@property(nonatomic, strong, nullable) id<FSTRemoteDocumentCache> remoteDocumentCache; +@property(nonatomic, strong, nullable) id<FSTPersistence> persistence; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.m b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.m new file mode 100644 index 0000000..a875934 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.m @@ -0,0 +1,151 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FSTRemoteDocumentCacheTests.h" + +#import "Core/FSTQuery.h" +#import "Local/FSTPersistence.h" +#import "Local/FSTWriteGroup.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentKey.h" +#import "Model/FSTDocumentSet.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const kDocPath = @"a/b"; +static NSString *const kLongDocPath = @"a/b/c/d/e/f"; +static const int kVersion = 42; + +@implementation FSTRemoteDocumentCacheTests { + NSDictionary<NSString *, id> *_kDocData; +} + +- (void)setUp { + [super setUp]; + + // essentially a constant, but can't be a compile-time one. + _kDocData = @{ @"a" : @1, @"b" : @2 }; +} + +- (void)testReadDocumentNotInCache { + if (!self.remoteDocumentCache) return; + + XCTAssertNil([self readEntryAtPath:kDocPath]); +} + +// Helper for next two tests. +- (void)setAndReadADocumentAtPath:(NSString *)path { + FSTDocument *written = [self setTestDocumentAtPath:path]; + FSTMaybeDocument *read = [self readEntryAtPath:path]; + XCTAssertEqualObjects(read, written); +} + +- (void)testSetAndReadADocument { + if (!self.remoteDocumentCache) return; + + [self setAndReadADocumentAtPath:kDocPath]; +} + +- (void)testSetAndReadADocumentAtDeepPath { + if (!self.remoteDocumentCache) return; + + [self setAndReadADocumentAtPath:kLongDocPath]; +} + +- (void)testSetAndReadDeletedDocument { + if (!self.remoteDocumentCache) return; + + FSTDeletedDocument *deletedDoc = FSTTestDeletedDoc(kDocPath, kVersion); + [self addEntry:deletedDoc]; + + XCTAssertEqualObjects([self readEntryAtPath:kDocPath], deletedDoc); +} + +- (void)testSetDocumentToNewValue { + if (!self.remoteDocumentCache) return; + + [self setTestDocumentAtPath:kDocPath]; + FSTDocument *newDoc = FSTTestDoc(kDocPath, kVersion, @{ @"data" : @2 }, NO); + [self addEntry:newDoc]; + XCTAssertEqualObjects([self readEntryAtPath:kDocPath], newDoc); +} + +- (void)testRemoveDocument { + if (!self.remoteDocumentCache) return; + + [self setTestDocumentAtPath:kDocPath]; + [self removeEntryAtPath:kDocPath]; + + XCTAssertNil([self readEntryAtPath:kDocPath]); +} + +- (void)testRemoveNonExistentDocument { + if (!self.remoteDocumentCache) return; + + // no-op, but make sure it doesn't throw. + XCTAssertNoThrow([self removeEntryAtPath:kDocPath]); +} + +// TODO(mikelehen): Write more elaborate tests once we have more elaborate implementations. +- (void)testDocumentsMatchingQuery { + if (!self.remoteDocumentCache) return; + + [self setTestDocumentAtPath:@"a/1"]; + [self setTestDocumentAtPath:@"b/1"]; + [self setTestDocumentAtPath:@"b/2"]; + [self setTestDocumentAtPath:@"c/1"]; + + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"b")]; + FSTDocumentDictionary *results = [self.remoteDocumentCache documentsMatchingQuery:query]; + NSArray *expected = + @[ FSTTestDoc(@"b/1", kVersion, _kDocData, NO), FSTTestDoc(@"b/2", kVersion, _kDocData, NO) ]; + for (FSTDocument *doc in expected) { + XCTAssertEqualObjects([results objectForKey:doc.key], doc); + } + + // TODO(mikelehen): Perhaps guard against extra documents in the result set once our + // implementations are smarter. +} + +#pragma mark - Helpers + +- (FSTDocument *)setTestDocumentAtPath:(NSString *)path { + FSTDocument *doc = FSTTestDoc(path, kVersion, _kDocData, NO); + [self addEntry:doc]; + return doc; +} + +- (void)addEntry:(FSTMaybeDocument *)maybeDoc { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"addEntry"]; + [self.remoteDocumentCache addEntry:maybeDoc group:group]; + [self.persistence commitGroup:group]; +} + +- (FSTMaybeDocument *_Nullable)readEntryAtPath:(NSString *)path { + return [self.remoteDocumentCache entryForKey:FSTTestDocKey(path)]; +} + +- (void)removeEntryAtPath:(NSString *)path { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeEntryAtPath"]; + [self.remoteDocumentCache removeEntryForKey:FSTTestDocKey(path) group:group]; + [self.persistence commitGroup:group]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTRemoteDocumentChangeBufferTests.m b/Firestore/Example/Tests/Local/FSTRemoteDocumentChangeBufferTests.m new file mode 100644 index 0000000..ebf7713 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTRemoteDocumentChangeBufferTests.m @@ -0,0 +1,113 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Local/FSTRemoteDocumentChangeBuffer.h" + +#import <XCTest/XCTest.h> + +#import "Local/FSTLevelDB.h" +#import "Local/FSTRemoteDocumentCache.h" +#import "Model/FSTDocument.h" + +#import "FSTHelpers.h" +#import "FSTPersistenceTestHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTRemoteDocumentChangeBufferTests : XCTestCase +@end + +@implementation FSTRemoteDocumentChangeBufferTests { + FSTLevelDB *_db; + id<FSTRemoteDocumentCache> _remoteDocumentCache; + FSTRemoteDocumentChangeBuffer *_remoteDocumentBuffer; + + FSTMaybeDocument *_kInitialADoc; + FSTMaybeDocument *_kInitialBDoc; +} + +- (void)setUp { + [super setUp]; + + _db = [FSTPersistenceTestHelpers levelDBPersistence]; + _remoteDocumentCache = [_db remoteDocumentCache]; + + // Add a couple initial items to the cache. + FSTWriteGroup *group = [_db startGroupWithAction:@"Add initial docs."]; + _kInitialADoc = FSTTestDoc(@"coll/a", 42, @{@"test" : @"data"}, NO); + [_remoteDocumentCache addEntry:_kInitialADoc group:group]; + + _kInitialBDoc = + [FSTDeletedDocument documentWithKey:FSTTestDocKey(@"coll/b") version:FSTTestVersion(314)]; + [_remoteDocumentCache addEntry:_kInitialBDoc group:group]; + [_db commitGroup:group]; + + _remoteDocumentBuffer = + [FSTRemoteDocumentChangeBuffer changeBufferWithCache:_remoteDocumentCache]; +} + +- (void)tearDown { + _remoteDocumentBuffer = nil; + _remoteDocumentCache = nil; + _db = nil; + + [super tearDown]; +} + +- (void)testReadUnchangedEntry { + XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")], + _kInitialADoc); +} + +- (void)testAddEntryAndReadItBack { + FSTMaybeDocument *newADoc = FSTTestDoc(@"coll/a", 43, @{@"new" : @"data"}, NO); + [_remoteDocumentBuffer addEntry:newADoc]; + XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")], newADoc); + + // B should still be unchanged. + XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/b")], + _kInitialBDoc); +} + +- (void)testApplyChanges { + FSTMaybeDocument *newADoc = FSTTestDoc(@"coll/a", 43, @{@"new" : @"data"}, NO); + [_remoteDocumentBuffer addEntry:newADoc]; + XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")], newADoc); + + // Reading directly against the cache should still yield the old result. + XCTAssertEqualObjects([_remoteDocumentCache entryForKey:FSTTestDocKey(@"coll/a")], _kInitialADoc); + + FSTWriteGroup *group = [_db startGroupWithAction:@"Apply changes"]; + [_remoteDocumentBuffer applyToWriteGroup:group]; + [_db commitGroup:group]; + + // Reading against the cache should now yield the new result. + XCTAssertEqualObjects([_remoteDocumentCache entryForKey:FSTTestDocKey(@"coll/a")], newADoc); +} + +- (void)testMethodsThrowAfterApply { + FSTWriteGroup *group = [_db startGroupWithAction:@"Apply changes"]; + [_remoteDocumentBuffer applyToWriteGroup:group]; + [_db commitGroup:group]; + + XCTAssertThrows([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")]); + XCTAssertThrows([_remoteDocumentBuffer addEntry:_kInitialADoc]); + XCTAssertThrows([_remoteDocumentBuffer applyToWriteGroup:group]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTWriteGroupTests.mm b/Firestore/Example/Tests/Local/FSTWriteGroupTests.mm new file mode 100644 index 0000000..1cd2feb --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTWriteGroupTests.mm @@ -0,0 +1,121 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Local/FSTWriteGroup.h" + +#import <XCTest/XCTest.h> +#include <leveldb/db.h> + +#import "Protos/objc/firestore/local/Mutation.pbobjc.h" +#import "Local/FSTLevelDB.h" +#import "Local/FSTLevelDBKey.h" + +#import "FSTPersistenceTestHelpers.h" + +using leveldb::ReadOptions; +using leveldb::Status; + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTWriteGroupTests : XCTestCase +@end + +@implementation FSTWriteGroupTests { + FSTLevelDB *_db; +} + +- (void)setUp { + [super setUp]; + + _db = [FSTPersistenceTestHelpers levelDBPersistence]; +} + +- (void)tearDown { + _db = nil; + + [super tearDown]; +} + +- (void)testCommit { + std::string key = [FSTLevelDBMutationKey keyWithUserID:"user1" batchID:42]; + FSTPBWriteBatch *message = [FSTPBWriteBatch message]; + message.batchId = 42; + + // This is a test that shows that committing an empty group does not fail. There are no side + // effects to verify though. + FSTWriteGroup *group = [_db startGroupWithAction:@"Empty commit"]; + XCTAssertNoThrow([_db commitGroup:group]); + + group = [_db startGroupWithAction:@"Put"]; + [group setMessage:message forKey:key]; + + std::string value; + Status status = _db.ptr->Get(ReadOptions(), key, &value); + XCTAssertTrue(status.IsNotFound()); + + [_db commitGroup:group]; + status = _db.ptr->Get(ReadOptions(), key, &value); + XCTAssertTrue(status.ok()); + + group = [_db startGroupWithAction:@"Delete"]; + [group removeMessageForKey:key]; + status = _db.ptr->Get(ReadOptions(), key, &value); + XCTAssertTrue(status.ok()); + + [_db commitGroup:group]; + status = _db.ptr->Get(ReadOptions(), key, &value); + XCTAssertTrue(status.IsNotFound()); +} + +- (void)testDescription { + std::string key = [FSTLevelDBMutationKey keyWithUserID:"user1" batchID:42]; + FSTPBWriteBatch *message = [FSTPBWriteBatch message]; + message.batchId = 42; + + FSTWriteGroup *group = [FSTWriteGroup groupWithAction:@"Action"]; + XCTAssertEqualObjects([group description], @"<FSTWriteGroup for Action: 0 changes (0 bytes):>"); + + [group setMessage:message forKey:key]; + XCTAssertEqualObjects([group description], + @"<FSTWriteGroup for Action: 1 changes (2 bytes):\n" + " - Put [mutation: userID=user1 batchID=42] (2 bytes)>"); + + [group removeMessageForKey:key]; + XCTAssertEqualObjects([group description], + @"<FSTWriteGroup for Action: 2 changes (2 bytes):\n" + " - Put [mutation: userID=user1 batchID=42] (2 bytes)\n" + " - Delete [mutation: userID=user1 batchID=42]>"); +} + +- (void)testCommittingWrongGroupThrows { + // If you don't create the group through persistence, it should throw. + FSTWriteGroup *group = [FSTWriteGroup groupWithAction:@"group"]; + XCTAssertThrows([_db commitGroup:group]); +} + +- (void)testCommittingTwiceThrows { + FSTWriteGroup *group = [_db startGroupWithAction:@"group"]; + [_db commitGroup:group]; + XCTAssertThrows([_db commitGroup:group]); +} + +- (void)testNestingGroupsThrows { + [_db startGroupWithAction:@"group1"]; + XCTAssertThrows([_db startGroupWithAction:@"group2"]); +} +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Model/FSTDatabaseIDTests.m b/Firestore/Example/Tests/Model/FSTDatabaseIDTests.m new file mode 100644 index 0000000..9e7299f --- /dev/null +++ b/Firestore/Example/Tests/Model/FSTDatabaseIDTests.m @@ -0,0 +1,45 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Model/FSTDatabaseID.h" + +#import <XCTest/XCTest.h> + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTDatabaseIDTests : XCTestCase +@end + +@implementation FSTDatabaseIDTests + +- (void)testConstructor { + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"]; + XCTAssertEqualObjects(databaseID.projectID, @"p"); + XCTAssertEqualObjects(databaseID.databaseID, @"d"); + XCTAssertFalse([databaseID isDefaultDatabase]); +} + +- (void)testDefaultDatabase { + FSTDatabaseID *databaseID = + [FSTDatabaseID databaseIDWithProject:@"p" database:kDefaultDatabaseID]; + XCTAssertEqualObjects(databaseID.projectID, @"p"); + XCTAssertEqualObjects(databaseID.databaseID, @"(default)"); + XCTAssertTrue([databaseID isDefaultDatabase]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Model/FSTDocumentKeyTests.m b/Firestore/Example/Tests/Model/FSTDocumentKeyTests.m new file mode 100644 index 0000000..ba68009 --- /dev/null +++ b/Firestore/Example/Tests/Model/FSTDocumentKeyTests.m @@ -0,0 +1,60 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Model/FSTDocumentKey.h" + +#import <XCTest/XCTest.h> + +#import "Model/FSTPath.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTDocumentKeyTests : XCTestCase +@end + +@implementation FSTDocumentKeyTests + +- (void)testConstructor { + FSTResourcePath *path = + [FSTResourcePath pathWithSegments:@[ @"rooms", @"firestore", @"messages", @"1" ]]; + FSTDocumentKey *key = [FSTDocumentKey keyWithPath:path]; + XCTAssertEqual(path, key.path); +} + +- (void)testComparison { + FSTDocumentKey *key1 = [FSTDocumentKey keyWithSegments:@[ @"a", @"b", @"c", @"d" ]]; + FSTDocumentKey *key2 = [FSTDocumentKey keyWithSegments:@[ @"a", @"b", @"c", @"d" ]]; + FSTDocumentKey *key3 = [FSTDocumentKey keyWithSegments:@[ @"x", @"y", @"z", @"w" ]]; + XCTAssertTrue([key1 isEqualToKey:key2]); + XCTAssertFalse([key1 isEqualToKey:key3]); + + FSTDocumentKey *empty = [FSTDocumentKey keyWithSegments:@[]]; + FSTDocumentKey *a = [FSTDocumentKey keyWithSegments:@[ @"a", @"a" ]]; + FSTDocumentKey *b = [FSTDocumentKey keyWithSegments:@[ @"b", @"b" ]]; + FSTDocumentKey *ab = [FSTDocumentKey keyWithSegments:@[ @"a", @"a", @"b", @"b" ]]; + + XCTAssertEqual(NSOrderedAscending, [empty compare:a]); + XCTAssertEqual(NSOrderedAscending, [a compare:b]); + XCTAssertEqual(NSOrderedAscending, [a compare:ab]); + + XCTAssertEqual(NSOrderedDescending, [a compare:empty]); + XCTAssertEqual(NSOrderedDescending, [b compare:a]); + XCTAssertEqual(NSOrderedDescending, [ab compare:a]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Model/FSTDocumentSetTests.m b/Firestore/Example/Tests/Model/FSTDocumentSetTests.m new file mode 100644 index 0000000..e473088 --- /dev/null +++ b/Firestore/Example/Tests/Model/FSTDocumentSetTests.m @@ -0,0 +1,142 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Model/FSTDocumentSet.h" + +#import <XCTest/XCTest.h> + +#import "Model/FSTDocument.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTDocumentSetTests : XCTestCase +@end + +@implementation FSTDocumentSetTests { + NSComparator _comp; + FSTDocument *_doc1; + FSTDocument *_doc2; + FSTDocument *_doc3; +} + +- (void)setUp { + [super setUp]; + + _comp = FSTTestDocComparator(@"sort"); + _doc1 = FSTTestDoc(@"docs/1", 0, @{ @"sort" : @2 }, NO); + _doc2 = FSTTestDoc(@"docs/2", 0, @{ @"sort" : @3 }, NO); + _doc3 = FSTTestDoc(@"docs/3", 0, @{ @"sort" : @1 }, NO); +} + +- (void)testCount { + XCTAssertEqual([FSTTestDocSet(_comp, @[]) count], 0); + XCTAssertEqual([FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]) count], 3); +} + +- (void)testHasKey { + FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2 ]); + + XCTAssertTrue([set containsKey:_doc1.key]); + XCTAssertTrue([set containsKey:_doc2.key]); + XCTAssertFalse([set containsKey:_doc3.key]); +} + +- (void)testDocumentForKey { + FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2 ]); + + XCTAssertEqualObjects([set documentForKey:_doc1.key], _doc1); + XCTAssertEqualObjects([set documentForKey:_doc2.key], _doc2); + XCTAssertNil([set documentForKey:_doc3.key]); +} + +- (void)testFirstAndLastDocument { + FSTDocumentSet *set = FSTTestDocSet(_comp, @[]); + XCTAssertNil([set firstDocument]); + XCTAssertNil([set lastDocument]); + + set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + XCTAssertEqualObjects([set firstDocument], _doc3); + XCTAssertEqualObjects([set lastDocument], _doc2); +} + +- (void)testKeepsDocumentsInTheRightOrder { + FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc3, _doc1, _doc2 ])); +} + +- (void)testPredecessorDocumentForKey { + FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + + XCTAssertNil([set predecessorDocumentForKey:_doc3.key]); + XCTAssertEqualObjects([set predecessorDocumentForKey:_doc1.key], _doc3); + XCTAssertEqualObjects([set predecessorDocumentForKey:_doc2.key], _doc1); +} + +- (void)testDeletes { + FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + + FSTDocumentSet *setWithoutDoc1 = [set documentSetByRemovingKey:_doc1.key]; + XCTAssertEqualObjects([[setWithoutDoc1 documentEnumerator] allObjects], (@[ _doc3, _doc2 ])); + XCTAssertEqual([setWithoutDoc1 count], 2); + + // Original remains unchanged + XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc3, _doc1, _doc2 ])); + + FSTDocumentSet *setWithoutDoc3 = [setWithoutDoc1 documentSetByRemovingKey:_doc3.key]; + XCTAssertEqualObjects([[setWithoutDoc3 documentEnumerator] allObjects], (@[ _doc2 ])); + XCTAssertEqual([setWithoutDoc3 count], 1); +} + +- (void)testUpdates { + FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + + FSTDocument *doc2Prime = FSTTestDoc(@"docs/2", 0, @{ @"sort" : @9 }, NO); + + set = [set documentSetByAddingDocument:doc2Prime]; + XCTAssertEqual([set count], 3); + XCTAssertEqualObjects([set documentForKey:doc2Prime.key], doc2Prime); + XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc3, _doc1, doc2Prime ])); +} + +- (void)testAddsDocsWithEqualComparisonValues { + FSTDocument *doc4 = FSTTestDoc(@"docs/4", 0, @{ @"sort" : @2 }, NO); + + FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, doc4 ]); + XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc1, doc4 ])); +} + +- (void)testIsEqual { + FSTDocumentSet *set1 = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2, _doc3 ]); + FSTDocumentSet *set2 = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2, _doc3 ]); + XCTAssertEqualObjects(set1, set1); + XCTAssertEqualObjects(set1, set2); + XCTAssertNotEqualObjects(set1, nil); + + FSTDocumentSet *sortedSet1 = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + FSTDocumentSet *sortedSet2 = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + XCTAssertEqualObjects(sortedSet1, sortedSet1); + XCTAssertEqualObjects(sortedSet1, sortedSet2); + XCTAssertNotEqualObjects(sortedSet1, nil); + + FSTDocumentSet *shortSet = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2 ]); + XCTAssertNotEqualObjects(set1, shortSet); + XCTAssertNotEqualObjects(set1, sortedSet1); +} +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Model/FSTDocumentTests.m b/Firestore/Example/Tests/Model/FSTDocumentTests.m new file mode 100644 index 0000000..9ceb0cd --- /dev/null +++ b/Firestore/Example/Tests/Model/FSTDocumentTests.m @@ -0,0 +1,101 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Model/FSTDocument.h" + +#import <XCTest/XCTest.h> + +#import "Core/FSTSnapshotVersion.h" +#import "Model/FSTDocumentKey.h" +#import "Model/FSTFieldValue.h" +#import "Model/FSTPath.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTDocumentTests : XCTestCase +@end + +@implementation FSTDocumentTests + +- (void)testConstructor { + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"messages/first"]; + FSTSnapshotVersion *version = FSTTestVersion(1); + FSTObjectValue *data = FSTTestObjectValue(@{ @"a" : @1 }); + FSTDocument *doc = + [FSTDocument documentWithData:data key:key version:version hasLocalMutations:NO]; + + XCTAssertEqualObjects(doc.key, [FSTDocumentKey keyWithPathString:@"messages/first"]); + XCTAssertEqualObjects(doc.version, version); + XCTAssertEqualObjects(doc.data, data); + XCTAssertEqual(doc.hasLocalMutations, NO); +} + +- (void)testExtractsFields { + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"rooms/eros"]; + FSTSnapshotVersion *version = FSTTestVersion(1); + FSTObjectValue *data = FSTTestObjectValue(@{ + @"desc" : @"Discuss all the project related stuff", + @"owner" : @{@"name" : @"Jonny", @"title" : @"scallywag"} + }); + FSTDocument *doc = + [FSTDocument documentWithData:data key:key version:version hasLocalMutations:NO]; + + XCTAssertEqualObjects([doc fieldForPath:FSTTestFieldPath(@"desc")], + [FSTStringValue stringValue:@"Discuss all the project related stuff"]); + XCTAssertEqualObjects([doc fieldForPath:FSTTestFieldPath(@"owner.title")], + [FSTStringValue stringValue:@"scallywag"]); +} + +- (void)testIsEqual { + FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"messages/first"]; + FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"messages/second"]; + FSTObjectValue *data1 = FSTTestObjectValue(@{ @"a" : @1 }); + FSTObjectValue *data2 = FSTTestObjectValue(@{ @"b" : @1 }); + FSTSnapshotVersion *version1 = FSTTestVersion(1); + + FSTDocument *doc1 = + [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:NO]; + FSTDocument *doc2 = + [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:NO]; + + XCTAssertEqualObjects(doc1, doc2); + XCTAssertEqualObjects( + doc1, [FSTDocument documentWithData:FSTTestObjectValue( + @{ @"a" : @1 }) + key:[FSTDocumentKey keyWithPathString:@"messages/first"] + version:version1 + hasLocalMutations:NO]); + + FSTSnapshotVersion *version2 = FSTTestVersion(2); + XCTAssertNotEqualObjects( + doc1, [FSTDocument documentWithData:data2 key:key1 version:version1 hasLocalMutations:NO]); + XCTAssertNotEqualObjects( + doc1, [FSTDocument documentWithData:data1 key:key2 version:version1 hasLocalMutations:NO]); + XCTAssertNotEqualObjects( + doc1, [FSTDocument documentWithData:data1 key:key1 version:version2 hasLocalMutations:NO]); + XCTAssertNotEqualObjects( + doc1, [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:YES]); + + XCTAssertEqualObjects( + [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:YES], + [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:5]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Model/FSTFieldValueTests.m b/Firestore/Example/Tests/Model/FSTFieldValueTests.m new file mode 100644 index 0000000..a357e60 --- /dev/null +++ b/Firestore/Example/Tests/Model/FSTFieldValueTests.m @@ -0,0 +1,576 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Model/FSTFieldValue.h" + +#import <XCTest/XCTest.h> + +#import "API/FIRFirestore+Internal.h" +#import "API/FSTUserDataConverter.h" +#import "Core/FSTTimestamp.h" +#import "Firestore/FIRGeoPoint.h" +#import "Model/FSTDatabaseID.h" +#import "Model/FSTFieldValue.h" +#import "Model/FSTPath.h" + +#import "FSTHelpers.h" + +/** Helper to wrap the values in a set of equality groups using FSTTestFieldValue(). */ +NSArray *FSTWrapGroups(NSArray *groups) { + NSMutableArray *wrapped = [NSMutableArray array]; + for (NSArray<id> *group in groups) { + NSMutableArray *wrappedGroup = [NSMutableArray array]; + for (id value in group) { + FSTFieldValue *wrappedValue; + // Server Timestamp values can't be parsed directly, so we have a couple predefined sentinel + // strings that can be used instead. + if ([value isEqual:@"server-timestamp-1"]) { + wrappedValue = [FSTServerTimestampValue + serverTimestampValueWithLocalWriteTime:FSTTestTimestamp(2016, 5, 20, 10, 20, 0)]; + } else if ([value isEqual:@"server-timestamp-2"]) { + wrappedValue = [FSTServerTimestampValue + serverTimestampValueWithLocalWriteTime:FSTTestTimestamp(2016, 10, 21, 15, 32, 0)]; + } else if ([value isKindOfClass:[FSTDocumentKeyReference class]]) { + // We directly convert these here so that the databaseIDs can be different. + FSTDocumentKeyReference *reference = (FSTDocumentKeyReference *)value; + wrappedValue = + [FSTReferenceValue referenceValue:reference.key databaseID:reference.databaseID]; + } else { + wrappedValue = FSTTestFieldValue(value); + } + [wrappedGroup addObject:wrappedValue]; + } + [wrapped addObject:wrappedGroup]; + } + return wrapped; +} + +@interface FSTFieldValueTests : XCTestCase +@end + +@implementation FSTFieldValueTests { + NSDate *date1; + NSDate *date2; +} + +- (void)setUp { + [super setUp]; + // Create a couple date objects for use in tests. + date1 = FSTTestDate(2016, 5, 20, 10, 20, 0); + date2 = FSTTestDate(2016, 10, 21, 15, 32, 0); +} + +- (void)testWrapIntegers { + NSArray *values = @[ + @(INT_MIN), @(-1), @0, @1, @2, @(UCHAR_MAX), @(INT_MAX), // Standard integers + @(LONG_MIN), @(LONG_MAX), @(LLONG_MIN), @(LLONG_MAX) // Larger values + ]; + for (id value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTIntegerValue class]); + XCTAssertEqualObjects([wrapped value], @([value longLongValue])); + } +} + +- (void)testWrapsDoubles { + // Note that 0x1.0p-1074 is a hex floating point literal representing the minimum subnormal + // number: <https://en.wikipedia.org/wiki/Denormal_number>. + NSArray *values = @[ + @(-INFINITY), @(-DBL_MAX), @(LLONG_MIN * -1.0), @(-1.1), @(-0x1.0p-1074), @(-0.0), @(0.0), + @(0x1.0p-1074), @(DBL_MIN), @(1.1), @(LLONG_MAX * 1.0), @(DBL_MAX), @(INFINITY) + ]; + for (id value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTDoubleValue class]); + XCTAssertEqualObjects([wrapped value], value); + } +} + +- (void)testWrapsNilAndNSNull { + FSTNullValue *nullValue = [FSTNullValue nullValue]; + XCTAssertEqual(FSTTestFieldValue(nil), nullValue); + XCTAssertEqual(FSTTestFieldValue([NSNull null]), nullValue); + XCTAssertEqual([nullValue value], [NSNull null]); +} + +- (void)testWrapsBooleans { + NSArray *values = @[ @YES, @NO, [NSNumber numberWithChar:1], [NSNumber numberWithChar:0] ]; + for (id value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTBooleanValue class]); + XCTAssertEqualObjects([wrapped value], value); + } + + // Unsigned chars could conceivably be handled consistently with signed chars but on arm64 these + // end up being stored as signed shorts. + FSTFieldValue *wrapped = FSTTestFieldValue([NSNumber numberWithUnsignedChar:1]); + XCTAssertEqualObjects(wrapped, [FSTIntegerValue integerValue:1]); +} + +union DoubleBits { + double d; + uint64_t bits; +}; + +- (void)testNormalizesNaNs { + // NOTE: With v1beta1 query semantics, it's no longer as important that our NaN representation + // matches the backend, since all NaNs are defined to sort as equal, but we preserve the + // normalization and this test regardless for now. + + // We use a canonical NaN bit pattern that's common for both Java and Objective-C. Specifically: + // - sign: 0 + // - exponent: 11 bits, all 1 + // - significand: 52 bits, MSB=1, rest=0 + // + // This matches the Firestore backend which uses Java's Double.doubleToLongBits which is defined + // to normalize all NaNs to this value. + union DoubleBits canonical = {.bits = 0x7ff8000000000000ULL}; + + // IEEE 754 specifies that NaN isn't equal to itself. + XCTAssertTrue(isnan(canonical.d)); + XCTAssertEqual(canonical.bits, canonical.bits); + XCTAssertNotEqual(canonical.d, canonical.d); + + // All permutations of the 51 other non-MSB significand bits are also NaNs. + union DoubleBits alternate = {.bits = 0x7fff000000000000ULL}; + XCTAssertTrue(isnan(alternate.d)); + XCTAssertNotEqual(alternate.bits, canonical.bits); + XCTAssertNotEqual(alternate.d, canonical.d); + + // Even though at the C-level assignment preserves non-canonical NaNs, NSNumber normalizes all + // NaNs to single shared instance, kCFNumberNaN. That NaN has no public definition for its value + // but it happens to match what we need. + union DoubleBits normalized = {.d = [[NSNumber numberWithDouble:alternate.d] doubleValue]}; + XCTAssertEqual(normalized.bits, canonical.bits); + + // Ensure we get the same normalization behavior (currently implemented explicitly by checking + // for isnan() and then explicitly assigning NAN). + union DoubleBits result; + result.d = [[FSTDoubleValue doubleValue:canonical.d] internalValue]; + XCTAssertEqual(result.bits, canonical.bits); + + result.d = [[FSTDoubleValue doubleValue:alternate.d] internalValue]; + XCTAssertEqual(result.bits, canonical.bits); + + // A NaN that's canonical except it has the sign bit set (would be negative if signs mattered) + union DoubleBits negative = {.bits = 0xfff8000000000000ULL}; + result.d = [[FSTDoubleValue doubleValue:negative.d] internalValue]; + XCTAssertTrue(isnan(negative.d)); + XCTAssertEqual(result.bits, canonical.bits); + + // A signaling NaN with significand where MSB is 0, and some non-MSB bit is one. + union DoubleBits signaling = {.bits = 0xfff4000000000000ULL}; + XCTAssertTrue(isnan(signaling.d)); + result.d = [[FSTDoubleValue doubleValue:signaling.d] internalValue]; + XCTAssertEqual(result.bits, canonical.bits); +} + +- (void)testZeros { + // Floating point numbers have an explicit sign bit so it's possible to end up with negative + // zero as a distinct value from positive zero. + union DoubleBits zero = {.d = 0.0}; + union DoubleBits negativeZero = {.d = -0.0}; + + // IEEE 754 requires these two zeros to compare equal. + XCTAssertNotEqual(zero.bits, negativeZero.bits); + XCTAssertEqual(zero.d, negativeZero.d); + + // NSNumber preserves the negative zero value but compares equal according to IEEE 754. + union DoubleBits normalized = {.d = [[NSNumber numberWithDouble:negativeZero.d] doubleValue]}; + XCTAssertEqual(normalized.bits, negativeZero.bits); + XCTAssertEqualObjects([NSNumber numberWithDouble:0.0], [NSNumber numberWithDouble:-0.0]); + + // FSTDoubleValue preserves positive/negative zero + union DoubleBits result; + result.d = [[[FSTDoubleValue doubleValue:zero.d] value] doubleValue]; + XCTAssertEqual(result.bits, zero.bits); + result.d = [[[FSTDoubleValue doubleValue:negativeZero.d] value] doubleValue]; + XCTAssertEqual(result.bits, negativeZero.bits); + + // ... but compares positive/negative zero as unequal, compatibly with Firestore. + XCTAssertNotEqualObjects([FSTDoubleValue doubleValue:0.0], [FSTDoubleValue doubleValue:-0.0]); +} + +- (void)testWrapStrings { + NSArray *values = @[ @"", @"abc" ]; + for (id value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTStringValue class]); + XCTAssertEqualObjects([wrapped value], value); + } +} + +- (void)testWrapDates { + NSArray *values = @[ FSTTestDate(1900, 12, 1, 1, 20, 30), FSTTestDate(2017, 4, 24, 13, 20, 30) ]; + for (id value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTTimestampValue class]); + XCTAssertEqualObjects([wrapped value], value); + + XCTAssertEqualObjects(((FSTTimestampValue *)wrapped).internalValue, + [FSTTimestamp timestampWithDate:value]); + } +} + +- (void)testWrapGeoPoints { + NSArray *values = @[ FSTTestGeoPoint(1.24, 4.56), FSTTestGeoPoint(-20, 100) ]; + + for (id value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTGeoPointValue class]); + XCTAssertEqualObjects([wrapped value], value); + } +} + +- (void)testWrapBlobs { + NSArray *values = @[ FSTTestData(1, 2, 3), FSTTestData(1, 2) ]; + for (id value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTBlobValue class]); + XCTAssertEqualObjects([wrapped value], value); + } +} + +- (void)testWrapResourceNames { + NSArray *values = @[ + FSTTestRef(@"project", kDefaultDatabaseID, @"foo/bar"), + FSTTestRef(@"project", kDefaultDatabaseID, @"foo/baz") + ]; + for (FSTDocumentKeyReference *value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTReferenceValue class]); + XCTAssertEqualObjects([wrapped value], value.key); + XCTAssertEqualObjects(((FSTDatabaseID *)wrapped).databaseID, value.databaseID); + } +} + +- (void)testWrapsEmptyObjects { + XCTAssertEqualObjects(FSTTestFieldValue(@{}), [FSTObjectValue objectValue]); +} + +- (void)testWrapsSimpleObjects { + FSTObjectValue *actual = FSTTestObjectValue( + @{ @"a" : @"foo", + @"b" : @(1L), + @"c" : @YES, + @"d" : [NSNull null] }); + FSTObjectValue *expected = [[FSTObjectValue alloc] initWithDictionary:@{ + @"a" : [FSTStringValue stringValue:@"foo"], + @"b" : [FSTIntegerValue integerValue:1LL], + @"c" : [FSTBooleanValue trueValue], + @"d" : [FSTNullValue nullValue] + }]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testWrapsNestedObjects { + FSTObjectValue *actual = FSTTestObjectValue(@{ @"a" : @{@"b" : @{@"c" : @"foo"}, @"d" : @YES} }); + FSTObjectValue *expected = [[FSTObjectValue alloc] initWithDictionary:@{ + @"a" : [[FSTObjectValue alloc] initWithDictionary:@{ + @"b" : + [[FSTObjectValue alloc] initWithDictionary:@{@"c" : [FSTStringValue stringValue:@"foo"]}], + @"d" : [FSTBooleanValue booleanValue:YES] + }] + }]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testExtractsFields { + FSTObjectValue *obj = FSTTestObjectValue(@{ @"foo" : @{@"a" : @YES, @"b" : @"string"} }); + FSTAssertIsKindOfClass(obj, FSTObjectValue); + + FSTAssertIsKindOfClass([obj valueForPath:FSTTestFieldPath(@"foo")], FSTObjectValue); + XCTAssertEqualObjects([obj valueForPath:FSTTestFieldPath(@"foo.a")], [FSTBooleanValue trueValue]); + XCTAssertEqualObjects([obj valueForPath:FSTTestFieldPath(@"foo.b")], + [FSTStringValue stringValue:@"string"]); + + XCTAssertNil([obj valueForPath:FSTTestFieldPath(@"foo.a.b")]); + XCTAssertNil([obj valueForPath:FSTTestFieldPath(@"bar")]); + XCTAssertNil([obj valueForPath:FSTTestFieldPath(@"bar.a")]); +} + +- (void)testOverwritesExistingFields { + FSTObjectValue *old = FSTTestObjectValue(@{@"a" : @"old"}); + FSTObjectValue *mod = + [old objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"a")]; + + // Should return a new object, leaving the old one unmodified. + XCTAssertNotEqual(old, mod); + XCTAssertEqualObjects(old, FSTTestFieldValue(@{@"a" : @"old"})); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{@"a" : @"mod"})); +} + +- (void)testAddsNewFields { + FSTObjectValue *empty = [FSTObjectValue objectValue]; + FSTObjectValue *mod = + [empty objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"a")]; + XCTAssertNotEqual(empty, mod); + XCTAssertEqualObjects(empty, FSTTestFieldValue(@{})); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{@"a" : @"mod"})); + + FSTObjectValue *old = mod; + mod = [old objectBySettingValue:FSTTestFieldValue(@1) forPath:FSTTestFieldPath(@"b")]; + XCTAssertNotEqual(old, mod); + XCTAssertEqualObjects(old, FSTTestFieldValue(@{@"a" : @"mod"})); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @"mod", @"b" : @1 })); +} + +- (void)testImplicitlyCreatesObjects { + FSTObjectValue *old = FSTTestObjectValue(@{@"a" : @"old"}); + FSTObjectValue *mod = + [old objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"b.c.d")]; + XCTAssertNotEqual(old, mod); + XCTAssertEqualObjects(old, FSTTestFieldValue(@{@"a" : @"old"})); + XCTAssertEqualObjects(mod, FSTTestFieldValue( + @{ @"a" : @"old", + @"b" : @{@"c" : @{@"d" : @"mod"}} })); +} + +- (void)testCanOverwritePrimitivesWithObjects { + FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @{@"b" : @"old"} }); + FSTObjectValue *mod = + [old objectBySettingValue:FSTTestFieldValue(@{@"b" : @"mod"}) forPath:FSTTestFieldPath(@"a")]; + XCTAssertNotEqual(old, mod); + XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @"old"} })); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @"mod"} })); +} + +- (void)testAddsToNestedObjects { + FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @{@"b" : @"old"} }); + FSTObjectValue *mod = + [old objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"a.c")]; + XCTAssertNotEqual(old, mod); + XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @"old"} })); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @"old", @"c" : @"mod"} })); +} + +- (void)testDeletesKeys { + FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @1, @"b" : @2 }); + FSTObjectValue *mod = [old objectByDeletingPath:FSTTestFieldPath(@"a")]; + XCTAssertNotEqual(old, mod); + XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @1, @"b" : @2 })); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"b" : @2 })); + + FSTObjectValue *empty = [mod objectByDeletingPath:FSTTestFieldPath(@"b")]; + XCTAssertNotEqual(mod, empty); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"b" : @2 })); + XCTAssertEqualObjects(empty, FSTTestFieldValue(@{})); +} + +- (void)testDeletesHandleMissingKeys { + FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @{@"b" : @1, @"c" : @2} }); + FSTObjectValue *mod = [old objectByDeletingPath:FSTTestFieldPath(@"b")]; + XCTAssertEqualObjects(old, mod); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @2} })); + + mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.d")]; + XCTAssertEqualObjects(old, mod); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @2} })); + + mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.b.c")]; + XCTAssertEqualObjects(old, mod); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @2} })); +} + +- (void)testDeletesNestedKeys { + FSTObjectValue *old = FSTTestObjectValue( + @{ @"a" : @{@"b" : @1, @"c" : @{@"d" : @2, @"e" : @3}} }); + FSTObjectValue *mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.c.d")]; + XCTAssertNotEqual(old, mod); + XCTAssertEqualObjects(old, FSTTestFieldValue( + @{ @"a" : @{@"b" : @1, @"c" : @{@"d" : @2, @"e" : @3}} })); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @{@"e" : @3}} })); + + old = mod; + mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.c")]; + XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @{@"e" : @3}} })); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1} })); + + old = mod; + mod = [old objectByDeletingPath:FSTTestFieldPath(@"a")]; + XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @1} })); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{})); +} + +- (void)testArrays { + FSTArrayValue *expected = [[FSTArrayValue alloc] + initWithValueNoCopy:@[ [FSTStringValue stringValue:@"value"], [FSTBooleanValue trueValue] ]]; + + FSTArrayValue *actual = (FSTArrayValue *)FSTTestFieldValue(@[ @"value", @YES ]); + XCTAssertEqualObjects(actual, expected); +} + +- (void)testValueEquality { + NSArray *groups = @[ + @[ FSTTestFieldValue(@YES), [FSTBooleanValue booleanValue:YES] ], + @[ FSTTestFieldValue(@NO), [FSTBooleanValue booleanValue:NO] ], + @[ FSTTestFieldValue([NSNull null]), [FSTNullValue nullValue] ], + @[ FSTTestFieldValue(@(0.0 / 0.0)), FSTTestFieldValue(@(NAN)), [FSTDoubleValue nanValue] ], + // -0.0 and 0.0 compare: the same (but are not isEqual:) + @[ FSTTestFieldValue(@(-0.0)) ], @[ FSTTestFieldValue(@0.0) ], + @[ FSTTestFieldValue(@1), FSTTestFieldValue(@1LL), [FSTIntegerValue integerValue:1LL] ], + // double and unit64_t values can compare: the same (but won't be isEqual:) + @[ FSTTestFieldValue(@1.0), [FSTDoubleValue doubleValue:1.0] ], + @[ FSTTestFieldValue(@1.1), [FSTDoubleValue doubleValue:1.1] ], + @[ + FSTTestFieldValue(FSTTestData(0, 1, 2, -1)), [FSTBlobValue blobValue:FSTTestData(0, 1, 2, -1)] + ], + @[ FSTTestFieldValue(FSTTestData(0, 1, -1)) ], + @[ FSTTestFieldValue(@"string"), [FSTStringValue stringValue:@"string"] ], + @[ FSTTestFieldValue(@"strin") ], + @[ FSTTestFieldValue(@"e\u0301b") ], // latin small letter e + combining acute accent + @[ FSTTestFieldValue(@"\u00e9a") ], // latin small letter e with acute accent + @[ + FSTTestFieldValue(date1), + [FSTTimestampValue timestampValue:[FSTTimestamp timestampWithDate:date1]] + ], + @[ FSTTestFieldValue(date2) ], + @[ + // NOTE: ServerTimestampValues can't be parsed via FSTTestFieldValue(). + [FSTServerTimestampValue + serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date1]], + [FSTServerTimestampValue + serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date1]] + ], + @[ [FSTServerTimestampValue + serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date2]] ], + @[ + FSTTestFieldValue(FSTTestGeoPoint(0, 1)), + [FSTGeoPointValue geoPointValue:FSTTestGeoPoint(0, 1)] + ], + @[ FSTTestFieldValue(FSTTestGeoPoint(1, 0)) ], + @[ + [FSTReferenceValue referenceValue:FSTTestDocKey(@"coll/doc1") + databaseID:[FSTDatabaseID databaseIDWithProject:@"project" + database:kDefaultDatabaseID]], + FSTTestFieldValue(FSTTestRef(@"project", kDefaultDatabaseID, @"coll/doc1")) + ], + @[ FSTTestRef(@"project", @"(default)", @"coll/doc2") ], + @[ FSTTestFieldValue(@[ @"foo", @"bar" ]), FSTTestFieldValue(@[ @"foo", @"bar" ]) ], + @[ FSTTestFieldValue(@[ @"foo", @"bar", @"baz" ]) ], @[ FSTTestFieldValue(@[ @"foo" ]) ], + @[ + FSTTestFieldValue( + @{ @"bar" : @1, + @"foo" : @2 }), + FSTTestFieldValue( + @{ @"foo" : @2, + @"bar" : @1 }) + ], + @[ FSTTestFieldValue( + @{ @"bar" : @2, + @"foo" : @1 }) ], + @[ FSTTestFieldValue( + @{ @"bar" : @1, + @"foo" : @1 }) ], + @[ FSTTestFieldValue( + @{ @"foo" : @1 }) ] + ]; + + FSTAssertEqualityGroups(groups); +} + +- (void)testValueOrdering { + NSArray *groups = @[ + // null first + @[ [NSNull null] ], + + // booleans + @[ @NO ], @[ @YES ], + + // numbers + @[ @(0.0 / 0.0) ], @[ @(-INFINITY) ], @[ @(-DBL_MAX) ], @[ @(LLONG_MIN) ], @[ @(-1.1) ], + @[ @(-1.0), @(-1LL) ], // longs and doubles compare the same + @[ @(-DBL_MIN) ], + @[ @(-0x1.0p-1074) ], // negative smallest subnormal + @[ @(-0.0), @(0.0), @(0LL) ], // zeros all compare the same + @[ @(0x1.0p-1074) ], // positive smallest subnormal + @[ @(DBL_MIN) ], @[ @1.0, @1LL ], // longs and doubles compare the same + @[ @1.1 ], @[ @(LLONG_MAX) ], @[ @(DBL_MAX) ], @[ @(INFINITY) ], + + // timestamps + @[ date1 ], @[ date2 ], + + // server timestamps come after all concrete timestamps. + // NOTE: server timestamps can't be parsed directly, so we have special sentinel strings (see + // FSTWrapGroups()). + @[ @"server-timestamp-1" ], @[ @"server-timestamp-2" ], + + // strings + @[ @"" ], @[ @"\000\ud7ff\ue000\uffff" ], @[ @"(╯°□°)╯︵ ┻━┻" ], @[ @"a" ], @[ @"abc def" ], + @[ @"e\u0301b" ], // latin small letter e + combining acute accent + latin small letter b + @[ @"æ" ], + @[ @"\u00e9a" ], // latin small letter e with acute accent + latin small letter a + + // blobs + @[ FSTTestData(-1) ], @[ FSTTestData(0, -1) ], @[ FSTTestData(0, 1, 2, 3, 4, -1) ], + @[ FSTTestData(0, 1, 2, 4, 3, -1) ], @[ FSTTestData(255, -1) ], + + // resource names + @[ FSTTestRef(@"p1", @"d1", @"c1/doc1") ], @[ FSTTestRef(@"p1", @"d1", @"c1/doc2") ], + @[ FSTTestRef(@"p1", @"d1", @"c10/doc1") ], @[ FSTTestRef(@"p1", @"d1", @"c2/doc1") ], + @[ FSTTestRef(@"p1", @"d2", @"c1/doc1") ], @[ FSTTestRef(@"p2", @"d1", @"c1/doc1") ], + + // Geo points + @[ FSTTestGeoPoint(-90, -180) ], @[ FSTTestGeoPoint(-90, 0) ], @[ FSTTestGeoPoint(-90, 180) ], + @[ FSTTestGeoPoint(0, -180) ], @[ FSTTestGeoPoint(0, 0) ], @[ FSTTestGeoPoint(0, 180) ], + @[ FSTTestGeoPoint(1, -180) ], @[ FSTTestGeoPoint(1, 0) ], @[ FSTTestGeoPoint(1, 180) ], + @[ FSTTestGeoPoint(90, -180) ], @[ FSTTestGeoPoint(90, 0) ], @[ FSTTestGeoPoint(90, 180) ], + + // Arrays + @[ @[] ], @[ @[ @"bar" ] ], @[ @[ @"foo" ] ], @[ @[ @"foo", @1 ] ], @[ @[ @"foo", @2 ] ], + @[ @[ @"foo", @"0" ] ], + + // Objects + @[ + @{ @"bar" : @0 } + ], + @[ + @{ @"bar" : @0, + @"foo" : @1 } + ], + @[ + @{ @"foo" : @1 } + ], + @[ + @{ @"foo" : @2 } + ], + @[ @{@"foo" : @"0"} ] + ]; + + NSArray *wrapped = FSTWrapGroups(groups); + FSTAssertComparisons(wrapped); +} + +- (void)testValue { + NSDate *date = [NSDate date]; + id input = @{ @"array" : @[ @1, date ], @"obj" : @{@"date" : date, @"string" : @"hi"} }; + FSTObjectValue *value = FSTTestObjectValue(input); + id output = [value value]; + { + XCTAssertTrue([output[@"array"][1] isKindOfClass:[NSDate class]]); + NSDate *actual = output[@"array"][1]; + XCTAssertEqualWithAccuracy(date.timeIntervalSince1970, actual.timeIntervalSince1970, + 0.000000001); + } + { + XCTAssertTrue([output[@"obj"][@"date"] isKindOfClass:[NSDate class]]); + NSDate *actual = output[@"obj"][@"date"]; + XCTAssertEqualWithAccuracy(date.timeIntervalSince1970, actual.timeIntervalSince1970, + 0.000000001); + } +} + +@end diff --git a/Firestore/Example/Tests/Model/FSTMutationTests.m b/Firestore/Example/Tests/Model/FSTMutationTests.m new file mode 100644 index 0000000..5ad9f94 --- /dev/null +++ b/Firestore/Example/Tests/Model/FSTMutationTests.m @@ -0,0 +1,216 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Model/FSTMutation.h" + +#import <XCTest/XCTest.h> + +#import "Core/FSTTimestamp.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentKey.h" +#import "Model/FSTFieldValue.h" +#import "Model/FSTPath.h" + +#import "FSTHelpers.h" + +@interface FSTMutationTests : XCTestCase +@end + +@implementation FSTMutationTests { + FSTTimestamp *_timestamp; +} + +- (void)setUp { + _timestamp = [FSTTimestamp timestamp]; +} + +- (void)testAppliesSetsToDocuments { + NSDictionary *docData = @{@"foo" : @"foo-value", @"baz" : @"baz-value"}; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *set = FSTTestSetMutation(@"collection/key", @{@"bar" : @"bar-value"}); + FSTMaybeDocument *setDoc = [set applyTo:baseDoc localWriteTime:_timestamp]; + + NSDictionary *expectedData = @{@"bar" : @"bar-value"}; + XCTAssertEqualObjects(setDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); +} + +- (void)testAppliesPatchesToDocuments { + NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value" }; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *patch = + FSTTestPatchMutation(@"collection/key", @{@"foo.bar" : @"new-bar-value"}, nil); + FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc localWriteTime:_timestamp]; + + NSDictionary *expectedData = @{ @"foo" : @{@"bar" : @"new-bar-value"}, @"baz" : @"baz-value" }; + XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); +} + +- (void)testDeletesValuesFromTheFieldMask { + NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value", @"baz" : @"baz-value"} }; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"collection/key"]; + FSTFieldMask *mask = [[FSTFieldMask alloc] initWithFields:@[ FSTTestFieldPath(@"foo.bar") ]]; + FSTMutation *patch = [[FSTPatchMutation alloc] initWithKey:key + fieldMask:mask + value:[FSTObjectValue objectValue] + precondition:[FSTPrecondition none]]; + FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc localWriteTime:_timestamp]; + + NSDictionary *expectedData = @{ @"foo" : @{@"baz" : @"baz-value"} }; + XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); +} + +- (void)testPatchesPrimitiveValue { + NSDictionary *docData = @{@"foo" : @"foo-value", @"baz" : @"baz-value"}; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *patch = + FSTTestPatchMutation(@"collection/key", @{@"foo.bar" : @"new-bar-value"}, nil); + FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc localWriteTime:_timestamp]; + + NSDictionary *expectedData = @{ @"foo" : @{@"bar" : @"new-bar-value"}, @"baz" : @"baz-value" }; + XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); +} + +- (void)testPatchingDeletedDocumentsDoesNothing { + FSTMaybeDocument *baseDoc = FSTTestDeletedDoc(@"collection/key", 0); + FSTMutation *patch = FSTTestPatchMutation(@"collection/key", @{@"foo" : @"bar"}, nil); + FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc localWriteTime:_timestamp]; + XCTAssertEqualObjects(patchedDoc, baseDoc); +} + +- (void)testAppliesLocalTransformsToDocuments { + NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value" }; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *transform = FSTTestTransformMutation(@"collection/key", @[ @"foo.bar" ]); + FSTMaybeDocument *transformedDoc = [transform applyTo:baseDoc localWriteTime:_timestamp]; + + // Server timestamps aren't parsed, so we manually insert it. + FSTObjectValue *expectedData = FSTTestObjectValue( + @{ @"foo" : @{@"bar" : @"<server-timestamp>"}, + @"baz" : @"baz-value" }); + expectedData = + [expectedData objectBySettingValue:[FSTServerTimestampValue + serverTimestampValueWithLocalWriteTime:_timestamp] + forPath:FSTTestFieldPath(@"foo.bar")]; + + FSTDocument *expectedDoc = [FSTDocument documentWithData:expectedData + key:FSTTestDocKey(@"collection/key") + version:FSTTestVersion(0) + hasLocalMutations:YES]; + + XCTAssertEqualObjects(transformedDoc, expectedDoc); +} + +- (void)testAppliesServerAckedTransformsToDocuments { + NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value" }; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *transform = FSTTestTransformMutation(@"collection/key", @[ @"foo.bar" ]); + + FSTMutationResult *mutationResult = [[FSTMutationResult alloc] + initWithVersion:FSTTestVersion(1) + transformResults:@[ [FSTTimestampValue timestampValue:_timestamp] ]]; + + FSTMaybeDocument *transformedDoc = + [transform applyTo:baseDoc localWriteTime:_timestamp mutationResult:mutationResult]; + + NSDictionary *expectedData = + @{ @"foo" : @{@"bar" : _timestamp.approximateDateValue}, + @"baz" : @"baz-value" }; + XCTAssertEqualObjects(transformedDoc, FSTTestDoc(@"collection/key", 0, expectedData, NO)); +} + +- (void)testDeleteDeletes { + NSDictionary *docData = @{@"foo" : @"bar"}; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *mutation = FSTTestDeleteMutation(@"collection/key"); + FSTMaybeDocument *result = [mutation applyTo:baseDoc localWriteTime:_timestamp]; + XCTAssertEqualObjects(result, FSTTestDeletedDoc(@"collection/key", 0)); +} + +- (void)testSetWithMutationResult { + NSDictionary *docData = @{@"foo" : @"bar"}; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *set = FSTTestSetMutation(@"collection/key", @{@"foo" : @"new-bar"}); + FSTMutationResult *mutationResult = + [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(4) transformResults:nil]; + FSTMaybeDocument *setDoc = + [set applyTo:baseDoc localWriteTime:_timestamp mutationResult:mutationResult]; + + NSDictionary *expectedData = @{@"foo" : @"new-bar"}; + XCTAssertEqualObjects(setDoc, FSTTestDoc(@"collection/key", 0, expectedData, NO)); +} + +- (void)testPatchWithMutationResult { + NSDictionary *docData = @{@"foo" : @"bar"}; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *patch = FSTTestPatchMutation(@"collection/key", @{@"foo" : @"new-bar"}, nil); + FSTMutationResult *mutationResult = + [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(4) transformResults:nil]; + FSTMaybeDocument *patchedDoc = + [patch applyTo:baseDoc localWriteTime:_timestamp mutationResult:mutationResult]; + + NSDictionary *expectedData = @{@"foo" : @"new-bar"}; + XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, NO)); +} + +#define ASSERT_VERSION_TRANSITION(mutation, base, expected) \ + do { \ + FSTMutationResult *mutationResult = \ + [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(0) transformResults:nil]; \ + FSTMaybeDocument *actual = \ + [mutation applyTo:base localWriteTime:_timestamp mutationResult:mutationResult]; \ + XCTAssertEqualObjects(actual, expected); \ + } while (0); + +/** + * Tests the transition table documented in FSTMutation.h. + */ +- (void)testTransitions { + FSTDocument *docV0 = FSTTestDoc(@"collection/key", 0, @{}, NO); + FSTDeletedDocument *deletedV0 = FSTTestDeletedDoc(@"collection/key", 0); + + FSTDocument *docV3 = FSTTestDoc(@"collection/key", 3, @{}, NO); + FSTDeletedDocument *deletedV3 = FSTTestDeletedDoc(@"collection/key", 3); + + FSTMutation *setMutation = FSTTestSetMutation(@"collection/key", @{}); + FSTMutation *patchMutation = FSTTestPatchMutation(@"collection/key", @{}, nil); + FSTMutation *deleteMutation = FSTTestDeleteMutation(@"collection/key"); + + ASSERT_VERSION_TRANSITION(setMutation, docV3, docV3); + ASSERT_VERSION_TRANSITION(setMutation, deletedV3, docV0); + ASSERT_VERSION_TRANSITION(setMutation, nil, docV0); + + ASSERT_VERSION_TRANSITION(patchMutation, docV3, docV3); + ASSERT_VERSION_TRANSITION(patchMutation, deletedV3, deletedV3); + ASSERT_VERSION_TRANSITION(patchMutation, nil, nil); + + ASSERT_VERSION_TRANSITION(deleteMutation, docV3, deletedV0); + ASSERT_VERSION_TRANSITION(deleteMutation, deletedV3, deletedV0); + ASSERT_VERSION_TRANSITION(deleteMutation, nil, deletedV0); +} + +#undef ASSERT_TRANSITION + +@end diff --git a/Firestore/Example/Tests/Model/FSTPathTests.m b/Firestore/Example/Tests/Model/FSTPathTests.m new file mode 100644 index 0000000..a5d3d80 --- /dev/null +++ b/Firestore/Example/Tests/Model/FSTPathTests.m @@ -0,0 +1,196 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FSTHelpers.h" +#import "Model/FSTPath.h" + +#import <XCTest/XCTest.h> + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTFieldPathTests : XCTestCase +@end + +@implementation FSTFieldPathTests + +- (void)testConstructor { + FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; + XCTAssertEqual(3, path.length); +} + +- (void)testIndexing { + FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; + XCTAssertEqualObjects(@"rooms", path.firstSegment); + XCTAssertEqualObjects(@"rooms", [path segmentAtIndex:0]); + XCTAssertEqualObjects(@"rooms", path[0]); + + XCTAssertEqualObjects(@"Eros", [path segmentAtIndex:1]); + XCTAssertEqualObjects(@"Eros", path[1]); + + XCTAssertEqualObjects(@"messages", [path segmentAtIndex:2]); + XCTAssertEqualObjects(@"messages", path[2]); + XCTAssertEqualObjects(@"messages", path.lastSegment); +} + +- (void)testPathByRemovingFirstSegment { + FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; + FSTFieldPath *same = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; + FSTFieldPath *second = [FSTFieldPath pathWithSegments:@[ @"Eros", @"messages" ]]; + FSTFieldPath *third = [FSTFieldPath pathWithSegments:@[ @"messages" ]]; + FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]]; + + XCTAssertEqualObjects(second, path.pathByRemovingFirstSegment); + XCTAssertEqualObjects(third, path.pathByRemovingFirstSegment.pathByRemovingFirstSegment); + XCTAssertEqualObjects( + empty, path.pathByRemovingFirstSegment.pathByRemovingFirstSegment.pathByRemovingFirstSegment); + // unmodified original + XCTAssertEqualObjects(same, path); +} + +- (void)testPathByRemovingLastSegment { + FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; + FSTFieldPath *same = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; + FSTFieldPath *second = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros" ]]; + FSTFieldPath *third = [FSTFieldPath pathWithSegments:@[ @"rooms" ]]; + FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]]; + + XCTAssertEqualObjects(second, path.pathByRemovingLastSegment); + XCTAssertEqualObjects(third, path.pathByRemovingLastSegment.pathByRemovingLastSegment); + XCTAssertEqualObjects( + empty, path.pathByRemovingLastSegment.pathByRemovingLastSegment.pathByRemovingLastSegment); + // unmodified original + XCTAssertEqualObjects(same, path); +} + +- (void)testPathByAppendingSegment { + FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms" ]]; + FSTFieldPath *rooms = [FSTFieldPath pathWithSegments:@[ @"rooms" ]]; + FSTFieldPath *roomsEros = [FSTFieldPath pathWithSegments:@[ @"rooms", @"eros" ]]; + FSTFieldPath *roomsEros1 = [FSTFieldPath pathWithSegments:@[ @"rooms", @"eros", @"1" ]]; + + XCTAssertEqualObjects(roomsEros, [path pathByAppendingSegment:@"eros"]); + XCTAssertEqualObjects(roomsEros1, + [[path pathByAppendingSegment:@"eros"] pathByAppendingSegment:@"1"]); + // unmodified original + XCTAssertEqualObjects(rooms, path); + + FSTFieldPath *sub = [FSTTestFieldPath(@"rooms.eros.1") pathByRemovingFirstSegment]; + FSTFieldPath *appended = [sub pathByAppendingSegment:@"2"]; + XCTAssertEqualObjects(appended, FSTTestFieldPath(@"eros.1.2")); +} + +- (void)testPathComparison { + FSTFieldPath *path1 = [FSTFieldPath pathWithSegments:@[ @"a", @"b", @"c" ]]; + FSTFieldPath *path2 = [FSTFieldPath pathWithSegments:@[ @"a", @"b", @"c" ]]; + FSTFieldPath *path3 = [FSTFieldPath pathWithSegments:@[ @"x", @"y", @"z" ]]; + XCTAssertTrue([path1 isEqual:path2]); + XCTAssertFalse([path1 isEqual:path3]); + + FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]]; + FSTFieldPath *a = [FSTFieldPath pathWithSegments:@[ @"a" ]]; + FSTFieldPath *b = [FSTFieldPath pathWithSegments:@[ @"b" ]]; + FSTFieldPath *ab = [FSTFieldPath pathWithSegments:@[ @"a", @"b" ]]; + + XCTAssertEqual(NSOrderedAscending, [empty compare:a]); + XCTAssertEqual(NSOrderedAscending, [a compare:b]); + XCTAssertEqual(NSOrderedAscending, [a compare:ab]); + + XCTAssertEqual(NSOrderedDescending, [a compare:empty]); + XCTAssertEqual(NSOrderedDescending, [b compare:a]); + XCTAssertEqual(NSOrderedDescending, [ab compare:a]); +} + +- (void)testIsPrefixOfPath { + FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]]; + FSTFieldPath *a = [FSTFieldPath pathWithSegments:@[ @"a" ]]; + FSTFieldPath *ab = [FSTFieldPath pathWithSegments:@[ @"a", @"b" ]]; + FSTFieldPath *abc = [FSTFieldPath pathWithSegments:@[ @"a", @"b", @"c" ]]; + FSTFieldPath *b = [FSTFieldPath pathWithSegments:@[ @"b" ]]; + FSTFieldPath *ba = [FSTFieldPath pathWithSegments:@[ @"b", @"a" ]]; + + XCTAssertTrue([empty isPrefixOfPath:a]); + XCTAssertTrue([empty isPrefixOfPath:ab]); + XCTAssertTrue([empty isPrefixOfPath:abc]); + XCTAssertTrue([empty isPrefixOfPath:empty]); + XCTAssertTrue([empty isPrefixOfPath:b]); + XCTAssertTrue([empty isPrefixOfPath:ba]); + + XCTAssertTrue([a isPrefixOfPath:a]); + XCTAssertTrue([a isPrefixOfPath:ab]); + XCTAssertTrue([a isPrefixOfPath:abc]); + XCTAssertFalse([a isPrefixOfPath:empty]); + XCTAssertFalse([a isPrefixOfPath:b]); + XCTAssertFalse([a isPrefixOfPath:ba]); + + XCTAssertFalse([ab isPrefixOfPath:a]); + XCTAssertTrue([ab isPrefixOfPath:ab]); + XCTAssertTrue([ab isPrefixOfPath:abc]); + XCTAssertFalse([ab isPrefixOfPath:empty]); + XCTAssertFalse([ab isPrefixOfPath:b]); + XCTAssertFalse([ab isPrefixOfPath:ba]); + + XCTAssertFalse([abc isPrefixOfPath:a]); + XCTAssertFalse([abc isPrefixOfPath:ab]); + XCTAssertTrue([abc isPrefixOfPath:abc]); + XCTAssertFalse([abc isPrefixOfPath:empty]); + XCTAssertFalse([abc isPrefixOfPath:b]); + XCTAssertFalse([abc isPrefixOfPath:ba]); +} + +- (void)testInvalidPaths { + XCTAssertThrows(FSTTestFieldPath(@"")); + XCTAssertThrows(FSTTestFieldPath(@".")); + XCTAssertThrows(FSTTestFieldPath(@".foo")); + XCTAssertThrows(FSTTestFieldPath(@"foo.")); + XCTAssertThrows(FSTTestFieldPath(@"foo..bar")); +} + +#define ASSERT_ROUND_TRIP(str, segments) \ + do { \ + FSTFieldPath *path = [FSTFieldPath pathWithServerFormat:str]; \ + XCTAssertEqual([path length], segments); \ + NSString *canonical = [path canonicalString]; \ + XCTAssertEqualObjects(canonical, str); \ + } while (0); + +- (void)testCanonicalString { + ASSERT_ROUND_TRIP(@"foo", 1); + ASSERT_ROUND_TRIP(@"foo.bar", 2); + ASSERT_ROUND_TRIP(@"foo.bar.baz", 3); + ASSERT_ROUND_TRIP(@"`.foo\\\\`", 1); + ASSERT_ROUND_TRIP(@"`.foo\\\\`.`.foo`", 2); + ASSERT_ROUND_TRIP(@"foo.`\\``.bar", 3); +} + +#undef ASSERT_ROUND_TRIP + +- (void)testCanonicalStringOfSubstring { + FSTFieldPath *path = [FSTFieldPath pathWithServerFormat:@"foo.bar.baz"]; + XCTAssertEqualObjects([path canonicalString], @"foo.bar.baz"); + + FSTFieldPath *pathTail = [path pathByRemovingFirstSegment]; + XCTAssertEqualObjects([pathTail canonicalString], @"bar.baz"); + + FSTFieldPath *pathHead = [path pathByRemovingLastSegment]; + XCTAssertEqualObjects([pathHead canonicalString], @"foo.bar"); + + XCTAssertEqualObjects([[pathTail pathByRemovingLastSegment] canonicalString], @"bar"); + XCTAssertEqualObjects([[pathHead pathByRemovingFirstSegment] canonicalString], @"bar"); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Remote/FSTDatastoreTests.m b/Firestore/Example/Tests/Remote/FSTDatastoreTests.m new file mode 100644 index 0000000..511de72 --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTDatastoreTests.m @@ -0,0 +1,58 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Firestore/FIRFirestoreErrors.h" +#import "Remote/FSTDatastore.h" + +#import <GRPCClient/GRPCCall.h> +#import <XCTest/XCTest.h> + +@interface FSTDatastoreTests : XCTestCase +@end + +@implementation FSTDatastoreTests + +- (void)testIsPermanentWriteError { + // From GRPCCall -cancel + NSError *error = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeCancelled + userInfo:@{NSLocalizedDescriptionKey : @"Canceled by app"}]; + XCTAssertFalse([FSTDatastore isPermanentWriteError:error]); + + // From GRPCCall -startNextRead + error = + [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeResourceExhausted + userInfo:@{ + NSLocalizedDescriptionKey : + @"Client does not have enough memory to hold the server response." + }]; + XCTAssertFalse([FSTDatastore isPermanentWriteError:error]); + + // From GRPCCall -startWithWriteable + error = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeUnavailable + userInfo:@{NSLocalizedDescriptionKey : @"Connectivity lost."}]; + XCTAssertFalse([FSTDatastore isPermanentWriteError:error]); + + // User info doesn't matter: + error = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeUnavailable + userInfo:nil]; + XCTAssertFalse([FSTDatastore isPermanentWriteError:error]); +} + +@end diff --git a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.m b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.m new file mode 100644 index 0000000..a172af7 --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.m @@ -0,0 +1,556 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Remote/FSTRemoteEvent.h" + +#import <XCTest/XCTest.h> + +#import "Local/FSTQueryData.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentKey.h" +#import "Remote/FSTExistenceFilter.h" +#import "Remote/FSTWatchChange.h" + +#import "FSTHelpers.h" +#import "FSTWatchChange+Testing.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTRemoteEventTests : XCTestCase +@end + +@implementation FSTRemoteEventTests { + NSData *_resumeToken1; + NSMutableDictionary<NSNumber *, NSNumber *> *_noPendingResponses; +} + +- (void)setUp { + _resumeToken1 = [@"resume1" dataUsingEncoding:NSUTF8StringEncoding]; + _noPendingResponses = [NSMutableDictionary dictionary]; +} + +- (FSTWatchChangeAggregator *)aggregatorWithTargets:(NSArray<NSNumber *> *)targets + outstanding: + (NSDictionary<NSNumber *, NSNumber *> *)outstanding + changes:(NSArray<FSTWatchChange *> *)watchChanges { + NSMutableDictionary<NSNumber *, FSTQueryData *> *listens = [NSMutableDictionary dictionary]; + FSTQueryData *dummyQueryData = [FSTQueryData alloc]; + for (NSNumber *targetID in targets) { + listens[targetID] = dummyQueryData; + } + FSTWatchChangeAggregator *aggregator = + [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:FSTTestVersion(3) + listenTargets:listens + pendingTargetResponses:outstanding]; + [aggregator addWatchChanges:watchChanges]; + return aggregator; +} + +- (void)testWillAccumulateDocumentAddedAndRemovedEvents { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @2, @3 ] + removedTargetIDs:@[ @4, @5, @6 ] + documentKey:doc1.key + document:doc1]; + + FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @4 ] + removedTargetIDs:@[ @2, @6 ] + documentKey:doc2.key + document:doc2]; + + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2, @3, @4, @5, @6 ] + outstanding:_noPendingResponses + changes:@[ change1, change2 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 2); + XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + + XCTAssertEqual(event.targetChanges.count, 6); + + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + + FSTUpdateMapping *mapping2 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[ doc2 ]]; + XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping2); + + FSTUpdateMapping *mapping3 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@3].mapping, mapping3); + + FSTUpdateMapping *mapping4 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc2 ] removedDocuments:@[ doc1 ]]; + XCTAssertEqualObjects(event.targetChanges[@4].mapping, mapping4); + + FSTUpdateMapping *mapping5 = + [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1 ]]; + XCTAssertEqualObjects(event.targetChanges[@5].mapping, mapping5); + + FSTUpdateMapping *mapping6 = + [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1, doc2 ]]; + XCTAssertEqualObjects(event.targetChanges[@6].mapping, mapping6); +} + +- (void)testWillIgnoreEventsForPendingTargets { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc1.key + document:doc1]; + + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved + targetIDs:@[ @1 ] + cause:nil]; + + FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded + targetIDs:@[ @1 ] + cause:nil]; + + FSTWatchChange *change4 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc2.key + document:doc2]; + + // We're waiting for the unwatch and watch ack + NSDictionary<NSNumber *, NSNumber *> *pendingResponses = @{ @1 : @2 }; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] + outstanding:pendingResponses + changes:@[ change1, change2, change3, change4 ]]; + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + // doc1 is ignored because it was part of an inactive target, but doc2 is in the changes + // because it become active. + XCTAssertEqual(event.documentUpdates.count, 1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + + XCTAssertEqual(event.targetChanges.count, 1); +} + +- (void)testWillIgnoreEventsForRemovedTargets { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc1.key + document:doc1]; + + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved + targetIDs:@[ @1 ] + cause:nil]; + + // We're waiting for the unwatch ack + NSDictionary<NSNumber *, NSNumber *> *pendingResponses = @{ @1 : @1 }; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[] outstanding:pendingResponses changes:@[ change1, change2 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + // doc1 is ignored because it was part of an inactive target + XCTAssertEqual(event.documentUpdates.count, 0); + + // Target 1 is ignored because it was removed + XCTAssertEqual(event.targetChanges.count, 0); +} + +- (void)testWillKeepResetMappingEvenWithUpdates { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"docs/3", 3, @{ @"value" : @3 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc1.key + document:doc1]; + // Reset stream, ignoring doc1 + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:@[ @1 ] + cause:nil]; + + // Add doc2, doc3 + FSTWatchChange *change3 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc2.key + document:doc2]; + FSTWatchChange *change4 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc3.key + document:doc3]; + + // Remove doc2 again, should not show up in reset mapping + FSTWatchChange *change5 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:@[ @1 ] + documentKey:doc2.key + document:doc2]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] + outstanding:_noPendingResponses + changes:@[ change1, change2, change3, change4, change5 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 3); + XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + XCTAssertEqualObjects(event.documentUpdates[doc3.key], doc3); + + XCTAssertEqual(event.targetChanges.count, 1); + + // Only doc3 is part of the new mapping + FSTResetMapping *expectedMapping = [FSTResetMapping mappingWithDocuments:@[ doc3 ]]; + + XCTAssertEqualObjects(event.targetChanges[@1].mapping, expectedMapping); +} + +- (void)testWillHandleSingleReset { + // Reset target + FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:@[ @1 ] + cause:nil]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 0); + + XCTAssertEqual(event.targetChanges.count, 1); + + // Reset mapping is empty + FSTResetMapping *expectedMapping = [FSTResetMapping mappingWithDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, expectedMapping); +} + +- (void)testWillHandleTargetAddAndRemovalInSameBatch { + FSTDocument *doc1a = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc1b = FSTTestDoc(@"docs/1", 1, @{ @"value" : @2 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[ @2 ] + documentKey:doc1a.key + document:doc1a]; + + FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @2 ] + removedTargetIDs:@[ @1 ] + documentKey:doc1b.key + document:doc1b]; + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2 ] + outstanding:_noPendingResponses + changes:@[ change1, change2 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 1); + XCTAssertEqualObjects(event.documentUpdates[doc1b.key], doc1b); + + XCTAssertEqual(event.targetChanges.count, 2); + + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1b ]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + + FSTUpdateMapping *mapping2 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1b ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping2); +} + +- (void)testTargetCurrentChangeWillMarkTheTargetCurrent { + FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 0); + XCTAssertEqual(event.targetChanges.count, 1); + FSTTargetChange *targetChange = event.targetChanges[@1]; + XCTAssertEqualObjects(targetChange.mapping, [[FSTUpdateMapping alloc] init]); + XCTAssertEqual(targetChange.currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(targetChange.resumeToken, _resumeToken1); +} + +- (void)testTargetAddedChangeWillResetPreviousState { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @3 ] + removedTargetIDs:@[ @2 ] + documentKey:doc1.key + document:doc1]; + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @1, @2, @3 ] + resumeToken:_resumeToken1]; + FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved + targetIDs:@[ @1 ] + cause:nil]; + FSTWatchChange *change4 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved + targetIDs:@[ @2 ] + cause:nil]; + FSTWatchChange *change5 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded + targetIDs:@[ @1 ] + cause:nil]; + FSTWatchChange *change6 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[ @3 ] + documentKey:doc2.key + document:doc2]; + + NSDictionary<NSNumber *, NSNumber *> *pendingResponses = @{ @1 : @2, @2 : @1 }; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1, @3 ] + outstanding:pendingResponses + changes:@[ change1, change2, change3, change4, change5, change6 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 2); + XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + + // target 1 and 3 are affected (1 because of re-add), target 2 is not because of remove + XCTAssertEqual(event.targetChanges.count, 2); + + // doc1 was before the remove, so it does not show up in the mapping + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc2 ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + // Current was before the remove + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateNone); + + // Doc1 was before the remove + FSTUpdateMapping *mapping3 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[ doc2 ]]; + XCTAssertEqualObjects(event.targetChanges[@3].mapping, mapping3); + // Current was before the remove + XCTAssertEqual(event.targetChanges[@3].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(event.targetChanges[@3].resumeToken, _resumeToken1); +} + +- (void)testNoChangeWillStillMarkTheAffectedTargets { + FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 0); + XCTAssertEqual(event.targetChanges.count, 1); + XCTAssertEqualObjects(event.targetChanges[@1].mapping, [[FSTUpdateMapping alloc] init]); + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateNone); + XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1); +} + +- (void)testExistenceFiltersWillReplacePreviousExistenceFilters { + FSTExistenceFilter *filter1 = [FSTExistenceFilter filterWithCount:1]; + FSTExistenceFilter *filter2 = [FSTExistenceFilter filterWithCount:2]; + FSTWatchChange *change1 = [FSTExistenceFilterWatchChange changeWithFilter:filter1 targetID:1]; + FSTWatchChange *change2 = [FSTExistenceFilterWatchChange changeWithFilter:filter1 targetID:2]; + // replace filter1 for target 2 + FSTWatchChange *change3 = [FSTExistenceFilterWatchChange changeWithFilter:filter2 targetID:2]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1, @2 ] + outstanding:_noPendingResponses + changes:@[ change1, change2, change3 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 0); + XCTAssertEqual(event.targetChanges.count, 0); + XCTAssertEqual(aggregator.existenceFilters.count, 2); + XCTAssertEqual(aggregator.existenceFilters[@1], filter1); + XCTAssertEqual(aggregator.existenceFilters[@2], filter2); +} + +- (void)testExistenceFilterMismatchResetsTarget { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc1.key + document:doc1]; + + FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc2.key + document:doc2]; + + FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] + outstanding:_noPendingResponses + changes:@[ change1, change2, change3 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 2); + XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + + XCTAssertEqual(event.targetChanges.count, 1); + + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1); + + [event handleExistenceFilterMismatchForTargetID:@1]; + + // Mapping is reset + XCTAssertEqualObjects(event.targetChanges[@1].mapping, [[FSTResetMapping alloc] init]); + // Reset the resume snapshot + XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(0)); + // Target needs to be set to not current + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkNotCurrent); + XCTAssertEqual(event.targetChanges[@1].resumeToken.length, 0); +} + +- (void)testDocumentUpdate { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDeletedDocument *deletedDoc1 = + [FSTDeletedDocument documentWithKey:doc1.key version:FSTTestVersion(3)]; + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"docs/3", 3, @{ @"value" : @3 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc1.key + document:doc1]; + + FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc2.key + document:doc2]; + + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1 ] + outstanding:_noPendingResponses + changes:@[ change1, change2 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 2); + XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + + // Update doc1 + [event addDocumentUpdate:deletedDoc1]; + [event addDocumentUpdate:doc3]; + + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 3); + // doc1 is replaced + XCTAssertEqualObjects(event.documentUpdates[doc1.key], deletedDoc1); + // doc2 is untouched + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + // doc3 is new + XCTAssertEqualObjects(event.documentUpdates[doc3.key], doc3); + + // Target is unchanged + XCTAssertEqual(event.targetChanges.count, 1); + + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); +} + +- (void)testResumeTokensHandledPerTarget { + NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding]; + FSTWatchChange *change1 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @2 ] + resumeToken:resumeToken2]; + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2 ] + outstanding:_noPendingResponses + changes:@[ change1, change2 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqual(event.targetChanges.count, 2); + + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1); + + XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping1); + XCTAssertEqualObjects(event.targetChanges[@2].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken2); +} + +- (void)testLastResumeTokenWins { + NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *resumeToken3 = [@"resume3" dataUsingEncoding:NSUTF8StringEncoding]; + + FSTWatchChange *change1 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:@[ @1 ] + resumeToken:resumeToken2]; + FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:@[ @2 ] + resumeToken:resumeToken3]; + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1, @2 ] + outstanding:_noPendingResponses + changes:@[ change1, change2, change3 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqual(event.targetChanges.count, 2); + + FSTResetMapping *mapping1 = [FSTResetMapping mappingWithDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, resumeToken2); + + XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping1); + XCTAssertEqualObjects(event.targetChanges[@2].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateNone); + XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken3); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m new file mode 100644 index 0000000..c4cf9df --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m @@ -0,0 +1,794 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Remote/FSTSerializerBeta.h" + +#import <GRPCClient/GRPCCall.h> +#import <XCTest/XCTest.h> + +#import "Core/FSTQuery.h" +#import "Core/FSTSnapshotVersion.h" +#import "Core/FSTTimestamp.h" +#import "Firestore/FIRFieldPath.h" +#import "Firestore/FIRFirestoreErrors.h" +#import "Firestore/FIRGeoPoint.h" +#import "Local/FSTQueryData.h" +#import "Model/FSTDatabaseID.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentKey.h" +#import "Model/FSTFieldValue.h" +#import "Model/FSTMutation.h" +#import "Model/FSTMutationBatch.h" +#import "Model/FSTPath.h" +#import "Protos/objc/firestore/local/MaybeDocument.pbobjc.h" +#import "Protos/objc/firestore/local/Mutation.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Common.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Document.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Query.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Write.pbobjc.h" +#import "Protos/objc/google/rpc/Status.pbobjc.h" +#import "Protos/objc/google/type/Latlng.pbobjc.h" +#import "Remote/FSTWatchChange.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTSerializerBeta (Test) +- (GCFSValue *)encodedNull; +- (GCFSValue *)encodedBool:(BOOL)value; +- (GCFSValue *)encodedDouble:(double)value; +- (GCFSValue *)encodedInteger:(int64_t)value; +- (GCFSValue *)encodedString:(NSString *)value; +- (GCFSValue *)encodedDate:(NSDate *)value; + +- (GCFSDocumentMask *)encodedFieldMask:(FSTFieldMask *)fieldMask; +- (NSMutableArray<GCFSDocumentTransform_FieldTransform *> *)encodedFieldTransforms: + (NSArray<FSTFieldTransform *> *)fieldTransforms; + +- (GCFSStructuredQuery_Filter *)encodedRelationFilter:(FSTRelationFilter *)filter; +@end + +@interface GCFSStructuredQuery_Order (Test) ++ (instancetype)messageWithProperty:(NSString *)property ascending:(BOOL)ascending; +@end + +@implementation GCFSStructuredQuery_Order (Test) + ++ (instancetype)messageWithProperty:(NSString *)property ascending:(BOOL)ascending { + GCFSStructuredQuery_Order *order = [GCFSStructuredQuery_Order message]; + order.field.fieldPath = property; + order.direction = ascending ? GCFSStructuredQuery_Direction_Ascending + : GCFSStructuredQuery_Direction_Descending; + return order; +} +@end + +@interface FSTSerializerBetaTests : XCTestCase +@property(nonatomic, strong) FSTSerializerBeta *serializer; +@end + +@implementation FSTSerializerBetaTests + +- (void)setUp { + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"]; + self.serializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseID]; +} + +- (void)testEncodesNull { + FSTFieldValue *model = [FSTNullValue nullValue]; + + GCFSValue *proto = [GCFSValue message]; + proto.nullValue = GPBNullValue_NullValue; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_NullValue]; +} + +- (void)testEncodesBool { + NSArray<NSNumber *> *examples = @[ @YES, @NO ]; + for (NSNumber *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.booleanValue = [example boolValue]; + + [self assertRoundTripForModel:model + proto:proto + type:GCFSValue_ValueType_OneOfCase_BooleanValue]; + } +} + +- (void)testEncodesIntegers { + NSArray<NSNumber *> *examples = @[ @(LLONG_MIN), @(-100), @(-1), @0, @1, @100, @(LLONG_MAX) ]; + for (NSNumber *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.integerValue = [example longLongValue]; + + [self assertRoundTripForModel:model + proto:proto + type:GCFSValue_ValueType_OneOfCase_IntegerValue]; + } +} + +- (void)testEncodesDoubles { + NSArray<NSNumber *> *examples = @[ + // normal negative numbers. + @(-INFINITY), @(-DBL_MAX), @(LLONG_MIN * 1.0 - 1.0), @(-2.0), @(-1.1), @(-1.0), @(-DBL_MIN), + + // negative smallest subnormal, zeroes, positive smallest subnormal + @(-0x1.0p-1074), @(-0.0), @(0.0), @(0x1.0p-1074), + + // and the rest + @(DBL_MIN), @0.1, @1.1, @(LLONG_MAX * 1.0), @(DBL_MAX), @(INFINITY), + + // NaN. + @(0.0 / 0.0) + ]; + for (NSNumber *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.doubleValue = [example doubleValue]; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_DoubleValue]; + } +} + +- (void)testEncodesStrings { + NSArray<NSString *> *examples = @[ + @"", + @"a", + @"abc def", + @"æ", + @"\0\ud7ff\ue000\uffff", + @"(╯°□°)╯︵ ┻━┻", + ]; + for (NSString *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.stringValue = example; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_StringValue]; + } +} + +- (void)testEncodesDates { + NSDateComponents *dateWithNanos = FSTTestDateComponents(2016, 1, 2, 10, 20, 50); + dateWithNanos.nanosecond = 500000000; + + NSArray<NSDate *> *examples = @[ + [[NSCalendar currentCalendar] dateFromComponents:dateWithNanos], + FSTTestDate(2016, 6, 17, 10, 50, 15) + ]; + + GCFSValue *timestamp1 = [GCFSValue message]; + timestamp1.timestampValue.seconds = 1451730050; + timestamp1.timestampValue.nanos = 500000000; + + GCFSValue *timestamp2 = [GCFSValue message]; + timestamp2.timestampValue.seconds = 1466160615; + timestamp2.timestampValue.nanos = 0; + NSArray<GCFSValue *> *expectedTimestamps = @[ timestamp1, timestamp2 ]; + + for (NSUInteger i = 0; i < [examples count]; i++) { + [self assertRoundTripForModel:FSTTestFieldValue(examples[i]) + proto:expectedTimestamps[i] + type:GCFSValue_ValueType_OneOfCase_TimestampValue]; + } +} + +- (void)testEncodesGeoPoints { + NSArray<FIRGeoPoint *> *examples = + @[ FSTTestGeoPoint(0, 0), FSTTestGeoPoint(1.24, 4.56), FSTTestGeoPoint(-90, 180) ]; + for (FIRGeoPoint *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.geoPointValue = [GTPLatLng message]; + proto.geoPointValue.latitude = example.latitude; + proto.geoPointValue.longitude = example.longitude; + + [self assertRoundTripForModel:model + proto:proto + type:GCFSValue_ValueType_OneOfCase_GeoPointValue]; + } +} + +- (void)testEncodesBlobs { + NSArray<NSData *> *examples = @[ + FSTTestData(-1), + FSTTestData(0, -1), + FSTTestData(0, 1, 2, -1), + FSTTestData(255, -1), + FSTTestData(0, 1, 255, -1), + ]; + for (NSData *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.bytesValue = example; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_BytesValue]; + } +} + +- (void)testEncodesResourceNames { + FSTDocumentKeyReference *reference = FSTTestRef(@"project", kDefaultDatabaseID, @"foo/bar"); + GCFSValue *proto = [GCFSValue message]; + proto.referenceValue = @"projects/project/databases/(default)/documents/foo/bar"; + + [self assertRoundTripForModel:FSTTestFieldValue(reference) + proto:proto + type:GCFSValue_ValueType_OneOfCase_ReferenceValue]; +} + +- (void)testEncodesArrays { + FSTFieldValue *model = FSTTestFieldValue(@[ @YES, @"foo" ]); + + GCFSValue *proto = [GCFSValue message]; + [proto.arrayValue.valuesArray addObjectsFromArray:@[ + [self.serializer encodedBool:YES], [self.serializer encodedString:@"foo"] + ]]; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_ArrayValue]; +} + +- (void)testEncodesEmptyMap { + FSTFieldValue *model = [FSTObjectValue objectValue]; + + GCFSValue *proto = [GCFSValue message]; + proto.mapValue = [GCFSMapValue message]; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_MapValue]; +} + +- (void)testEncodesNestedObjects { + FSTFieldValue *model = FSTTestFieldValue(@{ + @"b" : @YES, + @"d" : @(DBL_MAX), + @"i" : @1, + @"n" : [NSNull null], + @"s" : @"foo", + @"a" : @[ @2, @"bar", @{@"b" : @NO} ], + @"o" : @{ + @"d" : @100, + @"nested" : @{@"e" : @(LLONG_MIN)}, + }, + }); + + GCFSValue *innerObject = [GCFSValue message]; + innerObject.mapValue.fields[@"b"] = [self.serializer encodedBool:NO]; + + GCFSValue *middleArray = [GCFSValue message]; + [middleArray.arrayValue.valuesArray addObjectsFromArray:@[ + [self.serializer encodedInteger:2], [self.serializer encodedString:@"bar"], innerObject + ]]; + + innerObject = [GCFSValue message]; + innerObject.mapValue.fields[@"e"] = [self.serializer encodedInteger:LLONG_MIN]; + + GCFSValue *middleObject = [GCFSValue message]; + [middleObject.mapValue.fields addEntriesFromDictionary:@{ + @"d" : [self.serializer encodedInteger:100], + @"nested" : innerObject + }]; + + GCFSValue *proto = [GCFSValue message]; + [proto.mapValue.fields addEntriesFromDictionary:@{ + @"b" : [self.serializer encodedBool:YES], + @"d" : [self.serializer encodedDouble:DBL_MAX], + @"i" : [self.serializer encodedInteger:1], + @"n" : [self.serializer encodedNull], + @"s" : [self.serializer encodedString:@"foo"], + @"a" : middleArray, + @"o" : middleObject + }]; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_MapValue]; +} + +- (void)assertRoundTripForModel:(FSTFieldValue *)model + proto:(GCFSValue *)value + type:(GCFSValue_ValueType_OneOfCase)type { + GCFSValue *actualProto = [self.serializer encodedFieldValue:model]; + XCTAssertEqual(actualProto.valueTypeOneOfCase, type); + XCTAssertEqualObjects(actualProto, value); + + FSTFieldValue *actualModel = [self.serializer decodedFieldValue:value]; + XCTAssertEqualObjects(actualModel, model); +} + +- (void)testEncodesSetMutation { + FSTSetMutation *mutation = FSTTestSetMutation(@"docs/1", @{ @"a" : @"b", @"num" : @1 }); + GCFSWrite *proto = [GCFSWrite message]; + proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key]; + + [self assertRoundTripForMutation:mutation proto:proto]; +} + +- (void)testEncodesPatchMutation { + FSTPatchMutation *mutation = + FSTTestPatchMutation(@"docs/1", + @{ @"a" : @"b", + @"num" : @1, + @"some.de\\\\ep.th\\ing'" : @2 }, + nil); + GCFSWrite *proto = [GCFSWrite message]; + proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key]; + proto.updateMask = [self.serializer encodedFieldMask:mutation.fieldMask]; + proto.currentDocument.exists = YES; + + [self assertRoundTripForMutation:mutation proto:proto]; +} + +- (void)testEncodesDeleteMutation { + FSTDeleteMutation *mutation = FSTTestDeleteMutation(@"docs/1"); + GCFSWrite *proto = [GCFSWrite message]; + proto.delete_p = @"projects/p/databases/d/documents/docs/1"; + + [self assertRoundTripForMutation:mutation proto:proto]; +} + +- (void)testEncodesTransformMutation { + FSTTransformMutation *mutation = FSTTestTransformMutation(@"docs/1", @[ @"a", @"bar.baz" ]); + GCFSWrite *proto = [GCFSWrite message]; + proto.transform = [GCFSDocumentTransform message]; + proto.transform.document = [self.serializer encodedDocumentKey:mutation.key]; + proto.transform.fieldTransformsArray = + [self.serializer encodedFieldTransforms:mutation.fieldTransforms]; + proto.currentDocument.exists = YES; + + [self assertRoundTripForMutation:mutation proto:proto]; +} + +- (void)testEncodesSetMutationWithPrecondition { + FSTSetMutation *mutation = [[FSTSetMutation alloc] + initWithKey:FSTTestDocKey(@"foo/bar") + value:FSTTestObjectValue( + @{ @"a" : @"b", + @"num" : @1 }) + precondition:[FSTPrecondition preconditionWithUpdateTime:FSTTestVersion(4)]]; + GCFSWrite *proto = [GCFSWrite message]; + proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key]; + proto.currentDocument.updateTime = + [self.serializer encodedTimestamp:[[FSTTimestamp alloc] initWithSeconds:0 nanos:4000]]; + + [self assertRoundTripForMutation:mutation proto:proto]; +} + +- (void)assertRoundTripForMutation:(FSTMutation *)mutation proto:(GCFSWrite *)proto { + GCFSWrite *actualProto = [self.serializer encodedMutation:mutation]; + XCTAssertEqualObjects(actualProto, proto); + + FSTMutation *actualMutation = [self.serializer decodedMutation:proto]; + XCTAssertEqualObjects(actualMutation, mutation); +} + +- (void)testRoundTripSpecialFieldNames { + FSTMutation *set = FSTTestSetMutation(@"collection/key", @{ + @"field" : [NSString stringWithFormat:@"field %d", 1], + @"field.dot" : @2, + @"field\\slash" : @3 + }); + GCFSWrite *encoded = [self.serializer encodedMutation:set]; + FSTMutation *decoded = [self.serializer decodedMutation:encoded]; + XCTAssertEqualObjects(set, decoded); +} + +- (void)testEncodesListenRequestLabels { + FSTQuery *query = FSTTestQuery(@"collection/key"); + FSTQueryData *queryData = + [[FSTQueryData alloc] initWithQuery:query targetID:2 purpose:FSTQueryPurposeListen]; + + NSDictionary<NSString *, NSString *> *result = + [self.serializer encodedListenRequestLabelsForQueryData:queryData]; + XCTAssertNil(result); + + queryData = + [[FSTQueryData alloc] initWithQuery:query targetID:2 purpose:FSTQueryPurposeLimboResolution]; + result = [self.serializer encodedListenRequestLabelsForQueryData:queryData]; + XCTAssertEqualObjects(result, @{@"goog-listen-tags" : @"limbo-document"}); + + queryData = [[FSTQueryData alloc] initWithQuery:query + targetID:2 + purpose:FSTQueryPurposeExistenceFilterMismatch]; + result = [self.serializer encodedListenRequestLabelsForQueryData:queryData]; + XCTAssertEqualObjects(result, @{@"goog-listen-tags" : @"existence-filter-mismatch"}); +} + +- (void)testEncodesRelationFilter { + FSTRelationFilter *input = FSTTestFilter(@"item.part.top", @"==", @"food"); + GCFSStructuredQuery_Filter *actual = [self.serializer encodedRelationFilter:input]; + + GCFSStructuredQuery_Filter *expected = [GCFSStructuredQuery_Filter message]; + GCFSStructuredQuery_FieldFilter *prop = expected.fieldFilter; + prop.field.fieldPath = @"item.part.top"; + prop.op = GCFSStructuredQuery_FieldFilter_Operator_Equal; + prop.value.stringValue = @"food"; + XCTAssertEqualObjects(actual, expected); +} + +#pragma mark - encodedQuery + +- (void)testEncodesFirstLevelKeyQueries { + FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"docs/1")]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + [expected.documents.documentsArray addObject:@"projects/p/databases/d/documents/docs/1"]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesFirstLevelAncestorQueries { + FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"messages")]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"messages"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesNestedAncestorQueries { + FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"rooms/1/messages/10/attachments")]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"attachments"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesSingleFiltersAtFirstLevelCollections { + FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")] + queryByAddingFilter:FSTTestFilter(@"prop", @"<", @(42))]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"docs"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + + GCFSStructuredQuery_FieldFilter *filter = expected.query.structuredQuery.where.fieldFilter; + filter.field.fieldPath = @"prop"; + filter.op = GCFSStructuredQuery_FieldFilter_Operator_LessThan; + filter.value.integerValue = 42; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesMultipleFiltersOnDeeperCollections { + FSTQuery *q = [[[FSTQuery queryWithPath:FSTTestPath(@"rooms/1/messages/10/attachments")] + queryByAddingFilter:FSTTestFilter(@"prop", @">=", @(42))] + queryByAddingFilter:FSTTestFilter(@"author", @"==", @"dimond")]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"attachments"; + [expected.query.structuredQuery.fromArray addObject:from]; + + GCFSStructuredQuery_Filter *filter1 = [GCFSStructuredQuery_Filter message]; + GCFSStructuredQuery_FieldFilter *field1 = filter1.fieldFilter; + field1.field.fieldPath = @"prop"; + field1.op = GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual; + field1.value.integerValue = 42; + + GCFSStructuredQuery_Filter *filter2 = [GCFSStructuredQuery_Filter message]; + GCFSStructuredQuery_FieldFilter *field2 = filter2.fieldFilter; + field2.field.fieldPath = @"author"; + field2.op = GCFSStructuredQuery_FieldFilter_Operator_Equal; + field2.value.stringValue = @"dimond"; + + GCFSStructuredQuery_CompositeFilter *composite = + expected.query.structuredQuery.where.compositeFilter; + composite.op = GCFSStructuredQuery_CompositeFilter_Operator_And; + [composite.filtersArray addObject:filter1]; + [composite.filtersArray addObject:filter2]; + + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesNullFilter { + [self unaryFilterTestWithValue:[NSNull null] + expectedUnaryOperator:GCFSStructuredQuery_UnaryFilter_Operator_IsNull]; +} + +- (void)testEncodesNanFilter { + [self unaryFilterTestWithValue:@(NAN) + expectedUnaryOperator:GCFSStructuredQuery_UnaryFilter_Operator_IsNan]; +} + +- (void)unaryFilterTestWithValue:(id)value + expectedUnaryOperator:(GCFSStructuredQuery_UnaryFilter_Operator) + operator{ + FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")] + queryByAddingFilter:FSTTestFilter(@"prop", @"==", value)]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"docs"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + + GCFSStructuredQuery_UnaryFilter *filter = expected.query.structuredQuery.where.unaryFilter; + filter.field.fieldPath = @"prop"; + filter.op = operator; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesSortOrders { + FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")] + queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"prop") + ascending:YES]]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"docs"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesSortOrdersDescending { + FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"rooms/1/messages/10/attachments")] + queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"prop") + ascending:NO]]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"attachments"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:NO]]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:NO]]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesLimits { + FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")] queryBySettingLimit:26]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"docs"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.query.structuredQuery.limit.value = 26; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesResumeTokens { + FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"docs")]; + FSTQueryData *model = [[FSTQueryData alloc] initWithQuery:q + targetID:1 + purpose:FSTQueryPurposeListen + snapshotVersion:[FSTSnapshotVersion noVersion] + resumeToken:FSTTestData(1, 2, 3, -1)]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"docs"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.targetId = 1; + expected.resumeToken = FSTTestData(1, 2, 3, -1); + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (FSTQueryData *)queryDataForQuery:(FSTQuery *)query { + return [[FSTQueryData alloc] initWithQuery:query + targetID:1 + purpose:FSTQueryPurposeListen + snapshotVersion:[FSTSnapshotVersion noVersion] + resumeToken:[NSData data]]; +} + +- (void)assertRoundTripForQueryData:(FSTQueryData *)queryData proto:(GCFSTarget *)proto { + // Verify that the encoded FSTQueryData matches the target. + GCFSTarget *actualProto = [self.serializer encodedTarget:queryData]; + XCTAssertEqualObjects(actualProto, proto); + + // We don't have deserialization logic for full targets since they're not used for RPC + // interaction, but the query deserialization only *is* used for the local store. + FSTQuery *actualModel; + if (proto.targetTypeOneOfCase == GCFSTarget_TargetType_OneOfCase_Query) { + actualModel = [self.serializer decodedQueryFromQueryTarget:proto.query]; + } else { + actualModel = [self.serializer decodedQueryFromDocumentsTarget:proto.documents]; + } + XCTAssertEqualObjects(actualModel, queryData.query); +} + +- (void)testConvertsTargetChangeWithAdded { + FSTWatchChange *expected = + [[FSTWatchTargetChange alloc] initWithState:FSTWatchTargetChangeStateAdded + targetIDs:@[ @1, @4 ] + resumeToken:[NSData data] + cause:nil]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_Add; + [listenResponse.targetChange.targetIdsArray addValue:1]; + [listenResponse.targetChange.targetIdsArray addValue:4]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsTargetChangeWithRemoved { + FSTWatchChange *expected = [[FSTWatchTargetChange alloc] + initWithState:FSTWatchTargetChangeStateRemoved + targetIDs:@[ @1, @4 ] + resumeToken:FSTTestData(0, 1, 2, -1) + cause:[NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodePermissionDenied + userInfo:@{ + NSLocalizedDescriptionKey : @"Error message", + }]]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_Remove; + listenResponse.targetChange.cause.code = FIRFirestoreErrorCodePermissionDenied; + listenResponse.targetChange.cause.message = @"Error message"; + listenResponse.targetChange.resumeToken = FSTTestData(0, 1, 2, -1); + [listenResponse.targetChange.targetIdsArray addValue:1]; + [listenResponse.targetChange.targetIdsArray addValue:4]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsTargetChangeWithNoChange { + FSTWatchChange *expected = + [[FSTWatchTargetChange alloc] initWithState:FSTWatchTargetChangeStateNoChange + targetIDs:@[ @1, @4 ] + resumeToken:[NSData data] + cause:nil]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_NoChange; + [listenResponse.targetChange.targetIdsArray addValue:1]; + [listenResponse.targetChange.targetIdsArray addValue:4]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsDocumentChangeWithTargetIds { + FSTWatchChange *expected = [[FSTDocumentWatchChange alloc] + initWithUpdatedTargetIDs:@[ @1, @2 ] + removedTargetIDs:@[] + documentKey:FSTTestDocKey(@"coll/1") + document:FSTTestDoc(@"coll/1", 5, @{@"foo" : @"bar"}, NO)]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.documentChange.document.name = @"projects/p/databases/d/documents/coll/1"; + listenResponse.documentChange.document.updateTime.nanos = 5000; + GCFSValue *fooValue = [GCFSValue message]; + fooValue.stringValue = @"bar"; + [listenResponse.documentChange.document.fields setObject:fooValue forKey:@"foo"]; + [listenResponse.documentChange.targetIdsArray addValue:1]; + [listenResponse.documentChange.targetIdsArray addValue:2]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsDocumentChangeWithRemovedTargetIds { + FSTWatchChange *expected = [[FSTDocumentWatchChange alloc] + initWithUpdatedTargetIDs:@[ @2 ] + removedTargetIDs:@[ @1 ] + documentKey:FSTTestDocKey(@"coll/1") + document:FSTTestDoc(@"coll/1", 5, @{@"foo" : @"bar"}, NO)]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.documentChange.document.name = @"projects/p/databases/d/documents/coll/1"; + listenResponse.documentChange.document.updateTime.nanos = 5000; + GCFSValue *fooValue = [GCFSValue message]; + fooValue.stringValue = @"bar"; + [listenResponse.documentChange.document.fields setObject:fooValue forKey:@"foo"]; + [listenResponse.documentChange.removedTargetIdsArray addValue:1]; + [listenResponse.documentChange.targetIdsArray addValue:2]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsDocumentChangeWithDeletions { + FSTWatchChange *expected = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:@[ @1, @2 ] + documentKey:FSTTestDocKey(@"coll/1") + document:FSTTestDeletedDoc(@"coll/1", 5)]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.documentDelete.document = @"projects/p/databases/d/documents/coll/1"; + listenResponse.documentDelete.readTime.nanos = 5000; + [listenResponse.documentDelete.removedTargetIdsArray addValue:1]; + [listenResponse.documentDelete.removedTargetIdsArray addValue:2]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsDocumentChangeWithRemoves { + FSTWatchChange *expected = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:@[ @1, @2 ] + documentKey:FSTTestDocKey(@"coll/1") + document:nil]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.documentRemove.document = @"projects/p/databases/d/documents/coll/1"; + [listenResponse.documentRemove.removedTargetIdsArray addValue:1]; + [listenResponse.documentRemove.removedTargetIdsArray addValue:2]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Remote/FSTStreamTests.m b/Firestore/Example/Tests/Remote/FSTStreamTests.m new file mode 100644 index 0000000..f27b200 --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTStreamTests.m @@ -0,0 +1,139 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Remote/FSTDatastore.h" + +#import <OCMock/OCMock.h> +#import <XCTest/XCTest.h> + +#import "Auth/FSTEmptyCredentialsProvider.h" +#import "Core/FSTDatabaseInfo.h" +#import "Model/FSTDatabaseID.h" +#import "Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.h" +#import "Util/FSTDispatchQueue.h" + +/** Expose otherwise private methods for testing. */ +@interface FSTStream (Testing) + +- (void)writesFinishedWithError:(NSError *_Nullable)error; + +@end + +@interface FSTStreamTests : XCTestCase +@end + +@implementation FSTStreamTests { + dispatch_queue_t _testQueue; + FSTDatabaseInfo *_databaseInfo; + FSTDispatchQueue *_workerDispatchQueue; + id<FSTCredentialsProvider> _credentials; +} + +- (void)setUp { + [super setUp]; + + FSTDatabaseID *databaseID = + [FSTDatabaseID databaseIDWithProject:@"project" database:kDefaultDatabaseID]; + _databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID + persistenceKey:@"test" + host:@"test-host" + sslEnabled:NO]; + + _testQueue = dispatch_queue_create("com.firebase.testing", DISPATCH_QUEUE_SERIAL); + _workerDispatchQueue = [FSTDispatchQueue queueWith:_testQueue]; + _credentials = [[FSTEmptyCredentialsProvider alloc] init]; +} + +- (void)tearDown { + [super tearDown]; +} + +- (void)testWatchStreamStop { + id delegate = OCMStrictProtocolMock(@protocol(FSTWatchStreamDelegate)); + + FSTWatchStream *stream = + OCMPartialMock([[FSTWatchStream alloc] initWithDatabase:_databaseInfo + workerDispatchQueue:_workerDispatchQueue + credentials:_credentials + responseMessageClass:[GCFSWriteResponse class] + delegate:delegate]); + OCMStub([stream createRPCWithRequestsWriter:[OCMArg any]]).andReturn(nil); + + // Start the stream up but that's not really the interesting bit. This is complicated by the fact + // that startup involves redispatching after credentials are returned. + dispatch_semaphore_t openCompleted = dispatch_semaphore_create(0); + OCMStub([delegate watchStreamDidOpen]).andDo(^(NSInvocation *invocation) { + dispatch_semaphore_signal(openCompleted); + }); + dispatch_async(_testQueue, ^{ + [stream start]; + }); + dispatch_semaphore_wait(openCompleted, DISPATCH_TIME_FOREVER); + OCMVerifyAll(delegate); + + // Stop must not call watchStreamDidClose because the full implementation of the delegate could + // attempt to restart the stream in the event it had pending watches. + dispatch_sync(_testQueue, ^{ + [stream stop]; + }); + OCMVerifyAll(delegate); + + // Simulate a final callback from GRPC + [stream writesFinishedWithError:nil]; + // Drain queue + dispatch_sync(_testQueue, ^{ + }); + OCMVerifyAll(delegate); +} + +- (void)testWriteStreamStop { + id delegate = OCMStrictProtocolMock(@protocol(FSTWriteStreamDelegate)); + + FSTWriteStream *stream = + OCMPartialMock([[FSTWriteStream alloc] initWithDatabase:_databaseInfo + workerDispatchQueue:_workerDispatchQueue + credentials:_credentials + responseMessageClass:[GCFSWriteResponse class] + delegate:delegate]); + OCMStub([stream createRPCWithRequestsWriter:[OCMArg any]]).andReturn(nil); + + // Start the stream up but that's not really the interesting bit. + dispatch_semaphore_t openCompleted = dispatch_semaphore_create(0); + OCMStub([delegate writeStreamDidOpen]).andDo(^(NSInvocation *invocation) { + dispatch_semaphore_signal(openCompleted); + }); + dispatch_async(_testQueue, ^{ + [stream start]; + }); + dispatch_semaphore_wait(openCompleted, DISPATCH_TIME_FOREVER); + OCMVerifyAll(delegate); + + // Stop must not call writeStreamDidClose because the full implementation of this delegate could + // attempt to restart the stream in the event it had pending writes. + dispatch_sync(_testQueue, ^{ + [stream stop]; + }); + OCMVerifyAll(delegate); + + // Simulate a final callback from GRPC + [stream writesFinishedWithError:nil]; + // Drain queue + dispatch_sync(_testQueue, ^{ + }); + OCMVerifyAll(delegate); +} + +@end diff --git a/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h b/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h new file mode 100644 index 0000000..f94fe05 --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h @@ -0,0 +1,40 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +#import "Core/FSTTypes.h" +#import "Remote/FSTWatchChange.h" + +NS_ASSUME_NONNULL_BEGIN + +/** FSTWatchTargetChange is a change to a watch target. */ +@interface FSTWatchTargetChange (Testing) + ++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray<NSNumber *> *)targetIDs; + ++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray<NSNumber *> *)targetIDs + cause:(nullable NSError *)cause; + ++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray<NSNumber *> *)targetIDs + resumeToken:(nullable NSData *)resumeToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.m b/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.m new file mode 100644 index 0000000..cb5e479 --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.m @@ -0,0 +1,54 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FSTWatchChange+Testing.h" + +#import "Model/FSTDocument.h" +#import "Remote/FSTWatchChange.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTWatchTargetChange (Testing) + ++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray<NSNumber *> *)targetIDs { + return [[FSTWatchTargetChange alloc] initWithState:state + targetIDs:targetIDs + resumeToken:[NSData data] + cause:nil]; +} + ++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray<NSNumber *> *)targetIDs + cause:(nullable NSError *)cause { + return [[FSTWatchTargetChange alloc] initWithState:state + targetIDs:targetIDs + resumeToken:[NSData data] + cause:cause]; +} + ++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray<NSNumber *> *)targetIDs + resumeToken:(nullable NSData *)resumeToken { + return [[FSTWatchTargetChange alloc] initWithState:state + targetIDs:targetIDs + resumeToken:resumeToken + cause:nil]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Remote/FSTWatchChangeTests.m b/Firestore/Example/Tests/Remote/FSTWatchChangeTests.m new file mode 100644 index 0000000..ccbd644 --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTWatchChangeTests.m @@ -0,0 +1,66 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Remote/FSTWatchChange.h" + +#import <XCTest/XCTest.h> + +#import "Model/FSTDocument.h" +#import "Remote/FSTExistenceFilter.h" + +#import "FSTHelpers.h" +#import "FSTWatchChange+Testing.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTWatchChangeTests : XCTestCase +@end + +@implementation FSTWatchChangeTests + +- (void)testDocumentChange { + FSTMaybeDocument *doc = FSTTestDoc(@"a/b", 1, @{}, NO); + FSTDocumentWatchChange *change = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @2, @3 ] + removedTargetIDs:@[ @4, @5 ] + documentKey:doc.key + document:doc]; + XCTAssertEqual(change.updatedTargetIDs.count, 3); + XCTAssertEqual(change.removedTargetIDs.count, 2); + // Testing object identity here is fine. + XCTAssertEqual(change.document, doc); +} + +- (void)testExistenceFilterChange { + FSTExistenceFilter *filter = [FSTExistenceFilter filterWithCount:7]; + FSTExistenceFilterWatchChange *change = + [FSTExistenceFilterWatchChange changeWithFilter:filter targetID:5]; + XCTAssertEqual(change.filter.count, 7); + XCTAssertEqual(change.targetID, 5); +} + +- (void)testWatchTargetChange { + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:@[ @1, @2 ] + cause:nil]; + XCTAssertEqual(change.state, FSTWatchTargetChangeStateReset); + XCTAssertEqual(change.targetIDs.count, 2); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.m b/Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.m new file mode 100644 index 0000000..88b3f12 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.m @@ -0,0 +1,43 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FSTSpecTests.h" + +#import "Local/FSTLevelDB.h" + +#import "FSTPersistenceTestHelpers.h" +#import "FSTSyncEngineTestDriver.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * An implementation of FSTSpecTests that uses the LevelDB implementation of local storage. + * + * See the FSTSpecTests class comments for more information about how this works. + */ +@interface FSTLevelDBSpecTests : FSTSpecTests +@end + +@implementation FSTLevelDBSpecTests + +/** Overrides -[FSTSpecTests persistence] */ +- (id<FSTPersistence>)persistence { + return [FSTPersistenceTestHelpers levelDBPersistence]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.m b/Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.m new file mode 100644 index 0000000..9cf1f39 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.m @@ -0,0 +1,42 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FSTSpecTests.h" + +#import "Local/FSTMemoryPersistence.h" + +#import "FSTPersistenceTestHelpers.h" +#import "FSTSyncEngineTestDriver.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * An implementation of FSTSpecTests that uses the memory-only implementation of local storage. + * + * @see the FSTSpecTests class comments for more information about how this works. + */ +@interface FSTMemorySpecTests : FSTSpecTests +@end + +@implementation FSTMemorySpecTests + +/** Overrides -[FSTSpecTests persistence] */ +- (id<FSTPersistence>)persistence { + return [FSTPersistenceTestHelpers memoryPersistence]; +} +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h new file mode 100644 index 0000000..4ff1220 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h @@ -0,0 +1,68 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +#import "Remote/FSTDatastore.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTMockDatastore : FSTDatastore + ++ (instancetype)mockDatastoreWithWorkerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue; + +#pragma mark - Watch Stream manipulation. + +/** Injects an Added WatchChange containing the given targetIDs. */ +- (void)writeWatchTargetAddedWithTargetIDs:(NSArray<FSTBoxedTargetID *> *)targetIDs; + +/** Injects an Added WatchChange that marks the given targetIDs current. */ +- (void)writeWatchCurrentWithTargetIDs:(NSArray<FSTBoxedTargetID *> *)targetIDs + snapshotVersion:(FSTSnapshotVersion *)snapshotVersion + resumeToken:(NSData *)resumeToken; + +/** Injects a WatchChange as though it had come from the backend. */ +- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snap; + +/** Injects a stream failure as though it had come from the backend. */ +- (void)failWatchStreamWithError:(NSError *)error; + +/** Returns the set of active targets on the watch stream. */ +- (NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)activeTargets; + +/** Helper method to expose watch stream state to verify in tests. */ +- (BOOL)isWatchStreamOpen; + +#pragma mark - Write Stream manipulation. + +/** + * Returns the next write that was "sent to the backend", failing if there are no queued sent + */ +- (NSArray<FSTMutation *> *)nextSentWrite; + +/** Returns the number of writes that have been sent to the backend but not waited on yet. */ +- (int)writesSent; + +/** Injects a write ack as though it had come from the backend in response to a write. */ +- (void)ackWriteWithVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray<FSTMutationResult *> *)results; + +/** Injects a stream failure as though it had come from the backend. */ +- (void)failWriteWithError:(NSError *_Nullable)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.m b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.m new file mode 100644 index 0000000..1a1f659 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.m @@ -0,0 +1,344 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FSTMockDatastore.h" + +#import "Auth/FSTEmptyCredentialsProvider.h" +#import "Core/FSTDatabaseInfo.h" +#import "Core/FSTSnapshotVersion.h" +#import "Local/FSTQueryData.h" +#import "Model/FSTDatabaseID.h" +#import "Model/FSTMutation.h" +#import "Util/FSTAssert.h" +#import "Util/FSTLogger.h" + +#import "FSTWatchChange+Testing.h" + +@class GRPCProtoCall; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTMockWatchStream + +@interface FSTMockWatchStream : FSTWatchStream + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials + delegate:(id<FSTWatchStreamDelegate>)delegate NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials + responseMessageClass:(Class)responseMessageClass + delegate:(id<FSTWatchStreamDelegate>)delegate NS_UNAVAILABLE; + +@property(nonatomic, assign) BOOL open; + +@property(nonatomic, strong, readonly) + NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *activeTargets; + +@end + +@implementation FSTMockWatchStream + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials + delegate:(id<FSTWatchStreamDelegate>)delegate { + self = [super initWithDatabase:database + workerDispatchQueue:workerDispatchQueue + credentials:credentials + responseMessageClass:[FSTWatchChange class] + delegate:delegate]; + if (self) { + FSTAssert(database, @"Database must not be nil"); + _activeTargets = [NSMutableDictionary dictionary]; + } + return self; +} + +#pragma mark - Overridden FSTWatchStream methods. + +- (void)start { + FSTAssert(!self.open, @"Trying to start already started watch stream"); + self.open = YES; + [self handleStreamOpen]; +} + +- (BOOL)isOpen { + return self.open; +} + +- (BOOL)isStarted { + return self.open; +} + +- (void)handleStreamOpen { + [self.delegate watchStreamDidOpen]; +} + +- (void)watchQuery:(FSTQueryData *)query { + FSTLog(@"watchQuery: %d: %@", query.targetID, query.query); + // Snapshot version is ignored on the wire + FSTQueryData *sentQueryData = + [query queryDataByReplacingSnapshotVersion:[FSTSnapshotVersion noVersion] + resumeToken:query.resumeToken]; + self.activeTargets[@(query.targetID)] = sentQueryData; +} + +- (void)unwatchTargetID:(FSTTargetID)targetID { + FSTLog(@"unwatchTargetID: %d", targetID); + [self.activeTargets removeObjectForKey:@(targetID)]; +} + +- (void)failStreamWithError:(NSError *)error { + self.open = NO; + [self.delegate watchStreamDidClose:error]; +} + +#pragma mark - Helper methods. + +- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snap { + if ([change isKindOfClass:[FSTWatchTargetChange class]]) { + FSTWatchTargetChange *targetChange = (FSTWatchTargetChange *)change; + if (targetChange.cause) { + for (NSNumber *targetID in targetChange.targetIDs) { + if (!self.activeTargets[targetID]) { + // Technically removing an unknown target is valid (e.g. it could race with a + // server-side removal), but we want to pay extra careful attention in tests + // that we only remove targets we listened too. + FSTFail(@"Removing a non-active target"); + } + [self.activeTargets removeObjectForKey:targetID]; + } + } + } + [self.delegate watchStreamDidChange:change snapshotVersion:snap]; +} + +@end + +#pragma mark - FSTMockWriteStream + +@interface FSTMockWriteStream : FSTWriteStream + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials + delegate:(id<FSTWriteStreamDelegate>)delegate NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials + responseMessageClass:(Class)responseMessageClass + delegate:(id<FSTWatchStreamDelegate>)delegate NS_UNAVAILABLE; + +@property(nonatomic, assign) BOOL open; +@property(nonatomic, strong, readonly) NSMutableArray<NSArray<FSTMutation *> *> *sentMutations; + +@end + +@implementation FSTMockWriteStream + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials + delegate:(id<FSTWriteStreamDelegate>)delegate { + self = [super initWithDatabase:database + workerDispatchQueue:workerDispatchQueue + credentials:credentials + responseMessageClass:[FSTMutationResult class] + delegate:delegate]; + if (self) { + _sentMutations = [NSMutableArray array]; + } + return self; +} + +#pragma mark - Overridden FSTWriteStream methods. + +- (void)start { + FSTAssert(!self.open, @"Trying to start already started write stream"); + self.open = YES; + [self.sentMutations removeAllObjects]; + [self handleStreamOpen]; +} + +- (BOOL)isOpen { + return self.open; +} + +- (BOOL)isStarted { + return self.open; +} + +- (void)writeHandshake { + self.handshakeComplete = YES; + [self.delegate writeStreamDidCompleteHandshake]; +} + +- (void)writeMutations:(NSArray<FSTMutation *> *)mutations { + [self.sentMutations addObject:mutations]; +} + +- (void)handleStreamOpen { + [self.delegate writeStreamDidOpen]; +} + +#pragma mark - Helper methods. + +/** Injects a write ack as though it had come from the backend in response to a write. */ +- (void)ackWriteWithVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray<FSTMutationResult *> *)results { + [self.delegate writeStreamDidReceiveResponseWithVersion:commitVersion mutationResults:results]; +} + +/** Injects a failed write response as though it had come from the backend. */ +- (void)failStreamWithError:(NSError *)error { + self.open = NO; + [self.delegate writeStreamDidClose:error]; +} + +/** + * Returns the next write that was "sent to the backend", failing if there are no queued sent + */ +- (NSArray<FSTMutation *> *)nextSentWrite { + FSTAssert(self.sentMutations.count > 0, + @"Writes need to happen before you can call nextSentWrite."); + NSArray<FSTMutation *> *result = [self.sentMutations objectAtIndex:0]; + [self.sentMutations removeObjectAtIndex:0]; + return result; +} + +/** + * Returns the number of mutations that have been sent to the backend but not retrieved via + * nextSentWrite yet. + */ +- (int)sentMutationsCount { + return (int)self.sentMutations.count; +} + +@end + +#pragma mark - FSTMockDatastore + +@interface FSTMockDatastore () +@property(nonatomic, strong, nullable) FSTMockWatchStream *watchStream; +@property(nonatomic, strong, nullable) FSTMockWriteStream *writeStream; + +/** Properties implemented in FSTDatastore that are nonpublic. */ +@property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue; +@property(nonatomic, strong, readonly) id<FSTCredentialsProvider> credentials; + +@end + +@implementation FSTMockDatastore + ++ (instancetype)mockDatastoreWithWorkerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue { + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"project" database:@"database"]; + FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID + persistenceKey:@"persistence" + host:@"host" + sslEnabled:NO]; + + FSTEmptyCredentialsProvider *credentials = [[FSTEmptyCredentialsProvider alloc] init]; + + return [[FSTMockDatastore alloc] initWithDatabaseInfo:databaseInfo + workerDispatchQueue:workerDispatchQueue + credentials:credentials]; +} + +#pragma mark - Overridden FSTDatastore methods. + +- (FSTWatchStream *)createWatchStreamWithDelegate:(id<FSTWatchStreamDelegate>)delegate { + FSTAssert(self.databaseInfo, @"DatabaseInfo must not be nil"); + self.watchStream = [[FSTMockWatchStream alloc] initWithDatabase:self.databaseInfo + workerDispatchQueue:self.workerDispatchQueue + credentials:self.credentials + delegate:delegate]; + return self.watchStream; +} + +- (FSTWriteStream *)createWriteStreamWithDelegate:(id<FSTWriteStreamDelegate>)delegate { + FSTAssert(self.databaseInfo, @"DatabaseInfo must not be nil"); + self.writeStream = [[FSTMockWriteStream alloc] initWithDatabase:self.databaseInfo + workerDispatchQueue:self.workerDispatchQueue + credentials:self.credentials + delegate:delegate]; + return self.writeStream; +} + +- (void)authorizeAndStartRPC:(GRPCProtoCall *)rpc completion:(FSTVoidErrorBlock)completion { + FSTFail(@"FSTMockDatastore shouldn't be starting any RPCs."); +} + +#pragma mark - Method exposed for tests to call. + +- (NSArray<FSTMutation *> *)nextSentWrite { + return [self.writeStream nextSentWrite]; +} + +- (int)writesSent { + return [self.writeStream sentMutationsCount]; +} + +- (void)ackWriteWithVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray<FSTMutationResult *> *)results { + [self.writeStream ackWriteWithVersion:commitVersion mutationResults:results]; +} + +- (void)failWriteWithError:(NSError *_Nullable)error { + [self.writeStream failStreamWithError:error]; +} + +- (void)writeWatchTargetAddedWithTargetIDs:(NSArray<FSTBoxedTargetID *> *)targetIDs { + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded + targetIDs:targetIDs + cause:nil]; + [self writeWatchChange:change snapshotVersion:[FSTSnapshotVersion noVersion]]; +} + +- (void)writeWatchCurrentWithTargetIDs:(NSArray<FSTBoxedTargetID *> *)targetIDs + snapshotVersion:(FSTSnapshotVersion *)snapshotVersion + resumeToken:(NSData *)resumeToken { + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:targetIDs + resumeToken:resumeToken]; + [self writeWatchChange:change snapshotVersion:snapshotVersion]; +} + +- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snap { + [self.watchStream writeWatchChange:change snapshotVersion:snap]; +} + +- (void)failWatchStreamWithError:(NSError *)error { + [self.watchStream failStreamWithError:error]; +} + +- (NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)activeTargets { + return [self.watchStream.activeTargets copy]; +} + +- (BOOL)isWatchStreamOpen { + return self.watchStream.isOpen; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.h b/Firestore/Example/Tests/SpecTests/FSTSpecTests.h new file mode 100644 index 0000000..3a3dbb2 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/FSTSpecTests.h @@ -0,0 +1,46 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> +#import <XCTest/XCTest.h> + +@protocol FSTPersistence; + +NS_ASSUME_NONNULL_BEGIN + +/** + * FSTSpecTests run a set of portable event specifications from JSON spec files against a + * special isolated version of the Firestore client that allows precise control over when events + * are delivered. This allows us to test client behavior in a very reliable, deterministic way, + * including edge cases that would be difficult to reliably reproduce in a full integration test. + * + * Both events from user code (adding/removing listens, performing mutations) and events from the + * Datastore are simulated, while installing as much of the system in between as possible. + * + * FSTSpecTests is an abstract base class that must be subclassed to test against a specific local + * store implementation. To create a new variant of FSTSpecTests: + * + * + Subclass FSTSpecTests + * + override -persistence to create and return an appropriate id<FSTPersistence> implementation. + */ +@interface FSTSpecTests : XCTestCase + +/** Creates and returns an appropriate id<FSTPersistence> implementation. */ +- (id<FSTPersistence>)persistence; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.m b/Firestore/Example/Tests/SpecTests/FSTSpecTests.m new file mode 100644 index 0000000..f681347 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/FSTSpecTests.m @@ -0,0 +1,642 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FSTSpecTests.h" + +#import <GRPCClient/GRPCCall.h> + +#import "Auth/FSTUser.h" +#import "Core/FSTEventManager.h" +#import "Core/FSTQuery.h" +#import "Core/FSTSnapshotVersion.h" +#import "Core/FSTViewSnapshot.h" +#import "Firestore/FIRFirestoreErrors.h" +#import "Local/FSTEagerGarbageCollector.h" +#import "Local/FSTNoOpGarbageCollector.h" +#import "Local/FSTPersistence.h" +#import "Local/FSTQueryData.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentKey.h" +#import "Model/FSTFieldValue.h" +#import "Model/FSTMutation.h" +#import "Model/FSTPath.h" +#import "Remote/FSTExistenceFilter.h" +#import "Remote/FSTWatchChange.h" +#import "Util/FSTAssert.h" +#import "Util/FSTClasses.h" +#import "Util/FSTLogger.h" + +#import "FSTHelpers.h" +#import "FSTSyncEngineTestDriver.h" +#import "FSTWatchChange+Testing.h" + +NS_ASSUME_NONNULL_BEGIN + +// Disables all other tests; useful for debugging. Multiple tests can have this tag and they'll all +// be run (but all others won't). +static NSString *const kExclusiveTag = @"exclusive"; + +// A tag for tests that should be excluded from execution (on iOS), useful to allow the platforms +// to temporarily diverge. +static NSString *const kNoIOSTag = @"no-ios"; + +@interface FSTSpecTests () +@property(nonatomic, strong) FSTSyncEngineTestDriver *driver; + +// Some config info for the currently running spec; used when restarting the driver (for doRestart). +@property(nonatomic, assign) BOOL GCEnabled; +@property(nonatomic, strong) id<FSTPersistence> driverPersistence; +@end + +@implementation FSTSpecTests + +- (id<FSTPersistence>)persistence { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (void)setUpForSpecWithConfig:(NSDictionary *)config { + // Store persistence / GCEnabled so we can re-use it in doRestart. + self.driverPersistence = [self persistence]; + NSNumber *GCEnabled = config[@"useGarbageCollection"]; + self.GCEnabled = [GCEnabled boolValue]; + self.driver = [[FSTSyncEngineTestDriver alloc] initWithPersistence:self.driverPersistence + garbageCollector:self.garbageCollector]; + [self.driver start]; +} + +- (void)tearDownForSpec { + [self.driver shutdown]; + [self.driverPersistence shutdown]; +} + +/** + * Creates the appropriate garbage collector for the test configuration: an eager collector if + * GC is enabled or a no-op collector otherwise. + */ +- (id<FSTGarbageCollector>)garbageCollector { + return self.GCEnabled ? [[FSTEagerGarbageCollector alloc] init] + : [[FSTNoOpGarbageCollector alloc] init]; +} + +/** + * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for + * FSTSpecTests since it is incomplete without the implementations supplied by its subclasses. + */ +- (BOOL)isTestBaseClass { + return [self class] == [FSTSpecTests class]; +} + +#pragma mark - Methods for constructing objects from specs. + +- (nullable FSTQuery *)parseQuery:(id)querySpec { + if ([querySpec isKindOfClass:[NSString class]]) { + return [FSTQuery queryWithPath:[FSTResourcePath pathWithString:querySpec]]; + } else if ([querySpec isKindOfClass:[NSDictionary class]]) { + NSDictionary *queryDict = (NSDictionary *)querySpec; + NSString *path = queryDict[@"path"]; + __block FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithString:path]]; + if (queryDict[@"limit"]) { + NSNumber *limit = queryDict[@"limit"]; + query = [query queryBySettingLimit:limit.integerValue]; + } + if (queryDict[@"filters"]) { + NSArray *filters = queryDict[@"filters"]; + [filters enumerateObjectsUsingBlock:^(NSArray *_Nonnull filter, NSUInteger idx, + BOOL *_Nonnull stop) { + query = [query queryByAddingFilter:FSTTestFilter(filter[0], filter[1], filter[2])]; + }]; + } + if (queryDict[@"orderBys"]) { + NSArray *orderBys = queryDict[@"orderBys"]; + [orderBys enumerateObjectsUsingBlock:^(NSArray *_Nonnull orderBy, NSUInteger idx, + BOOL *_Nonnull stop) { + query = [query queryByAddingSortOrder:FSTTestOrderBy(orderBy[0], orderBy[1])]; + }]; + } + return query; + } else { + XCTFail(@"Invalid query: %@", querySpec); + return nil; + } +} + +- (FSTSnapshotVersion *)parseVersion:(NSNumber *_Nullable)version { + return FSTTestVersion(version.longLongValue); +} + +- (FSTDocumentViewChange *)parseChange:(NSArray *)change ofType:(FSTDocumentViewChangeType)type { + BOOL hasMutations = NO; + for (NSUInteger i = 3; i < change.count; ++i) { + if ([change[i] isEqual:@"local"]) { + hasMutations = YES; + } + } + NSNumber *version = change[1]; + FSTDocument *doc = FSTTestDoc(change[0], version.longLongValue, change[2], hasMutations); + return [FSTDocumentViewChange changeWithDocument:doc type:type]; +} + +#pragma mark - Methods for doing the steps of the spec test. + +- (void)doListen:(NSArray *)listenSpec { + FSTQuery *query = [self parseQuery:listenSpec[1]]; + FSTTargetID actualID = [self.driver addUserListenerWithQuery:query]; + + FSTTargetID expectedID = [listenSpec[0] intValue]; + XCTAssertEqual(actualID, expectedID); +} + +- (void)doUnlisten:(NSArray *)unlistenSpec { + FSTQuery *query = [self parseQuery:unlistenSpec[1]]; + [self.driver removeUserListenerWithQuery:query]; +} + +- (void)doSet:(NSArray *)setSpec { + [self.driver writeUserMutation:FSTTestSetMutation(setSpec[0], setSpec[1])]; +} + +- (void)doPatch:(NSArray *)patchSpec { + [self.driver writeUserMutation:FSTTestPatchMutation(patchSpec[0], patchSpec[1], nil)]; +} + +- (void)doDelete:(NSString *)key { + [self.driver writeUserMutation:FSTTestDeleteMutation(key)]; +} + +- (void)doWatchAck:(NSArray<NSNumber *> *)ackedTargets snapshot:(NSNumber *)watchSnapshot { + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded + targetIDs:ackedTargets + cause:nil]; + [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; +} + +- (void)doWatchCurrent:(NSArray<id> *)currentSpec snapshot:(NSNumber *)watchSnapshot { + NSArray<NSNumber *> *currentTargets = currentSpec[0]; + NSData *resumeToken = [currentSpec[1] dataUsingEncoding:NSUTF8StringEncoding]; + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:currentTargets + resumeToken:resumeToken]; + [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; +} + +- (void)doWatchRemove:(NSDictionary *)watchRemoveSpec snapshot:(NSNumber *)watchSnapshot { + NSError *error = nil; + NSDictionary *cause = watchRemoveSpec[@"cause"]; + if (cause) { + int code = ((NSNumber *)cause[@"code"]).intValue; + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey : @"Error from watchRemove.", + }; + error = [NSError errorWithDomain:FIRFirestoreErrorDomain code:code userInfo:userInfo]; + } + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved + targetIDs:watchRemoveSpec[@"targetIds"] + cause:error]; + [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; + // Unlike web, the FSTMockDatastore detects a watch removal with cause and will remove active + // targets +} + +- (void)doWatchEntity:(NSDictionary *)watchEntity snapshot:(NSNumber *_Nullable)watchSnapshot { + if (watchEntity[@"docs"]) { + FSTAssert(!watchEntity[@"doc"], @"Exactly one of |doc| or |docs| needs to be set."); + int count = 0; + NSArray *docs = watchEntity[@"docs"]; + for (NSDictionary *doc in docs) { + count++; + bool isLast = (count == docs.count); + NSMutableDictionary *watchSpec = [NSMutableDictionary dictionary]; + watchSpec[@"doc"] = doc; + if (watchEntity[@"targets"]) { + watchSpec[@"targets"] = watchEntity[@"targets"]; + } + if (watchEntity[@"removedTargets"]) { + watchSpec[@"removedTargets"] = watchEntity[@"removedTargets"]; + } + NSNumber *_Nullable version = nil; + if (isLast) { + version = watchSnapshot; + } + [self doWatchEntity:watchSpec snapshot:version]; + } + } else if (watchEntity[@"doc"]) { + NSArray *docSpec = watchEntity[@"doc"]; + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:docSpec[0]]; + FSTObjectValue *value = FSTTestObjectValue(docSpec[2]); + FSTSnapshotVersion *version = [self parseVersion:docSpec[1]]; + FSTMaybeDocument *doc = + [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO]; + FSTWatchChange *change = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:watchEntity[@"targets"] + removedTargetIDs:watchEntity[@"removedTargets"] + documentKey:doc.key + document:doc]; + [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; + } else if (watchEntity[@"key"]) { + FSTDocumentKey *docKey = [FSTDocumentKey keyWithPathString:watchEntity[@"key"]]; + FSTWatchChange *change = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:watchEntity[@"removedTargets"] + documentKey:docKey + document:nil]; + [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; + } else { + FSTFail(@"Either key, doc or docs must be set."); + } +} + +- (void)doWatchFilter:(NSArray *)watchFilter snapshot:(NSNumber *_Nullable)watchSnapshot { + NSArray<NSNumber *> *targets = watchFilter[0]; + FSTAssert(targets.count == 1, @"ExistenceFilters currently support exactly one target only."); + + int keyCount = watchFilter.count == 0 ? 0 : (int)watchFilter.count - 1; + + // TODO(dimond): extend this with different existence filters over time. + FSTExistenceFilter *filter = [FSTExistenceFilter filterWithCount:keyCount]; + FSTExistenceFilterWatchChange *change = + [FSTExistenceFilterWatchChange changeWithFilter:filter targetID:targets[0].intValue]; + [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; +} + +- (void)doWatchReset:(NSArray<NSNumber *> *)watchReset snapshot:(NSNumber *_Nullable)watchSnapshot { + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:watchReset + cause:nil]; + [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; +} + +- (void)doWatchStreamClose:(NSDictionary *)closeSpec { + NSDictionary *errorSpec = closeSpec[@"error"]; + int code = ((NSNumber *)(errorSpec[@"code"])).intValue; + [self.driver receiveWatchStreamError:code userInfo:errorSpec]; +} + +- (void)doWriteAck:(NSDictionary *)spec { + FSTSnapshotVersion *version = [self parseVersion:spec[@"version"]]; + NSNumber *expectUserCallback = spec[@"expectUserCallback"]; + + FSTMutationResult *mutationResult = + [[FSTMutationResult alloc] initWithVersion:version transformResults:nil]; + FSTOutstandingWrite *write = + [self.driver receiveWriteAckWithVersion:version mutationResults:@[ mutationResult ]]; + + if (expectUserCallback.boolValue) { + FSTAssert(write.done, @"Write should be done"); + FSTAssert(!write.error, @"Ack should not fail"); + } +} + +- (void)doFailWrite:(NSDictionary *)spec { + NSDictionary *errorSpec = spec[@"error"]; + NSNumber *expectUserCallback = spec[@"expectUserCallback"]; + + int code = ((NSNumber *)(errorSpec[@"code"])).intValue; + FSTOutstandingWrite *write = [self.driver receiveWriteError:code userInfo:errorSpec]; + + if (expectUserCallback.boolValue) { + FSTAssert(write.done, @"Write should be done"); + XCTAssertNotNil(write.error, @"Write should have failed"); + XCTAssertEqualObjects(write.error.domain, FIRFirestoreErrorDomain); + XCTAssertEqual(write.error.code, code); + } +} + +- (void)doChangeUser:(id)UID { + FSTUser *user = [UID isEqual:[NSNull null]] ? [FSTUser unauthenticatedUser] + : [[FSTUser alloc] initWithUID:UID]; + [self.driver changeUser:user]; +} + +- (void)doRestart { + // Any outstanding user writes should be automatically re-sent, so we want to preserve them + // when re-creating the driver. + FSTOutstandingWriteQueues *outstandingWrites = self.driver.outstandingWrites; + + [self.driver shutdown]; + + // NOTE: We intentionally don't shutdown / re-create driverPersistence, since we want to + // preserve the persisted state. This is a bit of a cheat since it means we're not exercising + // the initialization / start logic that would normally be hit, but simplifies the plumbing and + // allows us to run these tests against FSTMemoryPersistence as well (there would be no way to + // re-create FSTMemoryPersistence without losing all persisted state). + + self.driver = [[FSTSyncEngineTestDriver alloc] initWithPersistence:self.driverPersistence + garbageCollector:self.garbageCollector + initialUser:self.driver.currentUser + outstandingWrites:outstandingWrites]; + [self.driver start]; +} + +- (void)doStep:(NSDictionary *)step { + if (step[@"userListen"]) { + [self doListen:step[@"userListen"]]; + } else if (step[@"userUnlisten"]) { + [self doUnlisten:step[@"userUnlisten"]]; + } else if (step[@"userSet"]) { + [self doSet:step[@"userSet"]]; + } else if (step[@"userPatch"]) { + [self doPatch:step[@"userPatch"]]; + } else if (step[@"userDelete"]) { + [self doDelete:step[@"userDelete"]]; + } else if (step[@"watchAck"]) { + [self doWatchAck:step[@"watchAck"] snapshot:step[@"watchSnapshot"]]; + } else if (step[@"watchCurrent"]) { + [self doWatchCurrent:step[@"watchCurrent"] snapshot:step[@"watchSnapshot"]]; + } else if (step[@"watchRemove"]) { + [self doWatchRemove:step[@"watchRemove"] snapshot:step[@"watchSnapshot"]]; + } else if (step[@"watchEntity"]) { + [self doWatchEntity:step[@"watchEntity"] snapshot:step[@"watchSnapshot"]]; + } else if (step[@"watchFilter"]) { + [self doWatchFilter:step[@"watchFilter"] snapshot:step[@"watchSnapshot"]]; + } else if (step[@"watchReset"]) { + [self doWatchReset:step[@"watchReset"] snapshot:step[@"watchSnapshot"]]; + } else if (step[@"watchStreamClose"]) { + [self doWatchStreamClose:step[@"watchStreamClose"]]; + } else if (step[@"watchProto"]) { + // watchProto isn't yet used, and it's unclear how to create arbitrary protos from JSON. + FSTFail(@"watchProto is not yet supported."); + } else if (step[@"writeAck"]) { + [self doWriteAck:step[@"writeAck"]]; + } else if (step[@"failWrite"]) { + [self doFailWrite:step[@"failWrite"]]; + } else if (step[@"changeUser"]) { + [self doChangeUser:step[@"changeUser"]]; + } else if (step[@"restart"]) { + [self doRestart]; + } else { + XCTFail(@"Unknown step: %@", step); + } +} + +- (void)validateEvent:(FSTQueryEvent *)actual matches:(NSDictionary *)expected { + FSTQuery *expectedQuery = [self parseQuery:expected[@"query"]]; + XCTAssertEqualObjects(actual.query, expectedQuery); + if ([expected[@"errorCode"] integerValue] != 0) { + XCTAssertNotNil(actual.error); + XCTAssertEqual(actual.error.code, [expected[@"errorCode"] integerValue]); + } else { + NSMutableArray *expectedChanges = [NSMutableArray array]; + NSMutableArray *removed = expected[@"removed"]; + for (NSArray *changeSpec in removed) { + [expectedChanges + addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeRemoved]]; + } + NSMutableArray *added = expected[@"added"]; + for (NSArray *changeSpec in added) { + [expectedChanges + addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeAdded]]; + } + NSMutableArray *modified = expected[@"modified"]; + for (NSArray *changeSpec in modified) { + [expectedChanges + addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeModified]]; + } + NSMutableArray *metadata = expected[@"metadata"]; + for (NSArray *changeSpec in metadata) { + [expectedChanges + addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeMetadata]]; + } + XCTAssertEqualObjects(actual.viewSnapshot.documentChanges, expectedChanges); + + BOOL expectedHasPendingWrites = + expected[@"hasPendingWrites"] ? [expected[@"hasPendingWrites"] boolValue] : NO; + BOOL expectedIsFromCache = expected[@"fromCache"] ? [expected[@"fromCache"] boolValue] : NO; + XCTAssertEqual(actual.viewSnapshot.hasPendingWrites, expectedHasPendingWrites, + @"hasPendingWrites"); + XCTAssertEqual(actual.viewSnapshot.isFromCache, expectedIsFromCache, @"isFromCache"); + } +} + +- (void)validateStepExpectations:(NSMutableArray *_Nullable)stepExpectations { + NSArray<FSTQueryEvent *> *events = self.driver.capturedEventsSinceLastCall; + + if (!stepExpectations) { + XCTAssertEqual(events.count, 0); + for (FSTQueryEvent *event in events) { + XCTFail(@"Unexpected event: %@", event); + } + return; + } + + events = + [events sortedArrayUsingComparator:^NSComparisonResult(FSTQueryEvent *q1, FSTQueryEvent *q2) { + return [q1.query.canonicalID compare:q2.query.canonicalID]; + }]; + + XCTAssertEqual(events.count, stepExpectations.count); + NSUInteger i = 0; + for (; i < stepExpectations.count && i < events.count; ++i) { + [self validateEvent:events[i] matches:stepExpectations[i]]; + } + for (; i < stepExpectations.count; ++i) { + XCTFail(@"Missing event: %@", stepExpectations[i]); + } + for (; i < events.count; ++i) { + XCTFail(@"Unexpected event: %@", events[i]); + } +} + +- (void)validateStateExpectations:(nullable NSDictionary *)expected { + if (expected) { + if (expected[@"numOutstandingWrites"]) { + XCTAssertEqual([self.driver sentWritesCount], [expected[@"numOutstandingWrites"] intValue]); + } + if (expected[@"limboDocs"]) { + NSMutableSet<FSTDocumentKey *> *expectedLimboDocuments = [NSMutableSet set]; + NSArray *docNames = expected[@"limboDocs"]; + for (NSString *name in docNames) { + [expectedLimboDocuments addObject:FSTTestDocKey(name)]; + } + // Update the expected limbo documents + self.driver.expectedLimboDocuments = expectedLimboDocuments; + } + if (expected[@"activeTargets"]) { + NSMutableDictionary *expectedActiveTargets = [NSMutableDictionary dictionary]; + [expected[@"activeTargets"] enumerateKeysAndObjectsUsingBlock:^(NSString *targetIDString, + NSDictionary *queryData, + BOOL *stop) { + FSTTargetID targetID = [targetIDString intValue]; + FSTQuery *query = [self parseQuery:queryData[@"query"]]; + NSData *resumeToken = [queryData[@"resumeToken"] dataUsingEncoding:NSUTF8StringEncoding]; + // TODO(mcg): populate the purpose of the target once it's possible to encode that in the + // spec tests. For now, hard-code that it's a listen despite the fact that it's not always + // the right value. + expectedActiveTargets[@(targetID)] = + [[FSTQueryData alloc] initWithQuery:query + targetID:targetID + purpose:FSTQueryPurposeListen + snapshotVersion:[FSTSnapshotVersion noVersion] + resumeToken:resumeToken]; + }]; + self.driver.expectedActiveTargets = expectedActiveTargets; + } + } + + // Always validate that the expected limbo docs match the actual limbo docs. + [self validateLimboDocuments]; + // Always validate that the expected active targets match the actual active targets. + [self validateActiveTargets]; +} + +- (void)validateLimboDocuments { + // Make a copy so it can modified while checking against the expected limbo docs. + NSMutableDictionary<FSTDocumentKey *, FSTBoxedTargetID *> *actualLimboDocs = + [NSMutableDictionary dictionaryWithDictionary:self.driver.currentLimboDocuments]; + + // Validate that each limbo doc has an expected active target + [actualLimboDocs enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, + FSTBoxedTargetID *targetID, BOOL *stop) { + XCTAssertNotNil(self.driver.expectedActiveTargets[targetID], + @"Found limbo doc without an expected active target"); + }]; + + for (FSTDocumentKey *expectedLimboDoc in self.driver.expectedLimboDocuments) { + XCTAssertNotNil(actualLimboDocs[expectedLimboDoc], + @"Expected doc to be in limbo, but was not: %@", expectedLimboDoc); + [actualLimboDocs removeObjectForKey:expectedLimboDoc]; + } + XCTAssertTrue(actualLimboDocs.count == 0, "Unexpected docs in limbo: %@", actualLimboDocs); +} + +- (void)validateActiveTargets { + // Create a copy so we can modify it in tests + NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *actualTargets = + [NSMutableDictionary dictionaryWithDictionary:self.driver.activeTargets]; + + [self.driver.expectedActiveTargets enumerateKeysAndObjectsUsingBlock:^(FSTBoxedTargetID *targetID, + FSTQueryData *queryData, + BOOL *stop) { + XCTAssertNotNil(actualTargets[targetID], @"Expected active target not found: %@", queryData); + + // TODO(mcg): validate the purpose of the target once it's possible to encode that in the + // spec tests. For now, only validate properties that can be validated. + // XCTAssertEqualObjects(actualTargets[targetID], queryData); + + FSTQueryData *actual = actualTargets[targetID]; + XCTAssertEqualObjects(actual.query, queryData.query); + XCTAssertEqual(actual.targetID, queryData.targetID); + XCTAssertEqualObjects(actual.snapshotVersion, queryData.snapshotVersion); + XCTAssertEqualObjects(actual.resumeToken, queryData.resumeToken); + + [actualTargets removeObjectForKey:targetID]; + }]; + XCTAssertTrue(actualTargets.count == 0, "Unexpected active targets: %@", actualTargets); +} + +- (void)runSpecTestSteps:(NSArray *)steps config:(NSDictionary *)config { + @try { + [self setUpForSpecWithConfig:config]; + for (NSDictionary *step in steps) { + FSTLog(@"Doing step %@", step); + [self doStep:step]; + [self validateStepExpectations:step[@"expect"]]; + [self validateStateExpectations:step[@"stateExpect"]]; + } + [self.driver validateUsage]; + } @finally { + // Ensure that the driver is torn down even if the test is failing due to a thrown exception so + // that any resources held by the driver are released. This is important when the driver is + // backed by LevelDB because LevelDB locks its database. If -tearDownForSpec were not called + // after an exception then subsequent attempts to open the LevelDB will fail, making it harder + // to zero in on the spec tests as a culprit. + [self tearDownForSpec]; + } +} + +#pragma mark - The actual test methods. + +- (void)testSpecTests { + if ([self isTestBaseClass]) return; + + // Enumerate the .json files containing the spec tests. + NSMutableArray<NSString *> *specFiles = [NSMutableArray array]; + NSMutableArray<NSDictionary *> *parsedSpecs = [NSMutableArray array]; + NSBundle *bundle = [NSBundle bundleForClass:[self class]]; + NSFileManager *fs = [NSFileManager defaultManager]; + BOOL exclusiveMode = NO; + for (NSString *file in [fs enumeratorAtPath:[bundle bundlePath]]) { + if (![@"json" isEqual:[file pathExtension]]) { + continue; + } + + // Read and parse the JSON from the file. + NSString *fileName = [file stringByDeletingPathExtension]; + NSString *path = [bundle pathForResource:fileName ofType:@"json"]; + NSData *json = [NSData dataWithContentsOfFile:path]; + XCTAssertNotNil(json); + NSError *error = nil; + id _Nullable parsed = [NSJSONSerialization JSONObjectWithData:json options:0 error:&error]; + XCTAssertNil(error, @"%@", error); + XCTAssertTrue([parsed isKindOfClass:[NSDictionary class]]); + NSDictionary *testDict = (NSDictionary *)parsed; + + exclusiveMode = exclusiveMode || [self anyTestsAreMarkedExclusive:testDict]; + [specFiles addObject:fileName]; + [parsedSpecs addObject:testDict]; + } + + // Now iterate over them and run them. + __block bool ranAtLeastOneTest = NO; + for (NSUInteger i = 0; i < specFiles.count; i++) { + NSLog(@"Spec test file: %@", specFiles[i]); + // Iterate over the tests in the file and run them. + [parsedSpecs[i] enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + XCTAssertTrue([obj isKindOfClass:[NSDictionary class]]); + NSDictionary *testDescription = (NSDictionary *)obj; + NSString *describeName = testDescription[@"describeName"]; + NSString *itName = testDescription[@"itName"]; + NSString *name = [NSString stringWithFormat:@"%@ %@", describeName, itName]; + NSDictionary *config = testDescription[@"config"]; + NSArray *steps = testDescription[@"steps"]; + NSArray<NSString *> *tags = testDescription[@"tags"]; + + BOOL runTest = !exclusiveMode || [tags indexOfObject:kExclusiveTag] != NSNotFound; + if ([tags indexOfObject:kNoIOSTag] != NSNotFound) { + runTest = NO; + } + if (runTest) { + NSLog(@" Spec test: %@", name); + [self runSpecTestSteps:steps config:config]; + ranAtLeastOneTest = YES; + } else { + NSLog(@" [SKIPPED] Spec test: %@", name); + } + }]; + } + XCTAssertTrue(ranAtLeastOneTest); +} + +- (BOOL)anyTestsAreMarkedExclusive:(NSDictionary *)tests { + __block BOOL found = NO; + [tests enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + XCTAssertTrue([obj isKindOfClass:[NSDictionary class]]); + NSDictionary *testDescription = (NSDictionary *)obj; + NSArray<NSString *> *tags = testDescription[@"tags"]; + if ([tags indexOfObject:kExclusiveTag] != NSNotFound) { + found = YES; + *stop = YES; + } + }]; + return found; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h new file mode 100644 index 0000000..0643d76 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h @@ -0,0 +1,248 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +#import "Core/FSTTypes.h" + +@class FSTDocumentKey; +@class FSTMutation; +@class FSTMutationResult; +@class FSTQuery; +@class FSTQueryData; +@class FSTSnapshotVersion; +@class FSTUser; +@class FSTViewSnapshot; +@class FSTWatchChange; +@protocol FSTGarbageCollector; +@protocol FSTPersistence; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Interface used for object that contain exactly one of either a view snapshot or an error for the + * given query. + */ +@interface FSTQueryEvent : NSObject +@property(nonatomic, strong) FSTQuery *query; +@property(nonatomic, strong, nullable) FSTViewSnapshot *viewSnapshot; +@property(nonatomic, strong, nullable) NSError *error; +@end + +/** Holds an outstanding write and its result. */ +@interface FSTOutstandingWrite : NSObject +/** The write that is outstanding. */ +@property(nonatomic, strong, readwrite) FSTMutation *write; +/** Whether this write is done (regardless of whether it was successful or not). */ +@property(nonatomic, assign, readwrite) BOOL done; +/** The error - if any - of this write. */ +@property(nonatomic, strong, nullable, readwrite) NSError *error; +@end + +/** Mapping of user => array of FSTMutations for that user. */ +typedef NSDictionary<FSTUser *, NSArray<FSTOutstandingWrite *> *> FSTOutstandingWriteQueues; + +/** + * A test driver for FSTSyncEngine that allows simulated event delivery and capture. As much as + * possible, all sources of nondeterminism are removed so that test execution is consistent and + * reliable. + * + * FSTSyncEngineTestDriver: + * + * + constructs an FSTSyncEngine using a mocked FSTDatastore for the backend; + * + allows the caller to trigger events (user API calls and incoming FSTDatastore messages); + * + performs sequencing validation internally (e.g. that when a user mutation is initiated, the + * FSTSyncEngine correctly sends it to the remote store); and + * + exposes the set of FSTQueryEvents generated for the caller to verify. + * + * Events come in three major flavors: + * + * + user events: simulate user API calls + * + watch events: simulate RPC interactions with the Watch backend + * + write events: simulate RPC interactions with the Streaming Write backend + * + * Each method on the driver injects a different event into the system. + */ +@interface FSTSyncEngineTestDriver : NSObject + +/** + * Initializes the underlying FSTSyncEngine with the given local persistence implementation and + * garbage collection policy. + */ +- (instancetype)initWithPersistence:(id<FSTPersistence>)persistence + garbageCollector:(id<FSTGarbageCollector>)garbageCollector; + +/** + * Initializes the underlying FSTSyncEngine with the given local persistence implementation and + * a set of existing outstandingWrites (useful when your FSTPersistence object has + * persisted mutation queues). + */ +- (instancetype)initWithPersistence:(id<FSTPersistence>)persistence + garbageCollector:(id<FSTGarbageCollector>)garbageCollector + initialUser:(FSTUser *)initialUser + outstandingWrites:(FSTOutstandingWriteQueues *)outstandingWrites + NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/** Starts the FSTSyncEngine and its underlying components. */ +- (void)start; + +/** Validates that the API has been used correctly after a test is complete. */ +- (void)validateUsage; + +/** Shuts the FSTSyncEngine down. */ +- (void)shutdown; + +/** + * Adds a listener to the FSTSyncEngine as if the user had initiated a new listen for the given + * query. + * + * Resulting events are captured and made available via the capturedEventsSinceLastCall method. + * + * @param query A valid query to execute against the backend. + * @return The target ID assigned by the system to track the query. + */ +- (FSTTargetID)addUserListenerWithQuery:(FSTQuery *)query; + +/** + * Removes a listener from the FSTSyncEngine as if the user had removed a listener corresponding + * to the given query. + * + * Resulting events are captured and made available via the capturedEventsSinceLastCall method. + * + * @param query An identical query corresponding to one passed to -addUserListenerWithQuery. + */ +- (void)removeUserListenerWithQuery:(FSTQuery *)query; + +/** + * Delivers a WatchChange RPC to the FSTSyncEngine as if it were received from the backend watch + * service, either in response to addUserListener: or removeUserListener calls or because the + * simulated backend has new data. + * + * Resulting events are captured and made available via the capturedEventsSinceLastCall method. + * + * @param change Any type of watch change + * @param snapshot A snapshot version to attach, if applicable. This should be sent when + * simulating the server having sent a complete snapshot. + */ +- (void)receiveWatchChange:(FSTWatchChange *)change + snapshotVersion:(FSTSnapshotVersion *_Nullable)snapshot; + +/** + * Delivers a watch stream error as if the Streaming Watch backend has generated some kind of error. + * + * @param errorCode A FIRFirestoreErrorCode value, from FIRFirestoreErrors.h + * @param userInfo Any additional details that the server might have sent along with the error. + * For the moment this is effectively unused, but is logged. + */ +- (void)receiveWatchStreamError:(int)errorCode userInfo:(NSDictionary<NSString *, id> *)userInfo; + +/** + * Performs a mutation against the FSTSyncEngine as if the user had written the mutation through + * the API. + * + * Also retains the mutation so that the driver can validate that the sync engine sent the mutation + * to the remote store before receiveWatchChange:snapshotVersion: and receiveWriteError:userInfo: + * events are processed. + * + * @param mutation Any type of valid mutation. + */ +- (void)writeUserMutation:(FSTMutation *)mutation; + +/** + * Delivers a write error as if the Streaming Write backend has generated some kind of error. + * + * For the moment write errors are usually must be in response to a mutation that has been written + * with writeUserMutation:. Spontaneously errors due to idle timeout, server restart, or credential + * expiration aren't yet supported. + * + * @param errorCode A FIRFirestoreErrorCode value, from FIRFirestoreErrors.h + * @param userInfo Any additional details that the server might have sent along with the error. + * For the moment this is effectively unused, but is logged. + */ +- (FSTOutstandingWrite *)receiveWriteError:(int)errorCode + userInfo:(NSDictionary<NSString *, id> *)userInfo; + +/** + * Delivers a write acknowledgement as if the Streaming Write backend has acknowledged a write with + * the snapshot version at which the write was committed. + * + * @param commitVersion The snapshot version at which the simulated server has committed + * the mutation. Snapshot versions must be monotonically increasing. + * @param mutationResults The mutation results for the write that is being acked. + */ +- (FSTOutstandingWrite *)receiveWriteAckWithVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray<FSTMutationResult *> *)mutationResults; + +/** + * A count of the mutations written to the write stream by the FSTSyncEngine, but not yet + * acknowledged via receiveWriteError: or receiveWriteAckWithVersion:mutationResults. + */ +@property(nonatomic, readonly) int sentWritesCount; + +/** + * Switches the FSTSyncEngine to a new user. The test driver tracks the outstanding mutations for + * each user, so future receiveWriteAck/Error operations will validate the write sent to the mock + * datastore matches the next outstanding write for that user. + */ +- (void)changeUser:(FSTUser *)user; + +/** + * Returns all query events generated by the FSTSyncEngine in response to the event injection + * methods called previously. The events are cleared after each invocation of this method. + */ +- (NSArray<FSTQueryEvent *> *)capturedEventsSinceLastCall; + +/** + * The writes that have been sent to the FSTSyncEngine via writeUserMutation: but not yet + * acknowledged by calling receiveWriteAck/Error:. They are tracked per-user. + * + * It is mostly an implementation detail used internally to validate that the writes sent to the + * mock backend by the FSTSyncEngine match the user mutations that initiated them. + * + * It is exposed specifically for use with the + * initWithPersistence:GCEnabled:outstandingWrites: initializer to test persistence + * scenarios where the FSTSyncEngine is restarted while the FSTPersistence implementation still has + * outstanding persisted mutations. + * + * Note: The size of the list for the current user will generally be the same as + * sentWritesCount, but not necessarily, since the FSTRemoteStore limits the number of + * outstanding writes to the backend at a given time. + */ +@property(nonatomic, strong, readonly) FSTOutstandingWriteQueues *outstandingWrites; + +/** The current user for the FSTSyncEngine; determines which mutation queue is active. */ +@property(nonatomic, strong, readonly) FSTUser *currentUser; + +/** The current set of documents in limbo. */ +@property(nonatomic, strong, readonly) + NSDictionary<FSTDocumentKey *, FSTBoxedTargetID *> *currentLimboDocuments; + +/** The expected set of documents in limbo. */ +@property(nonatomic, strong, readwrite) NSSet<FSTDocumentKey *> *expectedLimboDocuments; + +/** The set of active targets as observed on the watch stream. */ +@property(nonatomic, strong, readonly) + NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *activeTargets; + +/** The expected set of active targets, keyed by target ID. */ +@property(nonatomic, strong, readwrite) + NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *expectedActiveTargets; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m new file mode 100644 index 0000000..b4c0b02 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m @@ -0,0 +1,291 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FSTSyncEngineTestDriver.h" + +#import <GRPCClient/GRPCCall.h> + +#import "Auth/FSTUser.h" +#import "Core/FSTEventManager.h" +#import "Core/FSTQuery.h" +#import "Core/FSTSnapshotVersion.h" +#import "Core/FSTSyncEngine.h" +#import "Firestore/FIRFirestoreErrors.h" +#import "Local/FSTLocalStore.h" +#import "Local/FSTPersistence.h" +#import "Model/FSTMutation.h" +#import "Remote/FSTDatastore.h" +#import "Remote/FSTWatchChange.h" +#import "Util/FSTAssert.h" +#import "Util/FSTDispatchQueue.h" +#import "Util/FSTLogger.h" + +#import "FSTMockDatastore.h" +#import "FSTSyncEngine+Testing.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTQueryEvent + +- (NSString *)description { + // The Query is also included in the view, so we skip it. + return [NSString stringWithFormat:@"<FSTQueryEvent: viewSnapshot=%@, error=%@>", + self.viewSnapshot, self.error]; +} + +@end + +@implementation FSTOutstandingWrite +@end + +@interface FSTSyncEngineTestDriver () + +#pragma mark - Parts of the Firestore system that the spec tests need to control. + +@property(nonatomic, strong, readonly) FSTMockDatastore *datastore; +@property(nonatomic, strong, readonly) FSTEventManager *eventManager; +@property(nonatomic, strong, readonly) FSTRemoteStore *remoteStore; +@property(nonatomic, strong, readonly) FSTLocalStore *localStore; +@property(nonatomic, strong, readonly) FSTSyncEngine *syncEngine; + +#pragma mark - Data structures for holding events sent by the watch stream. + +/** A block for the FSTEventAggregator to use to report events to the test. */ +@property(nonatomic, strong, readonly) void (^eventHandler)(FSTQueryEvent *); +/** The events received by our eventHandler and not yet retrieved via capturedEventsSinceLastCall */ +@property(nonatomic, strong, readonly) NSMutableArray<FSTQueryEvent *> *events; +/** A dictionary for tracking the listens on queries. */ +@property(nonatomic, strong, readonly) + NSMutableDictionary<FSTQuery *, FSTQueryListener *> *queryListeners; + +#pragma mark - Other data structures. +@property(nonatomic, strong, readwrite) FSTUser *currentUser; + +@end + +@implementation FSTSyncEngineTestDriver { + // ivar is declared as mutable. + NSMutableDictionary<FSTUser *, NSMutableArray<FSTOutstandingWrite *> *> *_outstandingWrites; +} + +- (instancetype)initWithPersistence:(id<FSTPersistence>)persistence + garbageCollector:(id<FSTGarbageCollector>)garbageCollector { + return [self initWithPersistence:persistence + garbageCollector:garbageCollector + initialUser:[FSTUser unauthenticatedUser] + outstandingWrites:@{}]; +} + +- (instancetype)initWithPersistence:(id<FSTPersistence>)persistence + garbageCollector:(id<FSTGarbageCollector>)garbageCollector + initialUser:(FSTUser *)initialUser + outstandingWrites:(FSTOutstandingWriteQueues *)outstandingWrites { + if (self = [super init]) { + // Create mutable copy of outstandingWrites. + _outstandingWrites = [NSMutableDictionary dictionary]; + [outstandingWrites enumerateKeysAndObjectsUsingBlock:^( + FSTUser *user, NSArray<FSTOutstandingWrite *> *writes, BOOL *stop) { + _outstandingWrites[user] = [writes mutableCopy]; + }]; + + _events = [NSMutableArray array]; + + // Set up the sync engine and various stores. + dispatch_queue_t mainQueue = dispatch_get_main_queue(); + FSTDispatchQueue *dispatchQueue = [FSTDispatchQueue queueWith:mainQueue]; + _localStore = [[FSTLocalStore alloc] initWithPersistence:persistence + garbageCollector:garbageCollector + initialUser:initialUser]; + _datastore = [FSTMockDatastore mockDatastoreWithWorkerDispatchQueue:dispatchQueue]; + + _remoteStore = [FSTRemoteStore remoteStoreWithLocalStore:_localStore datastore:_datastore]; + + _syncEngine = [[FSTSyncEngine alloc] initWithLocalStore:_localStore + remoteStore:_remoteStore + initialUser:initialUser]; + _remoteStore.syncEngine = _syncEngine; + _eventManager = [FSTEventManager eventManagerWithSyncEngine:_syncEngine]; + + _remoteStore.onlineStateDelegate = _eventManager; + + // Set up internal event tracking for the spec tests. + NSMutableArray<FSTQueryEvent *> *events = [NSMutableArray array]; + _eventHandler = ^(FSTQueryEvent *e) { + [events addObject:e]; + }; + _events = events; + + _queryListeners = [NSMutableDictionary dictionary]; + + _expectedLimboDocuments = [NSSet set]; + + _expectedActiveTargets = [NSDictionary dictionary]; + + _currentUser = initialUser; + } + return self; +} + +- (void)start { + [self.localStore start]; + [self.remoteStore start]; +} + +- (void)validateUsage { + // We could relax this if we found a reason to. + FSTAssert(self.events.count == 0, + @"You must clear all pending events by calling" + " capturedEventsSinceLastCall before calling shutdown."); +} + +- (void)shutdown { + [self.remoteStore shutdown]; + [self.localStore shutdown]; +} + +- (void)validateNextWriteSent:(FSTMutation *)expectedWrite { + NSArray<FSTMutation *> *request = [self.datastore nextSentWrite]; + // Make sure the write went through the pipe like we expected it to. + FSTAssert(request.count == 1, @"Only single mutation requests are supported at the moment"); + FSTMutation *actualWrite = request[0]; + FSTAssert([actualWrite isEqual:expectedWrite], + @"Mock datastore received write %@ but first outstanding mutation was %@", actualWrite, + expectedWrite); + FSTLog(@"A write was sent: %@", actualWrite); +} + +- (int)sentWritesCount { + return [self.datastore writesSent]; +} + +- (void)changeUser:(FSTUser *)user { + self.currentUser = user; + [self.syncEngine userDidChange:user]; +} + +- (FSTOutstandingWrite *)receiveWriteAckWithVersion:(FSTSnapshotVersion *)commitVersion + mutationResults: + (NSArray<FSTMutationResult *> *)mutationResults { + FSTOutstandingWrite *write = [self currentOutstandingWrites].firstObject; + [[self currentOutstandingWrites] removeObjectAtIndex:0]; + [self validateNextWriteSent:write.write]; + + [self.datastore ackWriteWithVersion:commitVersion mutationResults:mutationResults]; + + return write; +} + +- (FSTOutstandingWrite *)receiveWriteError:(int)errorCode + userInfo:(NSDictionary<NSString *, id> *)userInfo { + NSError *error = + [NSError errorWithDomain:FIRFirestoreErrorDomain code:errorCode userInfo:userInfo]; + + FSTOutstandingWrite *write = [self currentOutstandingWrites].firstObject; + [self validateNextWriteSent:write.write]; + + // If this is a permanent error, the mutation is not expected to be sent again so we remove it + // from currentOutstandingWrites. + if ([FSTDatastore isPermanentWriteError:error]) { + [[self currentOutstandingWrites] removeObjectAtIndex:0]; + } + + FSTLog(@"Failing a write."); + [self.datastore failWriteWithError:error]; + + return write; +} + +- (NSArray<FSTQueryEvent *> *)capturedEventsSinceLastCall { + NSArray<FSTQueryEvent *> *result = [self.events copy]; + [self.events removeAllObjects]; + return result; +} + +- (FSTTargetID)addUserListenerWithQuery:(FSTQuery *)query { + // TODO(dimond): Allow customizing listen options in spec tests + // TODO(dimond): Change spec tests to verify isFromCache on snapshots + FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES + includeDocumentMetadataChanges:YES + waitForSyncWhenOnline:NO]; + FSTQueryListener *listener = [[FSTQueryListener alloc] + initWithQuery:query + options:options + viewSnapshotHandler:^(FSTViewSnapshot *_Nullable snapshot, NSError *_Nullable error) { + FSTQueryEvent *event = [[FSTQueryEvent alloc] init]; + event.query = query; + event.viewSnapshot = snapshot; + event.error = error; + [self.events addObject:event]; + }]; + self.queryListeners[query] = listener; + return [self.eventManager addListener:listener]; +} + +- (void)removeUserListenerWithQuery:(FSTQuery *)query { + FSTQueryListener *listener = self.queryListeners[query]; + [self.queryListeners removeObjectForKey:query]; + [self.eventManager removeListener:listener]; +} + +- (void)writeUserMutation:(FSTMutation *)mutation { + FSTOutstandingWrite *write = [[FSTOutstandingWrite alloc] init]; + write.write = mutation; + [[self currentOutstandingWrites] addObject:write]; + FSTLog(@"sending a user write."); + [self.syncEngine writeMutations:@[ mutation ] + completion:^(NSError *_Nullable error) { + FSTLog(@"A callback was called with error: %@", error); + write.done = YES; + write.error = error; + }]; +} + +- (void)receiveWatchChange:(FSTWatchChange *)change + snapshotVersion:(FSTSnapshotVersion *_Nullable)snapshot { + [self.datastore writeWatchChange:change snapshotVersion:snapshot]; +} + +- (void)receiveWatchStreamError:(int)errorCode userInfo:(NSDictionary<NSString *, id> *)userInfo { + NSError *error = + [NSError errorWithDomain:FIRFirestoreErrorDomain code:errorCode userInfo:userInfo]; + + [self.datastore failWatchStreamWithError:error]; + // Unlike web, stream should re-open synchronously + FSTAssert(self.datastore.isWatchStreamOpen, @"Watch stream is open"); +} + +- (NSDictionary<FSTDocumentKey *, FSTBoxedTargetID *> *)currentLimboDocuments { + return [self.syncEngine currentLimboDocuments]; +} + +- (NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)activeTargets { + return [[self.datastore activeTargets] copy]; +} + +#pragma mark - Helper Methods + +- (NSMutableArray<FSTOutstandingWrite *> *)currentOutstandingWrites { + NSMutableArray<FSTOutstandingWrite *> *writes = _outstandingWrites[self.currentUser]; + if (!writes) { + writes = [NSMutableArray array]; + _outstandingWrites[self.currentUser] = writes; + } + return writes; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/json/README.md b/Firestore/Example/Tests/SpecTests/json/README.md new file mode 100644 index 0000000..bcc9b38 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/README.md @@ -0,0 +1,3 @@ +These json files are generated from the web test sources. + +TODO(mikelehen): Re-add instructions for generating these. diff --git a/Firestore/Example/Tests/SpecTests/json/collection_spec_test.json b/Firestore/Example/Tests/SpecTests/json/collection_spec_test.json new file mode 100644 index 0000000..ef41afe --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/collection_spec_test.json @@ -0,0 +1,147 @@ +{ + "Events are raised after watch ack": { + "describeName": "Collections:", + "itName": "Events are raised after watch ack", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 1000, + { + "foo": "bar" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 1000, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Events are raised for local sets before watch ack": { + "describeName": "Collections:", + "itName": "Events are raised for local sets before watch ack", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + } + ] + } +} diff --git a/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json b/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json new file mode 100644 index 0000000..ab42241 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json @@ -0,0 +1,738 @@ +{ + "Existence filter mismatch triggers re-run of query": { + "describeName": "Existence Filters:", + "itName": "Existence filter mismatch triggers re-run of query", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ], + [ + "collection/2", + 1000, + { + "v": 2 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ], + [ + "collection/2", + 1000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchFilter": [ + [ + 2 + ], + "collection/1" + ], + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "stateExpect": { + "limboDocs": [ + "collection/2" + ], + "activeTargets": { + "1": { + "query": { + "path": "collection/2", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 1 + ] + }, + { + "watchCurrent": [ + [ + 1 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/2", + 1000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Existence filter mismatch will drop resume token": { + "describeName": "Existence Filters:", + "itName": "Existence filter mismatch will drop resume token", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ], + [ + "collection/2", + 1000, + { + "v": 2 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "existence-filter-resume-token" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ], + [ + "collection/2", + 1000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + }, + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "existence-filter-resume-token" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchFilter": [ + [ + 2 + ], + "collection/1" + ], + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "stateExpect": { + "limboDocs": [ + "collection/2" + ], + "activeTargets": { + "1": { + "query": { + "path": "collection/2", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 1 + ] + }, + { + "watchCurrent": [ + [ + 1 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/2", + 1000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Existence filter limbo resolution is denied": { + "describeName": "Existence Filters:", + "itName": "Existence filter limbo resolution is denied", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ], + [ + "collection/2", + 1000, + { + "v": 2 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ], + [ + "collection/2", + 1000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchFilter": [ + [ + 2 + ], + "collection/1" + ], + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "stateExpect": { + "limboDocs": [ + "collection/2" + ], + "activeTargets": { + "1": { + "query": { + "path": "collection/2", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchRemove": { + "targetIds": [ + 1 + ], + "cause": { + "code": 7 + } + }, + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + }, + "limboDocs": [] + }, + "watchSnapshot": 3000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/2", + 1000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + } +} diff --git a/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json b/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json new file mode 100644 index 0000000..ee2d883 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json @@ -0,0 +1,1150 @@ +{ + "Limbo documents are deleted without an existence filter": { + "describeName": "Limbo Documents:", + "itName": "Limbo documents are deleted without an existence filter", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchReset": [ + 2 + ] + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "stateExpect": { + "limboDocs": [ + "collection/a" + ], + "activeTargets": { + "1": { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 1 + ] + }, + { + "watchCurrent": [ + [ + 1 + ], + "resume-token-2" + ], + "watchSnapshot": 1002, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Limbo documents are deleted with an existence filter": { + "describeName": "Limbo Documents:", + "itName": "Limbo documents are deleted with an existence filter", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchReset": [ + 2 + ] + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "stateExpect": { + "limboDocs": [ + "collection/a" + ], + "activeTargets": { + "1": { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 1 + ] + }, + { + "watchFilter": [ + [ + 1 + ] + ] + }, + { + "watchCurrent": [ + [ + 1 + ], + "resume-token-1002" + ], + "watchSnapshot": 1002, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Limbo documents are resolved with updates": { + "describeName": "Limbo Documents:", + "itName": "Limbo documents are resolved with updates", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchReset": [ + 2 + ] + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "stateExpect": { + "limboDocs": [ + "collection/a" + ], + "activeTargets": { + "1": { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "2": { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 1 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "b" + } + ] + ], + "targets": [ + 1 + ] + } + }, + { + "watchCurrent": [ + [ + 1 + ], + "resume-token-1002" + ], + "watchSnapshot": 1002, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "removed": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Limbo documents are resolved with updates in different snapshot than \"current\"": { + "describeName": "Limbo Documents:", + "itName": "Limbo documents are resolved with updates in different snapshot than \"current\"", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userListen": [ + 4, + { + "path": "collection", + "filters": [ + [ + "key", + "==", + "b" + ] + ], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "b" + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchReset": [ + 2 + ] + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "stateExpect": { + "limboDocs": [ + "collection/a" + ], + "activeTargets": { + "1": { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "2": { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "b" + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 4 + ] + }, + { + "watchAck": [ + 1 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "b" + } + ] + ], + "targets": [ + 1, + 4 + ] + } + }, + { + "watchCurrent": [ + [ + 4 + ], + "resume-token-1002" + ], + "watchSnapshot": 1002, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "b" + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "removed": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + }, + { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "b" + ] + ], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchCurrent": [ + [ + 1 + ], + "resume-token-1003" + ], + "watchSnapshot": 1003 + } + ] + }, + "Document remove message will cause docs to go in limbo": { + "describeName": "Limbo Documents:", + "itName": "Document remove message will cause docs to go in limbo", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/b", + 1001, + { + "key": "b" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1002" + ], + "watchSnapshot": 1002, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/b", + 1001, + { + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchEntity": { + "key": "collection/b", + "removedTargets": [ + 2 + ] + }, + "watchSnapshot": 1003, + "stateExpect": { + "limboDocs": [ + "collection/b" + ], + "activeTargets": { + "1": { + "query": { + "path": "collection/b", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 1 + ] + }, + { + "watchCurrent": [ + [ + 1 + ], + "resume-token-1004" + ], + "watchSnapshot": 1004, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/b", + 1001, + { + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + } +} diff --git a/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json b/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json new file mode 100644 index 0000000..5a02463 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json @@ -0,0 +1,1626 @@ +{ + "Documents in limit are replaced by remote event": { + "describeName": "Limits:", + "itName": "Documents in limit are replaced by remote event", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "expect": [ + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 1002, + { + "key": "b" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "removedTargets": [ + 2 + ] + }, + "watchSnapshot": 1002, + "expect": [ + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/b", + 1002, + { + "key": "b" + } + ] + ], + "removed": [ + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Deleted Document in limbo in full limit query": { + "describeName": "Limits:", + "itName": "Deleted Document in limbo in full limit query", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/b", + 1001, + { + "key": "b" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1002" + ], + "watchSnapshot": 1002, + "expect": [ + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/b", + 1001, + { + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchReset": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 1001, + { + "key": "b" + } + ], + [ + "collection/c", + 1002, + { + "key": "c" + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000, + "stateExpect": { + "limboDocs": [ + "collection/a" + ], + "activeTargets": { + "1": { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 1 + ] + }, + { + "watchCurrent": [ + [ + 1 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/c", + 1002, + { + "key": "c" + } + ] + ], + "removed": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Documents in limit can handle removed messages": { + "describeName": "Limits:", + "itName": "Documents in limit can handle removed messages", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "expect": [ + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 1002, + { + "key": "b" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchEntity": { + "key": "collection/c", + "removedTargets": [ + 2 + ] + }, + "watchSnapshot": 1002, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/b", + 1002, + { + "key": "b" + } + ] + ], + "removed": [ + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Documents in limit are can handle removed messages for only one of many query": { + "describeName": "Limits:", + "itName": "Documents in limit are can handle removed messages for only one of many query", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userListen": [ + 4, + { + "path": "collection", + "limit": 3, + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "limit": 3, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchAck": [ + 4 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "targets": [ + 4 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ] + }, + { + "watchCurrent": [ + [ + 4 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "expect": [ + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + }, + { + "query": { + "path": "collection", + "limit": 3, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 1002, + { + "key": "b" + } + ] + ], + "targets": [ + 2, + 4 + ] + } + }, + { + "watchEntity": { + "key": "collection/c", + "removedTargets": [ + 2 + ] + }, + "watchSnapshot": 1002, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "limit": 3, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/b", + 1002, + { + "key": "b" + } + ] + ], + "removed": [ + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + }, + { + "query": { + "path": "collection", + "limit": 3, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/b", + 1002, + { + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Multiple docs in limbo in full limit query": { + "describeName": "Limits:", + "itName": "Multiple docs in limbo in full limit query", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/b", + 1001, + { + "key": "b" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "expect": [ + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/b", + 1001, + { + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userListen": [ + 4, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/b", + 1001, + { + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 4 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/b", + 1001, + { + "key": "b" + } + ], + [ + "collection/c", + 1002, + { + "key": "c" + } + ], + [ + "collection/d", + 1003, + { + "key": "d" + } + ], + [ + "collection/e", + 1004, + { + "key": "e" + } + ], + [ + "collection/f", + 1005, + { + "key": "f" + } + ] + ], + "targets": [ + 4 + ] + } + }, + { + "watchCurrent": [ + [ + 4 + ], + "resume-token-1005" + ], + "watchSnapshot": 1005, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/c", + 1002, + { + "key": "c" + } + ], + [ + "collection/d", + 1003, + { + "key": "d" + } + ], + [ + "collection/e", + 1004, + { + "key": "e" + } + ], + [ + "collection/f", + 1005, + { + "key": "f" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchReset": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/e", + 1004, + { + "key": "e" + } + ], + [ + "collection/f", + 1005, + { + "key": "f" + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000, + "stateExpect": { + "limboDocs": [ + "collection/a", + "collection/b" + ], + "activeTargets": { + "1": { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "3": { + "query": { + "path": "collection/b", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 1 + ] + }, + { + "watchCurrent": [ + [ + 1 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "stateExpect": { + "limboDocs": [ + "collection/b", + "collection/c" + ], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "3": { + "query": { + "path": "collection/b", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "5": { + "query": { + "path": "collection/c", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + }, + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/c", + 1002, + { + "key": "c" + } + ] + ], + "removed": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchRemove": { + "targetIds": [ + 1 + ] + } + }, + { + "watchAck": [ + 3 + ] + }, + { + "watchCurrent": [ + [ + 3 + ], + "resume-token-2001" + ], + "watchSnapshot": 2001, + "stateExpect": { + "limboDocs": [ + "collection/c", + "collection/d" + ], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "5": { + "query": { + "path": "collection/c", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "7": { + "query": { + "path": "collection/d", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/b", + 1001, + { + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + }, + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/d", + 1003, + { + "key": "d" + } + ] + ], + "removed": [ + [ + "collection/b", + 1001, + { + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchRemove": { + "targetIds": [ + 3 + ] + } + }, + { + "watchAck": [ + 5 + ] + }, + { + "watchCurrent": [ + [ + 5 + ], + "resume-token-2002" + ], + "watchSnapshot": 2002, + "stateExpect": { + "limboDocs": [ + "collection/d" + ], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "7": { + "query": { + "path": "collection/d", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/c", + 1002, + { + "key": "c" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + }, + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/e", + 1004, + { + "key": "e" + } + ] + ], + "removed": [ + [ + "collection/c", + 1002, + { + "key": "c" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchRemove": { + "targetIds": [ + 5 + ] + } + }, + { + "watchAck": [ + 7 + ] + }, + { + "watchCurrent": [ + [ + 7 + ], + "resume-token-2003" + ], + "watchSnapshot": 2003, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/d", + 1003, + { + "key": "d" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + }, + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/f", + 1005, + { + "key": "f" + } + ] + ], + "removed": [ + [ + "collection/d", + 1003, + { + "key": "d" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchRemove": { + "targetIds": [ + 7 + ] + } + } + ] + } +} diff --git a/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json b/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json new file mode 100644 index 0000000..35704f2 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json @@ -0,0 +1,1524 @@ +{ + "Contents of query are cleared when listen is removed.": { + "describeName": "Listens:", + "itName": "Contents of query are cleared when listen is removed.", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "userListen": [ + 4, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "4": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + } + ] + }, + "Contents of query update when new data is received.": { + "describeName": "Listens:", + "itName": "Contents of query update when new data is received.", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 2000, + { + "key": "b" + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/b", + 2000, + { + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Ensure correct query results with latency-compensated deletes": { + "describeName": "Listens:", + "itName": "Ensure correct query results with latency-compensated deletes", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userDelete": "collection/b" + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "a": true + } + ], + [ + "collection/b", + 1000, + { + "b": true + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "a": true + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userListen": [ + 4, + { + "path": "collection", + "limit": 10, + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "limit": 10, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "limit": 10, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "a": true + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Will process removals without waiting for a consistent snapshot": { + "describeName": "Listens:", + "itName": "Will process removals without waiting for a consistent snapshot", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchRemove": { + "targetIds": [ + 2 + ], + "cause": { + "code": 8 + } + }, + "stateExpect": { + "activeTargets": {} + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 8, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Will gracefully process failed targets": { + "describeName": "Listens:", + "itName": "Will gracefully process failed targets", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection1", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection1", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userListen": [ + 4, + { + "path": "collection2", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection1", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection2", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchAck": [ + 4 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection1/a", + 1000, + { + "a": true + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection2/a", + 1001, + { + "b": true + } + ] + ], + "targets": [ + 4 + ] + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ], + "cause": { + "code": 8 + } + }, + "stateExpect": { + "activeTargets": { + "4": { + "query": { + "path": "collection2", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection1", + "filters": [], + "orderBys": [] + }, + "errorCode": 8, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchCurrent": [ + [ + 4 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection2", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection2/a", + 1001, + { + "b": true + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Will gracefully handle watch stream reverting snapshots": { + "describeName": "Listens:", + "itName": "Will gracefully handle watch stream reverting snapshots", + "tags": [], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "v": "v1000" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "v": "v1000" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 2000, + { + "v": "v2000" + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/a", + 2000, + { + "v": "v2000" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "resume-token-1000" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 2000, + { + "v": "v2000" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "v": "v1000" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000 + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 2000, + { + "v": "v2000" + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Will gracefully handle watch stream reverting snapshots (with restart)": { + "describeName": "Listens:", + "itName": "Will gracefully handle watch stream reverting snapshots (with restart)", + "tags": [], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "v": "v1000" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "v": "v1000" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 2000, + { + "v": "v2000" + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/a", + 2000, + { + "v": "v2000" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "restart": true, + "stateExpect": { + "activeTargets": {}, + "limboDocs": [] + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "resume-token-1000" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 2000, + { + "v": "v2000" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "v": "v1000" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000 + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 2000, + { + "v": "v2000" + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Individual documents cannot revert": { + "describeName": "Listens:", + "itName": "Individual documents cannot revert", + "tags": [], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "v": "v1000", + "visible": true + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "v": "v1000", + "visible": true + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": [ + 4, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "4": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "v": "v1000", + "visible": true + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 4 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 3000, + { + "v": "v3000", + "visible": false + } + ] + ], + "targets": [ + 4 + ] + } + }, + { + "watchCurrent": [ + [ + 4 + ], + "resume-token-4000" + ], + "watchSnapshot": 4000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/a", + 3000, + { + "v": "v3000", + "visible": false + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 4, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "watchRemove": { + "targetIds": [ + 4 + ] + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [] + }, + "resumeToken": "resume-token-1000" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 2000, + { + "v": "v2000", + "visible": false + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-5000" + ], + "watchSnapshot": 5000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": [ + 4, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "4": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "resume-token-4000" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 3000, + { + "v": "v3000", + "visible": false + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 4 + ] + }, + { + "watchEntity": { + "docs": [], + "targets": [ + 4 + ] + } + }, + { + "watchCurrent": [ + [ + 4 + ], + "resume-token-6000" + ], + "watchSnapshot": 6000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + } +} diff --git a/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json b/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json new file mode 100644 index 0000000..e58bae1 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json @@ -0,0 +1,151 @@ +{ + "Empty queries are resolved if client goes offline": { + "describeName": "Offline:", + "itName": "Empty queries are resolved if client goes offline", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + } + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + } + } + ] + }, + "A successful message delays offline status": { + "describeName": "Offline:", + "itName": "A successful message delays offline status", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + } + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + } + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + } + } + ] + } +} diff --git a/Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json b/Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json new file mode 100644 index 0000000..1009206 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json @@ -0,0 +1,155 @@ +{ + "orderBy applies filtering based on local state": { + "describeName": "OrderBy:", + "itName": "orderBy applies filtering based on local state", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userSet": [ + "collection/a", + { + "key": "a", + "sort": 1 + } + ] + }, + { + "userPatch": [ + "collection/b", + { + "sort": 2 + } + ] + }, + { + "userSet": [ + "collection/c", + { + "key": "b" + } + ] + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [ + [ + "sort", + "asc" + ] + ] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [ + [ + "sort", + "asc" + ] + ] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [ + [ + "sort", + "asc" + ] + ] + }, + "added": [ + [ + "collection/a", + 0, + { + "key": "a", + "sort": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 1001, + { + "key": "b" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [ + [ + "sort", + "asc" + ] + ] + }, + "added": [ + [ + "collection/b", + 1001, + { + "key": "b", + "sort": 2 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + } + ] + } +} diff --git a/Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json b/Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json new file mode 100644 index 0000000..158e337 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json @@ -0,0 +1,858 @@ +{ + "Local mutations are persisted and re-sent": { + "describeName": "Persistence:", + "itName": "Local mutations are persisted and re-sent", + "tags": [ + "persistence" + ], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userSet": [ + "collection/key1", + { + "foo": "bar" + } + ] + }, + { + "userSet": [ + "collection/key2", + { + "baz": "quu" + } + ] + }, + { + "restart": true, + "stateExpect": { + "activeTargets": {}, + "limboDocs": [], + "numOutstandingWrites": 2 + } + }, + { + "writeAck": { + "version": 1, + "expectUserCallback": false + } + }, + { + "writeAck": { + "version": 2, + "expectUserCallback": false + }, + "stateExpect": { + "numOutstandingWrites": 0 + } + } + ] + }, + "Persisted local mutations are visible to listeners": { + "describeName": "Persistence:", + "itName": "Persisted local mutations are visible to listeners", + "tags": [ + "persistence" + ], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userSet": [ + "collection/key1", + { + "foo": "bar" + } + ] + }, + { + "userSet": [ + "collection/key2", + { + "baz": "quu" + } + ] + }, + { + "restart": true, + "stateExpect": { + "activeTargets": {}, + "limboDocs": [] + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key1", + 0, + { + "foo": "bar" + }, + "local" + ], + [ + "collection/key2", + 0, + { + "baz": "quu" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + } + ] + }, + "Remote documents are persisted": { + "describeName": "Persistence:", + "itName": "Remote documents are persisted", + "tags": [ + "persistence" + ], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 1000, + { + "foo": "bar" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 1000, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "restart": true, + "stateExpect": { + "activeTargets": {}, + "limboDocs": [] + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "resume-token-1000" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 1000, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Remote documents from watch are not GC'd": { + "describeName": "Persistence:", + "itName": "Remote documents from watch are not GC'd", + "tags": [ + "persistence" + ], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 1000, + { + "foo": "bar" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 1000, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "resume-token-1000" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 1000, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Remote documents from user sets are not GC'd": { + "describeName": "Persistence:", + "itName": "Remote documents from user sets are not GC'd", + "tags": [ + "persistence" + ], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ] + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Mutation Queue is persisted across uid switches": { + "describeName": "Persistence:", + "itName": "Mutation Queue is persisted across uid switches", + "tags": [ + "persistence" + ], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userSet": [ + "users/anon", + { + "uid": "anon" + } + ] + }, + { + "changeUser": "user1", + "stateExpect": { + "numOutstandingWrites": 0 + } + }, + { + "userSet": [ + "users/user1", + { + "uid": "user1" + } + ] + }, + { + "userSet": [ + "users/user1", + { + "uid": "user1", + "extra": true + } + ] + }, + { + "changeUser": null, + "stateExpect": { + "numOutstandingWrites": 1 + } + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "changeUser": "user1", + "stateExpect": { + "numOutstandingWrites": 2 + } + }, + { + "writeAck": { + "version": 2000, + "expectUserCallback": true + } + }, + { + "writeAck": { + "version": 3000, + "expectUserCallback": true + } + } + ] + }, + "Mutation Queue is persisted across uid switches (with restarts)": { + "describeName": "Persistence:", + "itName": "Mutation Queue is persisted across uid switches (with restarts)", + "tags": [ + "persistence" + ], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userSet": [ + "users/anon", + { + "uid": "anon" + } + ] + }, + { + "changeUser": "user1", + "stateExpect": { + "numOutstandingWrites": 0 + } + }, + { + "userSet": [ + "users/user1", + { + "uid": "user1" + } + ] + }, + { + "userSet": [ + "users/user1", + { + "uid": "user1", + "extra": true + } + ] + }, + { + "changeUser": null + }, + { + "restart": true, + "stateExpect": { + "activeTargets": {}, + "limboDocs": [], + "numOutstandingWrites": 1 + } + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": false + } + }, + { + "changeUser": "user1" + }, + { + "restart": true, + "stateExpect": { + "activeTargets": {}, + "limboDocs": [], + "numOutstandingWrites": 2 + } + }, + { + "writeAck": { + "version": 2000, + "expectUserCallback": false + } + }, + { + "writeAck": { + "version": 3000, + "expectUserCallback": false + } + } + ] + }, + "Visible mutations reflect uid switches": { + "describeName": "Persistence:", + "itName": "Visible mutations reflect uid switches", + "tags": [ + "persistence" + ], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "users", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "users", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "users/existing", + 0, + { + "uid": "existing" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-500" + ], + "watchSnapshot": 500, + "expect": [ + { + "query": { + "path": "users", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "users/existing", + 0, + { + "uid": "existing" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userSet": [ + "users/anon", + { + "uid": "anon" + } + ], + "expect": [ + { + "query": { + "path": "users", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "users/anon", + 0, + { + "uid": "anon" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "changeUser": "user1", + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "users", + "filters": [], + "orderBys": [] + }, + "resumeToken": "resume-token-500" + } + } + }, + "expect": [ + { + "query": { + "path": "users", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "users/anon", + 0, + { + "uid": "anon" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userSet": [ + "users/user1", + { + "uid": "user1" + } + ], + "expect": [ + { + "query": { + "path": "users", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "users/user1", + 0, + { + "uid": "user1" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "changeUser": null, + "expect": [ + { + "query": { + "path": "users", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "users/anon", + 0, + { + "uid": "anon" + }, + "local" + ] + ], + "removed": [ + [ + "users/user1", + 0, + { + "uid": "user1" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + } + ] + } +} diff --git a/Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json b/Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json new file mode 100644 index 0000000..edb0751 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json @@ -0,0 +1,559 @@ +{ + "Waits for watch to remove targets": { + "describeName": "Remote store:", + "itName": "Waits for watch to remove targets", + "tags": [], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token" + ], + "watchSnapshot": 1000 + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Waits for watch to ack last target add": { + "describeName": "Remote store:", + "itName": "Waits for watch to ack last target add", + "tags": [], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token" + ], + "watchSnapshot": 1000 + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 1000, + { + "key": "b" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001 + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/c", + 1000, + { + "key": "c" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001 + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/d", + 1000, + { + "key": "d" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/d", + 1000, + { + "key": "d" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Cleans up watch state correctly": { + "describeName": "Remote store:", + "itName": "Cleans up watch state correctly", + "tags": [], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + } +} diff --git a/Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json b/Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json new file mode 100644 index 0000000..25ea84a --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json @@ -0,0 +1,250 @@ +{ + "Resume tokens are sent after watch stream restarts": { + "describeName": "Resume tokens:", + "itName": "Resume tokens are sent after watch stream restarts", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "custom-query-resume-token" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + }, + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "custom-query-resume-token" + } + } + } + } + ] + }, + "Resume tokens are used across new listens": { + "describeName": "Resume tokens:", + "itName": "Resume tokens are used across new listens", + "tags": [], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "custom-query-resume-token" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "custom-query-resume-token" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 2 + ], + "watchSnapshot": 1001 + } + ] + } +} diff --git a/Firestore/Example/Tests/SpecTests/json/write_spec_test.json b/Firestore/Example/Tests/SpecTests/json/write_spec_test.json new file mode 100644 index 0000000..60ed107 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/write_spec_test.json @@ -0,0 +1,5437 @@ +{ + "Two sequential writes to different documents smoke test.": { + "describeName": "Writes:", + "itName": "Two sequential writes to different documents smoke test.", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "v": 1 + } + ], + [ + "collection/b", + 500, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "v": 1 + } + ], + [ + "collection/b", + 500, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userSet": [ + "collection/a", + { + "v": 2 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/a", + 1000, + { + "v": 2 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 2000, + { + "v": 2 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000 + }, + { + "writeAck": { + "version": 2000, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a", + 2000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userSet": [ + "collection/b", + { + "v": 2 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/b", + 500, + { + "v": 2 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 2500, + { + "v": 2 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 3000 + }, + { + "writeAck": { + "version": 3000, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/b", + 2500, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Event is raised for a local set before and after the write ack": { + "describeName": "Writes:", + "itName": "Event is raised for a local set before and after the write ack", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 1000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 1000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userSet": [ + "collection/key", + { + "v": 2 + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/key", + 1000, + { + "v": 2 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 2000, + { + "v": 2 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000 + }, + { + "writeAck": { + "version": 2000, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/key", + 2000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Cache will not keep data for an outdated write ack": { + "describeName": "Writes:", + "itName": "Cache will not keep data for an outdated write ack", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 1000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 1000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userSet": [ + "collection/key", + { + "v": 2 + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/key", + 1000, + { + "v": 2 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 10000, + { + "v": 3 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 10000 + }, + { + "writeAck": { + "version": 2000, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/key", + 10000, + { + "v": 3 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Cache raises correct event if write is acked before watch delivers it": { + "describeName": "Writes:", + "itName": "Cache raises correct event if write is acked before watch delivers it", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 1000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 1000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userSet": [ + "collection/key", + { + "v": 2 + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/key", + 1000, + { + "v": 2 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 2000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 2000, + { + "v": 2 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/key", + 2000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Cache will hold local write until watch catches up": { + "describeName": "Writes:", + "itName": "Cache will hold local write until watch catches up", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 1000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 1000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userSet": [ + "collection/key", + { + "v": 3 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/key", + 1000, + { + "v": 3 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 3000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 2000, + { + "v": 2 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000 + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 3000, + { + "doc": "b" + } + ], + [ + "collection/key", + 3000, + { + "v": 3 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 3000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/b", + 3000, + { + "doc": "b" + } + ] + ], + "metadata": [ + [ + "collection/key", + 3000, + { + "v": 3 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes are pipelined": { + "describeName": "Writes:", + "itName": "Writes are pipelined", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token" + ] + }, + { + "userSet": [ + "collection/a0", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a0", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a1", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a1", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a2", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a2", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a3", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a3", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a4", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a4", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a5", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a5", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a6", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a6", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a7", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a7", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a8", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a8", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a9", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a9", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a10", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a10", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a11", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a11", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a12", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a12", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a13", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a13", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a14", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a14", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ], + "stateExpect": { + "numOutstandingWrites": 10 + } + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a0", + 1000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a0", + 1000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 2000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a1", + 2000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a1", + 2000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 3000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a2", + 3000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 3000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a2", + 3000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 4000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a3", + 4000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 4000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a3", + 4000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 5000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a4", + 5000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 5000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a4", + 5000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 6000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a5", + 6000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 6000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a5", + 6000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 7000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a6", + 7000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 7000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a6", + 7000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 8000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a7", + 8000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 8000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a7", + 8000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 9000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a8", + 9000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 9000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a8", + 9000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 10000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a9", + 10000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 10000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a9", + 10000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 11000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a10", + 11000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 11000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a10", + 11000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 12000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a11", + 12000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 12000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a11", + 12000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 13000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a12", + 13000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 13000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a12", + 13000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 14000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a13", + 14000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 14000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a13", + 14000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 15000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a14", + 15000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 15000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a14", + 15000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Pipelined writes can fail": { + "describeName": "Writes:", + "itName": "Pipelined writes can fail", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/a0", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a0", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a1", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a1", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a2", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a2", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a3", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a3", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a4", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a4", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a5", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a5", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a6", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a6", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a7", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a7", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a8", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a8", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a9", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a9", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a10", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a10", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a11", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a11", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a12", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a12", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a13", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a13", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a14", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a14", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ], + "stateExpect": { + "numOutstandingWrites": 10 + } + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a0", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a1", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a2", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a3", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a4", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a5", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a6", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a7", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a8", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a9", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a10", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a11", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a12", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a13", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a14", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ], + "stateExpect": { + "numOutstandingWrites": 0 + } + } + ] + }, + "Failed writes are released immediately.": { + "describeName": "Writes:", + "itName": "Failed writes are released immediately.", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userSet": [ + "collection/b", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/b", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 2000, + "expectUserCallback": true + } + }, + { + "userSet": [ + "collection/a", + { + "v": 2 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/a", + 1000, + { + "v": 2 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/a", + 1000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 2000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/b", + 2000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Held writes are not re-sent.": { + "describeName": "Writes:", + "itName": "Held writes are not re-sent.", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-500" + ], + "watchSnapshot": 500, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userSet": [ + "collection/a", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "userSet": [ + "collection/b", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/b", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 2000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "v": 1 + } + ], + [ + "collection/b", + 2000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a", + 1000, + { + "v": 1 + } + ], + [ + "collection/b", + 2000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Held writes are released when there are no queries left.": { + "describeName": "Writes:", + "itName": "Held writes are released when there are no queries left.", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-500" + ], + "watchSnapshot": 500, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userSet": [ + "collection/a", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "userListen": [ + 4, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "4": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + } + ] + }, + "Writes that fail with code invalid-argument are rejected": { + "describeName": "Writes:", + "itName": "Writes that fail with code invalid-argument are rejected", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 3 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code not-found are rejected": { + "describeName": "Writes:", + "itName": "Writes that fail with code not-found are rejected", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 5 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code already-exists are rejected": { + "describeName": "Writes:", + "itName": "Writes that fail with code already-exists are rejected", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 6 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code permission-denied are rejected": { + "describeName": "Writes:", + "itName": "Writes that fail with code permission-denied are rejected", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code failed-precondition are rejected": { + "describeName": "Writes:", + "itName": "Writes that fail with code failed-precondition are rejected", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 9 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code aborted are rejected": { + "describeName": "Writes:", + "itName": "Writes that fail with code aborted are rejected", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 10 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code out-of-range are rejected": { + "describeName": "Writes:", + "itName": "Writes that fail with code out-of-range are rejected", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 11 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code unimplemented are rejected": { + "describeName": "Writes:", + "itName": "Writes that fail with code unimplemented are rejected", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 12 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code data-loss are rejected": { + "describeName": "Writes:", + "itName": "Writes that fail with code data-loss are rejected", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 15 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code cancelled are retried": { + "describeName": "Writes:", + "itName": "Writes that fail with code cancelled are retried", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 1 + }, + "expectUserCallback": false + } + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code unknown are retried": { + "describeName": "Writes:", + "itName": "Writes that fail with code unknown are retried", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 2 + }, + "expectUserCallback": false + } + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code deadline-exceeded are retried": { + "describeName": "Writes:", + "itName": "Writes that fail with code deadline-exceeded are retried", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 4 + }, + "expectUserCallback": false + } + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code resource-exhausted are retried": { + "describeName": "Writes:", + "itName": "Writes that fail with code resource-exhausted are retried", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 8 + }, + "expectUserCallback": false + } + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code internal are retried": { + "describeName": "Writes:", + "itName": "Writes that fail with code internal are retried", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 13 + }, + "expectUserCallback": false + } + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code unavailable are retried": { + "describeName": "Writes:", + "itName": "Writes that fail with code unavailable are retried", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 14 + }, + "expectUserCallback": false + } + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code unauthenticated are retried": { + "describeName": "Writes:", + "itName": "Writes that fail with code unauthenticated are retried", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 16 + }, + "expectUserCallback": false + } + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Ensure correct events after patching a doc (including a delete) and getting watcher events.": { + "describeName": "Writes:", + "itName": "Ensure correct events after patching a doc (including a delete) and getting watcher events.", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/doc", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/doc", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/doc", + 1000, + { + "a": { + "b": 2 + }, + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-500" + ], + "watchSnapshot": 500, + "expect": [ + { + "query": { + "path": "collection/doc", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/doc", + 1000, + { + "a": { + "b": 2 + }, + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userPatch": [ + "collection/doc", + { + "v": 2, + "a.c": "<DELETE>" + } + ], + "expect": [ + { + "query": { + "path": "collection/doc", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/doc", + 1000, + { + "a": { + "b": 2 + }, + "v": 2 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/doc", + 2000, + { + "a": { + "b": 2 + }, + "v": 2 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000 + }, + { + "writeAck": { + "version": 2000, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/doc", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/doc", + 2000, + { + "a": { + "b": 2 + }, + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + } +} diff --git a/Firestore/Example/Tests/Tests-Info.plist b/Firestore/Example/Tests/Tests-Info.plist new file mode 100644 index 0000000..169b6f7 --- /dev/null +++ b/Firestore/Example/Tests/Tests-Info.plist @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundlePackageType</key> + <string>BNDL</string> + <key>CFBundleShortVersionString</key> + <string>1.0</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>1</string> +</dict> +</plist> diff --git a/Firestore/Example/Tests/Util/FSTAssertTests.m b/Firestore/Example/Tests/Util/FSTAssertTests.m new file mode 100644 index 0000000..f0734df --- /dev/null +++ b/Firestore/Example/Tests/Util/FSTAssertTests.m @@ -0,0 +1,105 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Util/FSTAssert.h" + +#import <XCTest/XCTest.h> + +@interface FSTAssertTests : XCTestCase +@end + +@implementation FSTAssertTests + +- (void)testFail { + @try { + [self failingMethod]; + XCTFail("Should not have succeeded"); + } @catch (NSException *ex) { + XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException); + XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: 0:foo:bar"); + } +} + +// A method guaranteed to fail. Note that the return type is intentionally something that is +// not actually returned in the body to ensure that the function attribute declarations in the +// macro properly mark this macro invocation as never returning. +- (int)failingMethod { + FSTFail(@"%d:%s:%@", 0, "foo", @"bar"); +} + +- (void)testCFail { + @try { + failingFunction(); + XCTFail("Should not have succeeded"); + } @catch (NSException *ex) { + XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException); + XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: 0:foo:bar"); + } +} + +// A function guaranteed to fail. Note that the return type is intentionally something that is +// not actually returned in the body to ensure that the function attribute declarations in the +// macro properly mark this macro invocation as never returning. +int failingFunction() { + FSTCFail(@"%d:%s:%@", 0, "foo", @"bar"); +} + +- (void)testAssertNonFailing { + @try { + FSTAssert(YES, @"shouldn't fail"); + } @catch (NSException *ex) { + XCTFail("Should not have failed, but got %@", ex); + } +} + +- (void)testAssertFailing { + @try { + FSTAssert(NO, @"should fail"); + XCTFail("Should not have succeeded"); + } @catch (NSException *ex) { + XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException); + XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: should fail"); + } +} + +- (void)testCAssertNonFailing { + @try { + nonAssertingFunction(); + } @catch (NSException *ex) { + XCTFail("Should not have failed, but got %@", ex); + } +} + +int nonAssertingFunction() { + FSTCAssert(YES, @"shouldn't fail"); + return 0; +} + +- (void)testCAssertFailing { + @try { + assertingFunction(); + XCTFail("Should not have succeeded"); + } @catch (NSException *ex) { + XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException); + XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: should fail"); + } +} + +int assertingFunction() { + FSTCAssert(NO, @"should fail"); +} + +@end diff --git a/Firestore/Example/Tests/Util/FSTComparisonTests.m b/Firestore/Example/Tests/Util/FSTComparisonTests.m new file mode 100644 index 0000000..9728f20 --- /dev/null +++ b/Firestore/Example/Tests/Util/FSTComparisonTests.m @@ -0,0 +1,143 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Util/FSTComparison.h" + +#import <XCTest/XCTest.h> + +union DoubleBits { + double d; + uint64_t bits; +}; + +#define ASSERT_BIT_EQUALS(expected, actual) \ + do { \ + union DoubleBits expectedBits = {.d = expected}; \ + union DoubleBits actualBits = {.d = expected}; \ + if (expectedBits.bits != actualBits.bits) { \ + XCTFail(@"Expected <%f> to compare equal to <%f> with bits <%llX> equal to <%llX>", actual, \ + expected, actualBits.bits, expectedBits.bits); \ + } \ + } while (0); + +#define ASSERT_ORDERED_SAME(doubleValue, longValue) \ + do { \ + NSComparisonResult result = FSTCompareMixed(doubleValue, longValue); \ + if (result != NSOrderedSame) { \ + XCTFail(@"Expected <%f> to compare equal to <%lld>", doubleValue, longValue); \ + } \ + } while (0); + +#define ASSERT_ORDERED_DESCENDING(doubleValue, longValue) \ + do { \ + NSComparisonResult result = FSTCompareMixed(doubleValue, longValue); \ + if (result != NSOrderedDescending) { \ + XCTFail(@"Expected <%f> to compare equal to <%lld>", doubleValue, longValue); \ + } \ + } while (0); + +#define ASSERT_ORDERED_ASCENDING(doubleValue, longValue) \ + do { \ + NSComparisonResult result = FSTCompareMixed(doubleValue, longValue); \ + if (result != NSOrderedAscending) { \ + XCTFail(@"Expected <%f> to compare equal to <%lld>", doubleValue, longValue); \ + } \ + } while (0); + +@interface FSTComparisonTests : XCTestCase +@end + +@implementation FSTComparisonTests + +- (void)testMixedComparison { + // Infinities + ASSERT_ORDERED_ASCENDING(-INFINITY, LLONG_MIN); + ASSERT_ORDERED_ASCENDING(-INFINITY, LLONG_MAX); + ASSERT_ORDERED_ASCENDING(-INFINITY, 0LL); + + ASSERT_ORDERED_DESCENDING(INFINITY, LLONG_MIN); + ASSERT_ORDERED_DESCENDING(INFINITY, LLONG_MAX); + ASSERT_ORDERED_DESCENDING(INFINITY, 0LL); + + // NaN + ASSERT_ORDERED_ASCENDING(NAN, LLONG_MIN); + ASSERT_ORDERED_ASCENDING(NAN, LLONG_MAX); + ASSERT_ORDERED_ASCENDING(NAN, 0LL); + + // Large values (note DBL_MIN is positive and near zero). + ASSERT_ORDERED_ASCENDING(-DBL_MAX, LLONG_MIN); + + // Tests around LLONG_MIN + ASSERT_BIT_EQUALS((double)LLONG_MIN, -0x1.0p63); + ASSERT_ORDERED_SAME(-0x1.0p63, LLONG_MIN); + ASSERT_ORDERED_ASCENDING(-0x1.0p63, LLONG_MIN + 1); + + XCTAssertLessThan(-0x1.0000000000001p63, -0x1.0p63); + ASSERT_ORDERED_ASCENDING(-0x1.0000000000001p63, LLONG_MIN); + ASSERT_ORDERED_DESCENDING(-0x1.FFFFFFFFFFFFFp62, LLONG_MIN); + + // Tests around LLONG_MAX + // Note LLONG_MAX cannot be exactly represented by a double, so the system rounds it to the + // nearest double, which is 2^63. This number, in turn is larger than the maximum representable + // as a long. + ASSERT_BIT_EQUALS(0x1.0p63, (double)LLONG_MAX); + ASSERT_ORDERED_DESCENDING(0x1.0p63, LLONG_MAX); + + // The largest value with an exactly long representation + XCTAssertEqual((long)0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFC00LL); + ASSERT_ORDERED_SAME(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFC00LL); + + ASSERT_ORDERED_DESCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFB00LL); + ASSERT_ORDERED_DESCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFBFFLL); + ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFC01LL); + ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFD00LL); + + ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFEp62, 0x7FFFFFFFFFFFFC00LL); + + // Tests around MAX_SAFE_INTEGER + ASSERT_ORDERED_SAME(0x1.FFFFFFFFFFFFFp52, 0x1FFFFFFFFFFFFFLL); + ASSERT_ORDERED_DESCENDING(0x1.FFFFFFFFFFFFFp52, 0x1FFFFFFFFFFFFELL); + ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFEp52, 0x1FFFFFFFFFFFFFLL); + ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFFp52, 0x20000000000000LL); + + // Tests around MIN_SAFE_INTEGER + ASSERT_ORDERED_SAME(-0x1.FFFFFFFFFFFFFp52, -0x1FFFFFFFFFFFFFLL); + ASSERT_ORDERED_ASCENDING(-0x1.FFFFFFFFFFFFFp52, -0x1FFFFFFFFFFFFELL); + ASSERT_ORDERED_DESCENDING(-0x1.FFFFFFFFFFFFEp52, -0x1FFFFFFFFFFFFFLL); + ASSERT_ORDERED_DESCENDING(-0x1.FFFFFFFFFFFFFp52, -0x20000000000000LL); + + // Tests around zero. + ASSERT_ORDERED_SAME(-0.0, 0LL); + ASSERT_ORDERED_SAME(0.0, 0LL); + + // The smallest representable positive value should be greater than zero + ASSERT_ORDERED_DESCENDING(DBL_MIN, 0LL); + ASSERT_ORDERED_ASCENDING(-DBL_MIN, 0LL); + + // Note that 0x1.0p-1074 is a hex floating point literal representing the minimum subnormal + // number: <https://en.wikipedia.org/wiki/Denormal_number>. + double minSubNormal = 0x1.0p-1074; + ASSERT_ORDERED_DESCENDING(minSubNormal, 0LL); + ASSERT_ORDERED_ASCENDING(-minSubNormal, 0LL); + + // Other sanity checks + ASSERT_ORDERED_ASCENDING(0.5, 1LL); + ASSERT_ORDERED_DESCENDING(0.5, 0LL); + ASSERT_ORDERED_ASCENDING(1.5, 2LL); + ASSERT_ORDERED_DESCENDING(1.5, 1LL); +} + +@end diff --git a/Firestore/Example/Tests/Util/FSTEventAccumulator.h b/Firestore/Example/Tests/Util/FSTEventAccumulator.h new file mode 100644 index 0000000..ae5392c --- /dev/null +++ b/Firestore/Example/Tests/Util/FSTEventAccumulator.h @@ -0,0 +1,41 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +@class FIRDocumentSnapshot; +@class FIRQuerySnapshot; +@class XCTestCase; +@class XCTestExpectation; + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^FSTGenericEventHandler)(id _Nullable, NSError *error); + +@interface FSTEventAccumulator : NSObject + ++ (instancetype)accumulatorForTest:(XCTestCase *)testCase; + +- (instancetype)init NS_UNAVAILABLE; + +- (id)awaitEventWithName:(NSString *)name; + +- (NSArray<id> *)awaitEvents:(NSUInteger)events name:(NSString *)name; + +@property(nonatomic, strong, readonly) FSTGenericEventHandler handler; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Util/FSTEventAccumulator.m b/Firestore/Example/Tests/Util/FSTEventAccumulator.m new file mode 100644 index 0000000..c7e5b41 --- /dev/null +++ b/Firestore/Example/Tests/Util/FSTEventAccumulator.m @@ -0,0 +1,94 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FSTEventAccumulator.h" + +#import <XCTest/XCTest.h> + +#import "Util/FSTAssert.h" + +#import "XCTestCase+Await.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTEventAccumulator () +- (instancetype)initForTest:(XCTestCase *)testCase NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, weak, readonly) XCTestCase *testCase; + +@property(nonatomic, assign) NSUInteger maxEvents; +@property(nonatomic, strong, nullable) XCTestExpectation *expectation; +@end + +@implementation FSTEventAccumulator { + NSMutableArray<id> *_events; +} + ++ (instancetype)accumulatorForTest:(XCTestCase *)testCase { + return [[FSTEventAccumulator alloc] initForTest:testCase]; +} + +- (instancetype)initForTest:(XCTestCase *)testCase { + if (self = [super init]) { + _testCase = testCase; + _events = [NSMutableArray array]; + } + return self; +} + +- (NSArray<id> *)awaitEvents:(NSUInteger)events name:(NSString *)name { + @synchronized(self) { + FSTAssert(!self.expectation, @"Existing expectation still pending?"); + self.expectation = [self.testCase expectationWithDescription:name]; + self.maxEvents = self.maxEvents + events; + [self checkFulfilled]; + } + + // Don't await within @synchronized block to avoid deadlocking. + [self.testCase awaitExpectations]; + + return [_events subarrayWithRange:NSMakeRange(self.maxEvents - events, events)]; +} + +- (id)awaitEventWithName:(NSString *)name { + NSArray<id> *events = [self awaitEvents:1 name:name]; + return events[0]; +} + +// Overrides the handler property +- (void (^)(id _Nullable, NSError *))handler { + return ^void(id _Nullable value, NSError *error) { + // We can't store nil in the _events array, but these are still interesting to tests so store + // NSNull instead. + id event = value ? value : [NSNull null]; + + @synchronized(self) { + [_events addObject:event]; + [self checkFulfilled]; + } + }; +} + +- (void)checkFulfilled { + if (_events.count >= self.maxEvents) { + [self.expectation fulfill]; + self.expectation = nil; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Util/FSTHelpers.h b/Firestore/Example/Tests/Util/FSTHelpers.h new file mode 100644 index 0000000..3facc9c --- /dev/null +++ b/Firestore/Example/Tests/Util/FSTHelpers.h @@ -0,0 +1,258 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +#import "API/FIRDocumentReference+Internal.h" +#import "Core/FSTTypes.h" +#import "Model/FSTDocumentDictionary.h" +#import "Model/FSTDocumentKeySet.h" + +@class FIRGeoPoint; +@class FSTDeleteMutation; +@class FSTDeletedDocument; +@class FSTDocument; +@class FSTDocumentKeyReference; +@class FSTDocumentSet; +@class FSTFieldPath; +@class FSTFieldValue; +@class FSTLocalViewChanges; +@class FSTPatchMutation; +@class FSTQuery; +@class FSTRemoteEvent; +@class FSTResourceName; +@class FSTResourcePath; +@class FSTSetMutation; +@class FSTSnapshotVersion; +@class FSTSortOrder; +@class FSTTargetChange; +@class FSTTimestamp; +@class FSTTransformMutation; +@class FSTView; +@class FSTViewSnapshot; +@class FSTObjectValue; +@protocol FSTFilter; + +NS_ASSUME_NONNULL_BEGIN + +#if __cplusplus +extern "C" { +#endif + +#define FSTAssertIsKindOfClass(value, classType) \ + do { \ + XCTAssertEqualObjects([value class], [classType class]); \ + } while (0); + +/** + * Asserts that the given NSSet of FSTDocumentKeys contains exactly the given expected keys. + * This is a macro instead of a method so that the failure shows up on the right line. + * + * @param actualSet An NSSet of FSTDocumentKeys. + * @param expectedArray A sorted array of keys that actualSet must be equal to (after converting + * to an array and sorting). + */ +#define FSTAssertEqualSets(actualSet, expectedArray) \ + do { \ + NSArray<FSTDocumentKey *> *actual = [(actualSet)allObjects]; \ + actual = [actual sortedArrayUsingSelector:@selector(compare:)]; \ + XCTAssertEqualObjects(actual, (expectedArray)); \ + } while (0) + +/** + * Takes an array of "equality group" arrays and asserts that the compare: selector returns the + * same as compare: on the indexes of the "equality groups" (NSOrderedSame for items in the same + * group). + */ +#define FSTAssertComparisons(values) \ + do { \ + for (NSUInteger i = 0; i < [values count]; i++) { \ + for (id left in values[i]) { \ + for (NSUInteger j = 0; j < [values count]; j++) { \ + for (id right in values[j]) { \ + NSComparisonResult expected = [@(i) compare:@(j)]; \ + NSComparisonResult result = [left compare:right]; \ + NSComparisonResult inverseResult = [right compare:left]; \ + XCTAssertEqual(result, expected, @"comparing %@ with %@ at (%lu, %lu)", left, right, \ + i, j); \ + XCTAssertEqual(inverseResult, -expected, @"comparing %@ with %@ at (%lu, %lu)", right, \ + left, j, i); \ + } \ + } \ + } \ + } \ + } while (0) + +/** + * Takes an array of "equality group" arrays and asserts that the isEqual: selector returns TRUE + * if-and-only-if items are in the same group. + * + * Additionally checks that the hash: selector returns the same value for items in the same group. + */ +#define FSTAssertEqualityGroups(values) \ + do { \ + for (NSUInteger i = 0; i < [values count]; i++) { \ + for (id left in values[i]) { \ + for (NSUInteger j = 0; j < [values count]; j++) { \ + for (id right in values[j]) { \ + if (i == j) { \ + XCTAssertEqualObjects(left, right); \ + XCTAssertEqual([left hash], [right hash], @"comparing hash of %@ with hash of %@", \ + left, right); \ + } else { \ + XCTAssertNotEqualObjects(left, right); \ + } \ + } \ + } \ + } \ + } \ + } while (0) + +// Helper for validating API exceptions. +#define FSTAssertThrows(expression, exceptionReason, ...) \ + ({ \ + BOOL __didThrow = NO; \ + @try { \ + (void)(expression); \ + } @catch (NSException * exception) { \ + __didThrow = YES; \ + XCTAssertEqualObjects(exception.reason, exceptionReason); \ + } \ + XCTAssertTrue(__didThrow, ##__VA_ARGS__); \ + }) + +/** Creates a new FSTTimestamp from components. Note that year, month, and day are all one-based. */ +FSTTimestamp *FSTTestTimestamp(int year, int month, int day, int hour, int minute, int second); + +/** Creates a new NSDate from components. Note that year, month, and day are all one-based. */ +NSDate *FSTTestDate(int year, int month, int day, int hour, int minute, int second); + +/** + * Creates a new NSData from the var args of bytes, must be terminated with a negative byte + */ +NSData *FSTTestData(int bytes, ...); + +/** Creates a new GeoPoint from the latitude and longitude values */ +FIRGeoPoint *FSTTestGeoPoint(double latitude, double longitude); + +/** + * Creates a new NSDateComponents from components. Note that year, month, and day are all + * one-based. + */ +NSDateComponents *FSTTestDateComponents( + int year, int month, int day, int hour, int minute, int second); + +FSTFieldPath *FSTTestFieldPath(NSString *field); + +/** Wraps a plain value into an FSTFieldValue instance. */ +FSTFieldValue *FSTTestFieldValue(id _Nullable value); + +/** Wraps a NSDictionary value into an FSTObjectValue instance. */ +FSTObjectValue *FSTTestObjectValue(NSDictionary<NSString *, id> *data); + +/** A convenience method for creating document keys for tests. */ +FSTDocumentKey *FSTTestDocKey(NSString *path); + +/** A convenience method for creating a document key set for tests. */ +FSTDocumentKeySet *FSTTestDocKeySet(NSArray<FSTDocumentKey *> *keys); + +/** Allow tests to just use an int literal for versions. */ +typedef int64_t FSTTestSnapshotVersion; + +/** A convenience method for creating snapshot versions for tests. */ +FSTSnapshotVersion *FSTTestVersion(FSTTestSnapshotVersion version); + +/** A convenience method for creating docs for tests. */ +FSTDocument *FSTTestDoc(NSString *path, + FSTTestSnapshotVersion version, + NSDictionary<NSString *, id> *data, + BOOL hasMutations); + +/** A convenience method for creating deleted docs for tests. */ +FSTDeletedDocument *FSTTestDeletedDoc(NSString *path, FSTTestSnapshotVersion version); + +/** A convenience method for creating resource paths from a path string. */ +FSTResourcePath *FSTTestPath(NSString *path); + +/** + * A convenience method for creating a document reference from a path string. + */ +FSTDocumentKeyReference *FSTTestRef(NSString *projectID, NSString *databaseID, NSString *path); + +/** A convenience method for creating a query for the given path (without any other filters). */ +FSTQuery *FSTTestQuery(NSString *path); + +/** + * A convenience method to create a FSTFilter using a string representation for both field + * and operator (<, <=, ==, >=, >). + */ +id<FSTFilter> FSTTestFilter(NSString *field, NSString *op, id value); + +/** A convenience method for creating sort orders. */ +FSTSortOrder *FSTTestOrderBy(NSString *field, NSString *direction); + +/** + * Creates an NSComparator that will compare FSTDocuments by the given fieldPath string then by + * key. + */ +NSComparator FSTTestDocComparator(NSString *fieldPath); + +/** + * Creates a FSTDocumentSet based on the given comparator, initially containing the given + * documents. + */ +FSTDocumentSet *FSTTestDocSet(NSComparator comp, NSArray<FSTDocument *> *docs); + +/** Computes changes to the view with the docs and then applies them and returns the snapshot. */ +FSTViewSnapshot *_Nullable FSTTestApplyChanges(FSTView *view, + NSArray<FSTMaybeDocument *> *docs, + FSTTargetChange *_Nullable targetChange); + +/** Creates a set mutation for the document key at the given path. */ +FSTSetMutation *FSTTestSetMutation(NSString *path, NSDictionary<NSString *, id> *values); + +/** Creates a patch mutation for the document key at the given path. */ +FSTPatchMutation *FSTTestPatchMutation(NSString *path, + NSDictionary<NSString *, id> *values, + NSArray<FSTFieldPath *> *_Nullable updateMask); + +FSTTransformMutation *FSTTestTransformMutation(NSString *path, + NSArray<NSString *> *serverTimestampFields); + +/** Creates a delete mutation for the document key at the given path. */ +FSTDeleteMutation *FSTTestDeleteMutation(NSString *path); + +/** Converts a list of documents to a sorted map. */ +FSTMaybeDocumentDictionary *FSTTestDocUpdates(NSArray<FSTMaybeDocument *> *docs); + +/** Creates a remote event with changes to a document. */ +FSTRemoteEvent *FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc, + NSArray<NSNumber *> *updatedInTargets, + NSArray<NSNumber *> *removedFromTargets); + +/** Creates a test view changes. */ +FSTLocalViewChanges *FSTTestViewChanges(FSTQuery *query, + NSArray<NSString *> *addedKeys, + NSArray<NSString *> *removedKeys); + +/** Creates a resume token to match the given snapshot version. */ +NSData *_Nullable FSTTestResumeTokenFromSnapshotVersion(FSTTestSnapshotVersion watchSnapshot); + +#if __cplusplus +} // extern "C" +#endif + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Util/FSTHelpers.m b/Firestore/Example/Tests/Util/FSTHelpers.m new file mode 100644 index 0000000..3b7f47f --- /dev/null +++ b/Firestore/Example/Tests/Util/FSTHelpers.m @@ -0,0 +1,348 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FSTHelpers.h" + +#import "API/FIRFieldPath+Internal.h" +#import "API/FSTUserDataConverter.h" +#import "Core/FSTQuery.h" +#import "Core/FSTSnapshotVersion.h" +#import "Core/FSTTimestamp.h" +#import "Core/FSTView.h" +#import "Firestore/FIRFieldPath.h" +#import "Firestore/FIRGeoPoint.h" +#import "Local/FSTLocalViewChanges.h" +#import "Local/FSTQueryData.h" +#import "Model/FSTDatabaseID.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentKey.h" +#import "Model/FSTDocumentSet.h" +#import "Model/FSTFieldValue.h" +#import "Model/FSTMutation.h" +#import "Model/FSTPath.h" +#import "Remote/FSTRemoteEvent.h" +#import "Remote/FSTWatchChange.h" +#import "Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +/** A string sentinel that can be used with FSTTestPatchMutation() to mark a field for deletion. */ +static NSString *const kDeleteSentinel = @"<DELETE>"; + +static const int kMicrosPerSec = 1000000; +static const int kMillisPerSec = 1000; + +FSTTimestamp *FSTTestTimestamp(int year, int month, int day, int hour, int minute, int second) { + NSDate *date = FSTTestDate(year, month, day, hour, minute, second); + return [FSTTimestamp timestampWithDate:date]; +} + +NSDate *FSTTestDate(int year, int month, int day, int hour, int minute, int second) { + NSDateComponents *comps = FSTTestDateComponents(year, month, day, hour, minute, second); + return [[NSCalendar currentCalendar] dateFromComponents:comps]; +} + +NSData *FSTTestData(int bytes, ...) { + va_list args; + va_start(args, bytes); /* Initialize the argument list. */ + + NSMutableData *data = [NSMutableData data]; + + int next = bytes; + while (next >= 0) { + uint8_t byte = (uint8_t)next; + [data appendBytes:&byte length:1]; + next = va_arg(args, int); + } + + va_end(args); + return [data copy]; +} + +FIRGeoPoint *FSTTestGeoPoint(double latitude, double longitude) { + return [[FIRGeoPoint alloc] initWithLatitude:latitude longitude:longitude]; +} + +NSDateComponents *FSTTestDateComponents( + int year, int month, int day, int hour, int minute, int second) { + NSDateComponents *comps = [[NSDateComponents alloc] init]; + comps.year = year; + comps.month = month; + comps.day = day; + comps.hour = hour; + comps.minute = minute; + comps.second = second; + + // Force time zone to UTC to avoid these values changing due to daylight saving. + comps.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; + return comps; +} + +FSTFieldPath *FSTTestFieldPath(NSString *field) { + return [FIRFieldPath pathWithDotSeparatedString:field].internalValue; +} + +FSTFieldValue *FSTTestFieldValue(id _Nullable value) { + FSTDatabaseID *databaseID = + [FSTDatabaseID databaseIDWithProject:@"project" database:kDefaultDatabaseID]; + FSTUserDataConverter *converter = + [[FSTUserDataConverter alloc] initWithDatabaseID:databaseID + preConverter:^id _Nullable(id _Nullable input) { + return input; + }]; + // HACK: We use parsedQueryValue: since it accepts scalars as well as arrays / objects, and + // our tests currently use FSTTestFieldValue() pretty generically so we don't know the intent. + return [converter parsedQueryValue:value]; +} + +FSTObjectValue *FSTTestObjectValue(NSDictionary<NSString *, id> *data) { + FSTFieldValue *wrapped = FSTTestFieldValue(data); + FSTCAssert([wrapped isKindOfClass:[FSTObjectValue class]], @"Unsupported value: %@", data); + return (FSTObjectValue *)wrapped; +} + +FSTDocumentKey *FSTTestDocKey(NSString *path) { + return [FSTDocumentKey keyWithPathString:path]; +} + +FSTDocumentKeySet *FSTTestDocKeySet(NSArray<FSTDocumentKey *> *keys) { + FSTDocumentKeySet *result = [FSTDocumentKeySet keySet]; + for (FSTDocumentKey *key in keys) { + result = [result setByAddingObject:key]; + } + return result; +} + +FSTSnapshotVersion *FSTTestVersion(FSTTestSnapshotVersion versionMicroseconds) { + int64_t seconds = versionMicroseconds / kMicrosPerSec; + int32_t nanos = (int32_t)(versionMicroseconds % kMicrosPerSec) * kMillisPerSec; + + FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:seconds nanos:nanos]; + return [FSTSnapshotVersion versionWithTimestamp:timestamp]; +} + +FSTDocument *FSTTestDoc(NSString *path, + FSTTestSnapshotVersion version, + NSDictionary<NSString *, id> *data, + BOOL hasMutations) { + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:path]; + return [FSTDocument documentWithData:FSTTestObjectValue(data) + key:key + version:FSTTestVersion(version) + hasLocalMutations:hasMutations]; +} + +FSTDeletedDocument *FSTTestDeletedDoc(NSString *path, FSTTestSnapshotVersion version) { + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:path]; + return [FSTDeletedDocument documentWithKey:key version:FSTTestVersion(version)]; +} + +static NSArray<NSString *> *FSTTestSplitPath(NSString *path) { + if ([path isEqualToString:@""]) { + return @[]; + } else { + return [path componentsSeparatedByString:@"/"]; + } +} + +FSTResourcePath *FSTTestPath(NSString *path) { + return [FSTResourcePath pathWithSegments:FSTTestSplitPath(path)]; +} + +FSTDocumentKeyReference *FSTTestRef(NSString *projectID, NSString *database, NSString *path) { + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:projectID database:database]; + return [[FSTDocumentKeyReference alloc] initWithKey:FSTTestDocKey(path) databaseID:databaseID]; +} + +FSTQuery *FSTTestQuery(NSString *path) { + return [FSTQuery queryWithPath:FSTTestPath(path)]; +} + +id<FSTFilter> FSTTestFilter(NSString *field, NSString *opString, id value) { + FSTFieldPath *path = FSTTestFieldPath(field); + FSTRelationFilterOperator op; + if ([opString isEqualToString:@"<"]) { + op = FSTRelationFilterOperatorLessThan; + } else if ([opString isEqualToString:@"<="]) { + op = FSTRelationFilterOperatorLessThanOrEqual; + } else if ([opString isEqualToString:@"=="]) { + op = FSTRelationFilterOperatorEqual; + } else if ([opString isEqualToString:@">="]) { + op = FSTRelationFilterOperatorGreaterThanOrEqual; + } else if ([opString isEqualToString:@">"]) { + op = FSTRelationFilterOperatorGreaterThan; + } else { + FSTCFail(@"Unsupported operator type: %@", opString); + } + + FSTFieldValue *data = FSTTestFieldValue(value); + if ([data isEqual:[FSTDoubleValue nanValue]]) { + FSTCAssert(op == FSTRelationFilterOperatorEqual, @"Must use == with NAN."); + return [[FSTNanFilter alloc] initWithField:path]; + } else if ([data isEqual:[FSTNullValue nullValue]]) { + FSTCAssert(op == FSTRelationFilterOperatorEqual, @"Must use == with Null."); + return [[FSTNullFilter alloc] initWithField:path]; + } else { + return [FSTRelationFilter filterWithField:path filterOperator:op value:data]; + } +} + +FSTSortOrder *FSTTestOrderBy(NSString *field, NSString *direction) { + FSTFieldPath *path = FSTTestFieldPath(field); + BOOL ascending; + if ([direction isEqualToString:@"asc"]) { + ascending = YES; + } else if ([direction isEqualToString:@"desc"]) { + ascending = NO; + } else { + FSTCFail(@"Unsupported direction: %@", direction); + } + return [FSTSortOrder sortOrderWithFieldPath:path ascending:ascending]; +} + +NSComparator FSTTestDocComparator(NSString *fieldPath) { + FSTQuery *query = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"docs" ]]] + queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(fieldPath) + ascending:YES]]; + return [query comparator]; +} + +FSTDocumentSet *FSTTestDocSet(NSComparator comp, NSArray<FSTDocument *> *docs) { + FSTDocumentSet *docSet = [FSTDocumentSet documentSetWithComparator:comp]; + for (FSTDocument *doc in docs) { + docSet = [docSet documentSetByAddingDocument:doc]; + } + return docSet; +} + +FSTSetMutation *FSTTestSetMutation(NSString *path, NSDictionary<NSString *, id> *values) { + return [[FSTSetMutation alloc] initWithKey:[FSTDocumentKey keyWithPathString:path] + value:FSTTestObjectValue(values) + precondition:[FSTPrecondition none]]; +} + +FSTPatchMutation *FSTTestPatchMutation(NSString *path, + NSDictionary<NSString *, id> *values, + NSArray<FSTFieldPath *> *_Nullable updateMask) { + BOOL merge = updateMask != nil; + + __block FSTObjectValue *objectValue = [FSTObjectValue objectValue]; + NSMutableArray<FSTFieldPath *> *fieldMaskPaths = [NSMutableArray array]; + [values enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { + FSTFieldPath *path = FSTTestFieldPath(key); + [fieldMaskPaths addObject:path]; + if (![value isEqual:kDeleteSentinel]) { + FSTFieldValue *parsedValue = FSTTestFieldValue(value); + objectValue = [objectValue objectBySettingValue:parsedValue forPath:path]; + } + }]; + + FSTDocumentKey *key = [FSTDocumentKey keyWithPath:FSTTestPath(path)]; + FSTFieldMask *mask = [[FSTFieldMask alloc] initWithFields:merge ? updateMask : fieldMaskPaths]; + return [[FSTPatchMutation alloc] initWithKey:key + fieldMask:mask + value:objectValue + precondition:[FSTPrecondition preconditionWithExists:YES]]; +} + +// For now this only creates TransformMutations with server timestamps. +FSTTransformMutation *FSTTestTransformMutation(NSString *path, + NSArray<NSString *> *serverTimestampFields) { + FSTDocumentKey *key = [FSTDocumentKey keyWithPath:FSTTestPath(path)]; + NSMutableArray<FSTFieldTransform *> *fieldTransforms = [NSMutableArray array]; + for (NSString *field in serverTimestampFields) { + FSTFieldPath *fieldPath = FSTTestFieldPath(field); + id<FSTTransformOperation> transformOp = [FSTServerTimestampTransform serverTimestampTransform]; + FSTFieldTransform *transform = + [[FSTFieldTransform alloc] initWithPath:fieldPath transform:transformOp]; + [fieldTransforms addObject:transform]; + } + return [[FSTTransformMutation alloc] initWithKey:key fieldTransforms:fieldTransforms]; +} + +FSTDeleteMutation *FSTTestDeleteMutation(NSString *path) { + return [[FSTDeleteMutation alloc] initWithKey:[FSTDocumentKey keyWithPathString:path] + precondition:[FSTPrecondition none]]; +} + +FSTMaybeDocumentDictionary *FSTTestDocUpdates(NSArray<FSTMaybeDocument *> *docs) { + FSTMaybeDocumentDictionary *updates = [FSTMaybeDocumentDictionary maybeDocumentDictionary]; + for (FSTMaybeDocument *doc in docs) { + updates = [updates dictionaryBySettingObject:doc forKey:doc.key]; + } + return updates; +} + +FSTViewSnapshot *_Nullable FSTTestApplyChanges(FSTView *view, + NSArray<FSTMaybeDocument *> *docs, + FSTTargetChange *_Nullable targetChange) { + return [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(docs)] + targetChange:targetChange] + .snapshot; +} + +FSTRemoteEvent *FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc, + NSArray<NSNumber *> *updatedInTargets, + NSArray<NSNumber *> *removedFromTargets) { + FSTDocumentWatchChange *change = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:updatedInTargets + removedTargetIDs:removedFromTargets + documentKey:doc.key + document:doc]; + NSMutableDictionary<NSNumber *, FSTQueryData *> *listens = [NSMutableDictionary dictionary]; + FSTQueryData *dummyQueryData = [FSTQueryData alloc]; + for (NSNumber *targetID in updatedInTargets) { + listens[targetID] = dummyQueryData; + } + for (NSNumber *targetID in removedFromTargets) { + listens[targetID] = dummyQueryData; + } + NSMutableDictionary<NSNumber *, NSNumber *> *pending = [NSMutableDictionary dictionary]; + FSTWatchChangeAggregator *aggregator = + [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:doc.version + listenTargets:listens + pendingTargetResponses:pending]; + [aggregator addWatchChange:change]; + return [aggregator remoteEvent]; +} + +/** Creates a resume token to match the given snapshot version. */ +NSData *_Nullable FSTTestResumeTokenFromSnapshotVersion(FSTTestSnapshotVersion snapshotVersion) { + if (snapshotVersion == 0) { + return nil; + } + + NSString *snapshotString = [NSString stringWithFormat:@"snapshot-%" PRId64, snapshotVersion]; + return [snapshotString dataUsingEncoding:NSUTF8StringEncoding]; +} + +FSTLocalViewChanges *FSTTestViewChanges(FSTQuery *query, + NSArray<NSString *> *addedKeys, + NSArray<NSString *> *removedKeys) { + FSTDocumentKeySet *added = [FSTDocumentKeySet keySet]; + for (NSString *keyPath in addedKeys) { + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:keyPath]; + added = [added setByAddingObject:key]; + } + FSTDocumentKeySet *removed = [FSTDocumentKeySet keySet]; + for (NSString *keyPath in removedKeys) { + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:keyPath]; + removed = [removed setByAddingObject:key]; + } + return [FSTLocalViewChanges changesForQuery:query addedKeys:added removedKeys:removed]; +} + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h new file mode 100644 index 0000000..cefd669 --- /dev/null +++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h @@ -0,0 +1,94 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> +#import <XCTest/XCTest.h> + +#import "XCTestCase+Await.h" + +@class FIRCollectionReference; +@class FIRDocumentSnapshot; +@class FIRDocumentReference; +@class FIRQuerySnapshot; +@class FIRFirestore; +@class FIRFirestoreSettings; +@class FIRQuery; +@class FSTEventAccumulator; + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTIntegrationTestCase : XCTestCase + +/** Returns the default Firestore project ID for testing. */ ++ (NSString *)projectID; + +/** Returns a FirestoreSettings configured to use either hexa or the emulator. */ ++ (FIRFirestoreSettings *)settings; + +/** Returns a new Firestore connected to the "test-db" project. */ +- (FIRFirestore *)firestore; + +/** Returns a new Firestore connected to the project with the given projectID. */ +- (FIRFirestore *)firestoreWithProjectID:(NSString *)projectID; + +/** Synchronously shuts down the given firestore. */ +- (void)shutdownFirestore:(FIRFirestore *)firestore; + +- (NSString *)documentPath; + +- (FIRDocumentReference *)documentRef; + +- (FIRCollectionReference *)collectionRef; + +- (FIRCollectionReference *)collectionRefWithDocuments: + (NSDictionary<NSString *, NSDictionary<NSString *, id> *> *)documents; + +- (void)writeAllDocuments:(NSDictionary<NSString *, NSDictionary<NSString *, id> *> *)documents + toCollection:(FIRCollectionReference *)collection; + +- (void)readerAndWriterOnDocumentRef:(void (^)(NSString *path, + FIRDocumentReference *readerRef, + FIRDocumentReference *writerRef))action; + +- (FIRDocumentSnapshot *)readDocumentForRef:(FIRDocumentReference *)ref; + +- (FIRQuerySnapshot *)readDocumentSetForRef:(FIRQuery *)query; + +- (void)writeDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary<NSString *, id> *)data; + +- (void)updateDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary<NSString *, id> *)data; + +- (void)deleteDocumentRef:(FIRDocumentReference *)ref; + +/** + * "Blocks" the current thread/run loop until the block returns YES. + * Should only be called on the main thread. + * The block is invoked frequently and in a loop (every couple of millseconds) to ensure fast + * test progress and make sure actions to be run on main thread are not blocked by this method. + */ +- (void)waitUntil:(BOOL (^)())predicate; + +@property(nonatomic, strong) FIRFirestore *db; +@property(nonatomic, strong) FSTEventAccumulator *eventAccumulator; +@end + +/** Converts the FIRQuerySnapshot to an NSArray containing the data of the documents in order. */ +NSArray<NSDictionary<NSString *, id> *> *FIRQuerySnapshotGetData(FIRQuerySnapshot *docs); + +/** Converts the FIRQuerySnapshot to an NSArray containing the document IDs in order. */ +NSArray<NSString *> *FIRQuerySnapshotGetIDs(FIRQuerySnapshot *docs); + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.m b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.m new file mode 100644 index 0000000..87a78c3 --- /dev/null +++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.m @@ -0,0 +1,285 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import Firestore; + +#import "FSTIntegrationTestCase.h" + +#import <FirebaseCommunity/FIRLogger.h> +#import <GRPCClient/GRPCCall+ChannelArg.h> +#import <GRPCClient/GRPCCall+Tests.h> + +#import "API/FIRFirestore+Internal.h" +#import "Auth/FSTEmptyCredentialsProvider.h" +#import "Local/FSTLevelDB.h" +#import "Model/FSTDatabaseID.h" +#import "Util/FSTDispatchQueue.h" +#import "Util/FSTUtil.h" + +#import "FSTEventAccumulator.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTIntegrationTestCase { + NSMutableArray<FIRFirestore *> *_firestores; +} + +- (void)setUp { + [super setUp]; + + [self clearPersistence]; + + _firestores = [NSMutableArray array]; + self.db = [self firestore]; + self.eventAccumulator = [FSTEventAccumulator accumulatorForTest:self]; +} + +- (void)tearDown { + @try { + for (FIRFirestore *firestore in _firestores) { + [self shutdownFirestore:firestore]; + } + } @finally { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [GRPCCall closeOpenConnections]; +#pragma clang diagnostic pop + _firestores = nil; + [super tearDown]; + } +} + +- (void)clearPersistence { + NSString *levelDBDir = [FSTLevelDB documentsDirectory]; + NSError *error; + if (![[NSFileManager defaultManager] removeItemAtPath:levelDBDir error:&error]) { + // file not found is okay. + XCTAssertTrue( + [error.domain isEqualToString:NSCocoaErrorDomain] && error.code == NSFileNoSuchFileError, + @"Failed to clear LevelDB Persistence: %@", error); + } +} + +- (FIRFirestore *)firestore { + return [self firestoreWithProjectID:[FSTIntegrationTestCase projectID]]; +} + ++ (NSString *)projectID { + NSString *project = [[NSProcessInfo processInfo] environment][@"PROJECT_ID"]; + if (!project) { + project = @"test-db"; + } + return project; +} + ++ (FIRFirestoreSettings *)settings { + FIRFirestoreSettings *settings = [[FIRFirestoreSettings alloc] init]; + NSString *host = [[NSProcessInfo processInfo] environment][@"DATASTORE_HOST"]; + settings.sslEnabled = YES; + if (!host) { + // If host is nil, there is no GoogleService-Info.plist. Check if a hexa integration test + // configuration is configured. The first bundle location is used by bazel builds. The + // second is used for github clones. + host = @"localhost:8081"; + settings.sslEnabled = YES; + NSString *certsPath = + [[NSBundle mainBundle] pathForResource:@"PlugIns/IntegrationTests.xctest/CAcert" + ofType:@"pem"]; + if (certsPath == nil) { + certsPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"CAcert" ofType:@"pem"]; + } + unsigned long long fileSize = + [[[NSFileManager defaultManager] attributesOfItemAtPath:certsPath error:nil] fileSize]; + + if (fileSize == 0) { + NSLog( + @"The cert is not properly configured. Make sure setup_integration_tests.py " + "has been run."); + } + [GRPCCall useTestCertsPath:certsPath testName:@"test_cert_2" forHost:host]; + } + settings.host = host; + settings.persistenceEnabled = YES; + NSLog(@"Configured integration test for %@ with SSL: %@", settings.host, + settings.sslEnabled ? @"YES" : @"NO"); + return settings; +} + +- (FIRFirestore *)firestoreWithProjectID:(NSString *)projectID { + NSString *persistenceKey = [NSString stringWithFormat:@"db%lu", (unsigned long)_firestores.count]; + + FSTDispatchQueue *workerDispatchQueue = [FSTDispatchQueue + queueWith:dispatch_queue_create("com.google.firebase.firestore", DISPATCH_QUEUE_SERIAL)]; + + FSTEmptyCredentialsProvider *credentialsProvider = [[FSTEmptyCredentialsProvider alloc] init]; + + FIRSetLoggerLevel(FIRLoggerLevelDebug); + // HACK: FIRFirestore expects a non-nil app, but for tests we cheat. + FIRApp *app = nil; + FIRFirestore *firestore = [[FIRFirestore alloc] initWithProjectID:projectID + database:kDefaultDatabaseID + persistenceKey:persistenceKey + credentialsProvider:credentialsProvider + workerDispatchQueue:workerDispatchQueue + firebaseApp:app]; + + firestore.settings = [FSTIntegrationTestCase settings]; + + [_firestores addObject:firestore]; + return firestore; +} + +- (void)shutdownFirestore:(FIRFirestore *)firestore { + XCTestExpectation *shutdownCompletion = [self expectationWithDescription:@"shutdown"]; + [firestore shutdownWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [shutdownCompletion fulfill]; + }]; + [self awaitExpectations]; +} + +- (NSString *)documentPath { + return [@"test-collection/" stringByAppendingString:[FSTUtil autoID]]; +} + +- (FIRDocumentReference *)documentRef { + return [self.db documentWithPath:[self documentPath]]; +} + +- (FIRCollectionReference *)collectionRef { + NSString *collectionName = [@"test-collection-" stringByAppendingString:[FSTUtil autoID]]; + return [self.db collectionWithPath:collectionName]; +} + +- (FIRCollectionReference *)collectionRefWithDocuments: + (NSDictionary<NSString *, NSDictionary<NSString *, id> *> *)documents { + FIRCollectionReference *collection = [self collectionRef]; + // Use a different instance to write the documents + [self writeAllDocuments:documents + toCollection:[[self firestore] collectionWithPath:collection.path]]; + return collection; +} + +- (void)writeAllDocuments:(NSDictionary<NSString *, NSDictionary<NSString *, id> *> *)documents + toCollection:(FIRCollectionReference *)collection { + [documents enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSDictionary<NSString *, id> *value, + BOOL *stop) { + FIRDocumentReference *ref = [collection documentWithPath:key]; + [self writeDocumentRef:ref data:value]; + }]; +} + +- (void)readerAndWriterOnDocumentRef:(void (^)(NSString *path, + FIRDocumentReference *readerRef, + FIRDocumentReference *writerRef))action { + FIRFirestore *reader = self.db; // for clarity + FIRFirestore *writer = [self firestore]; + + NSString *path = [self documentPath]; + FIRDocumentReference *readerRef = [reader documentWithPath:path]; + FIRDocumentReference *writerRef = [writer documentWithPath:path]; + action(path, readerRef, writerRef); +} + +- (FIRDocumentSnapshot *)readDocumentForRef:(FIRDocumentReference *)ref { + __block FIRDocumentSnapshot *result; + + XCTestExpectation *expectation = [self expectationWithDescription:@"getData"]; + [ref getDocumentWithCompletion:^(FIRDocumentSnapshot *doc, NSError *_Nullable error) { + XCTAssertNil(error); + result = doc; + [expectation fulfill]; + }]; + [self awaitExpectations]; + + return result; +} + +- (FIRQuerySnapshot *)readDocumentSetForRef:(FIRQuery *)query { + __block FIRQuerySnapshot *result; + + XCTestExpectation *expectation = [self expectationWithDescription:@"getData"]; + [query getDocumentsWithCompletion:^(FIRQuerySnapshot *documentSet, NSError *error) { + XCTAssertNil(error); + result = documentSet; + [expectation fulfill]; + }]; + [self awaitExpectations]; + + return result; +} + +- (void)writeDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary<NSString *, id> *)data { + XCTestExpectation *expectation = [self expectationWithDescription:@"setData"]; + [ref setData:data + completion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)updateDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary<id, id> *)data { + XCTestExpectation *expectation = [self expectationWithDescription:@"updateData"]; + [ref updateData:data + completion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)deleteDocumentRef:(FIRDocumentReference *)ref { + XCTestExpectation *expectation = [self expectationWithDescription:@"deleteDocument"]; + [ref deleteDocumentWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)waitUntil:(BOOL (^)())predicate { + NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate]; + double waitSeconds = [self defaultExpectationWaitSeconds]; + while (!predicate() && ([NSDate timeIntervalSinceReferenceDate] - start < waitSeconds)) { + // This waits for the next event or until the 100ms timeout is reached + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode + beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + } + if (!predicate()) { + XCTFail(@"Timeout"); + } +} + +NSArray<NSDictionary<NSString *, id> *> *FIRQuerySnapshotGetData(FIRQuerySnapshot *docs) { + NSMutableArray<NSDictionary<NSString *, id> *> *result = [NSMutableArray array]; + for (FIRDocumentSnapshot *doc in docs.documents) { + [result addObject:doc.data]; + } + return result; +} + +NSArray<NSString *> *FIRQuerySnapshotGetIDs(FIRQuerySnapshot *docs) { + NSMutableArray<NSString *> *result = [NSMutableArray array]; + for (FIRDocumentSnapshot *doc in docs.documents) { + [result addObject:doc.documentID]; + } + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Util/FSTUtilTests.m b/Firestore/Example/Tests/Util/FSTUtilTests.m new file mode 100644 index 0000000..998832d --- /dev/null +++ b/Firestore/Example/Tests/Util/FSTUtilTests.m @@ -0,0 +1,35 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Util/FSTUtil.h" + +#import <XCTest/XCTest.h> + +@interface FSTUtilTests : XCTestCase +@end + +@implementation FSTUtilTests + +- (void)testAutoID { + NSString *autoID = [FSTUtil autoID]; + XCTAssertEqual([autoID length], 20); + for (NSUInteger i = 0; i < 20; i++) { + unichar c = [autoID characterAtIndex:i]; + XCTAssert(c >= ' ' && c <= '~', @"Should be printable ascii characters."); + } +} + +@end diff --git a/Firestore/Example/Tests/Util/XCTestCase+Await.h b/Firestore/Example/Tests/Util/XCTestCase+Await.h new file mode 100644 index 0000000..9d575f9 --- /dev/null +++ b/Firestore/Example/Tests/Util/XCTestCase+Await.h @@ -0,0 +1,32 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <XCTest/XCTest.h> + +@interface XCTestCase (Await) + +/** + * Await all outstanding expectations with a reasonable timeout, and if any of them fail, XCTFail + * the test. + */ +- (void)awaitExpectations; + +/** + * Returns a reasonable timeout for testing against Firestore. + */ +- (double)defaultExpectationWaitSeconds; + +@end diff --git a/Firestore/Example/Tests/Util/XCTestCase+Await.m b/Firestore/Example/Tests/Util/XCTestCase+Await.m new file mode 100644 index 0000000..e200c8c --- /dev/null +++ b/Firestore/Example/Tests/Util/XCTestCase+Await.m @@ -0,0 +1,38 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "XCTestCase+Await.h" + +#import <Foundation/Foundation.h> + +static const double kExpectationWaitSeconds = 10.0; + +@implementation XCTestCase (Await) + +- (void)awaitExpectations { + [self waitForExpectationsWithTimeout:kExpectationWaitSeconds + handler:^(NSError *_Nullable expectationError) { + if (expectationError) { + XCTFail(@"Error waiting for timeout: %@", expectationError); + } + }]; +} + +- (double)defaultExpectationWaitSeconds { + return kExpectationWaitSeconds; +} + +@end diff --git a/Firestore/Example/Tests/en.lproj/InfoPlist.strings b/Firestore/Example/Tests/en.lproj/InfoPlist.strings new file mode 100644 index 0000000..477b28f --- /dev/null +++ b/Firestore/Example/Tests/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + |