Skip to content

Commit b9eacff

Browse files
committed
Collection Group Tests
1 parent 49001b8 commit b9eacff

File tree

1 file changed

+328
-0
lines changed

1 file changed

+328
-0
lines changed
Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.firestore.pipeline
16+
17+
import com.google.common.truth.Truth.assertThat
18+
import com.google.firebase.firestore.FieldPath as PublicFieldPath
19+
import com.google.firebase.firestore.RealtimePipelineSource
20+
import com.google.firebase.firestore.TestUtil
21+
import com.google.firebase.firestore.model.MutableDocument
22+
import com.google.firebase.firestore.pipeline.Expr.Companion.array
23+
import com.google.firebase.firestore.pipeline.Expr.Companion.arrayContains
24+
import com.google.firebase.firestore.pipeline.Expr.Companion.eqAny
25+
import com.google.firebase.firestore.pipeline.Expr.Companion.field
26+
import com.google.firebase.firestore.pipeline.Expr.Companion.gt
27+
import com.google.firebase.firestore.pipeline.Expr.Companion.neq
28+
import com.google.firebase.firestore.runPipeline
29+
import com.google.firebase.firestore.testutil.TestUtilKtx.doc
30+
import kotlinx.coroutines.flow.flowOf
31+
import kotlinx.coroutines.flow.toList
32+
import kotlinx.coroutines.runBlocking
33+
import org.junit.Test
34+
import org.junit.runner.RunWith
35+
import org.robolectric.RobolectricTestRunner
36+
37+
@RunWith(RobolectricTestRunner::class)
38+
internal class CollectionGroupTests {
39+
40+
private val db = TestUtil.firestore()
41+
42+
@Test
43+
fun `returns no result from empty db`(): Unit = runBlocking {
44+
val pipeline = RealtimePipelineSource(db).collectionGroup("users")
45+
val documents = emptyList<MutableDocument>()
46+
val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList()
47+
assertThat(result).isEmpty()
48+
}
49+
50+
@Test
51+
fun `returns single document`(): Unit = runBlocking {
52+
val pipeline = RealtimePipelineSource(db).collectionGroup("users")
53+
val doc1 = doc("users/bob", 1000, mapOf("score" to 90L, "rank" to 1L))
54+
val documents = listOf(doc1)
55+
val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList()
56+
assertThat(result).containsExactly(doc1)
57+
}
58+
59+
@Test
60+
fun `returns multiple documents`(): Unit = runBlocking {
61+
val pipeline = RealtimePipelineSource(db).collectionGroup("users")
62+
val doc1 = doc("users/bob", 1000, mapOf("score" to 90L, "rank" to 1L))
63+
val doc2 = doc("users/alice", 1000, mapOf("score" to 50L, "rank" to 3L))
64+
val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L, "rank" to 2L))
65+
val documents = listOf(doc1, doc2, doc3)
66+
val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList()
67+
assertThat(result).containsExactlyElementsIn(documents)
68+
}
69+
70+
@Test
71+
fun `skips other collection ids`(): Unit = runBlocking {
72+
val pipeline = RealtimePipelineSource(db).collectionGroup("users")
73+
val doc1 = doc("users/bob", 1000, mapOf("score" to 90L))
74+
val doc2 = doc("users-other/bob", 1000, mapOf("score" to 90L))
75+
val doc3 = doc("users/alice", 1000, mapOf("score" to 50L))
76+
val doc4 = doc("users-other/alice", 1000, mapOf("score" to 50L))
77+
val doc5 = doc("users/charlie", 1000, mapOf("score" to 97L))
78+
val doc6 = doc("users-other/charlie", 1000, mapOf("score" to 97L))
79+
val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6)
80+
val expectedDocs = listOf(doc1, doc3, doc5) // alice, bob, charlie (from 'users' only)
81+
val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList()
82+
assertThat(result).containsExactlyElementsIn(expectedDocs)
83+
}
84+
85+
@Test
86+
fun `different parents`(): Unit = runBlocking {
87+
val pipeline =
88+
RealtimePipelineSource(db).collectionGroup("games").sort(field("order").ascending())
89+
val doc1 = doc("users/bob/games/game1", 1000, mapOf("score" to 90L, "order" to 1L))
90+
val doc2 = doc("users/alice/games/game1", 1000, mapOf("score" to 90L, "order" to 2L))
91+
val doc3 = doc("users/bob/games/game2", 1000, mapOf("score" to 20L, "order" to 3L))
92+
val doc4 = doc("users/charlie/games/game1", 1000, mapOf("score" to 20L, "order" to 4L))
93+
val doc5 = doc("users/bob/games/game3", 1000, mapOf("score" to 30L, "order" to 5L))
94+
val doc6 = doc("users/alice/games/game2", 1000, mapOf("score" to 30L, "order" to 6L))
95+
val doc7 =
96+
doc("users/charlie/profiles/profile1", 1000, mapOf("order" to 7L)) // Different collection ID
97+
98+
val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6, doc7)
99+
val expectedDocs = listOf(doc1, doc2, doc3, doc4, doc5, doc6)
100+
val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList()
101+
assertThat(result).containsExactlyElementsIn(expectedDocs).inOrder()
102+
}
103+
104+
@Test
105+
fun `different parents stable ordering on path`(): Unit = runBlocking {
106+
val pipeline =
107+
RealtimePipelineSource(db)
108+
.collectionGroup("games")
109+
.sort(field(PublicFieldPath.documentId()).ascending())
110+
111+
val doc1 = doc("users/bob/games/1", 1000, mapOf("score" to 90L))
112+
val doc2 = doc("users/alice/games/2", 1000, mapOf("score" to 90L))
113+
val doc3 = doc("users/bob/games/3", 1000, mapOf("score" to 20L))
114+
val doc4 = doc("users/charlie/games/4", 1000, mapOf("score" to 20L))
115+
val doc5 = doc("users/bob/games/5", 1000, mapOf("score" to 30L))
116+
val doc6 = doc("users/alice/games/6", 1000, mapOf("score" to 30L))
117+
val doc7 =
118+
doc("users/charlie/profiles/7", 1000, mapOf<String, Any>()) // Different collection ID
119+
120+
val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6, doc7)
121+
// Expected order:
122+
// users/alice/games/2
123+
// users/alice/games/6
124+
// users/bob/games/1
125+
// users/bob/games/3
126+
// users/bob/games/5
127+
// users/charlie/games/4
128+
val expectedDocs = listOf(doc2, doc6, doc1, doc3, doc5, doc4)
129+
val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList()
130+
assertThat(result).containsExactlyElementsIn(expectedDocs)
131+
}
132+
133+
@Test
134+
fun `different parents stable ordering on key`(): Unit = runBlocking {
135+
// This test is identical to DifferentParentsStableOrderingOnPath
136+
val pipeline =
137+
RealtimePipelineSource(db)
138+
.collectionGroup("games")
139+
.sort(field(PublicFieldPath.documentId()).ascending())
140+
141+
val doc1 = doc("users/bob/games/1", 1000, mapOf("score" to 90L))
142+
val doc2 = doc("users/alice/games/2", 1000, mapOf("score" to 90L))
143+
val doc3 = doc("users/bob/games/3", 1000, mapOf("score" to 20L))
144+
val doc4 = doc("users/charlie/games/4", 1000, mapOf("score" to 20L))
145+
val doc5 = doc("users/bob/games/5", 1000, mapOf("score" to 30L))
146+
val doc6 = doc("users/alice/games/6", 1000, mapOf("score" to 30L))
147+
val doc7 =
148+
doc("users/charlie/profiles/7", 1000, mapOf<String, Any>()) // Different collection ID
149+
150+
val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6, doc7)
151+
val expectedDocs = listOf(doc2, doc6, doc1, doc3, doc5, doc4)
152+
val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList()
153+
assertThat(result).containsExactlyElementsIn(expectedDocs).inOrder()
154+
}
155+
156+
@Test
157+
fun `where on values`(): Unit = runBlocking {
158+
val pipeline =
159+
RealtimePipelineSource(db)
160+
.collectionGroup("users")
161+
.where(eqAny(field("score"), array(90L, 97L)))
162+
163+
val doc1 = doc("users/bob", 1000, mapOf("score" to 90L))
164+
val doc2 = doc("users/alice", 1000, mapOf("score" to 50L))
165+
val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L))
166+
val doc4 = doc("users/diane", 1000, mapOf("score" to 97L))
167+
val doc5 =
168+
doc(
169+
"profiles/admin/users/bob",
170+
1000,
171+
mapOf("score" to 90L)
172+
) // Different path, same collection ID
173+
174+
val documents = listOf(doc1, doc2, doc3, doc4, doc5)
175+
// Expected: bob(users), charlie(users), diane(users), bob(profiles/admin/users)
176+
val expectedDocs = listOf(doc1, doc3, doc4, doc5)
177+
val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList()
178+
assertThat(result).containsExactlyElementsIn(expectedDocs)
179+
}
180+
181+
@Test
182+
fun `where inequality on values`(): Unit = runBlocking {
183+
val pipeline =
184+
RealtimePipelineSource(db).collectionGroup("users").where(gt(field("score"), 80L))
185+
186+
val doc1 = doc("users/bob", 1000, mapOf("score" to 90L))
187+
val doc2 = doc("users/alice", 1000, mapOf("score" to 50L))
188+
val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L))
189+
val doc4 = doc("profiles/admin/users/bob", 1000, mapOf("score" to 90L)) // Different path
190+
191+
val documents = listOf(doc1, doc2, doc3, doc4)
192+
// Expected: bob(users), charlie(users), bob(profiles)
193+
val expectedDocs = listOf(doc1, doc3, doc4)
194+
val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList()
195+
assertThat(result).containsExactlyElementsIn(expectedDocs)
196+
}
197+
198+
@Test
199+
fun `where not equal on values`(): Unit = runBlocking {
200+
val pipeline =
201+
RealtimePipelineSource(db).collectionGroup("users").where(neq(field("score"), 50L))
202+
203+
val doc1 = doc("users/bob", 1000, mapOf("score" to 90L))
204+
val doc2 = doc("users/alice", 1000, mapOf("score" to 50L)) // This will be filtered out
205+
val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L))
206+
val doc4 = doc("profiles/admin/users/bob", 1000, mapOf("score" to 90L)) // Different path
207+
208+
val documents = listOf(doc1, doc2, doc3, doc4)
209+
// Expected: bob(users), charlie(users), bob(profiles)
210+
val expectedDocs = listOf(doc1, doc3, doc4)
211+
val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList()
212+
assertThat(result).containsExactlyElementsIn(expectedDocs)
213+
}
214+
215+
@Test
216+
fun `where array contains values`(): Unit = runBlocking {
217+
val pipeline =
218+
RealtimePipelineSource(db)
219+
.collectionGroup("users")
220+
.where(arrayContains(field("rounds"), "round3"))
221+
222+
val doc1 = doc("users/bob", 1000, mapOf("score" to 90L, "rounds" to listOf("round1", "round3")))
223+
val doc2 =
224+
doc("users/alice", 1000, mapOf("score" to 50L, "rounds" to listOf("round2", "round4")))
225+
val doc3 =
226+
doc(
227+
"users/charlie",
228+
1000,
229+
mapOf("score" to 97L, "rounds" to listOf("round2", "round3", "round4"))
230+
)
231+
val doc4 =
232+
doc(
233+
"profiles/admin/users/bob",
234+
1000,
235+
mapOf("score" to 90L, "rounds" to listOf("round1", "round3"))
236+
) // Different path
237+
238+
val documents = listOf(doc1, doc2, doc3, doc4)
239+
// Expected: bob(users), charlie(users), bob(profiles)
240+
val expectedDocs = listOf(doc1, doc3, doc4)
241+
val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList()
242+
assertThat(result).containsExactlyElementsIn(expectedDocs)
243+
}
244+
245+
@Test
246+
fun `sort on values`(): Unit = runBlocking {
247+
val pipeline =
248+
RealtimePipelineSource(db).collectionGroup("users").sort(field("score").descending())
249+
250+
val doc1 = doc("users/bob", 1000, mapOf("score" to 90L))
251+
val doc2 = doc("users/alice", 1000, mapOf("score" to 50L))
252+
val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L))
253+
val doc4 = doc("profiles/admin/users/bob", 1000, mapOf("score" to 90L)) // Different path
254+
255+
val documents = listOf(doc1, doc2, doc3, doc4)
256+
// Expected: charlie(97), bob(profiles, 90), bob(users, 90), alice(50)
257+
val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList()
258+
// Tie exists for doc1 and doc4, so check both orders are valid.
259+
assertThat(result).containsAtLeast(doc3, doc1, doc2).inOrder()
260+
assertThat(result).containsAtLeast(doc3, doc4, doc2).inOrder()
261+
}
262+
263+
@Test
264+
fun `sort on values has dense semantics`(): Unit = runBlocking {
265+
val pipeline =
266+
RealtimePipelineSource(db).collectionGroup("users").sort(field("score").descending())
267+
268+
val doc1 = doc("users/bob", 1000, mapOf("score" to 90L))
269+
val doc2 = doc("users/alice", 1000, mapOf("score" to 50L))
270+
val doc3 = doc("users/charlie", 1000, mapOf("number" to 97L)) // Missing 'score'
271+
val doc4 = doc("profiles/admin/users/bob", 1000, mapOf("score" to 90L)) // Different path
272+
273+
val documents = listOf(doc1, doc2, doc3, doc4)
274+
// Missing fields sort last in descending order (or first in ascending).
275+
// So, charlie (doc3) with missing 'score' comes after alice (doc2) with score 50.
276+
// Order for scores: 90, 90, 50, missing.
277+
val expectedDocs = listOf(doc4, doc1, doc2, doc3)
278+
val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList()
279+
// Tie exists for doc1 and doc4, so check both orders are valid.
280+
assertThat(result).containsAtLeast(doc1, doc2, doc3).inOrder()
281+
assertThat(result).containsAtLeast(doc4, doc2, doc3).inOrder()
282+
}
283+
284+
@Test
285+
fun `sort on path`(): Unit = runBlocking {
286+
val pipeline =
287+
RealtimePipelineSource(db)
288+
.collectionGroup("users")
289+
.sort(field(PublicFieldPath.documentId()).ascending())
290+
291+
val doc1 = doc("users/bob", 1000, mapOf("score" to 90L))
292+
val doc2 = doc("users/alice", 1000, mapOf("score" to 50L))
293+
val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L))
294+
val doc4 = doc("profiles/admin/users/bob", 1000, mapOf("score" to 90L)) // Different path
295+
296+
val documents = listOf(doc1, doc2, doc3, doc4)
297+
// Expected: sorted by path:
298+
// profiles/admin/users/bob (doc4)
299+
// users/alice (doc2)
300+
// users/bob (doc1)
301+
// users/charlie (doc3)
302+
val expectedDocs = listOf(doc4, doc2, doc1, doc3)
303+
val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList()
304+
assertThat(result).containsExactlyElementsIn(expectedDocs).inOrder()
305+
}
306+
307+
@Test
308+
fun `limit`(): Unit = runBlocking {
309+
val pipeline =
310+
RealtimePipelineSource(db)
311+
.collectionGroup("users")
312+
.sort(field(PublicFieldPath.documentId()).ascending())
313+
.limit(2)
314+
315+
val doc1 = doc("users/bob", 1000, mapOf("score" to 90L))
316+
val doc2 = doc("users/alice", 1000, mapOf("score" to 50L))
317+
val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L))
318+
val doc4 = doc("profiles/admin/users/bob", 1000, mapOf("score" to 90L)) // Different path
319+
320+
val documents = listOf(doc1, doc2, doc3, doc4)
321+
// Expected: sorted by path, then limited:
322+
// profiles/admin/users/bob (doc4)
323+
// users/alice (doc2)
324+
val expectedDocs = listOf(doc4, doc2)
325+
val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList()
326+
assertThat(result).containsExactlyElementsIn(expectedDocs).inOrder()
327+
}
328+
}

0 commit comments

Comments
 (0)