Skip to content

Commit 6ac2733

Browse files
authored
Merge pull request #963 from BranchMetrics/SDK-513-Search-Ads-update
SDK-513 search ads update
2 parents fbaf281 + 7846a4a commit 6ac2733

File tree

14 files changed

+559
-172
lines changed

14 files changed

+559
-172
lines changed
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//
2+
// BNCAppleAdClientTests.m
3+
// Branch-SDK-Tests
4+
//
5+
// Created by Ernest Cho on 11/7/19.
6+
// Copyright © 2019 Branch, Inc. All rights reserved.
7+
//
8+
9+
#import <XCTest/XCTest.h>
10+
#import <iAd/iAd.h>
11+
#import "BNCAppleAdClient.h"
12+
13+
// expose private property for testing
14+
@interface BNCAppleAdClient()
15+
16+
@property (nonatomic, strong, readwrite) id adClient;
17+
18+
@end
19+
20+
@interface BNCAppleAdClientTests : XCTestCase
21+
22+
@end
23+
24+
@implementation BNCAppleAdClientTests
25+
26+
- (void)setUp {
27+
// Put setup code here. This method is called before the invocation of each test method in the class.
28+
}
29+
30+
- (void)tearDown {
31+
// Put teardown code here. This method is called after the invocation of each test method in the class.
32+
}
33+
34+
// verifies AdClient loaded via reflection is the sharedClient
35+
- (void)testAdClientLoadsViaReflection {
36+
XCTAssertTrue([ADClient sharedClient] == [BNCAppleAdClient new].adClient);
37+
}
38+
39+
/*
40+
Expected payload varies by simulator or test device. In general, there is a payload of some sort.
41+
42+
This test fails on iOS 10 simulators. Some iPad simulators never respond. Some iPhone simulators return an error.
43+
*/
44+
- (void)testRequestAttribution {
45+
__block XCTestExpectation *expectation = [self expectationWithDescription:@"BNCAppleAdClient"];
46+
47+
BNCAppleAdClient *adClient = [BNCAppleAdClient new];
48+
[adClient requestAttributionDetailsWithBlock:^(NSDictionary<NSString *,NSObject *> * _Nonnull attributionDetails, NSError * _Nonnull error) {
49+
XCTAssertNil(error);
50+
51+
id tmp = [attributionDetails objectForKey:@"Version3.1"];
52+
if ([tmp isKindOfClass:NSDictionary.class]) {
53+
NSDictionary *tmpDict = (NSDictionary *)tmp;
54+
XCTAssertNotNil(tmpDict);
55+
56+
NSNumber *tmpBool = [tmpDict objectForKey:@"iad-attribution"];
57+
XCTAssertNotNil(tmpBool);
58+
} else {
59+
XCTFail(@"Did not find Search Ads attribution");
60+
}
61+
62+
[expectation fulfill];
63+
}];
64+
65+
[self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) {
66+
NSLog(@"%@", error);
67+
}];
68+
}
69+
70+
- (void)testRequestAttribution_Error {
71+
72+
// simulate failure to load by setting adClient to nil
73+
BNCAppleAdClient *adClient = [BNCAppleAdClient new];
74+
adClient.adClient = nil;
75+
76+
__block XCTestExpectation *expectation = [self expectationWithDescription:@"BNCAppleAdClient"];
77+
78+
[adClient requestAttributionDetailsWithBlock:^(NSDictionary<NSString *,NSObject *> * _Nonnull attributionDetails, NSError * _Nonnull error) {
79+
XCTAssertNotNil(error);
80+
XCTAssertTrue([error.localizedFailureReason containsString:@"ADClient is not available"]);
81+
82+
[expectation fulfill];
83+
}];
84+
85+
[self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) {
86+
NSLog(@"%@", error);
87+
}];
88+
}
89+
90+
@end

Branch-SDK-Tests/BNCAppleSearchAdsTests.m

Lines changed: 160 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,57 @@
77
//
88

