diff options
author | 2017-10-03 08:55:22 -0700 | |
---|---|---|
committer | 2017-10-03 08:55:22 -0700 | |
commit | bde743ed25166a0b320ae157bfb1d68064f531c9 (patch) | |
tree | 4dd7525d9df32fa5dbdb721d4b0d4f9b87f5e884 /Firestore/Example/Tests/Remote | |
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/Remote')
-rw-r--r-- | Firestore/Example/Tests/Remote/FSTDatastoreTests.m | 58 | ||||
-rw-r--r-- | Firestore/Example/Tests/Remote/FSTRemoteEventTests.m | 556 | ||||
-rw-r--r-- | Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m | 794 | ||||
-rw-r--r-- | Firestore/Example/Tests/Remote/FSTStreamTests.m | 139 | ||||
-rw-r--r-- | Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h | 40 | ||||
-rw-r--r-- | Firestore/Example/Tests/Remote/FSTWatchChange+Testing.m | 54 | ||||
-rw-r--r-- | Firestore/Example/Tests/Remote/FSTWatchChangeTests.m | 66 |
7 files changed, 1707 insertions, 0 deletions
diff --git a/Firestore/Example/Tests/Remote/FSTDatastoreTests.m b/Firestore/Example/Tests/Remote/FSTDatastoreTests.m new file mode 100644 index 0000000..511de72 --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTDatastoreTests.m @@ -0,0 +1,58 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Firestore/FIRFirestoreErrors.h" +#import "Remote/FSTDatastore.h" + +#import <GRPCClient/GRPCCall.h> +#import <XCTest/XCTest.h> + +@interface FSTDatastoreTests : XCTestCase +@end + +@implementation FSTDatastoreTests + +- (void)testIsPermanentWriteError { + // From GRPCCall -cancel + NSError *error = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeCancelled + userInfo:@{NSLocalizedDescriptionKey : @"Canceled by app"}]; + XCTAssertFalse([FSTDatastore isPermanentWriteError:error]); + + // From GRPCCall -startNextRead + error = + [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeResourceExhausted + userInfo:@{ + NSLocalizedDescriptionKey : + @"Client does not have enough memory to hold the server response." + }]; + XCTAssertFalse([FSTDatastore isPermanentWriteError:error]); + + // From GRPCCall -startWithWriteable + error = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeUnavailable + userInfo:@{NSLocalizedDescriptionKey : @"Connectivity lost."}]; + XCTAssertFalse([FSTDatastore isPermanentWriteError:error]); + + // User info doesn't matter: + error = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeUnavailable + userInfo:nil]; + XCTAssertFalse([FSTDatastore isPermanentWriteError:error]); +} + +@end diff --git a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.m b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.m new file mode 100644 index 0000000..a172af7 --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.m @@ -0,0 +1,556 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Remote/FSTRemoteEvent.h" + +#import <XCTest/XCTest.h> + +#import "Local/FSTQueryData.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentKey.h" +#import "Remote/FSTExistenceFilter.h" +#import "Remote/FSTWatchChange.h" + +#import "FSTHelpers.h" +#import "FSTWatchChange+Testing.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTRemoteEventTests : XCTestCase +@end + +@implementation FSTRemoteEventTests { + NSData *_resumeToken1; + NSMutableDictionary<NSNumber *, NSNumber *> *_noPendingResponses; +} + +- (void)setUp { + _resumeToken1 = [@"resume1" dataUsingEncoding:NSUTF8StringEncoding]; + _noPendingResponses = [NSMutableDictionary dictionary]; +} + +- (FSTWatchChangeAggregator *)aggregatorWithTargets:(NSArray<NSNumber *> *)targets + outstanding: + (NSDictionary<NSNumber *, NSNumber *> *)outstanding + changes:(NSArray<FSTWatchChange *> *)watchChanges { + NSMutableDictionary<NSNumber *, FSTQueryData *> *listens = [NSMutableDictionary dictionary]; + FSTQueryData *dummyQueryData = [FSTQueryData alloc]; + for (NSNumber *targetID in targets) { + listens[targetID] = dummyQueryData; + } + FSTWatchChangeAggregator *aggregator = + [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:FSTTestVersion(3) + listenTargets:listens + pendingTargetResponses:outstanding]; + [aggregator addWatchChanges:watchChanges]; + return aggregator; +} + +- (void)testWillAccumulateDocumentAddedAndRemovedEvents { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @2, @3 ] + removedTargetIDs:@[ @4, @5, @6 ] + documentKey:doc1.key + document:doc1]; + + FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @4 ] + removedTargetIDs:@[ @2, @6 ] + documentKey:doc2.key + document:doc2]; + + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2, @3, @4, @5, @6 ] + outstanding:_noPendingResponses + changes:@[ change1, change2 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 2); + XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + + XCTAssertEqual(event.targetChanges.count, 6); + + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + + FSTUpdateMapping *mapping2 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[ doc2 ]]; + XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping2); + + FSTUpdateMapping *mapping3 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@3].mapping, mapping3); + + FSTUpdateMapping *mapping4 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc2 ] removedDocuments:@[ doc1 ]]; + XCTAssertEqualObjects(event.targetChanges[@4].mapping, mapping4); + + FSTUpdateMapping *mapping5 = + [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1 ]]; + XCTAssertEqualObjects(event.targetChanges[@5].mapping, mapping5); + + FSTUpdateMapping *mapping6 = + [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1, doc2 ]]; + XCTAssertEqualObjects(event.targetChanges[@6].mapping, mapping6); +} + +- (void)testWillIgnoreEventsForPendingTargets { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc1.key + document:doc1]; + + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved + targetIDs:@[ @1 ] + cause:nil]; + + FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded + targetIDs:@[ @1 ] + cause:nil]; + + FSTWatchChange *change4 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc2.key + document:doc2]; + + // We're waiting for the unwatch and watch ack + NSDictionary<NSNumber *, NSNumber *> *pendingResponses = @{ @1 : @2 }; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] + outstanding:pendingResponses + changes:@[ change1, change2, change3, change4 ]]; + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + // doc1 is ignored because it was part of an inactive target, but doc2 is in the changes + // because it become active. + XCTAssertEqual(event.documentUpdates.count, 1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + + XCTAssertEqual(event.targetChanges.count, 1); +} + +- (void)testWillIgnoreEventsForRemovedTargets { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc1.key + document:doc1]; + + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved + targetIDs:@[ @1 ] + cause:nil]; + + // We're waiting for the unwatch ack + NSDictionary<NSNumber *, NSNumber *> *pendingResponses = @{ @1 : @1 }; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[] outstanding:pendingResponses changes:@[ change1, change2 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + // doc1 is ignored because it was part of an inactive target + XCTAssertEqual(event.documentUpdates.count, 0); + + // Target 1 is ignored because it was removed + XCTAssertEqual(event.targetChanges.count, 0); +} + +- (void)testWillKeepResetMappingEvenWithUpdates { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"docs/3", 3, @{ @"value" : @3 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc1.key + document:doc1]; + // Reset stream, ignoring doc1 + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:@[ @1 ] + cause:nil]; + + // Add doc2, doc3 + FSTWatchChange *change3 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc2.key + document:doc2]; + FSTWatchChange *change4 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc3.key + document:doc3]; + + // Remove doc2 again, should not show up in reset mapping + FSTWatchChange *change5 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:@[ @1 ] + documentKey:doc2.key + document:doc2]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] + outstanding:_noPendingResponses + changes:@[ change1, change2, change3, change4, change5 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 3); + XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + XCTAssertEqualObjects(event.documentUpdates[doc3.key], doc3); + + XCTAssertEqual(event.targetChanges.count, 1); + + // Only doc3 is part of the new mapping + FSTResetMapping *expectedMapping = [FSTResetMapping mappingWithDocuments:@[ doc3 ]]; + + XCTAssertEqualObjects(event.targetChanges[@1].mapping, expectedMapping); +} + +- (void)testWillHandleSingleReset { + // Reset target + FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:@[ @1 ] + cause:nil]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 0); + + XCTAssertEqual(event.targetChanges.count, 1); + + // Reset mapping is empty + FSTResetMapping *expectedMapping = [FSTResetMapping mappingWithDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, expectedMapping); +} + +- (void)testWillHandleTargetAddAndRemovalInSameBatch { + FSTDocument *doc1a = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc1b = FSTTestDoc(@"docs/1", 1, @{ @"value" : @2 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[ @2 ] + documentKey:doc1a.key + document:doc1a]; + + FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @2 ] + removedTargetIDs:@[ @1 ] + documentKey:doc1b.key + document:doc1b]; + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2 ] + outstanding:_noPendingResponses + changes:@[ change1, change2 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 1); + XCTAssertEqualObjects(event.documentUpdates[doc1b.key], doc1b); + + XCTAssertEqual(event.targetChanges.count, 2); + + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1b ]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + + FSTUpdateMapping *mapping2 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1b ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping2); +} + +- (void)testTargetCurrentChangeWillMarkTheTargetCurrent { + FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 0); + XCTAssertEqual(event.targetChanges.count, 1); + FSTTargetChange *targetChange = event.targetChanges[@1]; + XCTAssertEqualObjects(targetChange.mapping, [[FSTUpdateMapping alloc] init]); + XCTAssertEqual(targetChange.currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(targetChange.resumeToken, _resumeToken1); +} + +- (void)testTargetAddedChangeWillResetPreviousState { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @3 ] + removedTargetIDs:@[ @2 ] + documentKey:doc1.key + document:doc1]; + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @1, @2, @3 ] + resumeToken:_resumeToken1]; + FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved + targetIDs:@[ @1 ] + cause:nil]; + FSTWatchChange *change4 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved + targetIDs:@[ @2 ] + cause:nil]; + FSTWatchChange *change5 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded + targetIDs:@[ @1 ] + cause:nil]; + FSTWatchChange *change6 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[ @3 ] + documentKey:doc2.key + document:doc2]; + + NSDictionary<NSNumber *, NSNumber *> *pendingResponses = @{ @1 : @2, @2 : @1 }; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1, @3 ] + outstanding:pendingResponses + changes:@[ change1, change2, change3, change4, change5, change6 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 2); + XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + + // target 1 and 3 are affected (1 because of re-add), target 2 is not because of remove + XCTAssertEqual(event.targetChanges.count, 2); + + // doc1 was before the remove, so it does not show up in the mapping + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc2 ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + // Current was before the remove + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateNone); + + // Doc1 was before the remove + FSTUpdateMapping *mapping3 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[ doc2 ]]; + XCTAssertEqualObjects(event.targetChanges[@3].mapping, mapping3); + // Current was before the remove + XCTAssertEqual(event.targetChanges[@3].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(event.targetChanges[@3].resumeToken, _resumeToken1); +} + +- (void)testNoChangeWillStillMarkTheAffectedTargets { + FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 0); + XCTAssertEqual(event.targetChanges.count, 1); + XCTAssertEqualObjects(event.targetChanges[@1].mapping, [[FSTUpdateMapping alloc] init]); + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateNone); + XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1); +} + +- (void)testExistenceFiltersWillReplacePreviousExistenceFilters { + FSTExistenceFilter *filter1 = [FSTExistenceFilter filterWithCount:1]; + FSTExistenceFilter *filter2 = [FSTExistenceFilter filterWithCount:2]; + FSTWatchChange *change1 = [FSTExistenceFilterWatchChange changeWithFilter:filter1 targetID:1]; + FSTWatchChange *change2 = [FSTExistenceFilterWatchChange changeWithFilter:filter1 targetID:2]; + // replace filter1 for target 2 + FSTWatchChange *change3 = [FSTExistenceFilterWatchChange changeWithFilter:filter2 targetID:2]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1, @2 ] + outstanding:_noPendingResponses + changes:@[ change1, change2, change3 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 0); + XCTAssertEqual(event.targetChanges.count, 0); + XCTAssertEqual(aggregator.existenceFilters.count, 2); + XCTAssertEqual(aggregator.existenceFilters[@1], filter1); + XCTAssertEqual(aggregator.existenceFilters[@2], filter2); +} + +- (void)testExistenceFilterMismatchResetsTarget { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc1.key + document:doc1]; + + FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc2.key + document:doc2]; + + FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] + outstanding:_noPendingResponses + changes:@[ change1, change2, change3 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 2); + XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + + XCTAssertEqual(event.targetChanges.count, 1); + + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1); + + [event handleExistenceFilterMismatchForTargetID:@1]; + + // Mapping is reset + XCTAssertEqualObjects(event.targetChanges[@1].mapping, [[FSTResetMapping alloc] init]); + // Reset the resume snapshot + XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(0)); + // Target needs to be set to not current + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkNotCurrent); + XCTAssertEqual(event.targetChanges[@1].resumeToken.length, 0); +} + +- (void)testDocumentUpdate { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDeletedDocument *deletedDoc1 = + [FSTDeletedDocument documentWithKey:doc1.key version:FSTTestVersion(3)]; + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"docs/3", 3, @{ @"value" : @3 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc1.key + document:doc1]; + + FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc2.key + document:doc2]; + + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1 ] + outstanding:_noPendingResponses + changes:@[ change1, change2 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 2); + XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + + // Update doc1 + [event addDocumentUpdate:deletedDoc1]; + [event addDocumentUpdate:doc3]; + + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 3); + // doc1 is replaced + XCTAssertEqualObjects(event.documentUpdates[doc1.key], deletedDoc1); + // doc2 is untouched + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + // doc3 is new + XCTAssertEqualObjects(event.documentUpdates[doc3.key], doc3); + + // Target is unchanged + XCTAssertEqual(event.targetChanges.count, 1); + + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); +} + +- (void)testResumeTokensHandledPerTarget { + NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding]; + FSTWatchChange *change1 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @2 ] + resumeToken:resumeToken2]; + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2 ] + outstanding:_noPendingResponses + changes:@[ change1, change2 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqual(event.targetChanges.count, 2); + + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1); + + XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping1); + XCTAssertEqualObjects(event.targetChanges[@2].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken2); +} + +- (void)testLastResumeTokenWins { + NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *resumeToken3 = [@"resume3" dataUsingEncoding:NSUTF8StringEncoding]; + + FSTWatchChange *change1 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:@[ @1 ] + resumeToken:resumeToken2]; + FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:@[ @2 ] + resumeToken:resumeToken3]; + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1, @2 ] + outstanding:_noPendingResponses + changes:@[ change1, change2, change3 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqual(event.targetChanges.count, 2); + + FSTResetMapping *mapping1 = [FSTResetMapping mappingWithDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, resumeToken2); + + XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping1); + XCTAssertEqualObjects(event.targetChanges[@2].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateNone); + XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken3); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m new file mode 100644 index 0000000..c4cf9df --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m @@ -0,0 +1,794 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Remote/FSTSerializerBeta.h" + +#import <GRPCClient/GRPCCall.h> +#import <XCTest/XCTest.h> + +#import "Core/FSTQuery.h" +#import "Core/FSTSnapshotVersion.h" +#import "Core/FSTTimestamp.h" +#import "Firestore/FIRFieldPath.h" +#import "Firestore/FIRFirestoreErrors.h" +#import "Firestore/FIRGeoPoint.h" +#import "Local/FSTQueryData.h" +#import "Model/FSTDatabaseID.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentKey.h" +#import "Model/FSTFieldValue.h" +#import "Model/FSTMutation.h" +#import "Model/FSTMutationBatch.h" +#import "Model/FSTPath.h" +#import "Protos/objc/firestore/local/MaybeDocument.pbobjc.h" +#import "Protos/objc/firestore/local/Mutation.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Common.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Document.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Query.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Write.pbobjc.h" +#import "Protos/objc/google/rpc/Status.pbobjc.h" +#import "Protos/objc/google/type/Latlng.pbobjc.h" +#import "Remote/FSTWatchChange.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTSerializerBeta (Test) +- (GCFSValue *)encodedNull; +- (GCFSValue *)encodedBool:(BOOL)value; +- (GCFSValue *)encodedDouble:(double)value; +- (GCFSValue *)encodedInteger:(int64_t)value; +- (GCFSValue *)encodedString:(NSString *)value; +- (GCFSValue *)encodedDate:(NSDate *)value; + +- (GCFSDocumentMask *)encodedFieldMask:(FSTFieldMask *)fieldMask; +- (NSMutableArray<GCFSDocumentTransform_FieldTransform *> *)encodedFieldTransforms: + (NSArray<FSTFieldTransform *> *)fieldTransforms; + +- (GCFSStructuredQuery_Filter *)encodedRelationFilter:(FSTRelationFilter *)filter; +@end + +@interface GCFSStructuredQuery_Order (Test) ++ (instancetype)messageWithProperty:(NSString *)property ascending:(BOOL)ascending; +@end + +@implementation GCFSStructuredQuery_Order (Test) + ++ (instancetype)messageWithProperty:(NSString *)property ascending:(BOOL)ascending { + GCFSStructuredQuery_Order *order = [GCFSStructuredQuery_Order message]; + order.field.fieldPath = property; + order.direction = ascending ? GCFSStructuredQuery_Direction_Ascending + : GCFSStructuredQuery_Direction_Descending; + return order; +} +@end + +@interface FSTSerializerBetaTests : XCTestCase +@property(nonatomic, strong) FSTSerializerBeta *serializer; +@end + +@implementation FSTSerializerBetaTests + +- (void)setUp { + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"]; + self.serializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseID]; +} + +- (void)testEncodesNull { + FSTFieldValue *model = [FSTNullValue nullValue]; + + GCFSValue *proto = [GCFSValue message]; + proto.nullValue = GPBNullValue_NullValue; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_NullValue]; +} + +- (void)testEncodesBool { + NSArray<NSNumber *> *examples = @[ @YES, @NO ]; + for (NSNumber *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.booleanValue = [example boolValue]; + + [self assertRoundTripForModel:model + proto:proto + type:GCFSValue_ValueType_OneOfCase_BooleanValue]; + } +} + +- (void)testEncodesIntegers { + NSArray<NSNumber *> *examples = @[ @(LLONG_MIN), @(-100), @(-1), @0, @1, @100, @(LLONG_MAX) ]; + for (NSNumber *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.integerValue = [example longLongValue]; + + [self assertRoundTripForModel:model + proto:proto + type:GCFSValue_ValueType_OneOfCase_IntegerValue]; + } +} + +- (void)testEncodesDoubles { + NSArray<NSNumber *> *examples = @[ + // normal negative numbers. + @(-INFINITY), @(-DBL_MAX), @(LLONG_MIN * 1.0 - 1.0), @(-2.0), @(-1.1), @(-1.0), @(-DBL_MIN), + + // negative smallest subnormal, zeroes, positive smallest subnormal + @(-0x1.0p-1074), @(-0.0), @(0.0), @(0x1.0p-1074), + + // and the rest + @(DBL_MIN), @0.1, @1.1, @(LLONG_MAX * 1.0), @(DBL_MAX), @(INFINITY), + + // NaN. + @(0.0 / 0.0) + ]; + for (NSNumber *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.doubleValue = [example doubleValue]; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_DoubleValue]; + } +} + +- (void)testEncodesStrings { + NSArray<NSString *> *examples = @[ + @"", + @"a", + @"abc def", + @"æ", + @"\0\ud7ff\ue000\uffff", + @"(╯°□°)╯︵ ┻━┻", + ]; + for (NSString *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.stringValue = example; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_StringValue]; + } +} + +- (void)testEncodesDates { + NSDateComponents *dateWithNanos = FSTTestDateComponents(2016, 1, 2, 10, 20, 50); + dateWithNanos.nanosecond = 500000000; + + NSArray<NSDate *> *examples = @[ + [[NSCalendar currentCalendar] dateFromComponents:dateWithNanos], + FSTTestDate(2016, 6, 17, 10, 50, 15) + ]; + + GCFSValue *timestamp1 = [GCFSValue message]; + timestamp1.timestampValue.seconds = 1451730050; + timestamp1.timestampValue.nanos = 500000000; + + GCFSValue *timestamp2 = [GCFSValue message]; + timestamp2.timestampValue.seconds = 1466160615; + timestamp2.timestampValue.nanos = 0; + NSArray<GCFSValue *> *expectedTimestamps = @[ timestamp1, timestamp2 ]; + + for (NSUInteger i = 0; i < [examples count]; i++) { + [self assertRoundTripForModel:FSTTestFieldValue(examples[i]) + proto:expectedTimestamps[i] + type:GCFSValue_ValueType_OneOfCase_TimestampValue]; + } +} + +- (void)testEncodesGeoPoints { + NSArray<FIRGeoPoint *> *examples = + @[ FSTTestGeoPoint(0, 0), FSTTestGeoPoint(1.24, 4.56), FSTTestGeoPoint(-90, 180) ]; + for (FIRGeoPoint *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.geoPointValue = [GTPLatLng message]; + proto.geoPointValue.latitude = example.latitude; + proto.geoPointValue.longitude = example.longitude; + + [self assertRoundTripForModel:model + proto:proto + type:GCFSValue_ValueType_OneOfCase_GeoPointValue]; + } +} + +- (void)testEncodesBlobs { + NSArray<NSData *> *examples = @[ + FSTTestData(-1), + FSTTestData(0, -1), + FSTTestData(0, 1, 2, -1), + FSTTestData(255, -1), + FSTTestData(0, 1, 255, -1), + ]; + for (NSData *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.bytesValue = example; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_BytesValue]; + } +} + +- (void)testEncodesResourceNames { + FSTDocumentKeyReference *reference = FSTTestRef(@"project", kDefaultDatabaseID, @"foo/bar"); + GCFSValue *proto = [GCFSValue message]; + proto.referenceValue = @"projects/project/databases/(default)/documents/foo/bar"; + + [self assertRoundTripForModel:FSTTestFieldValue(reference) + proto:proto + type:GCFSValue_ValueType_OneOfCase_ReferenceValue]; +} + +- (void)testEncodesArrays { + FSTFieldValue *model = FSTTestFieldValue(@[ @YES, @"foo" ]); + + GCFSValue *proto = [GCFSValue message]; + [proto.arrayValue.valuesArray addObjectsFromArray:@[ + [self.serializer encodedBool:YES], [self.serializer encodedString:@"foo"] + ]]; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_ArrayValue]; +} + +- (void)testEncodesEmptyMap { + FSTFieldValue *model = [FSTObjectValue objectValue]; + + GCFSValue *proto = [GCFSValue message]; + proto.mapValue = [GCFSMapValue message]; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_MapValue]; +} + +- (void)testEncodesNestedObjects { + FSTFieldValue *model = FSTTestFieldValue(@{ + @"b" : @YES, + @"d" : @(DBL_MAX), + @"i" : @1, + @"n" : [NSNull null], + @"s" : @"foo", + @"a" : @[ @2, @"bar", @{@"b" : @NO} ], + @"o" : @{ + @"d" : @100, + @"nested" : @{@"e" : @(LLONG_MIN)}, + }, + }); + + GCFSValue *innerObject = [GCFSValue message]; + innerObject.mapValue.fields[@"b"] = [self.serializer encodedBool:NO]; + + GCFSValue *middleArray = [GCFSValue message]; + [middleArray.arrayValue.valuesArray addObjectsFromArray:@[ + [self.serializer encodedInteger:2], [self.serializer encodedString:@"bar"], innerObject + ]]; + + innerObject = [GCFSValue message]; + innerObject.mapValue.fields[@"e"] = [self.serializer encodedInteger:LLONG_MIN]; + + GCFSValue *middleObject = [GCFSValue message]; + [middleObject.mapValue.fields addEntriesFromDictionary:@{ + @"d" : [self.serializer encodedInteger:100], + @"nested" : innerObject + }]; + + GCFSValue *proto = [GCFSValue message]; + [proto.mapValue.fields addEntriesFromDictionary:@{ + @"b" : [self.serializer encodedBool:YES], + @"d" : [self.serializer encodedDouble:DBL_MAX], + @"i" : [self.serializer encodedInteger:1], + @"n" : [self.serializer encodedNull], + @"s" : [self.serializer encodedString:@"foo"], + @"a" : middleArray, + @"o" : middleObject + }]; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_MapValue]; +} + +- (void)assertRoundTripForModel:(FSTFieldValue *)model + proto:(GCFSValue *)value + type:(GCFSValue_ValueType_OneOfCase)type { + GCFSValue *actualProto = [self.serializer encodedFieldValue:model]; + XCTAssertEqual(actualProto.valueTypeOneOfCase, type); + XCTAssertEqualObjects(actualProto, value); + + FSTFieldValue *actualModel = [self.serializer decodedFieldValue:value]; + XCTAssertEqualObjects(actualModel, model); +} + +- (void)testEncodesSetMutation { + FSTSetMutation *mutation = FSTTestSetMutation(@"docs/1", @{ @"a" : @"b", @"num" : @1 }); + GCFSWrite *proto = [GCFSWrite message]; + proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key]; + + [self assertRoundTripForMutation:mutation proto:proto]; +} + +- (void)testEncodesPatchMutation { + FSTPatchMutation *mutation = + FSTTestPatchMutation(@"docs/1", + @{ @"a" : @"b", + @"num" : @1, + @"some.de\\\\ep.th\\ing'" : @2 }, + nil); + GCFSWrite *proto = [GCFSWrite message]; + proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key]; + proto.updateMask = [self.serializer encodedFieldMask:mutation.fieldMask]; + proto.currentDocument.exists = YES; + + [self assertRoundTripForMutation:mutation proto:proto]; +} + +- (void)testEncodesDeleteMutation { + FSTDeleteMutation *mutation = FSTTestDeleteMutation(@"docs/1"); + GCFSWrite *proto = [GCFSWrite message]; + proto.delete_p = @"projects/p/databases/d/documents/docs/1"; + + [self assertRoundTripForMutation:mutation proto:proto]; +} + +- (void)testEncodesTransformMutation { + FSTTransformMutation *mutation = FSTTestTransformMutation(@"docs/1", @[ @"a", @"bar.baz" ]); + GCFSWrite *proto = [GCFSWrite message]; + proto.transform = [GCFSDocumentTransform message]; + proto.transform.document = [self.serializer encodedDocumentKey:mutation.key]; + proto.transform.fieldTransformsArray = + [self.serializer encodedFieldTransforms:mutation.fieldTransforms]; + proto.currentDocument.exists = YES; + + [self assertRoundTripForMutation:mutation proto:proto]; +} + +- (void)testEncodesSetMutationWithPrecondition { + FSTSetMutation *mutation = [[FSTSetMutation alloc] + initWithKey:FSTTestDocKey(@"foo/bar") + value:FSTTestObjectValue( + @{ @"a" : @"b", + @"num" : @1 }) + precondition:[FSTPrecondition preconditionWithUpdateTime:FSTTestVersion(4)]]; + GCFSWrite *proto = [GCFSWrite message]; + proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key]; + proto.currentDocument.updateTime = + [self.serializer encodedTimestamp:[[FSTTimestamp alloc] initWithSeconds:0 nanos:4000]]; + + [self assertRoundTripForMutation:mutation proto:proto]; +} + +- (void)assertRoundTripForMutation:(FSTMutation *)mutation proto:(GCFSWrite *)proto { + GCFSWrite *actualProto = [self.serializer encodedMutation:mutation]; + XCTAssertEqualObjects(actualProto, proto); + + FSTMutation *actualMutation = [self.serializer decodedMutation:proto]; + XCTAssertEqualObjects(actualMutation, mutation); +} + +- (void)testRoundTripSpecialFieldNames { + FSTMutation *set = FSTTestSetMutation(@"collection/key", @{ + @"field" : [NSString stringWithFormat:@"field %d", 1], + @"field.dot" : @2, + @"field\\slash" : @3 + }); + GCFSWrite *encoded = [self.serializer encodedMutation:set]; + FSTMutation *decoded = [self.serializer decodedMutation:encoded]; + XCTAssertEqualObjects(set, decoded); +} + +- (void)testEncodesListenRequestLabels { + FSTQuery *query = FSTTestQuery(@"collection/key"); + FSTQueryData *queryData = + [[FSTQueryData alloc] initWithQuery:query targetID:2 purpose:FSTQueryPurposeListen]; + + NSDictionary<NSString *, NSString *> *result = + [self.serializer encodedListenRequestLabelsForQueryData:queryData]; + XCTAssertNil(result); + + queryData = + [[FSTQueryData alloc] initWithQuery:query targetID:2 purpose:FSTQueryPurposeLimboResolution]; + result = [self.serializer encodedListenRequestLabelsForQueryData:queryData]; + XCTAssertEqualObjects(result, @{@"goog-listen-tags" : @"limbo-document"}); + + queryData = [[FSTQueryData alloc] initWithQuery:query + targetID:2 + purpose:FSTQueryPurposeExistenceFilterMismatch]; + result = [self.serializer encodedListenRequestLabelsForQueryData:queryData]; + XCTAssertEqualObjects(result, @{@"goog-listen-tags" : @"existence-filter-mismatch"}); +} + +- (void)testEncodesRelationFilter { + FSTRelationFilter *input = FSTTestFilter(@"item.part.top", @"==", @"food"); + GCFSStructuredQuery_Filter *actual = [self.serializer encodedRelationFilter:input]; + + GCFSStructuredQuery_Filter *expected = [GCFSStructuredQuery_Filter message]; + GCFSStructuredQuery_FieldFilter *prop = expected.fieldFilter; + prop.field.fieldPath = @"item.part.top"; + prop.op = GCFSStructuredQuery_FieldFilter_Operator_Equal; + prop.value.stringValue = @"food"; + XCTAssertEqualObjects(actual, expected); +} + +#pragma mark - encodedQuery + +- (void)testEncodesFirstLevelKeyQueries { + FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"docs/1")]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + [expected.documents.documentsArray addObject:@"projects/p/databases/d/documents/docs/1"]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesFirstLevelAncestorQueries { + FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"messages")]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"messages"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesNestedAncestorQueries { + FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"rooms/1/messages/10/attachments")]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"attachments"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesSingleFiltersAtFirstLevelCollections { + FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")] + queryByAddingFilter:FSTTestFilter(@"prop", @"<", @(42))]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"docs"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + + GCFSStructuredQuery_FieldFilter *filter = expected.query.structuredQuery.where.fieldFilter; + filter.field.fieldPath = @"prop"; + filter.op = GCFSStructuredQuery_FieldFilter_Operator_LessThan; + filter.value.integerValue = 42; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesMultipleFiltersOnDeeperCollections { + FSTQuery *q = [[[FSTQuery queryWithPath:FSTTestPath(@"rooms/1/messages/10/attachments")] + queryByAddingFilter:FSTTestFilter(@"prop", @">=", @(42))] + queryByAddingFilter:FSTTestFilter(@"author", @"==", @"dimond")]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"attachments"; + [expected.query.structuredQuery.fromArray addObject:from]; + + GCFSStructuredQuery_Filter *filter1 = [GCFSStructuredQuery_Filter message]; + GCFSStructuredQuery_FieldFilter *field1 = filter1.fieldFilter; + field1.field.fieldPath = @"prop"; + field1.op = GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual; + field1.value.integerValue = 42; + + GCFSStructuredQuery_Filter *filter2 = [GCFSStructuredQuery_Filter message]; + GCFSStructuredQuery_FieldFilter *field2 = filter2.fieldFilter; + field2.field.fieldPath = @"author"; + field2.op = GCFSStructuredQuery_FieldFilter_Operator_Equal; + field2.value.stringValue = @"dimond"; + + GCFSStructuredQuery_CompositeFilter *composite = + expected.query.structuredQuery.where.compositeFilter; + composite.op = GCFSStructuredQuery_CompositeFilter_Operator_And; + [composite.filtersArray addObject:filter1]; + [composite.filtersArray addObject:filter2]; + + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesNullFilter { + [self unaryFilterTestWithValue:[NSNull null] + expectedUnaryOperator:GCFSStructuredQuery_UnaryFilter_Operator_IsNull]; +} + +- (void)testEncodesNanFilter { + [self unaryFilterTestWithValue:@(NAN) + expectedUnaryOperator:GCFSStructuredQuery_UnaryFilter_Operator_IsNan]; +} + +- (void)unaryFilterTestWithValue:(id)value + expectedUnaryOperator:(GCFSStructuredQuery_UnaryFilter_Operator) + operator{ + FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")] + queryByAddingFilter:FSTTestFilter(@"prop", @"==", value)]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"docs"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + + GCFSStructuredQuery_UnaryFilter *filter = expected.query.structuredQuery.where.unaryFilter; + filter.field.fieldPath = @"prop"; + filter.op = operator; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesSortOrders { + FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")] + queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"prop") + ascending:YES]]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"docs"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesSortOrdersDescending { + FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"rooms/1/messages/10/attachments")] + queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"prop") + ascending:NO]]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"attachments"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:NO]]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:NO]]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesLimits { + FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")] queryBySettingLimit:26]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"docs"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.query.structuredQuery.limit.value = 26; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesResumeTokens { + FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"docs")]; + FSTQueryData *model = [[FSTQueryData alloc] initWithQuery:q + targetID:1 + purpose:FSTQueryPurposeListen + snapshotVersion:[FSTSnapshotVersion noVersion] + resumeToken:FSTTestData(1, 2, 3, -1)]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"docs"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.targetId = 1; + expected.resumeToken = FSTTestData(1, 2, 3, -1); + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (FSTQueryData *)queryDataForQuery:(FSTQuery *)query { + return [[FSTQueryData alloc] initWithQuery:query + targetID:1 + purpose:FSTQueryPurposeListen + snapshotVersion:[FSTSnapshotVersion noVersion] + resumeToken:[NSData data]]; +} + +- (void)assertRoundTripForQueryData:(FSTQueryData *)queryData proto:(GCFSTarget *)proto { + // Verify that the encoded FSTQueryData matches the target. + GCFSTarget *actualProto = [self.serializer encodedTarget:queryData]; + XCTAssertEqualObjects(actualProto, proto); + + // We don't have deserialization logic for full targets since they're not used for RPC + // interaction, but the query deserialization only *is* used for the local store. + FSTQuery *actualModel; + if (proto.targetTypeOneOfCase == GCFSTarget_TargetType_OneOfCase_Query) { + actualModel = [self.serializer decodedQueryFromQueryTarget:proto.query]; + } else { + actualModel = [self.serializer decodedQueryFromDocumentsTarget:proto.documents]; + } + XCTAssertEqualObjects(actualModel, queryData.query); +} + +- (void)testConvertsTargetChangeWithAdded { + FSTWatchChange *expected = + [[FSTWatchTargetChange alloc] initWithState:FSTWatchTargetChangeStateAdded + targetIDs:@[ @1, @4 ] + resumeToken:[NSData data] + cause:nil]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_Add; + [listenResponse.targetChange.targetIdsArray addValue:1]; + [listenResponse.targetChange.targetIdsArray addValue:4]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsTargetChangeWithRemoved { + FSTWatchChange *expected = [[FSTWatchTargetChange alloc] + initWithState:FSTWatchTargetChangeStateRemoved + targetIDs:@[ @1, @4 ] + resumeToken:FSTTestData(0, 1, 2, -1) + cause:[NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodePermissionDenied + userInfo:@{ + NSLocalizedDescriptionKey : @"Error message", + }]]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_Remove; + listenResponse.targetChange.cause.code = FIRFirestoreErrorCodePermissionDenied; + listenResponse.targetChange.cause.message = @"Error message"; + listenResponse.targetChange.resumeToken = FSTTestData(0, 1, 2, -1); + [listenResponse.targetChange.targetIdsArray addValue:1]; + [listenResponse.targetChange.targetIdsArray addValue:4]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsTargetChangeWithNoChange { + FSTWatchChange *expected = + [[FSTWatchTargetChange alloc] initWithState:FSTWatchTargetChangeStateNoChange + targetIDs:@[ @1, @4 ] + resumeToken:[NSData data] + cause:nil]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_NoChange; + [listenResponse.targetChange.targetIdsArray addValue:1]; + [listenResponse.targetChange.targetIdsArray addValue:4]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsDocumentChangeWithTargetIds { + FSTWatchChange *expected = [[FSTDocumentWatchChange alloc] + initWithUpdatedTargetIDs:@[ @1, @2 ] + removedTargetIDs:@[] + documentKey:FSTTestDocKey(@"coll/1") + document:FSTTestDoc(@"coll/1", 5, @{@"foo" : @"bar"}, NO)]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.documentChange.document.name = @"projects/p/databases/d/documents/coll/1"; + listenResponse.documentChange.document.updateTime.nanos = 5000; + GCFSValue *fooValue = [GCFSValue message]; + fooValue.stringValue = @"bar"; + [listenResponse.documentChange.document.fields setObject:fooValue forKey:@"foo"]; + [listenResponse.documentChange.targetIdsArray addValue:1]; + [listenResponse.documentChange.targetIdsArray addValue:2]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsDocumentChangeWithRemovedTargetIds { + FSTWatchChange *expected = [[FSTDocumentWatchChange alloc] + initWithUpdatedTargetIDs:@[ @2 ] + removedTargetIDs:@[ @1 ] + documentKey:FSTTestDocKey(@"coll/1") + document:FSTTestDoc(@"coll/1", 5, @{@"foo" : @"bar"}, NO)]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.documentChange.document.name = @"projects/p/databases/d/documents/coll/1"; + listenResponse.documentChange.document.updateTime.nanos = 5000; + GCFSValue *fooValue = [GCFSValue message]; + fooValue.stringValue = @"bar"; + [listenResponse.documentChange.document.fields setObject:fooValue forKey:@"foo"]; + [listenResponse.documentChange.removedTargetIdsArray addValue:1]; + [listenResponse.documentChange.targetIdsArray addValue:2]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsDocumentChangeWithDeletions { + FSTWatchChange *expected = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:@[ @1, @2 ] + documentKey:FSTTestDocKey(@"coll/1") + document:FSTTestDeletedDoc(@"coll/1", 5)]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.documentDelete.document = @"projects/p/databases/d/documents/coll/1"; + listenResponse.documentDelete.readTime.nanos = 5000; + [listenResponse.documentDelete.removedTargetIdsArray addValue:1]; + [listenResponse.documentDelete.removedTargetIdsArray addValue:2]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsDocumentChangeWithRemoves { + FSTWatchChange *expected = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:@[ @1, @2 ] + documentKey:FSTTestDocKey(@"coll/1") + document:nil]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.documentRemove.document = @"projects/p/databases/d/documents/coll/1"; + [listenResponse.documentRemove.removedTargetIdsArray addValue:1]; + [listenResponse.documentRemove.removedTargetIdsArray addValue:2]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Remote/FSTStreamTests.m b/Firestore/Example/Tests/Remote/FSTStreamTests.m new file mode 100644 index 0000000..f27b200 --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTStreamTests.m @@ -0,0 +1,139 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Remote/FSTDatastore.h" + +#import <OCMock/OCMock.h> +#import <XCTest/XCTest.h> + +#import "Auth/FSTEmptyCredentialsProvider.h" +#import "Core/FSTDatabaseInfo.h" +#import "Model/FSTDatabaseID.h" +#import "Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.h" +#import "Util/FSTDispatchQueue.h" + +/** Expose otherwise private methods for testing. */ +@interface FSTStream (Testing) + +- (void)writesFinishedWithError:(NSError *_Nullable)error; + +@end + +@interface FSTStreamTests : XCTestCase +@end + +@implementation FSTStreamTests { + dispatch_queue_t _testQueue; + FSTDatabaseInfo *_databaseInfo; + FSTDispatchQueue *_workerDispatchQueue; + id<FSTCredentialsProvider> _credentials; +} + +- (void)setUp { + [super setUp]; + + FSTDatabaseID *databaseID = + [FSTDatabaseID databaseIDWithProject:@"project" database:kDefaultDatabaseID]; + _databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID + persistenceKey:@"test" + host:@"test-host" + sslEnabled:NO]; + + _testQueue = dispatch_queue_create("com.firebase.testing", DISPATCH_QUEUE_SERIAL); + _workerDispatchQueue = [FSTDispatchQueue queueWith:_testQueue]; + _credentials = [[FSTEmptyCredentialsProvider alloc] init]; +} + +- (void)tearDown { + [super tearDown]; +} + +- (void)testWatchStreamStop { + id delegate = OCMStrictProtocolMock(@protocol(FSTWatchStreamDelegate)); + + FSTWatchStream *stream = + OCMPartialMock([[FSTWatchStream alloc] initWithDatabase:_databaseInfo + workerDispatchQueue:_workerDispatchQueue + credentials:_credentials + responseMessageClass:[GCFSWriteResponse class] + delegate:delegate]); + OCMStub([stream createRPCWithRequestsWriter:[OCMArg any]]).andReturn(nil); + + // Start the stream up but that's not really the interesting bit. This is complicated by the fact + // that startup involves redispatching after credentials are returned. + dispatch_semaphore_t openCompleted = dispatch_semaphore_create(0); + OCMStub([delegate watchStreamDidOpen]).andDo(^(NSInvocation *invocation) { + dispatch_semaphore_signal(openCompleted); + }); + dispatch_async(_testQueue, ^{ + [stream start]; + }); + dispatch_semaphore_wait(openCompleted, DISPATCH_TIME_FOREVER); + OCMVerifyAll(delegate); + + // Stop must not call watchStreamDidClose because the full implementation of the delegate could + // attempt to restart the stream in the event it had pending watches. + dispatch_sync(_testQueue, ^{ + [stream stop]; + }); + OCMVerifyAll(delegate); + + // Simulate a final callback from GRPC + [stream writesFinishedWithError:nil]; + // Drain queue + dispatch_sync(_testQueue, ^{ + }); + OCMVerifyAll(delegate); +} + +- (void)testWriteStreamStop { + id delegate = OCMStrictProtocolMock(@protocol(FSTWriteStreamDelegate)); + + FSTWriteStream *stream = + OCMPartialMock([[FSTWriteStream alloc] initWithDatabase:_databaseInfo + workerDispatchQueue:_workerDispatchQueue + credentials:_credentials + responseMessageClass:[GCFSWriteResponse class] + delegate:delegate]); + OCMStub([stream createRPCWithRequestsWriter:[OCMArg any]]).andReturn(nil); + + // Start the stream up but that's not really the interesting bit. + dispatch_semaphore_t openCompleted = dispatch_semaphore_create(0); + OCMStub([delegate writeStreamDidOpen]).andDo(^(NSInvocation *invocation) { + dispatch_semaphore_signal(openCompleted); + }); + dispatch_async(_testQueue, ^{ + [stream start]; + }); + dispatch_semaphore_wait(openCompleted, DISPATCH_TIME_FOREVER); + OCMVerifyAll(delegate); + + // Stop must not call writeStreamDidClose because the full implementation of this delegate could + // attempt to restart the stream in the event it had pending writes. + dispatch_sync(_testQueue, ^{ + [stream stop]; + }); + OCMVerifyAll(delegate); + + // Simulate a final callback from GRPC + [stream writesFinishedWithError:nil]; + // Drain queue + dispatch_sync(_testQueue, ^{ + }); + OCMVerifyAll(delegate); +} + +@end diff --git a/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h b/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h new file mode 100644 index 0000000..f94fe05 --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h @@ -0,0 +1,40 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +#import "Core/FSTTypes.h" +#import "Remote/FSTWatchChange.h" + +NS_ASSUME_NONNULL_BEGIN + +/** FSTWatchTargetChange is a change to a watch target. */ +@interface FSTWatchTargetChange (Testing) + ++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray<NSNumber *> *)targetIDs; + ++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray<NSNumber *> *)targetIDs + cause:(nullable NSError *)cause; + ++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray<NSNumber *> *)targetIDs + resumeToken:(nullable NSData *)resumeToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.m b/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.m new file mode 100644 index 0000000..cb5e479 --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.m @@ -0,0 +1,54 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FSTWatchChange+Testing.h" + +#import "Model/FSTDocument.h" +#import "Remote/FSTWatchChange.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTWatchTargetChange (Testing) + ++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray<NSNumber *> *)targetIDs { + return [[FSTWatchTargetChange alloc] initWithState:state + targetIDs:targetIDs + resumeToken:[NSData data] + cause:nil]; +} + ++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray<NSNumber *> *)targetIDs + cause:(nullable NSError *)cause { + return [[FSTWatchTargetChange alloc] initWithState:state + targetIDs:targetIDs + resumeToken:[NSData data] + cause:cause]; +} + ++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray<NSNumber *> *)targetIDs + resumeToken:(nullable NSData *)resumeToken { + return [[FSTWatchTargetChange alloc] initWithState:state + targetIDs:targetIDs + resumeToken:resumeToken + cause:nil]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Remote/FSTWatchChangeTests.m b/Firestore/Example/Tests/Remote/FSTWatchChangeTests.m new file mode 100644 index 0000000..ccbd644 --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTWatchChangeTests.m @@ -0,0 +1,66 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Remote/FSTWatchChange.h" + +#import <XCTest/XCTest.h> + +#import "Model/FSTDocument.h" +#import "Remote/FSTExistenceFilter.h" + +#import "FSTHelpers.h" +#import "FSTWatchChange+Testing.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTWatchChangeTests : XCTestCase +@end + +@implementation FSTWatchChangeTests + +- (void)testDocumentChange { + FSTMaybeDocument *doc = FSTTestDoc(@"a/b", 1, @{}, NO); + FSTDocumentWatchChange *change = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @2, @3 ] + removedTargetIDs:@[ @4, @5 ] + documentKey:doc.key + document:doc]; + XCTAssertEqual(change.updatedTargetIDs.count, 3); + XCTAssertEqual(change.removedTargetIDs.count, 2); + // Testing object identity here is fine. + XCTAssertEqual(change.document, doc); +} + +- (void)testExistenceFilterChange { + FSTExistenceFilter *filter = [FSTExistenceFilter filterWithCount:7]; + FSTExistenceFilterWatchChange *change = + [FSTExistenceFilterWatchChange changeWithFilter:filter targetID:5]; + XCTAssertEqual(change.filter.count, 7); + XCTAssertEqual(change.targetID, 5); +} + +- (void)testWatchTargetChange { + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:@[ @1, @2 ] + cause:nil]; + XCTAssertEqual(change.state, FSTWatchTargetChangeStateReset); + XCTAssertEqual(change.targetIDs.count, 2); +} + +@end + +NS_ASSUME_NONNULL_END |