FakeBackendRPCIssuer.swift 7.6 KB

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