diff options
author | Gil <mcg@google.com> | 2017-10-03 08:55:22 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-10-03 08:55:22 -0700 |
commit | bde743ed25166a0b320ae157bfb1d68064f531c9 (patch) | |
tree | 4dd7525d9df32fa5dbdb721d4b0d4f9b87f5e884 /Firestore/Example/Tests/SpecTests | |
parent | bf550507ffa8beee149383a5bf1e2363bccefbb4 (diff) |
Release 4.3.0 (#327)
Initial release of Firestore at 0.8.0
Bump FirebaseCommunity to 0.1.3
Diffstat (limited to 'Firestore/Example/Tests/SpecTests')
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 + } + ] + } + ] + } +} |