diff --git a/.gitignore b/.gitignore index 592d6c4a..c1adfae2 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,5 @@ firebase-ios-sdk/ #scripts link scripts + +GoogleMulticastAppDelegate/Apps/SwiftUISample/SwiftUISample/GoogleService-Info.plist diff --git a/GoogleUtilitiesMulticastAppDelegate.podspec b/GoogleUtilitiesMulticastAppDelegate.podspec new file mode 100644 index 00000000..22245a42 --- /dev/null +++ b/GoogleUtilitiesMulticastAppDelegate.podspec @@ -0,0 +1,71 @@ +Pod::Spec.new do |s| + s.name = 'GoogleUtilitiesMulticastAppDelegate' + s.version = '7.7.0' + s.summary = 'GoogleUtilitiesMulticastAppDelegate' + + s.description = <<-DESC + GoogleUtilitiesMulticastAppDelegate + DESC + + s.homepage = 'https://github.com/google/GoogleUtilities' + s.license = { :type => 'Apache', :file => 'LICENSE' } + s.authors = 'Google, Inc.' + + s.source = { + :git => 'https://github.com/google/GoogleUtilities.git', + :tag => 'GoogleUtilitiesMulticastAppDelegate-' + s.version.to_s + } + + ios_deployment_target = '9.0' + osx_deployment_target = '10.12' + tvos_deployment_target = '10.0' + watchos_deployment_target = '6.0' + + s.ios.deployment_target = ios_deployment_target + # s.osx.deployment_target = osx_deployment_target + # s.tvos.deployment_target = tvos_deployment_target + # s.watchos.deployment_target = watchos_deployment_target + + s.cocoapods_version = '>= 1.4.0' + s.prefix_header_file = false + + s.pod_target_xcconfig = { + 'GCC_C_LANGUAGE_STANDARD' => 'c99', + 'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}"', + } + + base_dir = "GoogleUtilitiesMulticastAppDelegate/" + s.source_files = [ + base_dir + 'Sources/**/*.[hm]', + ] + + s.dependency 'GoogleUtilities/AppDelegateSwizzler', '~> 7.7' + + # s.test_spec 'unit' do |unit_tests| + # unit_tests.scheme = { :code_coverage => true } + # unit_tests.platforms = { + # :ios => ios_deployment_target, + # :osx => osx_deployment_target, + # :tvos => tvos_deployment_target + # } + # unit_tests.source_files = [ + # base_dir + 'Tests/Unit/**/*.[mh]', + # ] + # unit_tests.requires_app_host = true + # unit_tests.dependency 'OCMock' + # end + + # s.test_spec 'unit-swift' do |unit_tests_swift| + # unit_tests_swift.scheme = { :code_coverage => true } + # unit_tests_swift.platforms = { + # :ios => ios_deployment_target, + # :osx => osx_deployment_target, + # :tvos => tvos_deployment_target + # } + # unit_tests_swift.source_files = [ + # base_dir + 'Tests/Unit/**/*.swift', + # ] + + # unit_tests_swift.requires_app_host = true + # end +end diff --git a/GoogleUtilitiesMulticastAppDelegate/Sources/GULMulticastAppDelegate.m b/GoogleUtilitiesMulticastAppDelegate/Sources/GULMulticastAppDelegate.m new file mode 100644 index 00000000..08208e15 --- /dev/null +++ b/GoogleUtilitiesMulticastAppDelegate/Sources/GULMulticastAppDelegate.m @@ -0,0 +1,154 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "GoogleUtilitiesMulticastAppDelegate/Sources/Public/GoogleUtilities/GULMulticastAppDelegate.h" + +@interface GULMulticastAppDelegate () { + NSMutableArray *_interceptors; + id _defaultAppDelegate; +} +@end + +@implementation GULMulticastAppDelegate + +- (instancetype)init { + self = [super init]; + if (self) { + _interceptors = [[NSMutableArray alloc] init]; + } + return self; +} + +- (instancetype)initWithAppDelegate:(id)delegate { + self = [super init]; + if (self) { + _interceptors = [NSMutableArray arrayWithObject:delegate]; + _defaultAppDelegate = delegate; + } + return self; +} + ++ (id)multicastDelegate { + id appDelegate = [UIApplication sharedApplication].delegate; + if (!appDelegate) { + return nil; + } + if ([appDelegate conformsToProtocol:@protocol(GULMulticastAppDelegateProtocol)]) { + id multicastAppDelegate = + (id)appDelegate; + return multicastAppDelegate; + } + if ([appDelegate respondsToSelector:@selector(getMulticastDelegate)]) { + id multicastDelegate = + [appDelegate performSelector:@selector(getMulticastDelegate)]; + CFRetain((__bridge CFTypeRef)(multicastDelegate)); + return multicastDelegate; + } + return nil; +} + +- (id)getMulticastDelegate { + return self; +} + +- (void)addInterceptorWithInterceptor:(id)interceptor { + [_interceptors addObject:interceptor]; +} + +- (void)removeInterceptorWithInterceptor:(id)interceptor { + [_interceptors removeObject:interceptor]; +} + +- (BOOL)respondsToSelector:(SEL)aSelector { + if ([[self class] instancesRespondToSelector:aSelector]) { + return YES; + } + for (id interceptor in _interceptors) { + if (interceptor && [interceptor respondsToSelector:aSelector]) { + return YES; + } + } + return NO; +} + +- (void)setDefaultAppDelegate:(id)defaultAppDelegate { + [_interceptors addObject:defaultAppDelegate]; + _defaultAppDelegate = defaultAppDelegate; +} + +- (id)forwardingTargetForSelector:(SEL)aSelector { + return _defaultAppDelegate; +} + +#if !TARGET_OS_WATCH +#pragma mark - Open URL +- (BOOL)application:(GULApplication *)app + openURL:(NSURL *)url + options:(NSDictionary *)options { + BOOL result = NO; + for (id interceptor in _interceptors) { + result = result || [interceptor application:app openURL:url options:options]; + } + return result; +} + +#pragma mark - APNS methods +- (void)application:(GULApplication *)application + didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { + for (id interceptor in _interceptors) { + if ([interceptor respondsToSelector:@selector(application: + didRegisterForRemoteNotificationsWithDeviceToken:)]) { + [interceptor application:application + didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; + } + } +} + +#else // !TARGET_OS_WATCH +- (void)didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { + for (id interceptor in _interceptors) { + if ([interceptor + respondsToSelector:@selector(didRegisterForRemoteNotificationsWithDeviceToken)]) { + [interceptor didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; + } + } +} +#endif // !TARGET_OS_WATCH + +#if TARGET_OS_IOS || TARGET_OS_TV +- (void)application:(GULApplication *)application + didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { + for (id interceptor in _interceptors) { + if ([interceptor respondsToSelector:@selector(application: + didFailToRegisterForRemoteNotificationsWithError:)]) { + [interceptor application:application didFailToRegisterForRemoteNotificationsWithError:error]; + } + } +} + +- (void)application:(GULApplication *)application + didReceiveRemoteNotification:(NSDictionary *)userInfo + fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { + for (id interceptor in _interceptors) { + if ([interceptor respondsToSelector:@selector + (application:didReceiveRemoteNotification:fetchCompletionHandler:)]) { + [interceptor application:application + didReceiveRemoteNotification:userInfo + fetchCompletionHandler:completionHandler]; + } + } +} +#endif + +@end diff --git a/GoogleUtilitiesMulticastAppDelegate/Sources/Public/GoogleUtilities/GULMulticastAppDelegate.h b/GoogleUtilitiesMulticastAppDelegate/Sources/Public/GoogleUtilities/GULMulticastAppDelegate.h new file mode 100644 index 00000000..e59e8a40 --- /dev/null +++ b/GoogleUtilitiesMulticastAppDelegate/Sources/Public/GoogleUtilities/GULMulticastAppDelegate.h @@ -0,0 +1,40 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol GULMulticastAppDelegateProtocol + +- (void)addInterceptorWithInterceptor:(id)interceptor; + +- (void)removeInterceptorWithInterceptor:(id)interceptor; + +@end + +@interface GULMulticastAppDelegate : NSObject + +@property(nonatomic, copy) id defaultAppDelegate; + +- (instancetype)initWithAppDelegate:(id)delegate; + +- (void)addInterceptorWithInterceptor:(id)delegate; + ++ (id)multicastDelegate; +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleUtilitiesMulticastAppDelegate/Tests/Unit/GULMulticastAppDelegateTest.m b/GoogleUtilitiesMulticastAppDelegate/Tests/Unit/GULMulticastAppDelegateTest.m new file mode 100644 index 00000000..28007d98 --- /dev/null +++ b/GoogleUtilitiesMulticastAppDelegate/Tests/Unit/GULMulticastAppDelegateTest.m @@ -0,0 +1,1251 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "GoogleUtilitiesMulticastAppDelegate/Sources/Public/GoogleUtilities/GULMulticastAppDelegate.h" + +#import +#import +#import +#import "OCMock.h" + +#if (defined(__IPHONE_9_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_9_0)) +#define SDK_HAS_USERACTIVITY 1 +#endif + +/** Plist key that allows Firebase developers to disable App Delegate Proxying. Source of truth is + * the GULAppDelegateSwizzler class. + */ +static NSString *const kGULFirebaseAppDelegateProxyEnabledPlistKey = + @"FirebaseAppDelegateProxyEnabled"; + +/** Plist key that allows non-Firebase developers to disable App Delegate Proxying. Source of truth + * is the GULAppDelegateSwizzler class. + */ +static NSString *const kGULGoogleAppDelegateProxyEnabledPlistKey = + @"GoogleUtilitiesAppDelegateProxyEnabled"; + +#pragma mark - GULTestAppDelegate + +/** This class conforms to the application delegate protocol and is there to be able to test the + * App Delegate Swizzler's behavior. + */ +@interface GULTestAppDelegate : NSObject +, UIApplicationDelegate { + @public // Because we want to access the ivars from outside the class like obj->ivar for testing. + /** YES if the application:openURL:options: was called on an instance, NO otherwise. */ + BOOL _isOpenURLOptionsMethodCalled; + + /** Contains the backgroundSessionID that was passed to the + * application:handleEventsForBackgroundURLSession:completionHandler: method. + */ + NSString *_backgroundSessionID; + + /** YES if init was called. Used for memory layout testing after isa swizzling. */ + BOOL _isInitialized; + + /** An arbitrary number. Used for memory layout testing after isa swizzling. */ + int _arbitraryNumber; +} + +/** A URL property that is set by the app delegate methods, which is then used to verify if the app + * delegate methods were properly called. + */ +@property(nonatomic, strong) NSURL *url; +@property(nonatomic, strong) NSDictionary *openURLOptions; +@property(nonatomic, strong) NSString *openURLSourceApplication; + +@property(nonatomic, strong) NSUserActivity *userActivity; + +@property(nonatomic, strong) NSData *remoteNotificationsDeviceToken; +@property(nonatomic, strong) NSError *failToRegisterForRemoteNotificationsError; +@property(nonatomic, strong) NSDictionary *remoteNotification; + +#if TARGET_OS_IOS || TARGET_OS_TV +@property(nonatomic, copy) void (^remoteNotificationCompletionHandler)(UIBackgroundFetchResult); +#endif // TARGET_OS_IOS || TARGET_OS_TV + +/** + * The application is set each time a GULApplicationDelegate method is called + */ +@property(nonatomic, weak) GULApplication *application; + +@end + +@implementation GULTestAppDelegate + +// TODO: The static BOOLs below being accurate is dependent on the runtime loading +// GULTestAppDelegate before GULAppDelegateSwizzlerTest. It works, but it might be a good idea to +// figure a way to make this more deterministic. + +/** YES if GULTestAppDelegate responds to application:openURL:options:, NO otherwise. */ +static BOOL gRespondsToOpenURLHandler_iOS9; + +/** YES if GULTestAppDelegate responds to application:continueUserActivity:restorationHandler:, NO + * otherwise. + */ +static BOOL gRespondsToContinueUserActivity; + +/** YES if GULTestAppDelegate responds to + * application:handleEventsForBackgroundURLSession:completionHandler:, NO otherwise. + */ +static BOOL gRespondsToHandleBackgroundSession; + ++ (void)load { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + + gRespondsToOpenURLHandler_iOS9 = + [self instancesRespondToSelector:@selector(application:openURL:options:)]; + gRespondsToHandleBackgroundSession = + [self instancesRespondToSelector:@selector(application: + handleEventsForBackgroundURLSession:completionHandler:)]; + gRespondsToContinueUserActivity = [self + instancesRespondToSelector:@selector(application:continueUserActivity:restorationHandler:)]; +#pragma clang diagnostic pop +} + +- (instancetype)init { + self = [super init]; + if (self) { + _isOpenURLOptionsMethodCalled = NO; + _isInitialized = YES; + _arbitraryNumber = 123456789; + _backgroundSessionID = @"randomSessionID"; + _url = nil; + } + return self; +} + +- (BOOL)application:(GULApplication *)app + openURL:(NSURL *)url + options:(NSDictionary *)options { + self.application = app; + self.url = url; + self.openURLOptions = options; + _isOpenURLOptionsMethodCalled = YES; + return NO; +} + +- (BOOL)application:(GULApplication *)application + continueUserActivity:(NSUserActivity *)userActivity + restorationHandler:(void (^)(NSArray> *__nullable + restorableObjects))restorationHandler { + self.application = application; + self.userActivity = userActivity; + return NO; +} + +- (void)application:(GULApplication *)application + handleEventsForBackgroundURLSession:(nonnull NSString *)identifier + completionHandler:(nonnull void (^)(void))completionHandler { + self.application = application; + _backgroundSessionID = identifier; +} + +- (void)application:(GULApplication *)application + didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { + self.application = application; + self.remoteNotificationsDeviceToken = deviceToken; +} + +- (void)application:(GULApplication *)application + didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { + self.application = application; + self.failToRegisterForRemoteNotificationsError = error; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +- (void)application:(GULApplication *)application + didReceiveRemoteNotification:(NSDictionary *)userInfo { + self.application = application; + self.remoteNotification = userInfo; +} +#pragma clang diagnostic pop + +#if TARGET_OS_IOS || TARGET_OS_TV + +- (void)application:(GULApplication *)application + didReceiveRemoteNotification:(NSDictionary *)userInfo + fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { + self.application = application; + self.remoteNotification = userInfo; + self.remoteNotificationCompletionHandler = completionHandler; +} + +#endif // TARGET_OS_IOS || TARGET_OS_TV + +// These are methods to test whether changing the class still maintains behavior that the app +// delegate proxy shouldn't have modified. + +- (NSString *)someArbitraryMethod { + return @"blabla"; +} + ++ (int)someNumber { + return 890; +} + +@end + +@interface GULEmptyTestAppDelegate : NSObject +@end + +@implementation GULEmptyTestAppDelegate +@end + +#pragma mark - Interceptor class + +/** This is a class used to test whether interceptors work with the App Delegate Swizzler. */ +@interface GULTestInterceptorAppDelegate : NSObject + +/** URL sent to application:openURL:options:. */ +@property(nonatomic, copy) NSURL *URLForIOS9; + +/** The NSUserActivity sent to application:continueUserActivity:restorationHandler:. */ +@property(nonatomic, copy) NSUserActivity *userActivity; + +@end + +@implementation GULTestInterceptorAppDelegate + +- (BOOL)application:(GULApplication *)app + openURL:(NSURL *)url + options:(NSDictionary *)options { + _URLForIOS9 = [url copy]; + return YES; +} + +#if SDK_HAS_USERACTIVITY + +- (BOOL)application:(GULApplication *)application + continueUserActivity:(NSUserActivity *)userActivity + restorationHandler:(void (^)(NSArray> *__nullable + restorableObjects))restorationHandler { + _userActivity = userActivity; + return YES; +} + +#endif // SDK_HAS_USERACTIVITY + +@end + +@interface GULAppDelegateSwizzlerTest : XCTestCase +@property(nonatomic, strong) id mockSharedApplication; +@end + +@implementation GULAppDelegateSwizzlerTest + +- (void)setUp { + [super setUp]; + self.mockSharedApplication = OCMClassMock([GULApplication class]); + OCMStub([self.mockSharedApplication sharedApplication]).andReturn(self.mockSharedApplication); +} + +- (void)tearDown { + [GULAppDelegateSwizzler clearInterceptors]; + [GULAppDelegateSwizzler resetProxyOriginalDelegateOnceToken]; + self.mockSharedApplication = nil; + [super tearDown]; +} + +- (void)testNotAppDelegateIsNotSwizzled { + NSObject *notAppDelegate = [[NSObject alloc] init]; + OCMStub([self.mockSharedApplication delegate]).andReturn(notAppDelegate); + [GULAppDelegateSwizzler proxyOriginalDelegate]; + XCTAssertEqualObjects(NSStringFromClass([notAppDelegate class]), @"NSObject"); +} + +/** Tests proxying an object that responds to application delegate protocol and makes sure that + * it is isa swizzled and that the object after proxying responds to the expected methods + * and doesn't have its ivars modified. + */ +- (void)testProxyAppDelegate { + GULTestAppDelegate *realAppDelegate = [[GULTestAppDelegate alloc] init]; + OCMStub([self.mockSharedApplication delegate]).andReturn(realAppDelegate); + size_t sizeBefore = class_getInstanceSize([GULTestAppDelegate class]); + + Class realAppDelegateClassBefore = [realAppDelegate class]; + + // Create the proxy. + [GULAppDelegateSwizzler proxyOriginalDelegate]; + + XCTAssertTrue([realAppDelegate isKindOfClass:[GULTestAppDelegate class]]); + + NSString *newClassName = NSStringFromClass([realAppDelegate class]); + XCTAssertTrue([newClassName hasPrefix:@"GUL_"]); + // It is no longer GULTestAppDelegate class instance. + XCTAssertFalse([realAppDelegate isMemberOfClass:[GULTestAppDelegate class]]); + + size_t sizeAfter = class_getInstanceSize([realAppDelegate class]); + + // Class size must stay the same. + XCTAssertEqual(sizeBefore, sizeAfter); + + XCTAssertTrue([realAppDelegate respondsToSelector:@selector(application: + continueUserActivity:restorationHandler:)]); + XCTAssertTrue([realAppDelegate + respondsToSelector:@selector(application:didRegisterForRemoteNotificationsWithDeviceToken:)]); + XCTAssertTrue([realAppDelegate + respondsToSelector:@selector(application:didFailToRegisterForRemoteNotificationsWithError:)]); + XCTAssertTrue([realAppDelegate respondsToSelector:@selector(application: + didReceiveRemoteNotification:)]); +#if TARGET_OS_IOS || TARGET_OS_TV + XCTAssertTrue([realAppDelegate respondsToSelector:@selector(application:openURL:options:)]); + XCTAssertTrue([realAppDelegate + respondsToSelector:@selector(application: + handleEventsForBackgroundURLSession:completionHandler:)]); + XCTAssertTrue([realAppDelegate + respondsToSelector:@selector(application: + didReceiveRemoteNotification:fetchCompletionHandler:)]); +#endif // TARGET_OS_IOS || TARGET_OS_TV + + // Make sure that the class has changed. + XCTAssertNotEqualObjects([realAppDelegate class], realAppDelegateClassBefore); + + // Make sure that the ivars are not changed in memory as the subclass is created. Directly + // accessing the ivars should not crash. + XCTAssertEqual(realAppDelegate->_arbitraryNumber, 123456789); + XCTAssertEqual(realAppDelegate->_isInitialized, 1); + XCTAssertFalse(realAppDelegate->_isOpenURLOptionsMethodCalled); + XCTAssertEqualObjects(realAppDelegate->_backgroundSessionID, @"randomSessionID"); +} + +- (void)testProxyEmptyAppDelegate { + GULEmptyTestAppDelegate *realAppDelegate = [[GULEmptyTestAppDelegate alloc] init]; + OCMStub([self.mockSharedApplication delegate]).andReturn(realAppDelegate); + size_t sizeBefore = class_getInstanceSize([GULEmptyTestAppDelegate class]); + + Class realAppDelegateClassBefore = [realAppDelegate class]; + + // Create the proxy. + [GULAppDelegateSwizzler proxyOriginalDelegate]; + + XCTAssertTrue([realAppDelegate isKindOfClass:[GULEmptyTestAppDelegate class]]); + + NSString *newClassName = NSStringFromClass([realAppDelegate class]); + XCTAssertTrue([newClassName hasPrefix:@"GUL_"]); + // It is no longer GULTestAppDelegate class instance. + XCTAssertFalse([realAppDelegate isMemberOfClass:[GULEmptyTestAppDelegate class]]); + + size_t sizeAfter = class_getInstanceSize([realAppDelegate class]); + + // Class size must stay the same. + XCTAssertEqual(sizeBefore, sizeAfter); + + XCTAssertTrue([realAppDelegate respondsToSelector:@selector(application: + continueUserActivity:restorationHandler:)]); + // Remote notifications methods should be added only by + // -proxyOriginalDelegateIncludingAPNSMethods + XCTAssertFalse([realAppDelegate + respondsToSelector:@selector(application:didRegisterForRemoteNotificationsWithDeviceToken:)]); + XCTAssertFalse([realAppDelegate + respondsToSelector:@selector(application:didFailToRegisterForRemoteNotificationsWithError:)]); + XCTAssertFalse([realAppDelegate respondsToSelector:@selector(application: + didReceiveRemoteNotification:)]); +#if TARGET_OS_IOS || TARGET_OS_TV + // The implementation should not be added if there is no original implementation + XCTAssertFalse([realAppDelegate respondsToSelector:@selector(application:openURL:options:)]); + XCTAssertTrue([realAppDelegate + respondsToSelector:@selector(application: + handleEventsForBackgroundURLSession:completionHandler:)]); + XCTAssertFalse([realAppDelegate + respondsToSelector:@selector(application: + didReceiveRemoteNotification:fetchCompletionHandler:)]); +#endif // TARGET_OS_IOS || TARGET_OS_TV + + // Make sure that the class has changed. + XCTAssertNotEqualObjects([realAppDelegate class], realAppDelegateClassBefore); +} + +- (void)testProxyRemoteNotificationsMethodsEmptyAppDelegate { + GULEmptyTestAppDelegate *realAppDelegate = [[GULEmptyTestAppDelegate alloc] init]; + OCMStub([self.mockSharedApplication delegate]).andReturn(realAppDelegate); + size_t sizeBefore = class_getInstanceSize([GULEmptyTestAppDelegate class]); + + Class realAppDelegateClassBefore = [realAppDelegate class]; + + // Create the proxy. + [GULAppDelegateSwizzler proxyOriginalDelegateIncludingAPNSMethods]; + + XCTAssertTrue([realAppDelegate isKindOfClass:[GULEmptyTestAppDelegate class]]); + + NSString *newClassName = NSStringFromClass([realAppDelegate class]); + XCTAssertTrue([newClassName hasPrefix:@"GUL_"]); + // It is no longer GULTestAppDelegate class instance. + XCTAssertFalse([realAppDelegate isMemberOfClass:[GULEmptyTestAppDelegate class]]); + + size_t sizeAfter = class_getInstanceSize([realAppDelegate class]); + + // Class size must stay the same. + XCTAssertEqual(sizeBefore, sizeAfter); + + XCTAssertTrue([realAppDelegate respondsToSelector:@selector(application: + continueUserActivity:restorationHandler:)]); + + // Remote notifications methods should be added only by + // -proxyOriginalDelegateIncludingAPNSMethods + XCTAssertTrue([realAppDelegate + respondsToSelector:@selector(application:didRegisterForRemoteNotificationsWithDeviceToken:)]); + XCTAssertTrue([realAppDelegate + respondsToSelector:@selector(application:didFailToRegisterForRemoteNotificationsWithError:)]); + XCTAssertTrue([realAppDelegate respondsToSelector:@selector(application: + didReceiveRemoteNotification:)]); + +#if TARGET_OS_IOS || TARGET_OS_TV + // The implementation should not be added if there is no original implementation + XCTAssertFalse([realAppDelegate respondsToSelector:@selector(application:openURL:options:)]); + + XCTAssertTrue([realAppDelegate + respondsToSelector:@selector(application: + handleEventsForBackgroundURLSession:completionHandler:)]); + + // The implementation should not be added if there is no original implementation + XCTAssertFalse([realAppDelegate + respondsToSelector:@selector(application: + didReceiveRemoteNotification:fetchCompletionHandler:)]); + +#endif // TARGET_OS_IOS || TARGET_OS_TV + + // Make sure that the class has changed. + XCTAssertNotEqualObjects([realAppDelegate class], realAppDelegateClassBefore); +} + +- (void)testProxyRemoteNotificationsMethodsEmptyAppDelegateAfterInitialProxy { + GULEmptyTestAppDelegate *realAppDelegate = [[GULEmptyTestAppDelegate alloc] init]; + OCMStub([self.mockSharedApplication delegate]).andReturn(realAppDelegate); + size_t sizeBefore = class_getInstanceSize([GULEmptyTestAppDelegate class]); + + Class realAppDelegateClassBefore = [realAppDelegate class]; + + // Create the proxy. + [GULAppDelegateSwizzler proxyOriginalDelegate]; + + XCTAssertTrue([realAppDelegate isKindOfClass:[GULEmptyTestAppDelegate class]]); + + NSString *newClassName = NSStringFromClass([realAppDelegate class]); + XCTAssertTrue([newClassName hasPrefix:@"GUL_"]); + // It is no longer GULTestAppDelegate class instance. + XCTAssertFalse([realAppDelegate isMemberOfClass:[GULEmptyTestAppDelegate class]]); + + size_t sizeAfter = class_getInstanceSize([realAppDelegate class]); + + // Class size must stay the same. + XCTAssertEqual(sizeBefore, sizeAfter); + + XCTAssertTrue([realAppDelegate respondsToSelector:@selector(application: + continueUserActivity:restorationHandler:)]); + // Proxy remote notifications methods + [GULAppDelegateSwizzler proxyOriginalDelegateIncludingAPNSMethods]; + + XCTAssertTrue([realAppDelegate + respondsToSelector:@selector(application:didRegisterForRemoteNotificationsWithDeviceToken:)]); + XCTAssertTrue([realAppDelegate + respondsToSelector:@selector(application:didFailToRegisterForRemoteNotificationsWithError:)]); + XCTAssertTrue([realAppDelegate respondsToSelector:@selector(application: + didReceiveRemoteNotification:)]); +#if TARGET_OS_IOS || TARGET_OS_TV + // The implementation should not be added if there is no original implementation + XCTAssertFalse([realAppDelegate respondsToSelector:@selector(application:openURL:options:)]); + XCTAssertTrue([realAppDelegate + respondsToSelector:@selector(application: + handleEventsForBackgroundURLSession:completionHandler:)]); + + // The implementation should not be added if there is no original implementation + XCTAssertFalse([realAppDelegate + respondsToSelector:@selector(application: + didReceiveRemoteNotification:fetchCompletionHandler:)]); +#endif // TARGET_OS_IOS || TARGET_OS_TV + + // Make sure that the class has changed. + XCTAssertNotEqualObjects([realAppDelegate class], realAppDelegateClassBefore); +} + +#if SDK_HAS_USERACTIVITY +- (void)testHandleBackgroundSessionMethod { + GULTestAppDelegate *realAppDelegate = [[GULTestAppDelegate alloc] init]; + OCMStub([self.mockSharedApplication delegate]).andReturn(realAppDelegate); + + // Create the proxy. + [GULAppDelegateSwizzler proxyOriginalDelegate]; + + GULApplication *currentApplication = [GULApplication sharedApplication]; + NSString *sessionID = @"123"; + void (^nilHandler)(void) = nil; + [realAppDelegate application:currentApplication + handleEventsForBackgroundURLSession:sessionID + completionHandler:nilHandler]; + + // Intentionally access the ivars directly. It should be set to the session ID as the real method + // is called. + XCTAssertEqualObjects(realAppDelegate->_backgroundSessionID, sessionID); +} +#endif // SDK_HAS_USERACTIVITY + +/** Tests registering and unregistering invalid interceptors. */ +- (void)testInvalidInterceptor { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrows([GULAppDelegateSwizzler registerAppDelegateInterceptor:nil], + @"Should not register nil interceptor"); +#pragma clang diagnostic pop + XCTAssertEqual([GULAppDelegateSwizzler interceptors].count, 0); + + // Try to register some random object that does not conform to application delegate. + NSObject *randomObject = [[NSObject alloc] init]; + + XCTAssertThrows([GULAppDelegateSwizzler + registerAppDelegateInterceptor:(id)randomObject], + @"Should not register interceptor that does not conform to %@Delegate", + kGULApplicationClassName); + XCTAssertEqual([GULAppDelegateSwizzler interceptors].count, 0); + + GULTestInterceptorAppDelegate *interceptorAppDelegate = + [[GULTestInterceptorAppDelegate alloc] init]; + GULAppDelegateInterceptorID interceptorID = + [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptorAppDelegate]; + XCTAssertNotNil(interceptorID); + XCTAssertEqual([GULAppDelegateSwizzler interceptors].count, 1); + + // Register the same object. Should not change the number of objects. + XCTAssertNotNil([GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptorAppDelegate]); + XCTAssertEqual([GULAppDelegateSwizzler interceptors].count, 1); + + XCTAssertThrows([GULAppDelegateSwizzler unregisterAppDelegateInterceptorWithID:@""], + @"Should not unregister empty interceptor ID"); + XCTAssertEqual([GULAppDelegateSwizzler interceptors].count, 1); + + // Try to unregister an empty string. Should not remove anything. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrows([GULAppDelegateSwizzler unregisterAppDelegateInterceptorWithID:nil], + @"Should not unregister nil interceptorID"); + XCTAssertEqual([GULAppDelegateSwizzler interceptors].count, 1); + + // Try to unregister a random string. Should not remove anything. + [GULAppDelegateSwizzler unregisterAppDelegateInterceptorWithID:@"random ID"]; + XCTAssertEqual([GULAppDelegateSwizzler interceptors].count, 1); + + // Unregister the right one. + [GULAppDelegateSwizzler unregisterAppDelegateInterceptorWithID:interceptorID]; + XCTAssertEqual([GULAppDelegateSwizzler interceptors].count, 0); +} + +/** Tests that the description of appDelegate object doesn't change even after proxying it. */ +- (void)testDescription { + GULTestAppDelegate *realAppDelegate = [[GULTestAppDelegate alloc] init]; + OCMStub([self.mockSharedApplication delegate]).andReturn(realAppDelegate); + Class classBefore = [realAppDelegate class]; + NSString *descriptionBefore = [realAppDelegate description]; + + [GULAppDelegateSwizzler proxyOriginalDelegate]; + + Class classAfter = [realAppDelegate class]; + NSString *descriptionAfter = [realAppDelegate description]; + + NSString *descriptionString = + [NSString stringWithFormat:@"", realAppDelegate]; + + // The description must be the same even though the class has changed. + XCTAssertEqualObjects(descriptionBefore, descriptionAfter); + XCTAssertNotEqualObjects(classAfter, classBefore); + XCTAssertEqualObjects(descriptionAfter, descriptionString); +} + +/** Tests that methods that are not overridden by the App Delegate Proxy still work as expected. */ +- (void)testNotOverriddenMethods { + GULTestAppDelegate *realAppDelegate = [[GULTestAppDelegate alloc] init]; + OCMStub([self.mockSharedApplication delegate]).andReturn(realAppDelegate); + + // Create the proxy. + [GULAppDelegateSwizzler proxyOriginalDelegate]; + + // Make sure that original class instance method still works. + XCTAssertEqualObjects([realAppDelegate someArbitraryMethod], @"blabla"); + + // Make sure that the new subclass inherits the original class method. + XCTAssertEqual([[realAppDelegate class] someNumber], 890); + + // Make sure that the original class still works. + XCTAssertEqual([GULTestAppDelegate someNumber], 890); +} + +#if !SWIFT_PACKAGE +// TODO: Investigate why this test fails in Swift PM builds. + +/** Tests that if the app delegate changes after it has been proxied, the App Delegate Swizzler + * handles it correctly. + */ +- (void)testAppDelegateInstance { + // The test logic involves using KVC on the UIApplication.delegate property. This does not really + // work well with OCMPartialMock([GULApplication sharedApplication]) and triggers issue + // https://github.com/erikdoe/ocmock/issues/346. + // Let's stop mocking the shared application for this particular test. + [self.mockSharedApplication stopMocking]; + self.mockSharedApplication = nil; + + GULTestAppDelegate *realAppDelegate = [[GULTestAppDelegate alloc] init]; + + [GULApplication sharedApplication].delegate = realAppDelegate; + [GULAppDelegateSwizzler proxyOriginalDelegate]; + + XCTAssertEqualObjects([GULAppDelegateSwizzler originalDelegate], realAppDelegate); + + GULTestInterceptorAppDelegate *anotherAppDelegate = [[GULTestInterceptorAppDelegate alloc] init]; + XCTAssertNotEqualObjects(realAppDelegate, anotherAppDelegate); + + [GULApplication sharedApplication].delegate = anotherAppDelegate; + // Make sure that the new delegate is swizzled out and set correctly. + XCTAssertNil([GULAppDelegateSwizzler originalDelegate]); + + [GULAppDelegateSwizzler proxyOriginalDelegate]; + + // Swizzling of an updated app delegate is not supported so far. + XCTAssertNil([GULAppDelegateSwizzler originalDelegate]); +} +#endif + +#pragma mark - Tests the behaviour with interceptors + +#if TARGET_OS_IOS || TARGET_OS_TV +/** Tests that application:openURL:options: is invoked on the interceptor if it exists. */ +- (void)testApplicationOpenURLOptionsIsInvokedOnInterceptors { + if (@available(iOS 10, *)) { + id interceptor = OCMProtocolMock(@protocol(GULApplicationDelegate)); + OCMExpect([interceptor application:OCMOCK_ANY openURL:OCMOCK_ANY options:OCMOCK_ANY]) + .andReturn(NO); + + id interceptor2 = OCMProtocolMock(@protocol(GULApplicationDelegate)); + OCMExpect([interceptor2 application:OCMOCK_ANY openURL:OCMOCK_ANY options:OCMOCK_ANY]) + .andReturn(NO); + + NSURL *testURL = [[NSURL alloc] initWithString:@"https://www.google.com"]; + NSDictionary *testOpenURLOptions = @{UIApplicationOpenURLOptionUniversalLinksOnly : @"test"}; + + GULTestAppDelegate *testAppDelegate = [[GULTestAppDelegate alloc] init]; + OCMStub([self.mockSharedApplication delegate]).andReturn(testAppDelegate); + + [GULAppDelegateSwizzler proxyOriginalDelegate]; + [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor]; + [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor2]; + + [testAppDelegate application:[GULApplication sharedApplication] + openURL:testURL + options:testOpenURLOptions]; + OCMVerifyAll(interceptor); + OCMVerifyAll(interceptor2); + + // Check that original implementation was called with proper parameters + XCTAssertEqual(testAppDelegate.application, [GULApplication sharedApplication]); + XCTAssertEqual(testAppDelegate.url, testURL); + } +} + +/** Tests that the result of application:openURL:options: from all interceptors is ORed. */ +- (void)testResultOfApplicationOpenURLOptionsIsORed { + if (@available(iOS 10, *)) { + NSURL *testURL = [[NSURL alloc] initWithString:@"https://www.google.com"]; + NSDictionary *testOpenURLOptions = @{UIApplicationOpenURLOptionUniversalLinksOnly : @"test"}; + + GULTestAppDelegate *testAppDelegate = [[GULTestAppDelegate alloc] init]; + OCMStub([self.mockSharedApplication delegate]).andReturn(testAppDelegate); + [GULAppDelegateSwizzler proxyOriginalDelegate]; + + BOOL shouldOpen = [testAppDelegate application:[GULApplication sharedApplication] + openURL:testURL + options:testOpenURLOptions]; + // Verify that the original app delegate returns NO. + XCTAssertFalse(shouldOpen); + + id interceptor = OCMProtocolMock(@protocol(GULApplicationDelegate)); + OCMExpect([interceptor application:OCMOCK_ANY openURL:OCMOCK_ANY options:OCMOCK_ANY]) + .andReturn(NO); + [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor]; + shouldOpen = [testAppDelegate application:[GULApplication sharedApplication] + openURL:testURL + options:testOpenURLOptions]; + // Verify that if the only interceptor returns NO, the value is still NO. + XCTAssertFalse(shouldOpen); + + id interceptor2 = OCMProtocolMock(@protocol(GULApplicationDelegate)); + OCMExpect([interceptor2 application:OCMOCK_ANY openURL:OCMOCK_ANY options:OCMOCK_ANY]) + .andReturn(YES); + [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor2]; + + OCMExpect([interceptor application:OCMOCK_ANY openURL:OCMOCK_ANY options:OCMOCK_ANY]) + .andReturn(NO); + shouldOpen = [testAppDelegate application:[GULApplication sharedApplication] + openURL:testURL + options:testOpenURLOptions]; + // Verify that if one of the two interceptors returns YES, the value is YES. + XCTAssertTrue(shouldOpen); + } +} +#endif // TARGET_OS_IOS || TARGET_OS_TV + +#if TARGET_OS_IOS || TARGET_OS_TV +/** Tests that application:handleEventsForBackgroundURLSession:completionHandler: is invoked on the + * interceptors if it exists. + */ +- (void)testApplicationHandleEventsForBackgroundURLSessionIsInvokedOnInterceptors { + id interceptor = OCMProtocolMock(@protocol(GULApplicationDelegate)); + OCMExpect([interceptor application:OCMOCK_ANY + handleEventsForBackgroundURLSession:OCMOCK_ANY + completionHandler:OCMOCK_ANY]); + + id interceptor2 = OCMProtocolMock(@protocol(GULApplicationDelegate)); + OCMExpect([interceptor2 application:OCMOCK_ANY + handleEventsForBackgroundURLSession:OCMOCK_ANY + completionHandler:OCMOCK_ANY]); + + GULTestAppDelegate *testAppDelegate = [[GULTestAppDelegate alloc] init]; + OCMStub([self.mockSharedApplication delegate]).andReturn(testAppDelegate); + [GULAppDelegateSwizzler proxyOriginalDelegate]; + + [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor]; + [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor2]; + + NSString *backgroundSessionID = @"testBackgroundSessionID"; + [testAppDelegate application:[GULApplication sharedApplication] + handleEventsForBackgroundURLSession:backgroundSessionID + completionHandler:^{ + }]; + + OCMVerifyAll(interceptor); + OCMVerifyAll(interceptor2); + + // Check that original implementation was called with proper parameters + XCTAssertEqual(testAppDelegate.application, [GULApplication sharedApplication]); + XCTAssertEqual(testAppDelegate->_backgroundSessionID, backgroundSessionID); +} +#endif // TARGET_OS_IOS || TARGET_OS_TV + +/** Tests that application:continueUserActivity:restorationHandler: is invoked on the interceptors + * if it exists. + */ +- (void)testApplicationContinueUserActivityRestorationHandlerIsInvokedOnInterceptors { + id interceptor = OCMProtocolMock(@protocol(GULApplicationDelegate)); + OCMExpect([interceptor application:OCMOCK_ANY + continueUserActivity:OCMOCK_ANY + restorationHandler:OCMOCK_ANY]) + .andReturn(NO); + + id interceptor2 = OCMProtocolMock(@protocol(GULApplicationDelegate)); + OCMExpect([interceptor2 application:OCMOCK_ANY + continueUserActivity:OCMOCK_ANY + restorationHandler:OCMOCK_ANY]) + .andReturn(NO); + + NSUserActivity *testUserActivity = [[NSUserActivity alloc] initWithActivityType:@"test"]; + + GULTestAppDelegate *testAppDelegate = [[GULTestAppDelegate alloc] init]; + OCMStub([self.mockSharedApplication delegate]).andReturn(testAppDelegate); + [GULAppDelegateSwizzler proxyOriginalDelegate]; + + [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor]; + [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor2]; + + [testAppDelegate application:[GULApplication sharedApplication] + continueUserActivity:testUserActivity + restorationHandler:^(NSArray *restorableObjects){ + }]; + OCMVerifyAll(interceptor); + OCMVerifyAll(interceptor2); + + // Check that original implementation was called with proper parameters + XCTAssertEqual(testAppDelegate.application, [GULApplication sharedApplication]); + XCTAssertEqual(testAppDelegate.userActivity, testUserActivity); +} + +/** Tests that the results of application:continueUserActivity:restorationHandler: from the + * interceptors are ORed. + */ +- (void)testApplicationContinueUserActivityRestorationHandlerResultsAreORed { + GULTestAppDelegate *testAppDelegate = [[GULTestAppDelegate alloc] init]; + OCMStub([self.mockSharedApplication delegate]).andReturn(testAppDelegate); + [GULAppDelegateSwizzler proxyOriginalDelegate]; + NSUserActivity *testUserActivity = [[NSUserActivity alloc] initWithActivityType:@"test"]; + + BOOL shouldContinueUserActivity = [testAppDelegate application:[GULApplication sharedApplication] + continueUserActivity:testUserActivity + restorationHandler:^(NSArray *restorableObjects){ + }]; + // Verify that it is NO when there are no interceptors. + XCTAssertFalse(shouldContinueUserActivity); + + id interceptor = OCMProtocolMock(@protocol(GULApplicationDelegate)); + OCMExpect([interceptor application:OCMOCK_ANY + continueUserActivity:OCMOCK_ANY + restorationHandler:OCMOCK_ANY]) + .andReturn(NO); + [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor]; + shouldContinueUserActivity = [testAppDelegate application:[GULApplication sharedApplication] + continueUserActivity:testUserActivity + restorationHandler:^(NSArray *restorableObjects){ + }]; + // Verify that it is NO when the only interceptor returns a NO. + XCTAssertFalse(shouldContinueUserActivity); + + id interceptor2 = OCMProtocolMock(@protocol(GULApplicationDelegate)); + OCMExpect([interceptor2 application:OCMOCK_ANY + continueUserActivity:OCMOCK_ANY + restorationHandler:OCMOCK_ANY]) + .andReturn(YES); + [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor2]; + + OCMExpect([interceptor application:OCMOCK_ANY + continueUserActivity:OCMOCK_ANY + restorationHandler:OCMOCK_ANY]) + .andReturn(NO); + shouldContinueUserActivity = [testAppDelegate application:[GULApplication sharedApplication] + continueUserActivity:testUserActivity + restorationHandler:^(NSArray *restorableObjects){ + }]; + + // The result is YES if one of the interceptors returns YES. + XCTAssertTrue(shouldContinueUserActivity); +} + +- (void)testApplicationDidRegisterForRemoteNotificationsIsInvokedOnInterceptors { + NSData *deviceToken = [NSData data]; + GULApplication *application = [GULApplication sharedApplication]; + + id interceptor = OCMProtocolMock(@protocol(GULApplicationDelegate)); + OCMExpect([interceptor application:application + didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]); + + id interceptor2 = OCMProtocolMock(@protocol(GULApplicationDelegate)); + OCMExpect([interceptor2 application:application + didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]); + + GULTestAppDelegate *testAppDelegate = [[GULTestAppDelegate alloc] init]; + OCMStub([self.mockSharedApplication delegate]).andReturn(testAppDelegate); + [GULAppDelegateSwizzler proxyOriginalDelegateIncludingAPNSMethods]; + + [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor]; + [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor2]; + + [testAppDelegate application:application + didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; + OCMVerifyAll(interceptor); + OCMVerifyAll(interceptor2); + + XCTAssertEqual(testAppDelegate.application, application); + XCTAssertEqual(testAppDelegate.remoteNotificationsDeviceToken, deviceToken); +} + +- (void)testApplicationDidFailToRegisterForRemoteNotificationsIsInvokedOnInterceptors { + NSError *error = [NSError errorWithDomain:@"test" code:-1 userInfo:nil]; + GULApplication *application = [GULApplication sharedApplication]; + + id interceptor = OCMProtocolMock(@protocol(GULApplicationDelegate)); + OCMExpect([interceptor application:application + didFailToRegisterForRemoteNotificationsWithError:error]); + + id interceptor2 = OCMProtocolMock(@protocol(GULApplicationDelegate)); + OCMExpect([interceptor2 application:application + didFailToRegisterForRemoteNotificationsWithError:error]); + + GULTestAppDelegate *testAppDelegate = [[GULTestAppDelegate alloc] init]; + OCMStub([self.mockSharedApplication delegate]).andReturn(testAppDelegate); + [GULAppDelegateSwizzler proxyOriginalDelegateIncludingAPNSMethods]; + + [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor]; + [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor2]; + + [testAppDelegate application:application didFailToRegisterForRemoteNotificationsWithError:error]; + OCMVerifyAll(interceptor); + OCMVerifyAll(interceptor2); + + XCTAssertEqual(testAppDelegate.application, application); + XCTAssertEqual(testAppDelegate.failToRegisterForRemoteNotificationsError, error); +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +- (void)testApplicationDidReceiveRemoteNotificationIsInvokedOnInterceptors { + NSDictionary *notification = @{}; + GULApplication *application = [GULApplication sharedApplication]; + + id interceptor = OCMProtocolMock(@protocol(GULApplicationDelegate)); + OCMExpect([interceptor application:application didReceiveRemoteNotification:notification]); + + id interceptor2 = OCMProtocolMock(@protocol(GULApplicationDelegate)); + OCMExpect([interceptor2 application:application didReceiveRemoteNotification:notification]); + + GULTestAppDelegate *testAppDelegate = [[GULTestAppDelegate alloc] init]; + OCMStub([self.mockSharedApplication delegate]).andReturn(testAppDelegate); + [GULAppDelegateSwizzler proxyOriginalDelegateIncludingAPNSMethods]; + + [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor]; + [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor2]; + + [testAppDelegate application:application didReceiveRemoteNotification:notification]; + OCMVerifyAll(interceptor); + OCMVerifyAll(interceptor2); + + XCTAssertEqual(testAppDelegate.application, application); + XCTAssertEqual(testAppDelegate.remoteNotification, notification); +} +#pragma clang diagnostic pop + +#if (TARGET_OS_IOS || TARGET_OS_TV) && !TARGET_OS_MACCATALYST +- (void)testApplicationDidReceiveRemoteNotificationWithCompletionIsInvokedOnInterceptors { + NSDictionary *notification = @{}; + GULApplication *application = [GULApplication sharedApplication]; + void (^completion)(UIBackgroundFetchResult) = ^(UIBackgroundFetchResult result) { + }; + + id interceptor = OCMProtocolMock(@protocol(GULApplicationDelegate)); + OCMExpect([interceptor application:application + didReceiveRemoteNotification:notification + fetchCompletionHandler:[OCMArg isNotNil]]); + + id interceptor2 = OCMProtocolMock(@protocol(GULApplicationDelegate)); + OCMExpect([interceptor2 application:application + didReceiveRemoteNotification:notification + fetchCompletionHandler:[OCMArg isNotNil]]); + + GULTestAppDelegate *testAppDelegate = [[GULTestAppDelegate alloc] init]; + OCMStub([self.mockSharedApplication delegate]).andReturn(testAppDelegate); + [GULAppDelegateSwizzler proxyOriginalDelegateIncludingAPNSMethods]; + + [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor]; + [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor2]; + + [testAppDelegate application:application + didReceiveRemoteNotification:notification + fetchCompletionHandler:completion]; + OCMVerifyAll(interceptor); + OCMVerifyAll(interceptor2); + + XCTAssertEqual(testAppDelegate.application, application); + XCTAssertEqual(testAppDelegate.remoteNotification, notification); +} + +- (void)verifyCompletionCalledForObserverResult:(UIBackgroundFetchResult)observerResult1 + anotherObserverResult:(UIBackgroundFetchResult)observerResult2 + swizzledResult:(UIBackgroundFetchResult)swizzledResult + expectedResult:(UIBackgroundFetchResult)expectedResult { + NSDictionary *notification = @{}; + GULApplication *application = [GULApplication sharedApplication]; + + XCTestExpectation *completionExpectation = + [[XCTestExpectation alloc] initWithDescription:@"Completion called once"]; + + void (^completion)(UIBackgroundFetchResult) = ^(UIBackgroundFetchResult result) { + XCTAssertEqual(result, expectedResult); + [completionExpectation fulfill]; + }; + + void (^onDidReceiveRemoteNotification1)(NSInvocation *invocation) = ^(NSInvocation *invocation) { + void __unsafe_unretained (^localCompletionHandler)(UIBackgroundFetchResult) = nil; + [invocation getArgument:(void *)(&localCompletionHandler) atIndex:4]; + XCTAssertNotNil(localCompletionHandler); + localCompletionHandler(observerResult1); + }; + + id interceptor = OCMProtocolMock(@protocol(GULApplicationDelegate)); + OCMExpect([interceptor application:application + didReceiveRemoteNotification:notification + fetchCompletionHandler:[OCMArg isNotNil]]) + .andDo(onDidReceiveRemoteNotification1); + + void (^onDidReceiveRemoteNotification2)(NSInvocation *invocation) = ^(NSInvocation *invocation) { + void __unsafe_unretained (^localCompletionHandler)(UIBackgroundFetchResult) = nil; + [invocation getArgument:(void *)(&localCompletionHandler) atIndex:4]; + XCTAssertNotNil(localCompletionHandler); + localCompletionHandler(observerResult2); + }; + + id interceptor2 = OCMProtocolMock(@protocol(GULApplicationDelegate)); + OCMExpect([interceptor2 application:application + didReceiveRemoteNotification:notification + fetchCompletionHandler:[OCMArg isNotNil]]) + .andDo(onDidReceiveRemoteNotification2); + + GULTestAppDelegate *testAppDelegate = [[GULTestAppDelegate alloc] init]; + OCMStub([self.mockSharedApplication delegate]).andReturn(testAppDelegate); + [GULAppDelegateSwizzler proxyOriginalDelegateIncludingAPNSMethods]; + + [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor]; + [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor2]; + + [testAppDelegate application:application + didReceiveRemoteNotification:notification + fetchCompletionHandler:completion]; + testAppDelegate.remoteNotificationCompletionHandler(swizzledResult); + OCMVerifyAll(interceptor); + OCMVerifyAll(interceptor2); + [self waitForExpectations:@[ completionExpectation ] timeout:0.1]; +} + +- (void)testApplicationDidReceiveRemoteNotificationWithCompletionCompletionIsCalledOnce { + [self verifyCompletionCalledForObserverResult:UIBackgroundFetchResultNoData + anotherObserverResult:UIBackgroundFetchResultNoData + swizzledResult:UIBackgroundFetchResultNoData + expectedResult:UIBackgroundFetchResultNoData]; +} + +- (void) + testApplicationDidReceiveRemoteNotificationWithCompletionCompletionIsCalledOnce_HandleFailedState { + [self verifyCompletionCalledForObserverResult:UIBackgroundFetchResultFailed + anotherObserverResult:UIBackgroundFetchResultFailed + swizzledResult:UIBackgroundFetchResultFailed + expectedResult:UIBackgroundFetchResultFailed]; +} + +- (void)testApplicationDidReceiveRemoteNotificationWithCompletionCompletionIsCalledOnce_NoData { + [self verifyCompletionCalledForObserverResult:UIBackgroundFetchResultNoData + anotherObserverResult:UIBackgroundFetchResultFailed + swizzledResult:UIBackgroundFetchResultFailed + expectedResult:UIBackgroundFetchResultNoData]; +} +- (void) + testApplicationDidReceiveRemoteNotificationWithCompletionCompletionIsCalledOnce_HandleNewDataState_OthersFailed { + [self verifyCompletionCalledForObserverResult:UIBackgroundFetchResultNewData + anotherObserverResult:UIBackgroundFetchResultFailed + swizzledResult:UIBackgroundFetchResultFailed + expectedResult:UIBackgroundFetchResultNewData]; +} + +- (void) + testApplicationDidReceiveRemoteNotificationWithCompletionCompletionIsCalledOnce_HandleNewDataState_OthersNoData { + [self verifyCompletionCalledForObserverResult:UIBackgroundFetchResultNewData + anotherObserverResult:UIBackgroundFetchResultNoData + swizzledResult:UIBackgroundFetchResultNoData + expectedResult:UIBackgroundFetchResultNewData]; +} + +- (void) + testApplicationDidReceiveRemoteNotificationWithCompletionCompletionIsCalledOnce_HandleNewDataState_OthersNoDataFailed { + [self verifyCompletionCalledForObserverResult:UIBackgroundFetchResultNewData + anotherObserverResult:UIBackgroundFetchResultNoData + swizzledResult:UIBackgroundFetchResultFailed + expectedResult:UIBackgroundFetchResultNewData]; +} + +- (void)testApplicationDidReceiveRemoteNotificationWithCompletionImplementationIsNotAdded { + // The delegate without application:didReceiveRemoteNotification:fetchCompletionHandler: + // implementation + GULTestInterceptorAppDelegate *legacyDelegate = [[GULTestInterceptorAppDelegate alloc] init]; + OCMStub([self.mockSharedApplication delegate]).andReturn(legacyDelegate); + + XCTAssertFalse([legacyDelegate + respondsToSelector:@selector(application: + didReceiveRemoteNotification:fetchCompletionHandler:)]); + + [GULAppDelegateSwizzler proxyOriginalDelegateIncludingAPNSMethods]; + + XCTAssertFalse([legacyDelegate + respondsToSelector:@selector(application: + didReceiveRemoteNotification:fetchCompletionHandler:)]); +} +#endif // TARGET_OS_IOS || TARGET_OS_TV + +#pragma mark - Tests to test that Plist flag is honored + +/** Tests that app delegate proxy is enabled when there is no Info.plist dictionary. */ +- (void)testAppProxyPlistFlag_NoFlag { + // No keys anywhere. If there is no key, the default should be enabled. + NSDictionary *mainDictionary = nil; + id mainBundleMock = OCMPartialMock([NSBundle mainBundle]); + [[[mainBundleMock expect] andReturn:mainDictionary] infoDictionary]; + + XCTAssertTrue([GULAppDelegateSwizzler isAppDelegateProxyEnabled]); + [mainBundleMock stopMocking]; +} + +/** Tests that app delegate proxy is enabled when there is neither the Firebase nor the non-Firebase + * Info.plist key present. + */ +- (void)testAppProxyPlistFlag_NoAppDelegateProxyKey { + // No app delegate disable key. If there is no key, the default should be enabled. + NSDictionary *mainDictionary = @{@"randomKey" : @"randomValue"}; + id mainBundleMock = OCMPartialMock([NSBundle mainBundle]); + [[[mainBundleMock expect] andReturn:mainDictionary] infoDictionary]; + + XCTAssertTrue([GULAppDelegateSwizzler isAppDelegateProxyEnabled]); + [mainBundleMock stopMocking]; +} + +/** Tests that app delegate proxy is enabled when the Firebase plist is explicitly set to YES and + * the Google flag is not present. */ +- (void)testAppProxyPlistFlag_FirebaseEnabled { + // Set proxy enabled to YES. + NSDictionary *mainDictionary = @{kGULFirebaseAppDelegateProxyEnabledPlistKey : @(YES)}; + id mainBundleMock = OCMPartialMock([NSBundle mainBundle]); + [[[mainBundleMock expect] andReturn:mainDictionary] infoDictionary]; + + XCTAssertTrue([GULAppDelegateSwizzler isAppDelegateProxyEnabled]); + [mainBundleMock stopMocking]; +} + +/** Tests that app delegate proxy is enabled when the Google plist is explicitly set to YES and the + * Firebase flag is not present. */ +- (void)testAppProxyPlistFlag_GoogleEnabled { + // Set proxy enabled to YES. + NSDictionary *mainDictionary = @{kGULGoogleAppDelegateProxyEnabledPlistKey : @(YES)}; + id mainBundleMock = OCMPartialMock([NSBundle mainBundle]); + [[[mainBundleMock expect] andReturn:mainDictionary] infoDictionary]; + + XCTAssertTrue([GULAppDelegateSwizzler isAppDelegateProxyEnabled]); + [mainBundleMock stopMocking]; +} + +/** Tests that the App Delegate proxy is enabled when the Firebase flag has the wrong type of value + * and the Google flag is not present. */ +- (void)testAppProxyPlist_WrongFirebaseDisableFlagValueType { + // Set proxy enabled to "NO" - a string. + NSDictionary *mainDictionary = @{kGULFirebaseAppDelegateProxyEnabledPlistKey : @"NO"}; + id mainBundleMock = OCMPartialMock([NSBundle mainBundle]); + [[[mainBundleMock expect] andReturn:mainDictionary] infoDictionary]; + + XCTAssertTrue([GULAppDelegateSwizzler isAppDelegateProxyEnabled]); + [mainBundleMock stopMocking]; +} + +/** Tests that the App Delegate proxy is enabled when the Google flag has the wrong type of value + * and the Firebase flag is not present. */ +- (void)testAppProxyPlist_WrongGoogleDisableFlagValueType { + // Set proxy enabled to "NO" - a string. + NSDictionary *mainDictionary = @{kGULGoogleAppDelegateProxyEnabledPlistKey : @"NO"}; + id mainBundleMock = OCMPartialMock([NSBundle mainBundle]); + [[[mainBundleMock expect] andReturn:mainDictionary] infoDictionary]; + + XCTAssertTrue([GULAppDelegateSwizzler isAppDelegateProxyEnabled]); + [mainBundleMock stopMocking]; +} + +/** Tests that the App Delegate proxy is disabled when the Firebase flag is set to NO and the Google + * flag is not present. */ +- (void)testAppProxyPlist_FirebaseDisableFlag { + // Set proxy enabled to NO. + NSDictionary *mainDictionary = @{kGULFirebaseAppDelegateProxyEnabledPlistKey : @(NO)}; + id mainBundleMock = OCMPartialMock([NSBundle mainBundle]); + [[[mainBundleMock expect] andReturn:mainDictionary] infoDictionary]; + + XCTAssertFalse([GULAppDelegateSwizzler isAppDelegateProxyEnabled]); + [mainBundleMock stopMocking]; +} + +/** Tests that the App Delegate proxy is disabled when the Google flag is set to NO and the Firebase + * flag is not present. */ +- (void)testAppProxyPlist_GoogleDisableFlag { + // Set proxy enabled to NO. + NSDictionary *mainDictionary = @{kGULGoogleAppDelegateProxyEnabledPlistKey : @(NO)}; + id mainBundleMock = OCMPartialMock([NSBundle mainBundle]); + [[[mainBundleMock expect] andReturn:mainDictionary] infoDictionary]; + + XCTAssertFalse([GULAppDelegateSwizzler isAppDelegateProxyEnabled]); + [mainBundleMock stopMocking]; +} + +/** Tests that the App Delegate proxy is disabled when the Google flag is set to NO and the Firebase + * flag is set to YES. */ +- (void)testAppProxyPlist_GoogleDisableFlagFirebaseEnableFlag { + // Set proxy enabled to NO. + NSDictionary *mainDictionary = @{ + kGULGoogleAppDelegateProxyEnabledPlistKey : @(NO), + kGULFirebaseAppDelegateProxyEnabledPlistKey : @(YES) + }; + id mainBundleMock = OCMPartialMock([NSBundle mainBundle]); + [[[mainBundleMock expect] andReturn:mainDictionary] infoDictionary]; + + XCTAssertFalse([GULAppDelegateSwizzler isAppDelegateProxyEnabled]); + [mainBundleMock stopMocking]; +} + +/** Tests that the App Delegate proxy is disabled when the Google flag is set to NO and the Firebase + * flag is set to YES. */ +- (void)testAppProxyPlist_FirebaseDisableFlagGoogleEnableFlag { + // Set proxy enabled to NO. + NSDictionary *mainDictionary = @{ + kGULGoogleAppDelegateProxyEnabledPlistKey : @(YES), + kGULFirebaseAppDelegateProxyEnabledPlistKey : @(NO) + }; + id mainBundleMock = OCMPartialMock([NSBundle mainBundle]); + [[[mainBundleMock expect] andReturn:mainDictionary] infoDictionary]; + + XCTAssertFalse([GULAppDelegateSwizzler isAppDelegateProxyEnabled]); + [mainBundleMock stopMocking]; +} + +/** Tests that the App Delegate proxy is disabled when the Google flag is set to NO and the Firebase + * flag is set to NO. */ +- (void)testAppProxyPlist_FirebaseDisableFlagGoogleDisableFlag { + // Set proxy enabled to NO. + NSDictionary *mainDictionary = @{ + kGULGoogleAppDelegateProxyEnabledPlistKey : @(NO), + kGULFirebaseAppDelegateProxyEnabledPlistKey : @(NO) + }; + id mainBundleMock = OCMPartialMock([NSBundle mainBundle]); + [[[mainBundleMock expect] andReturn:mainDictionary] infoDictionary]; + + XCTAssertFalse([GULAppDelegateSwizzler isAppDelegateProxyEnabled]); + [mainBundleMock stopMocking]; +} + +/** Tests that the App Delegate is not proxied when it is disabled. */ +- (void)testAppDelegateIsNotProxiedWhenDisabled { + // Set proxy enabled to NO. + NSDictionary *mainDictionary = @{kGULFirebaseAppDelegateProxyEnabledPlistKey : @(NO)}; + id mainBundleMock = OCMPartialMock([NSBundle mainBundle]); + [[[mainBundleMock stub] andReturn:mainDictionary] infoDictionary]; + XCTAssertFalse([GULAppDelegateSwizzler isAppDelegateProxyEnabled]); + + id originalAppDelegate = OCMProtocolMock(@protocol(GULApplicationDelegate)); + Class originalAppDelegateClass = [originalAppDelegate class]; + XCTAssertNotNil(originalAppDelegate); + OCMStub([self.mockSharedApplication delegate]).andReturn(originalAppDelegate); + + [GULAppDelegateSwizzler proxyOriginalDelegate]; + [GULAppDelegateSwizzler proxyOriginalDelegateIncludingAPNSMethods]; + XCTAssertEqualObjects([originalAppDelegate class], originalAppDelegateClass); + + [mainBundleMock stopMocking]; +} + +// TODO(tejasd): There is some weirdness that happens (at least when running this locally on Xcode) +// where the actual app delegate is nilled out in one of these tests, causing the tests to fail. +// Disabling this test seems to fix the problem. + +/** Tests that the App Delegate is proxied when it is enabled. */ +- (void)testAppDelegateIsProxiedWhenEnabled { + // App Delegate Proxying is enabled by default. + XCTAssertTrue([GULAppDelegateSwizzler isAppDelegateProxyEnabled]); + + id originalAppDelegate = [[GULTestAppDelegate alloc] init]; + Class originalAppDelegateClass = [originalAppDelegate class]; + XCTAssertNotNil(originalAppDelegate); + OCMStub([self.mockSharedApplication delegate]).andReturn(originalAppDelegate); + + [GULAppDelegateSwizzler proxyOriginalDelegate]; + XCTAssertNotEqualObjects([originalAppDelegate class], originalAppDelegateClass); +} + +- (void)testAppDelegateIsProxiedIncludingAPNSMethodsWhenEnabled { + // App Delegate Proxying is enabled by default. + XCTAssertTrue([GULAppDelegateSwizzler isAppDelegateProxyEnabled]); + + id originalAppDelegate = [[GULTestAppDelegate alloc] init]; + Class originalAppDelegateClass = [originalAppDelegate class]; + XCTAssertNotNil(originalAppDelegate); + OCMStub([self.mockSharedApplication delegate]).andReturn(originalAppDelegate); + + [GULAppDelegateSwizzler proxyOriginalDelegateIncludingAPNSMethods]; + XCTAssertNotEqualObjects([originalAppDelegate class], originalAppDelegateClass); +} + +@end