FakeBackendRPCIssuer.swift 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  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: NSObject, 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: URLRequest?
  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: AuthRPCRequest>(with request: T,
  68. body: Data?,
  69. contentType: String,
  70. completionHandler: @escaping ((Data?, Error?) -> Void)) {
  71. self.contentType = contentType
  72. handler = completionHandler
  73. self.request = request
  74. requestURL = request.requestURL()
  75. // TODO: See if we can use the above generics to avoid all this.
  76. if let verifyRequester,
  77. let verifyRequest = request as? SendVerificationCodeRequest {
  78. verifyRequester(verifyRequest)
  79. } else if let verifyClientRequester,
  80. let verifyClientRequest = request as? VerifyClientRequest {
  81. verifyClientRequester(verifyClientRequest)
  82. } else if let projectConfigRequester,
  83. let projectConfigRequest = request as? GetProjectConfigRequest {
  84. projectConfigRequester(projectConfigRequest)
  85. } else if let verifyPasswordRequester,
  86. let verifyPasswordRequest = request as? VerifyPasswordRequest {
  87. verifyPasswordRequester(verifyPasswordRequest)
  88. } else if let verifyPhoneNumberRequester,
  89. let verifyPhoneNumberRequest = request as? VerifyPhoneNumberRequest {
  90. verifyPhoneNumberRequester(verifyPhoneNumberRequest)
  91. }
  92. if let _ = request as? GetAccountInfoRequest,
  93. let json = fakeGetAccountProviderJSON {
  94. guard let _ = try? respond(withJSON: ["users": json]) else {
  95. fatalError("fakeGetAccountProviderJSON respond failed")
  96. }
  97. return
  98. } else if let _ = request as? GetRecaptchaConfigRequest {
  99. guard let _ = try? respond(withJSON: ["recaptchaKey": recaptchaSiteKey])
  100. else {
  101. fatalError("GetRecaptchaConfigRequest respond failed")
  102. }
  103. return
  104. } else if let _ = request as? SecureTokenRequest {
  105. if let secureTokenNetworkError {
  106. guard let _ = try? respond(withData: nil,
  107. error: secureTokenNetworkError) else {
  108. fatalError("Failed to generate secureTokenNetworkError")
  109. }
  110. } else if let secureTokenErrorString {
  111. guard let _ = try? respond(serverErrorMessage: secureTokenErrorString) else {
  112. fatalError("Failed to generate secureTokenErrorString")
  113. }
  114. return
  115. } else if let json = fakeSecureTokenServiceJSON {
  116. guard let _ = try? respond(withJSON: json) else {
  117. fatalError("fakeGetAccountProviderJSON respond failed")
  118. }
  119. return
  120. }
  121. }
  122. if let body = body {
  123. requestData = body
  124. // Use the real implementation so that the complete request can
  125. // be verified during testing.
  126. AuthBackend.request(withURL: requestURL!,
  127. contentType: contentType,
  128. requestConfiguration: request.requestConfiguration()) { request in
  129. self.completeRequest = request
  130. }
  131. decodedRequest = try? JSONSerialization.jsonObject(with: body) as? [String: Any]
  132. }
  133. if let respondBlock {
  134. do {
  135. try respondBlock()
  136. } catch {
  137. XCTFail("Unexpected exception in respondBlock")
  138. }
  139. self.respondBlock = nextRespondBlock
  140. nextRespondBlock = nil
  141. }
  142. }
  143. @discardableResult func respond(serverErrorMessage errorMessage: String) throws -> Data {
  144. let error = NSError(domain: NSCocoaErrorDomain, code: 0)
  145. return try respond(serverErrorMessage: errorMessage, error: error)
  146. }
  147. @discardableResult
  148. func respond(serverErrorMessage errorMessage: String, error: NSError) throws -> Data {
  149. return try respond(withJSON: ["error": ["message": errorMessage]], error: error)
  150. }
  151. @discardableResult func respond(underlyingErrorMessage errorMessage: String,
  152. message: String = "See the reason") throws -> Data {
  153. let error = NSError(domain: NSCocoaErrorDomain, code: 0)
  154. return try respond(withJSON: ["error": ["message": message,
  155. "errors": [["reason": errorMessage]]] as [String: Any]],
  156. error: error)
  157. }
  158. @discardableResult func respond(withJSON json: [String: Any],
  159. error: NSError? = nil) throws -> Data {
  160. let data = try JSONSerialization.data(withJSONObject: json,
  161. options: JSONSerialization.WritingOptions.prettyPrinted)
  162. try respond(withData: data, error: error)
  163. return data
  164. }
  165. func respond(withData data: Data?, error: NSError?) throws {
  166. let handler = try XCTUnwrap(handler, "There is no pending RPC request.")
  167. XCTAssertTrue(
  168. (data != nil) || (error != nil),
  169. "At least one of: data or error should be been non-nil."
  170. )
  171. self.handler = nil
  172. handler(data, error)
  173. }
  174. }