FBSDKLoginKit/FBSDKLoginKitTests/LoginManagerTests.swift (1,133 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. */ @testable import FBSDKLoginKit import TestTools import XCTest final class LoginManagerTests: XCTestCase { // swiftlint:disable implicitly_unwrapped_optional var claims: [String: Any]! var internalUtility: TestInternalUtility! var loginManager: LoginManager! var keychainStoreFactory: TestKeychainStoreFactory! var keychainStore: TestKeychainStore! var urlOpener: TestURLOpener! var settings: TestSettings! var loginCompleter: TestLoginCompleter! var loginCompleterFactory: TestLoginCompleterFactory! var testUser: Profile! var graphRequestFactory: TestGraphRequestFactory! // swiftlint:enable implicitly_unwrapped_optional let appID = "7391628439" let challenge = "a =bcdef" let nonce = "fedcb =a" let jti = "a jti is just any string" let header = [ "alg": "RS256", "typ": "JWT", "kid": "abcd1234", ] // @lint-ignore FBOBJCDISCOURAGEDFUNCTION let formatter = DateFormatter() let sampleURL = URL(string: "https://example.com")! // swiftlint:disable:this force_unwrapping override func setUp() { super.setUp() resetClassDependencies() formatter.dateFormat = "MM/dd/yyyy" ApplicationDelegate.shared.application( UIApplication.shared, didFinishLaunchingWithOptions: [:] ) internalUtility = TestInternalUtility() keychainStore = TestKeychainStore() keychainStoreFactory = TestKeychainStoreFactory() keychainStoreFactory.stubbedKeychainStore = keychainStore urlOpener = TestURLOpener() settings = TestSettings() settings.appID = appID loginCompleter = TestLoginCompleter() loginCompleterFactory = TestLoginCompleterFactory(stubbedLoginCompleter: loginCompleter) graphRequestFactory = TestGraphRequestFactory() loginManager = LoginManager( internalUtility: internalUtility, keychainStoreFactory: keychainStoreFactory, accessTokenWallet: TestAccessTokenWallet.self, authenticationToken: TestAuthenticationTokenWallet.self, profile: TestProfileProvider.self, urlOpener: urlOpener, settings: settings, loginCompleterFactory: loginCompleterFactory, graphRequestFactory: graphRequestFactory ) testUser = createProfile() TestProfileProvider.current = testUser AuthenticationToken.current = nil TestAccessTokenWallet.currentAccessToken = nil claims = createClaims() } func createProfile() -> Profile { Profile( userID: "1234", firstName: "Test", middleName: "Middle", lastName: "User", name: "Test User", linkURL: URL(string: "https://www.facebook.com"), refreshDate: nil, imageURL: URL(string: "https://www.facebook.com/some_picture"), email: "email@email.com", friendIDs: ["123", "456"], birthday: formatter.date(from: "01/01/1990"), ageRange: UserAgeRange(from: ["min": 21]), hometown: Location( from: [ "id": "112724962075996", "name": "Martinez, California", ] ), location: Location( from: [ "id": "110843418940484", "name": "Seattle, Washington", ] ), gender: "male", isLimited: false ) } func createClaims() -> [String: Any] { let currentTime = Date().timeIntervalSince1970 return [ "iss": "https://facebook.com/dialog/oauth", "aud": appID, "nonce": nonce, "exp": currentTime + 60 * 60 * 48, // 2 days later "iat": currentTime - 60, // 1 min ago "jti": jti, "sub": "1234", "name": "Test User", "given_name": "Test", "middle_name": "Middle", "family_name": "User", "email": "email@email.com", "picture": "https://www.facebook.com/some_picture", "user_friends": ["123", "456"], "user_birthday": "01/01/1990", "user_age_range": ["min": 21], "user_hometown": ["id": "112724962075996", "name": "Martinez, California"], "user_location": ["id": "110843418940484", "name": "Seattle, Washington"], "user_gender": "male", "user_link": "https://www.facebook.com", ] } override func tearDown() { claims = nil internalUtility = nil loginManager = nil keychainStoreFactory = nil keychainStore = nil urlOpener = nil settings = nil loginCompleter = nil loginCompleterFactory = nil resetClassDependencies() super.tearDown() } func resetClassDependencies() { Profile.reset() AccessToken.resetClassDependencies() AccessToken.resetCurrentAccessTokenCache() AuthenticationToken.resetCurrentAuthenticationTokenCache() } func testDefaultDependencies() { let loginManager = LoginManager() XCTAssertTrue(loginManager.internalUtility is InternalUtility) XCTAssertTrue(loginManager.keychainStore.self is KeychainStore) XCTAssertTrue(loginManager.accessTokenWallet is AccessToken.Type) XCTAssertTrue(loginManager.authenticationToken is AuthenticationToken.Type) XCTAssertTrue(loginManager.profile is Profile.Type) XCTAssertTrue(loginManager.urlOpener is BridgeAPI) XCTAssertTrue(loginManager.settings is Settings) XCTAssertTrue(loginManager.loginCompleterFactory is LoginCompleterFactory) } // MARK: Opening URL func testOpenURLUsesLoginCompleterFactory() throws { let url = try XCTUnwrap( URL(string: "fb7391628439://authorize/#granted_scopes=public_profile&denied_scopes=email%2Cuser_friends&signed_request=ggarbage.eyJhbGdvcml0aG0iOiJITUFDSEEyNTYiLCJjb2RlIjoid2h5bm90IiwiaXNzdWVkX2F0IjoxNDIyNTAyMDkyLCJ1c2VyX2lkIjoiMTIzIn0&access_token=sometoken&expires_in=5183949&state=%7B%22challenge%22%3A%22a%2520%253Dbcdef%22%7D") ) _ = loginManager.application(nil, open: url, sourceApplication: "com.apple.mobilesafari", annotation: nil) XCTAssertEqual( loginCompleterFactory.capturedAppID, settings.appID, "Should create a login completer using the expected app identifier" ) XCTAssertEqual( loginCompleterFactory.capturedURLParameters["access_token"] as? String, "sometoken", "Should create a login completer using the parameters parsed from the url" ) XCTAssertEqual( loginCompleterFactory.capturedURLParameters["denied_scopes"] as? String, "email,user_friends", "Should create a login completer using the parameters parsed from the url" ) XCTAssertEqual( loginCompleterFactory.capturedURLParameters["expires_in"] as? String, "5183949", "Should create a login completer using the parameters parsed from the url" ) XCTAssertEqual( loginCompleterFactory.capturedURLParameters["granted_scopes"] as? String, "public_profile", "Should create a login completer using the parameters parsed from the url" ) XCTAssertEqual( loginCompleterFactory.capturedURLParameters["signed_request"] as? String, "ggarbage.eyJhbGdvcml0aG0iOiJITUFDSEEyNTYiLCJjb2RlIjoid2h5bm90IiwiaXNzdWVkX2F0IjoxNDIyNTAyMDkyLCJ1c2VyX2lkIjoiMTIzIn0", // swiftlint:disable:this line_length "Should create a login completer using the parameters parsed from the url" ) XCTAssertEqual( loginCompleterFactory.capturedURLParameters["state"] as? String, #"{"challenge":"a%20%3Dbcdef"}"#, "Should create a login completer using the parameters parsed from the url" ) XCTAssertEqual( loginCompleterFactory.capturedURLParameters["user_id"] as? String, "123", "Should create a login completer using the parameters parsed from the url" ) XCTAssertTrue( loginCompleterFactory.capturedAuthenticationTokenCreator is AuthenticationTokenFactory, "Should create a login completer using the expected authentication token factory" ) } // MARK: Completing Authentication func testCompletingAuthenticationWithMixedPermissionsWithExpectedChallenge() throws { var capturedResult: LoginManagerLoginResult? loginManager.setRequestedPermissions(["email", "user_friends"]) loginManager.handler = { result, _ in capturedResult = result } _ = keychainStore.setString( challenge, forKey: "expected_login_challenge", accessibility: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly ) let parameters = LoginCompletionParameters() parameters.accessTokenString = "accessTokenString" parameters.challenge = challenge parameters.authenticationTokenString = "sometoken" parameters.permissions = FBPermission.permissions(fromRawPermissions: ["public_profile"]) parameters.declinedPermissions = FBPermission.permissions(fromRawPermissions: ["email", "user_friends"]) parameters.userID = "123" loginManager.completeAuthentication(parameters, expectChallenge: true) let result = try XCTUnwrap(capturedResult) XCTAssertFalse(result.isCancelled) let tokenAfterAuth = try XCTUnwrap(TestAccessTokenWallet.currentAccessToken) XCTAssertEqual( tokenAfterAuth.tokenString, "accessTokenString" ) XCTAssertEqual( tokenAfterAuth.userID, "123", "failed to parse userID" ) XCTAssertEqual( tokenAfterAuth.permissions, ["public_profile"], "unexpected permissions" ) XCTAssertEqual( result.grantedPermissions, ["public_profile"], "unexpected permissions" ) let expectedDeclined = ["email", "user_friends"] XCTAssertEqual( tokenAfterAuth.declinedPermissions, Set(expectedDeclined.map(Permission.init)), "unexpected permissions" ) XCTAssertEqual( result.declinedPermissions, Set(expectedDeclined), "unexpected permissions" ) XCTAssertNil(result.authenticationToken) XCTAssertNil(keychainStore.keychainDictionary["expected_login_challenge"]) XCTAssertTrue(keychainStore.wasStringForKeyCalled) } func testCompletingAuthenticationWithCancellation() { TestAccessTokenWallet.currentAccessToken = SampleAccessTokens.validToken var capturedResult: LoginManagerLoginResult? var capturedError: Error? loginManager.handler = { result, error in capturedResult = result capturedError = error } let parameters = LoginCompletionParameters() parameters.error = SampleError() loginManager.completeAuthentication(parameters, expectChallenge: true) XCTAssertEqual( TestAccessTokenWallet.currentAccessToken, SampleAccessTokens.validToken, "Handling a cancelled auth attempt should not affect the current access token" ) XCTAssertNil(capturedResult) XCTAssertNotNil(capturedError) } // verify basic case of first login and no declined permissions. func testCompletingAuthenticationWithoutDeclines() throws { let url = try XCTUnwrap( URL(string: "fb7391628439://authorize/#granted_scopes=public_profile&denied_scopes=&signed_request=ggarbage.eyJhbGdvcml0aG0iOiJITUFDSEEyNTYiLCJjb2RlIjoid2h5bm90IiwiaXNzdWVkX2F0IjoxNDIyNTAyMDkyLCJ1c2VyX2lkIjoiMTIzIn0&access_token=sometoken&expires_in=5183949") ) TestAccessTokenWallet.currentAccessToken = SampleAccessTokens.validToken _ = loginManager.application(nil, open: url, sourceApplication: "com.apple.mobilesafari", annotation: nil) let completerHandler = try XCTUnwrap(loginCompleter.capturedCompletionHandler) let parameters = LoginCompletionParameters() parameters.appID = appID parameters.permissions = FBPermission.permissions(fromRawPermissions: ["public_profile"]) parameters.declinedPermissions = [] parameters.accessTokenString = "sometoken" completerHandler(parameters) let actualToken = try XCTUnwrap(TestAccessTokenWallet.currentAccessToken) XCTAssertEqual(actualToken.userID, "user123", "failed to parse userID") XCTAssertEqual(actualToken.declinedPermissions, [], "unexpected permissions") } // verify that recentlyDeclined is a subset of requestedPermissions (i.e., other declined permissions are not in recentlyDeclined) func testCompletingAuthenticationWithRecentlyDeclinedPermissions() throws { var capturedResult: LoginManagerLoginResult? var capturedError: Error? loginManager.handler = { result, error in capturedResult = result capturedError = error } loginManager.setRequestedPermissions(["user_friends"]) _ = keychainStore.setString( challenge, forKey: "expected_login_challenge", accessibility: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly ) let parameters = LoginCompletionParameters() parameters.accessTokenString = "accessTokenString" parameters.challenge = challenge parameters.authenticationTokenString = "sometoken" parameters.permissions = FBPermission.permissions(fromRawPermissions: ["public_profile"]) parameters.declinedPermissions = FBPermission.permissions(fromRawPermissions: ["user_likes", "user_friends"]) loginManager.completeAuthentication(parameters, expectChallenge: true) let result = try XCTUnwrap(capturedResult) XCTAssertFalse(result.isCancelled) XCTAssertEqual(result.declinedPermissions, Set(["user_friends"])) XCTAssertEqual(result.grantedPermissions, Set(["public_profile"])) XCTAssertNil(keychainStore.keychainDictionary["expected_login_challenge"]) XCTAssertTrue(keychainStore.wasStringForKeyCalled) XCTAssertNil(capturedError) } func testCompletingAuthenticationWithoutGrantedPermissions() throws { var capturedResult: LoginManagerLoginResult? var capturedError: Error? loginManager.handler = { result, error in capturedResult = result capturedError = error } loginManager.setRequestedPermissions(["user_friends"]) _ = keychainStore.setString( challenge, forKey: "expected_login_challenge", accessibility: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly ) let parameters = LoginCompletionParameters() parameters.accessTokenString = "accessTokenString" parameters.challenge = challenge parameters.authenticationTokenString = "sometoken" parameters.declinedPermissions = FBPermission.permissions(fromRawPermissions: ["user_likes", "user_friends"]) loginManager.completeAuthentication(parameters, expectChallenge: true) let result = try XCTUnwrap(capturedResult) XCTAssertNil(result.token) XCTAssertNil(capturedError) XCTAssertNil(keychainStore.keychainDictionary["expected_login_challenge"]) XCTAssertTrue(keychainStore.wasStringForKeyCalled) } // verify that a reauth for already granted permissions is not treated as a cancellation. func testCompletingReauthenticationSamePermissionsIsNotCancelled() throws { let existingToken = SampleAccessTokens.create(withPermissions: ["public_profile", "read_stream"]) TestAccessTokenWallet.currentAccessToken = existingToken var capturedResult: LoginManagerLoginResult? var capturedError: Error? loginManager.handler = { result, error in capturedResult = result capturedError = error } loginManager.setRequestedPermissions(["public_profile", "read_stream"]) _ = keychainStore.setString( challenge, forKey: "expected_login_challenge", accessibility: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly ) let parameters = LoginCompletionParameters() parameters.accessTokenString = "accessTokenString" parameters.challenge = challenge parameters.authenticationTokenString = "sometoken" parameters.permissions = FBPermission.permissions(fromRawPermissions: ["public_profile", "read_stream"]) loginManager.completeAuthentication(parameters, expectChallenge: true) let capturedRequest = graphRequestFactory.capturedRequests.first XCTAssertEqual( capturedRequest?.graphPath, "me", "Should create a graph request with the expected graph path" ) capturedRequest?.capturedCompletionHandler?(nil, ["id": existingToken.userID], nil) let result = try XCTUnwrap(capturedResult) XCTAssertNil(capturedError) XCTAssertFalse(result.isCancelled) } func testCompletingAuthenticationWithBadChallenge() { // Sets challenge that will not be matched by the parameters _ = keychainStore.setString( challenge, forKey: "expected_login_challenge", accessibility: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly ) var capturedResult: LoginManagerLoginResult? var capturedError: Error? loginManager.handler = { result, error in capturedResult = result capturedError = error } loginManager.setRequestedPermissions(["email", "user_friends"]) let parameters = LoginCompletionParameters() parameters.accessTokenString = "accessTokenString" parameters.challenge = "someotherchallenge" parameters.authenticationTokenString = "sometoken" parameters.declinedPermissions = FBPermission.permissions(fromRawPermissions: ["user_likes", "user_friends"]) loginManager.completeAuthentication(parameters, expectChallenge: true) XCTAssertNotNil(capturedError) XCTAssertNil(capturedResult) } func testCompletingAuthenticationWithNoChallengeAndError() { var capturedResult: LoginManagerLoginResult? var capturedError: Error? loginManager.handler = { result, error in capturedResult = result capturedError = error } loginManager.setRequestedPermissions(["email", "user_friends"]) let parameters = LoginCompletionParameters() parameters.error = SampleError() loginManager.completeAuthentication(parameters, expectChallenge: true) XCTAssertNil(capturedResult) XCTAssertNotNil(capturedError) } func testOpenURLWithNonFacebookURL() throws { let url = try XCTUnwrap( URL(string: "test://test?granted_scopes=public_profile&access_token=sometoken&expires_in=5183949") ) loginManager.state = .performingLogin XCTAssertFalse( loginManager.application(nil, open: url, sourceApplication: "com.apple.mobilesafari", annotation: nil) ) XCTAssertNil(loginCompleter.capturedCompletionHandler) XCTAssertEqual( loginManager.state, .idle, "For verifying if handleImplicitCancelOfLogIn is being called we check if the state is in idle" ) } func testOpenURLAuthWithAuthenticationToken() throws { _ = keychainStore.setString( nonce, forKey: "expected_login_nonce", accessibility: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly ) _ = keychainStore.setString( challenge, forKey: "expected_login_challenge", accessibility: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly ) let rawClaims = try XCTUnwrap(claims) let claimsData = try JSONSerialization.data(withJSONObject: rawClaims, options: []) let encodedClaims = claimsData.base64EncodedData() let headerData = try JSONSerialization.data(withJSONObject: header, options: []) let encodedHeader = headerData.base64EncodedData() let tokenString = "\(encodedHeader).\(encodedClaims).signature" var capturedResult: LoginManagerLoginResult? var capturedError: Error? loginManager.handler = { result, error in capturedResult = result capturedError = error } loginManager.setRequestedPermissions(["email", "user_friends"]) let parameters = LoginCompletionParameters() parameters.authenticationTokenString = tokenString parameters.authenticationToken = AuthenticationToken(tokenString: tokenString, nonce: nonce) parameters.challenge = challenge parameters.profile = testUser parameters.permissions = FBPermission.permissions(fromRawPermissions: ["public_profile"]) loginManager.completeAuthentication(parameters, expectChallenge: true) XCTAssertNil(capturedError) let result = try XCTUnwrap(capturedResult) XCTAssertFalse(result.isCancelled) let token = try XCTUnwrap(result.authenticationToken) validate(authenticationToken: token, expectedTokenString: tokenString) XCTAssertNil(keychainStore.keychainDictionary["expected_login_challenge"]) XCTAssertTrue(keychainStore.wasStringForKeyCalled) let profile = try XCTUnwrap(TestProfileProvider.current) try validate(profile: profile) XCTAssertNil(result.token) } func testCompletingAuthenticationWithAuthenticationTokenWithAccessToken() throws { _ = keychainStore.setString( nonce, forKey: "expected_login_nonce", accessibility: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly ) _ = keychainStore.setString( challenge, forKey: "expected_login_challenge", accessibility: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly ) let rawClaims = try XCTUnwrap(claims) let claimsData = try JSONSerialization.data(withJSONObject: rawClaims, options: []) let encodedClaims = claimsData.base64EncodedData() let headerData = try JSONSerialization.data(withJSONObject: header, options: []) let encodedHeader = headerData.base64EncodedData() let tokenString = "\(encodedHeader).\(encodedClaims).signature" var capturedResult: LoginManagerLoginResult? var capturedError: Error? loginManager.handler = { result, error in capturedResult = result capturedError = error } loginManager.setRequestedPermissions(["email", "user_friends"]) let parameters = LoginCompletionParameters() parameters.authenticationTokenString = tokenString parameters.authenticationToken = AuthenticationToken(tokenString: tokenString, nonce: nonce) parameters.challenge = challenge parameters.profile = testUser parameters.permissions = FBPermission.permissions(fromRawPermissions: ["public_profile"]) loginManager.completeAuthentication(parameters, expectChallenge: true) let result = try XCTUnwrap(capturedResult) XCTAssertFalse(result.isCancelled) let token = try XCTUnwrap(result.authenticationToken) validate(authenticationToken: token, expectedTokenString: tokenString) XCTAssertNil(keychainStore.keychainDictionary["expected_login_challenge"]) XCTAssertTrue(keychainStore.wasStringForKeyCalled) let profile = try XCTUnwrap(TestProfileProvider.current) try validate(profile: profile) XCTAssertNil(capturedError) } func testApplicationDidBecomeActiveWhileLogin() { loginManager.state = .performingLogin loginManager.applicationDidBecomeActive(UIApplication.shared) XCTAssertEqual(loginManager.state, .idle) } func testIsAuthenticationURL() { XCTAssertFalse( loginManager.isAuthenticationURL( URL(string: "https://www.facebook.com/some/test/url")! // swiftlint:disable:this force_unwrapping ) ) XCTAssertTrue( loginManager.isAuthenticationURL( URL(string: "https://www.facebook.com/v9.0/dialog/oauth/?test=test")! // swiftlint:disable:this force_unwrapping ) ) XCTAssertFalse( loginManager.isAuthenticationURL( URL(string: "123")! // swiftlint:disable:this force_unwrapping ) ) } func testShouldStopPropagationOfURL() { var url = URL(string: "fb\(appID)://no-op/test/")! // swiftlint:disable:this force_unwrapping XCTAssertTrue(loginManager.shouldStopPropagation(of: url)) url = URL(string: "fb\(appID)://")! // swiftlint:disable:this force_unwrapping XCTAssertFalse(loginManager.shouldStopPropagation(of: url)) url = URL(string: "https://no-op/")! // swiftlint:disable:this force_unwrapping XCTAssertFalse(loginManager.shouldStopPropagation(of: url)) } func testLoginWithSFVC() { internalUtility.stubbedAppURL = sampleURL internalUtility.stubbedFacebookURL = sampleURL loginManager.logIn(withPermissions: ["public_profile"], from: UIViewController()) { _, _ in } XCTAssertTrue( urlOpener.wasOpenURLWithSVCCalled, "openURLWithSafariViewController should be called" ) XCTAssertFalse( urlOpener.wasOpenURLWithoutSVCCalled, "openURL should not be called" ) XCTAssertTrue( loginManager.usedSFAuthSession, "If useSafariViewController is YES, _usedSFAuthSession should be YES and openURLWithSafariViewController should be invoked" // swiftlint:disable:this line_length ) XCTAssertNotNil(urlOpener.viewController) } func testCallingLoginWithStateChange() { internalUtility.isFacebookAppInstalled = false loginManager.usedSFAuthSession = false loginManager.state = .start var didInvokeCompletionSynchronously = false loginManager.logIn(withPermissions: ["public_profile"], from: UIViewController()) { _, _ in didInvokeCompletionSynchronously = true } XCTAssertFalse(didInvokeCompletionSynchronously) } // MARK: Login Parameters func testLoginTrackingEnabledLoginParams() throws { let configuration = LoginConfiguration( permissions: ["public_profile", "email"], tracking: .enabled ) let logger = try XCTUnwrap( LoginManagerLogger( loggingToken: "123", tracking: .enabled ) ) internalUtility.stubbedAppURL = sampleURL let parameters = try XCTUnwrap( loginManager.logInParameters( with: configuration, loggingToken: "", logger: logger, authMethod: "sfvc_auth" ) ) try validateCommonLoginParameters(parameters) XCTAssertEqual( parameters["response_type"], "id_token,token_or_nonce,signed_request,graph_domain" ) let scopes = parameters["scope"]? .split(separator: ",") .sorted() .joined(separator: ",") XCTAssertEqual( scopes, "email,openid,public_profile" ) XCTAssertNotNil(parameters["nonce"]) XCTAssertNil(parameters["tp"], "Regular login should not send a tracking parameter") let rawState = try XCTUnwrap(parameters["state"]) let state = try BasicUtility.object(forJSONString: rawState) as? [String: Any] XCTAssertEqual( state?["3_method"] as? String, "sfvc_auth" ) XCTAssertEqual( parameters["auth_type"], LoginAuthType.rerequest.rawValue ) XCTAssertNotNil(parameters["code_challenge"]) XCTAssertEqual(parameters["code_challenge_method"], "S256") } func testLoginTrackingLimitedLoginParams() throws { let configuration = LoginConfiguration( permissions: ["public_profile", "email"], tracking: .limited, nonce: "some_nonce" ) internalUtility.stubbedAppURL = sampleURL let parameters = try XCTUnwrap( loginManager.logInParameters( with: configuration, loggingToken: "", logger: nil, authMethod: "browser_auth" ) ) try validateCommonLoginParameters(parameters) XCTAssertEqual( parameters["response_type"], "id_token,graph_domain" ) let scopes = parameters["scope"]? .split(separator: ",") .sorted() .joined(separator: ",") XCTAssertEqual( scopes, "email,openid,public_profile" ) XCTAssertEqual( parameters["nonce"], "some_nonce" ) XCTAssertEqual( parameters["tp"], "ios_14_do_not_track" ) let rawState = try XCTUnwrap(parameters["state"]) let state = try BasicUtility.object(forJSONString: rawState) as? [String: Any] XCTAssertEqual( state?["3_method"] as? String, "browser_auth" ) XCTAssertEqual( parameters["auth_type"], LoginAuthType.rerequest.rawValue ) XCTAssertNil(parameters["code_challenge"]) XCTAssertNil(parameters["code_challenge_method"]) } func testLoginParamsWithNilConfiguration() { var capturedResult: LoginManagerLoginResult? var capturedError: Error? loginManager.handler = { result, error in capturedResult = result capturedError = error } let parameters = loginManager.logInParameters( with: nil, loggingToken: nil, logger: nil, authMethod: "sfvc_auth" ) XCTAssertNil(capturedResult) XCTAssertNotNil(capturedError) XCTAssertNil(parameters) } func testLoginParamsWithNilAuthType() throws { let configuration = LoginConfiguration( permissions: ["public_profile", "email"], tracking: .enabled, messengerPageId: nil, authType: nil ) let logger = LoginManagerLogger(loggingToken: "123", tracking: .enabled) internalUtility.stubbedAppURL = sampleURL let parameters = try XCTUnwrap( loginManager.logInParameters( with: configuration, loggingToken: nil, logger: logger, authMethod: "sfvc_auth" ) ) try validateCommonLoginParameters(parameters) XCTAssertEqual( parameters["response_type"], "id_token,token_or_nonce,signed_request,graph_domain" ) let scopes = parameters["scope"]? .split(separator: ",") .sorted() .joined(separator: ",") XCTAssertEqual( scopes, "email,openid,public_profile" ) XCTAssertNotNil(parameters["nonce"]) XCTAssertNil( parameters["tp"], "Regular login should not send a tracking parameter" ) let rawState = try XCTUnwrap(parameters["state"]) let state = try BasicUtility.object(forJSONString: rawState) as? [String: Any] XCTAssertEqual(state?["3_method"] as? String, "sfvc_auth") XCTAssertNil(parameters["auth_type"]) } func testLoginParamsWithExplicitlySetAuthType() throws { let configuration = LoginConfiguration( permissions: ["public_profile", "email"], tracking: .enabled, messengerPageId: nil, authType: .reauthorize ) let logger = LoginManagerLogger(loggingToken: "123", tracking: .enabled) internalUtility.stubbedAppURL = sampleURL let parameters = try XCTUnwrap( loginManager.logInParameters( with: configuration, loggingToken: nil, logger: logger, authMethod: "sfvc_auth" ) ) try validateCommonLoginParameters(parameters) XCTAssertEqual( parameters["response_type"], "id_token,token_or_nonce,signed_request,graph_domain" ) let scopes = parameters["scope"]? .split(separator: ",") .sorted() .joined(separator: ",") XCTAssertEqual( scopes, "email,openid,public_profile" ) XCTAssertNotNil(parameters["nonce"]) XCTAssertNil(parameters["tp"], "Regular login should not send a tracking parameter") let rawState = try XCTUnwrap(parameters["state"]) let state = try BasicUtility.object(forJSONString: rawState) as? [String: Any] XCTAssertEqual(state?["3_method"] as? String, "sfvc_auth") XCTAssertEqual( parameters["auth_type"], LoginAuthType.reauthorize.rawValue ) } func testLogInParametersFromNonAuthenticationURL() throws { let url = try XCTUnwrap( URL(string: "myapp://somelink/?al_applink_data=%7B%22target_url%22%3Anull%2C%22extras%22%3A%7B%22fb_login%22%3A%22%7B%5C%22granted_scopes%5C%22%3A%5C%22public_profile%5C%22%2C%5C%22denied_scopes%5C%22%3A%5C%22%5C%22%2C%5C%22signed_request%5C%22%3A%5C%22ggarbage.eyJhbGdvcml0aG0iOiJITUFDSEEyNTYiLCJjb2RlIjoid2h5bm90IiwiaXNzdWVkX2F0IjoxNDIyNTAyMDkyLCJ1c2VyX2lkIjoiMTIzIn0%5C%22%2C%5C%22nonce%5C%22%3A%5C%22someNonce%5C%22%2C%5C%22data_access_expiration_time%5C%22%3A%5C%221607374566%5C%22%2C%5C%22expires_in%5C%22%3A%5C%225183401%5C%22%7D%22%7D%2C%22referer_app_link%22%3A%7B%22url%22%3A%22fb%3A%5C%2F%5C%2F%5C%2F%22%2C%22app_name%22%3A%22Facebook%22%7D%7D") ) let parameters = try XCTUnwrap(loginManager.logInParameters(from: url)) XCTAssertEqual(parameters["nonce"], "someNonce") XCTAssertEqual(parameters["granted_scopes"], "public_profile") XCTAssertEqual(parameters["denied_scopes"], "") } // MARK: Logout func testLogout() { TestAccessTokenWallet.currentAccessToken = SampleAccessTokens.validToken TestAuthenticationTokenWallet.currentAuthenticationToken = SampleAuthenticationToken.validToken TestProfileProvider.current = testUser loginManager.logOut() XCTAssertNil(TestAccessTokenWallet.currentAccessToken) XCTAssertNil(TestAuthenticationTokenWallet.currentAuthenticationToken) XCTAssertNil(TestProfileProvider.current) } // MARK: Keychain Store func testStoreExpectedNonce() { loginManager.storeExpectedNonce("some_nonce") XCTAssertEqual(loginManager.keychainStore.string(forKey: "expected_login_nonce"), "some_nonce") loginManager.storeExpectedNonce(nil) XCTAssertNil(loginManager.keychainStore.string(forKey: "expected_login_nonce")) } // MARK: Reauthorization func testReauthorizingWithoutAccessToken() { var capturedResult: LoginManagerLoginResult? var capturedError: Error? loginManager.reauthorizeDataAccess(from: UIViewController()) { result, error in capturedResult = result capturedError = error } let error = capturedError as NSError? XCTAssertNil(capturedResult, "Should not have a result when reauthorizing without a current access token") XCTAssertEqual(error?.domain, LoginErrorDomain) XCTAssertEqual(error?.code, LoginError.missingAccessToken.rawValue) } func testReauthorizingWithAccessToken() { TestAccessTokenWallet.currentAccessToken = SampleAccessTokens.validToken XCTAssertNil(loginManager.configuration) loginManager.reauthorizeDataAccess(from: UIViewController()) { _, _ in } XCTAssertNotNil( loginManager.configuration, """ Reauthorizing data access for an available access token should log in. We are using the existence of the configuration created during the login flow as a proxy that this happened. """ ) XCTAssertEqual(loginManager.configuration?.tracking, .enabled) XCTAssertEqual(loginManager.configuration?.requestedPermissions, []) XCTAssertNotNil(loginManager.configuration?.nonce) } func testReauthorizingWithInvalidStartState() { loginManager.state = .start loginManager.reauthorizeDataAccess(from: UIViewController()) { _, _ in XCTFail("Should not actually reauthorize and call the handler in this test") } XCTAssertTrue(graphRequestFactory.capturedRequests.isEmpty) XCTAssertFalse(loginManager.state == .idle) } // MARK: Permissions func testRecentlyGrantedPermissionsWithoutPreviouslyGrantedOrRequestedPermissions() throws { let grantedPermissions = try XCTUnwrap( FBPermission.permissions(fromRawPermissions: ["email", "user_friends"]) ) let recentlyGrantedPermissions = loginManager.recentlyGrantedPermissions(fromGrantedPermissions: grantedPermissions) XCTAssertEqual(recentlyGrantedPermissions, grantedPermissions) } func testRecentlyGrantedPermissionsWithPreviouslyGrantedPermissions() throws { let grantedPermissions = try XCTUnwrap( FBPermission.permissions(fromRawPermissions: ["email", "user_friends"]) ) TestAccessTokenWallet.currentAccessToken = SampleAccessTokens.validToken let recentlyGrantedPermissions = loginManager.recentlyGrantedPermissions(fromGrantedPermissions: grantedPermissions) XCTAssertEqual(recentlyGrantedPermissions, grantedPermissions) } func testRecentlyGrantedPermissionsWithRequestedPermissions() throws { TestAccessTokenWallet.currentAccessToken = SampleAccessTokens.create(withPermissions: []) let grantedPermissions = try XCTUnwrap( FBPermission.permissions(fromRawPermissions: ["email", "user_friends"]) ) loginManager.setRequestedPermissions(["user_friends"]) let recentlyGrantedPermissions = loginManager.recentlyGrantedPermissions(fromGrantedPermissions: grantedPermissions) XCTAssertEqual(recentlyGrantedPermissions, grantedPermissions) } func testRecentlyGrantedPermissionsWithPreviouslyGrantedAndRequestedPermissions() throws { TestAccessTokenWallet.currentAccessToken = SampleAccessTokens.create(withPermissions: ["email"]) let grantedPermissions = try XCTUnwrap( FBPermission.permissions(fromRawPermissions: ["email", "user_friends"]) ) loginManager.setRequestedPermissions(["user_friends"]) let recentlyGrantedPermissions = loginManager.recentlyGrantedPermissions(fromGrantedPermissions: grantedPermissions) let expectedPermissions = try XCTUnwrap( FBPermission.permissions(fromRawPermissions: ["user_friends"]) ) XCTAssertEqual(recentlyGrantedPermissions, expectedPermissions) } func testRecentlyDeclinedPermissionsWithoutRequestedPermissions() throws { let declinedPermissions = try XCTUnwrap( FBPermission.permissions(fromRawPermissions: ["email", "user_friends"]) ) let recentlyDeclinedPermissions = loginManager.recentlyDeclinedPermissions( fromDeclinedPermissions: declinedPermissions ) XCTAssertTrue(recentlyDeclinedPermissions.isEmpty) } func testRecentlyDeclinedPermissionsWithRequestedPermissions() throws { let declinedPermissions = try XCTUnwrap( FBPermission.permissions(fromRawPermissions: ["email", "user_friends"]) ) loginManager.setRequestedPermissions(["user_friends"]) let recentlyDeclinedPermissions = loginManager.recentlyDeclinedPermissions( fromDeclinedPermissions: declinedPermissions ) let expectedPermissions = try XCTUnwrap( FBPermission.permissions(fromRawPermissions: ["user_friends"]) ) XCTAssertEqual(recentlyDeclinedPermissions, expectedPermissions) } // MARK: Reauthentication func testValidateReauthenticationGraphRequestCreation() { let result = LoginManagerLoginResult( token: SampleAccessTokens.validToken, authenticationToken: nil, isCancelled: false, grantedPermissions: [], declinedPermissions: [] ) loginManager.validateReauthentication(accessToken: SampleAccessTokens.validToken, result: result) let capturedRequest = graphRequestFactory.capturedRequests.first XCTAssertEqual( capturedRequest?.graphPath, "me", "Should create a graph request with the expected graph path" ) XCTAssertEqual( capturedRequest?.tokenString, SampleAccessTokens.validToken.tokenString, "Should create a graph request with the expected access token string" ) XCTAssertEqual( capturedRequest?.flags, [.doNotInvalidateTokenOnError, .disableErrorRecovery], "The graph request should not invalidate the token on error or disable error recovery" ) } func testValidateReauthenticationCompletionWithError() { let result = LoginManagerLoginResult( token: SampleAccessTokens.validToken, authenticationToken: nil, isCancelled: false, grantedPermissions: [], declinedPermissions: [] ) var capturedResult: LoginManagerLoginResult? var capturedError: Error? loginManager.handler = { result, error in capturedResult = result capturedError = error } loginManager.validateReauthentication(accessToken: SampleAccessTokens.validToken, result: result) graphRequestFactory.capturedRequests.first?.capturedCompletionHandler?(nil, nil, SampleError()) XCTAssertNotNil(capturedError) XCTAssertNil(capturedResult) } func testValidateReauthenticationCompletionWithMatchingUserID() { let result = LoginManagerLoginResult( token: SampleAccessTokens.validToken, authenticationToken: nil, isCancelled: false, grantedPermissions: [], declinedPermissions: [] ) var capturedResult: LoginManagerLoginResult? var capturedError: Error? loginManager.handler = { result, error in capturedResult = result capturedError = error } loginManager.validateReauthentication(accessToken: SampleAccessTokens.validToken, result: result) graphRequestFactory.capturedRequests.first?.capturedCompletionHandler?( nil, ["id": SampleAccessTokens.validToken.userID], nil ) XCTAssertNil(capturedError) XCTAssertEqual(capturedResult, result) } func testValidateReauthenticationCompletionWithMismatchedUserID() { let result = LoginManagerLoginResult( token: SampleAccessTokens.validToken, authenticationToken: nil, isCancelled: false, grantedPermissions: [], declinedPermissions: [] ) var capturedResult: LoginManagerLoginResult? var capturedError: Error? loginManager.handler = { result, error in capturedResult = result capturedError = error } loginManager.validateReauthentication(accessToken: SampleAccessTokens.validToken, result: result) graphRequestFactory.capturedRequests.first?.capturedCompletionHandler?(nil, ["id": "456"], nil) XCTAssertNotNil(capturedError) XCTAssertNil(capturedResult) } // MARK: isPerformingLogin func testIsPerformingLoginWhenIdle() { loginManager.state = .idle XCTAssertFalse(loginManager.isPerformingLogin) } func testIsPerformingLoginWhenStarted() { loginManager.state = .start XCTAssertFalse(loginManager.isPerformingLogin) } func testIsPerformingLoginWhenPerformingLogin() { loginManager.state = .performingLogin XCTAssertTrue(loginManager.isPerformingLogin) } // MARK: - Helpers func validate( authenticationToken: AuthenticationToken, expectedTokenString: String ) { XCTAssertNotNil(authenticationToken, "An Authentication token should be created after successful login") XCTAssertEqual( authenticationToken.tokenString, expectedTokenString, "A raw authentication token string should be stored" ) XCTAssertEqual( authenticationToken.nonce, nonce, "The nonce claims in the authentication token should be stored" ) } func validate(profile: Profile) throws { XCTAssertNotNil(profile, "user profile should be updated") XCTAssertEqual( profile.name, claims["name"] as? String, "failed to parse user name" ) XCTAssertEqual( profile.firstName, claims["given_name"] as? String, "failed to parse user first name" ) XCTAssertEqual( profile.middleName, claims["middle_name"] as? String, "failed to parse user middle name" ) XCTAssertEqual( profile.lastName, claims["family_name"] as? String, "failed to parse user last name" ) XCTAssertEqual( profile.userID, claims["sub"] as? String, "failed to parse userID" ) XCTAssertEqual( profile.imageURL?.absoluteString, claims["picture"] as? String, "failed to parse user profile picture" ) XCTAssertEqual( profile.email, claims["email"] as? String, "failed to parse user email" ) XCTAssertEqual( profile.friendIDs, claims["user_friends"] as? [String], "failed to parse user friends" ) // @lint-ignore FBOBJCDISCOURAGEDFUNCTION let formatter = DateFormatter() formatter.dateFormat = "MM/dd/yyyy" let birthday = try XCTUnwrap(profile.birthday) XCTAssertEqual( formatter.string(from: birthday), claims["user_birthday"] as? String, "failed to parse user birthday" ) let ageRange = try XCTUnwrap(claims["user_age_range"] as? [String: NSNumber]) XCTAssertEqual( profile.ageRange, UserAgeRange(from: ageRange), "failed to parse user age range" ) let hometown = try XCTUnwrap(claims["user_hometown"] as? [String: String]) XCTAssertEqual( profile.hometown, Location(from: hometown), "failed to parse user hometown" ) let location = try XCTUnwrap(claims["user_location"] as? [String: String]) XCTAssertEqual( profile.location, Location(from: location), "failed to parse user location" ) let gender = try XCTUnwrap(claims["user_gender"] as? String) XCTAssertEqual( profile.gender, gender, "failed to parse user gender" ) let rawLink = try XCTUnwrap(claims["user_link"] as? String) let link = try XCTUnwrap(URL(string: rawLink)) XCTAssertEqual( profile.linkURL, link, "failed to parse user link" ) } func validateCommonLoginParameters( _ parameters: [String: String], file: StaticString = #file, line: UInt = #line ) throws { XCTAssertEqual( parameters["client_id"], appID, file: file, line: line ) XCTAssertEqual( parameters["display"], "touch", file: file, line: line ) XCTAssertEqual( parameters["sdk"], "ios", file: file, line: line ) XCTAssertEqual( parameters["return_scopes"], "true", file: file, line: line ) XCTAssertEqual( parameters["fbapp_pres"], "0", file: file, line: line ) XCTAssertEqual( parameters["ies"], settings.isAutoLogAppEventsEnabled ? "1" : "0", file: file, line: line ) XCTAssertNotNil( parameters["e2e"], file: file, line: line ) let stateJsonString = try XCTUnwrap(parameters["state"], file: file, line: line) let state = try BasicUtility.object(forJSONString: stateJsonString) as? [String: Any] XCTAssertNotNil(state?["challenge"], file: file, line: line) XCTAssertNotNil(state?["0_auth_logger_id"], file: file, line: line) let cbt = try XCTUnwrap(parameters["cbt"], file: file, line: line) let cbtDouble = try XCTUnwrap(Double(cbt), file: file, line: line) let currentMilliseconds = 1000 * Date().timeIntervalSince1970 XCTAssertEqual(cbtDouble, currentMilliseconds, accuracy: 500, file: file, line: line) XCTAssertEqual( parameters["redirect_uri"], sampleURL.absoluteString, file: file, line: line ) } }