Files
firebase-ios-sdk/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm
T
Michael Lehenbauer d9f11627a3 Fixes decoding WriteResults for deletes (with no updateTime). (#1622)
This should resolve #1591.  We were inadvertantly decoding the WriteResult as
having an updateTime of 0 which caused it to be older than the version of the
document in the RemoteDocumentsCache. Therefore we kept the old version in
the cache instead of persisting the deleted document. This caused the document
to reappear in the query results until we resolved it via a limbo resolution.
2018-08-01 19:16:48 -07:00

898 lines
37 KiB
Plaintext

/*
* 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 <FirebaseFirestore/FIRFieldPath.h>
#import <FirebaseFirestore/FIRFieldValue.h>
#import <FirebaseFirestore/FIRFirestoreErrors.h>
#import <FirebaseFirestore/FIRGeoPoint.h>
#import <FirebaseFirestore/FIRTimestamp.h>
#import <GRPCClient/GRPCCall.h>
#import <XCTest/XCTest.h>
#include <vector>
#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<GCFSDocumentTransform_FieldTransform *> *)encodedFieldTransforms:
(const std::vector<FieldTransform> &)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<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", 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)testDecodesMutationResult {
GCFSWriteResult *proto = [GCFSWriteResult message];
proto.updateTime = [self.serializer encodedTimestamp:Timestamp{0, 4000}];
[proto.transformResultsArray addObject:[self.serializer encodedString:@"result"]];
FSTMutationResult *result = [self.serializer decodedMutationResult:proto];
XCTAssertEqual(result.version.value(), (SnapshotVersion{Timestamp{0, 4000}}));
XCTAssertEqualObjects(result.transformResults, @[ [FSTStringValue stringValue:@"result"] ]);
}
- (void)testDecodesDeleteMutationResult {
GCFSWriteResult *proto = [GCFSWriteResult message];
// Deletes don't set updateTime (or transformResults).
FSTMutationResult *result = [self.serializer decodedMutationResult:proto];
XCTAssertFalse(result.version.has_value());
XCTAssertEqual(result.transformResults.count, 0);
}
- (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<NSString *, NSString *> *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