diff --git a/Sources/MessagingPush/PushHandling/PushEventHandler.swift b/Sources/MessagingPush/PushHandling/PushEventHandler.swift index 574133ef9..a56c6ad82 100644 --- a/Sources/MessagingPush/PushHandling/PushEventHandler.swift +++ b/Sources/MessagingPush/PushHandling/PushEventHandler.swift @@ -4,6 +4,10 @@ import Foundation // A protocol that can handle push notification events. Such as when a push is received on the device or when a push is clicked on. // Note: This is meant to be an abstraction of the iOS `UNUserNotificationCenterDelegate` protocol. protocol PushEventHandler: AutoMockable { + // The SDK manages multiple push event handlers. We need a way to differentiate them between one another. + // The return value should uniquely identify the handler from other handlers installed in the app. + var identifier: String { get } + // Called when a push notification was acted upon. Either clicked or swiped away. // // Replacement of: `userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void)` diff --git a/Sources/MessagingPush/PushHandling/PushEventHandlerProxy.swift b/Sources/MessagingPush/PushHandling/PushEventHandlerProxy.swift index 80393efa4..bb87b4e23 100644 --- a/Sources/MessagingPush/PushHandling/PushEventHandlerProxy.swift +++ b/Sources/MessagingPush/PushHandling/PushEventHandlerProxy.swift @@ -27,7 +27,7 @@ class PushEventHandlerProxyImpl: PushEventHandlerProxy { public static let shared = PushEventHandlerProxyImpl() // Use a map so that we only save 1 instance of a given handler. - @Atomic private var nestedDelegates: [String: PushEventHandler] = [:] + @Atomic var nestedDelegates: [String: PushEventHandler] = [:] private let logger: Logger? @@ -37,7 +37,7 @@ class PushEventHandlerProxyImpl: PushEventHandlerProxy { } func addPushEventHandler(_ newHandler: PushEventHandler) { - nestedDelegates[String(describing: newHandler)] = newHandler + nestedDelegates[newHandler.identifier] = newHandler } func onPushAction(_ pushAction: PushNotificationAction, completionHandler: @escaping () -> Void) { @@ -65,11 +65,11 @@ class PushEventHandlerProxyImpl: PushEventHandlerProxy { // Using logs to give feedback to customer if 1 or more delegates do not call the async completion handler. // These logs could help in debuggging to determine what delegate did not call the completion handler. - self.logger?.info("Sending push notification, \(pushAction.push.title), event to: \(nameOfDelegateClass)). Customer.io SDK will wait for async completion handler to be called...") + self.logger?.info("Sending push notification, \(pushAction.push.title), action event to: \(nameOfDelegateClass)). Customer.io SDK will wait for async completion handler to be called...") delegate.onPushAction(pushAction) { Task { @MainActor in // in case the delegate calls the completion handler on a background thread, we need to switch back to the main thread. - self.logger?.info("Received async completion handler from \(nameOfDelegateClass).") + self.logger?.info("Received async completion handler from \(nameOfDelegateClass) for action push event.") if !hasResumed { hasResumed = true @@ -116,11 +116,11 @@ class PushEventHandlerProxyImpl: PushEventHandlerProxy { // Using logs to give feedback to customer if 1 or more delegates do not call the async completion handler. // These logs could help in debuggging to determine what delegate did not call the completion handler. - self.logger?.info("Sending push notification, \(push.title), event to: \(nameOfDelegateClass)). Customer.io SDK will wait for async completion handler to be called...") + self.logger?.info("Sending push notification, \(push.title), will display event to: \(nameOfDelegateClass)). Customer.io SDK will wait for async completion handler to be called...") delegate.shouldDisplayPushAppInForeground(push, completionHandler: { delegateShouldDisplayPushResult in Task { @MainActor in // in case the delegate calls the completion handler on a background thread, we need to switch back to the main thread. - self.logger?.info("Received async completion handler from \(nameOfDelegateClass).") + self.logger?.info("Received async completion handler from \(nameOfDelegateClass) for will display event.") if !hasResumed { hasResumed = true diff --git a/Sources/MessagingPush/PushHandling/iOSPushEventListener.swift b/Sources/MessagingPush/PushHandling/iOSPushEventListener.swift index bc04c6561..b5d9cbc57 100644 --- a/Sources/MessagingPush/PushHandling/iOSPushEventListener.swift +++ b/Sources/MessagingPush/PushHandling/iOSPushEventListener.swift @@ -21,6 +21,10 @@ class IOSPushEventListener: PushEventHandler { self.logger = logger } + var identifier: String { + "Cio.iOSPushEventListener" + } + func onPushAction(_ pushAction: PushNotificationAction, completionHandler: @escaping () -> Void) { guard let dateWhenPushDelivered = pushAction.push.deliveryDate else { return diff --git a/Sources/MessagingPush/UserNotificationsFramework/Wrappers.swift b/Sources/MessagingPush/UserNotificationsFramework/Wrappers.swift index ceefbf992..48a97726f 100644 --- a/Sources/MessagingPush/UserNotificationsFramework/Wrappers.swift +++ b/Sources/MessagingPush/UserNotificationsFramework/Wrappers.swift @@ -117,9 +117,19 @@ public struct UNNotificationWrapper: PushNotification { } } -class UNUserNotificationCenterDelegateWrapper: PushEventHandler { +class UNUserNotificationCenterDelegateWrapper: PushEventHandler, CustomStringConvertible { private let delegate: UNUserNotificationCenterDelegate + var description: String { + let nestedDelegateDescription = String(describing: delegate) + + return "Cio.NotificationCenterDelegateWrapper(\(nestedDelegateDescription))" + } + + var identifier: String { + String(describing: delegate) + } + init(delegate: UNUserNotificationCenterDelegate) { self.delegate = delegate } diff --git a/Sources/MessagingPush/autogenerated/AutoMockable.generated.swift b/Sources/MessagingPush/autogenerated/AutoMockable.generated.swift index e816d6f17..0c7523aba 100644 --- a/Sources/MessagingPush/autogenerated/AutoMockable.generated.swift +++ b/Sources/MessagingPush/autogenerated/AutoMockable.generated.swift @@ -563,7 +563,45 @@ class PushEventHandlerMock: PushEventHandler, Mock { Mocks.shared.add(mock: self) } + /** + When setter of the property called, the value given to setter is set here. + When the getter of the property called, the value set here will be returned. Your chance to mock the property. + */ + var underlyingIdentifier: String! + /// `true` if the getter or setter of property is called at least once. + var identifierCalled: Bool { + identifierGetCalled || identifierSetCalled + } + + /// `true` if the getter called on the property at least once. + var identifierGetCalled: Bool { + identifierGetCallsCount > 0 + } + + var identifierGetCallsCount = 0 + /// `true` if the setter called on the property at least once. + var identifierSetCalled: Bool { + identifierSetCallsCount > 0 + } + + var identifierSetCallsCount = 0 + /// The mocked property with a getter and setter. + var identifier: String { + get { + mockCalled = true + identifierGetCallsCount += 1 + return underlyingIdentifier + } + set(value) { + mockCalled = true + identifierSetCallsCount += 1 + underlyingIdentifier = value + } + } + public func resetMock() { + identifierGetCallsCount = 0 + identifierSetCallsCount = 0 onPushActionCallsCount = 0 onPushActionReceivedArguments = nil onPushActionReceivedInvocations = [] diff --git a/Tests/MessagingPush/IntegrationTest.swift b/Tests/MessagingPush/IntegrationTest.swift index 406e180d5..4a9a52920 100644 --- a/Tests/MessagingPush/IntegrationTest.swift +++ b/Tests/MessagingPush/IntegrationTest.swift @@ -27,4 +27,12 @@ class IntegrationTest: SharedTests.IntegrationTest { MessagingPush.initialize() } } + + // Create new mock instance and setup with set of defaults. + func getNewPushEventHandler() -> PushEventHandlerMock { + let newInstance = PushEventHandlerMock() + // We expect that each instance has it's own unique identifier. + newInstance.underlyingIdentifier = .random + return newInstance + } } diff --git a/Tests/MessagingPush/PushHandling/AutomaticPushClickedIntegrationTest.swift b/Tests/MessagingPush/PushHandling/AutomaticPushClickedIntegrationTest.swift index 84f15ec88..426dcb232 100644 --- a/Tests/MessagingPush/PushHandling/AutomaticPushClickedIntegrationTest.swift +++ b/Tests/MessagingPush/PushHandling/AutomaticPushClickedIntegrationTest.swift @@ -71,7 +71,7 @@ class AutomaticPushClickedIntegrationTest: IntegrationTest { func test_givenOtherPushHandlers_givenClickedOnCioPush_expectPushClickHandledByCioSdk() { let expectOtherClickHandlerToGetCallback = expectation(description: "Receive a callback") - let givenOtherPushHandler = PushEventHandlerMock() + let givenOtherPushHandler = getNewPushEventHandler() givenOtherPushHandler.onPushActionClosure = { _, onComplete in expectOtherClickHandlerToGetCallback.fulfill() onComplete() @@ -88,7 +88,7 @@ class AutomaticPushClickedIntegrationTest: IntegrationTest { func test_givenOtherPushHandlers_givenClickedOnPushNotSentFromCio_expectPushClickHandledByOtherHandler() { let givenPush = PushNotificationStub.getPushNotSentFromCIO() - let givenOtherPushHandler = PushEventHandlerMock() + let givenOtherPushHandler = getNewPushEventHandler() let expectOtherClickHandlerHandlesPush = expectation(description: "Other push handler should handle push.") givenOtherPushHandler.onPushActionClosure = { _, onComplete in @@ -106,21 +106,17 @@ class AutomaticPushClickedIntegrationTest: IntegrationTest { // Important to test that 2+ 3rd party push handlers for some use cases. func test_givenMultiplePushHandlers_givenClickedOnCioPush_expectPushClickHandledByCioSdk() { - let expectOtherPushHandlersCalled = expectation(description: "Receive a callback") - expectOtherPushHandlersCalled.expectedFulfillmentCount = 2 + let expectHandler1Called = expectation(description: "Receive a callback") + let expectHandler2Called = expectation(description: "Receive a callback") - // In order to add 2+ push handlers to SDK, each class needs to have a unique name. - // The SDK only accepts unique push event handlers. Creating this class makes each push handler unique. - class PushEventHandlerMock2: PushEventHandlerMock {} - - let givenOtherPushHandler1 = PushEventHandlerMock() - let givenOtherPushHandler2 = PushEventHandlerMock2() + let givenOtherPushHandler1 = getNewPushEventHandler() + let givenOtherPushHandler2 = getNewPushEventHandler() givenOtherPushHandler1.onPushActionClosure = { _, onComplete in - expectOtherPushHandlersCalled.fulfill() + expectHandler1Called.fulfill() onComplete() } givenOtherPushHandler2.onPushActionClosure = { _, onComplete in - expectOtherPushHandlersCalled.fulfill() + expectHandler2Called.fulfill() onComplete() } addOtherPushEventHandler(givenOtherPushHandler1) @@ -142,7 +138,7 @@ class AutomaticPushClickedIntegrationTest: IntegrationTest { func test_givenMultiplePushHandlers_givenClickedOnCioPush_givenOtherPushHandlerDoesNotCallCompletionHandler_expectCompletionHandlerDoesNotGetCalled() { let expectOtherClickHandlerToGetCallback = expectation(description: "Receive a callback") let givenPush = PushNotificationStub.getPushSentFromCIO() - let givenOtherPushHandler = PushEventHandlerMock() + let givenOtherPushHandler = getNewPushEventHandler() givenOtherPushHandler.onPushActionClosure = { _, _ in // Do not call completion handler. @@ -161,7 +157,7 @@ class AutomaticPushClickedIntegrationTest: IntegrationTest { func test_givenMultiplePushHandlers_givenClickedOnCioPush_givenOtherPushHandlerCallsCompletionHandler_expectCioSdkHandlesPush() { let givenPush = PushNotificationStub.getPushSentFromCIO() - let givenOtherPushHandler = PushEventHandlerMock() + let givenOtherPushHandler = getNewPushEventHandler() givenOtherPushHandler.onPushActionClosure = { _, onComplete in onComplete() } @@ -187,7 +183,7 @@ class AutomaticPushClickedIntegrationTest: IntegrationTest { func test_onPushAction_givenMultiplePushClickHandlers_simulateFcmSdkSwizzlingBehavior_expectNoInfiniteLoop() { let givenPush = PushNotificationStub.getPushNotSentFromCIO() - let givenOtherPushHandler = PushEventHandlerMock() + let givenOtherPushHandler = getNewPushEventHandler() let givenPushClickAction = PushNotificationActionStub(push: givenPush, didClickOnPush: true) let expectOtherClickHandlerHandlesPush = expectation(description: "Other push handler should handle push.") @@ -209,7 +205,7 @@ class AutomaticPushClickedIntegrationTest: IntegrationTest { func test_shouldDisplayPushAppInForeground_givenMultiplePushClickHandlers_simulateFcmSdkSwizzlingBehavior_expectNoInfiniteLoop() { let givenPush = PushNotificationStub.getPushNotSentFromCIO() - let givenOtherPushHandler = PushEventHandlerMock() + let givenOtherPushHandler = getNewPushEventHandler() let expectOtherClickHandlerHandlesPush = expectation(description: "Other push handler should handle push.") expectOtherClickHandlerHandlesPush.expectedFulfillmentCount = 1 // the other push click handler should only be called once, indicating an infinite loop is not created. @@ -240,7 +236,7 @@ class AutomaticPushClickedIntegrationTest: IntegrationTest { func test_onPushAction_givenMultiplePushClickHandlers_thirdPartySdkCallsCompletionHandlerTwice_expectSdkDoesNotCrash() { let givenPush = PushNotificationStub.getPushNotSentFromCIO() - let givenOtherPushHandler = PushEventHandlerMock() + let givenOtherPushHandler = getNewPushEventHandler() let givenPushClickAction = PushNotificationActionStub(push: givenPush, didClickOnPush: true) let expectOtherClickHandlerHandlesPush = expectation(description: "Other push handler should handle push.") @@ -262,7 +258,7 @@ class AutomaticPushClickedIntegrationTest: IntegrationTest { func test_shouldDisplayPushAppInForeground_givenMultiplePushClickHandlers_thirdPartySdkCallsCompletionHandlerTwice_expectSdkDoesNotCrash() { let givenPush = PushNotificationStub.getPushNotSentFromCIO() - let givenOtherPushHandler = PushEventHandlerMock() + let givenOtherPushHandler = getNewPushEventHandler() let expectOtherClickHandlerHandlesPush = expectation(description: "Other push handler should handle push.") expectOtherClickHandlerHandlesPush.expectedFulfillmentCount = 1 // the other push click handler should only be called once, indicating an infinite loop is not created. @@ -294,7 +290,7 @@ class AutomaticPushClickedIntegrationTest: IntegrationTest { func test_givenClickOnLocalPush_expectOtherClickHandlerHandlesClickEvent() { let givenLocalPush = PushNotificationStub.getLocalPush(pushId: .random) - let givenOtherPushHandler = PushEventHandlerMock() + let givenOtherPushHandler = getNewPushEventHandler() let expectOtherClickHandlerHandlesPush = expectation(description: "Other push handler should handle push.") expectOtherClickHandlerHandlesPush.expectedFulfillmentCount = 1 givenOtherPushHandler.onPushActionClosure = { _, onComplete in @@ -314,7 +310,7 @@ class AutomaticPushClickedIntegrationTest: IntegrationTest { let givenLocalPush = PushNotificationStub.getLocalPush(pushId: givenHardCodedPushId) let givenSecondLocalPush = PushNotificationStub.getLocalPush(pushId: givenHardCodedPushId) - let givenOtherPushHandler = PushEventHandlerMock() + let givenOtherPushHandler = getNewPushEventHandler() let expectOtherClickHandlerHandlesPush = expectation(description: "Other push handler should handle push.") expectOtherClickHandlerHandlesPush.expectedFulfillmentCount = 2 // Expect click handler to be able to handle both pushes, because each push is unique. givenOtherPushHandler.onPushActionClosure = { _, onComplete in diff --git a/Tests/MessagingPush/PushHandling/AutomaticPushDeliveredAppInForegroundTest.swift b/Tests/MessagingPush/PushHandling/AutomaticPushDeliveredAppInForegroundTest.swift index f9bac33df..ca6248549 100644 --- a/Tests/MessagingPush/PushHandling/AutomaticPushDeliveredAppInForegroundTest.swift +++ b/Tests/MessagingPush/PushHandling/AutomaticPushDeliveredAppInForegroundTest.swift @@ -52,7 +52,7 @@ class AutomaticPushDeliveredAppInForegrondTest: IntegrationTest { let givenPush = PushNotificationStub.getPushNotSentFromCIO() var otherPushHandlerCalled = false - let givenOtherPushHandler = PushEventHandlerMock() + let givenOtherPushHandler = getNewPushEventHandler() givenOtherPushHandler.shouldDisplayPushAppInForegroundClosure = { _, onComplete in otherPushHandlerCalled = true @@ -70,7 +70,7 @@ class AutomaticPushDeliveredAppInForegrondTest: IntegrationTest { let givenPush = PushNotificationStub.getPushSentFromCIO() let expectOtherPushHandlerCallbackCalled = expectation(description: "Expect other push handler callback called") - let givenOtherPushHandler = PushEventHandlerMock() + let givenOtherPushHandler = getNewPushEventHandler() givenOtherPushHandler.shouldDisplayPushAppInForegroundClosure = { _, onComplete in // We expect that other push handler gets callback of push event from CIO push expectOtherPushHandlerCallbackCalled.fulfill() @@ -95,12 +95,8 @@ class AutomaticPushDeliveredAppInForegrondTest: IntegrationTest { let expectOtherPushHandlerCallbackCalled = expectation(description: "Expect other push handler callback called") expectOtherPushHandlerCallbackCalled.expectedFulfillmentCount = 2 - // In order to add 2+ push handlers to SDK, each class needs to have a unique name. - // The SDK only accepts unique push event handlers. Creating this class makes each push handler unique. - class PushEventHandlerMock2: PushEventHandlerMock {} - - let givenOtherPushHandler1 = PushEventHandlerMock() - let givenOtherPushHandler2 = PushEventHandlerMock2() + let givenOtherPushHandler1 = getNewPushEventHandler() + let givenOtherPushHandler2 = getNewPushEventHandler() givenOtherPushHandler1.shouldDisplayPushAppInForegroundClosure = { _, onComplete in expectOtherPushHandlerCallbackCalled.fulfill() @@ -126,7 +122,7 @@ class AutomaticPushDeliveredAppInForegrondTest: IntegrationTest { let expectOtherClickHandlerToGetCallback = expectation(description: "Receive a callback") let givenPush = PushNotificationStub.getPushSentFromCIO() - let givenOtherPushHandler = PushEventHandlerMock() + let givenOtherPushHandler = getNewPushEventHandler() givenOtherPushHandler.shouldDisplayPushAppInForegroundClosure = { _, _ in // Do not call completion handler. diff --git a/Tests/MessagingPush/PushHandling/PushEventHandlerProxyTest.swift b/Tests/MessagingPush/PushHandling/PushEventHandlerProxyTest.swift index de42aeeea..61cf61350 100644 --- a/Tests/MessagingPush/PushHandling/PushEventHandlerProxyTest.swift +++ b/Tests/MessagingPush/PushHandling/PushEventHandlerProxyTest.swift @@ -3,7 +3,7 @@ import Foundation import SharedTests import XCTest -class PushEventHandlerProxyTest: UnitTest { +class PushEventHandlerProxyTest: IntegrationTest { private var proxy: PushEventHandlerProxyImpl! override func setUp() { @@ -12,13 +12,47 @@ class PushEventHandlerProxyTest: UnitTest { proxy = PushEventHandlerProxyImpl() } + // MARK: addPushEventHandler + + func test_addPushEventHandler_givenMultipleNotificationCenterDelegateWrapperClasses_expectSaveBothDelegates() { + class Delegate1: NSObject, UNUserNotificationCenterDelegate {} + class Delegate2: NSObject, UNUserNotificationCenterDelegate {} + + XCTAssertEqual(proxy.nestedDelegates.count, 0) + proxy.addPushEventHandler(UNUserNotificationCenterDelegateWrapper(delegate: Delegate1())) + XCTAssertEqual(proxy.nestedDelegates.count, 1) + proxy.addPushEventHandler(UNUserNotificationCenterDelegateWrapper(delegate: Delegate2())) + XCTAssertEqual(proxy.nestedDelegates.count, 2) + } + + func test_addPushEventHandler_givenIdenticalNotificationCenterDelegates_expectSaveOnly1Delegate() { + class Delegate: NSObject, UNUserNotificationCenterDelegate {} + + let instance = Delegate() + + XCTAssertEqual(proxy.nestedDelegates.count, 0) + proxy.addPushEventHandler(UNUserNotificationCenterDelegateWrapper(delegate: instance)) + XCTAssertEqual(proxy.nestedDelegates.count, 1) + proxy.addPushEventHandler(UNUserNotificationCenterDelegateWrapper(delegate: instance)) + XCTAssertEqual(proxy.nestedDelegates.count, 1) + } + + func test_addPushEventHandler_givenNewInstancesSameObjectNotificationCenterDelegateWrappers_expectSaveBothDelegate() { + class Delegate: NSObject, UNUserNotificationCenterDelegate {} + + XCTAssertEqual(proxy.nestedDelegates.count, 0) + proxy.addPushEventHandler(UNUserNotificationCenterDelegateWrapper(delegate: Delegate())) + XCTAssertEqual(proxy.nestedDelegates.count, 1) + proxy.addPushEventHandler(UNUserNotificationCenterDelegateWrapper(delegate: Delegate())) + XCTAssertEqual(proxy.nestedDelegates.count, 2) + } + // MARK: thread safety func test_onPushAction_ensureThreadSafetyCallingDelegates() { runTest(numberOfTimes: 100) { // Ensure no race conditions by running test many times. - let delegate1 = PushEventHandlerMock() - class PushEventHandlerMock2: PushEventHandlerMock {} - let delegate2 = PushEventHandlerMock2() + let delegate1 = getNewPushEventHandler() + let delegate2 = getNewPushEventHandler() let expectDelegatesReceiveEvent = expectation(description: "delegate1 received event") expectDelegatesReceiveEvent.expectedFulfillmentCount = 2 // 1 for each delegate. We do not care what order the delegates get called as long as all get called. @@ -57,9 +91,8 @@ class PushEventHandlerProxyTest: UnitTest { runTest(numberOfTimes: 100) { // Ensure no race conditions by running test many times. let givenPush = PushNotificationStub.getPushSentFromCIO() - let delegate1 = PushEventHandlerMock() - class PushEventHandlerMock2: PushEventHandlerMock {} - let delegate2 = PushEventHandlerMock2() + let delegate1 = getNewPushEventHandler() + let delegate2 = getNewPushEventHandler() let expectDelegatesReceiveEvent = expectation(description: "delegate1 received event") expectDelegatesReceiveEvent.expectedFulfillmentCount = 2 // 1 for each delegate. We do not care what order the delegates get called as long as all get called. @@ -114,7 +147,7 @@ class PushEventHandlerProxyTest: UnitTest { let push = PushNotificationStub.getPushSentFromCIO() var actual: Bool! - let handler = PushEventHandlerMock() + let handler = getNewPushEventHandler() handler.shouldDisplayPushAppInForegroundClosure = { _, onComplete in onComplete(true) } @@ -138,7 +171,7 @@ class PushEventHandlerProxyTest: UnitTest { // First, test that `false` is the default return result. - let handler1 = PushEventHandlerMock() + let handler1 = getNewPushEventHandler() handler1.shouldDisplayPushAppInForegroundClosure = { _, onComplete in onComplete(false) } @@ -155,8 +188,7 @@ class PushEventHandlerProxyTest: UnitTest { // Next, add another push handler that's return result is `true`. // We expect return result to now be `true`, since 1 handler returned `true`. - class PushEventHandlerMock2: PushEventHandlerMock {} - let handler2 = PushEventHandlerMock2() + let handler2 = getNewPushEventHandler() handler2.shouldDisplayPushAppInForegroundClosure = { _, onComplete in onComplete(true) } @@ -171,8 +203,7 @@ class PushEventHandlerProxyTest: UnitTest { XCTAssertTrue(actual) // Finally, check that once 1 handler returns `true`, the return result is always `true`. - class PushEventHandlerMock3: PushEventHandlerMock {} - let handler3 = PushEventHandlerMock3() + let handler3 = getNewPushEventHandler() handler3.shouldDisplayPushAppInForegroundClosure = { _, onComplete in onComplete(false) }