99
#import <XCTest/XCTest.h>
10+
#import <UIKit/UIKit.h>
11+
#import <iAd/iAd.h>
1012
#import "BNCAppleSearchAds.h"
13+
#import "BNCAppleAdClient.h"
14+
15+
@interface BNCAppleAdClientMock : NSObject <BNCAppleAdClientProtocol>
16+
17+
@property (nonatomic, assign, readwrite) NSInteger ignoreCount;
18+
@property (nonatomic, assign, readwrite) NSInteger numIgnores;
19+
20+
- (void)requestAttributionDetailsWithBlock:(void (^)(NSDictionary<NSString *,NSObject *> * _Nonnull, NSError * _Nonnull))completionHandler;
21+
22+
@end
23+
24+
@implementation BNCAppleAdClientMock
25+
26+
- (instancetype)init {
27+
self = [super init];
28+
if (self) {
29+
self.numIgnores = 0;
30+
self.ignoreCount = 0;
31+
}
32+
return self;
33+
}
34+
35+
- (void)requestAttributionDetailsWithBlock:(void (^)(NSDictionary<NSString *,NSObject *> * _Nonnull, NSError * _Nonnull))completionHandler {
36+
if (self.ignoreCount < self.numIgnores) {
37+
self.ignoreCount++;
38+
return;
39+
}
40+
41+
// Search Ads requires iOS 10+ but the API is iOS 9+
42+
if (@available(iOS 9, *)) {
43+
[[ADClient sharedClient] requestAttributionDetailsWithBlock:completionHandler];
44+
}
45+
}
46+
47+
@end
1148

12-
// expose private methods for unit testing
1349
@interface BNCAppleSearchAds()
1450

51+
// Expose private methods for testing
52+
@property (nonatomic, strong, readwrite) id <BNCAppleAdClientProtocol> adClient;
53+
1554
- (BOOL)isAppleSearchAdSavedToDictionary:(NSDictionary *)appleSearchAdDetails;
1655
- (BOOL)isDateWithinWindow:(NSDate *)installDate;
1756
- (BOOL)isAdClientAvailable;
1857
- (BOOL)isAppleTestData:(NSDictionary *)appleSearchAdDetails;
58+
- (BOOL)isSearchAdsErrorRetryable:(nullable NSError *)error;
59+
60+
- (void)requestAttributionWithMaxAttempts:(NSInteger)maxAttempts completion:(void (^_Nullable)(NSDictionary *__nullable attributionDetails, NSError *__nullable error, NSTimeInterval elapsedSeconds))completion;
1961

2062
- (void)requestAttributionWithCompletion:(void (^_Nullable)(NSDictionary *__nullable attributionDetails, NSError *__nullable error, NSTimeInterval elapsedSeconds))completion;
2163

@@ -37,10 +79,6 @@ - (void)tearDown {
3779

3880
}
3981

40-
- (void)testAdClientIsAvailable {
41-
XCTAssertTrue([self.appleSearchAds isAdClientAvailable]);
42-
}
43-
4482
- (void)testDateIsWithinWindow_DistantPast {
4583
XCTAssertFalse([self.appleSearchAds isDateWithinWindow:[NSDate distantPast]]);
4684
}
@@ -124,10 +162,34 @@ - (void)testIsTestData_NO {
124162
XCTAssertFalse([self.appleSearchAds isAppleTestData:testDataIndicators]);
125163
}
126164

165+
- (void)testIsSearchAdsErrorRetryable_Nil {
166+
XCTAssertFalse([self.appleSearchAds isSearchAdsErrorRetryable:nil]);
167+
}
168+
169+
- (void)testIsSearchAdsErrorRetryable_ADClientErrorUnknown {
170+
NSError *error = [NSError errorWithDomain:@"" code:ADClientErrorUnknown userInfo:nil];
171+
XCTAssertTrue([self.appleSearchAds isSearchAdsErrorRetryable:error]);
172+
}
173+
174+
- (void)testIsSearchAdsErrorRetryable_ADClientErrorLimitAdTracking {
175+
NSError *error = [NSError errorWithDomain:@"" code:ADClientErrorLimitAdTracking userInfo:nil];
176+
XCTAssertFalse([self.appleSearchAds isSearchAdsErrorRetryable:error]);
177+
}
178+
179+
- (void)testIsSearchAdsErrorRetryable_ADClientErrorMissingData {
180+
NSError *error = [NSError errorWithDomain:@"" code:ADClientErrorMissingData userInfo:nil];
181+
XCTAssertTrue([self.appleSearchAds isSearchAdsErrorRetryable:error]);
182+
}
183+
184+
- (void)testIsSearchAdsErrorRetryable_ADClientErrorCorruptResponse {
185+
NSError *error = [NSError errorWithDomain:@"" code:ADClientErrorCorruptResponse userInfo:nil];
186+
XCTAssertTrue([self.appleSearchAds isSearchAdsErrorRetryable:error]);
187+
}
188+
127189
/*
128190
Expected payload varies by simulator or test device. In general, there is a payload of some sort.
129191
130-
This test fails on iOS 10 simulators. iPad simulators never respond. iPhone simulators return an error.
192+
This test fails on iOS 10 simulators. Some iPad simulators never respond. Some iPhone simulators return an error.
131193
*/
132194
- (void)testRequestAppleSearchAds {
133195
__block XCTestExpectation *expectation = [self expectationWithDescription:@"AppleSearchAds"];
@@ -150,4 +212,96 @@ - (void)testRequestAppleSearchAds {
150212
}];
151213
}
152214

