FBSDKCoreKit/FBSDKCoreKitTests/Internal/InternalUtilityTests.swift (1,027 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 FBSDKCoreKit import TestTools import XCTest final class InternalUtilityTests: XCTestCase { let validParameters = ["foo": "bar"] let validPath = "example" let facebookUrlSchemeMissingMessage = "fbapi is missing from your Info.plist under LSApplicationQueriesSchemes and is required." // swiftlint:disable:this line_length let messengerUrlSchemeMissingMessage = "fb-messenger-share-api is missing from your Info.plist under LSApplicationQueriesSchemes and is required." // swiftlint:disable:this line_length let dialogURL = URL( string: "https://m.facebook.com/\(FBSDK_DEFAULT_GRAPH_API_VERSION)/dialog" )! // swiftlint:disable:this force_unwrapping let nonDialogURL = URL( string: "https://m.facebook.com/\(FBSDK_DEFAULT_GRAPH_API_VERSION)/foo" )! // swiftlint:disable:this force_unwrapping // swiftlint:disable implicitly_unwrapped_optional var internalUtility: InternalUtility! var bundle: TestBundle! var loggerFactory: TestLoggerFactory! var logger: TestLogger! var settings: TestSettings! var errorFactory: TestErrorFactory! // swiftlint:enable implicitly_unwrapped_optional force_unwrapping override func setUp() { super.setUp() InternalUtility.reset() bundle = TestBundle() loggerFactory = TestLoggerFactory() logger = TestLogger(loggingBehavior: .developerErrors) loggerFactory.logger = logger settings = TestSettings() errorFactory = TestErrorFactory() internalUtility = InternalUtility() internalUtility.deleteFacebookCookies() configureInternalUtility() } override func tearDown() { InternalUtility.reset() bundle = nil loggerFactory = nil logger = nil settings = nil errorFactory = nil internalUtility = nil super.tearDown() } func configureInternalUtility() { internalUtility.configure( withInfoDictionaryProvider: bundle, loggerFactory: loggerFactory, settings: settings, errorFactory: errorFactory ) } func testDefaultDependencies() { internalUtility = InternalUtility() XCTAssertNil( internalUtility.infoDictionaryProvider, "Should not have an info dictionary provider by default" ) XCTAssertNil( internalUtility.loggerFactory, "Should not have a logger factory by default" ) XCTAssertNil( internalUtility.settings, "Should not have settings by default" ) XCTAssertNil( internalUtility.errorFactory, "Should not have an error factory by default" ) } func testConfiguringWithDependencies() { XCTAssertTrue( internalUtility.infoDictionaryProvider === bundle, "Should be able to provide an info dictionary provider" ) XCTAssertTrue( internalUtility.loggerFactory === loggerFactory, "The shared instance should use the provided logger factory" ) XCTAssertTrue( internalUtility.settings === settings, "The shared instance should use the provided settings" ) XCTAssertIdentical( internalUtility.errorFactory, errorFactory, "The shared instance should use the provided error factory" ) } func testFacebookURL() throws { settings.facebookDomainPart = "" var urlString = "" urlString = try internalUtility.facebookURL( withHostPrefix: "", path: "", queryParameters: [:] ).absoluteString XCTAssertEqual(urlString, "https://facebook.com/\(FBSDK_DEFAULT_GRAPH_API_VERSION)") urlString = try internalUtility.facebookURL( withHostPrefix: "m.", path: "", queryParameters: [:] ).absoluteString XCTAssertEqual(urlString, "https://m.facebook.com/\(FBSDK_DEFAULT_GRAPH_API_VERSION)") urlString = try internalUtility.facebookURL( withHostPrefix: "m", path: "", queryParameters: [:] ).absoluteString XCTAssertEqual(urlString, "https://m.facebook.com/\(FBSDK_DEFAULT_GRAPH_API_VERSION)") urlString = try internalUtility.facebookURL( withHostPrefix: "m", path: "/dialog/share", queryParameters: [:] ).absoluteString XCTAssertEqual(urlString, "https://m.facebook.com/\(FBSDK_DEFAULT_GRAPH_API_VERSION)/dialog/share") urlString = try internalUtility.facebookURL( withHostPrefix: "m", path: "dialog/share", queryParameters: [:] ).absoluteString XCTAssertEqual(urlString, "https://m.facebook.com/\(FBSDK_DEFAULT_GRAPH_API_VERSION)/dialog/share") urlString = try internalUtility.facebookURL( withHostPrefix: "m", path: "dialog/share", queryParameters: ["key": "value"] ).absoluteString XCTAssertEqual( urlString, "https://m.facebook.com/\(FBSDK_DEFAULT_GRAPH_API_VERSION)/dialog/share?key=value" ) urlString = try internalUtility.facebookURL( withHostPrefix: "m", path: "/v1.0/dialog/share", queryParameters: [:] ).absoluteString XCTAssertEqual(urlString, "https://m.facebook.com/v1.0/dialog/share") urlString = try internalUtility.facebookURL( withHostPrefix: "m", path: "/dialog/share", queryParameters: [:], defaultVersion: "v2.0" ).absoluteString XCTAssertEqual(urlString, "https://m.facebook.com/v2.0/dialog/share") urlString = try internalUtility.facebookURL( withHostPrefix: "m", path: "/v1.0/dialog/share", queryParameters: [:], defaultVersion: "v2.0" ).absoluteString XCTAssertEqual(urlString, "https://m.facebook.com/v1.0/dialog/share") urlString = try internalUtility.facebookURL( withHostPrefix: "m", path: "/v987654321.2/dialog/share", queryParameters: [:] ).absoluteString XCTAssertEqual(urlString, "https://m.facebook.com/v987654321.2/dialog/share") urlString = try internalUtility.facebookURL( withHostPrefix: "m", path: "/v.1/dialog/share", queryParameters: [:], defaultVersion: "v2.0" ).absoluteString XCTAssertEqual(urlString, "https://m.facebook.com/v2.0/v.1/dialog/share") urlString = try internalUtility.facebookURL( withHostPrefix: "m", path: "/v1/dialog/share", queryParameters: [:], defaultVersion: "v2.0" ).absoluteString XCTAssertEqual(urlString, "https://m.facebook.com/v2.0/v1/dialog/share") settings.graphAPIVersion = "v3.3" urlString = try internalUtility.facebookURL( withHostPrefix: "m", path: "/v1/dialog/share", queryParameters: [:], defaultVersion: "" ).absoluteString XCTAssertEqual(urlString, "https://m.facebook.com/v3.3/v1/dialog/share") settings.graphAPIVersion = FBSDK_DEFAULT_GRAPH_API_VERSION urlString = try internalUtility.facebookURL( withHostPrefix: "m", path: "/dialog/share", queryParameters: [:], defaultVersion: "" ).absoluteString XCTAssertEqual(urlString, "https://m.facebook.com/\(FBSDK_DEFAULT_GRAPH_API_VERSION)/dialog/share") } func testFacebookGamingURL() throws { settings.facebookDomainPart = "" let authToken = AuthenticationToken( tokenString: "token_string", nonce: "nonce", graphDomain: "gaming" ) AuthenticationToken.current = authToken var urlString = try internalUtility.facebookURL( withHostPrefix: "graph", path: "", queryParameters: [:] ).absoluteString XCTAssertEqual(urlString, "https://graph.fb.gg/\(FBSDK_DEFAULT_GRAPH_API_VERSION)") urlString = try internalUtility.facebookURL( withHostPrefix: "graph-video", path: "", queryParameters: [:] ).absoluteString XCTAssertEqual(urlString, "https://graph-video.fb.gg/\(FBSDK_DEFAULT_GRAPH_API_VERSION)") } // MARK: - Extracting Permissions func testParsingPermissionsWithFuzzyValues() { // A lack of a runtime crash is considered a success here. (1 ... 100).forEach { _ in internalUtility.extractPermissions( fromResponse: SampleRawRemotePermissionList.randomValues, grantedPermissions: [], declinedPermissions: [], expiredPermissions: [] ) } } func testExtractingPermissionsFromResponseWithInvalidTopLevelKey() { let grantedPermissions = NSMutableSet() let declinedPermissions = NSMutableSet() let expiredPermissions = NSMutableSet() internalUtility.extractPermissions( fromResponse: SampleRawRemotePermissionList.missingTopLevelKey, grantedPermissions: grantedPermissions, declinedPermissions: declinedPermissions, expiredPermissions: expiredPermissions ) XCTAssertEqual(grantedPermissions.count, 0, "Should not add granted permissions if top level key is missing") XCTAssertEqual(declinedPermissions.count, 0, "Should not add declined permissions if top level key is missing") XCTAssertEqual(expiredPermissions.count, 0, "Should not add expired permissions if top level key is missing") } func testExtractingPermissionsFromResponseWithMissingPermissions() { let grantedPermissions = NSMutableSet() let declinedPermissions = NSMutableSet() let expiredPermissions = NSMutableSet() internalUtility.extractPermissions( fromResponse: SampleRawRemotePermissionList.missingPermissions, grantedPermissions: grantedPermissions, declinedPermissions: declinedPermissions, expiredPermissions: expiredPermissions ) XCTAssertEqual(grantedPermissions.count, 0, "Should not add missing granted permissions") XCTAssertEqual(declinedPermissions.count, 0, "Should not add missing declined permissions") XCTAssertEqual(expiredPermissions.count, 0, "Should not add missing expired permissions") } func testExtractingPermissionsFromResponseWithMissingStatus() { let grantedPermissions = NSMutableSet() let declinedPermissions = NSMutableSet() let expiredPermissions = NSMutableSet() internalUtility.extractPermissions( fromResponse: SampleRawRemotePermissionList.missingStatus, grantedPermissions: grantedPermissions, declinedPermissions: declinedPermissions, expiredPermissions: expiredPermissions ) XCTAssertEqual(grantedPermissions.count, 0, "Should not add a permission with a missing status") XCTAssertEqual(declinedPermissions.count, 0, "Should not add a permission with a missing status") XCTAssertEqual(expiredPermissions.count, 0, "Should not add a permission with a missing status") } func testExtractingPermissionsFromResponseWithValidPermissions() { let grantedPermissions = NSMutableSet() let declinedPermissions = NSMutableSet() let expiredPermissions = NSMutableSet() internalUtility.extractPermissions( fromResponse: SampleRawRemotePermissionList.validAllStatuses, grantedPermissions: grantedPermissions, declinedPermissions: declinedPermissions, expiredPermissions: expiredPermissions ) XCTAssertEqual(grantedPermissions.count, 1, "Should add granted permissions when available") XCTAssertTrue(grantedPermissions.contains("email"), "Should add the correct permission to granted permissions") XCTAssertEqual(declinedPermissions.count, 1, "Should add declined permissions when available") XCTAssertTrue(declinedPermissions.contains("birthday"), "Should add the correct permission to declined permissions") XCTAssertEqual(expiredPermissions.count, 1, "Should add expired permissions when available") XCTAssertTrue(expiredPermissions.contains("first_name"), "Should add the correct permission to expired permissions") } // MARK: - Can open URL scheme func testCanOpenUrlSchemeWithMissingScheme() { XCTAssertFalse( internalUtility._canOpenURLScheme(nil), "Should not be able to open a missing scheme" ) XCTAssertNil(logger.capturedContents, "A developer error should not be logged for a nil scheme") } func testCanOpenUrlSchemeWithInvalidSchemes() { [ "http: ", "", " ", "#@%(*&#$(^#@!$", "////foo", "foo: ", "foo: /", ] .forEach { invalidScheme in internalUtility._canOpenURLScheme(invalidScheme) verifyTestLoggerInvoked( loggingBehavior: .developerErrors, logEntry: "Invalid URL scheme provided: \(invalidScheme)" ) } } func testCanOpenUrlSchemeWithValidSchemes() { [ "foo", "FOO", "foo+bar", "foo-bar", "foo.bar", ] .forEach { validScheme in internalUtility._canOpenURLScheme(validScheme) XCTAssertNil(logger.capturedContents, "A developer error should not be logged for valid schemes") } } // MARK: - App URL Scheme func testAppURLSchemeWithMissingAppIdMissingSuffix() { settings.appID = nil settings.appURLSchemeSuffix = nil // This is not desired behavior but accurately reflects what is currently written. XCTAssertEqual( internalUtility.appURLScheme, "fb", "Should return an app url scheme derived from the app id and app url scheme suffix" ) } func testAppURLSchemeWithMissingAppIdInvalidSuffix() { settings.appID = nil settings.appURLSchemeSuffix = " " // This is not desired behavior but accurately reflects what is currently written. XCTAssertEqual( internalUtility.appURLScheme, "fb ", "Should return an app url scheme derived from the app id and app url scheme suffix" ) } func testAppURLSchemeWithMissingAppIdValidSuffix() { settings.appID = nil settings.appURLSchemeSuffix = "foo" // This is not desired behavior but accurately reflects what is currently written. XCTAssertEqual( internalUtility.appURLScheme, "fbfoo", "Should return an app url scheme derived from the app id and app url scheme suffix" ) } func testAppURLSchemeWithInvalidAppIdMissingSuffix() { let appID = " " settings.appID = appID settings.appURLSchemeSuffix = nil let expected = "fb\(appID)" // This is not desired behavior but accurately reflects what is currently written. XCTAssertEqual( internalUtility.appURLScheme, expected, "Should return an app url scheme derived app id and app url scheme suffix defined in settings" ) } func testAppURLSchemeWithInvalidAppIdInvalidSuffix() { let appID = " " let suffix = " " settings.appID = appID settings.appURLSchemeSuffix = suffix let expected = "fb\(appID)\(suffix)" // This is not desired behavior but accurately reflects what is currently written. XCTAssertEqual( internalUtility.appURLScheme, expected, "Should return an app url scheme derived app id and app url scheme suffix defined in settings" ) } func testAppURLSchemeWithInvalidAppIdValidSuffix() { let appID = " " let suffix = "foo" settings.appID = appID settings.appURLSchemeSuffix = suffix let expected = "fb\(appID)\(suffix)" // This is not desired behavior but accurately reflects what is currently written. XCTAssertEqual( internalUtility.appURLScheme, expected, "Should return an app url scheme derived app id and app url scheme suffix defined in settings" ) } func testAppURLSchemeWithValidAppIdMissingSuffix() { let appID = "appid" settings.appID = appID settings.appURLSchemeSuffix = nil let expected = "fb\(appID)" XCTAssertEqual( internalUtility.appURLScheme, expected, "Should return an app url scheme derived app id and app url scheme suffix defined in settings" ) } func testAppURLSchemeWithValidAppIdInvalidSuffix() { let appID = "appid" let suffix = " " settings.appID = appID settings.appURLSchemeSuffix = suffix let expected = "fb\(appID)\(suffix)" // This is not desired behavior but accurately reflects what is currently written. XCTAssertEqual( internalUtility.appURLScheme, expected, "Should return an app url scheme derived app id and app url scheme suffix defined in settings" ) } func testAppURLSchemeWithValidAppIdValidSuffix() { let appID = "appid" let suffix = "foo" settings.appID = appID settings.appURLSchemeSuffix = suffix let expected = "fb\(appID)\(suffix)" XCTAssertEqual( internalUtility.appURLScheme, expected, "Should return an app url scheme derived app id and app url scheme suffix defined in settings" ) } // MARK: - App URL with host func testAppUrlWithEmptyHost() throws { settings.appID = "appid" settings.appURLSchemeSuffix = "foo" let url = try internalUtility.appURL( withHost: "", path: validPath, queryParameters: validParameters ) XCTAssertNil(url.host, "Should not set an empty host.") } func testAppUrlWithValidHost() throws { settings.appID = "appid" settings.appURLSchemeSuffix = "foo" let url = try internalUtility.appURL( withHost: "facebook", path: validPath, queryParameters: validParameters ) XCTAssertEqual(url.host, "facebook", "Should set the expected host.") } // MARK: - Check registered can open url scheme func testCheckRegisteredCanOpenURLScheme() { let scheme = "foo" internalUtility.checkRegisteredCanOpenURLScheme(scheme) verifyTestLoggerInvoked( loggingBehavior: .developerErrors, logEntry: "\(scheme) is missing from your Info.plist under LSApplicationQueriesSchemes and is required." ) } func testCheckRegisteredCanOpenURLSchemeMultipleTimes() { let scheme = "foo" XCTAssertEqual(logger.logEntryCallCount, 0, "There should not be developer errors logged initially") internalUtility.checkRegisteredCanOpenURLScheme(scheme) XCTAssertEqual(logger.logEntryCallCount, 1, "One developer error should be logged") verifyTestLoggerInvoked( loggingBehavior: .developerErrors, logEntry: "\(scheme) is missing from your Info.plist under LSApplicationQueriesSchemes and is required." ) internalUtility.checkRegisteredCanOpenURLScheme(scheme) XCTAssertEqual(logger.logEntryCallCount, 1, "Additional errors should not be logged for the same error") } // MARK: - Dictionary from FBURL func testWithAuthorizeHostNoParameters() throws { let url = try XCTUnwrap(URL(string: "foo://authorize")) let parameters = internalUtility.parameters(fromFBURL: url) XCTAssertTrue( parameters.isEmpty, "Should not extract parameters from a url if there are none" ) } func testWithAuthorizeHostNoFragment() throws { let url = try XCTUnwrap(URL(string: "foo://authorize?foo=bar")) let parameters = internalUtility.parameters(fromFBURL: url) as? [String: String] XCTAssertEqual( parameters, ["foo": "bar"], "Should extract parameters from a url" ) } func testWithAuthorizeHostAndFragment() throws { let url = try XCTUnwrap(URL(string: "foo://authorize?foo=bar#param1=value1&param2=value2")) let parameters = internalUtility.parameters(fromFBURL: url) as? [String: String] let expectedParameters = [ "foo": "bar", "param1": "value1", "param2": "value2", ] XCTAssertEqual( parameters, expectedParameters, "Extracted parameters from an auth url should include fragment parameters" ) } func testWithoutAuthorizeHostNoParameters() throws { let url = try XCTUnwrap(URL(string: "foo://example")) let parameters = internalUtility.parameters(fromFBURL: url) XCTAssertTrue( parameters.isEmpty, "Should not extract parameters from a url if there are none" ) } func testWithoutAuthorizeHostNoFragment() throws { let url = try XCTUnwrap(URL(string: "foo://example?foo=bar")) let parameters = internalUtility.parameters(fromFBURL: url) as? [String: String] XCTAssertEqual( parameters, ["foo": "bar"], "Should extract parameters from a url" ) } func testWithoutAuthorizeHostWithFragment() throws { let url = try XCTUnwrap(URL(string: "foo://example?foo=bar#param1=value1&param2=value2")) let parameters = internalUtility.parameters(fromFBURL: url) as? [String: String] XCTAssertEqual( parameters, ["foo": "bar"], "Extracted parameters from a non auth url should not include fragment parameters" ) } // MARK: - Cookies func testDeletingDialogCookies() { let cookie1 = makeCookie(url: dialogURL) let cookie2 = makeCookie(url: dialogURL, name: "cookie 2") HTTPCookieStorage.shared.setCookie(cookie1) HTTPCookieStorage.shared.setCookie(cookie2) let cookies = HTTPCookieStorage.shared.cookies(for: dialogURL) let expectedCookies = [cookie1, cookie2] XCTAssertEqual(cookies, expectedCookies, "Sanity check that there are cookies to delete") internalUtility.deleteFacebookCookies() XCTAssertEqual( HTTPCookieStorage.shared.cookies(for: dialogURL), [], "All cookies for the facebook dialog url should be deleted" ) } func testDeletingNonDialogCookies() { let cookie = makeCookie(url: nonDialogURL) HTTPCookieStorage.shared.setCookie(cookie) let cookies = HTTPCookieStorage.shared.cookies(for: nonDialogURL) XCTAssertEqual(cookies, [cookie], "Sanity check that there are cookies to delete") internalUtility.deleteFacebookCookies() XCTAssertEqual( HTTPCookieStorage.shared.cookies(for: nonDialogURL), [cookie], "Should only delete cookies for the dialog url" ) } func testDeletingMixOfCookies() { let dialogCookie = makeCookie(url: dialogURL) let nonDialogCookie = makeCookie(url: nonDialogURL) HTTPCookieStorage.shared.setCookie(dialogCookie) HTTPCookieStorage.shared.setCookie(nonDialogCookie) internalUtility.deleteFacebookCookies() XCTAssertEqual( HTTPCookieStorage.shared.cookies(for: dialogURL), [], "Should delete cookies for the dialog url" ) XCTAssertEqual( HTTPCookieStorage.shared.cookies(for: nonDialogURL), [nonDialogCookie], "Should only delete cookies for the dialog url" ) } // MARK: - App Installation func testIsRegisteredCanOpenURLSchemeWithMissingScheme() { let querySchemes = [String]() bundle = TestBundle(infoDictionary: ["LSApplicationQueriesSchemes": querySchemes]) configureInternalUtility() XCTAssertFalse( internalUtility.isRegisteredCanOpenURLScheme(name), "Should not be consider a scheme to be registered if it's missing from the application query schemes" ) } func testIsRegisteredCanOpenURLSchemeWithScheme() { let querySchemes = [name] bundle = TestBundle(infoDictionary: ["LSApplicationQueriesSchemes": querySchemes]) configureInternalUtility() XCTAssertTrue( internalUtility.isRegisteredCanOpenURLScheme(name), "Should consider a scheme to be registered if it exists in the application query schemes" ) } func testIsRegisteredCanOpenURLSchemeCache() { let querySchemes = [name] bundle = TestBundle(infoDictionary: ["LSApplicationQueriesSchemes": querySchemes]) configureInternalUtility() XCTAssertTrue(internalUtility.isRegisteredCanOpenURLScheme(name), "Sanity check") bundle.infoDictionary = [:] XCTAssertTrue( internalUtility.isRegisteredCanOpenURLScheme(name), "Should return the cached value of the main bundle and not the updated values" ) } func testFacebookAppInstalledMissingQuerySchemes() { bundle = TestBundle(infoDictionary: [:]) configureInternalUtility() XCTAssertFalse(internalUtility.isFacebookAppInstalled) verifyTestLoggerInvoked(loggingBehavior: .developerErrors, logEntry: facebookUrlSchemeMissingMessage) } func testFacebookAppInstalledEmptyQuerySchemes() { let querySchemes = [String]() bundle = TestBundle(infoDictionary: ["LSApplicationQueriesSchemes": querySchemes]) configureInternalUtility() XCTAssertFalse(internalUtility.isFacebookAppInstalled) verifyTestLoggerInvoked(loggingBehavior: .developerErrors, logEntry: facebookUrlSchemeMissingMessage) } func testFacebookAppInstalledMissingQueryScheme() { let querySchemes = ["Foo"] bundle = TestBundle(infoDictionary: ["LSApplicationQueriesSchemes": querySchemes]) configureInternalUtility() XCTAssertFalse(internalUtility.isFacebookAppInstalled) verifyTestLoggerInvoked(loggingBehavior: .developerErrors, logEntry: facebookUrlSchemeMissingMessage) } func testFacebookAppInstalledValidQueryScheme() { let querySchemes = ["fbauth2"] bundle = TestBundle(infoDictionary: ["LSApplicationQueriesSchemes": querySchemes]) configureInternalUtility() XCTAssertFalse(internalUtility.isFacebookAppInstalled) XCTAssertNil(TestLogger.capturedLoggingBehavior) } func testFacebookAppInstalledCache() { bundle = TestBundle(infoDictionary: [:]) configureInternalUtility() XCTAssertEqual(logger.logEntryCallCount, 0, "There should not be developer errors logged initially") XCTAssertFalse(internalUtility.isFacebookAppInstalled) XCTAssertEqual(logger.logEntryCallCount, 1, "One developer error should be logged") verifyTestLoggerInvoked(loggingBehavior: .developerErrors, logEntry: facebookUrlSchemeMissingMessage) // Calling it again should not result in an additional call to the singleShotLogEntry method XCTAssertFalse(internalUtility.isFacebookAppInstalled) XCTAssertEqual(logger.logEntryCallCount, 1, "Additional errors should not be logged for the same error") } func testMessengerAppInstalledMissingQuerySchemes() { bundle = TestBundle(infoDictionary: [:]) configureInternalUtility() XCTAssertFalse(internalUtility.isMessengerAppInstalled) verifyTestLoggerInvoked( loggingBehavior: .developerErrors, logEntry: messengerUrlSchemeMissingMessage ) } func testMessengerAppInstalledEmptyQuerySchemes() { let querySchemes = [String]() bundle = TestBundle(infoDictionary: ["LSApplicationQueriesSchemes": querySchemes]) configureInternalUtility() XCTAssertFalse(internalUtility.isMessengerAppInstalled) verifyTestLoggerInvoked( loggingBehavior: .developerErrors, logEntry: messengerUrlSchemeMissingMessage ) } func testMessengerAppInstalledMissingQueryScheme() { let querySchemes = ["Foo"] bundle = TestBundle(infoDictionary: ["LSApplicationQueriesSchemes": querySchemes]) configureInternalUtility() XCTAssertFalse(internalUtility.isMessengerAppInstalled) verifyTestLoggerInvoked( loggingBehavior: .developerErrors, logEntry: messengerUrlSchemeMissingMessage ) } func testMessengerAppInstalledValidQueryScheme() { let querySchemes = ["fb-messenger-share-api"] bundle = TestBundle(infoDictionary: ["LSApplicationQueriesSchemes": querySchemes]) configureInternalUtility() XCTAssertFalse(internalUtility.isMessengerAppInstalled) XCTAssertNil(TestLogger.capturedLoggingBehavior) } func testMessengerAppInstalledCache() { bundle = TestBundle(infoDictionary: [:]) configureInternalUtility() XCTAssertTrue( TestLogger.capturedLogEntries.isEmpty, "There should not be developer errors logged initially" ) XCTAssertFalse(internalUtility.isMessengerAppInstalled) XCTAssertEqual(logger.logEntryCallCount, 1, "One developer error should be logged") verifyTestLoggerInvoked( loggingBehavior: .developerErrors, logEntry: messengerUrlSchemeMissingMessage ) // Calling it again should not result in an additional call to the singleShotLogEntry method XCTAssertFalse(internalUtility.isMessengerAppInstalled) XCTAssertEqual(logger.logEntryCallCount, 1, "Additional errors should not be logged for the same error") } // MARK: - Random Utility Methods func testIsBrowserURLWithNonBrowserURL() { [ URL(string: "file://foo")!, // swiftlint:disable:this force_unwrapping URL(string: "example://bar")!, // swiftlint:disable:this force_unwrapping ] .forEach { url in XCTAssertFalse( internalUtility.isBrowserURL(url), "\(url.absoluteString) should not be considered a browser url" ) } } func testIsBrowserURLWithBrowserURL() { [ URL(string: "HTTPS://example.com"), URL(string: "HTTP://example.com"), URL(string: "https://example.com"), URL(string: "http://example.com"), ] .compactMap { $0 } .forEach { url in XCTAssertTrue( internalUtility.isBrowserURL(url), "\(url.absoluteString) should be considered a browser url" ) } } func testIsFacebookBundleIdentifierWithInvalidIdentifiers() { [ "", "foo", "com.foo.bar", "com.facebook", ] .forEach { identifier in XCTAssertFalse( internalUtility.isFacebookBundleIdentifier(identifier), "\(identifier) should not be considered a facebook bundle indentifier" ) } } func testIsFacebookBundleIdentifierWithValidIdentifiers() { [ "com.facebook.", "com.facebook.foo", ".com.facebook.", ".com.facebook.foo", ] .forEach { identifier in XCTAssertTrue( internalUtility.isFacebookBundleIdentifier(identifier), "\(identifier) should be considered a facebook bundle indentifier" ) } } func testNonSafariBundleIdentifiers() { [ "", " ", "com.foo", ] .forEach { identifier in XCTAssertFalse( internalUtility.isSafariBundleIdentifier(identifier), "\(identifier) should not be considered a safari bundle identifier" ) } } func testSafariBundleIdentifiers() { [ "com.apple.mobilesafari", "com.apple.SafariViewService", ] .forEach { identifier in XCTAssertTrue( internalUtility.isSafariBundleIdentifier(identifier), "\(identifier) should be considered a safari bundle identifier" ) } } func testValidatingAppIDWhenUninitialized() { internalUtility = InternalUtility() settings.appID = "abc" // not actually used assertRaisesException(message: "Should raise an exception") { self.internalUtility.validateAppID() } } func testValidatingAppID() { settings.appID = nil assertRaisesException(message: "Should raise an exception") { self.internalUtility.validateAppID() } } func testValidateClientAccessTokenWhenUninitialized() { internalUtility = InternalUtility() settings.appID = "abc" // not actually used settings.clientToken = "123" // not actually used assertRaisesException(message: "Should raise an exception") { self.internalUtility.validateRequiredClientAccessToken() } } func testValidateClientAccessTokenWithoutClientTokenWithoutAppID() { settings.appID = nil settings.clientToken = nil assertRaisesException(message: "Should raise an exception") { self.internalUtility.validateRequiredClientAccessToken() } } func testValidateClientAccessTokenWithClientTokenWithoutAppID() { settings.appID = nil settings.clientToken = "client123" XCTAssertEqual( internalUtility.validateRequiredClientAccessToken(), "(null)|client123", "A valid client-access token should include the app identifier and the client token" ) } func testValidateClientAccessTokenWithClientTokenWithAppID() { settings.appID = "appid" settings.clientToken = "client123" XCTAssertEqual( internalUtility.validateRequiredClientAccessToken(), "appid|client123", "A valid client-access token should include the app identifier and the client token" ) } func testValidateClientAccessTokenWithoutClientTokenWithAppID() { settings.appID = "appid" settings.clientToken = nil assertRaisesException(message: "Should raise an exception") { self.internalUtility.validateRequiredClientAccessToken() } } func testIsRegisteredUrlSchemeWithRegisteredScheme() { bundle = makeBundle(registeredUrlSchemes: ["com.foo.bar"]) configureInternalUtility() XCTAssertTrue( internalUtility.isRegisteredURLScheme("com.foo.bar"), "Schemes in the bundle should be considered registered" ) } func testIsRegisteredUrlSchemeWithoutRegisteredScheme() { bundle = makeBundle(registeredUrlSchemes: ["com.foo.bar"]) configureInternalUtility() XCTAssertFalse( internalUtility.isRegisteredURLScheme("com.facebook"), "Schemes absent from the bundle should not be considered registered" ) } func testIsRegisteredUrlSchemeCaching() { bundle = TestBundle() configureInternalUtility() internalUtility.isRegisteredURLScheme("com.facebook") XCTAssertTrue( bundle.didAccessInfoDictionary, "Should query the bundle for URL types" ) bundle.reset() internalUtility.isRegisteredURLScheme("com.facebook") XCTAssertFalse( bundle.didAccessInfoDictionary, "Should not query the bundle more than once" ) } func testValidatingUrlSchemesWithoutAppID() { settings.appID = nil assertRaisesException( message: "Cannot validate url schemes without an app identifier" ) { self.internalUtility.validateURLSchemes() } } func testValidatingUrlSchemesWhenNotConfigured() { assertRaisesException( message: "Cannot validate url schemes before configuring" ) { self.internalUtility.validateURLSchemes() } } func testValidatingUrlSchemesWithAppIdMatchingBundleEntry() { settings.appID = "appid" settings.appURLSchemeSuffix = nil bundle = makeBundle(registeredUrlSchemes: ["fbappid"]) configureInternalUtility() assertDoesNotRaiseException( message: "The registered app url scheme must match the app id and url scheme suffix prepended with 'fb'" ) { self.internalUtility.validateURLSchemes() } } func testValidatingUrlSchemesWithNonAppIdMatchingBundleEntry() { settings.appID = "appid" settings.appURLSchemeSuffix = nil bundle = makeBundle(registeredUrlSchemes: ["fb123"]) configureInternalUtility() assertRaisesException( message: "The registered app url scheme must match the app id and url scheme suffix prepended with 'fb'" ) { self.internalUtility.validateURLSchemes() } } // We can't loop through these because of how stubbing works. func testValidatingFacebookUrlSchemes_api() { bundle = makeBundle(registeredUrlSchemes: ["fbapi"]) configureInternalUtility() assertRaisesException( message: "Should throw an error if fbapi is present in the bundle url schemes" ) { self.internalUtility.validateFacebookReservedURLSchemes() } } // We can't loop through these because of how stubbing works. func testValidatingFacebookUrlSchemes_messenger() { bundle = makeBundle(registeredUrlSchemes: ["fb-messenger-share-api"]) configureInternalUtility() assertRaisesException( message: "Should throw an error if fb-messenger-share-api is present in the bundle url schemes" ) { self.internalUtility.validateFacebookReservedURLSchemes() } } func testExtendDictionaryWithDefaultDataProcessingOptions() { let parameters = NSMutableDictionary() internalUtility.extendDictionary(withDataProcessingOptions: parameters) XCTAssertEqual( parameters, [:], "Parameters should not change with default data processing options" ) } func testExtendDictionaryWithDataProcessingOptions() { settings.persistableDataProcessingOptions = [ "data_processing_options": ["LDU"], "data_processing_options_country": 10, "data_processing_options_state": 100, ] let parameters = NSMutableDictionary() internalUtility.extendDictionary(withDataProcessingOptions: parameters) XCTAssertEqual( parameters["data_processing_options"] as? String, "[\"LDU\"]", "Parameters should be extended with expected data processing options" ) XCTAssertEqual( parameters["data_processing_options_country"] as? Int, 10, "Parameters should be extended with expected data processing options" ) XCTAssertEqual( parameters["data_processing_options_state"] as? Int, 100, "Parameters should be extended with expected data processing options" ) } func testIsPublishPermission() { [ "publish", "publishSomething", "manage", "manageSomething", "ads_management", "create_event", "rsvp_event", ] .forEach { permission in XCTAssertTrue(internalUtility.isPublishPermission(permission)) } [ "", "email", "_publish", ] .forEach { permission in XCTAssertFalse(internalUtility.isPublishPermission(permission)) } } func testIsUnityWithMissingSuffix() { settings.userAgentSuffix = nil XCTAssertFalse( internalUtility.isUnity, "User agent should determine whether an app is Unity" ) } func testIsUnityWithNonUnitySuffix() { settings.userAgentSuffix = "Foo" XCTAssertFalse( internalUtility.isUnity, "User agent should determine whether an app is Unity" ) } func testIsUnityWithUnitySuffix() { settings.userAgentSuffix = "__Unity__" XCTAssertTrue( internalUtility.isUnity, "User agent should determine whether an app is Unity" ) } func testHexadecimalStringFromData() throws { XCTAssertNil( internalUtility.hexadecimalString(from: Data()) ) let data = try XCTUnwrap("foo".data(using: .utf8)) let expected = "666f6f" XCTAssertEqual( internalUtility.hexadecimalString(from: data), expected ) } func testObjectIsEqualToObject() { var obj1: Any? = "foo" let obj2: Any = "foo" let obj3: Any = "bar" XCTAssertTrue(internalUtility.object(obj1 as Any, isEqualTo: obj1 as Any)) XCTAssertTrue(internalUtility.object(obj1 as Any, isEqualTo: obj2)) XCTAssertFalse(internalUtility.object(obj1 as Any, isEqualTo: obj3)) obj1 = nil XCTAssertFalse(internalUtility.object(obj1 as Any, isEqualTo: obj2)) XCTAssertFalse(internalUtility.object(obj2, isEqualTo: obj1 as Any)) } // MARK: - Helpers func verifyTestLoggerInvoked( loggingBehavior: LoggingBehavior, logEntry: String, file: StaticString = #file, line: UInt = #line ) { XCTAssertEqual( loggerFactory.capturedLoggingBehavior, loggingBehavior, file: file, line: line ) XCTAssertEqual( logger.capturedContents, logEntry, file: file, line: line ) } func makeCookie(url: URL, name: String = "MyCookie") -> HTTPCookie { HTTPCookie( properties: [ .originURL: url, .path: url.path, .name: name, .value: "Is good", ] )! // swiftlint:disable:this force_unwrapping } func makeBundle(registeredUrlSchemes: [String]) -> TestBundle { TestBundle( infoDictionary: [ "CFBundleURLTypes": [ ["CFBundleURLSchemes": registeredUrlSchemes], ], ] ) } }