From 62a9a71b42daf91e86f57e132d3d3ee3adf3b2e5 Mon Sep 17 00:00:00 2001 From: Andrey K Date: Tue, 30 May 2017 13:22:55 +0300 Subject: [PATCH] Refactoring and tests for app group message storage --- .../project.pbxproj | 20 ++-- .../xcschemes/MobileMessagingExample.xcscheme | 4 +- .../NotificationService.swift | 1 + .../MessageStorageTests.swift | 91 +++++++++++++++++-- .../RegistrationTests.swift | 1 - Pod/Classes/Core/MobileMessaging.swift | 8 +- .../Core/MobileMessagingAppDelegate.swift | 7 +- .../Core/Operations/MMMessageHandler.swift | 11 +-- .../MessageStorage/StorageProtocols.swift | 15 --- .../RichNotificationsExtensions.swift | 91 +++++++++++++++++-- 10 files changed, 192 insertions(+), 57 deletions(-) diff --git a/Example/MobileMessagingExample.xcodeproj/project.pbxproj b/Example/MobileMessagingExample.xcodeproj/project.pbxproj index 0071c5be..5ff8d8d8 100644 --- a/Example/MobileMessagingExample.xcodeproj/project.pbxproj +++ b/Example/MobileMessagingExample.xcodeproj/project.pbxproj @@ -8,10 +8,10 @@ /* Begin PBXBuildFile section */ 2499E18FB6EBAD6C8F7A7620 /* Pods_MobileMessagingExample_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F5E69FCAC421EB2E25ED816 /* Pods_MobileMessagingExample_Tests.framework */; }; - 5000030C1CCB7B2F00C91479 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; - 5000030D1CCB7B2F00C91479 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; - 500003101CCB7B3C00C91479 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; - 500003111CCB7B3C00C91479 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; + 5000030C1CCB7B2F00C91479 /* (null) in Resources */ = {isa = PBXBuildFile; }; + 5000030D1CCB7B2F00C91479 /* (null) in Resources */ = {isa = PBXBuildFile; }; + 500003101CCB7B3C00C91479 /* (null) in Resources */ = {isa = PBXBuildFile; }; + 500003111CCB7B3C00C91479 /* (null) in Resources */ = {isa = PBXBuildFile; }; 502129431CEEF450007CEF8E /* MessagesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 502129421CEEF450007CEF8E /* MessagesManager.swift */; }; 5087EDA71CEEF82500B85546 /* MessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5087EDA61CEEF82500B85546 /* MessageCell.swift */; }; 5087EDB01CEF067100B85546 /* InfoTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5087EDAC1CEF067100B85546 /* InfoTableViewController.swift */; }; @@ -560,14 +560,14 @@ buildActionMask = 2147483647; files = ( A0DB0FBD1E2F8FC0005F5979 /* mocks in Resources */, - 500003111CCB7B3C00C91479 /* BuildFile in Resources */, + 500003111CCB7B3C00C91479 /* (null) in Resources */, 50B8C10D1CAD3362000FB79A /* Images.xcassets in Resources */, - 500003101CCB7B3C00C91479 /* BuildFile in Resources */, + 500003101CCB7B3C00C91479 /* (null) in Resources */, A0953C291CAD1C660076488D /* Main.storyboard in Resources */, 8EC5E6061CEB4A36008D53A3 /* MobileMessagingExample_Tests_Device-Info.plist in Resources */, - 5000030D1CCB7B2F00C91479 /* BuildFile in Resources */, + 5000030D1CCB7B2F00C91479 /* (null) in Resources */, 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */, - 5000030C1CCB7B2F00C91479 /* BuildFile in Resources */, + 5000030C1CCB7B2F00C91479 /* (null) in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1030,6 +1030,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = MobileMessagingExample/MobileMessagingExample.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CURRENT_PROJECT_VERSION = 1460136653767; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = T6U248P7YM; @@ -1048,7 +1049,7 @@ OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\""; PRODUCT_BUNDLE_IDENTIFIER = com.infobip.mobilemessaging.example; PRODUCT_NAME = MobileMessagingExample; - PROVISIONING_PROFILE = "4aaea8b9-b39c-4690-9571-86af1428dcaf"; + PROVISIONING_PROFILE = "e8f44623-f9be-445a-bfdc-8f5df6053ef5"; PROVISIONING_PROFILE_SPECIFIER = "Mobile Messaging Example - Development"; SWIFT_VERSION = 3.0.1; }; @@ -1193,6 +1194,7 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = T6U248P7YM; FRAMEWORK_SEARCH_PATHS = ( diff --git a/Example/MobileMessagingExample.xcodeproj/xcshareddata/xcschemes/MobileMessagingExample.xcscheme b/Example/MobileMessagingExample.xcodeproj/xcshareddata/xcschemes/MobileMessagingExample.xcscheme index 3a54b68b..4e86500a 100644 --- a/Example/MobileMessagingExample.xcodeproj/xcshareddata/xcschemes/MobileMessagingExample.xcscheme +++ b/Example/MobileMessagingExample.xcodeproj/xcshareddata/xcschemes/MobileMessagingExample.xcscheme @@ -80,8 +80,8 @@ BaseMessage? { - if let idx = moMessages.index(where: { $0 == messageId }) { - return BaseMessage(messageId: moMessages[idx], direction: .MO, originalPayload: ["messageId": moMessages[idx]], createdDate: Date()) + if let idx = moMessages.index(where: { $0.messageId == messageId }) { + return BaseMessage(messageId: moMessages[idx].messageId, direction: .MO, originalPayload: ["messageId": moMessages[idx].messageId], createdDate: Date()) } else { return nil } @@ -273,8 +274,86 @@ class MessageStorageTests: MMTestCase { self.waitForExpectations(timeout: 60, handler: nil) } + @available(iOS 10.0, *) + func testThatMessageStorageIsBeingPopulatedWithNotificationExtensionHandledMessages() { + guard #available(iOS 10.0, *) else { + return + } + + cleanUpAndStop() + + let content = UNMutableNotificationContent() + content.userInfo = [ + "messageId": "mid1", + "aps": ["alert": ["title": "msg_title", "body": "msg_body"], "badge": 6, "sound": "default", "mutable-content": 1] + ] + let request = UNNotificationRequest(identifier: "id1", content: content, trigger: nil) + let contentHandler: (UNNotificationContent) -> Void = { content in + + } + let sharedStorageMock = SharedMessageStorageMock(applicationCode: "appCode", appGroupId: "groupId")! + + + MobileMessagingNotificationServiceExtension.startWithApplicationCode("appCode", appGroupId: "groupId") + MobileMessagingNotificationServiceExtension.sharedInstance?.deliveryReporter = SuccessfullDeliveryReporterMock(applicationCode: "appCode", baseUrl: "groupId") + MobileMessagingNotificationServiceExtension.sharedInstance?.sharedNotificationExtensionStorage = sharedStorageMock + MobileMessagingNotificationServiceExtension.didReceive(request, withContentHandler: contentHandler) + + XCTAssertEqual(sharedStorageMock.retrieveMessages().count, 1) + + let mockMessageStorage = MockMessageStorage() + XCTAssertEqual(mockMessageStorage.mtMessages.count, 0) + + let mm = mockedMMInstanceWithApplicationCode(MMTestConstants.kTestCorrectApplicationCode)!.withMessageStorage(mockMessageStorage) + mm.sharedNotificationExtensionStorage = sharedStorageMock + mm.start() + + weak var expectation = self.expectation(description: "") + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1)) { + mm.cleanUpAndStop() + XCTAssertEqual(sharedStorageMock.retrieveMessages().count, 0) + + expectation?.fulfill() + } + self.waitForExpectations(timeout: 60, handler: { _ in + XCTAssertEqual(mockMessageStorage.mtMessages.count, 1) + }) + } + override func tearDown() { self.defaultMessageStorage?.coreDataStorage?.drop() super.tearDown() } } + +class SuccessfullDeliveryReporterMock: DeliveryReporting { + required init(applicationCode: String, baseUrl: String) { + + } + + func report(messageIds: [String], completion: @escaping (Result) -> Void) { + completion(Result.Success(DeliveryReportResponse.init())) + } +} + +class SharedMessageStorageMock: AppGroupMessageStorage { + var inMemStorage = [String: Any]() + let applicationCode: String + required init?(applicationCode: String, appGroupId: String) { + self.applicationCode = applicationCode + } + + func save(message: MTMessage, isDelivered: Bool) { + var msgs = (inMemStorage[applicationCode] as? [MTMessage]) ?? [MTMessage]() + msgs.append(message) + inMemStorage[applicationCode] = msgs + } + + func retrieveMessages() -> [MTMessage] { + return (inMemStorage[applicationCode] as? [MTMessage]) ?? [MTMessage]() + } + + func cleanupMessages() { + inMemStorage[applicationCode] = nil + } +} diff --git a/Example/Tests/MobileMessagingTests/RegistrationTests.swift b/Example/Tests/MobileMessagingTests/RegistrationTests.swift index 7aaf962f..06868095 100644 --- a/Example/Tests/MobileMessagingTests/RegistrationTests.swift +++ b/Example/Tests/MobileMessagingTests/RegistrationTests.swift @@ -87,7 +87,6 @@ final class RegistrationTests: MMTestCase { self.waitForExpectations(timeout: 60) { _ in let ctx = (self.mobileMessagingInstance.internalStorage.mainThreadManagedObjectContext!) if let installation = InstallationManagedObject.MM_findFirstInContext(ctx) { - XCTAssertTrue(installation.dirtyAttributesSet.contains(AttributesSet.deviceToken), "Dirty flag may be false only after success registration") XCTAssertEqual(installation.internalUserId, nil, "Internal id must be nil, server denied the application code") XCTAssertEqual(installation.deviceToken, "someToken".mm_toHexademicalString, "Device token must be mocked properly. (current is \(String(describing: installation.deviceToken)))") diff --git a/Pod/Classes/Core/MobileMessaging.swift b/Pod/Classes/Core/MobileMessaging.swift index cf8d5080..5c7b6679 100644 --- a/Pod/Classes/Core/MobileMessaging.swift +++ b/Pod/Classes/Core/MobileMessaging.swift @@ -50,8 +50,10 @@ public final class MobileMessaging: NSObject { return self } + @available(iOS 10.0, *) public func withAppGroupId(_ appGroupId: String) -> MobileMessaging { self.appGroupId = appGroupId + self.sharedNotificationExtensionStorage = DefaultSharedDataStorage(applicationCode: applicationCode, appGroupId: appGroupId) return self } @@ -280,7 +282,7 @@ public final class MobileMessaging: NSObject { func cleanUpAndStop(_ clearKeychain: Bool = true) { MMLogDebug("Cleaning up MobileMessaging service...") if #available(iOS 10.0, *) { - UserDefaults.cleanupNotificationServiceExtensionContainer(forApplicationCode: applicationCode) + sharedNotificationExtensionStorage?.cleanupMessages() } MMCoreDataStorage.dropStorages(internalStorage: internalStorage, messageStorage: messageStorage as? MMDefaultMessageStorage) if (clearKeychain) { @@ -418,9 +420,11 @@ public final class MobileMessaging: NSObject { lazy var application: MMApplication! = UIApplication.shared lazy var reachabilityManager: ReachabilityManagerProtocol! = MMNetworkReachabilityManager.sharedInstance lazy var keychain: MMKeychain! = MMKeychain() - var appGroupId: String? + static var date: MMDate = MMDate() // testability + var appGroupId: String? + var sharedNotificationExtensionStorage: AppGroupMessageStorage? } extension UIApplication: MMApplication {} diff --git a/Pod/Classes/Core/MobileMessagingAppDelegate.swift b/Pod/Classes/Core/MobileMessagingAppDelegate.swift index 84590d50..d9203c6f 100644 --- a/Pod/Classes/Core/MobileMessagingAppDelegate.swift +++ b/Pod/Classes/Core/MobileMessagingAppDelegate.swift @@ -44,8 +44,11 @@ open class MobileMessagingAppDelegate: UIResponder, UIApplicationDelegate { public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool { if !isTestingProcessRunning { var session = MobileMessaging.withApplicationCode(applicationCode, notificationType: userNotificationType) - if let appGroupId = appGroupId { - session = session?.withAppGroupId(appGroupId) + + if #available(iOS 10.0, *) { + if let appGroupId = appGroupId { + session = session?.withAppGroupId(appGroupId) + } } session?.start() } diff --git a/Pod/Classes/Core/Operations/MMMessageHandler.swift b/Pod/Classes/Core/Operations/MMMessageHandler.swift index 76e9d98a..2f18462a 100644 --- a/Pod/Classes/Core/Operations/MMMessageHandler.swift +++ b/Pod/Classes/Core/Operations/MMMessageHandler.swift @@ -83,19 +83,10 @@ final class MMMessageHandler: MobileMessagingService { @available(iOS 10.0, *) func handleStorageFromNotificationServiceExtensionGroupContainer() { - guard let ud = UserDefaults.notificationServiceExtensionContainer, let mm = MobileMessaging.sharedInstance, let messageDataDicts = ud.array(forKey: mm.applicationCode) as? [StringKeyPayload] else + guard let mm = MobileMessaging.sharedInstance, let messages = mm.sharedNotificationExtensionStorage?.retrieveMessages() else { return } - let messages = messageDataDicts.flatMap({ messageDataTuple -> MTMessage? in - guard let payload = messageDataTuple["p"] as? StringKeyPayload, let date = messageDataTuple["d"] as? Date, let dlrSent = messageDataTuple["dlr"] as? Bool else { - return nil - } - let newMessage = MTMessage(payload: payload, createdDate: date) - newMessage?.isDeliveryReportSent = dlrSent - return newMessage - }) - ud.removeObject(forKey: mm.applicationCode) handleMTMessages(messages, notificationTapped: false, completion: nil) } diff --git a/Pod/Classes/MessageStorage/StorageProtocols.swift b/Pod/Classes/MessageStorage/StorageProtocols.swift index d81a5023..7aba8a4a 100644 --- a/Pod/Classes/MessageStorage/StorageProtocols.swift +++ b/Pod/Classes/MessageStorage/StorageProtocols.swift @@ -96,21 +96,6 @@ public typealias FetchResultBlock = ([BaseMessage]?) -> Void func update(messageSentStatus status: MOMessageSentStatus, for messageId: MessageId) } -extension UserDefaults { - @available(iOS 10.0, *) - class var notificationServiceExtensionContainer: UserDefaults? { - guard let appGroupId = MobileMessagingNotificationServiceExtension.sharedInstance?.appGroupId ?? MobileMessaging.sharedInstance?.appGroupId else { - return nil - } - return UserDefaults.init(suiteName: appGroupId) - } - - @available(iOS 10.0, *) - class func cleanupNotificationServiceExtensionContainer(forApplicationCode: String) { - UserDefaults.notificationServiceExtensionContainer?.removeObject(forKey: forApplicationCode) - } -} - /// The adapter dispatches all adaptee method calls into the adaptee's queue, /// and checks for existing messages to avoid duplications, that's all. class MMMessageStorageQueuedAdapter: MessageStorage { diff --git a/Pod/Classes/RichNotifications/RichNotificationsExtensions.swift b/Pod/Classes/RichNotifications/RichNotificationsExtensions.swift index e92abe67..62aa30e9 100644 --- a/Pod/Classes/RichNotifications/RichNotificationsExtensions.swift +++ b/Pod/Classes/RichNotifications/RichNotificationsExtensions.swift @@ -49,6 +49,7 @@ final public class MobileMessagingNotificationServiceExtension: NSObject { if sharedInstance == nil { sharedInstance = MobileMessagingNotificationServiceExtension(appCode: code, appGroupId: appGroupId) } + sharedInstance?.sharedNotificationExtensionStorage = DefaultSharedDataStorage(applicationCode: code, appGroupId: appGroupId) } public class func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { @@ -57,7 +58,11 @@ final public class MobileMessagingNotificationServiceExtension: NSObject { contentHandler(request.content) return } - sharedInstance.reportDelivery(mtMessage) + + sharedInstance.reportDelivery(mtMessage) { result in + sharedInstance.persistMessage(mtMessage, isDelivered: result.error == nil) + } + sharedInstance.currentTask = mtMessage.downloadImageAttachment { (url, error) in guard let url = url, let mContent = (request.content.mutableCopy() as? UNMutableNotificationContent), @@ -90,14 +95,62 @@ final public class MobileMessagingNotificationServiceExtension: NSObject { self.appGroupId = appGroupId } - private func reportDelivery(_ message: MTMessage) { - DeliveryReportRequest(dlrIds: [message.messageId])?.responseObject(applicationCode: applicationCode, baseURL: remoteAPIBaseURL) { response in - self.persistMessage(message, isDelivered: response.error == nil) - } + private func reportDelivery(_ message: MTMessage, completion: @escaping (Result) -> Void) { + deliveryReporter.report(messageIds: [message.messageId], completion: completion) } private func persistMessage(_ message: MTMessage, isDelivered: Bool) { - guard let ud = UserDefaults.notificationServiceExtensionContainer else { + sharedNotificationExtensionStorage?.save(message: message, isDelivered: isDelivered) + } + + let appGroupId: String + let applicationCode: String + let remoteAPIBaseURL = APIValues.prodBaseURLString + var currentTask: URLSessionDownloadTask? + var sharedNotificationExtensionStorage: AppGroupMessageStorage? + lazy var deliveryReporter: DeliveryReporting! = DeliveryReporter(applicationCode: self.applicationCode, baseUrl: self.remoteAPIBaseURL) +} + +protocol DeliveryReporting { + init(applicationCode: String, baseUrl: String) + func report(messageIds: [String], completion: @escaping (Result) -> Void) +} + +class DeliveryReporter: DeliveryReporting { + let applicationCode: String, baseUrl: String + + required init(applicationCode: String, baseUrl: String) { + self.applicationCode = applicationCode + self.baseUrl = baseUrl + } + + func report(messageIds: [String], completion: @escaping (Result) -> Void) { + guard let dlr = DeliveryReportRequest(dlrIds: messageIds) else { + completion(Result.Cancel) + return + } + dlr.responseObject(applicationCode: applicationCode, baseURL: baseUrl, completion: completion) + } +} + +protocol AppGroupMessageStorage { + init?(applicationCode: String, appGroupId: String) + func save(message: MTMessage, isDelivered: Bool) + func retrieveMessages() -> [MTMessage] + func cleanupMessages() +} + +@available(iOS 10.0, *) +class DefaultSharedDataStorage: AppGroupMessageStorage { + let applicationCode: String + let appGroupId: String + required init?(applicationCode: String, appGroupId: String) { + self.appGroupId = appGroupId + self.applicationCode = applicationCode + } + + func save(message: MTMessage, isDelivered: Bool) { + guard let ud = UserDefaults.init(suiteName: appGroupId) else { return } var savedMessageDicts = ud.object(forKey: applicationCode) as? [StringKeyPayload] ?? [] @@ -106,8 +159,26 @@ final public class MobileMessagingNotificationServiceExtension: NSObject { ud.synchronize() } - let appGroupId: String - let applicationCode: String - let remoteAPIBaseURL = APIValues.prodBaseURLString - var currentTask: URLSessionDownloadTask? + func retrieveMessages() -> [MTMessage] { + guard let ud = UserDefaults.init(suiteName: appGroupId), let messageDataDicts = ud.array(forKey: applicationCode) as? [StringKeyPayload] else + { + return [] + } + let messages = messageDataDicts.flatMap({ messageDataTuple -> MTMessage? in + guard let payload = messageDataTuple["p"] as? StringKeyPayload, let date = messageDataTuple["d"] as? Date, let dlrSent = messageDataTuple["dlr"] as? Bool else { + return nil + } + let newMessage = MTMessage(payload: payload, createdDate: date) + newMessage?.isDeliveryReportSent = dlrSent + return newMessage + }) + return messages + } + + func cleanupMessages() { + guard let ud = UserDefaults.init(suiteName: appGroupId) else { + return + } + ud.removeObject(forKey: applicationCode) + } }