FakeBackendRPCIssuer.swift 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. // Copyright 2023 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. import Foundation
  15. import XCTest
  16. @testable import FirebaseAuth
  17. // TODO(ncooke3): Investigate making this class support generics for the `request`.
  18. // TODO(ncooke3): Refactor to make checked Sendable.
  19. /// An implementation of `AuthBackendRPCIssuerProtocol` which is used to test
  20. /// backend request, response, and glue logic.
  21. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  22. final class FakeBackendRPCIssuer: AuthBackendRPCIssuerProtocol, @unchecked Sendable {
  23. /** @property requestURL
  24. @brief The URL which was requested.
  25. */
  26. var requestURL: URL?
  27. /** @property requestData
  28. @brief The raw data in the POST body.
  29. */
  30. var requestData: Data?
  31. /** @property decodedRequest
  32. @brief The raw data in the POST body decoded as JSON.
  33. */
  34. var decodedRequest: [String: Any]?
  35. /** @property contentType
  36. @brief The value of the content type HTTP header in the request.
  37. */
  38. var contentType: String?
  39. /** @property request
  40. @brief Save the request for validation.
  41. */
  42. var request: (any AuthRPCRequest)?
  43. /** @property completeRequest
  44. @brief The last request to be processed by the backend.
  45. */
  46. var completeRequest: Task<URLRequest, Never>!
  47. /** @var _handler
  48. @brief A block we must invoke when @c respondWithError or @c respondWithJSON are called.
  49. */
  50. private var handler: ((Data?, Error?) -> Void)?
  51. /** @var verifyRequester
  52. @brief Optional function to run tests on the request.
  53. */
  54. var verifyRequester: ((SendVerificationCodeRequest) -> (Data?, Error?))?
  55. var verifyClientRequester: ((VerifyClientRequest) -> (Data?, Error?))?
  56. var projectConfigRequester: ((GetProjectConfigRequest) -> (Data?, Error?))?
  57. var verifyPasswordRequester: ((VerifyPasswordRequest) -> (Data?, Error?))?
  58. var verifyPhoneNumberRequester: ((VerifyPhoneNumberRequest) -> Void)?
  59. var respondBlock: (() throws -> (Data?, Error?))?
  60. var nextRespondBlock: (() throws -> (Data?, Error?))?
  61. var fakeGetAccountProviderJSON: [[String: AnyHashable]]?
  62. var fakeSecureTokenServiceJSON: [String: AnyHashable]?
  63. var secureTokenNetworkError: NSError?
  64. var secureTokenErrorString: String?
  65. var recaptchaSiteKey = "projects/fakeProjectId/keys/mockSiteKey"
  66. var rceMode: String = "OFF"
  67. func asyncCallToURL<T>(with request: T, body: Data?,
  68. contentType: String) async -> (Data?, Error?)
  69. where T: FirebaseAuth.AuthRPCRequest {
  70. self.contentType = contentType
  71. self.request = request
  72. requestURL = request.requestURL()
  73. // TODO: See if we can use the above generics to avoid all this.
  74. if let verifyRequester,
  75. let verifyRequest = request as? SendVerificationCodeRequest {
  76. return verifyRequester(verifyRequest)
  77. } else if let verifyClientRequester,
  78. let verifyClientRequest = request as? VerifyClientRequest {
  79. return verifyClientRequester(verifyClientRequest)
  80. } else if let projectConfigRequester,
  81. let projectConfigRequest = request as? GetProjectConfigRequest {
  82. return projectConfigRequester(projectConfigRequest)
  83. } else if let verifyPasswordRequester,
  84. let verifyPasswordRequest = request as? VerifyPasswordRequest {
  85. return verifyPasswordRequester(verifyPasswordRequest)
  86. } else if let verifyPhoneNumberRequester,
  87. let verifyPhoneNumberRequest = request as? VerifyPhoneNumberRequest {
  88. verifyPhoneNumberRequester(verifyPhoneNumberRequest)
  89. }
  90. if let _ = request as? GetAccountInfoRequest,
  91. let json = fakeGetAccountProviderJSON {
  92. guard let (data, error) = try? respond(withJSON: ["users": json]) else {
  93. fatalError("fakeGetAccountProviderJSON respond failed")
  94. }
  95. return (data, error)
  96. } else if let _ = request as? GetRecaptchaConfigRequest {
  97. if rceMode != "OFF" { // Check if reCAPTCHA is enabled
  98. let recaptchaKey = recaptchaSiteKey // iOS key from your config
  99. let enforcementState = [
  100. ["provider": "EMAIL_PASSWORD_PROVIDER", "enforcementState": rceMode],
  101. ["provider": "PHONE_PROVIDER", "enforcementState": rceMode],
  102. ]
  103. guard let (data, error) = try? respond(withJSON: [
  104. "recaptchaKey": recaptchaKey,
  105. "recaptchaEnforcementState": enforcementState,
  106. ]) else {
  107. fatalError("GetRecaptchaConfigRequest respond failed")
  108. }
  109. return (data, error)
  110. } else { // reCAPTCHA OFF
  111. let enforcementState = [
  112. ["provider": "EMAIL_PASSWORD_PROVIDER", "enforcementState": "OFF"],
  113. ["provider": "PHONE_PROVIDER", "enforcementState": "OFF"],
  114. ]
  115. guard let (data, error) = try? respond(withJSON: [
  116. "recaptchaEnforcementState": enforcementState,
  117. ]) else {
  118. fatalError("GetRecaptchaConfigRequest respond failed")
  119. }
  120. return (data, error)
  121. }
  122. } else if let _ = request as? SecureTokenRequest {
  123. if let secureTokenNetworkError {
  124. return (nil, secureTokenNetworkError)
  125. } else if let secureTokenErrorString {
  126. guard let (data, error) = try? respond(serverErrorMessage: secureTokenErrorString) else {
  127. fatalError("Failed to generate secureTokenErrorString")
  128. }
  129. return (data, error)
  130. } else if let json = fakeSecureTokenServiceJSON {
  131. guard let (data, error) = try? respond(withJSON: json) else {
  132. fatalError("fakeGetAccountProviderJSON respond failed")
  133. }
  134. return (data, error)
  135. }
  136. }
  137. if let body = body {
  138. requestData = body
  139. // Use the real implementation so that the complete request can
  140. // be verified during testing.
  141. let requestURL = request.requestURL()
  142. let requestConfiguration = request.requestConfiguration()
  143. completeRequest = Task {
  144. await AuthBackend
  145. .request(
  146. for: requestURL,
  147. httpMethod: requestData == nil ? "GET" : "POST",
  148. contentType: contentType,
  149. requestConfiguration: requestConfiguration
  150. )
  151. }
  152. decodedRequest = try? JSONSerialization.jsonObject(with: body) as? [String: Any]
  153. }
  154. if let respondBlock {
  155. do {
  156. let (data, error) = try respondBlock()
  157. self.respondBlock = nextRespondBlock
  158. nextRespondBlock = nil
  159. return (data, error)
  160. } catch {
  161. return (nil, error)
  162. }
  163. }
  164. fatalError("Should never get here")
  165. }
  166. func respond(serverErrorMessage errorMessage: String) throws -> (Data, Error?) {
  167. let error = NSError(domain: NSCocoaErrorDomain, code: 0)
  168. return try respond(serverErrorMessage: errorMessage, error: error)
  169. }
  170. func respond(serverErrorMessage errorMessage: String, error: NSError) throws -> (Data, Error?) {
  171. return try respond(withJSON: ["error": ["message": errorMessage]], error: error)
  172. }
  173. func respond(underlyingErrorMessage errorMessage: String,
  174. message: String = "See the reason") throws -> (Data, Error?) {
  175. let error = NSError(domain: NSCocoaErrorDomain, code: 0)
  176. return try respond(
  177. withJSON: ["error": ["message": message,
  178. "errors": [["reason": errorMessage]]] as [String: Any]],
  179. error: error
  180. )
  181. }
  182. func respond(withJSON json: [String: Any], error: NSError? = nil) throws -> (Data, Error?) {
  183. return try (JSONSerialization.data(withJSONObject: json,
  184. options: JSONSerialization.WritingOptions.prettyPrinted),
  185. error)
  186. }
  187. }