Skip to content

Commit 4cc55e5

Browse files
Merge pull request #27 from Iterable/feature/ITBL-6331-codecov
[ITBL-6331] - Increase code coverage
2 parents 7b113a6 + f7dfee6 commit 4cc55e5

23 files changed

+1415
-475
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ script:
88
- xcodebuild test -workspace swift-sdk.xcworkspace -scheme swift-sdk -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone X' -enableCodeCoverage YES CODE_SIGNING_REQUIRED=NO GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES GCC_GENERATE_TEST_COVERAGE_FILES=YES | xcpretty -f `xcpretty-travis-formatter`
99
- bash <(curl -s https://codecov.io/bash)
1010
- xcodebuild test -workspace swift-sdk.xcworkspace -scheme ios9-tests -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 6s,OS=9.0' -enableCodeCoverage YES CODE_SIGNING_REQUIRED=NO GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES GCC_GENERATE_TEST_COVERAGE_FILES=YES | xcpretty -f `xcpretty-travis-formatter`
11+
- bash <(curl -s https://codecov.io/bash)
1112
- pod lib lint
1213

Tests/common/CommonExtensions.swift

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//
2+
//
3+
// Created by Tapash Majumder on 10/5/18.
4+
// Copyright © 2018 Iterable. All rights reserved.
5+
//
6+
7+
import Foundation
8+
9+
@testable import IterableSDK
10+
11+
extension Dictionary where Key == AnyHashable {
12+
func toData() -> Data {
13+
return try! JSONSerialization.data(withJSONObject: self, options: [])
14+
}
15+
}
16+
17+
extension IterableAPI {
18+
// Internal Only used in unit tests.
19+
static func initialize(apiKey: String,
20+
launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil,
21+
config: IterableConfig = IterableConfig(),
22+
dateProvider: DateProviderProtocol = SystemDateProvider(),
23+
networkSession: @escaping @autoclosure () -> NetworkSessionProtocol = URLSession(configuration: URLSessionConfiguration.default),
24+
notificationStateProvider: NotificationStateProviderProtocol = SystemNotificationStateProvider()) {
25+
internalImplementation = IterableAPIInternal.initialize(apiKey: apiKey, launchOptions: launchOptions, config: config, dateProvider: dateProvider, networkSession: networkSession, notificationStateProvider: notificationStateProvider)
26+
}
27+
}
28+
29+
30+
// used by objc tests, remove after rewriting in Swift
31+
extension IterableAPIInternal {
32+
@discardableResult public static func initialize(apiKey: String) -> IterableAPIInternal {
33+
return initialize(apiKey: apiKey, config:IterableConfig())
34+
}
35+
36+
// used by objc tests, remove after rewriting them in Swift
37+
@discardableResult public static func initialize(apiKey: String,
38+
config: IterableConfig) -> IterableAPIInternal {
39+
return initialize(apiKey: apiKey, launchOptions: nil, config:config)
40+
}
41+
}
42+
43+
extension IterableAPIInternal {
44+
@discardableResult static func initialize(apiKey: String,
45+
launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil,
46+
config: IterableConfig = IterableConfig(),
47+
dateProvider: DateProviderProtocol = SystemDateProvider(),
48+
networkSession: @escaping @autoclosure () -> NetworkSessionProtocol = URLSession(configuration: URLSessionConfiguration.default),
49+
notificationStateProvider: NotificationStateProviderProtocol = SystemNotificationStateProvider()) -> IterableAPIInternal {
50+
queue.sync {
51+
_sharedInstance = IterableAPIInternal(apiKey: apiKey, config: config, dateProvider: dateProvider, networkSession: networkSession, notificationStateProvider: notificationStateProvider)
52+
}
53+
return _sharedInstance!
54+
}
55+
}

Tests/common/CommonMocks.swift

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
//
2+
//
3+
// Created by Tapash Majumder on 6/13/18.
4+
// Copyright © 2018 Iterable. All rights reserved.
5+
//
6+
7+
//import XCTest
8+
9+
import Foundation
10+
import UserNotifications
11+
12+
@testable import IterableSDK
13+
14+
@available(iOS 10.0, *)
15+
struct MockNotificationResponse : NotificationResponseProtocol {
16+
let userInfo: [AnyHashable : Any]
17+
let actionIdentifier: String
18+
19+
init(userInfo: [AnyHashable : Any], actionIdentifier: String) {
20+
self.userInfo = userInfo;
21+
self.actionIdentifier = actionIdentifier
22+
}
23+
24+
var textInputResponse: UNTextInputNotificationResponse? {
25+
return nil
26+
}
27+
}
28+
29+
30+
@objcMembers
31+
public class MockUrlDelegate : NSObject, IterableURLDelegate {
32+
// returnValue = true if we handle the url, else false
33+
private override convenience init() {
34+
self.init(returnValue: false)
35+
}
36+
37+
public init(returnValue: Bool) {
38+
self.returnValue = returnValue
39+
}
40+
41+
private (set) var returnValue: Bool
42+
private (set) var url: URL?
43+
private (set) var context: IterableActionContext?
44+
var callback: ((URL, IterableActionContext)->Void)? = nil
45+
46+
public func handle(iterableURL url: URL, inContext context: IterableActionContext) -> Bool {
47+
self.url = url
48+
self.context = context
49+
callback?(url, context)
50+
return returnValue
51+
}
52+
}
53+
54+
@objcMembers
55+
public class MockCustomActionDelegate: NSObject, IterableCustomActionDelegate {
56+
// returnValue is reserved for future, don't rely on this
57+
private override convenience init() {
58+
self.init(returnValue: false)
59+
}
60+
61+
public init(returnValue: Bool) {
62+
self.returnValue = returnValue
63+
}
64+
65+
private (set) var returnValue: Bool
66+
private (set) var action: IterableAction?
67+
private (set) var context: IterableActionContext?
68+
var callback: ((String, IterableActionContext)->Void)? = nil
69+
70+
public func handle(iterableCustomAction action: IterableAction, inContext context: IterableActionContext) -> Bool {
71+
self.action = action
72+
self.context = context
73+
callback?(action.type, context)
74+
return returnValue
75+
}
76+
}
77+
78+
@objc public class MockUrlOpener : NSObject, UrlOpenerProtocol {
79+
@objc var ios10OpenedUrl: URL?
80+
@objc var preIos10openedUrl: URL?
81+
82+
public func open(url: URL) {
83+
if #available(iOS 10.0, *) {
84+
ios10OpenedUrl = url
85+
} else {
86+
preIos10openedUrl = url
87+
}
88+
}
89+
}
90+
91+
@objcMembers
92+
public class MockPushTracker : NSObject, PushTrackerProtocol {
93+
var campaignId: NSNumber?
94+
var templateId: NSNumber?
95+
var messageId: String?
96+
var appAlreadyRunnnig: Bool = false
97+
var dataFields: [AnyHashable : Any]?
98+
var onSuccess: OnSuccessHandler?
99+
var onFailure: OnFailureHandler?
100+
public var lastPushPayload: [AnyHashable : Any]?
101+
102+
public func trackPushOpen(_ userInfo: [AnyHashable : Any]) {
103+
trackPushOpen(userInfo, dataFields: nil)
104+
}
105+
106+
public func trackPushOpen(_ userInfo: [AnyHashable : Any], dataFields: [AnyHashable : Any]?) {
107+
trackPushOpen(userInfo, dataFields: dataFields, onSuccess: nil, onFailure: nil)
108+
}
109+
110+
public func trackPushOpen(_ userInfo: [AnyHashable : Any], dataFields: [AnyHashable : Any]?, onSuccess: OnSuccessHandler?, onFailure: OnFailureHandler?) {
111+
// save payload
112+
lastPushPayload = userInfo
113+
114+
if let metadata = IterableNotificationMetadata.metadata(fromLaunchOptions: userInfo), metadata.isRealCampaignNotification() {
115+
trackPushOpen(metadata.campaignId, templateId: metadata.templateId, messageId: metadata.messageId, appAlreadyRunning: false, dataFields: dataFields, onSuccess: onSuccess, onFailure: onFailure)
116+
} else {
117+
onFailure?("Not tracking push open - payload is not an Iterable notification, or a test/proof/ghost push", nil)
118+
}
119+
}
120+
121+
public func trackPushOpen(_ campaignId: NSNumber, templateId: NSNumber?, messageId: String?, appAlreadyRunning: Bool, dataFields: [AnyHashable : Any]?) {
122+
trackPushOpen(campaignId, templateId: templateId, messageId: messageId, appAlreadyRunning: appAlreadyRunning, dataFields: dataFields, onSuccess: nil, onFailure: nil)
123+
}
124+
125+
public func trackPushOpen(_ campaignId: NSNumber, templateId: NSNumber?, messageId: String?, appAlreadyRunning: Bool, dataFields: [AnyHashable : Any]?, onSuccess: OnSuccessHandler?, onFailure: OnFailureHandler?) {
126+
self.campaignId = campaignId
127+
self.templateId = templateId
128+
self.messageId = messageId
129+
self.appAlreadyRunnnig = appAlreadyRunning
130+
self.dataFields = dataFields
131+
self.onSuccess = onSuccess
132+
self.onFailure = onFailure
133+
}
134+
}
135+
136+
@objc public class MockApplicationStateProvider : NSObject, ApplicationStateProviderProtocol {
137+
private override convenience init() {
138+
self.init(applicationState: .active)
139+
}
140+
141+
@objc public init(applicationState: UIApplication.State) {
142+
self.applicationState = applicationState
143+
}
144+
145+
public var applicationState: UIApplication.State
146+
}
147+
148+
class MockNetworkSession: NetworkSessionProtocol {
149+
var request: URLRequest?
150+
var callback: ((Data?, URLResponse?, Error?) -> Void)?
151+
152+
var statusCode: Int
153+
var data: Data?
154+
var error: Error?
155+
156+
convenience init(statusCode: Int) {
157+
self.init(statusCode: statusCode,
158+
data: [:].toData(),
159+
error: nil)
160+
}
161+
162+
convenience init(statusCode: Int, json: [AnyHashable : Any], error: Error? = nil) {
163+
self.init(statusCode: statusCode,
164+
data: json.toData(),
165+
error: error)
166+
}
167+
168+
init(statusCode: Int, data: Data?, error: Error? = nil) {
169+
self.statusCode = statusCode
170+
self.data = data
171+
self.error = error
172+
}
173+
174+
func makeRequest(_ request: URLRequest, completionHandler: @escaping NetworkSessionProtocol.CompletionHandler) {
175+
DispatchQueue.main.async {
176+
self.request = request
177+
let response = HTTPURLResponse(url: request.url!, statusCode: self.statusCode, httpVersion: "HTTP/1.1", headerFields: [:])
178+
completionHandler(self.data, response, self.error)
179+
180+
self.callback?(self.data, response, self.error)
181+
}
182+
}
183+
184+
func getRequestBody() -> [AnyHashable : Any] {
185+
return MockNetworkSession.json(fromData: request!.httpBody!)
186+
}
187+
188+
static func json(fromData data: Data) -> [AnyHashable : Any] {
189+
return try! JSONSerialization.jsonObject(with: data, options: []) as! [AnyHashable : Any]
190+
}
191+
}
192+
193+
class NoNetworkNetworkSession: NetworkSessionProtocol {
194+
func makeRequest(_ request: URLRequest, completionHandler: @escaping NetworkSessionProtocol.CompletionHandler) {
195+
DispatchQueue.main.async {
196+
let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: [:])
197+
let error = NSError(domain: NSURLErrorDomain, code: -1009, userInfo: nil)
198+
completionHandler(try! JSONSerialization.data(withJSONObject: [:], options: []), response, error)
199+
}
200+
}
201+
}
202+
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//
2+
//
3+
// Created by Tapash Majumder on 10/9/18.
4+
// Copyright © 2018 Iterable. All rights reserved.
5+
//
6+
7+
import XCTest
8+
9+
import UserNotifications
10+
11+
@testable import IterableAppExtensions
12+
13+
class NotificationExtensionSwiftTests: XCTestCase {
14+
private var appExtension : ITBNotificationServiceExtension!
15+
private let delay = 0.05
16+
private let timeout = 15.0
17+
18+
override func setUp() {
19+
// Put setup code here. This method is called before the invocation of each test method in the class.
20+
super.setUp()
21+
UNUserNotificationCenter.current().setNotificationCategories([])
22+
appExtension = ITBNotificationServiceExtension()
23+
}
24+
25+
override func tearDown() {
26+
// Put teardown code here. This method is called after the invocation of each test method in the class.
27+
appExtension = nil
28+
super.tearDown()
29+
}
30+
31+
func testPushButtonWithNoType() {
32+
let content = UNMutableNotificationContent()
33+
let messageId = UUID().uuidString
34+
content.userInfo = [
35+
"itbl" : [
36+
"messageId" : messageId,
37+
"actionButtons" : [[
38+
"identifier" : "openAppButton",
39+
"title" : "Open App",
40+
"action" : [:],
41+
]]
42+
]
43+
]
44+
45+
let request = UNNotificationRequest(identifier: "request", content: content, trigger: nil)
46+
let expectation1 = expectation(description: "contentHandler is called")
47+
48+
appExtension.didReceive(request) { (deliveredContent) in
49+
DispatchQueue.main.asyncAfter(deadline: .now() + self.delay, execute: {
50+
UNUserNotificationCenter.current().getNotificationCategories(completionHandler: { (categories) in
51+
let createdCategory = categories.first(where: {$0.identifier == messageId})
52+
XCTAssertNotNil(createdCategory)
53+
XCTAssertEqual(createdCategory!.actions.count, 1, "Number of buttons matched")
54+
XCTAssertTrue(createdCategory!.actions.first!.options.contains(.foreground), "Action is foreground")
55+
expectation1.fulfill()
56+
})
57+
})
58+
}
59+
60+
wait(for: [expectation1], timeout: timeout)
61+
}
62+
63+
}

