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
0 commit comments