FakeBackendRPCIssuer.swift 6.9 KB

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