FBSDKCoreKit/FBSDKCoreKitTests/ApplicationDelegateTests.swift (514 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 ApplicationDelegateTests: XCTestCase {
// swiftlint:disable implicitly_unwrapped_optional
var notificationCenter: TestNotificationCenter!
var featureChecker: TestFeatureManager!
var appEvents: TestAppEvents!
var userDataStore: UserDefaultsSpy!
var observer: TestApplicationDelegateObserver!
var settings: TestSettings!
var backgroundEventLogger: TestBackgroundEventLogger!
var serverConfigurationProvider: TestServerConfigurationProvider!
let bitmaskKey = "com.facebook.sdk.kits.bitmask"
var paymentObserver: TestPaymentObserver!
var profile: Profile!
var components: CoreKitComponents!
var configurator: TestCoreKitConfigurator!
var delegate: ApplicationDelegate! // swiftlint:disable:this weak_delegate
// swiftlint:enable implicitly_unwrapped_optional
override func setUp() {
super.setUp()
resetTestDependencies()
notificationCenter = TestNotificationCenter()
featureChecker = TestFeatureManager()
appEvents = TestAppEvents()
userDataStore = UserDefaultsSpy()
observer = TestApplicationDelegateObserver()
settings = TestSettings()
backgroundEventLogger = TestBackgroundEventLogger(
infoDictionaryProvider: TestBundle(),
eventLogger: TestAppEvents()
)
serverConfigurationProvider = TestServerConfigurationProvider()
paymentObserver = TestPaymentObserver()
profile = Profile(
userID: name,
firstName: nil,
middleName: nil,
lastName: nil,
name: nil,
linkURL: nil,
refreshDate: nil
)
components = TestCoreKitComponents.makeComponents(
appEvents: appEvents,
defaultDataStore: userDataStore,
featureChecker: featureChecker,
notificationCenter: notificationCenter,
paymentObserver: paymentObserver,
serverConfigurationProvider: serverConfigurationProvider,
settings: settings,
backgroundEventLogger: backgroundEventLogger
)
configurator = TestCoreKitConfigurator(components: components)
makeDelegate()
delegate.resetApplicationObserverCache()
}
override func tearDown() {
notificationCenter = nil
featureChecker = nil
appEvents = nil
userDataStore = nil
observer = nil
settings = nil
backgroundEventLogger = nil
serverConfigurationProvider = nil
paymentObserver = nil
profile = nil
components = nil
delegate = nil
resetTestDependencies()
super.tearDown()
}
func makeDelegate(usesTestConfigurator: Bool = true) {
let configurator: CoreKitConfiguring = usesTestConfigurator
? configurator
: CoreKitConfigurator(components: components)
delegate = ApplicationDelegate(
components: components,
configurator: configurator
)
}
func resetTestDependencies() {
ApplicationDelegate.reset()
TestAccessTokenWallet.reset()
TestAuthenticationTokenWallet.reset()
TestGateKeeperManager.reset()
TestProfileProvider.reset()
}
func testDefaultComponentsAndConfiguration() throws {
delegate = ApplicationDelegate()
XCTAssertIdentical(
delegate.components,
CoreKitComponents.default,
"An application delegate should be created with the default components by default"
)
let configurator = try XCTUnwrap(
delegate.configurator as? CoreKitConfigurator,
"An application delegate should be created with a concrete configurator by default"
)
XCTAssertIdentical(
configurator.components,
CoreKitComponents.default,
"The configurator should be created with the default components by default"
)
XCTAssertEqual(
delegate.applicationObservers.count,
0,
"The delegate should have an empty hash table of application observers"
)
}
func testComponentsAndConfiguration() {
let components = TestCoreKitComponents.makeComponents()
let configurator = TestCoreKitConfigurator(components: components)
delegate = ApplicationDelegate(components: components, configurator: configurator)
XCTAssertIdentical(
delegate.components,
components,
"An application delegate should be created with the provided components"
)
XCTAssertIdentical(
delegate.configurator,
configurator,
"An application delegate should be created with the provided configurator"
)
XCTAssertEqual(
delegate.applicationObservers.count,
0,
"The delegate should have an empty hash table of application observers"
)
}
// MARK: - Initializing SDK
func testInitializingSdkAddsBridgeApiObserver() {
delegate.initializeSDK()
XCTAssertTrue(
delegate.applicationObservers.contains(BridgeAPI.shared),
"Should add the shared bridge api instance to the application observers"
)
}
func testInitializingSdkPerformsSettingsLogging() {
delegate.initializeSDK()
XCTAssertEqual(
settings.logWarningsCallCount,
1,
"Should have settings log warnings upon initialization"
)
XCTAssertEqual(
settings.logIfSDKSettingsChangedCallCount,
1,
"Should have settings log if there were changes upon initialization"
)
XCTAssertEqual(
settings.recordInstallCallCount,
1,
"Should have settings record installations upon initialization"
)
}
func testInitializingSdkPerformsBackgroundEventLogging() {
delegate.initializeSDK()
XCTAssertEqual(
backgroundEventLogger.logBackgroundRefresStatusCallCount,
1,
"Should have background event logger log background refresh status upon initialization"
)
}
func testInitializingSdkChecksInstrumentFeature() {
delegate.initializeSDK()
XCTAssert(
featureChecker.capturedFeaturesContains(.instrument),
"Should check if the instrument feature is enabled on initialization"
)
}
func testInitializingSDKPerformsDownstreamConfigurations() {
delegate.initializeSDK()
XCTAssertTrue(
configurator.performConfigurationCalled,
"Initializing the SDK should ask the configurator to perform downstream configuration"
)
}
func testDidFinishLaunchingLaunchedApp() {
delegate.isAppLaunched = true
XCTAssertFalse(
delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil),
"Should return false if the application is already launched"
)
}
func testDidFinishLaunchingSetsCurrentAccessTokenWithCache() {
let expected = SampleAccessTokens.validToken
let cache = TestTokenCache(
accessToken: expected,
authenticationToken: nil
)
TestAccessTokenWallet.tokenCache = cache
delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
XCTAssertEqual(
TestAccessTokenWallet.currentAccessToken,
expected,
"Should set the current access token to the cached access token when it exists"
)
}
func testDidFinishLaunchingSetsCurrentAccessTokenWithoutCache() {
TestAccessTokenWallet.currentAccessToken = SampleAccessTokens.validToken
TestAccessTokenWallet.tokenCache = TestTokenCache(
accessToken: nil,
authenticationToken: nil
)
delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
XCTAssertNil(
TestAccessTokenWallet.currentAccessToken,
"Should set the current access token to nil access token when there isn't a cached token"
)
}
func testDidFinishLaunchingSetsCurrentAuthenticationTokenWithCache() {
let expected = SampleAuthenticationToken.validToken
TestAuthenticationTokenWallet.tokenCache = TestTokenCache(
accessToken: nil,
authenticationToken: expected
)
delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
XCTAssertEqual(
TestAuthenticationTokenWallet.currentAuthenticationToken,
expected,
"Should set the current authentication token to the cached access token when it exists"
)
}
func testDidFinishLaunchingSetsCurrentAuthenticationTokenWithoutCache() {
TestAuthenticationTokenWallet.tokenCache = TestTokenCache(
accessToken: nil,
authenticationToken: nil
)
delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
XCTAssertNil(
TestAuthenticationTokenWallet.currentAuthenticationToken,
"Should set the current authentication token to nil access token when there isn't a cached token"
)
}
func testDidFinishLaunchingWithAutoLogEnabled() {
settings.isAutoLogAppEventsEnabled = true
userDataStore.set(1, forKey: bitmaskKey)
delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
XCTAssertEqual(
appEvents.capturedEventName,
.initializeSDK,
"Should log initialization when auto log app events is enabled"
)
}
func testDidFinishLaunchingWithAutoLogDisabled() {
settings.isAutoLogAppEventsEnabled = false
userDataStore.set(1, forKey: bitmaskKey)
delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
XCTAssertNil(
appEvents.capturedEventName,
"Should not log initialization when auto log app events are disabled"
)
}
func testDidFinishLaunchingWithObservers() {
delegate.isAppLaunched = false
let observer1 = TestApplicationDelegateObserver()
let observer2 = TestApplicationDelegateObserver()
delegate.addObserver(observer1)
delegate.addObserver(observer2)
let notifiedObservers = delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
XCTAssertEqual(
observer1.didFinishLaunchingCallCount,
1,
"Should invoke did finish launching on all observers"
)
XCTAssertEqual(
observer2.didFinishLaunchingCallCount,
1,
"Should invoke did finish launching on all observers"
)
XCTAssertTrue(notifiedObservers, "Should indicate if observers were notified")
}
func testDidFinishLaunchingWithoutObservers() {
let notifiedObservers = delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
XCTAssertFalse(notifiedObservers, "Should indicate if no observers were notified")
}
func testAppEventsEnabled() {
settings.isAutoLogAppEventsEnabled = true
let notification = Notification(
name: UIApplication.didBecomeActiveNotification,
object: self,
userInfo: nil
)
delegate.applicationDidBecomeActive(notification)
XCTAssertTrue(
appEvents.wasActivateAppCalled,
"Should have app events activate the app when autolog app events is enabled"
)
XCTAssertEqual(
appEvents.capturedApplicationState,
.active,
"Should set the application state to active when the notification is received"
)
}
func testAppEventsDisabled() {
settings.isAutoLogAppEventsEnabled = false
let notification = Notification(
name: UIApplication.didBecomeActiveNotification,
object: self,
userInfo: nil
)
delegate.applicationDidBecomeActive(notification)
XCTAssertFalse(
appEvents.wasActivateAppCalled,
"Should not have app events activate the app when autolog app events is enabled"
)
XCTAssertEqual(
appEvents.capturedApplicationState,
.active,
"Should set the application state to active when the notification is received"
)
}
func testSettingApplicationState() {
delegate.setApplicationState(.background)
XCTAssertEqual(
appEvents.capturedApplicationState,
.background,
"The value of applicationState after calling setApplicationState should be UIApplicationStateBackground"
)
}
func testInitializingSdkTriggersApplicationLifecycleNotificationsForAppEvents() {
delegate.initializeSDK(launchOptions: [:])
XCTAssertTrue(
appEvents.wasStartObservingApplicationLifecycleNotificationsCalled,
"Should have app events start observing application lifecycle notifications upon initialization"
)
}
func testInitializingSDKLogsAppEvent() {
userDataStore.setValue(1, forKey: bitmaskKey)
delegate._logSDKInitialize()
XCTAssertEqual(
appEvents.capturedEventName,
.initializeSDK
)
XCTAssertFalse(appEvents.capturedIsImplicitlyLogged)
}
func testInitializingSdkObservesSystemNotifications() {
delegate.initializeSDK(launchOptions: [:])
XCTAssertTrue(
notificationCenter.capturedAddObserverInvocations.contains(
TestNotificationCenter.ObserverEvidence(
observer: delegate as Any,
name: UIApplication.didEnterBackgroundNotification,
selector: #selector(ApplicationDelegate.applicationDidEnterBackground(_:)),
object: nil
)
),
"Should start observing application backgrounding upon initialization"
)
XCTAssertTrue(
notificationCenter.capturedAddObserverInvocations.contains(
TestNotificationCenter.ObserverEvidence(
observer: delegate as Any,
name: UIApplication.didBecomeActiveNotification,
selector: #selector(ApplicationDelegate.applicationDidBecomeActive(_:)),
object: nil
)
),
"Should start observing application foregrounding upon initialization"
)
XCTAssertTrue(
notificationCenter.capturedAddObserverInvocations.contains(
TestNotificationCenter.ObserverEvidence(
observer: delegate as Any,
name: UIApplication.willResignActiveNotification,
selector: #selector(ApplicationDelegate.applicationWillResignActive(_:)),
object: nil
)
),
"Should start observing application resignation upon initializtion"
)
}
func testInitializingSdkSetsSessionInformation() {
delegate.initializeSDK(
launchOptions: [
UIApplication.LaunchOptionsKey.sourceApplication: name,
.url: SampleURLs.valid,
]
)
XCTAssertEqual(
appEvents.capturedSetSourceApplication,
name,
"Should set the source application based on the launch options"
)
XCTAssertEqual(
appEvents.capturedSetSourceApplicationURL,
SampleURLs.valid,
"Should set the source application url based on the launch options"
)
}
func testInitializingSdkRegistersForSessionUpdates() {
delegate.initializeSDK(launchOptions: [:])
XCTAssertTrue(
appEvents.wasRegisterAutoResetSourceApplicationCalled,
"Should have the analytics session register to auto reset the source application"
)
}
// MARK: - DidFinishLaunching
func testDidFinishLaunchingLoadsServerConfiguration() {
delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
XCTAssertTrue(
serverConfigurationProvider.loadServerConfigurationWasCalled,
"Should load a server configuration on finishing launching the application"
)
}
func testDidFinishLaunchingSetsProfileWithCache() {
TestProfileProvider.stubbedCachedProfile = profile
delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
XCTAssertEqual(
TestProfileProvider.current,
profile,
"Should set the current profile to the value fetched from the cache"
)
}
func testDidFinishLaunchingSetsProfileWithoutCache() {
XCTAssertNil(
TestProfileProvider.stubbedCachedProfile,
"Setup should nil out the cached profile"
)
delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
XCTAssertNil(
TestProfileProvider.current,
"Should set the current profile to nil when the cache is empty"
)
}
// MARK: - URL Opening
func testOpeningURLChecksAEMFeatureAvailability() {
delegate.application(
UIApplication.shared,
open: SampleURLs.validApp,
options: [:]
)
XCTAssertTrue(
featureChecker.capturedFeaturesContains(.AEM),
"Opening a deep link should check if the AEM feature is enabled"
)
}
func testOpeningUniversalLinkChecksAEMFeatureAvailability() {
// See https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app
let userActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb)
userActivity.webpageURL = SampleURLs.validUniversalLink
delegate.application(
UIApplication.shared,
continue: userActivity
)
XCTAssertTrue(
featureChecker.capturedFeaturesContains(.AEM),
"Opening a deep link should check if the AEM feature is enabled"
)
}
func testOpeningUniversalLinkNonBrowsingWebDoesNotCheckAEMAvailability() {
let userActivity = NSUserActivity(activityType: "Example")
userActivity.webpageURL = SampleURLs.validUniversalLink
delegate.application(
UIApplication.shared,
continue: userActivity
)
XCTAssertFalse(
featureChecker.capturedFeaturesContains(.AEM),
"Opening a deep link should check if the AEM feature is enabled"
)
}
// MARK: - Application Observers
func testDefaultsObservers() {
XCTAssertEqual(
delegate.applicationObservers.count,
0,
"Should have no observers by default"
)
}
func testAddingNewObserver() {
delegate.addObserver(observer)
XCTAssertEqual(
delegate.applicationObservers.count,
1,
"Should be able to add a single observer"
)
}
func testAddingDuplicateObservers() {
delegate.addObserver(observer)
delegate.addObserver(observer)
XCTAssertEqual(
delegate.applicationObservers.count,
1,
"Should only add one instance of a given observer"
)
}
func testRemovingObserver() {
delegate.addObserver(observer)
delegate.removeObserver(observer)
XCTAssertEqual(
delegate.applicationObservers.count,
0,
"Should be able to remove observers that are present in the stored list"
)
}
func testRemovingMissingObserver() {
delegate.removeObserver(observer)
XCTAssertEqual(
delegate.applicationObservers.count,
0,
"Should not be able to remove absent observers"
)
}
func testAppNotifyObserversWhenAppWillResignActive() {
delegate.addObserver(observer)
let notification = Notification(
name: UIApplication.willResignActiveNotification,
object: UIApplication.shared,
userInfo: nil
)
delegate.applicationWillResignActive(notification)
XCTAssertTrue(
observer.wasWillResignActiveCalled,
"Should inform observers when the application will resign active status"
)
}
}