aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firestore/Example/Tests/SpecTests
diff options
context:
space:
mode:
authorGravatar Gil <mcg@google.com>2017-10-03 08:55:22 -0700
committerGravatar GitHub <noreply@github.com>2017-10-03 08:55:22 -0700
commitbde743ed25166a0b320ae157bfb1d68064f531c9 (patch)
tree4dd7525d9df32fa5dbdb721d4b0d4f9b87f5e884 /Firestore/Example/Tests/SpecTests
parentbf550507ffa8beee149383a5bf1e2363bccefbb4 (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/Tests/SpecTests')
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.m43
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.m42
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTMockDatastore.h68
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTMockDatastore.m344
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTSpecTests.h46
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTSpecTests.m642
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h248
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m291
-rw-r--r--Firestore/Example/Tests/SpecTests/json/README.md3
-rw-r--r--Firestore/Example/Tests/SpecTests/json/collection_spec_test.json147
-rw-r--r--Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json738
-rw-r--r--Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json1150
-rw-r--r--Firestore/Example/Tests/SpecTests/json/limit_spec_test.json1626
-rw-r--r--Firestore/Example/Tests/SpecTests/json/listen_spec_test.json1524
-rw-r--r--Firestore/Example/Tests/SpecTests/json/offline_spec_test.json151
-rw-r--r--Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json155
-rw-r--r--Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json858
-rw-r--r--Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json559
-rw-r--r--Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json250
-rw-r--r--Firestore/Example/Tests/SpecTests/json/write_spec_test.json5437
20 files changed, 14322 insertions, 0 deletions
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
+ }
+ ]
+ }
+ ]
+ }
+}