From 9118aca998dbe2ceac45d64b21a91c6376928df7 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 15 Aug 2024 14:42:50 -0700 Subject: [PATCH] Fix and Regression Test for FirebaseUI 1199 (#13505) --- FirebaseAuth/CHANGELOG.md | 4 + FirebaseAuth/Sources/Swift/Auth/Auth.swift | 8 +- .../Swift/AuthProvider/OAuthProvider.swift | 1 + .../Tests/Unit/FIROAuthProviderTests.m | 193 ++++++++++++++++++ Package.swift | 6 +- 5 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 FirebaseAuth/Tests/Unit/FIROAuthProviderTests.m diff --git a/FirebaseAuth/CHANGELOG.md b/FirebaseAuth/CHANGELOG.md index 0f6d994d704..253206a553e 100644 --- a/FirebaseAuth/CHANGELOG.md +++ b/FirebaseAuth/CHANGELOG.md @@ -5,6 +5,10 @@ will need expansion. (#13429) - [fixed] Fix crash introduced in 11.0.0 in phone authentication flow from implicitly unwrapping `nil` error after a token timeout. (#13470) +- [fixed] Objective-C only: `[OAuthProvider getCredentialWithUIDelegate]` was not calling its + completion handler in the main thread. Regressed in 11.0.0. The fix is only for CocoaPods and + Swift Package Manager. The zip and Carthage fix will roll out in 11.2.0. + (https://github.com/firebase/FirebaseUI-iOS/issues/1199) # 11.0.0 - [fixed] Fixed auth domain matching code to prioritize matching `firebaseapp.com` over `web.app` diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index cdf11a6ba27..7b7212c441c 100644 --- a/FirebaseAuth/Sources/Swift/Auth/Auth.swift +++ b/FirebaseAuth/Sources/Swift/Auth/Auth.swift @@ -2320,8 +2320,12 @@ extension Auth: AuthInterop { // MARK: Internal properties - /// Allow tests to swap in an alternate mainBundle. - var mainBundleUrlTypes: [[String: Any]]! + /// Allow tests to swap in an alternate mainBundle, including ObjC unit tests via CocoaPods. + #if FIREBASE_CI + @objc public var mainBundleUrlTypes: [[String: Any]]! + #else + var mainBundleUrlTypes: [[String: Any]]! + #endif /// The configuration object comprising of parameters needed to make a request to Firebase /// Auth's backend. diff --git a/FirebaseAuth/Sources/Swift/AuthProvider/OAuthProvider.swift b/FirebaseAuth/Sources/Swift/AuthProvider/OAuthProvider.swift index f04916cacae..c14fed69031 100644 --- a/FirebaseAuth/Sources/Swift/AuthProvider/OAuthProvider.swift +++ b/FirebaseAuth/Sources/Swift/AuthProvider/OAuthProvider.swift @@ -341,6 +341,7 @@ import Foundation /// - Parameter uiDelegate: An optional UI delegate used to present the mobile web flow. @available(iOS 13, tvOS 13, macOS 10.15, watchOS 8, *) @objc(getCredentialWithUIDelegate:completion:) + @MainActor open func credential(with uiDelegate: AuthUIDelegate?) async throws -> AuthCredential { return try await withCheckedThrowingContinuation { continuation in getCredentialWith(uiDelegate) { credential, error in diff --git a/FirebaseAuth/Tests/Unit/FIROAuthProviderTests.m b/FirebaseAuth/Tests/Unit/FIROAuthProviderTests.m new file mode 100644 index 00000000000..b5921312caa --- /dev/null +++ b/FirebaseAuth/Tests/Unit/FIROAuthProviderTests.m @@ -0,0 +1,193 @@ +/* + * 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 +#if TARGET_OS_IOS + +#import + +@import FirebaseAuth; +@import FirebaseCore; + +/** @var kExpectationTimeout + @brief The maximum time waiting for expectations to fulfill. + */ +static const NSTimeInterval kExpectationTimeout = 1; + +/** @var kFakeAuthorizedDomain + @brief A fake authorized domain for the app. + */ +static NSString *const kFakeAuthorizedDomain = @"test.firebaseapp.com"; + +/** @var kFakeBundleID + @brief A fake bundle ID. + */ +static NSString *const kFakeBundleID = @"com.firebaseapp.example"; + +/** @var kFakeAccessToken + @brief A fake access token for testing. + */ +static NSString *const kFakeAccessToken = @"fakeAccessToken"; + +/** @var kFakeIDToken + @brief A fake ID token for testing. + */ +static NSString *const kFakeIDToken = @"fakeIDToken"; + +/** @var kFakeProviderID + @brief A fake provider ID for testing. + */ +static NSString *const kFakeProviderID = @"fakeProviderID"; + +/** @var kFakeGivenName + @brief A fake given name for testing. + */ +static NSString *const kFakeGivenName = @"fakeGivenName"; + +/** @var kFakeFamilyName + @brief A fake family name for testing. + */ +static NSString *const kFakeFamilyName = @"fakeFamilyName"; + +/** @var kFakeAPIKey + @brief A fake API key. + */ +static NSString *const kFakeAPIKey = @"asdfghjkl"; + +/** @var kFakeEmulatorHost + @brief A fake emulator host. + */ +static NSString *const kFakeEmulatorHost = @"emulatorhost"; + +/** @var kFakeEmulatorPort + @brief A fake emulator port. + */ +static NSString *const kFakeEmulatorPort = @"12345"; + +/** @var kFakeClientID + @brief A fake client ID. + */ +static NSString *const kFakeClientID = @"123456.apps.googleusercontent.com"; + +/** @var kFakeReverseClientID + @brief The dot-reversed version of the fake client ID. + */ +static NSString *const kFakeReverseClientID = @"com.googleusercontent.apps.123456"; + +/** @var kFakeFirebaseAppID + @brief A fake Firebase app ID. + */ +static NSString *const kFakeFirebaseAppID = @"1:123456789:ios:123abc456def"; + +/** @var kFakeEncodedFirebaseAppID + @brief A fake encoded Firebase app ID to be used as a custom URL scheme. + */ +static NSString *const kFakeEncodedFirebaseAppID = @"app-1-123456789-ios-123abc456def"; + +/** @var kFakeTenantID + @brief A fake tenant ID. + */ +static NSString *const kFakeTenantID = @"tenantID"; + +/** @var kFakeOAuthResponseURL + @brief A fake OAuth response URL used in test. + */ +static NSString *const kFakeOAuthResponseURL = @"fakeOAuthResponseURL"; + +/** @var kFakeRedirectURLResponseURL + @brief A fake callback URL (minus the scheme) containing a fake response URL. + */ + +@interface FIROAuthProviderTests : XCTestCase + +@end + +@implementation FIROAuthProviderTests + +/** @fn testObtainingOAuthCredentialNoIDToken + @brief Tests the correct creation of an OAuthCredential without an IDToken. + */ +- (void)testObtainingOAuthCredentialNoIDToken { + FIRAuthCredential *credential = [FIROAuthProvider credentialWithProviderID:kFakeProviderID + accessToken:kFakeAccessToken]; + XCTAssertTrue([credential isKindOfClass:[FIROAuthCredential class]]); + FIROAuthCredential *OAuthCredential = (FIROAuthCredential *)credential; + XCTAssertEqualObjects(OAuthCredential.accessToken, kFakeAccessToken); + XCTAssertEqualObjects(OAuthCredential.provider, kFakeProviderID); + XCTAssertNil(OAuthCredential.IDToken); +} + +/** @fn testObtainingOAuthCredentialWithFullName + @brief Tests the correct creation of an OAuthCredential with a fullName. + */ +- (void)testObtainingOAuthCredentialWithFullName { + NSPersonNameComponents *fullName = [[NSPersonNameComponents alloc] init]; + fullName.givenName = kFakeGivenName; + fullName.familyName = kFakeFamilyName; + FIRAuthCredential *credential = [FIROAuthProvider appleCredentialWithIDToken:kFakeIDToken + rawNonce:nil + fullName:fullName]; + + XCTAssertTrue([credential isKindOfClass:[FIROAuthCredential class]]); + FIROAuthCredential *OAuthCredential = (FIROAuthCredential *)credential; + XCTAssertEqualObjects(OAuthCredential.provider, @"apple.com"); + XCTAssertEqualObjects(OAuthCredential.IDToken, kFakeIDToken); + XCTAssertNil(OAuthCredential.accessToken); +} + +/** @fn testObtainingOAuthCredentialWithIDToken + @brief Tests the correct creation of an OAuthCredential with an IDToken + */ +- (void)testObtainingOAuthCredentialWithIDToken { + FIRAuthCredential *credential = [FIROAuthProvider credentialWithProviderID:kFakeProviderID + IDToken:kFakeIDToken + accessToken:kFakeAccessToken]; + XCTAssertTrue([credential isKindOfClass:[FIROAuthCredential class]]); + FIROAuthCredential *OAuthCredential = (FIROAuthCredential *)credential; + XCTAssertEqualObjects(OAuthCredential.accessToken, kFakeAccessToken); + XCTAssertEqualObjects(OAuthCredential.provider, kFakeProviderID); + XCTAssertEqualObjects(OAuthCredential.IDToken, kFakeIDToken); +} + +/** @fn testGetCredentialWithUIDelegateWithClientIDOnMainThread + @brief Verifies @c getCredentialWithUIDelegate:completion: calls its completion handler on the + main thread. Regression test for firebase/FirebaseUI-iOS#1199. + */ +- (void)testGetCredentialWithUIDelegateWithClientIDOnMainThread { + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + + FIROptions *options = + [[FIROptions alloc] initWithGoogleAppID:@"0:0000000000000:ios:0000000000000000" + GCMSenderID:@"00000000000000000-00000000000-000000000"]; + options.APIKey = kFakeAPIKey; + options.projectID = @"myProjectID"; + options.clientID = kFakeClientID; + [FIRApp configureWithName:@"objAppName" options:options]; + FIRAuth *auth = [FIRAuth authWithApp:[FIRApp appNamed:@"objAppName"]]; + [auth setMainBundleUrlTypes:@[ @{@"CFBundleURLSchemes" : @[ kFakeReverseClientID ]} ]]; + + FIROAuthProvider *provider = [FIROAuthProvider providerWithProviderID:kFakeProviderID auth:auth]; + [provider getCredentialWithUIDelegate:nil + completion:^(FIRAuthCredential *_Nullable credential, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; +} +@end + +#endif // TARGET_OS_IOS diff --git a/Package.swift b/Package.swift index 6da2349566d..8647898aba5 100644 --- a/Package.swift +++ b/Package.swift @@ -461,8 +461,10 @@ let package = Package( // TODO: these tests rely on a non-zero UIApplication.shared. They run from CocoaPods. "PhoneAuthProviderTests.swift", "AuthNotificationManagerTests.swift", - "ObjCAPITests.m", // Only builds via CocoaPods until mixed language or its own target. - "ObjCGlobalTests.m", // Only builds via CocoaPods until mixed language or its own target. + // TODO: The following tests run in CocoaPods only, until mixed language or separate target. + "ObjCAPITests.m", + "ObjCGlobalTests.m", + "FIROAuthProviderTests.m", ] ), .target(