Tests/notification-extension-tests/NotificationExtensionTests.m

Lines changed: 2 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ @implementation NotificationExtensionTests
2525

2626
- (void)setUp {
2727
[super setUp];
28+
NSSet<UNNotificationCategory *> *categories = [[NSSet alloc] initWithArray:@[]];
29+
[[UNUserNotificationCenter currentNotificationCenter] setNotificationCategories:categories];
2830
self.extension = [[ITBNotificationServiceExtension alloc] init];
2931
}
3032

@@ -306,47 +308,6 @@ - (void)testPushTextInputForegroundButton {
306308
[self waitForExpectations:@[expectation] timeout:IterableNotificationCenterExpectationTimeout];
307309
}
308310

309-
- (void)testPushButtonWithNoType {
310-
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
311-
content.userInfo = @{
312-
@"itbl" : @{
313-
@"messageId": [[NSUUID UUID] UUIDString],
314-
@"actionButtons": @[@{
315-
@"identifier": @"openAppButton",
316-
@"title": @"Open App",
317-
@"action": @{
318-
319-
}
320-
}]
321-
}
322-
};
323-
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:@"request" content:content trigger:nil];
324-
325-
XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"contentHandler is called"];
326-
327-
[self.extension didReceiveNotificationRequest:request withContentHandler:^(UNNotificationContent *contentToDeliver) {
328-
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, IterableNotificationCenterRequestDelay * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
329-
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
330-
[center getNotificationCategoriesWithCompletionHandler:^(NSSet<UNNotificationCategory *> * _Nonnull categories) {
331-
UNNotificationCategory *createdCategory = nil;
332-
for (UNNotificationCategory *category in categories) {
333-
if ([category.identifier isEqualToString:content.userInfo[@"itbl"][@"messageId"]]) {
334-
createdCategory = category;
335-
}
336-
}
337-
XCTAssertNotNil(createdCategory, "Category exists");
338-
339-
XCTAssertEqual(createdCategory.actions.count, 1, "Number of buttons matches");
340-
XCTAssertTrue(createdCategory.actions.firstObject.options & UNNotificationActionOptionForeground, "Action is foreground");
341-
342-
[expectation fulfill];
343-
}];
344-
});
345-
}];
346-
347-
[self waitForExpectations:@[expectation] timeout:IterableNotificationCenterExpectationTimeout];
348-
}
349-
350311
- (void)testPushActionButtons {
351312

352313
}

0 commit comments

Comments
 (0)