diff --git a/FirebaseAuth/Sources/Auth/FIRAuth.m b/FirebaseAuth/Sources/Auth/FIRAuth.m index 6cf7f54845c..67a5238717c 100644 --- a/FirebaseAuth/Sources/Auth/FIRAuth.m +++ b/FirebaseAuth/Sources/Auth/FIRAuth.m @@ -30,7 +30,6 @@ #import "FirebaseAuth-Swift.h" #import "FirebaseAuth/Sources/Auth/FIRAuthDataResult_Internal.h" -#import "FirebaseAuth/Sources/Auth/FIRAuthDispatcher.h" #import "FirebaseAuth/Sources/Auth/FIRAuthGlobalWorkQueue.h" #import "FirebaseAuth/Sources/SystemService/FIRAuthStoredUserManager.h" #import "FirebaseAuth/Sources/User/FIRUser_Internal.h" diff --git a/FirebaseAuth/Sources/Auth/FIRAuthDispatcher.h b/FirebaseAuth/Sources/Auth/FIRAuthDispatcher.h deleted file mode 100644 index 16ccbc2db41..00000000000 --- a/FirebaseAuth/Sources/Auth/FIRAuthDispatcher.h +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 - -NS_ASSUME_NONNULL_BEGIN - -/** @typedef FIRAuthDispatcherImplBlock - @brief The type of block which can be set as the implementation for @c - dispatchAfterDelay:queue:callback: . - - @param delay The delay in seconds after which the task will be scheduled to execute. - @param queue The dispatch queue on which the task will be submitted. - @param task The task (block) to be scheduled for future execution. - */ -typedef void (^FIRAuthDispatcherImplBlock)(NSTimeInterval delay, - dispatch_queue_t queue, - void (^task)(void)); - -/** @class FIRAuthDispatchAfter - @brief A utility class used to facilitate scheduling tasks to be executed in the future. - */ -@interface FIRAuthDispatcher : NSObject - -/** @property dispatchAfterImplementation - @brief Allows custom implementation of dispatchAfterDelay:queue:callback:. - @remarks Set to nil to restore default implementation. - */ -@property(nonatomic, nullable, copy) FIRAuthDispatcherImplBlock dispatchAfterImplementation; - -/** @fn dispatchAfterDelay:queue:callback: - @brief Schedules task in the future after a specified delay. - - @param delay The delay in seconds after which the task will be scheduled to execute. - @param queue The dispatch queue on which the task will be submitted. - @param task The task (block) to be scheduled for future execution. - */ -- (void)dispatchAfterDelay:(NSTimeInterval)delay - queue:(dispatch_queue_t)queue - task:(void (^)(void))task; - -/** @fn sharedInstance - @brief Gets the shared instance of this class. - @return The shared instance of this clss - */ -+ (instancetype)sharedInstance; - -@end - -NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Auth/FIRAuthDispatcher.m b/FirebaseAuth/Sources/Auth/FIRAuthDispatcher.m deleted file mode 100644 index e5e32c391d9..00000000000 --- a/FirebaseAuth/Sources/Auth/FIRAuthDispatcher.m +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "FirebaseAuth/Sources/Auth/FIRAuthDispatcher.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FIRAuthDispatcher - -@synthesize dispatchAfterImplementation = _dispatchAfterImplementation; - -+ (instancetype)sharedInstance { - static dispatch_once_t onceToken; - static FIRAuthDispatcher *sharedInstance; - dispatch_once(&onceToken, ^{ - sharedInstance = [[self alloc] init]; - }); - return sharedInstance; -} - -- (void)dispatchAfterDelay:(NSTimeInterval)delay - queue:(dispatch_queue_t)queue - task:(void (^)(void))task { - if (_dispatchAfterImplementation) { - _dispatchAfterImplementation(delay, queue, task); - return; - } - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_SEC), queue, task); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Swift/Auth/AuthDispatcher.swift b/FirebaseAuth/Sources/Swift/Auth/AuthDispatcher.swift new file mode 100644 index 00000000000..422ce4748e5 --- /dev/null +++ b/FirebaseAuth/Sources/Swift/Auth/AuthDispatcher.swift @@ -0,0 +1,47 @@ +// 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 + +// TODO: Get rid of public's and @objc's once FirebaseAuth.m is in Swift. + +/** @class AuthDispatcher + @brief A utility class used to facilitate scheduling tasks to be executed in the future. + */ +@objc(FIRAuthDispatcher) public class AuthDispatcher: NSObject { + @objc(sharedInstance) public static let shared = AuthDispatcher() + + /** @property dispatchAfterImplementation + @brief Allows custom implementation of dispatchAfterDelay:queue:callback:. + @remarks Set to nil to restore default implementation. + */ + @objc public + var dispatchAfterImplementation: ((TimeInterval, DispatchQueue, @escaping () -> Void) -> Void)? + + /** @fn dispatchAfterDelay:queue:callback: + @brief Schedules task in the future after a specified delay. + + @param delay The delay in seconds after which the task will be scheduled to execute. + @param queue The dispatch queue on which the task will be submitted. + @param task The task (block) to be scheduled for future execution. + */ + @objc public + func dispatch(afterDelay delay: TimeInterval, queue: DispatchQueue, task: @escaping () -> Void) { + if let dispatchAfterImplementation { + dispatchAfterImplementation(delay, queue, task) + } else { + queue.asyncAfter(deadline: DispatchTime.now() + delay, execute: task) + } + } +} diff --git a/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift b/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift index d50e3663c21..feaf48be080 100644 --- a/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift +++ b/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift @@ -31,11 +31,10 @@ public protocol AuthBackendRPCIssuer: NSObjectProtocol { @param handler provided that handles POST response. Invoked asynchronously on the auth global work queue in the future. */ - func asyncPostToURLWithRequestConfiguration(request: AuthRPCRequest, - body: Data?, - contentType: String, - completionHandler: @escaping ((Data?, Error?) - -> Void)) + func asyncPostToURL(withRequest request: AuthRPCRequest, + body: Data?, + contentType: String, + completionHandler: @escaping ((Data?, Error?) -> Void)) } public class AuthBackendRPCIssuerImplementation: NSObject, AuthBackendRPCIssuer { @@ -51,11 +50,10 @@ public class AuthBackendRPCIssuerImplementation: NSObject, AuthBackendRPCIssuer fetcherService.reuseSession = false } - public func asyncPostToURLWithRequestConfiguration(request: AuthRPCRequest, - body: Data?, - contentType: String, - completionHandler: @escaping ((Data?, Error?) - -> Void)) { + public func asyncPostToURL(withRequest request: AuthRPCRequest, + body: Data?, + contentType: String, + completionHandler: @escaping ((Data?, Error?) -> Void)) { let requestConfiguration = request.requestConfiguration() let urlRequest = AuthBackend.request(withURL: request.requestURL(), contentType: contentType, requestConfiguration: requestConfiguration) @@ -283,114 +281,112 @@ private class AuthBackendRPCImplementation: NSObject, AuthBackendImplementation return } } - RPCIssuer.asyncPostToURLWithRequestConfiguration( - request: request, - body: bodyData, - contentType: "application/json" - ) { data, error in - // If there is an error with no body data at all, then this must be a - // network error. - guard let data = data else { - if let error = error { - callback(AuthErrorUtils.networkError(underlyingError: error)) - return - } else { - // TODO: this was ignored before - fatalError("Internal error") + RPCIssuer + .asyncPostToURL(withRequest: request, body: bodyData, contentType: "application/json") { + data, error in + // If there is an error with no body data at all, then this must be a + // network error. + guard let data = data else { + if let error = error { + callback(AuthErrorUtils.networkError(underlyingError: error)) + return + } else { + // TODO: this was ignored before + fatalError("Internal error") + } } - } - // Try to decode the HTTP response data which may contain either a - // successful response or error message. - var dictionary: [String: Any] - do { - let rawDecode = try JSONSerialization.jsonObject(with: data, - options: JSONSerialization.ReadingOptions - .mutableLeaves) - guard let decodedDictionary = rawDecode as? [String: Any] else { + // Try to decode the HTTP response data which may contain either a + // successful response or error message. + var dictionary: [String: Any] + do { + let rawDecode = try JSONSerialization.jsonObject(with: data, + options: JSONSerialization.ReadingOptions + .mutableLeaves) + guard let decodedDictionary = rawDecode as? [String: Any] else { + if error != nil { + callback(AuthErrorUtils.unexpectedErrorResponse(deserializedResponse: rawDecode, + underlyingError: error)) + } else { + callback(AuthErrorUtils.unexpectedResponse(deserializedResponse: rawDecode)) + } + return + } + dictionary = decodedDictionary + } catch let jsonError { if error != nil { - callback(AuthErrorUtils.unexpectedErrorResponse(deserializedResponse: rawDecode, - underlyingError: error)) + // We have an error, but we couldn't decode the body, so we have no + // additional information other than the raw response and the + // original NSError (the jsonError is inferred by the error code + // (AuthErrorCodeUnexpectedHTTPResponse, and is irrelevant.) + callback(AuthErrorUtils.unexpectedErrorResponse(data: data, underlyingError: error)) + return } else { - callback(AuthErrorUtils.unexpectedResponse(deserializedResponse: rawDecode)) + // This is supposed to be a "successful" response, but we couldn't + // deserialize the body. + callback(AuthErrorUtils.unexpectedResponse(data: data, underlyingError: jsonError)) + return } - return } - dictionary = decodedDictionary - } catch let jsonError { + + // At this point we either have an error with successfully decoded + // details in the body, or we have a response which must pass further + // validation before we know it's truly successful. We deal with the + // case where we have an error with successfully decoded error details + // first: if error != nil { - // We have an error, but we couldn't decode the body, so we have no - // additional information other than the raw response and the - // original NSError (the jsonError is inferred by the error code - // (AuthErrorCodeUnexpectedHTTPResponse, and is irrelevant.) - callback(AuthErrorUtils.unexpectedErrorResponse(data: data, underlyingError: error)) - return - } else { - // This is supposed to be a "successful" response, but we couldn't - // deserialize the body. - callback(AuthErrorUtils.unexpectedResponse(data: data, underlyingError: jsonError)) + if let errorDictionary = dictionary["error"] as? [String: Any] { + if let errorMessage = errorDictionary["message"] as? String { + if let clientError = AuthBackendRPCImplementation.clientError( + withServerErrorMessage: errorMessage, + errorDictionary: errorDictionary, + response: response, + error: error + ) { + callback(clientError) + return + } + } + // Not a message we know, return the message directly. + callback(AuthErrorUtils.unexpectedErrorResponse( + deserializedResponse: errorDictionary, + underlyingError: error + )) + return + } + // No error message at all, return the decoded response. + callback(AuthErrorUtils + .unexpectedErrorResponse(deserializedResponse: dictionary, underlyingError: error)) return } - } - // At this point we either have an error with successfully decoded - // details in the body, or we have a response which must pass further - // validation before we know it's truly successful. We deal with the - // case where we have an error with successfully decoded error details - // first: - if error != nil { - if let errorDictionary = dictionary["error"] as? [String: Any] { - if let errorMessage = errorDictionary["message"] as? String { - if let clientError = AuthBackendRPCImplementation.clientError( - withServerErrorMessage: errorMessage, - errorDictionary: errorDictionary, - response: response, - error: error - ) { + // Finally, we try to populate the response object with the JSON + // values. + do { + try response.setFields(dictionary: dictionary) + } catch { + callback(AuthErrorUtils + .RPCResponseDecodingError(deserializedResponse: dictionary, underlyingError: error)) + return + } + // In case returnIDPCredential of a verifyAssertion request is set to + // @YES, the server may return a 200 with a response that may contain a + // server error. + if let verifyAssertionRequest = request as? VerifyAssertionRequest { + if verifyAssertionRequest.returnIDPCredential { + if let errorMessage = dictionary["errorMessage"] as? String { + let clientError = AuthBackendRPCImplementation.clientError( + withServerErrorMessage: errorMessage, + errorDictionary: dictionary, + response: response, + error: error + ) callback(clientError) return } } - // Not a message we know, return the message directly. - callback(AuthErrorUtils.unexpectedErrorResponse( - deserializedResponse: errorDictionary, - underlyingError: error - )) - return - } - // No error message at all, return the decoded response. - callback(AuthErrorUtils - .unexpectedErrorResponse(deserializedResponse: dictionary, underlyingError: error)) - return - } - - // Finally, we try to populate the response object with the JSON - // values. - do { - try response.setFields(dictionary: dictionary) - } catch { - callback(AuthErrorUtils - .RPCResponseDecodingError(deserializedResponse: dictionary, underlyingError: error)) - return - } - // In case returnIDPCredential of a verifyAssertion request is set to - // @YES, the server may return a 200 with a response that may contain a - // server error. - if let verifyAssertionRequest = request as? VerifyAssertionRequest { - if verifyAssertionRequest.returnIDPCredential { - if let errorMessage = dictionary["errorMessage"] as? String { - let clientError = AuthBackendRPCImplementation.clientError( - withServerErrorMessage: errorMessage, - errorDictionary: dictionary, - response: response, - error: error - ) - callback(clientError) - return - } } + callback(nil) } - callback(nil) - } } private class func clientError(withServerErrorMessage serverErrorMessage: String, diff --git a/FirebaseAuth/Tests/Unit/AuthTests.swift b/FirebaseAuth/Tests/Unit/AuthTests.swift index 5333b826ec9..e10c68a63be 100644 --- a/FirebaseAuth/Tests/Unit/AuthTests.swift +++ b/FirebaseAuth/Tests/Unit/AuthTests.swift @@ -20,6 +20,14 @@ import FirebaseCore class AuthTests: RPCBaseTests { private let kEmail = "user@company.com" + private let kDisplayName = "DisplayName" + private let kLocalID = "testLocalId" + private let kAccessToken = "TEST_ACCESS_TOKEN" + private let kFakeEmailSignInLink = "https://test.app.goo.gl/?link=https://test.firebase" + + "app.com/__/auth/action?apiKey%3DtestAPIKey%26mode%3DsignIn%26oobCode%3Dtestoobcode%26continueU" + + "rl%3Dhttps://test.apps.com&ibi=com.test.com&ifl=https://test.firebaseapp.com/__/auth/" + + "action?apiKey%3DtestAPIKey%26mode%3DsignIn%26oobCode%3Dtestoobcode%26continueUrl%3Dhttps://" + + "test.apps.com" private let kContinueURL = "continueURL" static let kFakeAPIKey = "FAKE_API_KEY" static var auth: Auth? @@ -33,6 +41,31 @@ class AuthTests: RPCBaseTests { auth = Auth.auth(app: FirebaseApp.app(name: "test-AuthTests")!) } + override func setUp() { + super.setUp() + // Set FIRAuthDispatcher implementation in order to save the token refresh task for later + // execution. + AuthDispatcher.shared.dispatchAfterImplementation = { delay, queue, task in + XCTAssertNotNil(task) + XCTAssertGreaterThan(delay, 0) + // TODO: + XCTFail("implement this") + // XCTAssertEqual(FIRAuthGlobalWorkQueue(), queue) + // XCTAssertEqualObjects(FIRAuthGlobalWorkQueue(), queue); + // self->_FIRAuthDispatcherCallback = task; + } + // Wait until Auth initialization completes + waitForAuthGlobalWorkQueueDrain() + } + + private func waitForAuthGlobalWorkQueueDrain() { + let workerSemaphore = DispatchSemaphore(value: 0) + kAuthGlobalWorkQueue.async { + workerSemaphore.signal() + } + _ = workerSemaphore.wait(timeout: DispatchTime.distantFuture) + } + /** @fn testFetchSignInMethodsForEmailSuccess @brief Tests the flow of a successful @c fetchSignInMethodsForEmail:completion: call. */ @@ -61,7 +94,7 @@ class AuthTests: RPCBaseTests { XCTAssertEqual(request.APIKey, AuthTests.kFakeAPIKey) // 3. Send the response from the fake backend. - _ = try RPCIssuer?.respond(withJSON: ["signinMethods": allSignInMethods]) + try RPCIssuer?.respond(withJSON: ["signinMethods": allSignInMethods]) waitForExpectations(timeout: 5) } @@ -90,6 +123,56 @@ class AuthTests: RPCBaseTests { waitForExpectations(timeout: 5) } + // TODO: Three PhoneAuth tests here. + + /** @fn testSignInWithEmailLinkSuccess + @brief Tests the flow of a successful @c signInWithEmail:link:completion: call. + */ + func testSignInWithEmailLinkSuccess() throws { + let fakeCode = "testoobcode" + let kRefreshToken = "fakeRefreshToken" + let expectation = self.expectation(description: #function) + setFakeGetAccountProvider() + setFakeSecureTokenService() + + // 1. Create a group to synchronize request creation by the fake RPCIssuer in `fetchSignInMethods`. + let group = DispatchGroup() + RPCIssuer?.group = group + group.enter() + + try AuthTests.auth?.signOut() + AuthTests.auth?.signIn(withEmail: kEmail, link: kFakeEmailSignInLink) { authResult, error in + // 4. After the response triggers the callback, verify the returned signInMethods. + XCTAssertTrue(Thread.isMainThread) + guard let user = authResult?.user else { + XCTFail("authResult.user is missing") + return + } + XCTAssertEqual(user.refreshToken, kRefreshToken) + XCTAssertFalse(user.isAnonymous) + XCTAssertEqual(user.email, self.kEmail) + XCTAssertNil(error) + expectation.fulfill() + } + group.wait() + + // 2. After the fake RPCIssuer leaves the group, validate the created Request instance. + let request = try XCTUnwrap(RPCIssuer?.request as? EmailLinkSignInRequest) + XCTAssertEqual(request.email, kEmail) + XCTAssertEqual(request.oobCode, fakeCode) + XCTAssertEqual(request.APIKey, AuthTests.kFakeAPIKey) + + // 3. Send the response from the fake backend. + try RPCIssuer?.respond(withJSON: ["idToken": kAccessToken, + "email": kEmail, + "isNewUser": true, + "expiresIn": "kTestTokenExpirationTimeInterval", + "refreshToken": kRefreshToken]) + + waitForExpectations(timeout: 10) + assertUser(try XCTUnwrap(AuthTests.auth?.currentUser)) + } + /** @fn testSendPasswordResetEmailSuccess @brief Tests the flow of a successful @c sendPasswordReset call. */ @@ -206,4 +289,50 @@ class AuthTests: RPCBaseTests { waitForExpectations(timeout: 5) } + + // MARK: Helper Functions + + private func assertUser(_ user: User) { + XCTAssertEqual(user.uid, kLocalID) + XCTAssertEqual(user.displayName, kDisplayName) + XCTAssertEqual(user.email, kEmail) + XCTAssertFalse(user.isAnonymous) + XCTAssertEqual(user.providerData.count, 1) + } + + private func setFakeSecureTokenService() { + RPCIssuer?.fakeSecureTokenServiceJSON = ["access_token": kAccessToken] + } + + private func setFakeGetAccountProvider() { + let kProviderUserInfoKey = "providerUserInfo" + let kPhotoUrlKey = "photoUrl" + let kTestPhotoURL = "testPhotoURL" + let kProviderIDkey = "providerId" + let kDisplayNameKey = "displayName" + let kFederatedIDKey = "federatedId" + let kTestFederatedID = "testFederatedId" + let kEmailKey = "email" + let kPasswordHashKey = "passwordHash" + let kTestPasswordHash = "testPasswordHash" + let kTestProviderID = "testProviderID" + let kEmailVerifiedKey = "emailVerified" + let kLocalIDKey = "localId" + + RPCIssuer?.fakeGetAccountProviderJSON = [[ + kProviderUserInfoKey: [[ + kProviderIDkey: kTestProviderID, + kDisplayNameKey: kDisplayName, + kPhotoUrlKey: kTestPhotoURL, + kFederatedIDKey: kTestFederatedID, + kEmailKey: kEmail, + ]], + kLocalIDKey: kLocalID, + kDisplayNameKey: kDisplayName, + kEmailKey: kEmail, + kPhotoUrlKey: kTestPhotoURL, + kEmailVerifiedKey: true, + kPasswordHashKey: kTestPasswordHash, + ]] + } } diff --git a/FirebaseAuth/Tests/Unit/EmailLinkSignInTests.swift b/FirebaseAuth/Tests/Unit/EmailLinkSignInTests.swift index 2f2ef6137a0..2137eef1214 100644 --- a/FirebaseAuth/Tests/Unit/EmailLinkSignInTests.swift +++ b/FirebaseAuth/Tests/Unit/EmailLinkSignInTests.swift @@ -114,11 +114,11 @@ class EmailLinkSignInTests: RPCBaseTests { rpcError = error as? NSError } - _ = try RPCIssuer?.respond(withJSON: ["idToken": kTestIDTokenResponse, - "email": kTestEmailResponse, - "isNewUser": true, - "expiresIn": "\(kTestTokenExpirationTimeInterval)", - "refreshToken": kTestRefreshToken]) + try RPCIssuer?.respond(withJSON: ["idToken": kTestIDTokenResponse, + "email": kTestEmailResponse, + "isNewUser": true, + "expiresIn": "\(kTestTokenExpirationTimeInterval)", + "refreshToken": kTestRefreshToken]) XCTAssert(callbackInvoked) XCTAssertNil(rpcError) diff --git a/FirebaseAuth/Tests/Unit/FIRAuthDispatcherTests.m b/FirebaseAuth/Tests/Unit/FIRAuthDispatcherTests.m index 5dbdf386965..a9e9901da09 100644 --- a/FirebaseAuth/Tests/Unit/FIRAuthDispatcherTests.m +++ b/FirebaseAuth/Tests/Unit/FIRAuthDispatcherTests.m @@ -16,7 +16,7 @@ #import -#import "FirebaseAuth/Sources/Auth/FIRAuthDispatcher.h" +@import FirebaseAuth; /** @var kMaxDifferenceBetweenTimeIntervals @brief The maximum difference between time intervals (in seconds), after which they will be diff --git a/FirebaseAuth/Tests/Unit/FIRAuthTests.m b/FirebaseAuth/Tests/Unit/FIRAuthTests.m index c3df801174a..4efc202da92 100644 --- a/FirebaseAuth/Tests/Unit/FIRAuthTests.m +++ b/FirebaseAuth/Tests/Unit/FIRAuthTests.m @@ -425,44 +425,7 @@ - (void)testPhoneAuthMissingVerificationID { } #endif -/** @fn testSignInWithEmailLinkSuccess - @brief Tests the flow of a successful @c signInWithEmail:link:completion: call. - */ #ifdef TODO_SWIFT -- (void)testSignInWithEmailLinkSuccess { - NSString *fakeCode = @"testoobcode"; - OCMExpect([_mockBackend emailLinkSignin:[OCMArg any] callback:[OCMArg any]]) - .andCallBlock2(^(FIREmailLinkSignInRequest *_Nullable request, - FIREmailLinkSigninResponseCallback callback) { - XCTAssertEqualObjects(request.email, kEmail); - XCTAssertEqualObjects(request.oobCode, fakeCode); - dispatch_async(FIRAuthGlobalWorkQueue(), ^() { - id mockEmailLinkSignInResponse = OCMClassMock([FIREmailLinkSignInResponse class]); - [self stubTokensWithMockResponse:mockEmailLinkSignInResponse]; - callback(mockEmailLinkSignInResponse, nil); - OCMStub([mockEmailLinkSignInResponse refreshToken]).andReturn(kRefreshToken); - }); - }); - [self expectGetAccountInfo]; - XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; - [[FIRAuth auth] signOut:NULL]; - [[FIRAuth auth] - signInWithEmail:kEmail - link:kFakeEmailSignInlink - completion:^(FIRAuthDataResult *_Nullable authResult, NSError *_Nullable error) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertNotNil(authResult.user); - XCTAssertEqualObjects(authResult.user.refreshToken, kRefreshToken); - XCTAssertFalse(authResult.user.anonymous); - XCTAssertEqualObjects(authResult.user.email, kEmail); - XCTAssertNil(error); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; - [self assertUser:[FIRAuth auth].currentUser]; - OCMVerifyAll(_mockBackend); -} - /** @fn testSignInWithEmailLinkSuccessDeeplink @brief Tests the flow of a successful @c signInWithEmail:link:completion: call using a deep link. diff --git a/FirebaseAuth/Tests/Unit/FakeBackendRPCIssuer.swift b/FirebaseAuth/Tests/Unit/FakeBackendRPCIssuer.swift index 2401e564783..f2c94c9b94a 100644 --- a/FirebaseAuth/Tests/Unit/FakeBackendRPCIssuer.swift +++ b/FirebaseAuth/Tests/Unit/FakeBackendRPCIssuer.swift @@ -62,14 +62,31 @@ class FakeBackendRPCIssuer: NSObject, AuthBackendRPCIssuer { */ var group: DispatchGroup? - func asyncPostToURLWithRequestConfiguration(request: AuthRPCRequest, - body: Data?, - contentType: String, - completionHandler: @escaping ( - (Data?, Error?) -> Void - )) { + var fakeGetAccountProviderJSON: [[String: AnyHashable]]? + var fakeSecureTokenServiceJSON: [String: AnyHashable]? + + func asyncPostToURL(withRequest request: AuthRPCRequest, + body: Data?, + contentType: String, + completionHandler: @escaping ((Data?, Error?) -> Void)) { + self.contentType = contentType + handler = completionHandler self.request = request requestURL = request.requestURL() + + 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? SecureTokenRequest, + 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 @@ -79,8 +96,6 @@ class FakeBackendRPCIssuer: NSObject, AuthBackendRPCIssuer { requestConfiguration: request.requestConfiguration()) decodedRequest = try? JSONSerialization.jsonObject(with: body) as? [String: Any] } - self.contentType = contentType - handler = completionHandler if let group { group.leave() }