mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-09 22:36:01 +05:45
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.
898 lines
37 KiB
Plaintext
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
|