Skip to content

Commit bbad430

Browse files
authored
Integration tests, changelog, and minor fixes for array transforms. (firebase#1108)
1 parent cd4cdc0 commit bbad430

File tree

9 files changed

+381
-10
lines changed

9 files changed

+381
-10
lines changed

Firestore/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
# Unreleased
2+
- [feature] Added FieldValue.arrayUnion() and FieldValue.arrayRemove() methods
3+
which can be used inside setData() or updateData() calls to atomically add
4+
or remove specific elements to an array field in a document without using a
5+
transaction.
26
- [changed] Replaced the `DocumentListenOptions` object with a simple boolean.
37
Instead of calling
48
`addSnapshotListener(options: DocumentListenOptions.includeMetadataChanges(true))`

Firestore/Example/Firestore.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@
134134
6ED54761B845349D43DB6B78 /* Pods_Firestore_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 75A6FE51C1A02DF38F62FAAD /* Pods_Firestore_Example.framework */; };
135135
71719F9F1E33DC2100824A3D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */; };
136136
7346E61D20325C6900FD6CEF /* FSTDispatchQueueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7346E61C20325C6900FD6CEF /* FSTDispatchQueueTests.mm */; };
137+
73866AA12082B0A5009BB4FF /* FIRArrayTransformTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 73866A9F2082B069009BB4FF /* FIRArrayTransformTests.mm */; };
137138
873B8AEB1B1F5CCA007FD442 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */; };
138139
AB356EF7200EA5EB0089B766 /* field_value_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB356EF6200EA5EB0089B766 /* field_value_test.cc */; };
139140
AB380CFB2019388600D97691 /* target_id_generator_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB380CF82019382300D97691 /* target_id_generator_test.cc */; };
@@ -358,6 +359,7 @@
358359
69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
359360
71719F9E1E33DC2100824A3D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
360361
7346E61C20325C6900FD6CEF /* FSTDispatchQueueTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTDispatchQueueTests.mm; sourceTree = "<group>"; };
362+
73866A9F2082B069009BB4FF /* FIRArrayTransformTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRArrayTransformTests.mm; sourceTree = "<group>"; };
361363
75A6FE51C1A02DF38F62FAAD /* Pods_Firestore_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
362364
873B8AEA1B1F5CCA007FD442 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
363365
8E002F4AD5D9B6197C940847 /* Firestore.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = Firestore.podspec; path = ../Firestore.podspec; sourceTree = "<group>"; };
@@ -882,6 +884,7 @@
882884
DE51B1BC1F0D48AC0013853F /* API */ = {
883885
isa = PBXGroup;
884886
children = (
887+
73866A9F2082B069009BB4FF /* FIRArrayTransformTests.mm */,
885888
5492E070202154D600B64F25 /* FIRCursorTests.mm */,
886889
5492E06C202154D500B64F25 /* FIRDatabaseTests.mm */,
887890
5492E06A202154D500B64F25 /* FIRFieldsTests.mm */,
@@ -1555,6 +1558,7 @@
15551558
isa = PBXSourcesBuildPhase;
15561559
buildActionMask = 2147483647;
15571560
files = (
1561+
73866AA12082B0A5009BB4FF /* FIRArrayTransformTests.mm in Sources */,
15581562
5492E076202154D600B64F25 /* FIRValidationTests.mm in Sources */,
15591563
5492E072202154D600B64F25 /* FIRQueryTests.mm in Sources */,
15601564
5491BC731FB44593008B3588 /* FSTIntegrationTestCase.mm in Sources */,
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
/*
2+
* Copyright 2018 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+
17+
#import <FirebaseFirestore/FirebaseFirestore.h>
18+
19+
#import <XCTest/XCTest.h>
20+
21+
#import "Firestore/Example/Tests/Util/FSTEventAccumulator.h"
22+
#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h"
23+
24+
/**
25+
* Note: Transforms are tested pretty thoroughly in FIRServerTimestampTests (via set, update,
26+
* transactions, nested in documents, multiple transforms together, etc.) and so these tests
27+
* mostly focus on the array transform semantics.
28+
*/
29+
@interface FIRArrayTransformTests : FSTIntegrationTestCase
30+
@end
31+
32+
@implementation FIRArrayTransformTests {
33+
// A document reference to read and write to.
34+
FIRDocumentReference *_docRef;
35+
36+
// Accumulator used to capture events during the test.
37+
FSTEventAccumulator *_accumulator;
38+
39+
// Listener registration for a listener maintained during the course of the test.
40+
id<FIRListenerRegistration> _listenerRegistration;
41+
}
42+
43+
- (void)setUp {
44+
[super setUp];
45+
46+
_docRef = [self documentRef];
47+
_accumulator = [FSTEventAccumulator accumulatorForTest:self];
48+
_listenerRegistration =
49+
[_docRef addSnapshotListenerWithIncludeMetadataChanges:YES
50+
listener:_accumulator.valueEventHandler];
51+
52+
// Wait for initial nil snapshot to avoid potential races.
53+
FIRDocumentSnapshot *initialSnapshot = [_accumulator awaitEventWithName:@"initial event"];
54+
XCTAssertFalse(initialSnapshot.exists);
55+
}
56+
57+
- (void)tearDown {
58+
[_listenerRegistration remove];
59+
60+
[super tearDown];
61+
}
62+
63+
#pragma mark - Test Helpers
64+
65+
/** Waits for a snapshot with local writes. */
66+
- (FIRDocumentSnapshot *)waitForLocalEvent {
67+
FIRDocumentSnapshot *snapshot;
68+
do {
69+
snapshot = [_accumulator awaitEventWithName:@"Local event."];
70+
} while (!snapshot.metadata.hasPendingWrites);
71+
return snapshot;
72+
}
73+
74+
/** Waits for a snapshot that has no pending writes */
75+
- (FIRDocumentSnapshot *)waitForRemoteEvent {
76+
FIRDocumentSnapshot *snapshot;
77+
do {
78+
snapshot = [_accumulator awaitEventWithName:@"Remote event."];
79+
} while (snapshot.metadata.hasPendingWrites);
80+
return snapshot;
81+
}
82+
83+
/** Writes some initial data and consumes the events generated. */
84+
- (void)writeInitialData:(NSDictionary<NSString *, id> *)data {
85+
[self writeDocumentRef:_docRef data:data];
86+
XCTAssertEqualObjects([self waitForLocalEvent].data, data);
87+
XCTAssertEqualObjects([self waitForRemoteEvent].data, data);
88+
}
89+
90+
#pragma mark - Test Cases
91+
92+
- (void)testCreateDocumentWithArrayUnion {
93+
[self writeDocumentRef:_docRef
94+
data:@{
95+
@"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @1, @2 ]]
96+
}];
97+
id expected = @{ @"array" : @[ @1, @2 ] };
98+
XCTAssertEqualObjects([self waitForLocalEvent].data, expected);
99+
XCTAssertEqualObjects([self waitForRemoteEvent].data, expected);
100+
}
101+
102+
- (void)testAppendToArrayViaUpdate {
103+
[self writeInitialData:@{ @"array" : @[ @1, @3 ] }];
104+
105+
[self updateDocumentRef:_docRef
106+
data:@{
107+
@"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @2, @1, @4 ]]
108+
}];
109+
110+
id expected = @{ @"array" : @[ @1, @3, @2, @4 ] };
111+
XCTAssertEqualObjects([self waitForLocalEvent].data, expected);
112+
XCTAssertEqualObjects([self waitForRemoteEvent].data, expected);
113+
}
114+
115+
- (void)testAppendToArrayViaMergeSet {
116+
[self writeInitialData:@{ @"array" : @[ @1, @3 ] }];
117+
118+
[self mergeDocumentRef:_docRef
119+
data:@{
120+
@"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @2, @1, @4 ]]
121+
}];
122+
123+
id expected = @{ @"array" : @[ @1, @3, @2, @4 ] };
124+
XCTAssertEqualObjects([self waitForLocalEvent].data, expected);
125+
XCTAssertEqualObjects([self waitForRemoteEvent].data, expected);
126+
}
127+
128+
- (void)testAppendObjectToArrayViaUpdate {
129+
[self writeInitialData:@{ @"array" : @[ @{@"a" : @"hi"} ] }];
130+
131+
[self updateDocumentRef:_docRef
132+
data:@{
133+
@"array" : [FIRFieldValue
134+
fieldValueForArrayUnion:@[ @{@"a" : @"hi"}, @{@"a" : @"bye"} ]]
135+
}];
136+
137+
id expected = @{ @"array" : @[ @{@"a" : @"hi"}, @{@"a" : @"bye"} ] };
138+
XCTAssertEqualObjects([self waitForLocalEvent].data, expected);
139+
XCTAssertEqualObjects([self waitForRemoteEvent].data, expected);
140+
}
141+
142+
- (void)testRemoveFromArrayViaUpdate {
143+
[self writeInitialData:@{ @"array" : @[ @1, @3, @1, @3 ] }];
144+
145+
[self updateDocumentRef:_docRef
146+
data:@{
147+
@"array" : [FIRFieldValue fieldValueForArrayRemove:@[ @1, @4 ]]
148+
}];
149+
150+
id expected = @{ @"array" : @[ @3, @3 ] };
151+
XCTAssertEqualObjects([self waitForLocalEvent].data, expected);
152+
XCTAssertEqualObjects([self waitForRemoteEvent].data, expected);
153+
}
154+
155+
- (void)testRemoveFromArrayViaMergeSet {
156+
[self writeInitialData:@{ @"array" : @[ @1, @3, @1, @3 ] }];
157+
158+
[self mergeDocumentRef:_docRef
159+
data:@{
160+
@"array" : [FIRFieldValue fieldValueForArrayRemove:@[ @1, @4 ]]
161+
}];
162+
163+
id expected = @{ @"array" : @[ @3, @3 ] };
164+
XCTAssertEqualObjects([self waitForLocalEvent].data, expected);
165+
XCTAssertEqualObjects([self waitForRemoteEvent].data, expected);
166+
}
167+
168+
- (void)testRemoveObjectFromArrayViaUpdate {
169+
[self writeInitialData:@{ @"array" : @[ @{@"a" : @"hi"}, @{@"a" : @"bye"} ] }];
170+
171+
[self updateDocumentRef:_docRef
172+
data:@{
173+
@"array" : [FIRFieldValue fieldValueForArrayRemove:@[ @{@"a" : @"hi"} ]]
174+
}];
175+
176+
id expected = @{ @"array" : @[ @{@"a" : @"bye"} ] };
177+
XCTAssertEqualObjects([self waitForLocalEvent].data, expected);
178+
XCTAssertEqualObjects([self waitForRemoteEvent].data, expected);
179+
}
180+
181+
@end
182+
183+
/**
184+
* Unlike the FIRArrayTransformTests above, these tests intentionally avoid having any ongoing
185+
* listeners so that we can test what gets stored in the offline cache based purely on the write
186+
* acknowledgement (without receiving an updated document via watch). As such they also rely on
187+
* persistence being enabled so documents remain in the cache after the write.
188+
*/
189+
@interface FIRArrayTransformServerApplicationTests : FSTIntegrationTestCase
190+
@end
191+
192+
@implementation FIRArrayTransformServerApplicationTests {
193+
// A document reference to read and write to.
194+
FIRDocumentReference *_docRef;
195+
}
196+
197+
- (void)setUp {
198+
[super setUp];
199+
200+
_docRef = [self documentRef];
201+
}
202+
203+
/**
204+
* Helper that uses a temporary listener to read from cache (returning nil if no document seems
205+
* to be in cache). Can probably be replaced with get(source=cache) in the future.
206+
*/
207+
- (FIRDocumentSnapshot *_Nullable)getFromCache {
208+
FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self];
209+
id<FIRListenerRegistration> listenerRegistration =
210+
[_docRef addSnapshotListener:accumulator.valueEventHandler];
211+
FIRDocumentSnapshot *snapshot = [accumulator awaitEventWithName:@"listenForOneEvent"];
212+
[listenerRegistration remove];
213+
if (snapshot.metadata.fromCache) {
214+
return snapshot;
215+
} else {
216+
return nil;
217+
}
218+
}
219+
220+
- (void)testServerApplicationOfSetWithNoCachedBaseDoc {
221+
[self writeDocumentRef:_docRef
222+
data:@{
223+
@"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @1, @2 ]]
224+
}];
225+
id expected = @{ @"array" : @[ @1, @2 ] };
226+
XCTAssertEqualObjects([self getFromCache].data, expected);
227+
}
228+
229+
- (void)testServerApplicationOfUpdateWithNoCachedBaseDoc {
230+
// Write an initial document out-of-band so it's not in our cache
231+
[self writeDocumentRef:[[self firestore] documentWithPath:_docRef.path]
232+
data:@{
233+
@"array" : @[ @42 ]
234+
}];
235+
236+
[self updateDocumentRef:_docRef
237+
data:@{
238+
@"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @1, @2 ]]
239+
}];
240+
241+
// Nothing should be cached since it was an update and we had no base doc.
242+
XCTAssertNil([self getFromCache]);
243+
}
244+
245+
- (void)testServerApplicationOfMergeSetWithNoCachedBaseDoc {
246+
// Write an initial document out-of-band so it's not in our cache
247+
[self writeDocumentRef:[[self firestore] documentWithPath:_docRef.path]
248+
data:@{
249+
@"array" : @[ @42 ]
250+
}];
251+
252+
[self mergeDocumentRef:_docRef
253+
data:@{
254+
@"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @1, @2 ]]
255+
}];
256+
257+
// Document will be cached but we'll be missing 42.
258+
id expected = @{ @"array" : @[ @1, @2 ] };
259+
XCTAssertEqualObjects([self getFromCache].data, expected);
260+
}
261+
262+
- (void)testServerApplicationOfArrayUnionUpdateWithCachedBaseDoc {
263+
// Cache a document with an array.
264+
[self writeDocumentRef:_docRef data:@{ @"array" : @[ @42 ] }];
265+
266+
[self updateDocumentRef:_docRef
267+
data:@{
268+
@"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @1, @2 ]]
269+
}];
270+
271+
// Should have merged the update with the cached doc.
272+
id expected = @{ @"array" : @[ @42, @1, @2 ] };
273+
XCTAssertEqualObjects([self getFromCache].data, expected);
274+
}
275+
276+
- (void)testServerApplicationOfArrayRemoveUpdateWithCachedBaseDoc {
277+
// Cache a document with an array.
278+
[self writeDocumentRef:_docRef data:@{ @"array" : @[ @42, @1, @2 ] }];
279+
280+
[self updateDocumentRef:_docRef
281+
data:@{
282+
@"array" : [FIRFieldValue fieldValueForArrayRemove:@[ @1, @2 ]]
283+
}];
284+
285+
// Should have merged the update with the cached doc.
286+
id expected = @{ @"array" : @[ @42 ] };
287+
XCTAssertEqualObjects([self getFromCache].data, expected);
288+
}
289+
@end

Firestore/Example/Tests/Integration/API/FIRValidationTests.mm

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,51 @@ - (void)testFieldPathsWithInvalidSegmentsFail {
363363
}
364364
}
365365

366+
#pragma mark - ArrayUnion / ArrayRemove Validation
367+
368+
- (void)testArrayTransformsInQueriesFail {
369+
FSTAssertThrows(
370+
[[self collectionRef] queryWhereField:@"test"
371+
isEqualTo:@{
372+
@"test" : [FIRFieldValue fieldValueForArrayUnion:@[ @1 ]]
373+
}],
374+
@"FieldValue.arrayUnion() can only be used with updateData() and setData() (found in field "
375+
"test)");
376+
377+
FSTAssertThrows(
378+
[[self collectionRef] queryWhereField:@"test"
379+
isEqualTo:@{
380+
@"test" : [FIRFieldValue fieldValueForArrayRemove:@[ @1 ]]
381+
}],
382+
@"FieldValue.arrayRemove() can only be used with updateData() and setData() (found in field "
383+
@"test)");
384+
}
385+
386+
- (void)testInvalidArrayTransformElementFails {
387+
[self expectWrite:@{
388+
@"foo" : [FIRFieldValue fieldValueForArrayUnion:@[ @1, self ]]
389+
}
390+
toFailWithReason:@"Unsupported type: FIRValidationTests"];
391+
392+
[self expectWrite:@{
393+
@"foo" : [FIRFieldValue fieldValueForArrayRemove:@[ @1, self ]]
394+
}
395+
toFailWithReason:@"Unsupported type: FIRValidationTests"];
396+
}
397+
398+
- (void)testArraysInArrayTransformsFail {
399+
// This would result in a directly nested array which is not supported.
400+
[self expectWrite:@{
401+
@"foo" : [FIRFieldValue fieldValueForArrayUnion:@[ @1, @[ @"nested" ] ]]
402+
}
403+
toFailWithReason:@"Nested arrays are not supported"];
404+
405+
[self expectWrite:@{
406+
@"foo" : [FIRFieldValue fieldValueForArrayRemove:@[ @1, @[ @"nested" ] ]]
407+
}
408+
toFailWithReason:@"Nested arrays are not supported"];
409+
}
410+
366411
#pragma mark - Query Validation
367412

368413
- (void)testQueryWithNonPositiveLimitFails {

0 commit comments

Comments
 (0)