1
+ /*
2
+ * Copyright 2024 Google LLC
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 XCTest
18
+ @testable import FirebaseFirestore
19
+
20
+ // MARK: - Mock Objects for Testing
21
+
22
+ private class MockListenerRegistration : ListenerRegistration {
23
+ var isRemoved = false
24
+ func remove( ) {
25
+ isRemoved = true
26
+ }
27
+ }
28
+
29
+ private typealias SnapshotListener = ( QuerySnapshot ? , Error ? ) -> Void
30
+ private typealias DocumentSnapshotListener = ( DocumentSnapshot ? , Error ? ) -> Void
31
+
32
+ private class MockQuery : Query {
33
+ var capturedListener : SnapshotListener ?
34
+ let mockListenerRegistration = MockListenerRegistration ( )
35
+
36
+ override func addSnapshotListener(
37
+ _ listener: @escaping SnapshotListener
38
+ ) -> ListenerRegistration {
39
+ capturedListener = listener
40
+ return mockListenerRegistration
41
+ }
42
+
43
+ override func addSnapshotListener(
44
+ includeMetadataChanges: Bool ,
45
+ listener: @escaping SnapshotListener
46
+ ) -> ListenerRegistration {
47
+ capturedListener = listener
48
+ return mockListenerRegistration
49
+ }
50
+ }
51
+
52
+ private class MockDocumentReference : DocumentReference {
53
+ var capturedListener : DocumentSnapshotListener ?
54
+ let mockListenerRegistration = MockListenerRegistration ( )
55
+
56
+ override func addSnapshotListener(
57
+ _ listener: @escaping DocumentSnapshotListener
58
+ ) -> ListenerRegistration {
59
+ capturedListener = listener
60
+ return mockListenerRegistration
61
+ }
62
+
63
+ override func addSnapshotListener(
64
+ includeMetadataChanges: Bool ,
65
+ listener: @escaping DocumentSnapshotListener
66
+ ) -> ListenerRegistration {
67
+ capturedListener = listener
68
+ return mockListenerRegistration
69
+ }
70
+ }
71
+
72
+ // MARK: - AsyncSequenceTests
73
+
74
+ @available ( iOS 13 , tvOS 13 , macOS 10 . 15 , macCatalyst 13 , watchOS 7 , * )
75
+ class AsyncSequenceTests : XCTestCase {
76
+ func testQuerySnapshotsYieldsValues( ) async throws {
77
+ let mockQuery = MockQuery ( )
78
+ let expectation = XCTestExpectation ( description: " Received snapshot " )
79
+
80
+ let task = Task {
81
+ for try await _ in mockQuery. snapshots {
82
+ expectation. fulfill ( )
83
+ break // Exit after first result
84
+ }
85
+ }
86
+
87
+ // Ensure the listener has been set up
88
+ XCTAssertNotNil ( mockQuery. capturedListener)
89
+
90
+ // Simulate a snapshot event
91
+ mockQuery. capturedListener ? ( QuerySnapshot ( ) , nil )
92
+
93
+ await fulfillment ( of: [ expectation] , timeout: 1.0 )
94
+ task. cancel ( )
95
+ }
96
+
97
+ func testQuerySnapshotsThrowsErrors( ) async throws {
98
+ let mockQuery = MockQuery ( )
99
+ let expectedError = NSError ( domain: " TestError " , code: 123 , userInfo: nil )
100
+ var receivedError : Error ?
101
+
102
+ let task = Task {
103
+ do {
104
+ for try await _ in mockQuery. snapshots {
105
+ XCTFail ( " Should not have received a value. " )
106
+ }
107
+ } catch {
108
+ receivedError = error
109
+ }
110
+ }
111
+
112
+ // Ensure the listener has been set up
113
+ XCTAssertNotNil ( mockQuery. capturedListener)
114
+
115
+ // Simulate an error event
116
+ mockQuery. capturedListener ? ( nil , expectedError)
117
+
118
+ // Allow the task to process the error
119
+ try await Task . sleep ( nanoseconds: 100_000_000 )
120
+
121
+ XCTAssertNotNil ( receivedError)
122
+ XCTAssertEqual ( ( receivedError as NSError ? ) ? . domain, expectedError. domain)
123
+ XCTAssertEqual ( ( receivedError as NSError ? ) ? . code, expectedError. code)
124
+ task. cancel ( )
125
+ }
126
+
127
+ func testQuerySnapshotsCancellationRemovesListener( ) async throws {
128
+ let mockQuery = MockQuery ( )
129
+
130
+ let task = Task {
131
+ for try await _ in mockQuery. snapshots {
132
+ XCTFail ( " Should not receive any values as the task is cancelled immediately. " )
133
+ }
134
+ }
135
+
136
+ // Ensure the listener was attached before we cancel
137
+ XCTAssertNotNil ( mockQuery. capturedListener)
138
+ XCTAssertFalse ( mockQuery. mockListenerRegistration. isRemoved)
139
+
140
+ task. cancel ( )
141
+
142
+ // Allow time for the cancellation handler to execute
143
+ try await Task . sleep ( nanoseconds: 100_000_000 )
144
+
145
+ XCTAssertTrue ( mockQuery. mockListenerRegistration. isRemoved)
146
+ }
147
+
148
+ func testDocumentReferenceSnapshotsYieldsValues( ) async throws {
149
+ let mockDocRef = MockDocumentReference ( )
150
+ let expectation = XCTestExpectation ( description: " Received document snapshot " )
151
+
152
+ let task = Task {
153
+ for try await _ in mockDocRef. snapshots {
154
+ expectation. fulfill ( )
155
+ break
156
+ }
157
+ }
158
+
159
+ XCTAssertNotNil ( mockDocRef. capturedListener)
160
+ mockDocRef. capturedListener ? ( DocumentSnapshot ( ) , nil )
161
+
162
+ await fulfillment ( of: [ expectation] , timeout: 1.0 )
163
+ task. cancel ( )
164
+ }
165
+
166
+ func testDocumentReferenceSnapshotsCancellationRemovesListener( ) async throws {
167
+ let mockDocRef = MockDocumentReference ( )
168
+
169
+ let task = Task {
170
+ for try await _ in mockDocRef. snapshots {
171
+ XCTFail ( " Should not receive values. " )
172
+ }
173
+ }
174
+
175
+ XCTAssertNotNil ( mockDocRef. capturedListener)
176
+ XCTAssertFalse ( mockDocRef. mockListenerRegistration. isRemoved)
177
+
178
+ task. cancel ( )
179
+ try await Task . sleep ( nanoseconds: 100_000_000 )
180
+
181
+ XCTAssertTrue ( mockDocRef. mockListenerRegistration. isRemoved)
182
+ }
183
+ }
0 commit comments