| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- // Copyright 2023 Google LLC
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- import Foundation
- import XCTest
- @testable import FirebaseAuth
- // TODO: Investigate making this class support generics for the `request`.
- /** @class FakeBackendRPCIssuer
- @brief An implementation of @c AuthBackendRPCIssuer which is used to test backend request,
- response, and glue logic.
- */
- @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
- class FakeBackendRPCIssuer: AuthBackendRPCIssuer {
- /** @property requestURL
- @brief The URL which was requested.
- */
- var requestURL: URL?
- /** @property requestData
- @brief The raw data in the POST body.
- */
- var requestData: Data?
- /** @property decodedRequest
- @brief The raw data in the POST body decoded as JSON.
- */
- var decodedRequest: [String: Any]?
- /** @property contentType
- @brief The value of the content type HTTP header in the request.
- */
- var contentType: String?
- /** @property request
- @brief Save the request for validation.
- */
- var request: (any AuthRPCRequest)?
- /** @property completeRequest
- @brief The last request to be processed by the backend.
- */
- var completeRequest: Task<URLRequest, Never>!
- /** @var _handler
- @brief A block we must invoke when @c respondWithError or @c respondWithJSON are called.
- */
- private var handler: ((Data?, Error?) -> Void)?
- /** @var verifyRequester
- @brief Optional function to run tests on the request.
- */
- var verifyRequester: ((SendVerificationCodeRequest) -> Void)?
- var verifyClientRequester: ((VerifyClientRequest) -> Void)?
- var projectConfigRequester: ((GetProjectConfigRequest) -> Void)?
- var verifyPasswordRequester: ((VerifyPasswordRequest) -> Void)?
- var verifyPhoneNumberRequester: ((VerifyPhoneNumberRequest) -> Void)?
- var respondBlock: (() throws -> Void)?
- var nextRespondBlock: (() throws -> Void)?
- var fakeGetAccountProviderJSON: [[String: AnyHashable]]?
- var fakeSecureTokenServiceJSON: [String: AnyHashable]?
- var secureTokenNetworkError: NSError?
- var secureTokenErrorString: String?
- var recaptchaSiteKey = "unset recaptcha siteKey"
- func asyncCallToURL<T>(with request: T, body: Data?,
- contentType: String) async -> (Data?, Error?)
- where T: FirebaseAuth.AuthRPCRequest {
- return await withCheckedContinuation { continuation in
- self.asyncCallToURL(with: request, body: body, contentType: contentType) { data, error in
- continuation.resume(returning: (data, error))
- }
- }
- }
- func asyncCallToURL<T: AuthRPCRequest>(with request: T,
- body: Data?,
- contentType: String,
- completionHandler: @escaping ((Data?, Error?) -> Void)) {
- self.contentType = contentType
- handler = completionHandler
- self.request = request
- requestURL = request.requestURL()
- // TODO: See if we can use the above generics to avoid all this.
- if let verifyRequester,
- let verifyRequest = request as? SendVerificationCodeRequest {
- verifyRequester(verifyRequest)
- } else if let verifyClientRequester,
- let verifyClientRequest = request as? VerifyClientRequest {
- verifyClientRequester(verifyClientRequest)
- } else if let projectConfigRequester,
- let projectConfigRequest = request as? GetProjectConfigRequest {
- projectConfigRequester(projectConfigRequest)
- } else if let verifyPasswordRequester,
- let verifyPasswordRequest = request as? VerifyPasswordRequest {
- verifyPasswordRequester(verifyPasswordRequest)
- } else if let verifyPhoneNumberRequester,
- let verifyPhoneNumberRequest = request as? VerifyPhoneNumberRequest {
- verifyPhoneNumberRequester(verifyPhoneNumberRequest)
- }
- if let _ = request as? GetAccountInfoRequest,
- let json = fakeGetAccountProviderJSON {
- guard let _ = try? respond(withJSON: ["users": json]) else {
- fatalError("fakeGetAccountProviderJSON respond failed")
- }
- return
- } else if let _ = request as? GetRecaptchaConfigRequest {
- guard let _ = try? respond(withJSON: ["recaptchaKey": recaptchaSiteKey])
- else {
- fatalError("GetRecaptchaConfigRequest respond failed")
- }
- return
- } else if let _ = request as? SecureTokenRequest {
- if let secureTokenNetworkError {
- guard let _ = try? respond(withData: nil,
- error: secureTokenNetworkError) else {
- fatalError("Failed to generate secureTokenNetworkError")
- }
- } else if let secureTokenErrorString {
- guard let _ = try? respond(serverErrorMessage: secureTokenErrorString) else {
- fatalError("Failed to generate secureTokenErrorString")
- }
- return
- } else if let json = fakeSecureTokenServiceJSON {
- guard let _ = try? respond(withJSON: json) else {
- fatalError("fakeGetAccountProviderJSON respond failed")
- }
- return
- }
- }
- if let body = body {
- requestData = body
- // Use the real implementation so that the complete request can
- // be verified during testing.
- completeRequest = Task {
- await AuthBackend.request(withURL: requestURL!,
- contentType: contentType,
- requestConfiguration: request.requestConfiguration())
- }
- decodedRequest = try? JSONSerialization.jsonObject(with: body) as? [String: Any]
- }
- if let respondBlock {
- do {
- try respondBlock()
- } catch {
- XCTFail("Unexpected exception in respondBlock")
- }
- self.respondBlock = nextRespondBlock
- nextRespondBlock = nil
- }
- }
- func respond(serverErrorMessage errorMessage: String) throws {
- let error = NSError(domain: NSCocoaErrorDomain, code: 0)
- try respond(serverErrorMessage: errorMessage, error: error)
- }
- func respond(serverErrorMessage errorMessage: String, error: NSError) throws {
- let _ = try respond(withJSON: ["error": ["message": errorMessage]], error: error)
- }
- @discardableResult func respond(underlyingErrorMessage errorMessage: String,
- message: String = "See the reason") throws -> Data {
- let error = NSError(domain: NSCocoaErrorDomain, code: 0)
- return try respond(
- withJSON: ["error": ["message": message,
- "errors": [["reason": errorMessage]]] as [String: Any]],
- error: error
- )
- }
- @discardableResult func respond(withJSON json: [String: Any],
- error: NSError? = nil) throws -> Data {
- let data = try JSONSerialization.data(withJSONObject: json,
- options: JSONSerialization.WritingOptions.prettyPrinted)
- try respond(withData: data, error: error)
- return data
- }
- func respond(withData data: Data?, error: NSError?) throws {
- let handler = try XCTUnwrap(handler, "There is no pending RPC request.")
- XCTAssertTrue(
- (data != nil) || (error != nil),
- "At least one of: data or error should be been non-nil."
- )
- self.handler = nil
- handler(data, error)
- }
- }
|