/* * 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/Source/Remote/FSTSerializerBeta.h" #import #import #import #import #import #import #import #include #import "Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.h" #import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" #import "Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.h" #import "Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.h" #import "Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h" #import "Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.h" #import "Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.h" #import "Firestore/Protos/objc/google/rpc/Status.pbobjc.h" #import "Firestore/Protos/objc/google/type/Latlng.pbobjc.h" #import "Firestore/Source/API/FIRFieldValue+Internal.h" #import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTDocumentKey.h" #import "Firestore/Source/Model/FSTFieldValue.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Model/FSTMutationBatch.h" #import "Firestore/Source/Remote/FSTWatchChange.h" #import "Firestore/Example/Tests/API/FSTAPIHelpers.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" #include "Firestore/core/src/firebase/firestore/model/field_mask.h" #include "Firestore/core/src/firebase/firestore/model/field_transform.h" #include "Firestore/core/src/firebase/firestore/model/precondition.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" #include "Firestore/core/test/firebase/firestore/testutil/testutil.h" namespace testutil = firebase::firestore::testutil; namespace util = firebase::firestore::util; using firebase::Timestamp; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::FieldMask; using firebase::firestore::model::FieldTransform; using firebase::firestore::model::Precondition; using firebase::firestore::model::SnapshotVersion; 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:(const FieldMask &)fieldMask; - (NSMutableArray *)encodedFieldTransforms: (const std::vector &)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 { DatabaseId _databaseId; } @property(nonatomic, strong) FSTSerializerBeta *serializer; @end @implementation FSTSerializerBetaTests - (void)setUp { _databaseId = DatabaseId("p", "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 *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 *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 *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 *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 *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 *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 *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 *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", DatabaseId::kDefault, @"foo/bar"); _databaseId = DatabaseId("project", DatabaseId::kDefault); 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 }, {}); 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)testEncodesServerTimestampTransformMutation { FSTTransformMutation *mutation = FSTTestTransformMutation(@"docs/1", @{ @"a" : [FIRFieldValue fieldValueForServerTimestamp], @"bar.baz" : [FIRFieldValue fieldValueForServerTimestamp] }); 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)testEncodesArrayTransformMutations { FSTTransformMutation *mutation = FSTTestTransformMutation(@"docs/1", @{ @"a" : [FIRFieldValue fieldValueForArrayUnion:@[ @"a", @2 ]], @"bar.baz" : [FIRFieldValue fieldValueForArrayRemove:@[ @{ @"x" : @1 } ]] }); GCFSWrite *proto = [GCFSWrite message]; proto.transform = [GCFSDocumentTransform message]; proto.transform.document = [self.serializer encodedDocumentKey:mutation.key]; GCFSDocumentTransform_FieldTransform *arrayUnion = [GCFSDocumentTransform_FieldTransform message]; arrayUnion.fieldPath = @"a"; arrayUnion.appendMissingElements = [GCFSArrayValue message]; NSMutableArray *unionElements = arrayUnion.appendMissingElements.valuesArray; [unionElements addObject:[self.serializer encodedFieldValue:FSTTestFieldValue(@"a")]]; [unionElements addObject:[self.serializer encodedFieldValue:FSTTestFieldValue(@2)]]; [proto.transform.fieldTransformsArray addObject:arrayUnion]; GCFSDocumentTransform_FieldTransform *arrayRemove = [GCFSDocumentTransform_FieldTransform message]; arrayRemove.fieldPath = @"bar.baz"; arrayRemove.removeAllFromArray_p = [GCFSArrayValue message]; NSMutableArray *removeElements = arrayRemove.removeAllFromArray_p.valuesArray; [removeElements addObject:[self.serializer encodedFieldValue:FSTTestFieldValue(@{ @"x" : @1 })]]; [proto.transform.fieldTransformsArray addObject:arrayRemove]; 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:Precondition::UpdateTime(testutil::Version(4))]; GCFSWrite *proto = [GCFSWrite message]; proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key]; proto.currentDocument.updateTime = [self.serializer encodedTimestamp:Timestamp{0, 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 listenSequenceNumber:3 purpose:FSTQueryPurposeListen]; NSDictionary *result = [self.serializer encodedListenRequestLabelsForQueryData:queryData]; XCTAssertNil(result); queryData = [[FSTQueryData alloc] initWithQuery:query targetID:2 listenSequenceNumber:3 purpose:FSTQueryPurposeLimboResolution]; result = [self.serializer encodedListenRequestLabelsForQueryData:queryData]; XCTAssertEqualObjects(result, @{@"goog-listen-tags" : @"limbo-document"}); queryData = [[FSTQueryData alloc] initWithQuery:query targetID:2 listenSequenceNumber:3 purpose:FSTQueryPurposeExistenceFilterMismatch]; result = [self.serializer encodedListenRequestLabelsForQueryData:queryData]; XCTAssertEqualObjects(result, @{@"goog-listen-tags" : @"existence-filter-mismatch"}); } - (void)testEncodesRelationFilter { FSTRelationFilter *input = (FSTRelationFilter *)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); } - (void)testEncodesArrayContainsFilter { FSTRelationFilter *input = (FSTRelationFilter *)FSTTestFilter("item.tags", @"array_contains", @"food"); GCFSStructuredQuery_Filter *actual = [self.serializer encodedRelationFilter:input]; GCFSStructuredQuery_Filter *expected = [GCFSStructuredQuery_Filter message]; GCFSStructuredQuery_FieldFilter *prop = expected.fieldFilter; prop.field.fieldPath = @"item.tags"; prop.op = GCFSStructuredQuery_FieldFilter_Operator_ArrayContains; prop.value.stringValue = @"food"; XCTAssertEqualObjects(actual, expected); } #pragma mark - encodedQuery - (void)testEncodesFirstLevelKeyQueries { FSTQuery *q = FSTTestQuery("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 = FSTTestQuery("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 = FSTTestQuery("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 = [FSTTestQuery("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 = [[[FSTTestQuery("rooms/1/messages/10/attachments") queryByAddingFilter:FSTTestFilter("prop", @">=", @(42))] queryByAddingFilter:FSTTestFilter("author", @"==", @"dimond")] queryByAddingFilter:FSTTestFilter("tags", @"array_contains", @"pending")]; 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_Filter *filter3 = [GCFSStructuredQuery_Filter message]; GCFSStructuredQuery_FieldFilter *field3 = filter3.fieldFilter; field3.field.fieldPath = @"tags"; field3.op = GCFSStructuredQuery_FieldFilter_Operator_ArrayContains; field3.value.stringValue = @"pending"; GCFSStructuredQuery_CompositeFilter *composite = expected.query.structuredQuery.where.compositeFilter; composite.op = GCFSStructuredQuery_CompositeFilter_Operator_And; [composite.filtersArray addObject:filter1]; [composite.filtersArray addObject:filter2]; [composite.filtersArray addObject:filter3]; [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)op { FSTQuery *q = [FSTTestQuery("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 = op; expected.targetId = 1; [self assertRoundTripForQueryData:model proto:expected]; } - (void)testEncodesSortOrders { FSTQuery *q = [FSTTestQuery("docs") queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:testutil::Field("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 = [FSTTestQuery("rooms/1/messages/10/attachments") queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:testutil::Field("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 = [FSTTestQuery("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 = FSTTestQuery("docs"); FSTQueryData *model = [[FSTQueryData alloc] initWithQuery:q targetID:1 listenSequenceNumber:0 purpose:FSTQueryPurposeListen snapshotVersion:SnapshotVersion::None() 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 listenSequenceNumber:0 purpose:FSTQueryPurposeListen snapshotVersion:SnapshotVersion::None() 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