Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/google_sign_in/google_sign_in_ios/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 6.3.1

* Improves testability of iOS scene URL handling.

## 6.3.0

* Adds UIScene compatibility.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,13 @@ class TestSignIn: NSObject, FSIGIDSignIn {
// Whether signOut was called.
var signOutCalled = false

// URLs passed to handleURL:, and the value to return.
var handledURLs: [URL] = []
var handleURLResult = true

func handle(_ url: URL) -> Bool {
return true
handledURLs.append(url)
return handleURLResult
}

func restorePreviousSignIn(completion: (((any FSIGIDGoogleUser)?, Error?) -> Void)?) {
Expand Down Expand Up @@ -693,8 +698,228 @@ struct GoogleSignInPluginTests {
}
}
}

@Suite("urlHandling")
@MainActor
struct URLHandlingTests {
#if os(iOS) || targetEnvironment(macCatalyst)
@Test func applicationOpenURL() {
let (plugin, fakeSignIn) = createTestPlugin()
let url = URL(string: "com.googleusercontent.apps.test:/oauthredirect")!
fakeSignIn.handleURLResult = true

let handled = plugin.application(UIApplication.shared, open: url, options: [:])

#expect(handled == true)
#expect(fakeSignIn.handledURLs == [url])
}

@Test func applicationOpenURLReturnsHandleResult() {
let (plugin, fakeSignIn) = createTestPlugin()
let url = URL(string: "com.googleusercontent.apps.test:/oauthredirect")!
fakeSignIn.handleURLResult = false

let handled = plugin.application(UIApplication.shared, open: url, options: [:])

#expect(handled == false)
#expect(fakeSignIn.handledURLs == [url])
}

@Test func handleURLs() {
let (plugin, fakeSignIn) = createTestPlugin()
let firstURL = URL(string: "com.googleusercontent.apps.test:/oauthredirect")!
let secondURL = URL(string: "com.googleusercontent.apps.test:/another")!

plugin.handleURLs([firstURL, secondURL])

#expect(fakeSignIn.handledURLs == [firstURL, secondURL])
}
#endif
}

@Suite("errorMapping") struct ErrorMappingTests {
@Test(arguments: [
(GIDSignInError.keychain.rawValue, FSIGoogleSignInErrorCode.keychainError),
(GIDSignInError.EMM.rawValue, FSIGoogleSignInErrorCode.eemError),
(GIDSignInError.unknown.rawValue, FSIGoogleSignInErrorCode.unknown),
// Unrecognized SDK codes fall through to the default → unknown.
(12_345, FSIGoogleSignInErrorCode.unknown),
]) func mapsRemainingGIDSignInErrorCodes(
signInSDKErrorCode: Int,
expectedPigeonErrorCode: FSIGoogleSignInErrorCode
) async {
let (plugin, fakeSignIn) = createTestPlugin()
fakeSignIn.error = NSError(
domain: kGIDSignInErrorDomain, code: signInSDKErrorCode, userInfo: nil)

await confirmation("completion called") { confirmed in
plugin.signIn(withScopeHint: [], nonce: nil) { result, error in
#expect(error == nil)
#expect(result?.success == nil)
#expect(result?.error?.type == expectedPigeonErrorCode)
confirmed()
}
}
}

@Test func sanitizesComplexUserInfoInFlutterError() async {
let (plugin, _) = createTestPlugin()
let fakeUser = addSignedInUser(to: plugin)
let nested = NSError(domain: "NestedDomain", code: 99, userInfo: ["nestedKey": "nestedValue"])
let userInfo: [String: Any] = [
"string": "ok",
"number": NSNumber(value: 42),
"url": URL(string: "https://example.com/path")!,
"array": ["a", NSNumber(value: 1)],
"dict": ["inner": "value"],
NSUnderlyingErrorKey: nested,
"unsupported": Date(timeIntervalSince1970: 0),
]
fakeUser.error = NSError(domain: "BogusDomain", code: 7, userInfo: userInfo)

await confirmation("completion called") { confirmed in
plugin.refreshedAuthorizationTokens(forUser: fakeUser.userID!) { result, error in
#expect(result == nil)
#expect(error?.code == "BogusDomain: 7")
let details = error?.details as? [String: Any]
#expect(details?["string"] as? String == "ok")
#expect(details?["number"] as? NSNumber == NSNumber(value: 42))
#expect(details?["url"] as? String == "https://example.com/path")
#expect((details?["array"] as? [Any])?.count == 2)
#expect((details?["dict"] as? [String: Any])?["inner"] as? String == "value")
let nestedDetails = details?[NSUnderlyingErrorKey] as? [String: Any]
#expect(nestedDetails?["domain"] as? String == "NestedDomain")
#expect(nestedDetails?["code"] as? String == "99")
let unsupported = details?["unsupported"] as? String
#expect(unsupported?.contains("Unsupported type:") == true)
confirmed()
}
}
}
}

@Suite("disconnect") struct DisconnectTests {
@Test func disconnectReturnsFlutterErrorOnFailure() async {
let (plugin, fakeSignIn) = createTestPlugin()
fakeSignIn.error = NSError(
domain: "DisconnectDomain", code: 3, userInfo: ["reason": "failed"])

await confirmation("completion called") { confirmed in
plugin.disconnect { error in
#expect(error?.code == "DisconnectDomain: 3")
#expect((error?.details as? [String: Any])?["reason"] as? String == "failed")
confirmed()
}
}
}
}