215+
// min attempts = 1, so this should ignore the max attempts
216+
- (void)testRequestAppleSearchAdsWithRetry_0 {
217+
__block XCTestExpectation *expectation = [self expectationWithDescription:@"AppleSearchAds"];
218+
219+
[self.appleSearchAds requestAttributionWithMaxAttempts:0 completion:^(NSDictionary * _Nullable attributionDetails, NSError * _Nullable error, NSTimeInterval elapsedSeconds) {
220+
XCTAssertNil(error);
221+
XCTAssertTrue(elapsedSeconds > 0);
222+
223+
NSDictionary *tmpDict = [attributionDetails objectForKey:@"Version3.1"];
224+
XCTAssertNotNil(tmpDict);
225+
226+
NSNumber *tmpBool = [tmpDict objectForKey:@"iad-attribution"];
227+
XCTAssertNotNil(tmpBool);
228+
229+
[expectation fulfill];
230+
}];
231+
232+
[self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) {
233+
NSLog(@"%@", error);
234+
}];
235+
}
236+
237+
// should work as a basic pass through
238+
- (void)testRequestAppleSearchAdsWithRetry_1 {
239+
__block XCTestExpectation *expectation = [self expectationWithDescription:@"AppleSearchAds"];
240+
241+
[self.appleSearchAds requestAttributionWithMaxAttempts:1 completion:^(NSDictionary * _Nullable attributionDetails, NSError * _Nullable error, NSTimeInterval elapsedSeconds) {
242+
XCTAssertNil(error);
243+
XCTAssertTrue(elapsedSeconds > 0);
244+
245+
NSDictionary *tmpDict = [attributionDetails objectForKey:@"Version3.1"];
246+
XCTAssertNotNil(tmpDict);
247+
248+
NSNumber *tmpBool = [tmpDict objectForKey:@"iad-attribution"];
249+
XCTAssertNotNil(tmpBool);
250+
251+
[expectation fulfill];
252+
}];
253+
254+
[self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) {
255+
NSLog(@"%@", error);
256+
}];
257+
}
258+
259+
- (void)testRequestAppleSearchAdsWithRetry_NoResponse {
260+
__block XCTestExpectation *expectation = [self expectationWithDescription:@"AppleSearchAds"];
261+
262+
// Mock the adClient to never respond
263+
self.appleSearchAds.adClient = nil;
264+
265+
[self.appleSearchAds requestAttributionWithMaxAttempts:1 completion:^(NSDictionary * _Nullable attributionDetails, NSError * _Nullable error, NSTimeInterval elapsedSeconds) {
266+
XCTAssertNotNil(error);
267+
XCTAssertTrue(elapsedSeconds > 0);
268+
XCTAssertNil(attributionDetails);
269+
270+
[expectation fulfill];
271+
}];
272+
273+
[self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) {
274+
NSLog(@"%@", error);
275+
}];
276+
}
277+
278+
- (void)testRequestAppleSearchAdsWithRetry_3 {
279+
__block XCTestExpectation *expectation = [self expectationWithDescription:@"AppleSearchAds"];
280+
281+
// Mock the adClient to ignore 2 times
282+
__block BNCAppleAdClientMock *mock = [BNCAppleAdClientMock new];
283+
mock.ignoreCount = 2;
284+
self.appleSearchAds.adClient = mock;
285+
286+
[self.appleSearchAds requestAttributionWithMaxAttempts:3 completion:^(NSDictionary * _Nullable attributionDetails, NSError * _Nullable error, NSTimeInterval elapsedSeconds) {
287+
XCTAssertNil(error);
288+
XCTAssertTrue(elapsedSeconds > 0);
289+
290+
NSDictionary *tmpDict = [attributionDetails objectForKey:@"Version3.1"];
291+
XCTAssertNotNil(tmpDict);
292+
293+
NSNumber *tmpBool = [tmpDict objectForKey:@"iad-attribution"];
294+
XCTAssertNotNil(tmpBool);
295+
296+
// verifies things were ignored
297+
XCTAssert(mock.ignoreCount == 2);
298+
299+
[expectation fulfill];
300+
}];
301+
302+
[self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) {
303+
NSLog(@"%@", error);
304+
}];
305+
}
306+
153307
@end
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//
2+
// BNCAppleAdClient.h
3+
// Branch
4+
//
5+
// Created by Ernest Cho on 11/7/19.
6+
// Copyright © 2019 Branch, Inc. All rights reserved.
7+
//
8+
9+
#import <Foundation/Foundation.h>
10+
11+
NS_ASSUME_NONNULL_BEGIN
12+
13+
// protocol for easier mocking of ADClient behavior in tests
14+
@protocol BNCAppleAdClientProtocol <NSObject>
15+
16+
@required
17+
- (void)requestAttributionDetailsWithBlock:(void (^)(NSDictionary<NSString *,NSObject *> *attributionDetails, NSError *error))completionHandler;
18+
19+
@end
20+
21+
@interface BNCAppleAdClient : NSObject <BNCAppleAdClientProtocol>
22+
23+
- (void)requestAttributionDetailsWithBlock:(void (^)(NSDictionary<NSString *,NSObject *> *attributionDetails, NSError *error))completionHandler;
24+
25+
@end
26+
27+
NS_ASSUME_NONNULL_END
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//
2+
// BNCAppleAdClient.m
3+
// Branch
4+
//
5+
// Created by Ernest Cho on 11/7/19.
6+
// Copyright © 2019 Branch, Inc. All rights reserved.
7+
//
8+
9+
#import "BNCAppleAdClient.h"
10+
#import "NSError+Branch.h"
11+
12+
@interface BNCAppleAdClient()
13+
14+
@property (nonatomic, strong, readwrite) Class adClientClass;
15+
@property (nonatomic, assign, readwrite) SEL adClientSharedClient;
16+
@property (nonatomic, assign, readwrite) SEL adClientRequestAttribution;
17+
18+
@property (nonatomic, strong, readwrite) id adClient;
19+
20+
@end
21+
22+
// ADClient facade that uses reflection to detect and make it available
23+
@implementation BNCAppleAdClient
24+
25+
- (instancetype)init {
26+
self = [super init];
27+
if (self) {
28+
self.adClientClass = NSClassFromString(@"ADClient");
29+
self.adClientSharedClient = NSSelectorFromString(@"sharedClient");
30+
self.adClientRequestAttribution = NSSelectorFromString(@"requestAttributionDetailsWithBlock:");
31+
32+
self.adClient = [self loadAdClient];
33+
}
34+
return self;
35+
}
36+
37+
- (id)loadAdClient {
38+
if ([self isAdClientAvailable]) {
39+
return ((id (*)(id, SEL))[self.adClientClass methodForSelector:self.adClientSharedClient])(self.adClientClass, self.adClientSharedClient);
40+
}
41+
return nil;
42+
}
43+
44+
- (BOOL)isAdClientAvailable {
45+
BOOL ADClientIsAvailable = self.adClientClass &&
46+
[self.adClientClass instancesRespondToSelector:self.adClientRequestAttribution] &&
47+
[self.adClientClass methodForSelector:self.adClientSharedClient];
48+
49+
if (ADClientIsAvailable) {
50+
return YES;
51+
}
52+
return NO;
53+
}
54+
55+
- (void)requestAttributionDetailsWithBlock:(void (^)(NSDictionary<NSString *,NSObject *> *attributionDetails, NSError *error))completionHandler {
56+
if (self.adClient) {
57+
((void (*)(id, SEL, void (^ __nullable)(NSDictionary *__nullable attrDetails, NSError * __nullable error)))
58+
[self.adClient methodForSelector:self.adClientRequestAttribution])
59+
(self.adClient, self.adClientRequestAttribution, completionHandler);
60+
} else {
61+
if (completionHandler) {
62+
completionHandler(nil, [NSError branchErrorWithCode:BNCGeneralError localizedMessage:@"ADClient is not available. Requires iAD.framework and iOS 10+"]);
63+
}
64+
}
65+
}
66+
67+
@end

0 commit comments

Comments
 (0)