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