AuthStateChangesAsyncTests.swift 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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. @testable import FirebaseAuth
  15. import FirebaseCore
  16. import FirebaseCoreInternal
  17. import XCTest
  18. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
  19. class AuthStateChangesAsyncTests: RPCBaseTests {
  20. var auth: Auth!
  21. static let testNum = UnfairLock<Int>(0)
  22. override func setUp() {
  23. super.setUp()
  24. let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000",
  25. gcmSenderID: "00000000000000000-00000000000-000000000")
  26. options.apiKey = "FAKE_API_KEY"
  27. options.projectID = "myProjectID"
  28. let name = "test-\(Self.self)\(Self.testNum.value())"
  29. Self.testNum.withLock { $0 += 1 }
  30. FirebaseApp.configure(name: name, options: options)
  31. let app = FirebaseApp.app(name: name)!
  32. #if (os(macOS) && !FIREBASE_AUTH_TESTING_USE_MACOS_KEYCHAIN) || SWIFT_PACKAGE
  33. let keychainStorageProvider = FakeAuthKeychainStorage()
  34. #else
  35. let keychainStorageProvider = AuthKeychainStorageReal.shared
  36. #endif
  37. auth = Auth(
  38. app: app,
  39. keychainStorageProvider: keychainStorageProvider,
  40. backend: authBackend
  41. )
  42. waitForAuthGlobalWorkQueueDrain()
  43. }
  44. override func tearDown() {
  45. auth = nil
  46. FirebaseApp.resetApps()
  47. super.tearDown()
  48. }
  49. private func waitForAuthGlobalWorkQueueDrain() {
  50. kAuthGlobalWorkQueue.sync {}
  51. }
  52. func testAuthStateChangesStreamYieldsUserOnSignIn() async throws {
  53. // Given
  54. let initialNilExpectation = expectation(description: "Stream should emit initial nil user")
  55. let signInExpectation = expectation(description: "Stream should emit signed-in user")
  56. try? auth.signOut()
  57. var iteration = 0
  58. let task = Task {
  59. for await user in auth.authStateChanges {
  60. if iteration == 0 {
  61. XCTAssertNil(user, "The initial user should be nil")
  62. initialNilExpectation.fulfill()
  63. } else if iteration == 1 {
  64. XCTAssertNotNil(user, "The stream should yield the new user")
  65. XCTAssertEqual(user?.uid, kLocalID)
  66. signInExpectation.fulfill()
  67. }
  68. iteration += 1
  69. }
  70. }
  71. // Wait for the initial nil value to be emitted before proceeding.
  72. await fulfillment(of: [initialNilExpectation], timeout: 1.0)
  73. // When
  74. // A user is signed in.
  75. setFakeGetAccountProviderAnonymous()
  76. setFakeSecureTokenService()
  77. rpcIssuer.respondBlock = {
  78. try self.rpcIssuer.respond(withJSON: ["idToken": "TEST_ACCESS_TOKEN",
  79. "refreshToken": self.kRefreshToken,
  80. "isNewUser": true])
  81. }
  82. _ = try await auth.signInAnonymously()
  83. // Then
  84. // The stream should emit the new, signed-in user.
  85. await fulfillment(of: [signInExpectation], timeout: 2.0)
  86. task.cancel()
  87. }
  88. func testAuthStateChangesStreamIsCancelled() async throws {
  89. // Given
  90. let initialNilExpectation =
  91. expectation(description: "Stream should emit initial nil user")
  92. let streamCancelledExpectation =
  93. expectation(description: "Stream should not emit a value after cancellation")
  94. streamCancelledExpectation.isInverted = true
  95. try? auth.signOut()
  96. var iteration = 0
  97. let task = Task {
  98. for await _ in auth.authStateChanges {
  99. if iteration == 0 {
  100. initialNilExpectation.fulfill()
  101. } else {
  102. // This line should not be reached. If it is, the inverted expectation will be
  103. // fulfilled, and the test will fail as intended.
  104. streamCancelledExpectation.fulfill()
  105. }
  106. iteration += 1
  107. }
  108. }
  109. // Wait for the stream to emit its initial `nil` value.
  110. await fulfillment(of: [initialNilExpectation], timeout: 1.0)
  111. // When: The listening task is cancelled.
  112. task.cancel()
  113. // And an attempt is made to trigger another update.
  114. setFakeGetAccountProviderAnonymous()
  115. setFakeSecureTokenService()
  116. rpcIssuer.respondBlock = {
  117. try self.rpcIssuer.respond(withJSON: ["idToken": "TEST_ACCESS_TOKEN",
  118. "refreshToken": self.kRefreshToken,
  119. "isNewUser": true])
  120. }
  121. _ = try? await auth.signInAnonymously()
  122. // Then: Wait for a period to ensure the inverted expectation is not fulfilled.
  123. await fulfillment(of: [streamCancelledExpectation], timeout: 1.0)
  124. // And explicitly check that the loop only ever ran once.
  125. XCTAssertEqual(iteration, 1, "The stream should have only emitted its initial value.")
  126. }
  127. func testAuthStateChangesStreamYieldsNilOnSignOut() async throws {
  128. // Given
  129. let initialNilExpectation = expectation(description: "Stream should emit initial nil user")
  130. let signInExpectation = expectation(description: "Stream should emit signed-in user")
  131. let signOutExpectation = expectation(description: "Stream should emit nil after sign-out")
  132. try? auth.signOut()
  133. var iteration = 0
  134. let task = Task {
  135. for await user in auth.authStateChanges {
  136. switch iteration {
  137. case 0:
  138. XCTAssertNil(user, "The initial user should be nil")
  139. initialNilExpectation.fulfill()
  140. case 1:
  141. XCTAssertNotNil(user, "The stream should yield the signed-in user")
  142. signInExpectation.fulfill()
  143. case 2:
  144. XCTAssertNil(user, "The stream should yield nil after sign-out")
  145. signOutExpectation.fulfill()
  146. default:
  147. XCTFail("The stream should not have emitted more than three values.")
  148. }
  149. iteration += 1
  150. }
  151. }
  152. // Wait for the initial nil value.
  153. await fulfillment(of: [initialNilExpectation], timeout: 1.0)
  154. // Sign in a user.
  155. setFakeGetAccountProviderAnonymous()
  156. setFakeSecureTokenService()
  157. rpcIssuer.respondBlock = {
  158. try self.rpcIssuer.respond(withJSON: ["idToken": "TEST_ACCESS_TOKEN",
  159. "refreshToken": self.kRefreshToken,
  160. "isNewUser": true])
  161. }
  162. _ = try await auth.signInAnonymously()
  163. await fulfillment(of: [signInExpectation], timeout: 2.0)
  164. // When
  165. try auth.signOut()
  166. // Then
  167. await fulfillment(of: [signOutExpectation], timeout: 2.0)
  168. task.cancel()
  169. }
  170. }