FSTSerializerBetaTests.mm 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946
  1. /*
  2. * Copyright 2017 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import "Firestore/Source/Remote/FSTSerializerBeta.h"
  17. #import <FirebaseFirestore/FIRFieldPath.h>
  18. #import <FirebaseFirestore/FIRFieldValue.h>
  19. #import <FirebaseFirestore/FIRFirestoreErrors.h>
  20. #import <FirebaseFirestore/FIRGeoPoint.h>
  21. #import <FirebaseFirestore/FIRTimestamp.h>
  22. #import <XCTest/XCTest.h>
  23. #include <memory>
  24. #include <vector>
  25. #import "Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.h"
  26. #import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h"
  27. #import "Firestore/Protos/objc/google/firestore/v1/Common.pbobjc.h"
  28. #import "Firestore/Protos/objc/google/firestore/v1/Document.pbobjc.h"
  29. #import "Firestore/Protos/objc/google/firestore/v1/Firestore.pbobjc.h"
  30. #import "Firestore/Protos/objc/google/firestore/v1/Query.pbobjc.h"
  31. #import "Firestore/Protos/objc/google/firestore/v1/Write.pbobjc.h"
  32. #import "Firestore/Protos/objc/google/rpc/Status.pbobjc.h"
  33. #import "Firestore/Protos/objc/google/type/Latlng.pbobjc.h"
  34. #import "Firestore/Source/API/FIRFieldValue+Internal.h"
  35. #import "Firestore/Source/Core/FSTQuery.h"
  36. #import "Firestore/Source/Local/FSTQueryData.h"
  37. #import "Firestore/Source/Model/FSTDocument.h"
  38. #import "Firestore/Source/Model/FSTMutation.h"
  39. #import "Firestore/Source/Model/FSTMutationBatch.h"
  40. #import "Firestore/Example/Tests/API/FSTAPIHelpers.h"
  41. #import "Firestore/Example/Tests/Util/FSTHelpers.h"
  42. #include "Firestore/core/include/firebase/firestore/firestore_errors.h"
  43. #include "Firestore/core/src/firebase/firestore/core/field_filter.h"
  44. #include "Firestore/core/src/firebase/firestore/core/filter.h"
  45. #include "Firestore/core/src/firebase/firestore/model/database_id.h"
  46. #include "Firestore/core/src/firebase/firestore/model/field_mask.h"
  47. #include "Firestore/core/src/firebase/firestore/model/field_path.h"
  48. #include "Firestore/core/src/firebase/firestore/model/field_transform.h"
  49. #include "Firestore/core/src/firebase/firestore/model/field_value.h"
  50. #include "Firestore/core/src/firebase/firestore/model/precondition.h"
  51. #include "Firestore/core/src/firebase/firestore/remote/watch_change.h"
  52. #include "Firestore/core/src/firebase/firestore/util/hard_assert.h"
  53. #include "Firestore/core/src/firebase/firestore/util/status.h"
  54. #include "Firestore/core/src/firebase/firestore/util/string_apple.h"
  55. #include "Firestore/core/test/firebase/firestore/testutil/testutil.h"
  56. namespace testutil = firebase::firestore::testutil;
  57. namespace util = firebase::firestore::util;
  58. using firebase::Timestamp;
  59. using firebase::firestore::FirestoreErrorCode;
  60. using firebase::firestore::core::FieldFilter;
  61. using firebase::firestore::model::DatabaseId;
  62. using firebase::firestore::model::DocumentKey;
  63. using firebase::firestore::model::DocumentState;
  64. using firebase::firestore::model::FieldMask;
  65. using firebase::firestore::model::FieldPath;
  66. using firebase::firestore::model::FieldTransform;
  67. using firebase::firestore::model::FieldValue;
  68. using firebase::firestore::model::ObjectValue;
  69. using firebase::firestore::model::Precondition;
  70. using firebase::firestore::model::SnapshotVersion;
  71. using firebase::firestore::remote::DocumentWatchChange;
  72. using firebase::firestore::remote::ExistenceFilterWatchChange;
  73. using firebase::firestore::remote::WatchChange;
  74. using firebase::firestore::remote::WatchTargetChange;
  75. using firebase::firestore::remote::WatchTargetChangeState;
  76. using firebase::firestore::testutil::Array;
  77. using firebase::firestore::util::Status;
  78. using testutil::Filter;
  79. using testutil::Ref;
  80. using testutil::Value;
  81. namespace {
  82. template <typename T>
  83. bool Equals(const WatchChange &lhs, const WatchChange &rhs) {
  84. return static_cast<const T &>(lhs) == static_cast<const T &>(rhs);
  85. }
  86. // Compares two `WatchChange`s taking into account their actual derived type.
  87. bool IsWatchChangeEqual(const WatchChange &lhs, const WatchChange &rhs) {
  88. if (lhs.type() != rhs.type()) {
  89. return false;
  90. }
  91. switch (lhs.type()) {
  92. case WatchChange::Type::Document:
  93. return Equals<DocumentWatchChange>(lhs, rhs);
  94. case WatchChange::Type::ExistenceFilter:
  95. return Equals<ExistenceFilterWatchChange>(lhs, rhs);
  96. case WatchChange::Type::TargetChange:
  97. return Equals<WatchTargetChange>(lhs, rhs);
  98. }
  99. UNREACHABLE();
  100. }
  101. NSString *const kDocumentKeyPath =
  102. [[NSString alloc] initWithUTF8String:FieldPath::kDocumentKeyPath];
  103. } // namespace
  104. NS_ASSUME_NONNULL_BEGIN
  105. @interface GCFSStructuredQuery_Order (Test)
  106. + (instancetype)messageWithProperty:(NSString *)property ascending:(BOOL)ascending;
  107. @end
  108. @implementation GCFSStructuredQuery_Order (Test)
  109. + (instancetype)messageWithProperty:(NSString *)property ascending:(BOOL)ascending {
  110. GCFSStructuredQuery_Order *order = [GCFSStructuredQuery_Order message];
  111. order.field.fieldPath = property;
  112. order.direction = ascending ? GCFSStructuredQuery_Direction_Ascending
  113. : GCFSStructuredQuery_Direction_Descending;
  114. return order;
  115. }
  116. @end
  117. @interface FSTSerializerBetaTests : XCTestCase
  118. @property(nonatomic, strong) FSTSerializerBeta *serializer;
  119. @end
  120. @implementation FSTSerializerBetaTests
  121. - (void)setUp {
  122. self.serializer = [[FSTSerializerBeta alloc] initWithDatabaseID:DatabaseId("p", "d")];
  123. }
  124. - (void)testEncodesNull {
  125. FieldValue model = FieldValue::Null();
  126. GCFSValue *proto = [GCFSValue message];
  127. proto.nullValue = GPBNullValue_NullValue;
  128. [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_NullValue];
  129. }
  130. - (void)testEncodesBool {
  131. NSArray<NSNumber *> *examples = @[ @YES, @NO ];
  132. for (NSNumber *example in examples) {
  133. FieldValue model = FSTTestFieldValue(example);
  134. GCFSValue *proto = [GCFSValue message];
  135. proto.booleanValue = [example boolValue];
  136. [self assertRoundTripForModel:model
  137. proto:proto
  138. type:GCFSValue_ValueType_OneOfCase_BooleanValue];
  139. }
  140. }
  141. - (void)testEncodesIntegers {
  142. NSArray<NSNumber *> *examples = @[ @(LLONG_MIN), @(-100), @(-1), @0, @1, @100, @(LLONG_MAX) ];
  143. for (NSNumber *example in examples) {
  144. FieldValue model = FSTTestFieldValue(example);
  145. GCFSValue *proto = [GCFSValue message];
  146. proto.integerValue = [example longLongValue];
  147. [self assertRoundTripForModel:model
  148. proto:proto
  149. type:GCFSValue_ValueType_OneOfCase_IntegerValue];
  150. }
  151. }
  152. - (void)testEncodesDoubles {
  153. NSArray<NSNumber *> *examples = @[
  154. // normal negative numbers.
  155. @(-INFINITY), @(-DBL_MAX), @(LLONG_MIN * 1.0 - 1.0), @(-2.0), @(-1.1), @(-1.0), @(-DBL_MIN),
  156. // negative smallest subnormal, zeroes, positive smallest subnormal
  157. @(-0x1.0p-1074), @(-0.0), @(0.0), @(0x1.0p-1074),
  158. // and the rest
  159. @(DBL_MIN), @0.1, @1.1, @(LLONG_MAX * 1.0), @(DBL_MAX), @(INFINITY),
  160. // NaN.
  161. @(0.0 / 0.0)
  162. ];
  163. for (NSNumber *example in examples) {
  164. FieldValue model = FSTTestFieldValue(example);
  165. GCFSValue *proto = [GCFSValue message];
  166. proto.doubleValue = [example doubleValue];
  167. [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_DoubleValue];
  168. }
  169. }
  170. - (void)testEncodesStrings {
  171. NSArray<NSString *> *examples = @[
  172. @"",
  173. @"a",
  174. @"abc def",
  175. @"æ",
  176. @"\0\ud7ff\ue000\uffff",
  177. @"(╯°□°)╯︵ ┻━┻",
  178. ];
  179. for (NSString *example in examples) {
  180. FieldValue model = FSTTestFieldValue(example);
  181. GCFSValue *proto = [GCFSValue message];
  182. proto.stringValue = example;
  183. [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_StringValue];
  184. }
  185. }
  186. - (void)testEncodesDates {
  187. NSDateComponents *dateWithNanos = FSTTestDateComponents(2016, 1, 2, 10, 20, 50);
  188. dateWithNanos.nanosecond = 500000000;
  189. NSArray<NSDate *> *examples = @[
  190. [[NSCalendar currentCalendar] dateFromComponents:dateWithNanos],
  191. FSTTestDate(2016, 6, 17, 10, 50, 15)
  192. ];
  193. GCFSValue *timestamp1 = [GCFSValue message];
  194. timestamp1.timestampValue.seconds = 1451730050;
  195. timestamp1.timestampValue.nanos = 500000000;
  196. GCFSValue *timestamp2 = [GCFSValue message];
  197. timestamp2.timestampValue.seconds = 1466160615;
  198. timestamp2.timestampValue.nanos = 0;
  199. NSArray<GCFSValue *> *expectedTimestamps = @[ timestamp1, timestamp2 ];
  200. for (NSUInteger i = 0; i < [examples count]; i++) {
  201. [self assertRoundTripForModel:FSTTestFieldValue(examples[i])
  202. proto:expectedTimestamps[i]
  203. type:GCFSValue_ValueType_OneOfCase_TimestampValue];
  204. }
  205. }
  206. - (void)testEncodesGeoPoints {
  207. NSArray<FIRGeoPoint *> *examples =
  208. @[ FSTTestGeoPoint(0, 0), FSTTestGeoPoint(1.24, 4.56), FSTTestGeoPoint(-90, 180) ];
  209. for (FIRGeoPoint *example in examples) {
  210. FieldValue model = FSTTestFieldValue(example);
  211. GCFSValue *proto = [GCFSValue message];
  212. proto.geoPointValue = [GTPLatLng message];
  213. proto.geoPointValue.latitude = example.latitude;
  214. proto.geoPointValue.longitude = example.longitude;
  215. [self assertRoundTripForModel:model
  216. proto:proto
  217. type:GCFSValue_ValueType_OneOfCase_GeoPointValue];
  218. }
  219. }
  220. - (void)testEncodesBlobs {
  221. NSArray<NSData *> *examples = @[
  222. FSTTestData(-1),
  223. FSTTestData(0, -1),
  224. FSTTestData(0, 1, 2, -1),
  225. FSTTestData(255, -1),
  226. FSTTestData(0, 1, 255, -1),
  227. ];
  228. for (NSData *example in examples) {
  229. FieldValue model = FSTTestFieldValue(example);
  230. GCFSValue *proto = [GCFSValue message];
  231. proto.bytesValue = example;
  232. [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_BytesValue];
  233. }
  234. }
  235. - (void)testEncodesResourceNames {
  236. self.serializer = [[FSTSerializerBeta alloc] initWithDatabaseID:DatabaseId("project")];
  237. FSTDocumentKeyReference *reference = FSTTestRef("project", DatabaseId::kDefault, @"foo/bar");
  238. GCFSValue *proto = [GCFSValue message];
  239. proto.referenceValue = @"projects/project/databases/(default)/documents/foo/bar";
  240. [self assertRoundTripForModel:FSTTestFieldValue(reference)
  241. proto:proto
  242. type:GCFSValue_ValueType_OneOfCase_ReferenceValue];
  243. }
  244. - (void)testEncodesArrays {
  245. FieldValue model = FSTTestFieldValue(@[ @YES, @"foo" ]);
  246. GCFSValue *proto = [GCFSValue message];
  247. [proto.arrayValue.valuesArray addObjectsFromArray:@[
  248. [self.serializer encodedBool:true], [self.serializer encodedString:"foo"]
  249. ]];
  250. [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_ArrayValue];
  251. }
  252. - (void)testEncodesEmptyMap {
  253. FieldValue model = ObjectValue::Empty();
  254. GCFSValue *proto = [GCFSValue message];
  255. proto.mapValue = [GCFSMapValue message];
  256. [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_MapValue];
  257. }
  258. - (void)testEncodesNestedObjects {
  259. FieldValue model = FSTTestFieldValue(@{
  260. @"b" : @YES,
  261. @"d" : @(DBL_MAX),
  262. @"i" : @1,
  263. @"n" : [NSNull null],
  264. @"s" : @"foo",
  265. @"a" : @[ @2, @"bar", @{@"b" : @NO} ],
  266. @"o" : @{
  267. @"d" : @100,
  268. @"nested" : @{@"e" : @(LLONG_MIN)},
  269. },
  270. });
  271. GCFSValue *innerObject = [GCFSValue message];
  272. innerObject.mapValue.fields[@"b"] = [self.serializer encodedBool:false];
  273. GCFSValue *middleArray = [GCFSValue message];
  274. [middleArray.arrayValue.valuesArray addObjectsFromArray:@[
  275. [self.serializer encodedInteger:2], [self.serializer encodedString:"bar"], innerObject
  276. ]];
  277. innerObject = [GCFSValue message];
  278. innerObject.mapValue.fields[@"e"] = [self.serializer encodedInteger:LLONG_MIN];
  279. GCFSValue *middleObject = [GCFSValue message];
  280. [middleObject.mapValue.fields addEntriesFromDictionary:@{
  281. @"d" : [self.serializer encodedInteger:100],
  282. @"nested" : innerObject
  283. }];
  284. GCFSValue *proto = [GCFSValue message];
  285. [proto.mapValue.fields addEntriesFromDictionary:@{
  286. @"b" : [self.serializer encodedBool:true],
  287. @"d" : [self.serializer encodedDouble:DBL_MAX],
  288. @"i" : [self.serializer encodedInteger:1],
  289. @"n" : [self.serializer encodedNull],
  290. @"s" : [self.serializer encodedString:"foo"],
  291. @"a" : middleArray,
  292. @"o" : middleObject
  293. }];
  294. [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_MapValue];
  295. }
  296. - (void)assertRoundTripForModel:(const FieldValue &)model
  297. proto:(GCFSValue *)value
  298. type:(GCFSValue_ValueType_OneOfCase)type {
  299. GCFSValue *actualProto = [self.serializer encodedFieldValue:model];
  300. XCTAssertEqual(actualProto.valueTypeOneOfCase, type);
  301. XCTAssertEqualObjects(actualProto, value);
  302. FieldValue actualModel = [self.serializer decodedFieldValue:value];
  303. XCTAssertEqual(actualModel, model);
  304. }
  305. - (void)testEncodesSetMutation {
  306. FSTSetMutation *mutation = FSTTestSetMutation(@"docs/1", @{@"a" : @"b", @"num" : @1});
  307. GCFSWrite *proto = [GCFSWrite message];
  308. proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key];
  309. [self assertRoundTripForMutation:mutation proto:proto];
  310. }
  311. - (void)testEncodesPatchMutation {
  312. FSTPatchMutation *mutation = FSTTestPatchMutation(
  313. "docs/1", @{@"a" : @"b", @"num" : @1, @"some.de\\\\ep.th\\ing'" : @2}, {});
  314. GCFSWrite *proto = [GCFSWrite message];
  315. proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key];
  316. proto.updateMask = [self.serializer encodedFieldMask:*(mutation.fieldMask)];
  317. proto.currentDocument.exists = YES;
  318. [self assertRoundTripForMutation:mutation proto:proto];
  319. }
  320. - (void)testEncodesDeleteMutation {
  321. FSTDeleteMutation *mutation = FSTTestDeleteMutation(@"docs/1");
  322. GCFSWrite *proto = [GCFSWrite message];
  323. proto.delete_p = @"projects/p/databases/d/documents/docs/1";
  324. [self assertRoundTripForMutation:mutation proto:proto];
  325. }
  326. - (void)testEncodesServerTimestampTransformMutation {
  327. FSTTransformMutation *mutation = FSTTestTransformMutation(@"docs/1", @{
  328. @"a" : [FIRFieldValue fieldValueForServerTimestamp],
  329. @"bar.baz" : [FIRFieldValue fieldValueForServerTimestamp]
  330. });
  331. GCFSWrite *proto = [GCFSWrite message];
  332. proto.transform = [GCFSDocumentTransform message];
  333. proto.transform.document = [self.serializer encodedDocumentKey:mutation.key];
  334. proto.transform.fieldTransformsArray =
  335. [self.serializer encodedFieldTransforms:mutation.fieldTransforms];
  336. proto.currentDocument.exists = YES;
  337. [self assertRoundTripForMutation:mutation proto:proto];
  338. }
  339. - (void)testEncodesArrayTransformMutations {
  340. FSTTransformMutation *mutation = FSTTestTransformMutation(@"docs/1", @{
  341. @"a" : [FIRFieldValue fieldValueForArrayUnion:@[ @"a", @2 ]],
  342. @"bar.baz" : [FIRFieldValue fieldValueForArrayRemove:@[ @{@"x" : @1} ]]
  343. });
  344. GCFSWrite *proto = [GCFSWrite message];
  345. proto.transform = [GCFSDocumentTransform message];
  346. proto.transform.document = [self.serializer encodedDocumentKey:mutation.key];
  347. GCFSDocumentTransform_FieldTransform *arrayUnion = [GCFSDocumentTransform_FieldTransform message];
  348. arrayUnion.fieldPath = @"a";
  349. arrayUnion.appendMissingElements = [GCFSArrayValue message];
  350. NSMutableArray *unionElements = arrayUnion.appendMissingElements.valuesArray;
  351. [unionElements addObject:[self.serializer encodedFieldValue:FSTTestFieldValue(@"a")]];
  352. [unionElements addObject:[self.serializer encodedFieldValue:FSTTestFieldValue(@2)]];
  353. [proto.transform.fieldTransformsArray addObject:arrayUnion];
  354. GCFSDocumentTransform_FieldTransform *arrayRemove =
  355. [GCFSDocumentTransform_FieldTransform message];
  356. arrayRemove.fieldPath = @"bar.baz";
  357. arrayRemove.removeAllFromArray_p = [GCFSArrayValue message];
  358. NSMutableArray *removeElements = arrayRemove.removeAllFromArray_p.valuesArray;
  359. [removeElements addObject:[self.serializer encodedFieldValue:FSTTestFieldValue(@{@"x" : @1})]];
  360. [proto.transform.fieldTransformsArray addObject:arrayRemove];
  361. proto.currentDocument.exists = YES;
  362. [self assertRoundTripForMutation:mutation proto:proto];
  363. }
  364. - (void)testEncodesSetMutationWithPrecondition {
  365. FSTSetMutation *mutation =
  366. [[FSTSetMutation alloc] initWithKey:FSTTestDocKey(@"foo/bar")
  367. value:FSTTestObjectValue(@{@"a" : @"b", @"num" : @1})
  368. precondition:Precondition::UpdateTime(testutil::Version(4))];
  369. GCFSWrite *proto = [GCFSWrite message];
  370. proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key];
  371. proto.currentDocument.updateTime = [self.serializer encodedTimestamp:Timestamp{0, 4000}];
  372. [self assertRoundTripForMutation:mutation proto:proto];
  373. }
  374. - (void)assertRoundTripForMutation:(FSTMutation *)mutation proto:(GCFSWrite *)proto {
  375. GCFSWrite *actualProto = [self.serializer encodedMutation:mutation];
  376. XCTAssertEqualObjects(actualProto, proto);
  377. FSTMutation *actualMutation = [self.serializer decodedMutation:proto];
  378. XCTAssertEqualObjects(actualMutation, mutation);
  379. }
  380. - (void)testDecodesMutationResult {
  381. SnapshotVersion commitVersion = testutil::Version(3000);
  382. SnapshotVersion updateVersion = testutil::Version(4000);
  383. GCFSWriteResult *proto = [GCFSWriteResult message];
  384. proto.updateTime = [self.serializer encodedTimestamp:updateVersion.timestamp()];
  385. [proto.transformResultsArray addObject:[self.serializer encodedString:"result"]];
  386. FSTMutationResult *result = [self.serializer decodedMutationResult:proto
  387. commitVersion:commitVersion];
  388. XCTAssertEqual(result.version, updateVersion);
  389. XCTAssertTrue(result.transformResults.has_value());
  390. XCTAssertEqual(*result.transformResults, Array("result").array_value());
  391. }
  392. - (void)testDecodesDeleteMutationResult {
  393. GCFSWriteResult *proto = [GCFSWriteResult message];
  394. SnapshotVersion commitVersion = testutil::Version(4000);
  395. FSTMutationResult *result = [self.serializer decodedMutationResult:proto
  396. commitVersion:commitVersion];
  397. XCTAssertEqual(result.version, commitVersion);
  398. XCTAssertFalse(result.transformResults.has_value());
  399. }
  400. - (void)testRoundTripSpecialFieldNames {
  401. FSTMutation *set = FSTTestSetMutation(@"collection/key", @{
  402. @"field" : [NSString stringWithFormat:@"field %d", 1],
  403. @"field.dot" : @2,
  404. @"field\\slash" : @3
  405. });
  406. GCFSWrite *encoded = [self.serializer encodedMutation:set];
  407. FSTMutation *decoded = [self.serializer decodedMutation:encoded];
  408. XCTAssertEqualObjects(set, decoded);
  409. }
  410. - (void)testEncodesListenRequestLabels {
  411. FSTQuery *query = FSTTestQuery("collection/key");
  412. FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query
  413. targetID:2
  414. listenSequenceNumber:3
  415. purpose:FSTQueryPurposeListen];
  416. NSDictionary<NSString *, NSString *> *result =
  417. [self.serializer encodedListenRequestLabelsForQueryData:queryData];
  418. XCTAssertNil(result);
  419. queryData = [[FSTQueryData alloc] initWithQuery:query
  420. targetID:2
  421. listenSequenceNumber:3
  422. purpose:FSTQueryPurposeLimboResolution];
  423. result = [self.serializer encodedListenRequestLabelsForQueryData:queryData];
  424. XCTAssertEqualObjects(result, @{@"goog-listen-tags" : @"limbo-document"});
  425. queryData = [[FSTQueryData alloc] initWithQuery:query
  426. targetID:2
  427. listenSequenceNumber:3
  428. purpose:FSTQueryPurposeExistenceFilterMismatch];
  429. result = [self.serializer encodedListenRequestLabelsForQueryData:queryData];
  430. XCTAssertEqualObjects(result, @{@"goog-listen-tags" : @"existence-filter-mismatch"});
  431. }
  432. - (void)testEncodesUnaryFilter {
  433. auto input = Filter("item", "==", nullptr);
  434. GCFSStructuredQuery_Filter *actual = [self.serializer encodedUnaryOrFieldFilter:*input];
  435. GCFSStructuredQuery_Filter *expected = [GCFSStructuredQuery_Filter message];
  436. GCFSStructuredQuery_UnaryFilter *prop = expected.unaryFilter;
  437. prop.field.fieldPath = @"item";
  438. prop.op = GCFSStructuredQuery_UnaryFilter_Operator_IsNull;
  439. XCTAssertEqualObjects(actual, expected);
  440. auto roundTripped = [self.serializer decodedUnaryFilter:prop];
  441. XCTAssertEqual(*input, *roundTripped);
  442. }
  443. - (void)testEncodesFieldFilter {
  444. auto input = Filter("item.part.top", "==", "food");
  445. GCFSStructuredQuery_Filter *actual = [self.serializer encodedUnaryOrFieldFilter:*input];
  446. GCFSStructuredQuery_Filter *expected = [GCFSStructuredQuery_Filter message];
  447. GCFSStructuredQuery_FieldFilter *prop = expected.fieldFilter;
  448. prop.field.fieldPath = @"item.part.top";
  449. prop.op = GCFSStructuredQuery_FieldFilter_Operator_Equal;
  450. prop.value.stringValue = @"food";
  451. XCTAssertEqualObjects(actual, expected);
  452. auto roundTripped = [self.serializer decodedFieldFilter:prop];
  453. XCTAssertEqual(*input, *roundTripped);
  454. }
  455. - (void)testEncodesArrayContainsFilter {
  456. auto input = Filter("item.tags", "array_contains", "food");
  457. GCFSStructuredQuery_Filter *actual = [self.serializer encodedUnaryOrFieldFilter:*input];
  458. GCFSStructuredQuery_Filter *expected = [GCFSStructuredQuery_Filter message];
  459. GCFSStructuredQuery_FieldFilter *prop = expected.fieldFilter;
  460. prop.field.fieldPath = @"item.tags";
  461. prop.op = GCFSStructuredQuery_FieldFilter_Operator_ArrayContains;
  462. prop.value.stringValue = @"food";
  463. XCTAssertEqualObjects(actual, expected);
  464. auto roundTripped = [self.serializer decodedFieldFilter:prop];
  465. XCTAssertEqual(*input, *roundTripped);
  466. }
  467. - (void)testEncodesKeyFieldFilter {
  468. auto input = Filter("__name__", "==", Ref("p/d", "coll/doc"));
  469. GCFSStructuredQuery_Filter *actual = [self.serializer encodedUnaryOrFieldFilter:*input];
  470. GCFSStructuredQuery_Filter *expected = [GCFSStructuredQuery_Filter message];
  471. GCFSStructuredQuery_FieldFilter *prop = expected.fieldFilter;
  472. prop.field.fieldPath = @"__name__";
  473. prop.op = GCFSStructuredQuery_FieldFilter_Operator_Equal;
  474. prop.value.referenceValue = @"projects/p/databases/d/documents/coll/doc";
  475. XCTAssertEqualObjects(actual, expected);
  476. auto roundTripped = [self.serializer decodedFieldFilter:prop];
  477. XCTAssertEqual(*input, *roundTripped);
  478. }
  479. #pragma mark - encodedQuery
  480. - (void)testEncodesFirstLevelKeyQueries {
  481. FSTQuery *q = FSTTestQuery("docs/1");
  482. FSTQueryData *model = [self queryDataForQuery:q];
  483. GCFSTarget *expected = [GCFSTarget message];
  484. [expected.documents.documentsArray addObject:@"projects/p/databases/d/documents/docs/1"];
  485. expected.targetId = 1;
  486. [self assertRoundTripForQueryData:model proto:expected];
  487. }
  488. - (void)testEncodesFirstLevelAncestorQueries {
  489. FSTQuery *q = FSTTestQuery("messages");
  490. FSTQueryData *model = [self queryDataForQuery:q];
  491. GCFSTarget *expected = [GCFSTarget message];
  492. expected.query.parent = @"projects/p/databases/d/documents";
  493. GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
  494. from.collectionId = @"messages";
  495. [expected.query.structuredQuery.fromArray addObject:from];
  496. [expected.query.structuredQuery.orderByArray
  497. addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
  498. expected.targetId = 1;
  499. [self assertRoundTripForQueryData:model proto:expected];
  500. }
  501. - (void)testEncodesNestedAncestorQueries {
  502. FSTQuery *q = FSTTestQuery("rooms/1/messages/10/attachments");
  503. FSTQueryData *model = [self queryDataForQuery:q];
  504. GCFSTarget *expected = [GCFSTarget message];
  505. expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10";
  506. GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
  507. from.collectionId = @"attachments";
  508. [expected.query.structuredQuery.fromArray addObject:from];
  509. [expected.query.structuredQuery.orderByArray
  510. addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
  511. expected.targetId = 1;
  512. [self assertRoundTripForQueryData:model proto:expected];
  513. }
  514. - (void)testEncodesSingleFiltersAtFirstLevelCollections {
  515. FSTQuery *q = [FSTTestQuery("docs") queryByAddingFilter:Filter("prop", "<", 42)];
  516. FSTQueryData *model = [self queryDataForQuery:q];
  517. GCFSTarget *expected = [GCFSTarget message];
  518. expected.query.parent = @"projects/p/databases/d/documents";
  519. GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
  520. from.collectionId = @"docs";
  521. [expected.query.structuredQuery.fromArray addObject:from];
  522. [expected.query.structuredQuery.orderByArray
  523. addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]];
  524. [expected.query.structuredQuery.orderByArray
  525. addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
  526. GCFSStructuredQuery_FieldFilter *filter = expected.query.structuredQuery.where.fieldFilter;
  527. filter.field.fieldPath = @"prop";
  528. filter.op = GCFSStructuredQuery_FieldFilter_Operator_LessThan;
  529. filter.value.integerValue = 42;
  530. expected.targetId = 1;
  531. [self assertRoundTripForQueryData:model proto:expected];
  532. }
  533. - (void)testEncodesMultipleFiltersOnDeeperCollections {
  534. FSTQuery *q = [[[FSTTestQuery("rooms/1/messages/10/attachments")
  535. queryByAddingFilter:Filter("prop", ">=", 42)]
  536. queryByAddingFilter:Filter("author", "==", "dimond")]
  537. queryByAddingFilter:Filter("tags", "array_contains", "pending")];
  538. FSTQueryData *model = [self queryDataForQuery:q];
  539. GCFSTarget *expected = [GCFSTarget message];
  540. expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10";
  541. GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
  542. from.collectionId = @"attachments";
  543. [expected.query.structuredQuery.fromArray addObject:from];
  544. GCFSStructuredQuery_Filter *filter1 = [GCFSStructuredQuery_Filter message];
  545. GCFSStructuredQuery_FieldFilter *field1 = filter1.fieldFilter;
  546. field1.field.fieldPath = @"prop";
  547. field1.op = GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual;
  548. field1.value.integerValue = 42;
  549. GCFSStructuredQuery_Filter *filter2 = [GCFSStructuredQuery_Filter message];
  550. GCFSStructuredQuery_FieldFilter *field2 = filter2.fieldFilter;
  551. field2.field.fieldPath = @"author";
  552. field2.op = GCFSStructuredQuery_FieldFilter_Operator_Equal;
  553. field2.value.stringValue = @"dimond";
  554. GCFSStructuredQuery_Filter *filter3 = [GCFSStructuredQuery_Filter message];
  555. GCFSStructuredQuery_FieldFilter *field3 = filter3.fieldFilter;
  556. field3.field.fieldPath = @"tags";
  557. field3.op = GCFSStructuredQuery_FieldFilter_Operator_ArrayContains;
  558. field3.value.stringValue = @"pending";
  559. GCFSStructuredQuery_CompositeFilter *composite =
  560. expected.query.structuredQuery.where.compositeFilter;
  561. composite.op = GCFSStructuredQuery_CompositeFilter_Operator_And;
  562. [composite.filtersArray addObject:filter1];
  563. [composite.filtersArray addObject:filter2];
  564. [composite.filtersArray addObject:filter3];
  565. [expected.query.structuredQuery.orderByArray
  566. addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]];
  567. [expected.query.structuredQuery.orderByArray
  568. addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
  569. expected.targetId = 1;
  570. [self assertRoundTripForQueryData:model proto:expected];
  571. }
  572. - (void)testEncodesNullFilter {
  573. [self unaryFilterTestWithValue:Value(nullptr)
  574. expectedUnaryOperator:GCFSStructuredQuery_UnaryFilter_Operator_IsNull];
  575. }
  576. - (void)testEncodesNanFilter {
  577. [self unaryFilterTestWithValue:Value(NAN)
  578. expectedUnaryOperator:GCFSStructuredQuery_UnaryFilter_Operator_IsNan];
  579. }
  580. - (void)unaryFilterTestWithValue:(FieldValue)value
  581. expectedUnaryOperator:(GCFSStructuredQuery_UnaryFilter_Operator)op {
  582. FSTQuery *q = [FSTTestQuery("docs") queryByAddingFilter:Filter("prop", "==", value)];
  583. FSTQueryData *model = [self queryDataForQuery:q];
  584. GCFSTarget *expected = [GCFSTarget message];
  585. expected.query.parent = @"projects/p/databases/d/documents";
  586. GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
  587. from.collectionId = @"docs";
  588. [expected.query.structuredQuery.fromArray addObject:from];
  589. [expected.query.structuredQuery.orderByArray
  590. addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
  591. GCFSStructuredQuery_UnaryFilter *filter = expected.query.structuredQuery.where.unaryFilter;
  592. filter.field.fieldPath = @"prop";
  593. filter.op = op;
  594. expected.targetId = 1;
  595. [self assertRoundTripForQueryData:model proto:expected];
  596. }
  597. - (void)testEncodesSortOrders {
  598. FSTQuery *q = [FSTTestQuery("docs")
  599. queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:testutil::Field("prop")
  600. ascending:YES]];
  601. FSTQueryData *model = [self queryDataForQuery:q];
  602. GCFSTarget *expected = [GCFSTarget message];
  603. expected.query.parent = @"projects/p/databases/d/documents";
  604. GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
  605. from.collectionId = @"docs";
  606. [expected.query.structuredQuery.fromArray addObject:from];
  607. [expected.query.structuredQuery.orderByArray
  608. addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]];
  609. [expected.query.structuredQuery.orderByArray
  610. addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
  611. expected.targetId = 1;
  612. [self assertRoundTripForQueryData:model proto:expected];
  613. }
  614. - (void)testEncodesSortOrdersDescending {
  615. FSTQuery *q = [FSTTestQuery("rooms/1/messages/10/attachments")
  616. queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:testutil::Field("prop")
  617. ascending:NO]];
  618. FSTQueryData *model = [self queryDataForQuery:q];
  619. GCFSTarget *expected = [GCFSTarget message];
  620. expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10";
  621. GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
  622. from.collectionId = @"attachments";
  623. [expected.query.structuredQuery.fromArray addObject:from];
  624. [expected.query.structuredQuery.orderByArray
  625. addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:NO]];
  626. [expected.query.structuredQuery.orderByArray
  627. addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:NO]];
  628. expected.targetId = 1;
  629. [self assertRoundTripForQueryData:model proto:expected];
  630. }
  631. - (void)testEncodesLimits {
  632. FSTQuery *q = [FSTTestQuery("docs") queryBySettingLimit:26];
  633. FSTQueryData *model = [self queryDataForQuery:q];
  634. GCFSTarget *expected = [GCFSTarget message];
  635. expected.query.parent = @"projects/p/databases/d/documents";
  636. GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
  637. from.collectionId = @"docs";
  638. [expected.query.structuredQuery.fromArray addObject:from];
  639. [expected.query.structuredQuery.orderByArray
  640. addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
  641. expected.query.structuredQuery.limit.value = 26;
  642. expected.targetId = 1;
  643. [self assertRoundTripForQueryData:model proto:expected];
  644. }
  645. - (void)testEncodesResumeTokens {
  646. FSTQuery *q = FSTTestQuery("docs");
  647. FSTQueryData *model = [[FSTQueryData alloc] initWithQuery:q
  648. targetID:1
  649. listenSequenceNumber:0
  650. purpose:FSTQueryPurposeListen
  651. snapshotVersion:SnapshotVersion::None()
  652. resumeToken:FSTTestData(1, 2, 3, -1)];
  653. GCFSTarget *expected = [GCFSTarget message];
  654. expected.query.parent = @"projects/p/databases/d/documents";
  655. GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
  656. from.collectionId = @"docs";
  657. [expected.query.structuredQuery.fromArray addObject:from];
  658. [expected.query.structuredQuery.orderByArray
  659. addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
  660. expected.targetId = 1;
  661. expected.resumeToken = FSTTestData(1, 2, 3, -1);
  662. [self assertRoundTripForQueryData:model proto:expected];
  663. }
  664. - (FSTQueryData *)queryDataForQuery:(FSTQuery *)query {
  665. return [[FSTQueryData alloc] initWithQuery:query
  666. targetID:1
  667. listenSequenceNumber:0
  668. purpose:FSTQueryPurposeListen
  669. snapshotVersion:SnapshotVersion::None()
  670. resumeToken:[NSData data]];
  671. }
  672. - (void)assertRoundTripForQueryData:(FSTQueryData *)queryData proto:(GCFSTarget *)proto {
  673. // Verify that the encoded FSTQueryData matches the target.
  674. GCFSTarget *actualProto = [self.serializer encodedTarget:queryData];
  675. XCTAssertEqualObjects(actualProto, proto);
  676. // We don't have deserialization logic for full targets since they're not used for RPC
  677. // interaction, but the query deserialization only *is* used for the local store.
  678. FSTQuery *actualModel;
  679. if (proto.targetTypeOneOfCase == GCFSTarget_TargetType_OneOfCase_Query) {
  680. actualModel = [self.serializer decodedQueryFromQueryTarget:proto.query];
  681. } else {
  682. actualModel = [self.serializer decodedQueryFromDocumentsTarget:proto.documents];
  683. }
  684. XCTAssertEqualObjects(actualModel, queryData.query);
  685. }
  686. - (void)testConvertsTargetChangeWithAdded {
  687. WatchTargetChange expected{WatchTargetChangeState::Added, {1, 4}};
  688. GCFSListenResponse *listenResponse = [GCFSListenResponse message];
  689. listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_Add;
  690. [listenResponse.targetChange.targetIdsArray addValue:1];
  691. [listenResponse.targetChange.targetIdsArray addValue:4];
  692. std::unique_ptr<WatchChange> actual = [self.serializer decodedWatchChange:listenResponse];
  693. XCTAssertTrue(IsWatchChangeEqual(*actual, expected));
  694. }
  695. - (void)testConvertsTargetChangeWithRemoved {
  696. WatchTargetChange expected{WatchTargetChangeState::Removed,
  697. {1, 4},
  698. FSTTestData(0, 1, 2, -1),
  699. Status{FirestoreErrorCode::PermissionDenied, "Error message"}};
  700. GCFSListenResponse *listenResponse = [GCFSListenResponse message];
  701. listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_Remove;
  702. listenResponse.targetChange.cause.code = FIRFirestoreErrorCodePermissionDenied;
  703. listenResponse.targetChange.cause.message = @"Error message";
  704. listenResponse.targetChange.resumeToken = FSTTestData(0, 1, 2, -1);
  705. [listenResponse.targetChange.targetIdsArray addValue:1];
  706. [listenResponse.targetChange.targetIdsArray addValue:4];
  707. std::unique_ptr<WatchChange> actual = [self.serializer decodedWatchChange:listenResponse];
  708. XCTAssertTrue(IsWatchChangeEqual(*actual, expected));
  709. }
  710. - (void)testConvertsTargetChangeWithNoChange {
  711. WatchTargetChange expected{WatchTargetChangeState::NoChange, {1, 4}};
  712. GCFSListenResponse *listenResponse = [GCFSListenResponse message];
  713. listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_NoChange;
  714. [listenResponse.targetChange.targetIdsArray addValue:1];
  715. [listenResponse.targetChange.targetIdsArray addValue:4];
  716. std::unique_ptr<WatchChange> actual = [self.serializer decodedWatchChange:listenResponse];
  717. XCTAssertTrue(IsWatchChangeEqual(*actual, expected));
  718. }
  719. - (void)testConvertsDocumentChangeWithTargetIds {
  720. DocumentWatchChange expected {
  721. {1, 2}, {}, FSTTestDocKey(@"coll/1"),
  722. FSTTestDoc("coll/1", 5, @{@"foo" : @"bar"}, DocumentState::kSynced)
  723. };
  724. GCFSListenResponse *listenResponse = [GCFSListenResponse message];
  725. listenResponse.documentChange.document.name = @"projects/p/databases/d/documents/coll/1";
  726. listenResponse.documentChange.document.updateTime.nanos = 5000;
  727. GCFSValue *fooValue = [GCFSValue message];
  728. fooValue.stringValue = @"bar";
  729. [listenResponse.documentChange.document.fields setObject:fooValue forKey:@"foo"];
  730. [listenResponse.documentChange.targetIdsArray addValue:1];
  731. [listenResponse.documentChange.targetIdsArray addValue:2];
  732. std::unique_ptr<WatchChange> actual = [self.serializer decodedWatchChange:listenResponse];
  733. XCTAssertTrue(IsWatchChangeEqual(*actual, expected));
  734. }
  735. - (void)testConvertsDocumentChangeWithRemovedTargetIds {
  736. DocumentWatchChange expected {
  737. {2}, {1}, FSTTestDocKey(@"coll/1"),
  738. FSTTestDoc("coll/1", 5, @{@"foo" : @"bar"}, DocumentState::kSynced)
  739. };
  740. GCFSListenResponse *listenResponse = [GCFSListenResponse message];
  741. listenResponse.documentChange.document.name = @"projects/p/databases/d/documents/coll/1";
  742. listenResponse.documentChange.document.updateTime.nanos = 5000;
  743. GCFSValue *fooValue = [GCFSValue message];
  744. fooValue.stringValue = @"bar";
  745. [listenResponse.documentChange.document.fields setObject:fooValue forKey:@"foo"];
  746. [listenResponse.documentChange.removedTargetIdsArray addValue:1];
  747. [listenResponse.documentChange.targetIdsArray addValue:2];
  748. std::unique_ptr<WatchChange> actual = [self.serializer decodedWatchChange:listenResponse];
  749. XCTAssertTrue(IsWatchChangeEqual(*actual, expected));
  750. }
  751. - (void)testConvertsDocumentChangeWithDeletions {
  752. DocumentWatchChange expected{
  753. {}, {1, 2}, FSTTestDocKey(@"coll/1"), FSTTestDeletedDoc("coll/1", 5, NO)};
  754. GCFSListenResponse *listenResponse = [GCFSListenResponse message];
  755. listenResponse.documentDelete.document = @"projects/p/databases/d/documents/coll/1";
  756. listenResponse.documentDelete.readTime.nanos = 5000;
  757. [listenResponse.documentDelete.removedTargetIdsArray addValue:1];
  758. [listenResponse.documentDelete.removedTargetIdsArray addValue:2];
  759. std::unique_ptr<WatchChange> actual = [self.serializer decodedWatchChange:listenResponse];
  760. XCTAssertTrue(IsWatchChangeEqual(*actual, expected));
  761. }
  762. - (void)testConvertsDocumentChangeWithRemoves {
  763. DocumentWatchChange expected{{}, {1, 2}, FSTTestDocKey(@"coll/1"), nil};
  764. GCFSListenResponse *listenResponse = [GCFSListenResponse message];
  765. listenResponse.documentRemove.document = @"projects/p/databases/d/documents/coll/1";
  766. [listenResponse.documentRemove.removedTargetIdsArray addValue:1];
  767. [listenResponse.documentRemove.removedTargetIdsArray addValue:2];
  768. std::unique_ptr<WatchChange> actual = [self.serializer decodedWatchChange:listenResponse];
  769. XCTAssertTrue(IsWatchChangeEqual(*actual, expected));
  770. }
  771. @end
  772. NS_ASSUME_NONNULL_END