@Suite("userData") struct UserDataTests {
@Test func signInWithoutProfileImageOmitsPhotoUrl() async {
let (plugin, fakeSignIn) = createTestPlugin()
let fakeUser = TestGoogleUser("mockID")
fakeUser.profile = TestProfileData(name: "Name", email: "user@example.com", imageURL: nil)
fakeSignIn.signInResult = TestSignInResult(user: fakeUser)

await confirmation("completion called") { confirmed in
plugin.signIn(withScopeHint: [], nonce: nil) { result, error in
#expect(error == nil)
#expect(result?.success?.user.photoUrl == nil)
#expect(result?.success?.user.displayName == "Name")
confirmed()
}
}
}
}

#if os(iOS) || targetEnvironment(macCatalyst)
@Suite("topViewController")
@MainActor
struct TopViewControllerTests {
@Test func usesNavigationControllerVisibleController() async {
let root = UIViewController()
let nav = UINavigationController(rootViewController: root)
let top = UIViewController()
nav.pushViewController(top, animated: false)

let viewProvider = TestViewProvider()
viewProvider.viewController = nav
let (plugin, fakeSignIn) = createTestPlugin(viewProvider: viewProvider)
fakeSignIn.signInResult = TestSignInResult(user: TestGoogleUser("id"))

await confirmation("completion called") { confirmed in
plugin.signIn(withScopeHint: [], nonce: nil) { _, _ in confirmed() }
}
#expect(fakeSignIn.presentingViewController === top)
}

@Test func usesTabBarControllerSelectedController() async {
let selected = UIViewController()
let other = UIViewController()
let tab = UITabBarController()
tab.viewControllers = [selected, other]
tab.selectedViewController = selected

let viewProvider = TestViewProvider()
viewProvider.viewController = tab
let (plugin, fakeSignIn) = createTestPlugin(viewProvider: viewProvider)
fakeSignIn.signInResult = TestSignInResult(user: TestGoogleUser("id"))

await confirmation("completion called") { confirmed in
plugin.signIn(withScopeHint: [], nonce: nil) { _, _ in confirmed() }
}
#expect(fakeSignIn.presentingViewController === selected)
}

@Test func usesPresentedViewController() async {
let presented = UIViewController()
let host = StubHostingViewController(presented: presented)

let viewProvider = TestViewProvider()
viewProvider.viewController = host
let (plugin, fakeSignIn) = createTestPlugin(viewProvider: viewProvider)
fakeSignIn.signInResult = TestSignInResult(user: TestGoogleUser("id"))

await confirmation("completion called") { confirmed in
plugin.signIn(withScopeHint: [], nonce: nil) { _, _ in confirmed() }
}
#expect(fakeSignIn.presentingViewController === presented)
}
}
#endif
}

#if os(iOS) || targetEnvironment(macCatalyst)
/// Stand-in for UIOpenURLContext that only needs to respond to `URL`.
final class FakeOpenURLContext: NSObject {
@objc(URL) var openURL: URL

init(url: URL) {
self.openURL = url
super.init()
}
}

/// Lets tests stub `presentedViewController` without a real window presentation.
@MainActor
final class StubHostingViewController: UIViewController {
private let stubPresented: UIViewController?

init(presented: UIViewController?) {
self.stubPresented = presented
super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override var presentedViewController: UIViewController? {
stubPresented
}
}
#endif

func loadGoogleServiceInfo() -> [String: Any]? {
if let plistPath = Bundle(for: TestSignIn.self).path(
forResource: "GoogleService-Info", ofType: "plist")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,18 @@ - (instancetype)initWithSignIn:(NSObject<FSIGIDSignIn> *)signIn

#if TARGET_OS_IOS

- (void)handleURLs:(NSArray<NSURL *> *)urls {
for (NSURL *url in urls) {
[self.signIn handleURL:url];
}
}

- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts {
NSMutableArray<NSURL *> *urls = [NSMutableArray arrayWithCapacity:URLContexts.count];
for (UIOpenURLContext *context in URLContexts) {
NSURL *url = context.URL;
[self.signIn handleURL:url];
[urls addObject:context.URL];
}
[self handleURLs:urls];
}

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ NS_ASSUME_NONNULL_BEGIN
googleServiceProperties:(nullable NSDictionary<NSString *, id> *)googleServiceProperties
NS_DESIGNATED_INITIALIZER;

#if TARGET_OS_IOS
/// Forwards each URL to GIDSignIn. Extracted so tests can cover scene URL
/// handling without constructing UIOpenURLContext.
- (void)handleURLs:(NSArray<NSURL *> *)urls NS_SWIFT_NAME(handleURLs(_:));
#endif

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,15 @@ - (void)allowSignInPermissions {
[[XCUIApplication alloc] initWithBundleIdentifier:@"com.apple.springboard"];
XCUIElement *permissionAlert = springboard.alerts.firstMatch;
if ([permissionAlert waitForExistenceWithTimeout:5.0]) {
[permissionAlert.buttons[@"Continue"] tap];
// The alert is titled in the simulator's language, so the confirmation
// button can't be looked up by name. It is the last of the alert's two
// buttons ("Cancel" and "Continue") in every localization.
XCUIElementQuery *alertButtons = permissionAlert.buttons;
NSUInteger buttonCount = alertButtons.count;
if (buttonCount == 0) {
XCTFail(@"Sign In permission alert has no buttons");
}
[[alertButtons elementBoundByIndex:buttonCount - 1] tap];
} else {
os_log(OS_LOG_DEFAULT, "Permission alert not detected, continuing.");
}
Expand Down
2 changes: 1 addition & 1 deletion packages/google_sign_in/google_sign_in_ios/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: google_sign_in_ios
description: iOS implementation of the google_sign_in plugin.
repository: https://github.com/flutter/packages/tree/main/packages/google_sign_in/google_sign_in_ios
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22
version: 6.3.0
version: 6.3.1

environment:
sdk: ^3.10.0
Expand Down
Loading