aboutsummaryrefslogtreecommitdiffhomepage
path: root/Example/Messaging
diff options
context:
space:
mode:
authorGravatar Paul Beusterien <paulbeusterien@google.com>2017-05-15 12:27:07 -0700
committerGravatar Paul Beusterien <paulbeusterien@google.com>2017-05-15 12:27:07 -0700
commit98ba64449a632518bd2b86fe8d927f4a960d3ddc (patch)
tree131d9c4272fa6179fcda6c5a33fcb3b1bd57ad2e /Example/Messaging
parent32461366c9e204a527ca05e6e9b9404a2454ac51 (diff)
Initial
Diffstat (limited to 'Example/Messaging')
-rw-r--r--Example/Messaging/App/AppDelegate.swift114
-rw-r--r--Example/Messaging/App/Base.lproj/LaunchScreen.storyboard27
-rw-r--r--Example/Messaging/App/Base.lproj/Main.storyboard48
-rw-r--r--Example/Messaging/App/Data+MessagingExtensions.swift25
-rw-r--r--Example/Messaging/App/Environment.swift28
-rw-r--r--Example/Messaging/App/GoogleService-Info.plist30
-rw-r--r--Example/Messaging/App/Messaging-Info.plist53
-rw-r--r--Example/Messaging/App/MessagingViewController.swift332
-rw-r--r--Example/Messaging/App/Messaging_Example.entitlements8
-rw-r--r--Example/Messaging/App/NotificationsController.swift132
-rw-r--r--Example/Messaging/Messaging_Example-Bridging-Header.h17
-rw-r--r--Example/Messaging/Tests/FIRMessagingClientTest.m308
-rw-r--r--Example/Messaging/Tests/FIRMessagingCodedInputStreamTest.m116
-rw-r--r--Example/Messaging/Tests/FIRMessagingConnectionTest.m480
-rw-r--r--Example/Messaging/Tests/FIRMessagingContextManagerServiceTest.m183
-rw-r--r--Example/Messaging/Tests/FIRMessagingDataMessageManagerTest.m662
-rw-r--r--Example/Messaging/Tests/FIRMessagingFakeConnection.h63
-rw-r--r--Example/Messaging/Tests/FIRMessagingFakeConnection.m150
-rw-r--r--Example/Messaging/Tests/FIRMessagingFakeSocket.h36
-rw-r--r--Example/Messaging/Tests/FIRMessagingFakeSocket.m89
-rw-r--r--Example/Messaging/Tests/FIRMessagingLinkHandlingTest.m94
-rw-r--r--Example/Messaging/Tests/FIRMessagingPendingTopicsListTest.m263
-rw-r--r--Example/Messaging/Tests/FIRMessagingPubSubTest.m81
-rw-r--r--Example/Messaging/Tests/FIRMessagingRegistrarTest.m134
-rw-r--r--Example/Messaging/Tests/FIRMessagingRemoteNotificationsProxyTest.m279
-rw-r--r--Example/Messaging/Tests/FIRMessagingRmqManagerTest.m332
-rw-r--r--Example/Messaging/Tests/FIRMessagingSecureSocketTest.m323
-rw-r--r--Example/Messaging/Tests/FIRMessagingServiceTest.m288
-rw-r--r--Example/Messaging/Tests/FIRMessagingSyncMessageManagerTest.m256
-rw-r--r--Example/Messaging/Tests/FIRMessagingTest.m214
-rw-r--r--Example/Messaging/Tests/FIRMessagingTestNotificationUtilities.h23
-rw-r--r--Example/Messaging/Tests/FIRMessagingTestNotificationUtilities.m31
-rw-r--r--Example/Messaging/Tests/Info.plist24
33 files changed, 5243 insertions, 0 deletions
diff --git a/Example/Messaging/App/AppDelegate.swift b/Example/Messaging/App/AppDelegate.swift
new file mode 100644
index 0000000..0f40a4e
--- /dev/null
+++ b/Example/Messaging/App/AppDelegate.swift
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 UIKit
+import FirebaseDev
+import UserNotifications
+
+@UIApplicationMain
+class AppDelegate: UIResponder, UIApplicationDelegate {
+
+ var window: UIWindow?
+
+ static let isWithinUnitTest: Bool = {
+ if let testClass = NSClassFromString("XCTestCase") {
+ return true
+ } else {
+ return false
+ }
+ }()
+
+ static var hasPresentedInvalidServiceInfoPlistAlert = false
+
+ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
+ guard !AppDelegate.isWithinUnitTest else {
+ // During unit tests, we don't want to initialize Firebase, since by default we want to able
+ // to run unit tests without requiring a non-dummy GoogleService-Info.plist file
+ return true
+ }
+
+ guard SampleAppUtilities.appContainsRealServiceInfoPlist() else {
+ // We can't run because the GoogleService-Info.plist file is likely the dummy file which needs
+ // to be replaced with a real one, or somehow the file has been removed from the app bundle.
+ // See: https://github.com/firebase/firebase-ios-sdk/
+ // We'll present a friendly alert when the app becomes active.
+ return true
+ }
+
+ FirebaseApp.configure()
+ Messaging.messaging().delegate = self
+
+ NotificationsController.configure()
+
+ if #available(iOS 8.0, *) {
+ // Always register for remote notifications. This will not show a prompt to the user, as by
+ // default it will provision silent notifications. We can use UNUserNotificationCenter to
+ // request authorization for user-facing notifications.
+ application.registerForRemoteNotifications()
+ } else {
+ // iOS 7 didn't differentiate between user-facing and other notifications, so we should just
+ // register for remote notifications
+ NotificationsController.shared.registerForUserFacingNotificationsFor(application)
+ }
+
+ printFCMToken()
+ return true
+ }
+
+ func printFCMToken() {
+ if let token = Messaging.messaging().fcmToken {
+ print("FCM Token: \(token)")
+ } else {
+ print("FCM Token: nil")
+ }
+ }
+
+ func application(_ application: UIApplication,
+ didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
+ print("APNS Token: \(deviceToken.hexByteString)")
+ NotificationCenter.default.post(name: APNSTokenReceivedNotification, object: nil)
+ if #available(iOS 8.0, *) {
+ } else {
+ // On iOS 7, receiving a device token also means our user notifications were granted, so fire
+ // the notification to update our user notifications UI
+ NotificationCenter.default.post(name: UserNotificationsChangedNotification, object: nil)
+ }
+ }
+
+ func application(_ application: UIApplication,
+ didRegister notificationSettings: UIUserNotificationSettings) {
+ NotificationCenter.default.post(name: UserNotificationsChangedNotification, object: nil)
+ }
+
+ func applicationDidBecomeActive(_ application: UIApplication) {
+ // If the app didn't start property due to an invalid GoogleService-Info.plist file, show an
+ // alert to the developer.
+ if !SampleAppUtilities.appContainsRealServiceInfoPlist() &&
+ !AppDelegate.hasPresentedInvalidServiceInfoPlistAlert {
+ if let vc = window?.rootViewController {
+ SampleAppUtilities.presentAlertForInvalidServiceInfoPlistFrom(vc)
+ AppDelegate.hasPresentedInvalidServiceInfoPlistAlert = true
+ }
+ }
+ }
+}
+
+extension AppDelegate: MessagingDelegate {
+ func messaging(_ messaging: Messaging, didRefreshRegistrationToken fcmToken: String) {
+ printFCMToken()
+ }
+}
+
diff --git a/Example/Messaging/App/Base.lproj/LaunchScreen.storyboard b/Example/Messaging/App/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..fdf3f97
--- /dev/null
+++ b/Example/Messaging/App/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11134" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+ <dependencies>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11106"/>
+ <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="EHf-IW-A2E">
+ <objects>
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
+ <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+ <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="53" y="375"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Example/Messaging/App/Base.lproj/Main.storyboard b/Example/Messaging/App/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..6df1a82
--- /dev/null
+++ b/Example/Messaging/App/Base.lproj/Main.storyboard
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12120" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="taE-sK-BOl">
+ <device id="retina4_7" orientation="portrait">
+ <adaptation id="fullscreen"/>
+ </device>
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12088"/>
+ <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+ </dependencies>
+ <scenes>
+ <!--Firebase Cloud Messaging-->
+ <scene sceneID="tne-QT-ifu">
+ <objects>
+ <viewController id="BYZ-38-t0r" customClass="MessagingViewController" customModule="Messaging_Example" customModuleProvider="target" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
+ <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+ <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+ </view>
+ <navigationItem key="navigationItem" title="Firebase Cloud Messaging" id="z1u-kE-qKb"/>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="698" y="164"/>
+ </scene>
+ <!--Navigation Controller-->
+ <scene sceneID="rmF-xz-rwn">
+ <objects>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="Ju1-Bj-8eG" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ <navigationController id="taE-sK-BOl" sceneMemberID="viewController">
+ <navigationBar key="navigationBar" contentMode="scaleToFill" id="iTL-Kg-11w">
+ <rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
+ <autoresizingMask key="autoresizingMask"/>
+ </navigationBar>
+ <connections>
+ <segue destination="BYZ-38-t0r" kind="relationship" relationship="rootViewController" id="04R-HZ-bi6"/>
+ </connections>
+ </navigationController>
+ </objects>
+ <point key="canvasLocation" x="-92" y="165"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Example/Messaging/App/Data+MessagingExtensions.swift b/Example/Messaging/App/Data+MessagingExtensions.swift
new file mode 100644
index 0000000..99ded25
--- /dev/null
+++ b/Example/Messaging/App/Data+MessagingExtensions.swift
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 Foundation
+
+extension Data {
+ // Print Data as a string of bytes in hex, such as the common representation of APNs device tokens
+ // See: http://stackoverflow.com/a/40031342/9849
+ var hexByteString: String {
+ return self.map { String(format: "%02.2hhx", $0) }.joined()
+ }
+}
diff --git a/Example/Messaging/App/Environment.swift b/Example/Messaging/App/Environment.swift
new file mode 100644
index 0000000..5219c64
--- /dev/null
+++ b/Example/Messaging/App/Environment.swift
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 Foundation
+
+struct Environment {
+ static let isSimulator: Bool = {
+ var isSim = false
+ #if arch(i386) || arch(x86_64)
+ isSim = true
+ #endif
+
+ return isSim
+ }()
+}
diff --git a/Example/Messaging/App/GoogleService-Info.plist b/Example/Messaging/App/GoogleService-Info.plist
new file mode 100644
index 0000000..89afffe
--- /dev/null
+++ b/Example/Messaging/App/GoogleService-Info.plist
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>API_KEY</key>
+ <string>correct_api_key</string>
+ <key>TRACKING_ID</key>
+ <string>correct_tracking_id</string>
+ <key>CLIENT_ID</key>
+ <string>correct_client_id</string>
+ <key>REVERSED_CLIENT_ID</key>
+ <string>correct_reversed_client_id</string>
+ <key>ANDROID_CLIENT_ID</key>
+ <string>correct_android_client_id</string>
+ <key>GOOGLE_APP_ID</key>
+ <string>1:123:ios:123abc</string>
+ <key>GCM_SENDER_ID</key>
+ <string>correct_gcm_sender_id</string>
+ <key>PLIST_VERSION</key>
+ <string>1</string>
+ <key>BUNDLE_ID</key>
+ <string>com.google.FirebaseSDKTests</string>
+ <key>PROJECT_ID</key>
+ <string>abc-xyz-123</string>
+ <key>DATABASE_URL</key>
+ <string>https://abc-xyz-123.firebaseio.com</string>
+ <key>STORAGE_BUCKET</key>
+ <string>project-id-123.storage.firebase.com</string>
+</dict>
+</plist>
diff --git a/Example/Messaging/App/Messaging-Info.plist b/Example/Messaging/App/Messaging-Info.plist
new file mode 100644
index 0000000..e42f39d
--- /dev/null
+++ b/Example/Messaging/App/Messaging-Info.plist
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleDisplayName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UIBackgroundModes</key>
+ <array>
+ <string>remote-notification</string>
+ </array>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIMainStoryboardFile</key>
+ <string>Main</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>armv7</string>
+ </array>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+</dict>
+</plist>
diff --git a/Example/Messaging/App/MessagingViewController.swift b/Example/Messaging/App/MessagingViewController.swift
new file mode 100644
index 0000000..00ed3ff
--- /dev/null
+++ b/Example/Messaging/App/MessagingViewController.swift
@@ -0,0 +1,332 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 UIKit
+
+import FirebaseDev
+
+enum Row: String {
+ case apnsToken = "apnsToken"
+ case apnsStatus = "apnsStatus"
+ case requestAPNSPermissions = "requestAPNSPermissions"
+ case fcmToken = "fcmToken"
+}
+
+enum PermissionsButtonTitle: String {
+ case requestPermissions = "Request User Notifications"
+ case noAPNS = "Cannot Request Permissions (No APNs)"
+ case alreadyRequested = "Already Requested Permissions"
+ case simulator = "Cannot Request Permissions (Simulator)"
+}
+
+class MessagingViewController: UIViewController {
+
+ let tableView: UITableView
+
+ var sections = [[Row]]()
+ var sectionHeaderTitles = [String?]()
+
+ var allowedNotificationTypes: [NotificationsControllerAllowedNotificationType]?
+
+ // Cached rows by Row type. Since this is largely a fixed table view, we'll
+ // keep track of our created cells and UI, rather than have all the logic
+
+ required init?(coder aDecoder: NSCoder) {
+ tableView = UITableView(frame: CGRect.zero, style: .grouped)
+ tableView.rowHeight = UITableViewAutomaticDimension
+ tableView.estimatedRowHeight = 44
+ // Allow UI Controls within the table to be immediately responsive
+ tableView.delaysContentTouches = false
+ super.init(coder: aDecoder)
+ tableView.dataSource = self
+ tableView.delegate = self
+ }
+
+ override func loadView() {
+ super.loadView()
+ view = UIView(frame: CGRect.zero)
+ view.addSubview(self.tableView)
+ // Ensure that the tableView always is the size of the view
+ tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ let center = NotificationCenter.default
+ center.addObserver(self,
+ selector: #selector(onAPNSTokenReceived) ,
+ name: APNSTokenReceivedNotification,
+ object: nil)
+ center.addObserver(self,
+ selector: #selector(onUserNotificationSettingsChanged),
+ name: UserNotificationsChangedNotification,
+ object: nil)
+ center.addObserver(self,
+ selector: #selector(onFCMTokenRefreshed),
+ name: Notification.Name.MessagingRegistrationTokenRefreshed,
+ object: nil)
+ updateAllowedNotificationTypes {
+ self.resetTableContents()
+ self.tableView.reloadData()
+ }
+ }
+
+ func onAPNSTokenReceived() {
+ // Reload the appropriate cells
+ updateAllowedNotificationTypes {
+ if let tokenPath = self.indexPathFor(.apnsToken),
+ let statusPath = self.indexPathFor(.apnsStatus),
+ let requestPath = self.indexPathFor(.requestAPNSPermissions) {
+ self.updateIndexPaths(indexPaths: [tokenPath, statusPath, requestPath])
+ }
+ }
+ }
+
+ func onFCMTokenRefreshed() {
+ if let indexPath = indexPathFor(.fcmToken) {
+ updateIndexPaths(indexPaths: [indexPath])
+ }
+ }
+
+ func onUserNotificationSettingsChanged() {
+ updateAllowedNotificationTypes {
+ if let statusPath = self.indexPathFor(.apnsStatus),
+ let requestPath = self.indexPathFor(.requestAPNSPermissions) {
+ self.updateIndexPaths(indexPaths: [statusPath, requestPath])
+ }
+ }
+ }
+
+ private func updateIndexPaths(indexPaths: [IndexPath]) {
+ tableView.beginUpdates()
+ tableView.reloadRows(at: indexPaths, with: .none)
+ tableView.endUpdates()
+ }
+
+ fileprivate func updateAllowedNotificationTypes(_ completion: (() -> Void)?) {
+ NotificationsController.shared.getAllowedNotificationTypes { (types) in
+ self.allowedNotificationTypes = types
+ self.updateRequestAPNSButton()
+ completion?()
+ }
+ }
+
+ fileprivate func updateRequestAPNSButton() {
+ guard !Environment.isSimulator else {
+ requestPermissionsButton.isEnabled = false
+ requestPermissionsButton.setTitle(PermissionsButtonTitle.simulator.rawValue, for: .normal)
+ return
+ }
+ guard let allowedTypes = allowedNotificationTypes else {
+ requestPermissionsButton.isEnabled = false
+ requestPermissionsButton.setTitle(PermissionsButtonTitle.noAPNS.rawValue, for: .normal)
+ return
+ }
+
+ requestPermissionsButton.isEnabled =
+ (allowedTypes.count == 1 && allowedTypes.first! == .silent)
+
+ let title: PermissionsButtonTitle =
+ (requestPermissionsButton.isEnabled ? .requestPermissions : .alreadyRequested)
+ requestPermissionsButton.setTitle(title.rawValue, for: .normal)
+ }
+
+ // MARK: UI (Cells and Buttons) Defined as lazy properties
+ lazy var apnsTableCell: UITableViewCell = {
+ let cell = UITableViewCell(style: .subtitle, reuseIdentifier: Row.apnsToken.rawValue)
+ cell.textLabel?.numberOfLines = 0
+ cell.textLabel?.lineBreakMode = .byWordWrapping
+ return cell
+ }()
+
+ lazy var apnsStatusTableCell: UITableViewCell = {
+ let cell = UITableViewCell(style: UITableViewCellStyle.value1, reuseIdentifier: Row.apnsStatus.rawValue)
+ cell.textLabel?.text = "Allowed:"
+ cell.detailTextLabel?.numberOfLines = 0
+ cell.detailTextLabel?.lineBreakMode = .byWordWrapping
+ return cell
+ }()
+
+ lazy var requestPermissionsButton: UIButton = {
+ let button = UIButton(type: .system)
+ button.setTitle(PermissionsButtonTitle.requestPermissions.rawValue, for: .normal)
+ button.setTitleColor(UIColor.gray, for: .highlighted)
+ button.setTitleColor(UIColor.gray, for: .disabled)
+ button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
+ button.addTarget(self,
+ action: #selector(onRequestUserNotificationsButtonTapped),
+ for: .touchUpInside)
+ return button
+ }()
+
+ lazy var apnsRequestPermissionsTableCell: UITableViewCell = {
+ let cell = UITableViewCell(style: .default,
+ reuseIdentifier: Row.requestAPNSPermissions.rawValue)
+ cell.selectionStyle = .none
+ cell.contentView.addSubview(self.requestPermissionsButton)
+ self.requestPermissionsButton.frame = cell.contentView.bounds
+ self.requestPermissionsButton.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ return cell
+ }()
+
+ lazy var fcmTokenTableCell: UITableViewCell = {
+ let cell = UITableViewCell(style: .subtitle, reuseIdentifier: Row.fcmToken.rawValue)
+ cell.textLabel?.numberOfLines = 0
+ cell.textLabel?.lineBreakMode = .byCharWrapping
+ return cell
+ }()
+}
+
+// MARK: - Configuring the table view and cells with information
+extension MessagingViewController {
+ func resetTableContents() {
+ sections.removeAll()
+ sectionHeaderTitles.removeAll()
+
+ // APNS
+ let apnsSection: [Row] = [.apnsToken, .apnsStatus, .requestAPNSPermissions]
+ sections.append(apnsSection)
+ sectionHeaderTitles.append("APNs")
+
+ // FCM
+ let fcmSection: [Row] = [.fcmToken]
+ sections.append(fcmSection)
+ sectionHeaderTitles.append("FCM Token")
+
+ }
+
+ func indexPathFor(_ rowId: Row) -> IndexPath? {
+ var sectionIndex = 0
+ for section in sections {
+ var rowIndex = 0
+ for row in section {
+ if row == rowId {
+ return IndexPath(row: rowIndex, section: sectionIndex)
+ }
+ rowIndex += 1
+ }
+ sectionIndex += 1
+ }
+ return nil
+ }
+
+ func configureCell(_ cell: UITableViewCell, withAPNSToken apnsToken: Data?) {
+ guard !Environment.isSimulator else {
+ cell.textLabel?.text = "APNs notifications are not supported in the simulator."
+ cell.detailTextLabel?.text = nil
+ return
+ }
+ if let apnsToken = apnsToken {
+ cell.textLabel?.text = apnsToken.hexByteString
+ cell.detailTextLabel?.text = "Tap to Share"
+ } else {
+ cell.textLabel?.text = "None"
+ cell.detailTextLabel?.text = nil
+ }
+ }
+
+ func configureCellWithAPNSStatus(_ cell: UITableViewCell) {
+ if let allowedNotificationTypes = allowedNotificationTypes {
+ let displayableTypes: [String] = allowedNotificationTypes.map { return $0.rawValue }
+ cell.detailTextLabel?.text = displayableTypes.joined(separator: ", ")
+ } else {
+ cell.detailTextLabel?.text = "Retrieving..."
+ }
+ }
+
+ func configureCell(_ cell: UITableViewCell, withFCMToken fcmToken: String?) {
+ if let fcmToken = fcmToken {
+ cell.textLabel?.text = fcmToken
+ cell.detailTextLabel?.text = "Tap to Share"
+ } else {
+ cell.textLabel?.text = "None"
+ cell.detailTextLabel?.text = nil
+ }
+ }
+}
+
+// MARK: - UITableViewDataSource
+extension MessagingViewController: UITableViewDataSource {
+ func numberOfSections(in tableView: UITableView) -> Int {
+ return sections.count
+ }
+
+ public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ return sections[section].count
+ }
+
+ public func tableView(_ tableView: UITableView,
+ cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let row = sections[indexPath.section][indexPath.row]
+
+ let cell: UITableViewCell
+ switch row {
+ case .apnsToken:
+ cell = apnsTableCell
+ configureCell(cell, withAPNSToken: Messaging.messaging().apnsToken)
+ case .apnsStatus:
+ cell = apnsStatusTableCell
+ configureCellWithAPNSStatus(cell)
+ case .requestAPNSPermissions:
+ cell = apnsRequestPermissionsTableCell
+ case .fcmToken:
+ cell = fcmTokenTableCell
+ configureCell(cell, withFCMToken: Messaging.messaging().fcmToken)
+ }
+ return cell
+ }
+
+ func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ return sectionHeaderTitles[section]
+ }
+}
+
+// MARK: - UITableViewDelegate
+extension MessagingViewController: UITableViewDelegate {
+ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ tableView.deselectRow(at: indexPath, animated: true)
+
+ let row = sections[indexPath.section][indexPath.row]
+ switch row {
+ case .apnsToken:
+ if let apnsToken = Messaging.messaging().apnsToken {
+ showActivityViewControllerFor(sharedItem: apnsToken.hexByteString)
+ }
+ case .fcmToken:
+ if let fcmToken = Messaging.messaging().fcmToken {
+ showActivityViewControllerFor(sharedItem: fcmToken)
+ }
+ default: break
+ }
+ }
+}
+
+// MARK: - UI Controls
+extension MessagingViewController {
+ func onRequestUserNotificationsButtonTapped(sender: UIButton) {
+ NotificationsController.shared.registerForUserFacingNotificationsFor(UIApplication.shared)
+ }
+}
+
+// MARK: - Activity View Controller
+extension MessagingViewController {
+ func showActivityViewControllerFor(sharedItem: Any) {
+ let activityViewController = UIActivityViewController(activityItems: [sharedItem],
+ applicationActivities: nil)
+ present(activityViewController, animated: true, completion: nil)
+ }
+}
+
diff --git a/Example/Messaging/App/Messaging_Example.entitlements b/Example/Messaging/App/Messaging_Example.entitlements
new file mode 100644
index 0000000..903def2
--- /dev/null
+++ b/Example/Messaging/App/Messaging_Example.entitlements
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>aps-environment</key>
+ <string>development</string>
+</dict>
+</plist>
diff --git a/Example/Messaging/App/NotificationsController.swift b/Example/Messaging/App/NotificationsController.swift
new file mode 100644
index 0000000..726d980
--- /dev/null
+++ b/Example/Messaging/App/NotificationsController.swift
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 UIKit
+import UserNotifications
+
+import FirebaseDev
+
+enum NotificationsControllerAllowedNotificationType: String {
+ case none = "None"
+ case silent = "Silent Updates"
+ case alert = "Alerts"
+ case badge = "Badges"
+ case sound = "Sounds"
+}
+
+let APNSTokenReceivedNotification: Notification.Name
+ = Notification.Name(rawValue: "APNSTokenReceivedNotification")
+let UserNotificationsChangedNotification: Notification.Name
+ = Notification.Name(rawValue: "UserNotificationsChangedNotification")
+
+class NotificationsController: NSObject {
+
+ static let shared: NotificationsController = {
+ let instance = NotificationsController()
+ return instance
+ }()
+
+ class func configure() {
+ let sharedController = NotificationsController.shared
+ // Always become the delegate of UNUserNotificationCenter, even before we've requested user
+ // permissions
+ if #available(iOS 10.0, *) {
+ UNUserNotificationCenter.current().delegate = sharedController
+ }
+ }
+
+ func registerForUserFacingNotificationsFor(_ application: UIApplication) {
+ if #available(iOS 10.0, *) {
+ UNUserNotificationCenter.current()
+ .requestAuthorization(options: [.alert, .badge, .sound],
+ completionHandler: { (granted, error) in
+ NotificationCenter.default.post(name: UserNotificationsChangedNotification, object: nil)
+ })
+ } else if #available(iOS 8.0, *) {
+ let userNotificationSettings = UIUserNotificationSettings(types: [.alert, .badge, .sound],
+ categories: [])
+ application.registerUserNotificationSettings(userNotificationSettings)
+
+ } else {
+ application.registerForRemoteNotifications(matching: [.alert, .badge, .sound])
+ }
+ }
+
+ func getAllowedNotificationTypes(_ completion:
+ @escaping (_ allowedTypes: [NotificationsControllerAllowedNotificationType]) -> Void) {
+
+ guard Messaging.messaging().apnsToken != nil else {
+ completion([.none])
+ return
+ }
+
+ var types: [NotificationsControllerAllowedNotificationType] = [.silent]
+ if #available(iOS 10.0, *) {
+ UNUserNotificationCenter.current().getNotificationSettings(completionHandler: { (settings) in
+ if settings.alertSetting == .enabled {
+ types.append(.alert)
+ }
+ if settings.badgeSetting == .enabled {
+ types.append(.badge)
+ }
+ if settings.soundSetting == .enabled {
+ types.append(.sound)
+ }
+ DispatchQueue.main.async {
+ completion(types)
+ }
+ })
+ } else if #available(iOS 8.0, *) {
+ if let userNotificationSettings = UIApplication.shared.currentUserNotificationSettings {
+ if userNotificationSettings.types.contains(.alert) {
+ types.append(.alert)
+ }
+ if userNotificationSettings.types.contains(.badge) {
+ types.append(.badge)
+ }
+ if userNotificationSettings.types.contains(.sound) {
+ types.append(.sound)
+ }
+ }
+ completion(types)
+ } else {
+ let enabledTypes = UIApplication.shared.enabledRemoteNotificationTypes()
+ if enabledTypes.contains(.alert) {
+ types.append(.alert)
+ }
+ if enabledTypes.contains(.badge) {
+ types.append(.badge)
+ }
+ if enabledTypes.contains(.sound) {
+ types.append(.sound)
+ }
+ completion(types)
+ }
+ }
+}
+
+// MARK: - UNUserNotificationCenterDelegate
+@available(iOS 10.0, *)
+extension NotificationsController: UNUserNotificationCenterDelegate {
+
+ func userNotificationCenter(_ center: UNUserNotificationCenter,
+ willPresent notification: UNNotification,
+ withCompletionHandler completionHandler:
+ @escaping (UNNotificationPresentationOptions) -> Void) {
+ // Always show the incoming notification, even if the app is in foreground
+ completionHandler([.alert, .badge, .sound])
+ }
+}
diff --git a/Example/Messaging/Messaging_Example-Bridging-Header.h b/Example/Messaging/Messaging_Example-Bridging-Header.h
new file mode 100644
index 0000000..6bbbdba
--- /dev/null
+++ b/Example/Messaging/Messaging_Example-Bridging-Header.h
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 "FIRSampleAppUtilities.h"
diff --git a/Example/Messaging/Tests/FIRMessagingClientTest.m b/Example/Messaging/Tests/FIRMessagingClientTest.m
new file mode 100644
index 0000000..f36ec53
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingClientTest.m
@@ -0,0 +1,308 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 XCTest;
+
+#import <OCMock/OCMock.h>
+
+#import "Protos/GtalkCore.pbobjc.h"
+
+#import "FIRMessagingCheckinService.h"
+#import "FIRMessagingClient.h"
+#import "FIRMessagingConnection.h"
+#import "FIRMessagingDataMessageManager.h"
+#import "FIRMessagingFakeConnection.h"
+#import "FIRMessagingRegistrar.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessagingSecureSocket.h"
+#import "FIRMessagingUtilities.h"
+#import "NSError+FIRMessaging.h"
+
+#import "FIRReachabilityChecker.h"
+
+static NSString *const kFIRMessagingUserDefaultsSuite = @"FIRMessagingClientTestUserDefaultsSuite";
+
+static NSString *const kDeviceAuthId = @"123456";
+static NSString *const kSecretToken = @"56789";
+static NSString *const kDigest = @"com.google.digest";
+static NSString *const kVersionInfo = @"1.0";
+static NSString *const kSubscriptionID = @"abcdef-subscription-id";
+static NSString *const kDeletedSubscriptionID = @"deleted-abcdef-subscription-id";
+static NSString *const kFIRMessagingAppIDToken = @"1234xyzdef56789";
+static NSString *const kTopicToSubscribeTo = @"/topics/abcdef/hello-world";
+
+@interface FIRMessagingRegistrar ()
+
+@property(nonatomic, readwrite, strong) FIRMessagingCheckinService *checkinService;
+
+@end
+
+@interface FIRMessagingClient () <FIRMessagingConnectionDelegate>
+
+@property(nonatomic, readwrite, strong) FIRMessagingConnection *connection;
+@property(nonatomic, readwrite, strong) FIRMessagingRegistrar *registrar;
+
+@property(nonatomic, readwrite, assign) int64_t lastConnectedTimestamp;
+@property(nonatomic, readwrite, assign) int64_t lastDisconnectedTimestamp;
+@property(nonatomic, readwrite, assign) NSUInteger subscribeRetryCount;
+@property(nonatomic, readwrite, assign) NSUInteger connectRetryCount;
+
+- (NSTimeInterval)connectionTimeoutInterval;
+- (void)setupConnection;
+
+@end
+
+@interface FIRMessagingConnection () <FIRMessagingSecureSocketDelegate>
+
+@property(nonatomic, readwrite, strong) FIRMessagingSecureSocket *socket;
+
+- (void)setupConnectionSocket;
+- (void)connectToSocket:(FIRMessagingSecureSocket *)socket;
+- (NSTimeInterval)connectionTimeoutInterval;
+- (void)sendHeartbeatPing;
+
+@end
+
+@interface FIRMessagingSecureSocket ()
+
+@property(nonatomic, readwrite, assign) FIRMessagingSecureSocketState state;
+
+@end
+
+@interface FIRMessagingClientTest : XCTestCase
+
+@property(nonatomic, readwrite, strong) FIRMessagingClient *client;
+@property(nonatomic, readwrite, strong) id mockClient;
+@property(nonatomic, readwrite, strong) id mockReachability;
+@property(nonatomic, readwrite, strong) id mockRmqManager;
+@property(nonatomic, readwrite, strong) id mockClientDelegate;
+@property(nonatomic, readwrite, strong) id mockDataMessageManager;
+@property(nonatomic, readwrite, strong) id mockRegistrar;
+
+// argument callback blocks
+@property(nonatomic, readwrite, copy) FIRMessagingConnectCompletionHandler connectCompletion;
+@property(nonatomic, readwrite, copy) FIRMessagingTopicOperationCompletion subscribeCompletion;
+
+@end
+
+@implementation FIRMessagingClientTest
+
+- (void)setUp {
+ [super setUp];
+ _mockClientDelegate =
+ OCMStrictProtocolMock(@protocol(FIRMessagingClientDelegate));
+ _mockReachability = OCMClassMock([FIRReachabilityChecker class]);
+ _mockRmqManager = OCMClassMock([FIRMessagingRmqManager class]);
+ _client = [[FIRMessagingClient alloc] initWithDelegate:_mockClientDelegate
+ reachability:_mockReachability
+ rmq2Manager:_mockRmqManager];
+ _mockClient = OCMPartialMock(_client);
+ _mockRegistrar = OCMPartialMock([_client registrar]);
+ [_mockClient setRegistrar:_mockRegistrar];
+ _mockDataMessageManager = OCMClassMock([FIRMessagingDataMessageManager class]);
+ [_mockClient setDataMessageManager:_mockDataMessageManager];
+}
+
+- (void)tearDown {
+ // remove all handlers
+ [self tearDownMocksAndHandlers];
+ // Mock all sockets to disconnect in a nice way
+ [[[(id)self.client.connection.socket stub] andDo:^(NSInvocation *invocation) {
+ self.client.connection.socket.state = kFIRMessagingSecureSocketClosed;
+ }] disconnect];
+
+ [self.client teardown];
+ [super tearDown];
+}
+
+- (void)tearDownMocksAndHandlers {
+ self.connectCompletion = nil;
+ self.subscribeCompletion = nil;
+}
+
+- (void)setupConnectionWithFakeLoginResult:(BOOL)loginResult
+ heartbeatTimeout:(NSTimeInterval)heartbeatTimeout {
+ [self setupFakeConnectionWithClass:[FIRMessagingFakeConnection class]
+ withSetupCompletionHandler:^(FIRMessagingConnection *connection) {
+ FIRMessagingFakeConnection *fakeConnection = (FIRMessagingFakeConnection *)connection;
+ fakeConnection.shouldFakeSuccessLogin = loginResult;
+ fakeConnection.fakeConnectionTimeout = heartbeatTimeout;
+ }];
+}
+
+- (void)testSetupConnection {
+ XCTAssertNil(self.client.connection);
+ [self.client setupConnection];
+ XCTAssertNotNil(self.client.connection);
+ XCTAssertNotNil(self.client.connection.delegate);
+}
+
+- (void)testConnectSuccess_withCachedFcmDefaults {
+ [self addFIRMessagingPreferenceKeysToUserDefaults];
+
+ // login request should be successful
+ [self setupConnectionWithFakeLoginResult:YES heartbeatTimeout:1.0];
+
+ XCTestExpectation *setupConnection = [self
+ expectationWithDescription:@"Fcm should successfully setup a connection"];
+
+ [self.client connectWithHandler:^(NSError *error) {
+ XCTAssertNil(error);
+ [setupConnection fulfill];
+ }];
+
+ [self waitForExpectationsWithTimeout:1.0 handler:^(NSError *error) {
+ XCTAssertNil(error);
+ }];
+}
+
+- (void)testsConnectWithNoNetworkError_withCachedFcmDefaults {
+ // connection timeout interval is 1s
+ [[[self.mockClient stub] andReturnValue:@(1)] connectionTimeoutInterval];
+ [self addFIRMessagingPreferenceKeysToUserDefaults];
+
+ [self setupFakeConnectionWithClass:[FIRMessagingFakeFailConnection class]
+ withSetupCompletionHandler:^(FIRMessagingConnection *connection) {
+ FIRMessagingFakeFailConnection *fakeConnection = (FIRMessagingFakeFailConnection *)connection;
+ fakeConnection.shouldFakeSuccessLogin = NO;
+ // should fail only once
+ fakeConnection.failCount = 1;
+ }];
+
+ XCTestExpectation *connectExpectation = [self
+ expectationWithDescription:@"Should retry connection if once failed"];
+ [self.client connectWithHandler:^(NSError *error) {
+ XCTAssertNotNil(error);
+ XCTAssertEqual(kFIRMessagingErrorCodeNetwork, error.code);
+ [connectExpectation fulfill];
+ }];
+
+ [self waitForExpectationsWithTimeout:10.0
+ handler:^(NSError *error) {
+ XCTAssertNil(error);
+ }];
+}
+
+- (void)testConnectSuccessOnSecondTry_withCachedFcmDefaults {
+ // connection timeout interval is 1s
+ [[[self.mockClient stub] andReturnValue:@(1)] connectionTimeoutInterval];
+ [self addFIRMessagingPreferenceKeysToUserDefaults];
+
+ // the network is available
+ [[[self.mockReachability stub]
+ andReturnValue:@(kFIRReachabilityViaWifi)] reachabilityStatus];
+
+ [self setupFakeConnectionWithClass:[FIRMessagingFakeFailConnection class]
+ withSetupCompletionHandler:^(FIRMessagingConnection *connection) {
+ FIRMessagingFakeFailConnection *fakeConnection = (FIRMessagingFakeFailConnection *)connection;
+ fakeConnection.shouldFakeSuccessLogin = NO;
+ // should fail only once
+ fakeConnection.failCount = 1;
+ }];
+
+ XCTestExpectation *connectExpectation = [self
+ expectationWithDescription:@"Should retry connection if once failed"];
+ [self.client connectWithHandler:^(NSError *error) {
+ XCTAssertNil(error);
+ [connectExpectation fulfill];
+ }];
+
+ [self waitForExpectationsWithTimeout:10.0
+ handler:^(NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertTrue(
+ [self.client isConnectionActive]);
+ }];
+}
+
+- (void)testDisconnectAfterConnect {
+ // setup the connection
+ [self addFIRMessagingPreferenceKeysToUserDefaults];
+
+ // login request should be successful
+ // Connection should not timeout because of heartbeat failure. Therefore set heartbeatTimeout
+ // to a large value.
+ [self setupConnectionWithFakeLoginResult:YES heartbeatTimeout:100.0];
+
+ [[[self.mockClient stub] andReturnValue:@(1)] connectionTimeoutInterval];
+
+ // the network is available
+ [[[self.mockReachability stub]
+ andReturnValue:@(kFIRReachabilityViaWifi)] reachabilityStatus];
+
+ XCTestExpectation *setupConnection =
+ [self expectationWithDescription:@"Fcm should successfully setup a connection"];
+
+ __block int timesConnected = 0;
+ FIRMessagingConnectCompletionHandler handler = ^(NSError *error) {
+ XCTAssertNil(error);
+ timesConnected++;
+ if (timesConnected == 1) {
+ [setupConnection fulfill];
+ // disconnect the connection after some time
+ FIRMessagingFakeConnection *fakeConnection = (FIRMessagingFakeConnection *)[self.mockClient connection];
+ dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (0.2 * NSEC_PER_SEC));
+ dispatch_after(time, dispatch_get_main_queue(), ^{
+ // disconnect now
+ [(FIRMessagingFakeConnection *)fakeConnection mockSocketDisconnect];
+ [(FIRMessagingFakeConnection *)fakeConnection disconnectNow];
+ });
+ } else {
+ XCTFail(@"Fcm should only connect at max 2 times");
+ }
+ };
+ [self.mockClient connectWithHandler:handler];
+
+ // reconnect after disconnect
+ XCTAssertTrue(self.client.isConnectionActive);
+
+ [self waitForExpectationsWithTimeout:10.0
+ handler:^(NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertNotEqual(self.client.lastDisconnectedTimestamp, 0);
+ XCTAssertTrue(self.client.isConnectionActive);
+ }];
+}
+
+#pragma mark - Private Helpers
+
+- (void)setupFakeConnectionWithClass:(Class)connectionClass
+ withSetupCompletionHandler:(void (^)(FIRMessagingConnection *))handler {
+ [[[self.mockClient stub] andDo:^(NSInvocation *invocation) {
+ self.client.connection =
+ [[connectionClass alloc] initWithAuthID:kDeviceAuthId
+ token:kSecretToken
+ host:[FIRMessagingFakeConnection fakeHost]
+ port:[FIRMessagingFakeConnection fakePort]
+ runLoop:[NSRunLoop mainRunLoop]
+ rmq2Manager:self.mockRmqManager
+ fcmManager:self.mockDataMessageManager];
+ self.client.connection.delegate = self.client;
+ handler(self.client.connection);
+ }] setupConnection];
+}
+
+- (void)addFIRMessagingPreferenceKeysToUserDefaults {
+ id mockCheckinService = OCMClassMock([FIRMessagingCheckinService class]);
+ [[[mockCheckinService stub] andReturn:kDeviceAuthId] deviceAuthID];
+ [[[mockCheckinService stub] andReturn:kSecretToken] secretToken];
+ [[[mockCheckinService stub] andReturnValue:@YES] hasValidCheckinInfo];
+
+ [[[self.mockRegistrar stub] andReturn:mockCheckinService] checkinService];
+}
+
+@end
+
diff --git a/Example/Messaging/Tests/FIRMessagingCodedInputStreamTest.m b/Example/Messaging/Tests/FIRMessagingCodedInputStreamTest.m
new file mode 100644
index 0000000..7cc2d97
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingCodedInputStreamTest.m
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 XCTest;
+
+#import "FIRMessagingCodedInputStream.h"
+
+@interface FIRMessagingCodedInputStreamTest : XCTestCase
+@end
+
+@implementation FIRMessagingCodedInputStreamTest
+
+- (void)testReadingSmallDataStream {
+ FIRMessagingCodedInputStream *stream =
+ [[FIRMessagingCodedInputStream alloc] initWithData:[[self class] sampleData1]];
+ int8_t actualTag = 2;
+ int8_t tag;
+ XCTAssertTrue([stream readTag:&tag]);
+ XCTAssertEqual(actualTag, tag);
+
+ // test length
+ int32_t actualLength = 4;
+ int32_t length;
+ XCTAssertTrue([stream readLength:&length]);
+ XCTAssertEqual(actualLength, length);
+
+ NSData *actualData = [[self class] packetDataForSampleData1];
+ NSData *data = [stream readDataWithLength:length];
+ XCTAssertTrue([actualData isEqualToData:data]);
+}
+
+- (void)testReadingLargeDataStream {
+ FIRMessagingCodedInputStream *stream =
+ [[FIRMessagingCodedInputStream alloc] initWithData:[[self class] sampleData2]];
+ int8_t actualTag = 5;
+ int8_t tag;
+ XCTAssertTrue([stream readTag:&tag]);
+ XCTAssertEqual(actualTag, tag);
+
+ int32_t actualLength = 257;
+ int32_t length;
+ XCTAssertTrue([stream readLength:&length]);
+ XCTAssertEqual(actualLength, length);
+
+ NSData *actualData = [[self class] packetDataForSampleData2];
+ NSData *data = [stream readDataWithLength:length];
+ XCTAssertTrue([actualData isEqualToData:data]);
+}
+
+- (void)testReadingInvalidDataStream {
+ FIRMessagingCodedInputStream *stream =
+ [[FIRMessagingCodedInputStream alloc] initWithData:[[self class] invalidData]];
+ int8_t actualTag = 7;
+ int8_t tag;
+ XCTAssertTrue([stream readTag:&tag]);
+ XCTAssertEqual(actualTag, tag);
+
+ int32_t actualLength = 2;
+ int32_t length;
+ XCTAssertTrue([stream readLength:&length]);
+ XCTAssertEqual(actualLength, length);
+
+ XCTAssertNil([stream readDataWithLength:length]);
+}
+
++ (NSData *)sampleData1 {
+ // tag = 2,
+ // length = 4,
+ // data = integer 255
+ const char data[] = { 0x02, 0x04, 0x80, 0x00, 0x00, 0xff };
+ return [NSData dataWithBytes:data length:6];
+}
+
++ (NSData *)packetDataForSampleData1 {
+ const char data[] = { 0x80, 0x00, 0x00, 0xff };
+ return [NSData dataWithBytes:data length:4];
+}
+
++ (NSData *)sampleData2 {
+ // test reading varint properly
+ // tag = 5,
+ // length = 257,
+ // data = length 257
+ const char tagAndLength[] = { 0x05, 0x81, 0x02 };
+ NSMutableData *data = [NSMutableData dataWithBytes:tagAndLength length:3];
+ [data appendData:[self packetDataForSampleData2]];
+ return data;
+}
+
++ (NSData *)packetDataForSampleData2 {
+ char packetData[257] = { 0xff, 0xff, 0xff };
+ return [NSData dataWithBytes:packetData length:257];
+}
+
++ (NSData *)invalidData {
+ // tag = 7,
+ // length = 2,
+ // data = (length 1)
+ const char data[] = { 0x07, 0x02, 0xff };
+ return [NSData dataWithBytes:data length:3];
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingConnectionTest.m b/Example/Messaging/Tests/FIRMessagingConnectionTest.m
new file mode 100644
index 0000000..47b29d2
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingConnectionTest.m
@@ -0,0 +1,480 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 XCTest;
+
+#import <OCMock/OCMock.h>
+
+#import "Protos/GtalkCore.pbobjc.h"
+#import <GoogleToolboxForMac/GTMDefines.h>
+
+#import "FIRMessagingClient.h"
+#import "FIRMessagingConnection.h"
+#import "FIRMessagingDataMessageManager.h"
+#import "FIRMessagingFakeConnection.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessagingSecureSocket.h"
+#import "FIRMessagingUtilities.h"
+
+static NSString *const kDeviceAuthId = @"123456";
+static NSString *const kSecretToken = @"56789";
+
+// used to verify if we are sending in the right proto or not.
+// set it to negative value to disable this check
+static FIRMessagingProtoTag currentProtoSendTag;
+
+@interface FIRMessagingSecureSocket ()
+
+@property(nonatomic, readwrite, assign) FIRMessagingSecureSocketState state;
+
+@end
+
+@interface FIRMessagingSecureSocket (test_FIRMessagingConnection)
+
+- (void)_successconnectToHost:(NSString *)host
+ port:(NSUInteger)port
+ onRunLoop:(NSRunLoop *)runLoop;
+- (void)_fakeSuccessfulSocketConnect;
+
+@end
+
+@implementation FIRMessagingSecureSocket (test_FIRMessagingConnection)
+
+- (void)_successconnectToHost:(NSString *)host
+ port:(NSUInteger)port
+ onRunLoop:(NSRunLoop *)runLoop {
+ // created ports, opened streams
+ // invoke callback async
+ [self _fakeSuccessfulSocketConnect];
+}
+
+- (void)_fakeSuccessfulSocketConnect {
+ self.state = kFIRMessagingSecureSocketOpen;
+ [self.delegate secureSocketDidConnect:self];
+}
+
+@end
+
+// make sure these are defined in FIRMessagingConnection
+@interface FIRMessagingConnection () <FIRMessagingSecureSocketDelegate>
+
+@property(nonatomic, readwrite, assign) int64_t lastLoginServerTimestamp;
+@property(nonatomic, readwrite, assign) int lastStreamIdAcked;
+@property(nonatomic, readwrite, assign) int inStreamId;
+@property(nonatomic, readwrite, assign) int outStreamId;
+
+@property(nonatomic, readwrite, strong) FIRMessagingSecureSocket *socket;
+
+@property(nonatomic, readwrite, strong) NSMutableArray *unackedS2dIds;
+@property(nonatomic, readwrite, strong) NSMutableDictionary *ackedS2dMap;
+@property(nonatomic, readwrite, strong) NSMutableArray *d2sInfos;
+
+- (void)setupConnectionSocket;
+- (void)connectToSocket:(FIRMessagingSecureSocket *)socket;
+- (NSTimeInterval)connectionTimeoutInterval;
+- (void)sendHeartbeatPing;
+
+@end
+
+
+@interface FIRMessagingConnectionTest : XCTestCase
+
+@property(nonatomic, readwrite, assign) BOOL didSuccessfullySendData;
+
+@property(nonatomic, readwrite, strong) NSUserDefaults *userDefaults;
+@property(nonatomic, readwrite, strong) FIRMessagingConnection *fakeConnection;
+@property(nonatomic, readwrite, strong) id mockClient;
+@property(nonatomic, readwrite, strong) id mockConnection;
+@property(nonatomic, readwrite, strong) id mockRmq;
+@property(nonatomic, readwrite, strong) id mockDataMessageManager;
+
+@end
+
+@implementation FIRMessagingConnectionTest
+
+- (void)setUp {
+ [super setUp];
+ _userDefaults = [[NSUserDefaults alloc] init];
+ _mockRmq = OCMClassMock([FIRMessagingRmqManager class]);
+ _mockDataMessageManager = OCMClassMock([FIRMessagingDataMessageManager class]);
+ // fake connection is only used to simulate the socket behavior
+ _fakeConnection = [[FIRMessagingFakeConnection alloc] initWithAuthID:kDeviceAuthId
+ token:kSecretToken
+ host:[FIRMessagingFakeConnection fakeHost]
+ port:[FIRMessagingFakeConnection fakePort]
+ runLoop:[NSRunLoop currentRunLoop]
+ rmq2Manager:_mockRmq
+ fcmManager:_mockDataMessageManager];
+
+ _mockClient = OCMClassMock([FIRMessagingClient class]);
+ _fakeConnection.delegate = _mockClient;
+ _mockConnection = OCMPartialMock(_fakeConnection);
+ _didSuccessfullySendData = NO;
+}
+
+- (void)tearDown {
+ [self.fakeConnection teardown];
+ [super tearDown];
+}
+
+- (void)testInitialConnectionNotConnected {
+ XCTAssertEqual(kFIRMessagingConnectionNotConnected, [self.fakeConnection state]);
+}
+
+- (void)testSuccessfulSocketConnection {
+ [self.fakeConnection signIn];
+
+ // should be connected now
+ XCTAssertEqual(kFIRMessagingConnectionConnected, self.fakeConnection.state);
+ XCTAssertEqual(0, self.fakeConnection.lastStreamIdAcked);
+ XCTAssertEqual(0, self.fakeConnection.inStreamId);
+ XCTAssertEqual(0, self.fakeConnection.ackedS2dMap.count);
+ XCTAssertEqual(0, self.fakeConnection.unackedS2dIds.count);
+
+ [self stubSocketDisconnect:self.fakeConnection.socket];
+}
+
+- (void)testSignInAndThenSignOut {
+ [self.fakeConnection signIn];
+ [self stubSocketDisconnect:self.fakeConnection.socket];
+ [self.fakeConnection signOut];
+ XCTAssertEqual(kFIRMessagingSecureSocketClosed, self.fakeConnection.socket.state);
+}
+
+- (void)testSuccessfulSignIn {
+ [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
+ XCTAssertEqual(self.fakeConnection.state, kFIRMessagingConnectionSignedIn);
+ XCTAssertEqual(self.fakeConnection.outStreamId, 2);
+ XCTAssertTrue(self.didSuccessfullySendData);
+}
+
+- (void)testSignOut_whenSignedIn {
+ [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
+
+ // should be signed in now
+ id mockSocket = self.fakeConnection.socket;
+ [self.fakeConnection signOut];
+ XCTAssertEqual(self.fakeConnection.state, kFIRMessagingConnectionNotConnected);
+ XCTAssertEqual(self.fakeConnection.outStreamId, 3);
+ XCTAssertNil([(FIRMessagingSecureSocket *)mockSocket delegate]);
+ XCTAssertTrue(self.didSuccessfullySendData);
+ OCMVerify([mockSocket sendData:[OCMArg any]
+ withTag:kFIRMessagingProtoTagClose
+ rmqId:[OCMArg isNil]]);
+}
+
+- (void)testReceiveCloseProto {
+ [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
+
+ id mockSocket = self.fakeConnection.socket;
+ GtalkClose *close = [[GtalkClose alloc] init];
+ [self.fakeConnection secureSocket:mockSocket
+ didReceiveData:[close data]
+ withTag:kFIRMessagingProtoTagClose];
+ XCTAssertEqual(self.fakeConnection.state, kFIRMessagingConnectionNotConnected);
+ XCTAssertTrue(self.didSuccessfullySendData);
+}
+
+- (void)testLoginRequest {
+ XCTAssertEqual(kFIRMessagingConnectionNotConnected, [self.fakeConnection state]);
+ [self.fakeConnection setupConnectionSocket];
+
+ id socketMock = OCMPartialMock(self.fakeConnection.socket);
+ self.fakeConnection.socket = socketMock;
+
+ [[[socketMock stub]
+ andDo:^(NSInvocation *invocation) {
+ [socketMock _fakeSuccessfulSocketConnect];
+ }]
+ connectToHost:[FIRMessagingFakeConnection fakeHost]
+ port:[FIRMessagingFakeConnection fakePort]
+ onRunLoop:[OCMArg any]];
+
+ [[[socketMock stub] andCall:@selector(_sendData:withTag:rmqId:) onObject:self]
+ // do nothing
+ sendData:[OCMArg any]
+ withTag:kFIRMessagingProtoTagLoginRequest
+ rmqId:[OCMArg isNil]];
+
+ // swizzle disconnect socket
+ OCMVerify([[[socketMock stub] andCall:@selector(_disconnectSocket)
+ onObject:self] disconnect]);
+
+ currentProtoSendTag = kFIRMessagingProtoTagLoginRequest;
+ // send login request
+ [self.fakeConnection connectToSocket:socketMock];
+
+ // verify login request sent
+ XCTAssertEqual(1, self.fakeConnection.outStreamId);
+ XCTAssertTrue(self.didSuccessfullySendData);
+}
+
+- (void)testLoginRequest_withPendingMessagesInRmq {
+ // TODO: add fake messages to rmq and test login request with them
+}
+
+- (void)testLoginRequest_withSuccessfulResponse {
+ [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
+
+ OCMVerify([self.mockClient didLoginWithConnection:[OCMArg isEqual:self.fakeConnection]]);
+
+ // should send a heartbeat ping too
+ XCTAssertEqual(self.fakeConnection.outStreamId, 2);
+ // update for the received login response proto
+ XCTAssertEqual(self.fakeConnection.inStreamId, 1);
+ // did send data during login
+ XCTAssertTrue(self.didSuccessfullySendData);
+}
+
+- (void)testConnectionTimeout {
+ XCTAssertEqual(kFIRMessagingConnectionNotConnected, [self.fakeConnection state]);
+
+ [self.fakeConnection setupConnectionSocket];
+
+ id socketMock = OCMPartialMock(self.fakeConnection.socket);
+ self.fakeConnection.socket = socketMock;
+
+ [[[socketMock stub]
+ andDo:^(NSInvocation *invocation) {
+ [socketMock _fakeSuccessfulSocketConnect];
+ }]
+ connectToHost:[FIRMessagingFakeConnection fakeHost]
+ port:[FIRMessagingFakeConnection fakePort]
+ onRunLoop:[OCMArg any]];
+
+ [self.fakeConnection connectToSocket:socketMock];
+ XCTAssertEqual(self.fakeConnection.state, kFIRMessagingConnectionConnected);
+
+ GtalkLoginResponse *response = [[GtalkLoginResponse alloc] init];
+ [response setId_p:@""];
+
+ // connection timeout has been scheduled
+ // should disconnect since we wait for more time
+ XCTestExpectation *disconnectExpectation =
+ [self expectationWithDescription:
+ @"FCM connection should timeout without receiving "
+ @"any data for a timeout interval"];
+ [[[socketMock stub]
+ andDo:^(NSInvocation *invocation) {
+ [self _disconnectSocket];
+ [disconnectExpectation fulfill];
+ }] disconnect];
+
+ // simulate connection receiving login response
+ [self.fakeConnection secureSocket:socketMock
+ didReceiveData:[response data]
+ withTag:kFIRMessagingProtoTagLoginResponse];
+
+ [self waitForExpectationsWithTimeout:2.0
+ handler:^(NSError *error) {
+ XCTAssertNil(error);
+ }];
+
+ [socketMock verify];
+ XCTAssertEqual(self.fakeConnection.state, kFIRMessagingConnectionNotConnected);
+}
+
+- (void)testDataMessageReceive {
+ [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
+ GtalkDataMessageStanza *stanza = [[GtalkDataMessageStanza alloc] init];
+ [stanza setCategory:@"special"];
+ [stanza setFrom:@"xyz"];
+ [self.fakeConnection secureSocket:self.fakeConnection.socket
+ didReceiveData:[stanza data]
+ withTag:kFIRMessagingProtoTagDataMessageStanza];
+
+ OCMVerify([self.mockClient connectionDidRecieveMessage:[OCMArg checkWithBlock:^BOOL(id obj) {
+ GtalkDataMessageStanza *message = (GtalkDataMessageStanza *)obj;
+ return [[message category] isEqual:@"special"] && [[message from] isEqual:@"xyz"];
+ }]]);
+ // did send data while login
+ XCTAssertTrue(self.didSuccessfullySendData);
+}
+
+- (void)testDataMessageReceiveWithInvalidTag {
+ [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
+ GtalkDataMessageStanza *stanza = [[GtalkDataMessageStanza alloc] init];
+ BOOL didCauseException = NO;
+ @try {
+ [self.fakeConnection secureSocket:self.fakeConnection.socket
+ didReceiveData:[stanza data]
+ withTag:kFIRMessagingProtoTagInvalid];
+ } @catch (NSException *exception) {
+ didCauseException = YES;
+ } @finally {
+ }
+ XCTAssertFalse(didCauseException);
+}
+
+- (void)testDataMessageReceiveWithTagThatDoesntEquateToClass {
+ [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
+ GtalkDataMessageStanza *stanza = [[GtalkDataMessageStanza alloc] init];
+ BOOL didCauseException = NO;
+ int8_t tagWhichDoesntEquateToClass = INT8_MAX;
+ @try {
+ [self.fakeConnection secureSocket:self.fakeConnection.socket
+ didReceiveData:[stanza data]
+ withTag:tagWhichDoesntEquateToClass];
+ } @catch (NSException *exception) {
+ didCauseException = YES;
+ } @finally {
+ }
+ XCTAssertFalse(didCauseException);
+}
+
+- (void)testHeartbeatSend {
+ [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection]; // outstreamId should be 2
+ XCTAssertEqual(self.fakeConnection.outStreamId, 2);
+ [self.fakeConnection sendHeartbeatPing];
+ id mockSocket = self.fakeConnection.socket;
+ OCMVerify([mockSocket sendData:[OCMArg any]
+ withTag:kFIRMessagingProtoTagHeartbeatPing
+ rmqId:[OCMArg isNil]]);
+ XCTAssertEqual(self.fakeConnection.outStreamId, 3);
+ // did send data
+ XCTAssertTrue(self.didSuccessfullySendData);
+}
+
+- (void)testHeartbeatReceived {
+ [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
+ XCTAssertEqual(self.fakeConnection.outStreamId, 2);
+ GtalkHeartbeatPing *ping = [[GtalkHeartbeatPing alloc] init];
+ [self.fakeConnection secureSocket:self.fakeConnection.socket
+ didReceiveData:[ping data]
+ withTag:kFIRMessagingProtoTagHeartbeatPing];
+ XCTAssertEqual(self.fakeConnection.inStreamId, 2);
+ id mockSocket = self.fakeConnection.socket;
+ OCMVerify([mockSocket sendData:[OCMArg any]
+ withTag:kFIRMessagingProtoTagHeartbeatAck
+ rmqId:[OCMArg isNil]]);
+ XCTAssertEqual(self.fakeConnection.outStreamId, 3);
+ // did send data
+ XCTAssertTrue(self.didSuccessfullySendData);
+}
+
+// TODO: Add tests for Selective/Stream ACK's
+
+#pragma mark - Stubs
+
+- (void)_disconnectSocket {
+ self.fakeConnection.socket.state = kFIRMessagingSecureSocketClosed;
+}
+
+- (void)_sendData:(NSData *)data withTag:(int8_t)tag rmqId:(NSString *)rmqId {
+ _GTMDevLog(@"FIRMessaging Socket: Send data with Tag: %d rmq: %@", tag, rmqId);
+ if (currentProtoSendTag > 0) {
+ XCTAssertEqual(tag, currentProtoSendTag);
+ }
+ self.didSuccessfullySendData = YES;
+}
+
+#pragma mark - Private Helpers
+
+/**
+ * Stub socket disconnect to prevent spurious assert. Since we mock the socket object being
+ * used by the connection, while we teardown the client we also disconnect the socket to tear
+ * it down. Since we are using mock sockets we need to stub the `disconnect` to prevent some
+ * assertions from taking place.
+ * The `_disconectSocket` has the gist of the actual socket disconnect without any assertions.
+ */
+- (void)stubSocketDisconnect:(id)mockSocket {
+ [[[mockSocket stub] andCall:@selector(_disconnectSocket)
+ onObject:self] disconnect];
+
+ [mockSocket verify];
+}
+
+- (void)mockSuccessfulSignIn {
+ XCTAssertEqual(kFIRMessagingConnectionNotConnected, [self.fakeConnection state]);
+ [self.fakeConnection setupConnectionSocket];
+
+ id socketMock = OCMPartialMock(self.fakeConnection.socket);
+ self.fakeConnection.socket = socketMock;
+
+ [[[socketMock stub]
+ andDo:^(NSInvocation *invocation) {
+ [socketMock _fakeSuccessfulSocketConnect];
+ }]
+ connectToHost:[FIRMessagingFakeConnection fakeHost]
+ port:[FIRMessagingFakeConnection fakePort]
+ onRunLoop:[OCMArg any]];
+
+ [[[socketMock stub] andCall:@selector(_sendData:withTag:rmqId:) onObject:self]
+ // do nothing
+ sendData:[OCMArg any]
+ withTag:kFIRMessagingProtoTagLoginRequest
+ rmqId:[OCMArg isNil]];
+
+ // send login request
+ currentProtoSendTag = kFIRMessagingProtoTagLoginRequest;
+ [self.fakeConnection connectToSocket:socketMock];
+
+ GtalkLoginResponse *response = [[GtalkLoginResponse alloc] init];
+ [response setId_p:@""];
+
+ // simulate connection receiving login response
+ [self.fakeConnection secureSocket:socketMock
+ didReceiveData:[response data]
+ withTag:kFIRMessagingProtoTagLoginResponse];
+
+ OCMVerify([self.mockClient didLoginWithConnection:[OCMArg isEqual:self.fakeConnection]]);
+
+ // should receive data
+ XCTAssertTrue(self.didSuccessfullySendData);
+ // should send a heartbeat ping too
+ XCTAssertEqual(self.fakeConnection.outStreamId, 2);
+ // update for the received login response proto
+ XCTAssertEqual(self.fakeConnection.inStreamId, 1);
+}
+
+- (void)setupSuccessfulLoginRequestWithConnection:(FIRMessagingConnection *)fakeConnection {
+ [fakeConnection setupConnectionSocket];
+
+ id socketMock = OCMPartialMock(fakeConnection.socket);
+ fakeConnection.socket = socketMock;
+
+ [[[socketMock stub]
+ andDo:^(NSInvocation *invocation) {
+ [socketMock _fakeSuccessfulSocketConnect];
+ }]
+ connectToHost:[FIRMessagingFakeConnection fakeHost]
+ port:[FIRMessagingFakeConnection fakePort]
+ onRunLoop:[OCMArg any]];
+
+ [[[socketMock stub] andCall:@selector(_sendData:withTag:rmqId:) onObject:self]
+ // do nothing
+ sendData:[OCMArg any]
+ withTag:kFIRMessagingProtoTagLoginRequest
+ rmqId:[OCMArg isNil]];
+
+ // swizzle disconnect socket
+ [[[socketMock stub] andCall:@selector(_disconnectSocket)
+ onObject:self] disconnect];
+
+ // send login request
+ currentProtoSendTag = kFIRMessagingProtoTagLoginRequest;
+ [fakeConnection connectToSocket:socketMock];
+
+ GtalkLoginResponse *response = [[GtalkLoginResponse alloc] init];
+ [response setId_p:@""];
+
+ // simulate connection receiving login response
+ [fakeConnection secureSocket:socketMock
+ didReceiveData:[response data]
+ withTag:kFIRMessagingProtoTagLoginResponse];
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingContextManagerServiceTest.m b/Example/Messaging/Tests/FIRMessagingContextManagerServiceTest.m
new file mode 100644
index 0000000..cb48e7f
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingContextManagerServiceTest.m
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 <XCTest/XCTest.h>
+
+#import <OCMock/OCMock.h>
+
+#import "FIRMessagingContextManagerService.h"
+
+@interface FIRMessagingContextManagerServiceTest : XCTestCase
+
+@property(nonatomic, readwrite, strong) NSDateFormatter *dateFormatter;
+@property(nonatomic, readwrite, strong) NSMutableArray *scheduledLocalNotifications;
+
+@end
+
+@implementation FIRMessagingContextManagerServiceTest
+
+- (void)setUp {
+ [super setUp];
+ self.dateFormatter = [[NSDateFormatter alloc] init];
+ [self.dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
+ self.scheduledLocalNotifications = [NSMutableArray array];
+ [self mockSchedulingLocalNotifications];
+}
+
+- (void)tearDown {
+ [super tearDown];
+}
+
+/**
+ * Test invalid context manager message, missing lt_start string.
+ */
+- (void)testInvalidContextManagerMessage_missingStartTime {
+ NSDictionary *message = @{
+ @"hello" : @"world",
+ };
+ XCTAssertFalse([FIRMessagingContextManagerService isContextManagerMessage:message]);
+}
+
+/**
+ * Test valid context manager message.
+ */
+- (void)testValidContextManagerMessage {
+ NSDictionary *message = @{
+ kFIRMessagingContextManagerLocalTimeStart: @"2015-12-12 00:00:00",
+ @"hello" : @"world",
+ };
+ XCTAssertTrue([FIRMessagingContextManagerService isContextManagerMessage:message]);
+}
+
+// TODO: Enable these tests. They fail because we cannot schedule local
+// notifications on OSX without permission. It's better to mock AppDelegate's
+// scheduleLocalNotification to mock scheduling behavior.
+
+/**
+ * Context Manager message with future start date should be successfully scheduled.
+ */
+- (void)testMessageWithFutureStartTime {
+ NSString *messageIdentifier = @"fcm-cm-test1";
+ NSString *startTimeString = @"2020-01-12 12:00:00"; // way into the future
+ NSDictionary *message = @{
+ kFIRMessagingContextManagerLocalTimeStart: startTimeString,
+ kFIRMessagingContextManagerBodyKey : @"Hello world!",
+ @"id": messageIdentifier,
+ @"hello" : @"world"
+ };
+
+ XCTAssertTrue([FIRMessagingContextManagerService handleContextManagerMessage:message]);
+
+ XCTAssertEqual(self.scheduledLocalNotifications.count, 1);
+ UILocalNotification *notification = [self.scheduledLocalNotifications firstObject];
+ NSDate *date = [self.dateFormatter dateFromString:startTimeString];
+ XCTAssertEqual([notification.fireDate compare:date], NSOrderedSame);
+}
+
+/**
+ * Context Manager message with past end date should not be scheduled.
+ */
+- (void)testMessageWithPastEndTime {
+ NSString *messageIdentifier = @"fcm-cm-test1";
+ NSString *startTimeString = @"2010-01-12 12:00:00"; // way into the past
+ NSString *endTimeString = @"2011-01-12 12:00:00"; // way into the past
+ NSDictionary *message = @{
+ kFIRMessagingContextManagerLocalTimeStart: startTimeString,
+ kFIRMessagingContextManagerLocalTimeEnd : endTimeString,
+ kFIRMessagingContextManagerBodyKey : @"Hello world!",
+ @"id": messageIdentifier,
+ @"hello" : @"world"
+ };
+
+ XCTAssertTrue([FIRMessagingContextManagerService handleContextManagerMessage:message]);
+ XCTAssertEqual(self.scheduledLocalNotifications.count, 0);
+}
+
+/**
+ * Context Manager message with past start and future end date should be successfully
+ * scheduled.
+ */
+- (void)testMessageWithPastStartAndFutureEndTime {
+ NSString *messageIdentifier = @"fcm-cm-test1";
+ NSDate *startDate = [NSDate dateWithTimeIntervalSinceNow:-1000]; // past
+ NSDate *endDate = [NSDate dateWithTimeIntervalSinceNow:1000]; // future
+ NSString *startTimeString = [self.dateFormatter stringFromDate:startDate];
+ NSString *endTimeString = [self.dateFormatter stringFromDate:endDate];
+
+ NSDictionary *message = @{
+ kFIRMessagingContextManagerLocalTimeStart : startTimeString,
+ kFIRMessagingContextManagerLocalTimeEnd : endTimeString,
+ kFIRMessagingContextManagerBodyKey : @"Hello world!",
+ @"id": messageIdentifier,
+ @"hello" : @"world"
+ };
+
+ XCTAssertTrue([FIRMessagingContextManagerService handleContextManagerMessage:message]);
+
+ XCTAssertEqual(self.scheduledLocalNotifications.count, 1);
+ UILocalNotification *notification = [self.scheduledLocalNotifications firstObject];
+ // schedule notification after start date
+ XCTAssertEqual([notification.fireDate compare:startDate], NSOrderedDescending);
+ // schedule notification after end date
+ XCTAssertEqual([notification.fireDate compare:endDate], NSOrderedAscending);
+}
+
+/**
+ * Test correctly parsing user data in local notifications.
+ */
+- (void)testTimedNotificationsUserInfo {
+ NSString *messageIdentifierKey = @"message.id";
+ NSString *messageIdentifier = @"fcm-cm-test1";
+ NSString *startTimeString = @"2020-01-12 12:00:00"; // way into the future
+
+ NSString *customDataKey = @"hello";
+ NSString *customData = @"world";
+ NSDictionary *message = @{
+ kFIRMessagingContextManagerLocalTimeStart : startTimeString,
+ kFIRMessagingContextManagerBodyKey : @"Hello world!",
+ messageIdentifierKey : messageIdentifier,
+ customDataKey : customData,
+ };
+
+ XCTAssertTrue([FIRMessagingContextManagerService handleContextManagerMessage:message]);
+
+ XCTAssertEqual(self.scheduledLocalNotifications.count, 1);
+ UILocalNotification *notification = [self.scheduledLocalNotifications firstObject];
+ XCTAssertEqualObjects(notification.userInfo[messageIdentifierKey], messageIdentifier);
+ XCTAssertEqualObjects(notification.userInfo[customDataKey], customData);
+}
+
+#pragma mark - Private Helpers
+
+- (void)mockSchedulingLocalNotifications {
+ id mockApplication = OCMPartialMock([UIApplication sharedApplication]);
+ __block UILocalNotification *notificationToSchedule;
+ [[[mockApplication stub]
+ andDo:^(NSInvocation *invocation) {
+ // Mock scheduling a notification
+ if (notificationToSchedule) {
+ [self.scheduledLocalNotifications addObject:notificationToSchedule];
+ }
+ }] scheduleLocalNotification:[OCMArg checkWithBlock:^BOOL(id obj) {
+ if ([obj isKindOfClass:[UILocalNotification class]]) {
+ notificationToSchedule = obj;
+ return YES;
+ }
+ return NO;
+ }]];
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingDataMessageManagerTest.m b/Example/Messaging/Tests/FIRMessagingDataMessageManagerTest.m
new file mode 100644
index 0000000..2b4f407
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingDataMessageManagerTest.m
@@ -0,0 +1,662 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 XCTest;
+
+#import <OCMock/OCMock.h>
+
+#import "Protos/GtalkCore.pbobjc.h"
+
+#import "FIRMessaging.h"
+#import "FIRMessagingClient.h"
+#import "FIRMessagingConfig.h"
+#import "FIRMessagingConnection.h"
+#import "FIRMessagingDataMessageManager.h"
+#import "FIRMessagingReceiver.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessagingSyncMessageManager.h"
+#import "FIRMessagingUtilities.h"
+#import "FIRMessaging_Private.h"
+#import "FIRMessagingConstants.h"
+#import "FIRMessagingDefines.h"
+#import "NSError+FIRMessaging.h"
+
+static NSString *const kFIRMessagingUserDefaultsSuite = @"FIRMessagingClientTestUserDefaultsSuite";
+
+static NSString *const kFIRMessagingAppIDToken = @"1234abcdef789";
+
+static NSString *const kMessagePersistentID = @"abcdef123";
+static NSString *const kMessageFrom = @"com.example.gcm";
+static NSString *const kMessageTo = @"123456789";
+static NSString *const kCollapseKey = @"collapse-1";
+static NSString *const kAppDataItemKey = @"hello";
+static NSString *const kAppDataItemValue = @"world";
+static NSString *const kAppDataItemInvalidKey = @"google.hello";
+
+static NSString *const kRmqDatabaseName = @"gcm-dmm-test";
+
+@interface FIRMessagingDataMessageManager()
+
+@property(nonatomic, readwrite, weak) FIRMessagingRmqManager *rmq2Manager;
+
+- (NSString *)categoryForUpstreamMessages;
+
+@end
+
+@interface FIRMessagingDataMessageManagerTest : XCTestCase
+
+@property(nonatomic, readwrite, strong) id mockClient;
+@property(nonatomic, readwrite, strong) id mockRmqManager;
+@property(nonatomic, readwrite, strong) id mockReceiver;
+@property(nonatomic, readwrite, strong) id mockSyncMessageManager;
+@property(nonatomic, readwrite, strong) FIRMessagingDataMessageManager *dataMessageManager;
+@property(nonatomic, readwrite, strong) id mockDataMessageManager;
+
+@end
+
+@implementation FIRMessagingDataMessageManagerTest
+
+- (void)setUp {
+ [super setUp];
+ _mockClient = OCMClassMock([FIRMessagingClient class]);
+ _mockReceiver = OCMClassMock([FIRMessagingReceiver class]);
+ _mockRmqManager = OCMClassMock([FIRMessagingRmqManager class]);
+ _mockSyncMessageManager = OCMClassMock([FIRMessagingSyncMessageManager class]);
+ _dataMessageManager = [[FIRMessagingDataMessageManager alloc]
+ initWithDelegate:_mockReceiver
+ client:_mockClient
+ rmq2Manager:_mockRmqManager
+ syncMessageManager:_mockSyncMessageManager];
+ [_dataMessageManager refreshDelayedMessages];
+ _mockDataMessageManager = OCMPartialMock(_dataMessageManager);
+}
+
+
+- (void)testSendValidMessage_withNoConnection {
+ // mock no connection initially
+ NSString *messageID = @"1";
+ BOOL mockConnectionActive = NO;
+ [[[self.mockClient stub] andDo:^(NSInvocation *invocation) {
+ NSValue *returnValue = [NSValue valueWithBytes:&mockConnectionActive
+ objCType:@encode(BOOL)];
+ [invocation setReturnValue:&returnValue];
+ }] isConnectionActive];
+
+ BOOL(^isValidStanza)(id obj) = ^BOOL(id obj) {
+ if ([obj isKindOfClass:[GtalkDataMessageStanza class]]) {
+ GtalkDataMessageStanza *message = (GtalkDataMessageStanza *)obj;
+ return ([message.id_p isEqualToString:messageID] && [message.to isEqualToString:kMessageTo]);
+ }
+ return NO;
+ };
+ OCMExpect([self.mockReceiver willSendDataMessageWithID:[OCMArg isEqual:messageID]
+ error:[OCMArg isNil]]);
+ [[[self.mockRmqManager stub] andReturnValue:@YES]
+ saveRmqMessage:[OCMArg checkWithBlock:isValidStanza]
+ error:[OCMArg anyObjectRef]];
+
+ // should be logged into the service
+ [self addFakeFIRMessagingRegistrationToken];
+ [self.dataMessageManager setDeviceAuthID:@"auth-id" secretToken:@"secret-token"];
+ // try to send messages with no connection should be queued into RMQ
+ NSMutableDictionary *message = [self upstreamMessageWithID:messageID ttl:-1 delay:0];
+ [self.dataMessageManager sendDataMessageStanza:message];
+
+ OCMVerifyAll(self.mockReceiver);
+ OCMVerifyAll(self.mockRmqManager);
+}
+
+- (void)testSendValidMessage_withoutCheckinAuthentication {
+ NSString *messageID = @"1";
+ NSMutableDictionary *message = [self standardFIRMessagingMessageWithMessageID:messageID];
+
+ OCMExpect([self.mockReceiver
+ willSendDataMessageWithID:[OCMArg isEqual:messageID]
+ error:[OCMArg checkWithBlock:^BOOL(id obj) {
+ if ([obj isKindOfClass:[NSError class]]) {
+ NSError *error = (NSError *)obj;
+ return error.code == kFIRMessagingErrorCodeMissingDeviceID;
+ }
+ return NO;
+ }]]);
+
+ // do not log into checkin service
+ [self.dataMessageManager sendDataMessageStanza:message];
+
+ OCMVerifyAll(self.mockReceiver);
+}
+
+- (void)testSendInvalidMessage_withNoTo {
+ NSString *messageID = @"1";
+ NSMutableDictionary *message =
+ [FIRMessaging createFIRMessagingMessageWithMessage:@{ kAppDataItemKey : kAppDataItemValue}
+ to:@""
+ withID:messageID
+ timeToLive:-1
+ delay:0];
+
+ OCMExpect([self.mockReceiver
+ willSendDataMessageWithID:[OCMArg isEqual:messageID]
+ error:[OCMArg checkWithBlock:^BOOL(id obj) {
+ if ([obj isKindOfClass:[NSError class]]) {
+ NSError *error = (NSError *)obj;
+ return error.code == kFIRMessagingErrorMissingTo;
+ }
+ return NO;
+ }]]);
+
+ // should be logged into the service
+ [self addFakeFIRMessagingRegistrationToken];
+ [self.dataMessageManager setDeviceAuthID:@"auth-id" secretToken:@"secret-token"];
+ [self.dataMessageManager sendDataMessageStanza:message];
+
+ OCMVerifyAll(self.mockReceiver);
+}
+
+- (void)testSendInvalidMessage_withSizeExceeded {
+ NSString *messageID = @"1";
+ NSString *veryLargeString = [@"a" stringByPaddingToLength:4 * 1024 // 4kB
+ withString:@"b"
+ startingAtIndex:0];
+ NSMutableDictionary *message =
+ [FIRMessaging createFIRMessagingMessageWithMessage:@{ kAppDataItemKey : veryLargeString }
+ to:kMessageTo
+ withID:messageID
+ timeToLive:-1
+ delay:0];
+
+ OCMExpect([self.mockReceiver
+ willSendDataMessageWithID:[OCMArg isEqual:messageID]
+ error:[OCMArg checkWithBlock:^BOOL(id obj) {
+ if ([obj isKindOfClass:[NSError class]]) {
+ NSError *error = (NSError *)obj;
+ return error.code == kFIRMessagingErrorSizeExceeded;
+ }
+ return NO;
+ }]]);
+
+ [self addFakeFIRMessagingRegistrationToken];
+ // should be logged into the service
+ [self.dataMessageManager setDeviceAuthID:@"auth-id" secretToken:@"secret-token"];
+ [self.dataMessageManager sendDataMessageStanza:message];
+
+ OCMVerifyAll(self.mockReceiver);
+}
+
+// TODO: Add test with rawData exceeding 4KB in size
+
+- (void)testSendValidMessage_withRmqSaveError {
+ NSString *messageID = @"1";
+ NSMutableDictionary *message = [self standardFIRMessagingMessageWithMessageID:messageID];
+ [[[self.mockRmqManager stub] andReturnValue:@NO]
+ saveRmqMessage:[OCMArg any] error:[OCMArg anyObjectRef]];
+
+ OCMExpect([self.mockReceiver
+ willSendDataMessageWithID:[OCMArg isEqual:messageID]
+ error:[OCMArg checkWithBlock:^BOOL(id obj) {
+ if ([obj isKindOfClass:[NSError class]]) {
+ NSError *error = (NSError *)obj;
+ return error.code == kFIRMessagingErrorSave;
+ }
+ return NO;
+ }]]);
+
+ // should be logged into the service
+ [self.dataMessageManager setDeviceAuthID:@"auth-id" secretToken:@"secret-token"];
+ [self addFakeFIRMessagingRegistrationToken];
+ [self.dataMessageManager sendDataMessageStanza:message];
+
+ OCMVerifyAll(self.mockReceiver);
+}
+
+- (void)testSendValidMessage_withTTL0 {
+ // simulate a valid connection
+ [[[self.mockClient stub] andReturnValue:@YES] isConnectionActive];
+ NSString *messageID = @"1";
+ NSMutableDictionary *message = [self upstreamMessageWithID:messageID ttl:0 delay:0];
+
+ BOOL(^isValidStanza)(id obj) = ^BOOL(id obj) {
+ if ([obj isKindOfClass:[GtalkDataMessageStanza class]]) {
+ GtalkDataMessageStanza *stanza = (GtalkDataMessageStanza *)obj;
+ return ([stanza.id_p isEqualToString:messageID] &&
+ [stanza.to isEqualToString:kMessageTo] &&
+ stanza.ttl == 0);
+ }
+ return NO;
+ };
+
+ OCMExpect([self.mockClient sendMessage:[OCMArg checkWithBlock:isValidStanza]]);
+
+ [self.dataMessageManager setDeviceAuthID:@"auth-id" secretToken:@"secret-token"];
+ [self addFakeFIRMessagingRegistrationToken];
+ [self.dataMessageManager sendDataMessageStanza:message];
+
+ OCMVerifyAll(self.mockClient);
+}
+
+// TODO: This is failing on simulator 7.1 & 8.2, take this out temporarily
+- (void)XXX_testSendValidMessage_withTTL0AndNoFIRMessagingConnection {
+ // simulate a invalid connection
+ [[[self.mockClient stub] andReturnValue:@NO] isConnectionActive];
+
+ // simulate network reachability
+ FIRMessaging *service = [FIRMessaging messaging];
+ id mockService = OCMPartialMock(service);
+ [[[mockService stub] andReturnValue:@YES] isNetworkAvailable];
+
+ NSString *messageID = @"1";
+ NSMutableDictionary *message = [self upstreamMessageWithID:messageID ttl:0 delay:0];
+
+
+ BOOL(^isValidStanza)(id obj) = ^BOOL(id obj) {
+ if ([obj isKindOfClass:[GtalkDataMessageStanza class]]) {
+ GtalkDataMessageStanza *stanza = (GtalkDataMessageStanza *)obj;
+ return ([stanza.id_p isEqualToString:messageID] &&
+ [stanza.to isEqualToString:kMessageTo] &&
+ stanza.ttl == 0);
+ }
+ return NO;
+ };
+
+ // should save the message to be sent when we reconnect the next time
+ OCMExpect([self.mockClient sendOnConnectOrDrop:[OCMArg checkWithBlock:isValidStanza]]);
+ // should also try to reconnect immediately
+ OCMExpect([self.mockClient retryConnectionImmediately:[OCMArg isEqual:@YES]]);
+
+ [self.dataMessageManager setDeviceAuthID:@"auth-id" secretToken:@"secret-token"];
+ [self addFakeFIRMessagingRegistrationToken];
+ [self.dataMessageManager sendDataMessageStanza:message];
+
+ OCMVerifyAll(self.mockClient);
+}
+
+// TODO: Investigate why this test is flaky
+- (void)xxx_testSendValidMessage_withTTL0AndNoNetwork {
+ // simulate a invalid connection
+ [[[self.mockClient stub] andReturnValue:@NO] isConnectionActive];
+
+ NSString *messageID = @"1";
+ NSMutableDictionary *message = [self upstreamMessageWithID:messageID ttl:0 delay:0];
+
+
+ // should drop the message since there is no network
+ OCMExpect([self.mockReceiver willSendDataMessageWithID:[OCMArg isEqual:messageID]
+ error:[OCMArg checkWithBlock:^BOOL(id obj) {
+ if ([obj isKindOfClass:[NSError class]]) {
+ NSError *error = (NSError *)obj;
+ return error.code == kFIRMessagingErrorCodeNetwork;
+ }
+ return NO;
+ }]]);
+
+ [self.dataMessageManager setDeviceAuthID:@"auth-id" secretToken:@"secret-token"];
+ [self addFakeFIRMessagingRegistrationToken];
+ [self.dataMessageManager sendDataMessageStanza:message];
+
+ OCMVerifyAll(self.mockReceiver);
+}
+
+// TODO: This failed on simulator 7.1 & 8.2, take this out temporarily
+- (void)XXX_testDelayedMessagesBeingResentOnReconnect {
+ static BOOL isConnectionActive = NO;
+ OCMStub([self.mockClient isConnectionActive]).andDo(^(NSInvocation *invocation) {
+ [invocation setReturnValue:&isConnectionActive];
+ });
+
+ // message that lives for 2 seconds
+ NSString *messageID = @"1";
+ int ttl = 2;
+ NSMutableDictionary *message = [self upstreamMessageWithID:messageID ttl:ttl delay:1];
+
+ __block GtalkDataMessageStanza *firstMessageStanza;
+
+ OCMStub([self.mockRmqManager saveRmqMessage:[OCMArg any]
+ error:[OCMArg anyObjectRef]]).andReturn(YES);
+
+ OCMExpect([self.mockReceiver willSendDataMessageWithID:[OCMArg isEqual:messageID]
+ error:[OCMArg isNil]]);
+
+ [self.dataMessageManager setDeviceAuthID:@"auth-id" secretToken:@"secret-token"];
+ [self addFakeFIRMessagingRegistrationToken];
+ [self.dataMessageManager sendDataMessageStanza:message];
+
+ __block FIRMessagingDataMessageHandler dataMessageHandler;
+
+ [[[self.mockRmqManager stub] andDo:^(NSInvocation *invocation) {
+ dataMessageHandler([FIRMessagingGetRmq2Id(firstMessageStanza) longLongValue],
+ firstMessageStanza);
+ }]
+ scanWithRmqMessageHandler:[OCMArg isNil]
+ dataMessageHandler:[OCMArg checkWithBlock:^BOOL(id obj) {
+ dataMessageHandler = obj;
+ return YES;
+ }]];
+
+ // expect both 1 and 2 messages to be sent once we regain connection
+ __block BOOL firstMessageSent = NO;
+ __block BOOL secondMessageSent = NO;
+ XCTestExpectation *didSendAllMessages =
+ [self expectationWithDescription:@"Did send all messages"];
+ OCMExpect([self.mockClient sendMessage:[OCMArg checkWithBlock:^BOOL(id obj) {
+ // [didSendAllMessages fulfill];
+ if ([obj isKindOfClass:[GtalkDataMessageStanza class]]) {
+ GtalkDataMessageStanza *message = (GtalkDataMessageStanza *)obj;
+ if ([@"1" isEqualToString:message.id_p]) {
+ firstMessageSent = YES;
+ } else if ([@"2" isEqualToString:message.id_p]) {
+ secondMessageSent = YES;
+ }
+ if (firstMessageSent && secondMessageSent) {
+ [didSendAllMessages fulfill];
+ }
+ return firstMessageSent || secondMessageSent;
+ }
+ return NO;
+ }]]);
+
+ // send the second message after some delay
+ [NSThread sleepForTimeInterval:2.0];
+
+ isConnectionActive = YES;
+ // simulate active connection
+ NSString *newMessageID = @"2";
+ NSMutableDictionary *newMessage = [self upstreamMessageWithID:newMessageID
+ ttl:0
+ delay:0];
+ // send another message to resend not sent messages
+ [self.dataMessageManager sendDataMessageStanza:newMessage];
+
+ [self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) {
+ XCTAssertNil(error);
+ OCMVerifyAll(self.mockClient);
+ OCMVerifyAll(self.mockReceiver);
+ }];
+}
+
+- (void)testSendDelayedMessage_shouldNotSend {
+ // should not send a delayed message even with an active connection
+ // simulate active connection
+ [[[self.mockClient stub] andReturnValue:OCMOCK_VALUE(YES)] isConnectionActive];
+ [[self.mockClient reject] sendMessage:[OCMArg any]];
+
+ [[self.mockReceiver reject] didSendDataMessageWithID:[OCMArg any]];
+
+ // delayed message
+ NSString *messageID = @"1";
+ [self.dataMessageManager setDeviceAuthID:@"auth-id" secretToken:@"secret-token"];
+ NSMutableDictionary *message = [self upstreamMessageWithID:messageID ttl:0 delay:1];
+ [self.dataMessageManager sendDataMessageStanza:message];
+
+ OCMVerifyAll(self.mockClient);
+ OCMVerifyAll(self.mockReceiver);
+}
+
+- (void)testProcessPacket_withValidPacket {
+ GtalkDataMessageStanza *message = [self validDataMessagePacket];
+ NSDictionary *parsedMessage = [self.dataMessageManager processPacket:message];
+ XCTAssertEqualObjects(parsedMessage[kFIRMessagingFromKey], message.from);
+ XCTAssertEqualObjects(parsedMessage[kFIRMessagingCollapseKey], message.token);
+ XCTAssertEqualObjects(parsedMessage[kFIRMessagingMessageIDKey], kMessagePersistentID);
+ XCTAssertEqualObjects(parsedMessage[kAppDataItemKey], kAppDataItemValue);
+ XCTAssertEqual(4, parsedMessage.count);
+}
+
+- (void)testProcessPacket_withOnlyFrom {
+ GtalkDataMessageStanza *message = [self validDataMessageWithOnlyFrom];
+ NSDictionary *parsedMessage = [self.dataMessageManager processPacket:message];
+ XCTAssertEqualObjects(parsedMessage[kFIRMessagingFromKey], message.from);
+ XCTAssertEqualObjects(parsedMessage[kFIRMessagingMessageIDKey], kMessagePersistentID);
+ XCTAssertEqual(2, parsedMessage.count);
+}
+
+- (void)testProcessPacket_withInvalidPacket {
+ GtalkDataMessageStanza *message = [self invalidDataMessageUsingReservedKeyword];
+ NSDictionary *parsedMessage = [self.dataMessageManager processPacket:message];
+ XCTAssertEqualObjects(parsedMessage[kFIRMessagingFromKey], message.from);
+ XCTAssertEqualObjects(parsedMessage[kFIRMessagingMessageIDKey], kMessagePersistentID);
+ XCTAssertEqual(2, parsedMessage.count);
+}
+
+/**
+ * Test parsing a duplex message.
+ */
+- (void)testProcessPacket_withDuplexMessage {
+ GtalkDataMessageStanza *stanza = [self validDuplexmessage];
+ NSDictionary *parsedMessage = [self.dataMessageManager processPacket:stanza];
+ XCTAssertEqual(5, parsedMessage.count);
+ XCTAssertEqualObjects(parsedMessage[kFIRMessagingFromKey], stanza.from);
+ XCTAssertEqualObjects(parsedMessage[kFIRMessagingCollapseKey], stanza.token);
+ XCTAssertEqualObjects(parsedMessage[kFIRMessagingMessageIDKey], kMessagePersistentID);
+ XCTAssertEqualObjects(parsedMessage[kAppDataItemKey], kAppDataItemValue);
+ XCTAssertTrue([parsedMessage[kFIRMessagingMessageSyncViaMCSKey] boolValue]);
+}
+
+- (void)testReceivingParsedMessage {
+ NSDictionary *message = @{ @"hello" : @"world" };
+ OCMStub([self.mockReceiver didReceiveMessage:[OCMArg isEqual:message] withIdentifier:[OCMArg any]]);
+ [self.dataMessageManager didReceiveParsedMessage:message];
+ OCMVerify([self.mockReceiver didReceiveMessage:message withIdentifier:[OCMArg any]]);
+}
+
+/**
+ * Test receiving a new duplex message notifies the receiver callback.
+ */
+- (void)testReceivingNewDuplexMessage {
+ GtalkDataMessageStanza *message = [self validDuplexmessage];
+ NSDictionary *parsedMessage = [self.dataMessageManager processPacket:message];
+ [[[self.mockSyncMessageManager stub] andReturnValue:@(NO)]
+ didReceiveMCSSyncMessage:parsedMessage];
+ OCMStub([self.mockReceiver didReceiveMessage:[OCMArg isEqual:message] withIdentifier:[OCMArg any]]);
+ [self.dataMessageManager didReceiveParsedMessage:parsedMessage];
+ OCMVerify([self.mockReceiver didReceiveMessage:[OCMArg any] withIdentifier:[OCMArg any]]);
+}
+
+/**
+ * Test receiving a duplicated duplex message does not notify the receiver callback.
+ */
+- (void)testReceivingDuplicateDuplexMessage {
+ GtalkDataMessageStanza *message = [self validDuplexmessage];
+ NSDictionary *parsedMessage = [self.dataMessageManager processPacket:message];
+ [[[self.mockSyncMessageManager stub] andReturnValue:@(YES)]
+ didReceiveMCSSyncMessage:parsedMessage];
+ [[self.mockReceiver reject] didReceiveMessage:[OCMArg any] withIdentifier:[OCMArg any]];
+ [self.dataMessageManager didReceiveParsedMessage:parsedMessage];
+}
+
+/**
+ * In this test we simulate a real RMQ manager and send messages simulating no
+ * active connection. Then we simulate a new connection being established and
+ * the client receives a Streaming ACK which should result in resending RMQ messages.
+ */
+- (void)testResendSavedMessages {
+ static BOOL isClientConnected = NO;
+ [[[self.mockClient stub] andDo:^(NSInvocation *invocation) {
+ [invocation setReturnValue:&isClientConnected];
+ }] isConnectionActive];
+
+ // Set a fake, valid bundle identifier
+ [[[self.mockDataMessageManager stub] andReturn:@"gcm-dmm-test"] categoryForUpstreamMessages];
+
+ [FIRMessagingRmqManager removeDatabaseWithName:kRmqDatabaseName];
+ FIRMessagingRmqManager *newRmqManager =
+ [[FIRMessagingRmqManager alloc] initWithDatabaseName:kRmqDatabaseName];
+ [newRmqManager loadRmqId];
+ // have a real RMQ store
+ [self.dataMessageManager setRmq2Manager:newRmqManager];
+
+ [self addFakeFIRMessagingRegistrationToken];
+ [self.dataMessageManager setDeviceAuthID:@"auth-id" secretToken:@"secret-token"];
+
+ // send a couple of message with no connection should be saved to RMQ
+ [self.dataMessageManager sendDataMessageStanza:
+ [self upstreamMessageWithID:@"1" ttl:20000 delay:0]];
+ [self.dataMessageManager sendDataMessageStanza:
+ [self upstreamMessageWithID:@"2" ttl:20000 delay:0]];
+
+ [NSThread sleepForTimeInterval:1.0];
+ isClientConnected = YES;
+ // after the usual version, login assertion we would receive a SelectiveAck
+ // assuming we we weren't able to send any messages we won't delete anything
+ // from the RMQ but try to resend whatever is there
+ __block int didRecieveMessages = 0;
+ id mockConnection = OCMClassMock([FIRMessagingConnection class]);
+
+ BOOL (^resendMessageBlock)(id obj) = ^BOOL(id obj) {
+ if ([obj isKindOfClass:[GtalkDataMessageStanza class]]) {
+ GtalkDataMessageStanza *message = (GtalkDataMessageStanza *)obj;
+ NSLog(@"hello resending %@, %d", message.id_p, didRecieveMessages);
+ if ([@"1" isEqualToString:message.id_p]) {
+ didRecieveMessages |= 1; // right most bit for 1st message
+ return YES;
+ } else if ([@"2" isEqualToString:message.id_p]) {
+ didRecieveMessages |= (1<<1); // second from RMB for 2nd message
+ return YES;
+ }
+ }
+ return NO;
+ };
+ [[[mockConnection stub] andDo:^(NSInvocation *invocation) {
+ // pass
+ }] sendProto:[OCMArg checkWithBlock:resendMessageBlock]];
+
+ [self.dataMessageManager resendMessagesWithConnection:mockConnection];
+
+ // should send both messages
+ XCTAssert(didRecieveMessages == 3);
+ OCMVerifyAll(mockConnection);
+}
+
+- (void)testResendingExpiredMessagesFails {
+ // TODO: Test that expired messages should not be sent on resend
+ static BOOL isClientConnected = NO;
+ [[[self.mockClient stub] andDo:^(NSInvocation *invocation) {
+ [invocation setReturnValue:&isClientConnected];
+ }] isConnectionActive];
+
+ // Set a fake, valid bundle identifier
+ [[[self.mockDataMessageManager stub] andReturn:@"gcm-dmm-test"] categoryForUpstreamMessages];
+
+ [FIRMessagingRmqManager removeDatabaseWithName:kRmqDatabaseName];
+ FIRMessagingRmqManager *newRmqManager =
+ [[FIRMessagingRmqManager alloc] initWithDatabaseName:kRmqDatabaseName];
+ [newRmqManager loadRmqId];
+ // have a real RMQ store
+ [self.dataMessageManager setRmq2Manager:newRmqManager];
+
+ [self.dataMessageManager setDeviceAuthID:@"auth-id" secretToken:@"secret-token"];
+ // send a message that expires in 1 sec
+ [self.dataMessageManager sendDataMessageStanza:
+ [self upstreamMessageWithID:@"1" ttl:1 delay:0]];
+
+ // wait for 2 seconds (let the above message expire)
+ [NSThread sleepForTimeInterval:2.0];
+ isClientConnected = YES;
+
+ id mockConnection = OCMClassMock([FIRMessagingConnection class]);
+
+ [[mockConnection reject] sendProto:[OCMArg any]];
+ [self.dataMessageManager resendMessagesWithConnection:mockConnection];
+
+ // rmq should not have any pending messages
+ [newRmqManager scanWithRmqMessageHandler:^(int64_t rmqId, int8_t tag, NSData *data) {
+ XCTFail(@"RMQ should not have any message");
+ }
+ dataMessageHandler:nil];
+}
+
+#pragma mark - Private
+
+- (void)addFakeFIRMessagingRegistrationToken {
+ // [[FIRMessagingDefaultsManager sharedInstance] saveAppIDToken:kFIRMessagingAppIDToken];
+}
+
+#pragma mark - Create Packet
+
+- (GtalkDataMessageStanza *)validDataMessagePacket {
+ GtalkDataMessageStanza *message = [[GtalkDataMessageStanza alloc] init];
+ message.from = kMessageFrom;
+ message.token = kCollapseKey;
+ message.persistentId = kMessagePersistentID;
+ GtalkAppData *item = [[GtalkAppData alloc] init];
+ item.key = kAppDataItemKey;
+ item.value = kAppDataItemValue;
+ message.appDataArray = [NSMutableArray arrayWithObject:item];
+ return message;
+}
+
+- (GtalkDataMessageStanza *)validDataMessageWithOnlyFrom {
+ GtalkDataMessageStanza *message = [[GtalkDataMessageStanza alloc] init];
+ message.from = kMessageFrom;
+ message.persistentId = kMessagePersistentID;
+ return message;
+}
+
+- (GtalkDataMessageStanza *)invalidDataMessageUsingReservedKeyword {
+ GtalkDataMessageStanza *message = [[GtalkDataMessageStanza alloc] init];
+ message.from = kMessageFrom;
+ message.persistentId = kMessagePersistentID;
+ GtalkAppData *item = [[GtalkAppData alloc] init];
+ item.key = kAppDataItemInvalidKey;
+ item.value = kAppDataItemValue;
+ message.appDataArray = [NSMutableArray arrayWithObject:item];
+ return message;
+}
+
+- (GtalkDataMessageStanza *)validDataMessageForFIRMessaging {
+ GtalkDataMessageStanza *message = [[GtalkDataMessageStanza alloc] init];
+ message.from = kMessageFrom;
+ message.token = @"com.google.gcm";
+ return message;
+}
+
+- (GtalkDataMessageStanza *)validDuplexmessage {
+ GtalkDataMessageStanza *message = [[GtalkDataMessageStanza alloc] init];
+ message.from = kMessageFrom;
+ message.token = kCollapseKey;
+ message.persistentId = kMessagePersistentID;
+ GtalkAppData *item = [[GtalkAppData alloc] init];
+ item.key = kAppDataItemKey;
+ item.value = kAppDataItemValue;
+ GtalkAppData *duplexItem = [[GtalkAppData alloc] init];
+ duplexItem.key = @"gcm.duplex";
+ duplexItem.value = @"1";
+ message.appDataArray = [NSMutableArray arrayWithObjects:item, duplexItem, nil];
+ return message;
+}
+
+#pragma mark - Create Message
+
+- (NSMutableDictionary *)standardFIRMessagingMessageWithMessageID:(NSString *)messageID {
+ NSDictionary *message = @{ kAppDataItemKey : kAppDataItemValue };
+ return [FIRMessaging createFIRMessagingMessageWithMessage:message
+ to:kMessageTo
+ withID:messageID
+ timeToLive:-1
+ delay:0];
+}
+
+- (NSMutableDictionary *)upstreamMessageWithID:(NSString *)messageID
+ ttl:(int64_t)ttl
+ delay:(int)delay {
+ NSDictionary *message = @{ kAppDataItemInvalidKey : kAppDataItemValue };
+ return [FIRMessaging createFIRMessagingMessageWithMessage:message
+ to:kMessageTo
+ withID:messageID
+ timeToLive:ttl
+ delay:delay];
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingFakeConnection.h b/Example/Messaging/Tests/FIRMessagingFakeConnection.h
new file mode 100644
index 0000000..7fe52bf
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingFakeConnection.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 "FIRMessagingConnection.h"
+
+/**
+ * A bunch of different fake connections are used to simulate various connection behaviours.
+ * A fake connection that successfully conects to remote host.
+ */
+// TODO: Split FIRMessagingConnection to make it more testable.
+@interface FIRMessagingFakeConnection : FIRMessagingConnection
+
+@property(nonatomic, readwrite, assign) BOOL shouldFakeSuccessLogin;
+
+// timeout caused by heartbeat failure (defaults to 0.5s)
+@property(nonatomic, readwrite, assign) NSTimeInterval fakeConnectionTimeout;
+
+/**
+ * Should stub the socket disconnect to not fail when called
+ */
+- (void)mockSocketDisconnect;
+
+/**
+ * Calls disconnect on the socket(which should theoretically be mocked by the above method) and
+ * let the socket delegate know that it has been disconnected.
+ */
+- (void)disconnectNow;
+
+/**
+ * The fake host to connect to.
+ */
++ (NSString *)fakeHost;
+
+/**
+ * The fake port used to connect.
+ */
++ (int)fakePort;
+
+@end
+
+/**
+ * A fake connection that simulates failure a certain number of times before success.
+ */
+// TODO: Coalesce this with the FIRMessagingFakeConnection itself.
+@interface FIRMessagingFakeFailConnection : FIRMessagingFakeConnection
+
+@property(nonatomic, readwrite, assign) int failCount;
+@property(nonatomic, readwrite, assign) int signInRequests;
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingFakeConnection.m b/Example/Messaging/Tests/FIRMessagingFakeConnection.m
new file mode 100644
index 0000000..de2e0bb
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingFakeConnection.m
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 "FIRMessagingFakeConnection.h"
+
+#import <OCMock/OCMock.h>
+
+#import "Protos/GtalkCore.pbobjc.h"
+
+#import "FIRMessagingSecureSocket.h"
+#import "FIRMessagingUtilities.h"
+
+static NSString *const kHost = @"localhost";
+static const int kPort = 6234;
+
+@interface FIRMessagingSecureSocket ()
+
+@property(nonatomic, readwrite, assign) FIRMessagingSecureSocketState state;
+
+@end
+
+@interface FIRMessagingConnection ()
+
+@property(nonatomic, readwrite, strong) FIRMessagingSecureSocket *socket;
+
+- (void)setupConnectionSocket;
+- (void)connectToSocket:(FIRMessagingSecureSocket *)socket;
+- (NSTimeInterval)connectionTimeoutInterval;
+- (void)sendHeartbeatPing;
+- (void)secureSocket:(FIRMessagingSecureSocket *)socket
+ didReceiveData:(NSData *)data
+ withTag:(int8_t)tag;
+
+@end
+
+@implementation FIRMessagingFakeConnection
+
+- (void)signIn {
+
+ // use this if you don't really want to mock/stub the login behaviour. In case
+ // you want to stub the login behavoiur you should do these things manually in
+ // your test and add custom logic in between as required for your testing.
+ [self setupConnectionSocket];
+
+ id socketMock = OCMPartialMock(self.socket);
+ self.socket = socketMock;
+ [[[socketMock stub]
+ andDo:^(NSInvocation *invocation) {
+ if (self.shouldFakeSuccessLogin) {
+ [self willFakeSuccessfulLoginToFCM];
+ }
+ self.socket.state = kFIRMessagingSecureSocketOpen;
+ [self.socket.delegate secureSocketDidConnect:self.socket];
+ }]
+ connectToHost:kHost
+ port:kPort
+ onRunLoop:[OCMArg any]];
+
+ [self connectToSocket:socketMock];
+}
+
+- (NSTimeInterval)connectionTimeoutInterval {
+ if (self.fakeConnectionTimeout) {
+ return self.fakeConnectionTimeout;
+ } else {
+ return 0.5; // 0.5s
+ }
+}
+
+- (void)mockSocketDisconnect {
+ id mockSocket = self.socket;
+ [[[mockSocket stub] andDo:^(NSInvocation *invocation) {
+ self.socket.state = kFIRMessagingSecureSocketClosed;
+ }] disconnect];
+}
+
+- (void)disconnectNow {
+ [self.socket disconnect];
+ [self.socket.delegate didDisconnectWithSecureSocket:self.socket];
+}
+
++ (NSString *)fakeHost {
+ return @"localhost";
+}
+
++ (int)fakePort {
+ return 6234;
+}
+
+- (void)willFakeSuccessfulLoginToFCM {
+ id mockSocket = self.socket;
+ [[[mockSocket stub]
+ andDo:^(NSInvocation *invocation) {
+ // mock successful login
+
+ GtalkLoginResponse *response = [[GtalkLoginResponse alloc] init];
+ [response setId_p:@""];
+ [self secureSocket:self.socket
+ didReceiveData:[response data]
+ withTag:kFIRMessagingProtoTagLoginResponse];
+ }]
+ sendData:[OCMArg any]
+ withTag:kFIRMessagingProtoTagLoginRequest
+ rmqId:[OCMArg isNil]];
+}
+
+@end
+
+@implementation FIRMessagingFakeFailConnection
+
+- (void)signIn {
+ self.signInRequests++;
+ [self setupConnectionSocket];
+ id mockSocket = OCMPartialMock(self.socket);
+ self.socket = mockSocket;
+ [[[mockSocket stub]
+ andDo:^(NSInvocation *invocation) {
+ [self mockSocketDisconnect];
+ if (self.signInRequests <= self.failCount) {
+ // do nothing -- should timeout
+ } else {
+ // since we will always fail once we would disconnect the socket before
+ // we ever try again thus mock the disconnect to change the state and
+ // prevent any assertions
+ [self willFakeSuccessfulLoginToFCM];
+ self.socket.state = kFIRMessagingSecureSocketOpen;
+ [self.socket.delegate secureSocketDidConnect:self.socket];
+ }
+ }]
+ connectToHost:kHost
+ port:kPort
+ onRunLoop:[OCMArg any]];
+
+ [self connectToSocket:mockSocket];
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingFakeSocket.h b/Example/Messaging/Tests/FIRMessagingFakeSocket.h
new file mode 100644
index 0000000..ecba9dc
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingFakeSocket.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 "FIRMessagingSecureSocket.h"
+
+@interface FIRMessagingFakeSocket : FIRMessagingSecureSocket
+
+/**
+ * Initialize socket with a given buffer size. Designated Initializer.
+ *
+ * @param bufferSize The buffer size used to connect the input and the output stream. Note
+ * when we write data to the output stream it's read in terms of this buffer
+ * size. So for tests using `FIRMessagingFakeSocket` you should use an appropriate
+ * buffer size in terms of what you are writing to the buffer and what should
+ * be read. Since there is no "flush" operation in NSStream we would have to
+ * live with this.
+ *
+ * @see {FIRMessagingSecureSocketTest} for example usage.
+ * @return A fake secure socket.
+ */
+- (instancetype)initWithBufferSize:(uint8_t)bufferSize;
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingFakeSocket.m b/Example/Messaging/Tests/FIRMessagingFakeSocket.m
new file mode 100644
index 0000000..2b0b477
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingFakeSocket.m
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 "FIRMessagingFakeSocket.h"
+
+#import "FIRMessagingConstants.h"
+#import "FIRMessagingDefines.h"
+#import <GoogleToolboxForMac/GTMDefines.h>
+
+@interface FIRMessagingSecureSocket() <NSStreamDelegate>
+
+@property(nonatomic, readwrite, assign) FIRMessagingSecureSocketState state;
+@property(nonatomic, readwrite, strong) NSInputStream *inStream;
+@property(nonatomic, readwrite, strong) NSOutputStream *outStream;
+
+@property(nonatomic, readwrite, assign) BOOL isInStreamOpen;
+@property(nonatomic, readwrite, assign) BOOL isOutStreamOpen;
+
+@property(nonatomic, readwrite, strong) NSRunLoop *runLoop;
+
+@end
+
+@interface FIRMessagingFakeSocket ()
+
+@property(nonatomic, readwrite, assign) int8_t bufferSize;
+
+@end
+
+@implementation FIRMessagingFakeSocket
+
+- (instancetype)initWithBufferSize:(uint8_t)bufferSize {
+ self = [super init];
+ if (self) {
+ _bufferSize = bufferSize;
+ }
+ return self;
+}
+
+- (void)connectToHost:(NSString *)host
+ port:(NSUInteger)port
+ onRunLoop:(NSRunLoop *)runLoop {
+ self.state = kFIRMessagingSecureSocketOpening;
+ self.runLoop = runLoop;
+
+ CFReadStreamRef inputStreamRef = nil;
+ CFWriteStreamRef outputStreamRef = nil;
+
+ CFStreamCreateBoundPair(NULL,
+ &inputStreamRef,
+ &outputStreamRef,
+ self.bufferSize);
+
+ self.inStream = CFBridgingRelease(inputStreamRef);
+ self.outStream = CFBridgingRelease(outputStreamRef);
+ if (!self.inStream || !self.outStream) {
+ FIRMessaging_FAIL(@"cannot create a fake socket");
+ return;
+ }
+
+ self.isInStreamOpen = NO;
+ self.isOutStreamOpen = NO;
+
+ [self openStream:self.outStream];
+ [self openStream:self.inStream];
+}
+
+- (void)openStream:(NSStream *)stream {
+ _GTMDevAssert(stream, @"Cannot open nil stream");
+ if (stream) {
+ stream.delegate = self;
+ [stream scheduleInRunLoop:self.runLoop forMode:NSDefaultRunLoopMode];
+ [stream open];
+ }
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingLinkHandlingTest.m b/Example/Messaging/Tests/FIRMessagingLinkHandlingTest.m
new file mode 100644
index 0000000..94ce530
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingLinkHandlingTest.m
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 XCTest;
+
+#import <OCMock/OCMock.h>
+
+#import "FIRMessaging.h"
+#import "FIRMessagingConfig.h"
+#import "FIRMessagingConstants.h"
+#import "FIRMessagingTestNotificationUtilities.h"
+
+@interface FIRMessaging ()
+
+- (instancetype)initWithConfig:(FIRMessagingConfig *)config;
+- (NSURL *)linkURLFromMessage:(NSDictionary *)message;
+
+@end
+
+@interface FIRMessagingLinkHandlingTest : XCTestCase
+
+@property(nonatomic, readonly, strong) FIRMessaging *messaging;
+
+@end
+
+@implementation FIRMessagingLinkHandlingTest
+
+- (void)setUp {
+ [super setUp];
+
+ FIRMessagingConfig *config = [FIRMessagingConfig defaultConfig];
+ _messaging = [[FIRMessaging alloc] initWithConfig:config];
+}
+
+- (void)tearDown {
+ _messaging = nil;
+ [super tearDown];
+}
+
+#pragma mark - Link Handling Testing
+
+- (void)testNonExistentLinkInMessage {
+ NSMutableDictionary *notification =
+ [FIRMessagingTestNotificationUtilities createBasicNotificationWithUniqueMessageID];
+ NSURL *url = [self.messaging linkURLFromMessage:notification];
+ XCTAssertNil(url);
+}
+
+- (void)testEmptyLinkInMessage {
+ NSMutableDictionary *notification =
+ [FIRMessagingTestNotificationUtilities createBasicNotificationWithUniqueMessageID];
+ notification[kFIRMessagingMessageLinkKey] = @"";
+ NSURL *url = [self.messaging linkURLFromMessage:notification];
+ XCTAssertNil(url);
+}
+
+- (void)testNonStringLinkInMessage {
+ NSMutableDictionary *notification =
+ [FIRMessagingTestNotificationUtilities createBasicNotificationWithUniqueMessageID];
+ notification[kFIRMessagingMessageLinkKey] = @(5);
+ NSURL *url = [self.messaging linkURLFromMessage:notification];
+ XCTAssertNil(url);
+}
+
+- (void)testInvalidURLStringLinkInMessage {
+ NSMutableDictionary *notification =
+ [FIRMessagingTestNotificationUtilities createBasicNotificationWithUniqueMessageID];
+ notification[kFIRMessagingMessageLinkKey] = @"This is not a valid url string";
+ NSURL *url = [self.messaging linkURLFromMessage:notification];
+ XCTAssertNil(url);
+}
+
+- (void)testValidURLStringLinkInMessage {
+ NSMutableDictionary *notification =
+ [FIRMessagingTestNotificationUtilities createBasicNotificationWithUniqueMessageID];
+ notification[kFIRMessagingMessageLinkKey] = @"https://www.google.com/";
+ NSURL *url = [self.messaging linkURLFromMessage:notification];
+ XCTAssertTrue([url.absoluteString isEqualToString:@"https://www.google.com/"]);
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingPendingTopicsListTest.m b/Example/Messaging/Tests/FIRMessagingPendingTopicsListTest.m
new file mode 100644
index 0000000..2033cb4
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingPendingTopicsListTest.m
@@ -0,0 +1,263 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 <OCMock/OCMock.h>
+#import <XCTest/XCTest.h>
+
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingPendingTopicsList.h"
+#import "FIRMessagingTopicsCommon.h"
+
+typedef void (^MockDelegateSubscriptionHandler)(NSString *topic,
+ FIRMessagingTopicAction action,
+ FIRMessagingTopicOperationCompletion completion);
+
+/**
+ * This object lets us provide a stub delegate where we can customize the behavior by providing
+ * blocks. We need to use this instead of stubbing a OCMockProtocol because our delegate methods
+ * take primitive values (e.g. action), which is not easy to use from OCMock
+ * @see http://stackoverflow.com/a/6332023
+ */
+@interface MockPendingTopicsListDelegate: NSObject <FIRMessagingPendingTopicsListDelegate>
+
+@property(nonatomic, assign) BOOL isReady;
+@property(nonatomic, copy) MockDelegateSubscriptionHandler subscriptionHandler;
+@property(nonatomic, copy) void(^updateHandler)();
+
+@end
+
+@implementation MockPendingTopicsListDelegate
+
+- (BOOL)pendingTopicsListCanRequestTopicUpdates:(FIRMessagingPendingTopicsList *)list {
+ return self.isReady;
+}
+
+- (void)pendingTopicsList:(FIRMessagingPendingTopicsList *)list
+ requestedUpdateForTopic:(NSString *)topic
+ action:(FIRMessagingTopicAction)action
+ completion:(FIRMessagingTopicOperationCompletion)completion {
+ if (self.subscriptionHandler) {
+ self.subscriptionHandler(topic, action, completion);
+ }
+}
+
+- (void)pendingTopicsListDidUpdate:(FIRMessagingPendingTopicsList *)list {
+ if (self.updateHandler) {
+ self.updateHandler();
+ }
+}
+
+@end
+
+@interface FIRMessagingPendingTopicsListTest : XCTestCase
+
+/// Using this delegate lets us prevent any topic operations from start, making it easy to measure
+/// our batches
+@property(nonatomic, strong) MockPendingTopicsListDelegate *notReadyDelegate;
+/// Using this delegate will always begin topic operations (which will never return by default).
+/// Useful for overriding with block-methods to handle update requests
+@property(nonatomic, strong) MockPendingTopicsListDelegate *alwaysReadyDelegate;
+
+@end
+
+@implementation FIRMessagingPendingTopicsListTest
+
+- (void)setUp {
+ [super setUp];
+ self.notReadyDelegate = [[MockPendingTopicsListDelegate alloc] init];
+ self.notReadyDelegate.isReady = NO;
+
+ self.alwaysReadyDelegate = [[MockPendingTopicsListDelegate alloc] init];
+ self.alwaysReadyDelegate.isReady = YES;
+}
+
+- (void)tearDown {
+ self.notReadyDelegate = nil;
+ self.alwaysReadyDelegate = nil;
+ [super tearDown];
+}
+
+- (void)testAddSingleTopic {
+ FIRMessagingPendingTopicsList *pendingTopics = [[FIRMessagingPendingTopicsList alloc] init];
+ pendingTopics.delegate = self.notReadyDelegate;
+
+ [pendingTopics addOperationForTopic:@"/topics/0"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ XCTAssertEqual(pendingTopics.numberOfBatches, 1);
+}
+
+- (void)testAddSameTopicAndActionMultipleTimes {
+ FIRMessagingPendingTopicsList *pendingTopics = [[FIRMessagingPendingTopicsList alloc] init];
+ pendingTopics.delegate = self.notReadyDelegate;
+
+ [pendingTopics addOperationForTopic:@"/topics/0"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ [pendingTopics addOperationForTopic:@"/topics/0"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ [pendingTopics addOperationForTopic:@"/topics/0"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ XCTAssertEqual(pendingTopics.numberOfBatches, 1);
+}
+
+- (void)testAddMultiplePendingTopicsWithSameAction {
+ FIRMessagingPendingTopicsList *pendingTopics = [[FIRMessagingPendingTopicsList alloc] init];
+ pendingTopics.delegate = self.notReadyDelegate;
+
+ for (NSInteger i = 0; i < 10; i++) {
+ NSString *topic = [NSString stringWithFormat:@"/topics/%ld", i];
+ [pendingTopics addOperationForTopic:topic
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ }
+ XCTAssertEqual(pendingTopics.numberOfBatches, 1);
+}
+
+- (void)testAddTopicsWithDifferentActions {
+ FIRMessagingPendingTopicsList *pendingTopics = [[FIRMessagingPendingTopicsList alloc] init];
+ pendingTopics.delegate = self.notReadyDelegate;
+
+ [pendingTopics addOperationForTopic:@"/topics/0"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ [pendingTopics addOperationForTopic:@"/topics/1"
+ withAction:FIRMessagingTopicActionUnsubscribe
+ completion:nil];
+ [pendingTopics addOperationForTopic:@"/topics/2"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ XCTAssertEqual(pendingTopics.numberOfBatches, 3);
+}
+
+- (void)testBatchSizeReductionAfterSuccessfulTopicUpdate {
+ FIRMessagingPendingTopicsList *pendingTopics = [[FIRMessagingPendingTopicsList alloc] init];
+ pendingTopics.delegate = self.alwaysReadyDelegate;
+
+ XCTestExpectation *batchSizeReductionExpectation =
+ [self expectationWithDescription:@"Batch size was reduced after topic suscription"];
+
+ FIRMessaging_WEAKIFY(self)
+ self.alwaysReadyDelegate.subscriptionHandler =
+ ^(NSString *topic,
+ FIRMessagingTopicAction action,
+ FIRMessagingTopicOperationCompletion completion) {
+ // Simulate that the handler is generally called asynchronously
+ dispatch_async(dispatch_get_main_queue(), ^{
+ FIRMessaging_STRONGIFY(self)
+ if (action == FIRMessagingTopicActionUnsubscribe) {
+ XCTAssertEqual(pendingTopics.numberOfBatches, 1);
+ [batchSizeReductionExpectation fulfill];
+ }
+ completion(FIRMessagingTopicOperationResultSucceeded, nil);
+ });
+ };
+
+ [pendingTopics addOperationForTopic:@"/topics/0"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ [pendingTopics addOperationForTopic:@"/topics/1"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ [pendingTopics addOperationForTopic:@"/topics/2"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ [pendingTopics addOperationForTopic:@"/topics/1"
+ withAction:FIRMessagingTopicActionUnsubscribe
+ completion:nil];
+
+ [self waitForExpectationsWithTimeout:5.0 handler:nil];
+}
+
+- (void)testCompletionOfTopicUpdatesInSameThread {
+ FIRMessagingPendingTopicsList *pendingTopics = [[FIRMessagingPendingTopicsList alloc] init];
+ pendingTopics.delegate = self.alwaysReadyDelegate;
+
+ XCTestExpectation *allOperationsSucceededed =
+ [self expectationWithDescription:@"All queued operations succeeded"];
+
+ self.alwaysReadyDelegate.subscriptionHandler =
+ ^(NSString *topic,
+ FIRMessagingTopicAction action,
+ FIRMessagingTopicOperationCompletion completion) {
+ // Typically, our callbacks happen asynchronously, but to ensure resilience,
+ // call back the operation on the same thread it was called in.
+ completion(FIRMessagingTopicOperationResultSucceeded, nil);
+ };
+
+ self.alwaysReadyDelegate.updateHandler = ^{
+ if (pendingTopics.numberOfBatches == 0) {
+ [allOperationsSucceededed fulfill];
+ }
+ };
+
+ [pendingTopics addOperationForTopic:@"/topics/0"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ [pendingTopics addOperationForTopic:@"/topics/1"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ [pendingTopics addOperationForTopic:@"/topics/2"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+
+ [self waitForExpectationsWithTimeout:5.0 handler:nil];
+}
+
+- (void)testAddingTopicToCurrentBatchWhileCurrentBatchTopicsInFlight {
+
+ FIRMessagingPendingTopicsList *pendingTopics = [[FIRMessagingPendingTopicsList alloc] init];
+ pendingTopics.delegate = self.alwaysReadyDelegate;
+
+ NSString *stragglerTopic = @"/topics/straggler";
+ XCTestExpectation *stragglerTopicWasAddedToInFlightOperations =
+ [self expectationWithDescription:@"The topic was added to in-flight operations"];
+
+ self.alwaysReadyDelegate.subscriptionHandler =
+ ^(NSString *topic,
+ FIRMessagingTopicAction action,
+ FIRMessagingTopicOperationCompletion completion) {
+ if ([topic isEqualToString:stragglerTopic]) {
+ [stragglerTopicWasAddedToInFlightOperations fulfill];
+ }
+ // Add a 0.5 second delay to the completion, to give time to add a straggler before the batch
+ // is completed
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)),
+ dispatch_get_main_queue(),
+ ^{
+ completion(FIRMessagingTopicOperationResultSucceeded, nil);
+ });
+ };
+
+ // This is a normal topic, which should start fairly soon, but take a while to complete
+ [pendingTopics addOperationForTopic:@"/topics/0"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ // While waiting for the first topic to complete, we add another topic after a slight delay
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)),
+ dispatch_get_main_queue(),
+ ^{
+ [pendingTopics addOperationForTopic:stragglerTopic
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ });
+
+ [self waitForExpectationsWithTimeout:5.0 handler:nil];
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingPubSubTest.m b/Example/Messaging/Tests/FIRMessagingPubSubTest.m
new file mode 100644
index 0000000..2981b54
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingPubSubTest.m
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 XCTest;
+
+#import "FIRMessagingPubSub.h"
+
+@interface FIRMessagingPubSubTest : XCTestCase
+@end
+
+@implementation FIRMessagingPubSubTest
+
+static NSString *const kTopicName = @"topic-Name";
+
+#pragma mark - topicMatchForSender tests
+
+/// Tests that an empty topic name is an invalid topic.
+- (void)testTopicMatchForEmptyTopicPrefix {
+ XCTAssertFalse([FIRMessagingPubSub isValidTopicWithPrefix:@""]);
+}
+
+/// Tests that a topic with an invalid prefix is not a valid topic name.
+- (void)testTopicMatchWithInvalidTopicPrefix {
+ XCTAssertFalse([FIRMessagingPubSub isValidTopicWithPrefix:@"/topics+abcdef/"]);
+}
+
+/// Tests that a topic with a valid prefix but invalid name is not a valid topic name.
+- (void)testTopicMatchWithValidTopicPrefixButInvalidName {
+ XCTAssertFalse([FIRMessagingPubSub isValidTopicWithPrefix:@"/topics/aaaaaa/topics/lala"]);
+}
+
+/// Tests that multiple backslashes in topics is an invalid topic name.
+- (void)testTopicMatchForInvalidTopicPrefix_multipleBackslash {
+ XCTAssertFalse([FIRMessagingPubSub isValidTopicWithPrefix:@"/topics//abc"]);
+}
+
+/// Tests a topic name with a valid prefix and name.
+- (void)testTopicMatchForValidTopicSender {
+ NSString *topic = [NSString stringWithFormat:@"/topics/%@", kTopicName];
+ XCTAssertTrue([FIRMessagingPubSub isValidTopicWithPrefix:topic]);
+}
+
+/// Tests topic prefix for topics with no prefix.
+- (void)testTopicHasNoTopicPrefix {
+ XCTAssertFalse([FIRMessagingPubSub hasTopicsPrefix:@""]);
+}
+
+/// Tests topic prefix for valid prefix.
+- (void)testTopicHasValidToicsPrefix {
+ XCTAssertTrue([FIRMessagingPubSub hasTopicsPrefix:@"/topics/"]);
+}
+
+/// Tests topic prefix wih no prefix.
+- (void)testAddTopicPrefix_withNoPrefix {
+ NSString *topic = [FIRMessagingPubSub addPrefixToTopic:@""];
+ XCTAssertTrue([FIRMessagingPubSub hasTopicsPrefix:topic]);
+ XCTAssertFalse([FIRMessagingPubSub isValidTopicWithPrefix:topic]);
+}
+
+/// Tests adding the "/topics/" prefix for topic name which already has a prefix.
+- (void)testAddTopicPrefix_withPrefix {
+ NSString *topic = [NSString stringWithFormat:@"/topics/%@", kTopicName];
+ topic = [FIRMessagingPubSub addPrefixToTopic:topic];
+ XCTAssertTrue([FIRMessagingPubSub hasTopicsPrefix:topic]);
+ XCTAssertTrue([FIRMessagingPubSub isValidTopicWithPrefix:topic]);
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingRegistrarTest.m b/Example/Messaging/Tests/FIRMessagingRegistrarTest.m
new file mode 100644
index 0000000..b32851c
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingRegistrarTest.m
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 UIKit;
+@import XCTest;
+
+#import <OCMock/OCMock.h>
+
+#import "FIRMessagingCheckinService.h"
+#import "FIRMessagingPubSubRegistrar.h"
+#import "FIRMessagingRegistrar.h"
+#import "FIRMessagingUtilities.h"
+#import "NSError+FIRMessaging.h"
+
+static NSString *const kFIRMessagingUserDefaultsSuite =
+ @"FIRMessagingRegistrarTestUserDefaultsSuite";
+
+static NSString *const kDeviceAuthId = @"12345";
+static NSString *const kSecretToken = @"45657809";
+static NSString *const kVersionInfo = @"1.0";
+static NSString *const kTopicToSubscribeTo = @"/topics/xyz/hello-world";
+static NSString *const kFIRMessagingAppIDToken = @"abcdefgh1234lmno";
+static NSString *const kSubscriptionID = @"sample-subscription-id-xyz";
+
+@interface FIRMessagingRegistrar ()
+
+@property(nonatomic, readwrite, strong) FIRMessagingPubSubRegistrar *pubsubRegistrar;
+@property(nonatomic, readwrite, strong) FIRMessagingCheckinService *checkinService;
+
+@end
+
+@interface FIRMessagingRegistrarTest : XCTestCase
+
+@property(nonatomic, readwrite, strong) FIRMessagingRegistrar *registrar;
+@property(nonatomic, readwrite, strong) id mockRegistrar;
+@property(nonatomic, readwrite, strong) id mockCheckin;
+@property(nonatomic, readwrite, strong) id mockPubsubRegistrar;
+
+@end
+
+@implementation FIRMessagingRegistrarTest
+
+- (void)setUp {
+ [super setUp];
+ _registrar = [[FIRMessagingRegistrar alloc] init];
+ _mockRegistrar = OCMPartialMock(_registrar);
+ _mockCheckin = OCMPartialMock(_registrar.checkinService);
+ _registrar.checkinService = _mockCheckin;
+ _registrar.pubsubRegistrar = OCMClassMock([FIRMessagingPubSubRegistrar class]);
+ _mockPubsubRegistrar = _registrar.pubsubRegistrar;
+}
+
+- (void)testUpdateSubscriptionWithValidCheckinData {
+ [self stubCheckinService];
+
+ [self.registrar updateSubscriptionToTopic:kTopicToSubscribeTo
+ withToken:kFIRMessagingAppIDToken
+ options:nil
+ shouldDelete:NO
+ handler:
+ ^(FIRMessagingTopicOperationResult result, NSError *error) {
+ }];
+
+ OCMVerify([self.mockPubsubRegistrar updateSubscriptionToTopic:[OCMArg isEqual:kTopicToSubscribeTo]
+ withToken:[OCMArg isEqual:kFIRMessagingAppIDToken]
+ options:nil
+ shouldDelete:NO
+ handler:OCMOCK_ANY]);
+}
+
+- (void)testUpdateSubscription {
+ [self stubCheckinService];
+
+ __block FIRMessagingTopicOperationCompletion pubsubCompletion;
+ [[[self.mockPubsubRegistrar stub]
+ andDo:^(NSInvocation *invocation) {
+ pubsubCompletion(FIRMessagingTopicOperationResultSucceeded, nil);
+ }]
+ updateSubscriptionToTopic:kTopicToSubscribeTo
+ withToken:kFIRMessagingAppIDToken
+ options:nil
+ shouldDelete:NO
+ handler:[OCMArg checkWithBlock:^BOOL(id obj) {
+ return (pubsubCompletion = obj) != nil;
+ }]];
+
+ [self.registrar updateSubscriptionToTopic:kTopicToSubscribeTo
+ withToken:kFIRMessagingAppIDToken
+ options:nil
+ shouldDelete:NO
+ handler:
+ ^(FIRMessagingTopicOperationResult result, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqual(result, FIRMessagingTopicOperationResultSucceeded);
+ }];
+}
+
+- (void)testFailedUpdateSubscriptionWithNoCheckin {
+ // Mock checkin service to always return NO for hasValidCheckinInfo
+ [[[self.mockCheckin stub] andReturnValue:@NO] hasValidCheckinInfo];
+ // This should not create a network request since we don't have checkin info
+ [self.registrar updateSubscriptionToTopic:kTopicToSubscribeTo
+ withToken:kFIRMessagingAppIDToken
+ options:nil
+ shouldDelete:NO
+ handler:
+ ^(FIRMessagingTopicOperationResult result, NSError *error) {
+ XCTAssertNotNil(error);
+ XCTAssertEqual(result, FIRMessagingTopicOperationResultError);
+ }];
+}
+
+#pragma mark - Private Helpers
+
+- (void)stubCheckinService {
+ [[[self.mockCheckin stub] andReturn:kDeviceAuthId] deviceAuthID];
+ [[[self.mockCheckin stub] andReturn:kSecretToken] secretToken];
+ [[[self.mockCheckin stub] andReturnValue:@YES] hasValidCheckinInfo];
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingRemoteNotificationsProxyTest.m b/Example/Messaging/Tests/FIRMessagingRemoteNotificationsProxyTest.m
new file mode 100644
index 0000000..9138c50
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingRemoteNotificationsProxyTest.m
@@ -0,0 +1,279 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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.
+ */
+
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+@import UserNotifications;
+#endif
+@import XCTest;
+
+#import <OCMock/OCMock.h>
+
+#import "FIRMessaging.h"
+#import "FIRMessagingRemoteNotificationsProxy.h"
+
+#pragma mark - Expose Internal Methods for Testing
+// Expose some internal properties and methods here, in order to test
+@interface FIRMessagingRemoteNotificationsProxy ()
+
+@property(readonly, nonatomic) BOOL didSwizzleMethods;
+@property(readonly, nonatomic) BOOL didSwizzleAppDelegateMethods;
+
+@property(readonly, nonatomic) BOOL hasSwizzledUserNotificationDelegate;
+@property(readonly, nonatomic) BOOL isObservingUserNotificationDelegateChanges;
+
+@property(strong, readonly, nonatomic) id userNotificationCenter;
+@property(strong, readonly, nonatomic) id currentUserNotificationCenterDelegate;
+
++ (instancetype)sharedProxy;
+
+- (BOOL)swizzleAppDelegateMethods:(id<UIApplicationDelegate>)appDelegate;
+- (void)listenForDelegateChangesInUserNotificationCenter:(id)notificationCenter;
+- (void)swizzleUserNotificationCenterDelegate:(id)delegate;
+- (void)unswizzleUserNotificationCenterDelegate:(id)delegate;
+
+void FCM_swizzle_appDidReceiveRemoteNotification(id self,
+ SEL _cmd,
+ UIApplication *app,
+ NSDictionary *userInfo);
+void FCM_swizzle_appDidReceiveRemoteNotificationWithHandler(
+ id self, SEL _cmd, UIApplication *app, NSDictionary *userInfo,
+ void (^handler)(UIBackgroundFetchResult));
+void FCM_swizzle_willPresentNotificationWithHandler(
+ id self, SEL _cmd, id center, id notification, void (^handler)(NSUInteger));
+
+@end
+
+#pragma mark - Incomplete App Delegate
+@interface IncompleteAppDelegate : NSObject <UIApplicationDelegate>
+@end
+@implementation IncompleteAppDelegate
+@end
+
+#pragma mark - Fake AppDelegate
+@interface FakeAppDelegate : NSObject <UIApplicationDelegate>
+@property(nonatomic) BOOL remoteNotificationMethodWasCalled;
+@property(nonatomic) BOOL remoteNotificationWithFetchHandlerWasCalled;
+@end
+@implementation FakeAppDelegate
+- (void)application:(UIApplication *)application
+ didReceiveRemoteNotification:(NSDictionary *)userInfo {
+ self.remoteNotificationMethodWasCalled = YES;
+}
+- (void)application:(UIApplication *)application
+ didReceiveRemoteNotification:(NSDictionary *)userInfo
+ fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
+ self.remoteNotificationWithFetchHandlerWasCalled = YES;
+}
+@end
+
+#pragma mark - Incompete UNUserNotificationCenterDelegate
+@interface IncompleteUserNotificationCenterDelegate : NSObject <UNUserNotificationCenterDelegate>
+@end
+@implementation IncompleteUserNotificationCenterDelegate
+@end
+
+#pragma mark - Fake UNUserNotificationCenterDelegate
+
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+@interface FakeUserNotificationCenterDelegate : NSObject <UNUserNotificationCenterDelegate>
+@property(nonatomic) BOOL willPresentWasCalled;
+@end
+@implementation FakeUserNotificationCenterDelegate
+- (void)userNotificationCenter:(UNUserNotificationCenter *)center
+ willPresentNotification:(UNNotification *)notification
+ withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))
+ completionHandler {
+ self.willPresentWasCalled = YES;
+}
+@end
+#endif // __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+
+#pragma mark - Local, Per-Test Properties
+
+@interface FIRMessagingRemoteNotificationsProxyTest : XCTestCase
+
+@property(nonatomic, strong) FIRMessagingRemoteNotificationsProxy *proxy;
+@property(nonatomic, strong) id mockProxy;
+@property(nonatomic, strong) id mockProxyClass;
+@property(nonatomic, strong) id mockMessagingClass;
+
+@end
+
+@implementation FIRMessagingRemoteNotificationsProxyTest
+
+- (void)setUp {
+ [super setUp];
+ _proxy = [[FIRMessagingRemoteNotificationsProxy alloc] init];
+ _mockProxy = OCMPartialMock(_proxy);
+ _mockProxyClass = OCMClassMock([FIRMessagingRemoteNotificationsProxy class]);
+ // Update +sharedProxy to always return our partial mock of FIRMessagingRemoteNotificationsProxy
+ OCMStub([_mockProxyClass sharedProxy]).andReturn(_mockProxy);
+ // Many of our swizzled methods call [FIRMessaging messaging], but we don't need it,
+ // so just stub it to return nil
+ _mockMessagingClass = OCMClassMock([FIRMessaging class]);
+ OCMStub([_mockMessagingClass messaging]).andReturn(nil);
+}
+
+- (void)tearDown {
+ [_mockMessagingClass stopMocking];
+ _mockMessagingClass = nil;
+
+ [_mockProxyClass stopMocking];
+ _mockProxyClass = nil;
+
+ [_mockProxy stopMocking];
+ _mockProxy = nil;
+
+ _proxy = nil;
+ [super tearDown];
+}
+
+#pragma mark - Method Swizzling Tests
+
+- (void)testSwizzlingNonAppDelegate {
+ id randomObject = @"Random Object that is not an App Delegate";
+ [self.proxy swizzleAppDelegateMethods:randomObject];
+ XCTAssertFalse(self.proxy.didSwizzleAppDelegateMethods);
+}
+
+- (void)testSwizzlingAppDelegate {
+ IncompleteAppDelegate *incompleteAppDelegate = [[IncompleteAppDelegate alloc] init];
+ [self.proxy swizzleAppDelegateMethods:incompleteAppDelegate];
+ XCTAssertTrue(self.proxy.didSwizzleAppDelegateMethods);
+}
+
+- (void)testSwizzledIncompleteAppDelegateRemoteNotificationMethod {
+ IncompleteAppDelegate *incompleteAppDelegate = [[IncompleteAppDelegate alloc] init];
+ [self.mockProxy swizzleAppDelegateMethods:incompleteAppDelegate];
+ SEL selector = @selector(application:didReceiveRemoteNotification:);
+ XCTAssertTrue([incompleteAppDelegate respondsToSelector:selector]);
+ [incompleteAppDelegate application:OCMClassMock([UIApplication class])
+ didReceiveRemoteNotification:@{}];
+ // Verify our swizzled method was called
+ OCMVerify(FCM_swizzle_appDidReceiveRemoteNotification);
+}
+
+// If the remote notification with fetch handler is NOT implemented, we will force-implement
+// the backup -application:didReceiveRemoteNotification: method
+- (void)testIncompleteAppDelegateRemoteNotificationWithFetchHandlerMethod {
+ IncompleteAppDelegate *incompleteAppDelegate = [[IncompleteAppDelegate alloc] init];
+ [self.mockProxy swizzleAppDelegateMethods:incompleteAppDelegate];
+ SEL remoteNotificationWithFetchHandler =
+ @selector(application:didReceiveRemoteNotification:fetchCompletionHandler:);
+ XCTAssertFalse([incompleteAppDelegate respondsToSelector:remoteNotificationWithFetchHandler]);
+
+ SEL remoteNotification = @selector(application:didReceiveRemoteNotification:);
+ XCTAssertTrue([incompleteAppDelegate respondsToSelector:remoteNotification]);
+}
+
+- (void)testSwizzledAppDelegateRemoteNotificationMethods {
+ FakeAppDelegate *appDelegate = [[FakeAppDelegate alloc] init];
+ [self.mockProxy swizzleAppDelegateMethods:appDelegate];
+ [appDelegate application:OCMClassMock([UIApplication class]) didReceiveRemoteNotification:@{}];
+ // Verify our swizzled method was called
+ OCMVerify(FCM_swizzle_appDidReceiveRemoteNotification);
+ // Verify our original method was called
+ XCTAssertTrue(appDelegate.remoteNotificationMethodWasCalled);
+
+ // Now call the remote notification with handler method
+ [appDelegate application:OCMClassMock([UIApplication class])
+ didReceiveRemoteNotification:@{}
+ fetchCompletionHandler:^(UIBackgroundFetchResult result) {}];
+ // Verify our swizzled method was called
+ OCMVerify(FCM_swizzle_appDidReceiveRemoteNotificationWithHandler);
+ // Verify our original method was called
+ XCTAssertTrue(appDelegate.remoteNotificationWithFetchHandlerWasCalled);
+}
+
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+
+- (void)testListeningForDelegateChangesOnInvalidUserNotificationCenter {
+ id randomObject = @"Random Object that is not a User Notification Center";
+ [self.proxy listenForDelegateChangesInUserNotificationCenter:randomObject];
+ XCTAssertFalse(self.proxy.isObservingUserNotificationDelegateChanges);
+}
+
+- (void)testSwizzlingInvalidUserNotificationCenterDelegate {
+ id randomObject = @"Random Object that is not a User Notification Center Delegate";
+ [self.proxy swizzleUserNotificationCenterDelegate:randomObject];
+ XCTAssertFalse(self.proxy.hasSwizzledUserNotificationDelegate);
+}
+
+- (void)testSwizzlingUserNotificationsCenterDelegate {
+ FakeUserNotificationCenterDelegate *delegate = [[FakeUserNotificationCenterDelegate alloc] init];
+ [self.proxy swizzleUserNotificationCenterDelegate:delegate];
+ XCTAssertTrue(self.proxy.hasSwizzledUserNotificationDelegate);
+}
+
+// Use a fake delegate that doesn't actually implement the needed delegate method.
+// Our swizzled method should still be called.
+
+- (void)testIncompleteUserNotificationCenterDelegateMethod {
+ // Early exit if running on pre iOS 10
+ if (![UNNotification class]) {
+ return;
+ }
+ IncompleteUserNotificationCenterDelegate *delegate =
+ [[IncompleteUserNotificationCenterDelegate alloc] init];
+ [self.mockProxy swizzleUserNotificationCenterDelegate:delegate];
+ SEL selector = @selector(userNotificationCenter:willPresentNotification:withCompletionHandler:);
+ XCTAssertTrue([delegate respondsToSelector:selector]);
+ // Invoking delegate method should also invoke our swizzled method
+ // The swizzled method uses the +sharedProxy, which should be
+ // returning our mocked proxy.
+ // Use non-nil, proper classes, otherwise our SDK bails out.
+ [delegate userNotificationCenter:OCMClassMock([UNUserNotificationCenter class])
+ willPresentNotification:[self generateMockNotification]
+ withCompletionHandler:^(NSUInteger options) {}];
+ // Verify our swizzled method was called
+ OCMVerify(FCM_swizzle_willPresentNotificationWithHandler);
+}
+
+// Use an object that does actually implement the needed method. Both should be called.
+- (void)testSwizzledUserNotificationsCenterDelegate {
+ // Early exit if running on pre iOS 10
+ if (![UNNotification class]) {
+ return;
+ }
+ FakeUserNotificationCenterDelegate *delegate = [[FakeUserNotificationCenterDelegate alloc] init];
+ [self.mockProxy swizzleUserNotificationCenterDelegate:delegate];
+ // Invoking delegate method should also invoke our swizzled method
+ // The swizzled method uses the +sharedProxy, which should be
+ // returning our mocked proxy.
+ // Use non-nil, proper classes, otherwise our SDK bails out.
+ [delegate userNotificationCenter:OCMClassMock([UNUserNotificationCenter class])
+ willPresentNotification:[self generateMockNotification]
+ withCompletionHandler:^(NSUInteger options) {}];
+ // Verify our swizzled method was called
+ OCMVerify(FCM_swizzle_willPresentNotificationWithHandler);
+ // Verify our original method was called
+ XCTAssertTrue(delegate.willPresentWasCalled);
+}
+
+- (id)generateMockNotification {
+ // Stub out: notification.request.content.userInfo
+ id mockNotification = OCMClassMock([UNNotification class]);
+ id mockRequest = OCMClassMock([UNNotificationRequest class]);
+ id mockContent = OCMClassMock([UNNotificationContent class]);
+ OCMStub([mockContent userInfo]).andReturn(@{});
+ OCMStub([mockRequest content]).andReturn(mockContent);
+ OCMStub([mockNotification request]).andReturn(mockRequest);
+ return mockNotification;
+}
+
+#endif // __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingRmqManagerTest.m b/Example/Messaging/Tests/FIRMessagingRmqManagerTest.m
new file mode 100644
index 0000000..88d7c4e
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingRmqManagerTest.m
@@ -0,0 +1,332 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 <XCTest/XCTest.h>
+
+#import "Protos/GtalkCore.pbobjc.h"
+
+#import "FIRMessagingPersistentSyncMessage.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessagingUtilities.h"
+
+static NSString *const kRmqDatabaseName = @"rmq-test-db";
+static NSString *const kRmqDataMessageCategory = @"com.google.gcm-rmq-test";
+
+@interface FIRMessagingRmqManagerTest : XCTestCase
+
+@property(nonatomic, readwrite, strong) FIRMessagingRmqManager *rmqManager;
+
+@end
+
+@implementation FIRMessagingRmqManagerTest
+
+- (void)setUp {
+ [super setUp];
+ // Make sure we start off with a clean state each time
+ [FIRMessagingRmqManager removeDatabaseWithName:kRmqDatabaseName];
+ _rmqManager = [[FIRMessagingRmqManager alloc] initWithDatabaseName:kRmqDatabaseName];
+}
+
+- (void)tearDown {
+ [super tearDown];
+ [FIRMessagingRmqManager removeDatabaseWithName:kRmqDatabaseName];
+}
+
+/**
+ * Add s2d messages with different RMQ-ID's to the RMQ. Fetch the messages
+ * and verify that all messages were successfully saved.
+ */
+- (void)testSavingS2dMessages {
+ NSArray *messageIDs = @[ @"message1", @"message2", @"123456" ];
+ for (NSString *messageID in messageIDs) {
+ [self.rmqManager saveS2dMessageWithRmqId:messageID];
+ }
+ NSArray *rmqMessages = [self.rmqManager unackedS2dRmqIds];
+ XCTAssertEqual(messageIDs.count, rmqMessages.count);
+ for (NSString *messageID in rmqMessages) {
+ XCTAssertTrue([messageIDs containsObject:messageID]);
+ }
+}
+
+/**
+ * Add s2d messages with different RMQ-ID's to the RMQ. Delete some of the
+ * messages stored, assuming we received a server ACK for them. The remaining
+ * messages should be fetched successfully.
+ */
+- (void)testDeletingS2dMessages {
+ NSArray *addMessages = @[ @"message1", @"message2", @"message3", @"message4"];
+ for (NSString *messageID in addMessages) {
+ [self.rmqManager saveS2dMessageWithRmqId:messageID];
+ }
+ NSArray *removeMessages = @[ addMessages[1], addMessages[3] ];
+ [self.rmqManager removeS2dIds:removeMessages];
+ NSArray *remainingMessages = [self.rmqManager unackedS2dRmqIds];
+ XCTAssertEqual(2, remainingMessages.count);
+ XCTAssertTrue([remainingMessages containsObject:addMessages[0]]);
+ XCTAssertTrue([remainingMessages containsObject:addMessages[2]]);
+}
+
+/**
+ * Test deleting a s2d message that is not in the persistent store. This shouldn't
+ * crash or alter the valid contents of the RMQ store.
+ */
+- (void)testDeletingInvalidS2dMessage {
+ NSString *validMessageID = @"validMessage123";
+ [self.rmqManager saveS2dMessageWithRmqId:validMessageID];
+ NSString *invalidMessageID = @"invalidMessage123";
+ [self.rmqManager removeS2dIds:@[invalidMessageID]];
+ NSArray *remainingMessages = [self.rmqManager unackedS2dRmqIds];
+ XCTAssertEqual(1, remainingMessages.count);
+ XCTAssertEqualObjects(validMessageID, remainingMessages[0]);
+}
+
+/**
+ * Test loading the RMQ-ID for d2s messages when there are no outgoing messages in the RMQ.
+ */
+- (void)testLoadRmqIDWithNoD2sMessages {
+ [self.rmqManager loadRmqId];
+ XCTAssertEqual(-1, [self maxRmqIDInRmqStoreForD2SMessages]);
+}
+
+/**
+ * Test that outgoing RMQ messages are correctly saved
+ */
+- (void)testOutgoingRmqWithValidMessages {
+ NSString *from = @"rmq-test";
+ [self.rmqManager loadRmqId];
+ GtalkDataMessageStanza *message1 = [self dataMessageWithMessageID:@"message1"
+ from:from
+ data:nil];
+ NSError *error;
+
+ // should successfully save the message to RMQ
+ XCTAssertTrue([self.rmqManager saveRmqMessage:message1 error:&error]);
+ XCTAssertNil(error);
+
+ GtalkDataMessageStanza *message2 = [self dataMessageWithMessageID:@"message2"
+ from:from
+ data:nil];
+
+ // should successfully save the second message to RMQ
+ XCTAssertTrue([self.rmqManager saveRmqMessage:message2 error:&error]);
+ XCTAssertNil(error);
+
+ // message1 should have RMQ-ID = 2, message2 = 3
+ XCTAssertEqual(3, [self maxRmqIDInRmqStoreForD2SMessages]);
+ [self.rmqManager scanWithRmqMessageHandler:nil
+ dataMessageHandler:^(int64_t rmqId, GtalkDataMessageStanza *stanza) {
+ if (rmqId == 2) {
+ XCTAssertEqualObjects(@"message1", stanza.id_p);
+ } else if (rmqId == 3) {
+ XCTAssertEqualObjects(@"message2", stanza.id_p);
+ } else {
+ XCTFail(@"Invalid RmqID %lld for s2d message", rmqId);
+ }
+ }];
+}
+
+/**
+ * Test that an outgoing message with different properties is correctly saved to the RMQ.
+ */
+- (void)testOutgoingDataMessageIsCorrectlySaved {
+ NSString *from = @"rmq-test";
+ NSString *messageID = @"message123";
+ NSString *to = @"to-senderID-123";
+ int32_t ttl = 2400;
+ NSString *registrationToken = @"registration-token";
+ NSDictionary *data = @{
+ @"hello" : @"world",
+ @"count" : @"2",
+ };
+
+ [self.rmqManager loadRmqId];
+ GtalkDataMessageStanza *message = [self dataMessageWithMessageID:messageID
+ from:from
+ data:data];
+ [message setTo:to];
+ [message setTtl:ttl];
+ [message setRegId:registrationToken];
+ NSError *error;
+
+ // should successfully save the message to RMQ
+ XCTAssertTrue([self.rmqManager saveRmqMessage:message error:&error]);
+ XCTAssertNil(error);
+
+ [self.rmqManager scanWithRmqMessageHandler:nil
+ dataMessageHandler:^(int64_t rmqId, GtalkDataMessageStanza *stanza) {
+ XCTAssertEqualObjects(from, stanza.from);
+ XCTAssertEqualObjects(messageID, stanza.id_p);
+ XCTAssertEqualObjects(to, stanza.to);
+ XCTAssertEqualObjects(registrationToken, stanza.regId);
+ XCTAssertEqual(ttl, stanza.ttl);
+ NSMutableDictionary *d = [NSMutableDictionary dictionary];
+ for (GtalkAppData *appData in stanza.appDataArray) {
+ d[appData.key] = appData.value;
+ }
+ XCTAssertTrue([data isEqualToDictionary:d]);
+ }];
+}
+
+/**
+ * Test D2S messages being deleted from RMQ.
+ */
+- (void)testDeletingD2SMessagesFromRMQ {
+ NSString *message1 = @"message123";
+ NSString *ackedMessage = @"message234";
+ NSString *from = @"from-rmq-test";
+ GtalkDataMessageStanza *stanza1 = [self dataMessageWithMessageID:message1 from:from data:nil];
+ GtalkDataMessageStanza *stanza2 = [self dataMessageWithMessageID:ackedMessage
+ from:from
+ data:nil];
+ NSError *error;
+ XCTAssertTrue([self.rmqManager saveRmqMessage:stanza1 error:&error]);
+ XCTAssertNil(error);
+ XCTAssertTrue([self.rmqManager saveRmqMessage:stanza2 error:&error]);
+ XCTAssertNil(error);
+
+ __block int64_t ackedMessageRmqID = -1;
+ [self.rmqManager scanWithRmqMessageHandler:nil
+ dataMessageHandler:^(int64_t rmqId, GtalkDataMessageStanza *stanza) {
+ if ([stanza.id_p isEqualToString:ackedMessage]) {
+ ackedMessageRmqID = rmqId;
+ }
+ }];
+ // should be a valid RMQ ID
+ XCTAssertTrue(ackedMessageRmqID > 0);
+
+ // delete the acked message
+ NSString *rmqIDString = [NSString stringWithFormat:@"%lld", ackedMessageRmqID];
+ XCTAssertEqual(1, [self.rmqManager removeRmqMessagesWithRmqId:rmqIDString]);
+
+ // should only have one message in the d2s RMQ
+ [self.rmqManager scanWithRmqMessageHandler:nil
+ dataMessageHandler:^(int64_t rmqId, GtalkDataMessageStanza *stanza) {
+ // the acked message was queued later so should have
+ // rmqID = ackedMessageRMQID - 1
+ XCTAssertEqual(ackedMessageRmqID - 1, rmqId);
+ XCTAssertEqual(message1, stanza2.id_p);
+ }];
+}
+
+/**
+ * Test saving a sync message to SYNC_RMQ.
+ */
+- (void)testSavingSyncMessage {
+ NSString *rmqID = @"fake-rmq-id-1";
+ int64_t expirationTime = FIRMessagingCurrentTimestampInSeconds() + 1;
+ XCTAssertTrue([self.rmqManager saveSyncMessageWithRmqID:rmqID
+ expirationTime:expirationTime
+ apnsReceived:YES
+ mcsReceived:NO
+ error:nil]);
+
+ FIRMessagingPersistentSyncMessage *persistentMessage = [self.rmqManager querySyncMessageWithRmqID:rmqID];
+ XCTAssertEqual(persistentMessage.expirationTime, expirationTime);
+ XCTAssertTrue(persistentMessage.apnsReceived);
+ XCTAssertFalse(persistentMessage.mcsReceived);
+}
+
+/**
+ * Test updating a sync message initially received via MCS, now being received via APNS.
+ */
+- (void)testUpdateMessageReceivedViaAPNS {
+ NSString *rmqID = @"fake-rmq-id-1";
+ int64_t expirationTime = FIRMessagingCurrentTimestampInSeconds() + 1;
+ XCTAssertTrue([self.rmqManager saveSyncMessageWithRmqID:rmqID
+ expirationTime:expirationTime
+ apnsReceived:NO
+ mcsReceived:YES
+ error:nil]);
+
+ // Message was now received via APNS
+ XCTAssertTrue([self.rmqManager updateSyncMessageViaAPNSWithRmqID:rmqID error:nil]);
+
+ FIRMessagingPersistentSyncMessage *persistentMessage = [self.rmqManager querySyncMessageWithRmqID:rmqID];
+ XCTAssertTrue(persistentMessage.apnsReceived);
+ XCTAssertTrue(persistentMessage.mcsReceived);
+}
+
+/**
+ * Test updating a sync message initially received via APNS, now being received via MCS.
+ */
+- (void)testUpdateMessageReceivedViaMCS {
+ NSString *rmqID = @"fake-rmq-id-1";
+ int64_t expirationTime = FIRMessagingCurrentTimestampInSeconds() + 1;
+ XCTAssertTrue([self.rmqManager saveSyncMessageWithRmqID:rmqID
+ expirationTime:expirationTime
+ apnsReceived:YES
+ mcsReceived:NO
+ error:nil]);
+
+ // Message was now received via APNS
+ XCTAssertTrue([self.rmqManager updateSyncMessageViaMCSWithRmqID:rmqID error:nil]);
+
+ FIRMessagingPersistentSyncMessage *persistentMessage = [self.rmqManager querySyncMessageWithRmqID:rmqID];
+ XCTAssertTrue(persistentMessage.apnsReceived);
+ XCTAssertTrue(persistentMessage.mcsReceived);
+}
+
+/**
+ * Test deleting sync messages from SYNC_RMQ.
+ */
+- (void)testDeleteSyncMessage {
+ NSString *rmqID = @"fake-rmq-id-1";
+ int64_t expirationTime = FIRMessagingCurrentTimestampInSeconds() + 1;
+ XCTAssertTrue([self.rmqManager saveSyncMessageWithRmqID:rmqID
+ expirationTime:expirationTime
+ apnsReceived:YES
+ mcsReceived:NO
+ error:nil]);
+ XCTAssertNotNil([self.rmqManager querySyncMessageWithRmqID:rmqID]);
+
+ // should successfully delete the message
+ XCTAssertTrue([self.rmqManager deleteSyncMessageWithRmqID:rmqID]);
+ XCTAssertNil([self.rmqManager querySyncMessageWithRmqID:rmqID]);
+}
+
+#pragma mark - Private Helpers
+
+- (GtalkDataMessageStanza *)dataMessageWithMessageID:(NSString *)messageID
+ from:(NSString *)from
+ data:(NSDictionary *)data {
+ GtalkDataMessageStanza *stanza = [[GtalkDataMessageStanza alloc] init];
+ [stanza setId_p:messageID];
+ [stanza setFrom:from];
+ [stanza setCategory:kRmqDataMessageCategory];
+
+ for (NSString *key in data) {
+ NSString *val = data[key];
+ GtalkAppData *appData = [[GtalkAppData alloc] init];
+ [appData setKey:key];
+ [appData setValue:val];
+ [[stanza appDataArray] addObject:appData];
+ }
+
+ return stanza;
+}
+
+- (int64_t)maxRmqIDInRmqStoreForD2SMessages {
+ __block int64_t maxRmqID = -1;
+ [self.rmqManager scanWithRmqMessageHandler:^(int64_t rmqId, int8_t tag, NSData *data) {
+ if (rmqId > maxRmqID) {
+ maxRmqID = rmqId;
+ }
+ }
+ dataMessageHandler:nil];
+ return maxRmqID;
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingSecureSocketTest.m b/Example/Messaging/Tests/FIRMessagingSecureSocketTest.m
new file mode 100644
index 0000000..9f6186b
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingSecureSocketTest.m
@@ -0,0 +1,323 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 XCTest;
+
+#import <OCMock/OCMock.h>
+
+#import "Protos/GtalkCore.pbobjc.h"
+
+#import "FIRMessagingConnection.h"
+#import "FIRMessagingFakeSocket.h"
+#import "FIRMessagingSecureSocket.h"
+#import "FIRMessagingUtilities.h"
+
+@interface FIRMessagingConnection ()
+
++ (GtalkLoginRequest *)loginRequestWithToken:(NSString *)token authID:(NSString *)authID;
+
+@end
+
+@interface FIRMessagingSecureSocket() <NSStreamDelegate>
+
+@property(nonatomic, readwrite, assign) FIRMessagingSecureSocketState state;
+@property(nonatomic, readwrite, strong) NSInputStream *inStream;
+@property(nonatomic, readwrite, strong) NSOutputStream *outStream;
+
+@property(nonatomic, readwrite, assign) BOOL isVersionSent;
+@property(nonatomic, readwrite, assign) BOOL isVersionReceived;
+@property(nonatomic, readwrite, assign) BOOL isInStreamOpen;
+@property(nonatomic, readwrite, assign) BOOL isOutStreamOpen;
+
+@property(nonatomic, readwrite, strong) NSRunLoop *runLoop;
+
+- (BOOL)performRead;
+
+@end
+
+typedef void(^FIRMessagingTestSocketDisconnectHandler)(void);
+typedef void(^FIRMessagingTestSocketConnectHandler)(void);
+
+@interface FIRMessagingSecureSocketTest : XCTestCase <FIRMessagingSecureSocketDelegate>
+
+@property(nonatomic, readwrite, strong) FIRMessagingFakeSocket *socket;
+@property(nonatomic, readwrite, strong) id mockSocket;
+@property(nonatomic, readwrite, strong) NSError *protoParseError;
+@property(nonatomic, readwrite, strong) GPBMessage *protoReceived;
+@property(nonatomic, readwrite, assign) int8_t protoTagReceived;
+
+@property(nonatomic, readwrite, copy) FIRMessagingTestSocketDisconnectHandler disconnectHandler;
+@property(nonatomic, readwrite, copy) FIRMessagingTestSocketConnectHandler connectHandler;
+
+@end
+
+static BOOL isSafeToDisconnectSocket = NO;
+
+@implementation FIRMessagingSecureSocketTest
+
+- (void)setUp {
+ [super setUp];
+ isSafeToDisconnectSocket = NO;
+ self.protoParseError = nil;
+ self.protoReceived = nil;
+ self.protoTagReceived = 0;
+}
+
+- (void)tearDown {
+ self.disconnectHandler = nil;
+ self.connectHandler = nil;
+ isSafeToDisconnectSocket = YES;
+ [self.socket disconnect];
+ [super tearDown];
+}
+
+#pragma mark - Test Reading
+
+- (void)testSendingVersion {
+ // read as soon as 1 byte is written
+ [self createAndConnectSocketWithBufferSize:1];
+ uint8_t versionByte = 40;
+ [self.socket.outStream write:&versionByte maxLength:1];
+
+ [[[self.mockSocket stub] andDo:^(NSInvocation *invocation) {
+ XCTAssertTrue(isSafeToDisconnectSocket,
+ @"Should not disconnect socket now");
+ }] disconnect];
+ XCTestExpectation *shouldAcceptVersionExpectation =
+ [self expectationWithDescription:@"Socket should accept version"];
+ dispatch_time_t delay =
+ dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
+ dispatch_after(delay, dispatch_get_main_queue(), ^{
+ XCTAssertTrue(self.socket.isVersionReceived);
+ [shouldAcceptVersionExpectation fulfill];
+ });
+
+ [self waitForExpectationsWithTimeout:3.0
+ handler:^(NSError *error) {
+ XCTAssertNil(error);
+ }];
+}
+
+- (void)testReceivingDataMessage {
+ [self createAndConnectSocketWithBufferSize:61];
+ [self writeVersionToOutStream];
+ GtalkDataMessageStanza *message = [[GtalkDataMessageStanza alloc] init];
+ [message setCategory:@"socket-test-category"];
+ [message setFrom:@"socket-test-from"];
+ FIRMessagingSetLastStreamId(message, 2);
+ FIRMessagingSetRmq2Id(message, @"socket-test-rmq");
+
+ XCTestExpectation *dataExpectation = [self
+ expectationWithDescription:@"FIRMessaging socket should receive data message"];
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)),
+ dispatch_get_main_queue(), ^{
+ [self.socket sendData:[message data]
+ withTag:kFIRMessagingProtoTagDataMessageStanza
+ rmqId:FIRMessagingGetRmq2Id(message)];
+ });
+
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)),
+ dispatch_get_main_queue(), ^{
+ XCTAssertEqual(self.protoTagReceived, kFIRMessagingProtoTagDataMessageStanza);
+ [dataExpectation fulfill];
+ });
+
+ [self waitForExpectationsWithTimeout:5.0
+ handler:^(NSError *error) {
+ XCTAssertNil(error);
+ }];
+}
+
+#pragma mark - Writing
+
+- (void)testLoginRequest {
+ [self createAndConnectSocketWithBufferSize:99];
+
+ XCTestExpectation *loginExpectation =
+ [self expectationWithDescription:@"Socket send valid login proto"];
+ [self writeVersionToOutStream];
+ GtalkLoginRequest *loginRequest =
+ [FIRMessagingConnection loginRequestWithToken:@"gcmtoken" authID:@"gcmauthid"];
+ FIRMessagingSetLastStreamId(loginRequest, 1);
+
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)),
+ dispatch_get_main_queue(), ^{
+ [self.socket sendData:[loginRequest data]
+ withTag:FIRMessagingGetTagForProto(loginRequest)
+ rmqId:FIRMessagingGetRmq2Id(loginRequest)];
+ });
+
+ dispatch_time_t delay =
+ dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC));
+ dispatch_after(delay, dispatch_get_main_queue(), ^{
+ XCTAssertTrue(self.socket.isVersionReceived);
+ XCTAssertEqual(self.protoTagReceived, kFIRMessagingProtoTagLoginRequest);
+ [loginExpectation fulfill];
+ });
+
+ [self waitForExpectationsWithTimeout:6.0
+ handler:^(NSError *error) {
+ XCTAssertNil(error);
+ }];
+}
+
+- (void)testSendingImproperData {
+ [self createAndConnectSocketWithBufferSize:124];
+ [self writeVersionToOutStream];
+
+ NSString *randomString = @"some random data string";
+ NSData *randomData = [randomString dataUsingEncoding:NSUTF8StringEncoding];
+
+ XCTestExpectation *parseErrorExpectation =
+ [self expectationWithDescription:@"Sending improper data results in a parse error"];
+
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)),
+ dispatch_get_main_queue(), ^{
+ [self.socket sendData:randomData withTag:3 rmqId:@"some-random-rmq-id"];
+ });
+
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.5 * NSEC_PER_SEC)),
+ dispatch_get_main_queue(), ^{
+ if (self.protoParseError != nil) {
+ [parseErrorExpectation fulfill];
+ }
+ });
+
+ [self waitForExpectationsWithTimeout:3.0 handler:nil];
+}
+
+- (void)testSendingDataWithImproperTag {
+ [self createAndConnectSocketWithBufferSize:124];
+ [self writeVersionToOutStream];
+ const char dataString[] = { 0x02, 0x02, 0x11, 0x11, 0x11, 0x11 }; // tag 10, random data
+ NSData *randomData = [NSData dataWithBytes:dataString length:6];
+
+ // Create an expectation for a method which should not be invoked during this test.
+ // This is required to allow us to wait for the socket stream to be read and
+ // processed by FIRMessagingSecureSocket
+ OCMExpect([self.mockSocket disconnect]);
+
+ NSTimeInterval sendDelay = 2.0;
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(sendDelay * NSEC_PER_SEC)),
+ dispatch_get_main_queue(), ^{
+ [self.socket sendData:randomData withTag:10 rmqId:@"some-random-rmq-id"];
+ });
+
+ @try {
+ // While waiting to verify this call, an exception should be thrown
+ // trying to parse the random data in our delegate.
+ // Wait slightly longer than the sendDelay, to allow for the parsing
+ OCMVerifyAllWithDelay(self.mockSocket, sendDelay+0.25);
+ XCTFail(@"Invalid data being read should have thrown an exception.");
+ }
+ @catch (NSException *exception) {
+ XCTAssertNotNil(exception);
+ }
+ @finally { }
+}
+
+- (void)testDisconnect {
+ [self createAndConnectSocketWithBufferSize:1];
+ [self writeVersionToOutStream];
+ // version read and written let's disconnect
+ XCTestExpectation *disconnectExpectation =
+ [self expectationWithDescription:@"socket should disconnect properly"];
+ self.disconnectHandler = ^{
+ [disconnectExpectation fulfill];
+ };
+
+ [self.socket disconnect];
+
+ [self waitForExpectationsWithTimeout:5.0
+ handler:^(NSError *error) {
+ XCTAssertNil(error);
+ }];
+
+ XCTAssertNil(self.socket.inStream);
+ XCTAssertNil(self.socket.outStream);
+ XCTAssertEqual(self.socket.state, kFIRMessagingSecureSocketClosed);
+}
+
+- (void)testSocketOpening {
+ XCTestExpectation *openSocketExpectation =
+ [self expectationWithDescription:@"Socket should open properly"];
+ self.connectHandler = ^{
+ [openSocketExpectation fulfill];
+ };
+ [self createAndConnectSocketWithBufferSize:1];
+ [self writeVersionToOutStream];
+
+ [self waitForExpectationsWithTimeout:10.0
+ handler:^(NSError *error) {
+ XCTAssertNil(error);
+ }];
+
+ XCTAssertTrue(self.socket.isInStreamOpen);
+ XCTAssertTrue(self.socket.isOutStreamOpen);
+}
+
+#pragma mark - FIRMessagingSecureSocketDelegate protocol
+
+- (void)secureSocket:(FIRMessagingSecureSocket *)socket
+ didReceiveData:(NSData *)data
+ withTag:(int8_t)tag {
+ NSError *error;
+ GPBMessage *proto =
+ [FIRMessagingGetClassForTag((FIRMessagingProtoTag)tag) parseFromData:data
+ error:&error];
+ self.protoParseError = error;
+ self.protoReceived = proto;
+ self.protoTagReceived = tag;
+}
+
+- (void)secureSocket:(FIRMessagingSecureSocket *)socket
+ didSendProtoWithTag:(int8_t)tag
+ rmqId:(NSString *)rmqId {
+ // do nothing
+}
+
+- (void)secureSocketDidConnect:(FIRMessagingSecureSocket *)socket {
+ if (self.connectHandler) {
+ self.connectHandler();
+ }
+}
+
+- (void)didDisconnectWithSecureSocket:(FIRMessagingSecureSocket *)socket {
+ if (self.disconnectHandler) {
+ self.disconnectHandler();
+ }
+}
+
+#pragma mark - Private Helpers
+
+- (void)createAndConnectSocketWithBufferSize:(uint8_t)bufferSize {
+ self.socket = [[FIRMessagingFakeSocket alloc] initWithBufferSize:bufferSize];
+ self.mockSocket = OCMPartialMock(self.socket);
+ self.socket.delegate = self;
+
+ [self.socket connectToHost:@"localhost"
+ port:6234
+ onRunLoop:[NSRunLoop mainRunLoop]];
+}
+
+- (void)writeVersionToOutStream {
+ uint8_t versionByte = 40;
+ [self.socket.outStream write:&versionByte maxLength:1];
+ // don't resend the version
+ self.socket.isVersionSent = YES;
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingServiceTest.m b/Example/Messaging/Tests/FIRMessagingServiceTest.m
new file mode 100644
index 0000000..bdbc0c2
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingServiceTest.m
@@ -0,0 +1,288 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 UIKit;
+@import XCTest;
+
+#import <OCMock/OCMock.h>
+
+#import "FIRMessaging.h"
+#import "FIRMessagingClient.h"
+#import "FIRMessagingConfig.h"
+#import "FIRMessagingPubSub.h"
+#import "FIRMessagingTopicsCommon.h"
+#import "InternalHeaders/FIRMessagingInternalUtilities.h"
+#import "NSError+FIRMessaging.h"
+
+@interface FIRMessaging () <FIRMessagingClientDelegate>
+
+@property(nonatomic, readwrite, strong) FIRMessagingClient *client;
+@property(nonatomic, readwrite, strong) FIRMessagingPubSub *pubsub;
+@property(nonatomic, readwrite, strong) NSString *defaultFcmToken;
+
+@end
+
+@interface FIRMessagingPubSub ()
+
+@property(nonatomic, readwrite, strong) FIRMessagingClient *client;
+
+@end
+
+
+@interface FIRMessagingServiceTest : XCTestCase
+
+@end
+
+@implementation FIRMessagingServiceTest
+
+- (void)testSubscribe {
+ id mockClient = OCMClassMock([FIRMessagingClient class]);
+ FIRMessaging *service = [FIRMessaging messaging];
+ [service setClient:mockClient];
+ [service.pubsub setClient:mockClient];
+
+ XCTestExpectation *subscribeExpectation =
+ [self expectationWithDescription:@"Should call subscribe on FIRMessagingClient"];
+ NSString *token = @"abcdefghijklmn";
+ NSString *topic = @"/topics/some-random-topic";
+
+ [[[mockClient stub]
+ andDo:^(NSInvocation *invocation) {
+ [subscribeExpectation fulfill];
+ }]
+ updateSubscriptionWithToken:token
+ topic:topic
+ options:OCMOCK_ANY
+ shouldDelete:NO
+ handler:OCMOCK_ANY];
+
+ [service.pubsub subscribeWithToken:token
+ topic:topic
+ options:nil
+ handler:^(FIRMessagingTopicOperationResult result, NSError *error) {
+ // not a nil block
+ }];
+
+ // should call updateSubscription
+ [self waitForExpectationsWithTimeout:0.1
+ handler:^(NSError *error) {
+ XCTAssertNil(error);
+ [mockClient verify];
+ }];
+}
+
+- (void)testUnsubscribe {
+ id mockClient = OCMClassMock([FIRMessagingClient class]);
+ FIRMessaging *messaging = [FIRMessaging messaging];
+ [messaging setClient:mockClient];
+ [messaging.pubsub setClient:mockClient];
+
+ XCTestExpectation *subscribeExpectation =
+ [self expectationWithDescription:@"Should call unsubscribe on FIRMessagingClient"];
+
+ NSString *token = @"abcdefghijklmn";
+ NSString *topic = @"/topics/some-random-topic";
+
+ [[[mockClient stub] andDo:^(NSInvocation *invocation) {
+ [subscribeExpectation fulfill];
+ }]
+ updateSubscriptionWithToken:[OCMArg isEqual:token]
+ topic:[OCMArg isEqual:topic]
+ options:[OCMArg checkWithBlock:^BOOL(id obj) {
+ if ([obj isKindOfClass:[NSDictionary class]]) {
+ return [(NSDictionary *)obj count] == 0;
+ }
+ return NO;
+ }]
+ shouldDelete:YES
+ handler:OCMOCK_ANY];
+
+ [messaging.pubsub unsubscribeWithToken:token
+ topic:topic
+ options:nil
+ handler:^(FIRMessagingTopicOperationResult result, NSError *error){
+
+ }];
+
+ // should call updateSubscription
+ [self waitForExpectationsWithTimeout:0.1
+ handler:^(NSError *error) {
+ XCTAssertNil(error);
+ [mockClient verify];
+ }];
+}
+
+/**
+ * Test using PubSub without explicitly starting FIRMessagingService.
+ */
+- (void)testSubscribeWithoutStart {
+ [[[FIRMessaging messaging] pubsub] subscribeWithToken:@"abcdef1234"
+ topic:@"/topics/hello-world"
+ options:nil
+ handler:
+ ^(FIRMessagingTopicOperationResult result, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqual(kFIRMessagingErrorCodePubSubFIRMessagingNotSetup,
+ error.code);
+ }];
+}
+
+- (void)testSubscribeWithInvalidToken {
+ FIRMessaging *messaging = [FIRMessaging messaging];
+
+ XCTestExpectation *exceptionExpectation =
+ [self expectationWithDescription:@"Should throw exception for invalid token"];
+ @try {
+ [messaging.pubsub subscribeWithToken:@""
+ topic:@"/topics/hello-world"
+ options:nil
+ handler:
+ ^(FIRMessagingTopicOperationResult result, NSError *error) {
+ XCTFail(@"Should not invoke the handler");
+ }];
+ }
+ @catch (NSException *exception) {
+ [exceptionExpectation fulfill];
+ }
+ @finally {
+ [self waitForExpectationsWithTimeout:0.1 handler:^(NSError *error) {
+ XCTAssertNil(error);
+ }];
+ }
+}
+
+- (void)testUnsubscribeWithInvalidTopic {
+ FIRMessaging *messaging = [FIRMessaging messaging];
+
+ XCTestExpectation *exceptionExpectation =
+ [self expectationWithDescription:@"Should throw exception for invalid token"];
+ @try {
+ [messaging.pubsub unsubscribeWithToken:@"abcdef1234"
+ topic:nil
+ options:nil
+ handler:
+ ^(FIRMessagingTopicOperationResult result, NSError *error) {
+ XCTFail(@"Should not invoke the handler");
+ }];
+ }
+ @catch (NSException *exception) {
+ [exceptionExpectation fulfill];
+ }
+ @finally {
+ [self waitForExpectationsWithTimeout:0.1 handler:^(NSError *error) {
+ XCTAssertNil(error);
+ }];
+ }
+}
+
+- (void)testSubscribeWithNoTopicPrefix {
+ FIRMessaging *messaging = [FIRMessaging messaging];
+ FIRMessagingPubSub *pubSub = messaging.pubsub;
+ id mockPubSub = OCMClassMock([FIRMessagingPubSub class]);
+
+ NSString *topicName = @"topicWithoutPrefix";
+ NSString *topicNameWithPrefix = [FIRMessagingPubSub addPrefixToTopic:topicName];
+ messaging.pubsub = mockPubSub;
+ messaging.defaultFcmToken = @"fake-default-token";
+ OCMExpect([messaging.pubsub subscribeToTopic:[OCMArg isEqual:topicNameWithPrefix]]);
+ [messaging subscribeToTopic:topicName];
+ OCMVerifyAll(mockPubSub);
+ // Need to swap back since it's a singleton and hence will live beyond the scope of this test.
+ messaging.pubsub = pubSub;
+}
+
+- (void)testSubscribeWithTopicPrefix {
+ FIRMessaging *messaging = [FIRMessaging messaging];
+ FIRMessagingPubSub *pubSub = messaging.pubsub;
+ id mockPubSub = OCMClassMock([FIRMessagingPubSub class]);
+
+ NSString *topicName = @"/topics/topicWithoutPrefix";
+ messaging.pubsub = mockPubSub;
+ messaging.defaultFcmToken = @"fake-default-token";
+ OCMExpect([messaging.pubsub subscribeToTopic:[OCMArg isEqual:topicName]]);
+ [messaging subscribeToTopic:topicName];
+ OCMVerifyAll(mockPubSub);
+ // Need to swap back since it's a singleton and hence will live beyond the scope of this test.
+ messaging.pubsub = pubSub;
+}
+
+- (void)testUnsubscribeWithNoTopicPrefix {
+ FIRMessaging *messaging = [FIRMessaging messaging];
+ FIRMessagingPubSub *pubSub = messaging.pubsub;
+ id mockPubSub = OCMClassMock([FIRMessagingPubSub class]);
+
+ NSString *topicName = @"topicWithoutPrefix";
+ NSString *topicNameWithPrefix = [FIRMessagingPubSub addPrefixToTopic:topicName];
+ messaging.pubsub = mockPubSub;
+ messaging.defaultFcmToken = @"fake-default-token";
+ OCMExpect([messaging.pubsub unsubscribeFromTopic:[OCMArg isEqual:topicNameWithPrefix]]);
+ [messaging unsubscribeFromTopic:topicName];
+ OCMVerifyAll(mockPubSub);
+ // Need to swap back since it's a singleton and hence will live beyond the scope of this test.
+ messaging.pubsub = pubSub;
+}
+
+- (void)testUnsubscribeWithTopicPrefix {
+ FIRMessaging *messaging = [FIRMessaging messaging];
+ FIRMessagingPubSub *pubSub = messaging.pubsub;
+ id mockPubSub = OCMClassMock([FIRMessagingPubSub class]);
+
+ NSString *topicName = @"/topics/topicWithPrefix";
+ messaging.pubsub = mockPubSub;
+ messaging.defaultFcmToken = @"fake-default-token";
+ OCMExpect([messaging.pubsub unsubscribeFromTopic:[OCMArg isEqual:topicName]]);
+ [messaging unsubscribeFromTopic:topicName];
+ OCMVerifyAll(mockPubSub);
+ // Need to swap back since it's a singleton and hence will live beyond the scope of this test.
+ messaging.pubsub = pubSub;
+}
+
+- (void)testFIRMessagingSDKVersionInFIRMessagingService {
+ Class versionClass = NSClassFromString(kFIRMessagingSDKClassString);
+ SEL versionSelector = NSSelectorFromString(kFIRMessagingSDKVersionSelectorString);
+ if ([versionClass respondsToSelector:versionSelector]) {
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ id versionString = [versionClass performSelector:versionSelector];
+#pragma clang diagnostic pop
+
+ XCTAssertTrue([versionString isKindOfClass:[NSString class]]);
+ } else {
+ XCTFail("%@ does not respond to selector %@",
+ kFIRMessagingSDKClassString, kFIRMessagingSDKVersionSelectorString);
+ }
+}
+
+- (void)testFIRMessagingSDKLocaleInFIRMessagingService {
+ Class klass = NSClassFromString(kFIRMessagingSDKClassString);
+ SEL localeSelector = NSSelectorFromString(kFIRMessagingSDKLocaleSelectorString);
+ if ([klass respondsToSelector:localeSelector]) {
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ id locale = [klass performSelector:localeSelector];
+#pragma clang diagnostic pop
+
+ XCTAssertTrue([locale isKindOfClass:[NSString class]]);
+ XCTAssertNotNil(locale);
+ } else {
+ XCTFail("%@ does not respond to selector %@",
+ kFIRMessagingSDKClassString, kFIRMessagingSDKLocaleSelectorString);
+ }
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingSyncMessageManagerTest.m b/Example/Messaging/Tests/FIRMessagingSyncMessageManagerTest.m
new file mode 100644
index 0000000..e40e1f2
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingSyncMessageManagerTest.m
@@ -0,0 +1,256 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 <XCTest/XCTest.h>
+
+#import "FIRMessagingPersistentSyncMessage.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessagingSyncMessageManager.h"
+#import "FIRMessagingUtilities.h"
+#import "FIRMessagingConstants.h"
+
+static NSString *const kRmqSqliteFilename = @"rmq-sync-manager-test";
+
+@interface FIRMessagingSyncMessageManagerTest : XCTestCase
+
+@property(nonatomic, readwrite, strong) FIRMessagingRmqManager *rmqManager;
+@property(nonatomic, readwrite, strong) FIRMessagingSyncMessageManager *syncMessageManager;
+
+@end
+
+@implementation FIRMessagingSyncMessageManagerTest
+
+- (void)setUp {
+ [super setUp];
+ // Make sure the db state is clean before we begin.
+ [FIRMessagingRmqManager removeDatabaseWithName:kRmqSqliteFilename];
+ self.rmqManager = [[FIRMessagingRmqManager alloc] initWithDatabaseName:kRmqSqliteFilename];
+ self.syncMessageManager = [[FIRMessagingSyncMessageManager alloc] initWithRmqManager:self.rmqManager];
+}
+
+- (void)tearDown {
+ [[self.rmqManager class] removeDatabaseWithName:kRmqSqliteFilename];
+ [super tearDown];
+}
+
+/**
+ * Test receiving a new sync message via APNS should be added to SYNC_RMQ.
+ */
+- (void)testNewAPNSMessage {
+ int64_t expirationTime = FIRMessagingCurrentTimestampInSeconds() + 86400; // 1 day in future
+
+ NSDictionary *oldMessage = @{
+ kFIRMessagingMessageIDKey : @"fake-rmq-1",
+ kFIRMessagingMessageSyncViaMCSKey : @(expirationTime),
+ @"hello" : @"world",
+ };
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:oldMessage]);
+
+ NSDictionary *newMessage = @{
+ kFIRMessagingMessageIDKey : @"fake-rmq-2",
+ kFIRMessagingMessageSyncViaMCSKey : @(expirationTime),
+ @"hello" : @"world",
+ };
+
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:newMessage]);
+}
+
+/**
+ * Test receiving a new sync message via MCS should be added to SYNC_RMQ.
+ */
+- (void)testNewMCSMessage {
+ int64_t expirationTime = FIRMessagingCurrentTimestampInSeconds() + 86400; // 1 day in future
+ NSDictionary *oldMessage = @{
+ kFIRMessagingMessageIDKey : @"fake-rmq-1",
+ kFIRMessagingMessageSyncViaMCSKey : @(expirationTime),
+ @"hello" : @"world",
+ };
+ XCTAssertFalse([self.syncMessageManager didReceiveMCSSyncMessage:oldMessage]);
+
+ NSDictionary *newMessage = @{
+ kFIRMessagingMessageIDKey : @"fake-rmq-2",
+ kFIRMessagingMessageSyncViaMCSKey : @(expirationTime),
+ @"hello" : @"world",
+ };
+
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:newMessage]);
+}
+
+/**
+ * Test receiving a duplicate message via APNS.
+ */
+- (void)testDuplicateAPNSMessage {
+ NSString *messageID = @"fake-rmq-1";
+ int64_t expirationTime = FIRMessagingCurrentTimestampInSeconds() + 86400; // 1 day in future
+ NSDictionary *newMessage = @{
+ kFIRMessagingMessageIDKey : messageID,
+ kFIRMessagingMessageSyncViaMCSKey : @(expirationTime),
+ @"hello" : @"world",
+ };
+
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:newMessage]);
+
+ // The message is a duplicate
+ XCTAssertTrue([self.syncMessageManager didReceiveAPNSSyncMessage:newMessage]);
+
+ FIRMessagingPersistentSyncMessage *persistentMessage =
+ [self.rmqManager querySyncMessageWithRmqID:messageID];
+ XCTAssertTrue(persistentMessage.apnsReceived);
+ XCTAssertFalse(persistentMessage.mcsReceived);
+}
+
+/**
+ * Test receiving a duplicate message via MCS.
+ */
+- (void)testDuplicateMCSMessage {
+ NSString *messageID = @"fake-rmq-1";
+ int64_t expirationTime = FIRMessagingCurrentTimestampInSeconds() + 86400; // 1 day in future
+ NSDictionary *newMessage = @{
+ kFIRMessagingMessageIDKey : messageID,
+ kFIRMessagingMessageSyncViaMCSKey : @(expirationTime),
+ @"hello" : @"world",
+ };
+
+ XCTAssertFalse([self.syncMessageManager didReceiveMCSSyncMessage:newMessage]);
+
+ // The message is a duplicate
+ XCTAssertTrue([self.syncMessageManager didReceiveMCSSyncMessage:newMessage]);
+
+ FIRMessagingPersistentSyncMessage *persistentMessage =
+ [self.rmqManager querySyncMessageWithRmqID:messageID];
+ XCTAssertFalse(persistentMessage.apnsReceived);
+ XCTAssertTrue(persistentMessage.mcsReceived);
+}
+
+/**
+ * Test receiving a sync message both via APNS and MCS.
+ */
+- (void)testMessageReceivedBothViaAPNSAndMCS {
+ NSString *messageID = @"fake-rmq-1";
+ int64_t expirationTime = FIRMessagingCurrentTimestampInSeconds() + 86400; // 1 day in future
+ NSDictionary *newMessage = @{
+ kFIRMessagingMessageIDKey : messageID,
+ kFIRMessagingMessageSyncViaMCSKey : @(expirationTime),
+ @"hello" : @"world",
+ };
+
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:newMessage]);
+ // Duplicate of the above received APNS message
+ XCTAssertTrue([self.syncMessageManager didReceiveMCSSyncMessage:newMessage]);
+
+ // Since we've received both APNS and MCS messages we should have deleted them from SYNC_RMQ
+ FIRMessagingPersistentSyncMessage *persistentMessage =
+ [self.rmqManager querySyncMessageWithRmqID:messageID];
+ XCTAssertNil(persistentMessage);
+}
+
+- (void)testDeletingExpiredMessages {
+ NSString *unexpiredMessageID = @"fake-not-expired-rmqID";
+ int64_t futureExpirationTime = 86400; // 1 day in future
+ NSDictionary *unexpiredMessage = @{
+ kFIRMessagingMessageIDKey : unexpiredMessageID,
+ kFIRMessagingMessageSyncMessageTTLKey : @(futureExpirationTime),
+ @"hello" : @"world",
+ };
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:unexpiredMessage]);
+
+ NSString *expiredMessageID = @"fake-expired-rmqID";
+ int64_t past = -86400; // 1 day in past
+ NSDictionary *expiredMessage = @{
+ kFIRMessagingMessageIDKey : expiredMessageID,
+ kFIRMessagingMessageSyncMessageTTLKey : @(past),
+ @"hello" : @"world",
+ };
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:expiredMessage]);
+
+ NSString *noTTLMessageID = @"no-ttl-rmqID"; // no TTL specified should be 4 weeks
+ NSDictionary *noTTLMessage = @{
+ kFIRMessagingMessageIDKey : noTTLMessageID,
+ @"hello" : @"world",
+ };
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:noTTLMessage]);
+
+ [self.syncMessageManager removeExpiredSyncMessages];
+
+ XCTAssertNotNil([self.rmqManager querySyncMessageWithRmqID:unexpiredMessageID]);
+ XCTAssertNil([self.rmqManager querySyncMessageWithRmqID:expiredMessageID]);
+ XCTAssertNotNil([self.rmqManager querySyncMessageWithRmqID:noTTLMessageID]);
+}
+
+- (void)testDeleteFinishedMessages {
+ NSString *unexpiredMessageID = @"fake-not-expired-rmqID";
+ int64_t futureExpirationTime = 86400; // 1 day in future
+ NSDictionary *unexpiredMessage = @{
+ kFIRMessagingMessageIDKey : unexpiredMessageID,
+ kFIRMessagingMessageSyncMessageTTLKey : @(futureExpirationTime),
+ @"hello" : @"world",
+ };
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:unexpiredMessage]);
+
+ NSString *noTTLMessageID = @"no-ttl-rmqID"; // no TTL specified should be 4 weeks
+ NSDictionary *noTTLMessage = @{
+ kFIRMessagingMessageIDKey : noTTLMessageID,
+ @"hello" : @"world",
+ };
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:noTTLMessage]);
+
+ // Mark the no-TTL message as received via MCS too
+ XCTAssertTrue([self.rmqManager updateSyncMessageViaMCSWithRmqID:noTTLMessageID error:nil]);
+
+ [self.syncMessageManager removeExpiredSyncMessages];
+
+ XCTAssertNotNil([self.rmqManager querySyncMessageWithRmqID:unexpiredMessageID]);
+ XCTAssertNil([self.rmqManager querySyncMessageWithRmqID:noTTLMessageID]);
+}
+
+- (void)testDeleteFinishedAndExpiredMessages {
+ NSString *unexpiredMessageID = @"fake-not-expired-rmqID";
+ int64_t futureExpirationTime = 86400; // 1 day in future
+ NSDictionary *unexpiredMessage = @{
+ kFIRMessagingMessageIDKey : unexpiredMessageID,
+ kFIRMessagingMessageSyncMessageTTLKey : @(futureExpirationTime),
+ @"hello" : @"world",
+ };
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:unexpiredMessage]);
+
+ NSString *expiredMessageID = @"fake-expired-rmqID";
+ int64_t past = -86400; // 1 day in past
+ NSDictionary *expiredMessage = @{
+ kFIRMessagingMessageIDKey : expiredMessageID,
+ kFIRMessagingMessageSyncMessageTTLKey : @(past),
+ @"hello" : @"world",
+ };
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:expiredMessage]);
+
+ NSString *noTTLMessageID = @"no-ttl-rmqID"; // no TTL specified should be 4 weeks
+ NSDictionary *noTTLMessage = @{
+ kFIRMessagingMessageIDKey : noTTLMessageID,
+ @"hello" : @"world",
+ };
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:noTTLMessage]);
+
+ // Mark the no-TTL message as received via MCS too
+ XCTAssertTrue([self.rmqManager updateSyncMessageViaMCSWithRmqID:noTTLMessageID error:nil]);
+
+ // Remove expired or finished sync messages.
+ [self.syncMessageManager removeExpiredSyncMessages];
+
+ XCTAssertNotNil([self.rmqManager querySyncMessageWithRmqID:unexpiredMessageID]);
+ XCTAssertNil([self.rmqManager querySyncMessageWithRmqID:expiredMessageID]);
+ XCTAssertNil([self.rmqManager querySyncMessageWithRmqID:noTTLMessageID]);
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingTest.m b/Example/Messaging/Tests/FIRMessagingTest.m
new file mode 100644
index 0000000..3d7c95f
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingTest.m
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 XCTest;
+
+#import <OCMock/OCMock.h>
+
+#import "FIRMessaging.h"
+#import "FIRMessagingConfig.h"
+#import "FIRMessagingInstanceIDProxy.h"
+
+extern NSString *const kFIRMessagingFCMTokenFetchAPNSOption;
+
+@interface FIRMessaging ()
+
+@property(nonatomic, readwrite, strong) NSString *defaultFcmToken;
+@property(nonatomic, readwrite, strong) NSData *apnsTokenData;
+@property(nonatomic, readwrite, strong) FIRMessagingInstanceIDProxy *instanceIDProxy;
+
+- (instancetype)initWithConfig:(FIRMessagingConfig *)config;
+// Direct Channel Methods
+- (void)updateAutomaticClientConnection;
+- (BOOL)shouldBeConnectedAutomatically;
+
+@end
+
+@interface FIRMessagingTest : XCTestCase
+
+@property(nonatomic, readonly, strong) FIRMessaging *messaging;
+@property(nonatomic, readwrite, strong) id mockMessaging;
+@property(nonatomic, readwrite, strong) id mockInstanceIDProxy;
+
+@end
+
+@implementation FIRMessagingTest
+
+- (void)setUp {
+ [super setUp];
+ FIRMessagingConfig *config = [FIRMessagingConfig defaultConfig];
+ _messaging = [[FIRMessaging alloc] initWithConfig:config];
+ _mockMessaging = OCMPartialMock(self.messaging);
+ _mockInstanceIDProxy = OCMPartialMock(self.messaging.instanceIDProxy);
+ self.messaging.instanceIDProxy = _mockInstanceIDProxy;
+}
+
+- (void)tearDown {
+ _messaging = nil;
+ _mockMessaging = nil;
+ [super tearDown];
+}
+
+#pragma mark - Direct Channel Establishment Testing
+
+// Should connect with valid token and application in foreground
+- (void)testDoesAutomaticallyConnectIfTokenAvailableAndForegrounded {
+ // Disable actually attempting a connection
+ [[[_mockMessaging stub] andDo:^(NSInvocation *invocation) {
+ // Doing nothing on purpose, when -updateAutomaticClientConnection is called
+ }] updateAutomaticClientConnection];
+ // Set direct channel to be established after disabling connection attempt
+ self.messaging.shouldEstablishDirectChannel = YES;
+ // Set a "valid" token (i.e. not nil or empty)
+ self.messaging.defaultFcmToken = @"1234567";
+ // Swizzle application state to return UIApplicationStateActive
+ UIApplication *app = [UIApplication sharedApplication];
+ id mockApp = OCMPartialMock(app);
+ [[[mockApp stub] andReturnValue:@(UIApplicationStateActive)] applicationState];
+ BOOL shouldBeConnected = [_mockMessaging shouldBeConnectedAutomatically];
+ XCTAssertTrue(shouldBeConnected);
+}
+
+// Should not connect if application is active, but token is empty
+- (void)testDoesNotAutomaticallyConnectIfTokenIsEmpty {
+ // Disable actually attempting a connection
+ [[[_mockMessaging stub] andDo:^(NSInvocation *invocation) {
+ // Doing nothing on purpose, when -updateAutomaticClientConnection is called
+ }] updateAutomaticClientConnection];
+ // Set direct channel to be established after disabling connection attempt
+ self.messaging.shouldEstablishDirectChannel = YES;
+ // By default, there should be no fcmToken
+ // Swizzle application state to return UIApplicationStateActive
+ UIApplication *app = [UIApplication sharedApplication];
+ id mockApp = OCMPartialMock(app);
+ [[[mockApp stub] andReturnValue:@(UIApplicationStateActive)] applicationState];
+ BOOL shouldBeConnected = [_mockMessaging shouldBeConnectedAutomatically];
+ XCTAssertFalse(shouldBeConnected);
+}
+
+// Should not connect if token valid but application isn't active
+- (void)testDoesNotAutomaticallyConnectIfApplicationNotActive {
+ // Disable actually attempting a connection
+ [[[_mockMessaging stub] andDo:^(NSInvocation *invocation) {
+ // Doing nothing on purpose, when -updateAutomaticClientConnection is called
+ }] updateAutomaticClientConnection];
+ // Set direct channel to be established after disabling connection attempt
+ self.messaging.shouldEstablishDirectChannel = YES;
+ // Set a "valid" token (i.e. not nil or empty)
+ self.messaging.defaultFcmToken = @"abcd1234";
+ // Swizzle application state to return UIApplicationStateActive
+ UIApplication *app = [UIApplication sharedApplication];
+ id mockApp = OCMPartialMock(app);
+ [[[mockApp stub] andReturnValue:@(UIApplicationStateBackground)] applicationState];
+ BOOL shouldBeConnected = [_mockMessaging shouldBeConnectedAutomatically];
+ XCTAssertFalse(shouldBeConnected);
+}
+
+#pragma mark - FCM Token Fetching and Deleting
+
+#ifdef NEED_WORKAROUND_FOR_PRIVATE_OCMOCK_getArgumentAtIndexAsObject
+- (void)testAPNSTokenIncludedInOptionsIfAvailableDuringTokenFetch {
+ self.messaging.apnsTokenData =
+ [@"PRETENDING_TO_BE_A_DEVICE_TOKEN" dataUsingEncoding:NSUTF8StringEncoding];
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Included APNS Token data in options dict."];
+ // Inspect the 'options' dictionary to tell whether our expectation was fulfilled
+ [[[self.mockInstanceIDProxy stub] andDo:^(NSInvocation *invocation) {
+ // Calling getArgument:atIndex: directly leads to an EXC_BAD_ACCESS; use OCMock's wrapper.
+ NSDictionary *options = [invocation getArgumentAtIndexAsObject:4];
+ if (options[@"apns_token"] != nil) {
+ [expectation fulfill];
+ }
+ }] tokenWithAuthorizedEntity:OCMOCK_ANY scope:OCMOCK_ANY options:OCMOCK_ANY handler:OCMOCK_ANY];
+ [self.messaging retrieveFCMTokenForSenderID:@"123456"
+ completion:^(NSString * _Nullable FCMToken,
+ NSError * _Nullable error) {}];
+ [self waitForExpectationsWithTimeout:0.1 handler:nil];
+}
+
+- (void)testAPNSTokenNotIncludedIfUnavailableDuringTokenFetch {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Included APNS Token data not included in options dict."];
+ // Inspect the 'options' dictionary to tell whether our expectation was fulfilled
+ [[[self.mockInstanceIDProxy stub] andDo:^(NSInvocation *invocation) {
+ // Calling getArgument:atIndex: directly leads to an EXC_BAD_ACCESS; use OCMock's wrapper.
+ NSDictionary *options = [invocation getArgumentAtIndexAsObject:4];
+ if (options[@"apns_token"] == nil) {
+ [expectation fulfill];
+ }
+ }] tokenWithAuthorizedEntity:OCMOCK_ANY scope:OCMOCK_ANY options:OCMOCK_ANY handler:OCMOCK_ANY];
+ [self.messaging retrieveFCMTokenForSenderID:@"123456"
+ completion:^(NSString * _Nullable FCMToken,
+ NSError * _Nullable error) {}];
+ [self waitForExpectationsWithTimeout:0.1 handler:nil];
+}
+#endif
+
+- (void)testReturnsErrorWhenFetchingTokenWithoutSenderID {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Returned an error fetching token without Sender ID"];
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnonnull"
+ [self.messaging retrieveFCMTokenForSenderID:nil
+ completion:
+ ^(NSString * _Nullable FCMToken, NSError * _Nullable error) {
+ if (error != nil) {
+ [expectation fulfill];
+ }
+ }];
+#pragma clang diagnostic pop
+ [self waitForExpectationsWithTimeout:0.1 handler:nil];
+}
+
+- (void)testReturnsErrorWhenFetchingTokenWithEmptySenderID {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Returned an error fetching token with empty Sender ID"];
+ [self.messaging retrieveFCMTokenForSenderID:@""
+ completion:
+ ^(NSString * _Nullable FCMToken, NSError * _Nullable error) {
+ if (error != nil) {
+ [expectation fulfill];
+ }
+ }];
+ [self waitForExpectationsWithTimeout:0.1 handler:nil];
+}
+
+- (void)testReturnsErrorWhenDeletingTokenWithoutSenderID {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Returned an error deleting token without Sender ID"];
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnonnull"
+ [self.messaging deleteFCMTokenForSenderID:nil completion:^(NSError * _Nullable error) {
+ if (error != nil) {
+ [expectation fulfill];
+ }
+ }];
+#pragma clang diagnostic pop
+ [self waitForExpectationsWithTimeout:0.1 handler:nil];
+}
+
+- (void)testReturnsErrorWhenDeletingTokenWithEmptySenderID {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Returned an error deleting token with empty Sender ID"];
+ [self.messaging deleteFCMTokenForSenderID:@"" completion:^(NSError * _Nullable error) {
+ if (error != nil) {
+ [expectation fulfill];
+ }
+ }];
+ [self waitForExpectationsWithTimeout:0.1 handler:nil];
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingTestNotificationUtilities.h b/Example/Messaging/Tests/FIRMessagingTestNotificationUtilities.h
new file mode 100644
index 0000000..0bc6010
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingTestNotificationUtilities.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 <Foundation/Foundation.h>
+
+@interface FIRMessagingTestNotificationUtilities : NSObject
+
++ (NSMutableDictionary *)createBasicNotificationWithUniqueMessageID;
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingTestNotificationUtilities.m b/Example/Messaging/Tests/FIRMessagingTestNotificationUtilities.m
new file mode 100644
index 0000000..43842ee
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingTestNotificationUtilities.m
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 "FIRMessagingTestNotificationUtilities.h"
+
+#import "FIRMessagingConstants.h"
+
+@implementation FIRMessagingTestNotificationUtilities
+
++ (NSMutableDictionary *)createBasicNotificationWithUniqueMessageID {
+ NSMutableDictionary *notification = [NSMutableDictionary dictionary];
+ // Always generate a unique message id
+ notification[kFIRMessagingMessageIDKey] =
+ [NSString stringWithFormat:@"%@", @([NSDate date].timeIntervalSince1970)];
+ return notification;
+}
+
+@end
diff --git a/Example/Messaging/Tests/Info.plist b/Example/Messaging/Tests/Info.plist
new file mode 100644
index 0000000..ba72822
--- /dev/null
+++ b/Example/Messaging/Tests/Info.plist
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>BNDL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>