mirror of
https://github.com/google/gtm-session-fetcher
synced 2026-11-09 22:25:56 +05:45
2111 lines
97 KiB
Objective-C
2111 lines
97 KiB
Objective-C
/* Copyright 2014 Google Inc. All rights reserved.
|
|
*
|
|
* 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 <TargetConditionals.h>
|
|
|
|
#if !TARGET_OS_WATCH
|
|
|
|
#if !defined(__has_feature) || !__has_feature(objc_arc)
|
|
#error "This file requires ARC support."
|
|
#endif
|
|
|
|
#import <XCTest/XCTest.h>
|
|
#include <stdlib.h>
|
|
#include <sys/sysctl.h>
|
|
#include <unistd.h>
|
|
|
|
#import <GTMSessionFetcher/GTMSessionFetcherService.h>
|
|
#import "GTMSessionFetcherService+Internal.h"
|
|
|
|
#import "GTMSessionFetcherFetchingTest.h"
|
|
#import "GTMSessionFetcherTestServer.h"
|
|
|
|
// Helper macro to create fetcher start/stop notification expectations. These use alloc/init
|
|
// directly to prevent them being waited for by wait helper methods on XCTestCase.
|
|
#define CREATE_START_STOP_NOTIFICATION_EXPECTATIONS(START_COUNT, STOP_COUNT) \
|
|
XCTestExpectation *fetcherStartedExpectation__ = \
|
|
[[XCTNSNotificationExpectation alloc] initWithName:kGTMSessionFetcherStartedNotification]; \
|
|
XCTestExpectation *fetcherStoppedExpectation__ = \
|
|
[[XCTNSNotificationExpectation alloc] initWithName:kGTMSessionFetcherStoppedNotification]; \
|
|
fetcherStartedExpectation__.expectedFulfillmentCount = (START_COUNT); \
|
|
fetcherStartedExpectation__.assertForOverFulfill = YES; \
|
|
fetcherStoppedExpectation__.expectedFulfillmentCount = (STOP_COUNT); \
|
|
fetcherStoppedExpectation__.assertForOverFulfill = YES;
|
|
|
|
// Helper macro to wait on the notification expectations created by the CREATE_START_STOP macro.
|
|
// Using -[XCTestCase waitForExpectations...] methods will NOT wait for them.
|
|
#define WAIT_FOR_START_STOP_NOTIFICATION_EXPECTATIONS() \
|
|
[self waitForExpectations:@[ fetcherStartedExpectation__, fetcherStoppedExpectation__ ] \
|
|
timeout:10.0];
|
|
|
|
static NSDictionary<NSString *, NSString *> *FetcherHeadersWithoutUserAgent(
|
|
GTMSessionFetcher *fetcher) {
|
|
NSMutableDictionary<NSString *, NSString *> *headers =
|
|
[fetcher.request.allHTTPHeaderFields mutableCopy];
|
|
[headers removeObjectForKey:@"User-Agent"];
|
|
return headers;
|
|
}
|
|
|
|
typedef void (^FetcherWillStartBlock)(GTMSessionFetcher *,
|
|
GTMFetcherDecoratorFetcherWillStartCompletionHandler);
|
|
|
|
@interface GTMSessionFetcherTestDecorator : NSObject <GTMFetcherDecoratorProtocol>
|
|
|
|
@property(nonatomic, readonly) FetcherWillStartBlock fetcherWillStartBlock;
|
|
@property(nonatomic, readonly) BOOL synchronous;
|
|
@property(nonatomic, readonly, nullable) NSData *fetchedData;
|
|
@property(nonatomic, readonly, nullable) NSError *fetchError;
|
|
|
|
- (instancetype)init NS_UNAVAILABLE;
|
|
- (instancetype)initWithHeaders:(NSDictionary<NSString *, NSString *> *)headersToAdd;
|
|
- (instancetype)initWithHeadersSynchronous:(NSDictionary<NSString *, NSString *> *)headersToAdd;
|
|
- (instancetype)initWithFetcherWillStartBlock:(FetcherWillStartBlock)fetcherWillStartBlock
|
|
synchronous:(BOOL)synchronous NS_DESIGNATED_INITIALIZER;
|
|
|
|
@end
|
|
|
|
@implementation GTMSessionFetcherTestDecorator
|
|
|
|
@synthesize fetcherWillStartBlock = _fetcherWillStartBlock, synchronous = _synchronous,
|
|
fetchedData = _fetchedData, fetchError = _fetchError;
|
|
|
|
- (instancetype)initWithHeaders:(NSDictionary<NSString *, NSString *> *)headersToAdd {
|
|
return [self
|
|
initWithFetcherWillStartBlock:^(
|
|
GTMSessionFetcher *fetcher,
|
|
GTMFetcherDecoratorFetcherWillStartCompletionHandler completionHandler) {
|
|
NSMutableURLRequest *request = [fetcher.request mutableCopy];
|
|
for (NSString *field in headersToAdd) {
|
|
[request setValue:headersToAdd[field] forHTTPHeaderField:field];
|
|
}
|
|
completionHandler(request, /*error=*/nil);
|
|
}
|
|
synchronous:NO];
|
|
}
|
|
|
|
- (instancetype)initWithHeadersSynchronous:(NSDictionary<NSString *, NSString *> *)headersToAdd {
|
|
return [self
|
|
initWithFetcherWillStartBlock:^(
|
|
GTMSessionFetcher *fetcher,
|
|
GTMFetcherDecoratorFetcherWillStartCompletionHandler completionHandler) {
|
|
NSMutableURLRequest *request = [fetcher.request mutableCopy];
|
|
for (NSString *field in headersToAdd) {
|
|
[request setValue:headersToAdd[field] forHTTPHeaderField:field];
|
|
}
|
|
completionHandler(request, /*error=*/nil);
|
|
}
|
|
synchronous:YES];
|
|
}
|
|
|
|
- (instancetype)initWithFetcherWillStartBlock:(FetcherWillStartBlock)fetcherWillStartBlock
|
|
synchronous:(BOOL)synchronous {
|
|
self = [super init];
|
|
if (self) {
|
|
_fetcherWillStartBlock = fetcherWillStartBlock;
|
|
_synchronous = synchronous;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)fetcherWillStart:(GTMSessionFetcher *)fetcher
|
|
completionHandler:(GTMFetcherDecoratorFetcherWillStartCompletionHandler)handler {
|
|
if (self.synchronous) {
|
|
_fetcherWillStartBlock(fetcher, handler);
|
|
} else {
|
|
__weak __typeof__(self) weakSelf = self;
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
__strong __typeof__(self) strongSelf = weakSelf;
|
|
if (!strongSelf) {
|
|
return;
|
|
}
|
|
strongSelf.fetcherWillStartBlock(fetcher, handler);
|
|
});
|
|
}
|
|
}
|
|
|
|
- (void)fetcherDidFinish:(GTMSessionFetcher *)fetcher
|
|
withData:(nullable NSData *)data
|
|
error:(nullable NSError *)error
|
|
completionHandler:(void (^)(void))handler {
|
|
_fetchedData = [data copy];
|
|
_fetchError = error;
|
|
if (self.synchronous) {
|
|
handler();
|
|
} else {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
handler();
|
|
});
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
@interface GTMSessionFetcherService (GTMSessionFetcherServiceInternal)
|
|
|
|
- (id)delegateDispatcherForFetcher:(GTMSessionFetcher *)fetcher;
|
|
|
|
@end
|
|
|
|
@interface GTMSessionFetcherServiceTestObjectProxy : NSProxy
|
|
@end
|
|
|
|
@implementation GTMSessionFetcherServiceTestObjectProxy {
|
|
id _proxiedObject;
|
|
}
|
|
|
|
+ (instancetype)proxyForObject:(id)object {
|
|
GTMSessionFetcherServiceTestObjectProxy *proxy = [self alloc];
|
|
proxy->_proxiedObject = object;
|
|
return proxy;
|
|
}
|
|
|
|
- (BOOL)isKindOfClass:(Class)aClass {
|
|
return [_proxiedObject isKindOfClass:aClass];
|
|
}
|
|
|
|
- (void)forwardInvocation:(NSInvocation *)invocation {
|
|
[invocation invokeWithTarget:_proxiedObject];
|
|
}
|
|
|
|
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
|
|
return [_proxiedObject methodSignatureForSelector:sel];
|
|
}
|
|
|
|
@end
|
|
|
|
@interface GTMSessionFetcherServiceTest : XCTestCase {
|
|
GTMSessionFetcherTestServer *_testServer;
|
|
BOOL _isServerRunning;
|
|
NSTimeInterval _timeoutInterval;
|
|
}
|
|
|
|
@end
|
|
|
|
// File available in Tests folder.
|
|
static NSString *const kValidFileName = @"gettysburgaddress.txt";
|
|
|
|
@implementation GTMSessionFetcherServiceTest
|
|
|
|
static bool IsCurrentProcessBeingDebugged(void) {
|
|
int result = 0;
|
|
|
|
pid_t pid = getpid();
|
|
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
|
|
u_int mibSize = sizeof(mib) / sizeof(int);
|
|
size_t actualSize;
|
|
|
|
if (sysctl(mib, mibSize, NULL, &actualSize, NULL, 0) == 0) {
|
|
if (actualSize >= sizeof(struct kinfo_proc)) {
|
|
struct kinfo_proc *info = (struct kinfo_proc *)malloc(actualSize);
|
|
|
|
if (info) {
|
|
// This comes from looking at the Darwin xnu Kernel
|
|
if (sysctl(mib, mibSize, info, &actualSize, NULL, 0) == 0)
|
|
result = (info->kp_proc.p_flag & P_TRACED) ? 1 : 0;
|
|
|
|
free(info);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
- (void)setUp {
|
|
_testServer = [[GTMSessionFetcherTestServer alloc] init];
|
|
_isServerRunning = _testServer != nil;
|
|
_timeoutInterval = IsCurrentProcessBeingDebugged() ? 3600.0 : 30.0;
|
|
|
|
XCTAssertTrue(_isServerRunning,
|
|
@">>> http test server failed to launch; skipping service tests\n");
|
|
|
|
[super setUp];
|
|
}
|
|
|
|
- (void)tearDown {
|
|
_testServer = nil;
|
|
_isServerRunning = NO;
|
|
|
|
[super tearDown];
|
|
}
|
|
|
|
- (void)testFetcherService {
|
|
if (!_isServerRunning) return;
|
|
|
|
// Utility blocks for counting array entries for a specific host.
|
|
NSUInteger (^URLsPerHost)(NSArray *, NSString *) = ^(NSArray *URLs, NSString *host) {
|
|
NSUInteger counter = 0;
|
|
for (NSURL *url in URLs) {
|
|
if ([host isEqual:url.host]) {
|
|
counter++;
|
|
}
|
|
}
|
|
return counter;
|
|
};
|
|
|
|
NSUInteger (^FetchersPerHost)(NSArray *, NSString *) = ^(NSArray *fetchers, NSString *host) {
|
|
NSArray *fetcherURLs = [fetchers valueForKeyPath:@"request.URL"];
|
|
return URLsPerHost(fetcherURLs, host);
|
|
};
|
|
|
|
// Utility block for finding the minimum priority fetcher for a specific host.
|
|
NSInteger (^PriorityPerHost)(NSArray *, NSString *) = ^(NSArray *fetchers, NSString *host) {
|
|
NSInteger val = NSIntegerMax;
|
|
for (GTMSessionFetcher *fetcher in fetchers) {
|
|
if ([host isEqual:fetcher.request.URL.host]) {
|
|
val = MIN(val, fetcher.servicePriority);
|
|
}
|
|
}
|
|
return val;
|
|
};
|
|
|
|
// We'll verify we fetched from the server the same data that is on disk.
|
|
NSData *gettysburgAddress = [_testServer documentDataAtPath:kValidFileName];
|
|
|
|
// We'll create several fetchers. Only 2 should run simultaneously (per host).
|
|
// 1 should fail; a few will be stopped the rest should succeeed.
|
|
const NSUInteger kMaxRunningFetchersPerHost = 2;
|
|
|
|
NSString *const kUserAgent = @"ServiceTest-UA";
|
|
|
|
GTMSessionFetcherService *service = [[GTMSessionFetcherService alloc] init];
|
|
service.maxRunningFetchersPerHost = kMaxRunningFetchersPerHost;
|
|
service.userAgent = kUserAgent;
|
|
service.allowLocalhostRequest = YES;
|
|
|
|
// Make URLs for a valid fetch, a fetch that returns a status error,
|
|
// and a valid fetch with a different host.
|
|
NSURL *validFileURL = [_testServer localURLForFile:kValidFileName];
|
|
|
|
NSString *invalidFile = [kValidFileName stringByAppendingString:@"?status=400"];
|
|
NSURL *invalidFileURL = [_testServer localURLForFile:invalidFile];
|
|
|
|
NSString *stopSilentFile = [kValidFileName stringByAppendingString:@"?stop=silent"];
|
|
NSURL *stopSilentFileURL = [_testServer localURLForFile:stopSilentFile];
|
|
|
|
NSString *stopCallbackFile = [kValidFileName stringByAppendingString:@"?stop=callback"];
|
|
NSURL *stopCallbackFileURL = [_testServer localURLForFile:stopCallbackFile];
|
|
|
|
NSURL *altValidURL = [_testServer localv6URLForFile:invalidFile];
|
|
|
|
XCTAssertEqualObjects(validFileURL.host, @"localhost", @"unexpected host");
|
|
XCTAssertEqualObjects(invalidFileURL.host, @"localhost", @"unexpected host");
|
|
XCTAssertEqualObjects(stopSilentFileURL.host, @"localhost", @"unexpected host");
|
|
XCTAssertEqualObjects(stopCallbackFileURL.host, @"localhost", @"unexpected host");
|
|
XCTAssertEqualObjects(altValidURL.host, @"::1", @"unexpected host");
|
|
|
|
// Make an array with the urls from the different hosts, including one
|
|
// that will fail with a status 400 error.
|
|
NSMutableArray *urlArray = [NSMutableArray array];
|
|
for (int idx = 1; idx <= 4; idx++) [urlArray addObject:validFileURL];
|
|
[urlArray addObject:invalidFileURL];
|
|
// Ensure things will get queued due to the per host limit.
|
|
XCTAssertGreaterThan(urlArray.count, kMaxRunningFetchersPerHost);
|
|
for (int idx = 1; idx <= 5; idx++) [urlArray addObject:validFileURL];
|
|
for (int idx = 1; idx <= 3; idx++) [urlArray addObject:stopSilentFileURL];
|
|
for (int idx = 1; idx <= 5; idx++) [urlArray addObject:altValidURL];
|
|
for (int idx = 1; idx <= 3; idx++) [urlArray addObject:stopCallbackFileURL];
|
|
for (int idx = 1; idx <= 5; idx++) [urlArray addObject:validFileURL];
|
|
for (int idx = 1; idx <= 2; idx++) [urlArray addObject:stopCallbackFileURL];
|
|
for (int idx = 1; idx <= 2; idx++) [urlArray addObject:stopSilentFileURL];
|
|
NSUInteger totalNumberOfFetchers = urlArray.count;
|
|
|
|
// These are used to track the fetchers via notifications.
|
|
__block NSMutableArray *pending = [NSMutableArray array];
|
|
__block NSMutableArray *running = [NSMutableArray array];
|
|
__block NSMutableArray *completed = [NSMutableArray array];
|
|
// Fetchers that will get stopped.
|
|
NSMutableArray *stopped = [NSMutableArray array];
|
|
|
|
NSInteger priorityVal = 0;
|
|
|
|
// Create all the fetchers.
|
|
NSMutableArray *fetchersInFlight = [NSMutableArray array];
|
|
NSMutableArray *observers = [NSMutableArray array];
|
|
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
|
|
NSMutableArray *expectations = [NSMutableArray array];
|
|
for (NSURL *fileURL in urlArray) {
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURL:fileURL];
|
|
XCTestExpectation *expectation =
|
|
[self expectationWithDescription:(id _Nonnull)fileURL.absoluteString];
|
|
[expectations addObject:expectation];
|
|
|
|
// Fetcher start notification.
|
|
id startObserver = [nc
|
|
addObserverForName:kGTMSessionFetcherStartedNotification
|
|
object:fetcher
|
|
queue:nil
|
|
usingBlock:^(NSNotification *note) {
|
|
XCTAssertTrue([pending containsObject:fetcher]);
|
|
|
|
// Verify that we have at most two fetchers running for this fetcher's host.
|
|
[running addObject:fetcher];
|
|
[pending removeObject:fetcher];
|
|
|
|
NSURLRequest *fetcherReq = fetcher.request;
|
|
NSURL *fetcherReqURL = fetcherReq.URL;
|
|
|
|
NSString *host = fetcherReqURL.host;
|
|
NSUInteger numberRunning = FetchersPerHost(running, host);
|
|
XCTAssertTrue(numberRunning > 0, @"count error");
|
|
XCTAssertTrue(numberRunning <= kMaxRunningFetchersPerHost, @"too many running");
|
|
|
|
NSInteger pendingPriority = PriorityPerHost(pending, host);
|
|
XCTAssertTrue(fetcher.servicePriority <= pendingPriority,
|
|
@"a pending fetcher has greater priority");
|
|
|
|
XCTAssert([service.issuedFetchers containsObject:fetcher], @"%@", fetcher);
|
|
|
|
NSArray *matches = [service issuedFetchersWithRequestURL:fetcherReqURL];
|
|
NSUInteger idx = NSNotFound;
|
|
if (matches) {
|
|
idx = [matches indexOfObjectIdenticalTo:fetcher];
|
|
}
|
|
XCTAssertTrue(idx != NSNotFound, @"Missing %@ in %@", fetcherReqURL, matches);
|
|
NSURL *fakeURL = [NSURL URLWithString:@"http://example.com/bad"];
|
|
matches = [service issuedFetchersWithRequestURL:fakeURL];
|
|
XCTAssertEqual(matches.count, (NSUInteger)0);
|
|
|
|
NSString *agent = [fetcherReq valueForHTTPHeaderField:@"User-Agent"];
|
|
XCTAssertEqualObjects(agent, kUserAgent);
|
|
}];
|
|
[observers addObject:startObserver];
|
|
|
|
// Fetcher stopped notification.
|
|
id stopObserver =
|
|
[nc addObserverForName:kGTMSessionFetcherStoppedNotification
|
|
object:fetcher
|
|
queue:nil
|
|
usingBlock:^(NSNotification *note) {
|
|
XCTAssertTrue([running containsObject:fetcher]);
|
|
|
|
// Verify that we only have two fetchers running.
|
|
[completed addObject:fetcher];
|
|
[running removeObject:fetcher];
|
|
|
|
// A fetcher that gets stopped should not trigger any notifications.
|
|
NSURL *fetcherReqURL = fetcher.request.URL;
|
|
XCTAssertFalse([fetcherReqURL.query hasPrefix:@"stop="]);
|
|
|
|
NSString *host = fetcherReqURL.host;
|
|
NSUInteger numberRunning = FetchersPerHost(running, host);
|
|
NSUInteger numberPending = FetchersPerHost(pending, host);
|
|
NSUInteger numberCompleted = FetchersPerHost(completed, host);
|
|
NSUInteger numberStopped = FetchersPerHost(stopped, host);
|
|
|
|
XCTAssertLessThanOrEqual(numberRunning, kMaxRunningFetchersPerHost,
|
|
@"too many running");
|
|
XCTAssertLessThanOrEqual(
|
|
numberPending + numberRunning + numberCompleted + numberStopped,
|
|
URLsPerHost(urlArray, host),
|
|
@"%d issued running (pending:%u running:%u completed:%u stopped: %u)",
|
|
(unsigned int)totalNumberOfFetchers, (unsigned int)numberPending,
|
|
(unsigned int)numberRunning, (unsigned int)numberCompleted,
|
|
(unsigned int)numberStopped);
|
|
|
|
// The stop notification may be posted on the main thread before or after the
|
|
// fetcher service has been notified the fetcher has stopped.
|
|
}];
|
|
[observers addObject:stopObserver];
|
|
|
|
// Set the fetch priority to a value that cycles 0, 1, -1, 0, ...
|
|
priorityVal++;
|
|
if (priorityVal > 1) priorityVal = -1;
|
|
fetcher.servicePriority = priorityVal;
|
|
|
|
BOOL stopFetch = [fileURL.query hasPrefix:@"stop="];
|
|
BOOL stopWithCallback = [fileURL.query isEqual:@"stop=callback"];
|
|
if (stopWithCallback) {
|
|
fetcher.stopFetchingTriggersCompletionHandler = YES;
|
|
}
|
|
|
|
if (stopFetch) {
|
|
// All the fetchers that will be stopped should end up queued, as we're
|
|
// also testing that they are handled correctly for callbacks and the
|
|
// general per host limits. So make sure we've got atleast the max per
|
|
// host pending already.
|
|
NSUInteger numberPending = FetchersPerHost(pending, fetcher.request.URL.host);
|
|
XCTAssertGreaterThanOrEqual(numberPending, kMaxRunningFetchersPerHost);
|
|
} else {
|
|
[pending addObject:fetcher];
|
|
}
|
|
|
|
// Start this fetcher.
|
|
[fetchersInFlight addObject:fetcher];
|
|
[fetcher beginFetchWithCompletionHandler:^(NSData *fetchData, NSError *fetchError) {
|
|
// Callback.
|
|
XCTAssert([fetchersInFlight containsObject:fetcher]);
|
|
[fetchersInFlight removeObjectIdenticalTo:fetcher];
|
|
|
|
// The query should be empty except for the URL with a status code or the ones
|
|
// marked for stopping.
|
|
NSString *query = [fetcher.request.URL query];
|
|
BOOL isValidRequest = (query.length == 0);
|
|
if (isValidRequest) {
|
|
XCTAssertEqualObjects(fetchData, gettysburgAddress, @"Bad fetch data");
|
|
XCTAssertNil(fetchError, @"unexpected %@ %@", fetchError, fetchError.userInfo);
|
|
} else {
|
|
if ([query isEqual:@"stop=callback"]) {
|
|
XCTAssertEqualObjects(fetchError.domain, kGTMSessionFetcherErrorDomain,
|
|
@"expected error");
|
|
XCTAssertEqual(fetchError.code, GTMSessionFetcherErrorUserCancelled, @"expected error");
|
|
} else if ([query hasPrefix:@"stop="]) {
|
|
XCTFail(@"Should not have gotten completion for stopped call");
|
|
} else {
|
|
// This is the query with ?status=400.
|
|
XCTAssertEqual(fetchError.code, (NSInteger)400, @"expected error");
|
|
}
|
|
}
|
|
[expectation fulfill];
|
|
}];
|
|
if (stopFetch) {
|
|
[fetcher stopFetching];
|
|
[stopped addObject:fetcher];
|
|
if (stopWithCallback) {
|
|
// The completion will remove it from inflight and complete the expectation.
|
|
} else {
|
|
[fetchersInFlight removeObject:fetcher];
|
|
[expectation fulfill];
|
|
}
|
|
}
|
|
}
|
|
|
|
[self waitForExpectations:expectations timeout:_timeoutInterval];
|
|
|
|
// Check the notification counts.
|
|
XCTAssertEqual(pending.count, (NSUInteger)0, @"still pending: %@", pending);
|
|
XCTAssertEqual(running.count, (NSUInteger)0, @"still running: %@", running);
|
|
XCTAssertEqual(completed.count + stopped.count, (NSUInteger)totalNumberOfFetchers, @"incomplete");
|
|
// All the expected callbacks happened.
|
|
XCTAssertEqual(fetchersInFlight.count, (NSUInteger)0, @"Uncompleted: %@", fetchersInFlight);
|
|
|
|
XCTAssertEqual([service numberOfFetchers], (NSUInteger)0, @"service non-empty");
|
|
|
|
for (id observer in observers) {
|
|
[[NSNotificationCenter defaultCenter] removeObserver:observer];
|
|
}
|
|
}
|
|
|
|
- (void)testConcurrentCallbackQueue {
|
|
if (!_isServerRunning) return;
|
|
|
|
GTMSessionFetcherService *service = [[GTMSessionFetcherService alloc] init];
|
|
service.allowLocalhostRequest = YES;
|
|
[service setConcurrentCallbackQueue:dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)];
|
|
|
|
XCTestExpectation *progressExpectation = [self expectationWithDescription:@"progress block"];
|
|
XCTestExpectation *completionExpectation = [self expectationWithDescription:@"completion"];
|
|
|
|
NSURL *fileURL = [_testServer localURLForFile:kValidFileName];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURL:fileURL];
|
|
fetcher.receivedProgressBlock = ^(int64_t bytesReceived, int64_t totalBytesReceived) {
|
|
// Sleep for a time before fulfilling the expectation.
|
|
sleep(1);
|
|
[progressExpectation fulfill];
|
|
};
|
|
|
|
CREATE_START_STOP_NOTIFICATION_EXPECTATIONS(1, 1);
|
|
[fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
|
|
[completionExpectation fulfill];
|
|
}];
|
|
|
|
// The progress block should be executed prior to the completion block, even though
|
|
// they will be dispatched at essentially the same time; if they are executing on
|
|
// the global concurrent queue, the completion will fulfill its expectation first
|
|
// due to the sleep in the progress block.
|
|
[self waitForExpectations:@[ progressExpectation, completionExpectation ]
|
|
timeout:_timeoutInterval
|
|
enforceOrder:YES];
|
|
|
|
WAIT_FOR_START_STOP_NOTIFICATION_EXPECTATIONS();
|
|
}
|
|
|
|
- (void)testStopAllFetchers {
|
|
[self stopAllFetchersHelperUseStopAllAPI:YES callbacksAfterStop:NO];
|
|
}
|
|
|
|
- (void)testStopAllFetchersSeparately {
|
|
[self stopAllFetchersHelperUseStopAllAPI:NO callbacksAfterStop:NO];
|
|
}
|
|
|
|
- (void)testStopAllFetchersWithCallbacks {
|
|
[self stopAllFetchersHelperUseStopAllAPI:YES callbacksAfterStop:YES];
|
|
}
|
|
|
|
- (void)testStopAllFetchersSeparatelyWithCallbacks {
|
|
[self stopAllFetchersHelperUseStopAllAPI:NO callbacksAfterStop:YES];
|
|
}
|
|
|
|
- (void)stopAllFetchersHelperUseStopAllAPI:(BOOL)useStopAllAPI
|
|
callbacksAfterStop:(BOOL)doStopCallbacks {
|
|
if (!_isServerRunning) return;
|
|
|
|
GTMSessionFetcherService *service = [[GTMSessionFetcherService alloc] init];
|
|
service.maxRunningFetchersPerHost = 2;
|
|
service.allowLocalhostRequest = YES;
|
|
|
|
// Create three fetchers for each of two URLs, so there should be
|
|
// two running and one delayed for each.
|
|
NSURL *validFileURL = [_testServer localURLForFile:kValidFileName];
|
|
NSURL *altValidURL = [_testServer localv6URLForFile:kValidFileName];
|
|
|
|
// Add three fetches for each URL.
|
|
NSArray *urlArray =
|
|
@[ validFileURL, altValidURL, validFileURL, altValidURL, validFileURL, altValidURL ];
|
|
|
|
// Expect two started/stopped fetchers for each host, for 4 total.
|
|
CREATE_START_STOP_NOTIFICATION_EXPECTATIONS(4, 4);
|
|
|
|
XCTestExpectation *fetcherCallbackExpectation =
|
|
[[XCTNSNotificationExpectation alloc] initWithName:@"callback"];
|
|
if (doStopCallbacks) {
|
|
fetcherCallbackExpectation.expectedFulfillmentCount = urlArray.count;
|
|
fetcherCallbackExpectation.assertForOverFulfill = YES;
|
|
} else {
|
|
// No callbacks, just complete this expectation now.
|
|
[fetcherCallbackExpectation fulfill];
|
|
}
|
|
|
|
// Create and start all the fetchers.
|
|
for (NSURL *fileURL in urlArray) {
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURL:fileURL];
|
|
fetcher.stopFetchingTriggersCompletionHandler = doStopCallbacks;
|
|
[fetcher beginFetchWithCompletionHandler:^(NSData *fetchData, NSError *fetchError) {
|
|
if (doStopCallbacks) {
|
|
// Since they are getting stopped, they should all produce errors, not data.
|
|
XCTAssertNil(fetchData);
|
|
XCTAssertNotNil(fetchError);
|
|
[fetcherCallbackExpectation fulfill];
|
|
} else {
|
|
// We shouldn't reach any of the callbacks.
|
|
XCTFail(@"Fetcher completed but should have been stopped");
|
|
}
|
|
}];
|
|
}
|
|
|
|
// Two hosts.
|
|
XCTAssertEqual(service.runningFetchersByHost.count, (NSUInteger)2, @"hosts running");
|
|
XCTAssertEqual(service.delayedFetchersByHost.count, (NSUInteger)2, @"hosts delayed");
|
|
|
|
// We should see two fetchers running and one delayed for each host.
|
|
NSArray *localhosts = [service.runningFetchersByHost objectForKey:@"localhost"];
|
|
XCTAssertEqual(localhosts.count, (NSUInteger)2, @"hosts running");
|
|
NSArray *localv6hosts = [service.runningFetchersByHost objectForKey:@"::1"];
|
|
XCTAssertEqual(localv6hosts.count, (NSUInteger)2, @"hosts running");
|
|
|
|
localhosts = [service.delayedFetchersByHost objectForKey:@"localhost"];
|
|
XCTAssertEqual(localhosts.count, (NSUInteger)1, @"hosts delayed");
|
|
localv6hosts = [service.delayedFetchersByHost objectForKey:@"::1"];
|
|
XCTAssertEqual(localv6hosts.count, (NSUInteger)1, @"hosts delayed");
|
|
|
|
XCTAssertNil(service.stoppedAllFetchersDate);
|
|
|
|
if (useStopAllAPI) {
|
|
[service stopAllFetchers];
|
|
} else {
|
|
// Stop the delayed fetchers first so a delayed one doesn't get started when canceling
|
|
// a running one.
|
|
NSArray *delayedFetchersByHost = service.delayedFetchersByHost.allValues;
|
|
NSArray *runningFetchersByHost = service.runningFetchersByHost.allValues;
|
|
for (NSArray *delayedForHost in delayedFetchersByHost) {
|
|
NSArray *delayed = [delayedForHost copy];
|
|
for (GTMSessionFetcher *fetcher in delayed) {
|
|
[fetcher stopFetching];
|
|
}
|
|
}
|
|
for (NSArray *runningForHost in runningFetchersByHost) {
|
|
NSArray *running = [runningForHost copy];
|
|
for (GTMSessionFetcher *fetcher in running) {
|
|
[fetcher stopFetching];
|
|
}
|
|
}
|
|
}
|
|
|
|
[self waitForExpectations:@[
|
|
fetcherCallbackExpectation, fetcherStartedExpectation__, fetcherStoppedExpectation__
|
|
]
|
|
timeout:10.0];
|
|
|
|
XCTAssertEqual(service.runningFetchersByHost.count, (NSUInteger)0, @"hosts running");
|
|
XCTAssertEqual(service.delayedFetchersByHost.count, (NSUInteger)0, @"hosts delayed");
|
|
|
|
if (useStopAllAPI) {
|
|
XCTAssertNotNil(service.stoppedAllFetchersDate);
|
|
} else {
|
|
XCTAssertNil(service.stoppedAllFetchersDate);
|
|
}
|
|
}
|
|
|
|
- (void)testSessionReuse {
|
|
if (!_isServerRunning) return;
|
|
|
|
GTMSessionFetcherService *service = [[GTMSessionFetcherService alloc] init];
|
|
service.allowLocalhostRequest = YES;
|
|
|
|
const NSTimeInterval kUnusedSessionTimeout = 3.0;
|
|
service.unusedSessionTimeout = kUnusedSessionTimeout;
|
|
|
|
NSURL *validFileURL = [_testServer localURLForFile:kValidFileName];
|
|
|
|
NSArray *urlArray = @[ validFileURL, validFileURL, validFileURL, validFileURL ];
|
|
NSMutableSet *uniqueSessions = [NSMutableSet set];
|
|
NSMutableSet *uniqueTasks = [NSMutableSet set];
|
|
|
|
// Creating tasks for the array twice, so double expected counts.
|
|
CREATE_START_STOP_NOTIFICATION_EXPECTATIONS(urlArray.count * 2, urlArray.count * 2);
|
|
|
|
//
|
|
// Create and start all the fetchers without reusing the session.
|
|
//
|
|
service.reuseSession = NO;
|
|
XCTestExpectation *completionExpectation =
|
|
[self expectationWithDescription:@"non-reuse completion"];
|
|
completionExpectation.expectedFulfillmentCount = urlArray.count;
|
|
for (NSURL *fileURL in urlArray) {
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURL:fileURL];
|
|
[fetcher beginFetchWithCompletionHandler:^(NSData *fetchData, NSError *fetchError) {
|
|
XCTAssertNotNil(fetchData);
|
|
XCTAssertNil(fetchError);
|
|
[completionExpectation fulfill];
|
|
}];
|
|
[uniqueSessions addObject:[NSValue valueWithNonretainedObject:fetcher.session]];
|
|
[uniqueTasks addObject:[NSValue valueWithNonretainedObject:fetcher.sessionTask]];
|
|
|
|
XCTAssertEqual(fetcher.session.delegate, fetcher);
|
|
}
|
|
|
|
[self waitForExpectations:@[ completionExpectation ] timeout:_timeoutInterval];
|
|
|
|
// We should have one unique session per fetcher.
|
|
XCTAssertEqual(uniqueTasks.count, urlArray.count);
|
|
XCTAssertEqual(uniqueSessions.count, urlArray.count, @"%@", uniqueSessions);
|
|
XCTAssertNil([service session]);
|
|
XCTAssertNil([service sessionDelegate]);
|
|
|
|
// Inside the delegate dispatcher, there should now be a nil map of tasks to fetchers.
|
|
NSDictionary *taskMap = [(id)service.sessionDelegate valueForKey:@"taskToFetcherMap"];
|
|
XCTAssertNil(taskMap);
|
|
|
|
//
|
|
// Now reuse the session for multiple fetches.
|
|
//
|
|
[uniqueSessions removeAllObjects];
|
|
[uniqueTasks removeAllObjects];
|
|
[service resetSession];
|
|
|
|
service.reuseSession = YES;
|
|
completionExpectation = [self expectationWithDescription:@"reuse completion"];
|
|
completionExpectation.expectedFulfillmentCount = urlArray.count;
|
|
for (NSURL *fileURL in urlArray) {
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURL:fileURL];
|
|
[fetcher beginFetchWithCompletionHandler:^(NSData *fetchData, NSError *fetchError) {
|
|
XCTAssertNotNil(fetchData);
|
|
XCTAssertNil(fetchError);
|
|
[completionExpectation fulfill];
|
|
}];
|
|
[uniqueSessions addObject:[NSValue valueWithNonretainedObject:fetcher.session]];
|
|
[uniqueTasks addObject:[NSValue valueWithNonretainedObject:fetcher.sessionTask]];
|
|
|
|
XCTAssertEqual(fetcher.session.delegate, service.sessionDelegate);
|
|
}
|
|
|
|
[self waitForExpectations:@[ completionExpectation ] timeout:_timeoutInterval];
|
|
|
|
// We should have used two sessions total.
|
|
XCTAssertEqual(uniqueTasks.count, urlArray.count);
|
|
XCTAssertEqual(uniqueSessions.count, (NSUInteger)1, @"%@", uniqueSessions);
|
|
|
|
// Inside the delegate dispatcher, there should be an empty map of tasks to fetchers.
|
|
taskMap = [(id)service.sessionDelegate valueForKey:@"taskToFetcherMap"];
|
|
XCTAssertEqualObjects(taskMap, @{});
|
|
|
|
// Because we set kUnusedSessionDiscardInterval to 3 seconds earlier, there
|
|
// should still be a remembered session immediately after the fetches finish.
|
|
NSURLSession *session = [service session];
|
|
XCTAssertNotNil(session);
|
|
|
|
// Wait up to 5 seconds for the sessions to become invalid.
|
|
XCTestExpectation *exp =
|
|
[self expectationForNotification:kGTMSessionFetcherServiceSessionBecameInvalidNotification
|
|
object:service
|
|
handler:^(NSNotification *notification) {
|
|
NSURLSession *invalidSession = [notification.userInfo
|
|
objectForKey:kGTMSessionFetcherServiceSessionKey];
|
|
XCTAssertEqualObjects(invalidSession, session);
|
|
return YES;
|
|
}];
|
|
|
|
[self waitForExpectations:@[ exp ] timeout:_timeoutInterval];
|
|
|
|
// Ensure all started fetchers have stopped.
|
|
WAIT_FOR_START_STOP_NOTIFICATION_EXPECTATIONS();
|
|
|
|
// Unlike right after the fetches finish, now the session should be nil.
|
|
XCTAssertNil(service.session);
|
|
}
|
|
|
|
- (void)testSessionAbandonment {
|
|
if (!_isServerRunning) return;
|
|
|
|
GTMSessionFetcherService *service = [[GTMSessionFetcherService alloc] init];
|
|
service.allowLocalhostRequest = YES;
|
|
service.reuseSession = YES;
|
|
service.maxRunningFetchersPerHost = 2;
|
|
|
|
// A race condition exists that was exposed by changing the service's sessionDelegateQueue
|
|
// from the mainQueue to a serial background queue. Because of that change, this race condition
|
|
// began to manifest as a flake in this test.
|
|
//
|
|
// Inside beginFetchWithCompletionHandler:, the fetcher gets the NSURLSession from the fetcher
|
|
// service before creating its NSURLSessionTask. The locks protecting the NSURLSession within
|
|
// the service only protect it during that getter call, and it's possible that before the
|
|
// fetcher's NSURLSessionTask is created another thread could come along and reset the
|
|
// NSURLSession, invalidating it and preventing new NSURLSessionTasks from being created on it.
|
|
//
|
|
// Migrating from the main to a background queue for the session's delegate queue made it
|
|
// possible for this test to encounter this race condition, as the first fetcher's completion
|
|
// handler was never be called before all fetchers were created and the test waited for all
|
|
// fetchers to be complete.
|
|
//
|
|
// This race condition is similar to another one known in the service's delegate dispatcher,
|
|
// and the most sure way to fix is likely to refactor to require every fetcher is created from
|
|
// a service, letting the service protect the entire process of creating the NSURLSession and
|
|
// the session tasks.
|
|
//
|
|
// Until then, stopping this test from flaking by setting the session delegate queue to
|
|
// be the main queue.
|
|
service.sessionDelegateQueue = [NSOperationQueue mainQueue];
|
|
|
|
const NSTimeInterval kUnusedSessionTimeout = 3.0;
|
|
service.unusedSessionTimeout = kUnusedSessionTimeout;
|
|
|
|
XCTestExpectation *expectation = [self expectationWithDescription:@"fetcher completions"];
|
|
|
|
NSURL *validFileURL = [_testServer localURLForFile:kValidFileName];
|
|
NSArray *urlArray = @[ validFileURL, validFileURL, validFileURL, validFileURL ];
|
|
[expectation setExpectedFulfillmentCount:urlArray.count];
|
|
|
|
CREATE_START_STOP_NOTIFICATION_EXPECTATIONS(urlArray.count, urlArray.count);
|
|
|
|
__block int numberOfCallsBack = 0;
|
|
__block int numberOfErrors = 0;
|
|
|
|
for (NSURL *fileURL in urlArray) {
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURL:fileURL];
|
|
[fetcher beginFetchWithCompletionHandler:^(NSData *fetchData, NSError *fetchError) {
|
|
if (fetchError != nil) {
|
|
++numberOfErrors;
|
|
}
|
|
|
|
// If NSURLSession had a suspended task, it won't have called its delegate,
|
|
// so the fetcher will have manufactured a callback with a cancellation error.
|
|
XCTAssert((fetchData != nil && fetchError == nil) ||
|
|
(fetchData == nil && fetchError.code == NSURLErrorCancelled),
|
|
@"error=%@, data=%@", fetchError, fetchData);
|
|
|
|
// On the first completion, we'll reset the session.
|
|
++numberOfCallsBack;
|
|
if (numberOfCallsBack == 1) {
|
|
[service resetSession];
|
|
|
|
// Inside the delegate dispatcher, there should be a nil map of tasks to fetchers.
|
|
NSDictionary *taskMap = [(id)service.sessionDelegate valueForKey:@"taskToFetcherMap"];
|
|
XCTAssertNil(taskMap);
|
|
}
|
|
|
|
[expectation fulfill];
|
|
}];
|
|
}
|
|
[self waitForExpectations:@[ expectation ] timeout:_timeoutInterval];
|
|
|
|
// Here we verify that all fetchers were called back.
|
|
XCTAssertEqual(numberOfCallsBack, (int)urlArray.count);
|
|
|
|
// On some builds (Mac/iOS and certain machines), all are succeeding; on some,
|
|
// one finishes with an error, apparently a task ending up suspended when we
|
|
// reset the session. This may resolve as all builds migrate to a common version of
|
|
// NSURLSession; if not, we should try to figure out why this is inconsistent.
|
|
// On the simulator, all are succeeding.
|
|
XCTAssertLessThanOrEqual(numberOfErrors, 1);
|
|
|
|
WAIT_FOR_START_STOP_NOTIFICATION_EXPECTATIONS();
|
|
}
|
|
|
|
- (void)testThreadingStress {
|
|
if (!_isServerRunning) return;
|
|
|
|
// We'll create and start a lot of fetchers on three different queues.
|
|
dispatch_queue_t mainQueue = dispatch_get_main_queue();
|
|
dispatch_queue_t bgParallel = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
|
|
dispatch_queue_t bgSerial = dispatch_queue_create("com.example.bgSerial", DISPATCH_QUEUE_SERIAL);
|
|
NSArray *queues = @[ mainQueue, bgParallel, bgSerial ];
|
|
|
|
GTMSessionFetcherService *service = [[GTMSessionFetcherService alloc] init];
|
|
service.allowLocalhostRequest = YES;
|
|
|
|
NSURL *validFileURL = [_testServer localURLForFile:kValidFileName];
|
|
|
|
__block int completedFetchCounter = 0;
|
|
int const kNumberOfFetchersToCreate = 100;
|
|
|
|
CREATE_START_STOP_NOTIFICATION_EXPECTATIONS(kNumberOfFetchersToCreate, kNumberOfFetchersToCreate);
|
|
|
|
// Keep track of fetch failures that are probably due to our test server
|
|
// failing to keep up.
|
|
NSMutableIndexSet *overloadIndexes = [NSMutableIndexSet indexSet];
|
|
|
|
for (NSUInteger index = 0; index < kNumberOfFetchersToCreate; index++) {
|
|
NSString *desc = [NSString stringWithFormat:@"Fetcher %lu", (unsigned long)index];
|
|
XCTestExpectation *expectation = [self expectationWithDescription:desc];
|
|
|
|
dispatch_queue_t queue = queues[index % queues.count];
|
|
dispatch_async(queue, ^{
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURL:validFileURL];
|
|
fetcher.callbackQueue = queues[(index / queues.count) % queues.count]; // epicycle of queues
|
|
|
|
[fetcher beginFetchWithCompletionHandler:^(NSData *fetchData, NSError *fetchError) {
|
|
@synchronized(self) {
|
|
++completedFetchCounter;
|
|
[expectation fulfill];
|
|
|
|
BOOL isServerOverload = (fetchError.code == EINVAL) ||
|
|
([fetchError.domain isEqualToString:NSURLErrorDomain] &&
|
|
(fetchError.code == NSURLErrorTimedOut ||
|
|
fetchError.code == NSURLErrorCannotConnectToHost ||
|
|
fetchError.code == NSURLErrorNetworkConnectionLost));
|
|
if (isServerOverload) {
|
|
// Overloads of our test server are showing up as mysterious "invalid argument"
|
|
// POSIX domain errors, timeouts, or connection failures.
|
|
// We'll check afterwards that most of the fetches succeeded.
|
|
[overloadIndexes addIndex:(NSUInteger)index];
|
|
return;
|
|
}
|
|
|
|
const char *expectedQueueLabel =
|
|
dispatch_queue_get_label(queues[(index / 3) % queues.count]);
|
|
const char *actualQueueLabel = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL);
|
|
XCTAssert(strcmp(actualQueueLabel, expectedQueueLabel) == 0,
|
|
@"queue mismatch on index %lu: %s (expected %s)", (unsigned long)index,
|
|
actualQueueLabel, expectedQueueLabel);
|
|
|
|
XCTAssertNotNil(fetchData, @"index %lu", (unsigned long)index);
|
|
XCTAssertNil(fetchError, @"index %lu", (unsigned long)index);
|
|
}
|
|
}];
|
|
});
|
|
}
|
|
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
|
|
XCTAssertLessThan(overloadIndexes.count, 10U);
|
|
if (overloadIndexes.count) {
|
|
NSLog(@"Server overloads: %@", overloadIndexes);
|
|
}
|
|
|
|
WAIT_FOR_START_STOP_NOTIFICATION_EXPECTATIONS();
|
|
|
|
// We should have one unique session per fetcher.
|
|
XCTAssertEqual(completedFetchCounter, kNumberOfFetchersToCreate);
|
|
}
|
|
|
|
- (void)testFetcherServiceTestBlock {
|
|
// No test server needed.
|
|
_testServer = nil;
|
|
_isServerRunning = NO;
|
|
|
|
GTMSessionFetcherService *service = [[GTMSessionFetcherService alloc] init];
|
|
service.allowedInsecureSchemes = @[ @"http" ];
|
|
|
|
// Create four fetchers, with alternating success and failure test blocks.
|
|
|
|
NSString *host = @"bad.example.com";
|
|
NSData *resultData = [@"Freebles" dataUsingEncoding:NSUTF8StringEncoding];
|
|
|
|
service.testBlock =
|
|
^(GTMSessionFetcher *fetcherToTest, GTMSessionFetcherTestResponse testResponse) {
|
|
NSData *fakedResultData;
|
|
NSHTTPURLResponse *fakedResultResponse;
|
|
NSError *fakedResultError;
|
|
|
|
NSURL *requestURL = fetcherToTest.request.URL;
|
|
NSString *pathStr = requestURL.path.lastPathComponent;
|
|
BOOL isOdd = (([pathStr intValue] % 2) != 0);
|
|
if (isOdd) {
|
|
// Succeed.
|
|
fakedResultData = resultData;
|
|
fakedResultResponse = [[NSHTTPURLResponse alloc] initWithURL:requestURL
|
|
statusCode:200
|
|
HTTPVersion:@"HTTP/1.1"
|
|
headerFields:@{@"Bearded" : @"Collie"}];
|
|
fakedResultError = nil;
|
|
} else {
|
|
// Fail.
|
|
fakedResultData = nil;
|
|
fakedResultResponse = [[NSHTTPURLResponse alloc] initWithURL:requestURL
|
|
statusCode:500
|
|
HTTPVersion:@"HTTP/1.1"
|
|
headerFields:@{@"Afghan" : @"Hound"}];
|
|
fakedResultError = [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
|
|
code:500
|
|
userInfo:@{kGTMSessionFetcherStatusDataKey : @"Oops"}];
|
|
}
|
|
|
|
testResponse(fakedResultResponse, fakedResultData, fakedResultError);
|
|
};
|
|
|
|
const int kNumberOfFetchersToCreate = 4;
|
|
CREATE_START_STOP_NOTIFICATION_EXPECTATIONS(kNumberOfFetchersToCreate, kNumberOfFetchersToCreate);
|
|
|
|
for (int idx = 1; idx <= kNumberOfFetchersToCreate; idx++) {
|
|
XCTestExpectation *fetcherCompletedExpectation =
|
|
[self expectationWithDescription:[NSString stringWithFormat:@"fetcher completed %d", idx]];
|
|
NSString *urlStr = [NSString stringWithFormat:@"http://%@/%d", host, idx];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:urlStr];
|
|
|
|
[fetcher beginFetchWithCompletionHandler:^(NSData *fetchData, NSError *fetchError) {
|
|
BOOL isOdd = ((idx % 2) != 0);
|
|
if (isOdd) {
|
|
// Should have succeeded.
|
|
XCTAssertEqualObjects(fetchData, resultData);
|
|
XCTAssertNil(fetchError);
|
|
XCTAssertEqual(fetcher.statusCode, (NSInteger)200);
|
|
XCTAssertEqualObjects(fetcher.responseHeaders[@"Bearded"], @"Collie");
|
|
} else {
|
|
// Should have failed.
|
|
XCTAssertNil(fetchData);
|
|
XCTAssertEqual(fetchError.code, (NSInteger)500);
|
|
XCTAssertEqual(fetcher.statusCode, 500);
|
|
XCTAssertEqualObjects(fetcher.responseHeaders[@"Afghan"], @"Hound");
|
|
}
|
|
[fetcherCompletedExpectation fulfill];
|
|
}];
|
|
}
|
|
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
|
|
WAIT_FOR_START_STOP_NOTIFICATION_EXPECTATIONS();
|
|
|
|
XCTAssertEqual([service.runningFetchersByHost objectForKey:host].count, (NSUInteger)0);
|
|
}
|
|
|
|
- (void)testMockCreationMethod {
|
|
// No test server needed.
|
|
_testServer = nil;
|
|
_isServerRunning = NO;
|
|
|
|
CREATE_START_STOP_NOTIFICATION_EXPECTATIONS(2, 2);
|
|
|
|
// Test with data.
|
|
NSData *data = [@"abcdefg" dataUsingEncoding:NSUTF8StringEncoding];
|
|
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:data fakedError:nil];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"http://example.invalid"];
|
|
|
|
XCTestExpectation *expectFinishedWithData = [self expectationWithDescription:@"Called back"];
|
|
|
|
[fetcher beginFetchWithCompletionHandler:^(NSData *fetchData, NSError *fetchError) {
|
|
XCTAssertEqualObjects(fetchData, data);
|
|
XCTAssertNil(fetchError);
|
|
[expectFinishedWithData fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
|
|
// Test with error.
|
|
NSError *error = [NSError errorWithDomain:@"example.com" code:-321 userInfo:nil];
|
|
service = [GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:error];
|
|
fetcher = [service fetcherWithURLString:@"http://example.invalid"];
|
|
|
|
XCTestExpectation *expectFinishedWithError = [self expectationWithDescription:@"Called back"];
|
|
|
|
[fetcher beginFetchWithCompletionHandler:^(NSData *fetchData, NSError *fetchError) {
|
|
XCTAssertNil(fetchData);
|
|
XCTAssertEqualObjects(fetchError, error);
|
|
[expectFinishedWithError fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
|
|
WAIT_FOR_START_STOP_NOTIFICATION_EXPECTATIONS();
|
|
}
|
|
|
|
// Test to ensure that the service's default user-agent does not override a user-agent set on the
|
|
// request itself.
|
|
- (void)testUserAgentFromRequest {
|
|
NSString *const kUserAgentHeader = @"User-Agent";
|
|
NSString *const kUserAgentValue = @"TestUserAgentFromRequest";
|
|
GTMSessionFetcherService *service = [[GTMSessionFetcherService alloc] init];
|
|
|
|
service.allowLocalhostRequest = YES;
|
|
|
|
NSURL *validFileURL = [_testServer localURLForFile:kValidFileName];
|
|
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:validFileURL];
|
|
|
|
[request setValue:kUserAgentValue forHTTPHeaderField:kUserAgentHeader];
|
|
|
|
GTMSessionFetcher *fetcher = [service fetcherWithRequest:request];
|
|
NSURLRequest *fetcherRequest = fetcher.request;
|
|
|
|
NSString *userAgent = [fetcherRequest valueForHTTPHeaderField:kUserAgentHeader];
|
|
XCTAssertEqualObjects(userAgent, kUserAgentValue);
|
|
}
|
|
|
|
// Test to ensure that setting the service's default user-agent to nil, causes the service to use
|
|
// the user-agent in the configuration.
|
|
- (void)testUserAgentFromSessionConfiguration {
|
|
if (!_isServerRunning) return;
|
|
|
|
CREATE_START_STOP_NOTIFICATION_EXPECTATIONS(1, 1);
|
|
|
|
NSString *const kUserAgentHeader = @"User-Agent";
|
|
NSString *const kUserAgentValue = @"TestUserAgentFromSessionConfig";
|
|
|
|
// Build the session configuration to use, which includes the User-Agent header in the
|
|
// HTTPAdditionalHeaders property.
|
|
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
|
|
|
|
config.HTTPAdditionalHeaders = @{kUserAgentHeader : kUserAgentValue};
|
|
|
|
GTMSessionFetcherService *service = [[GTMSessionFetcherService alloc] init];
|
|
|
|
// Clear the default user-agent.
|
|
service.userAgent = nil;
|
|
service.configuration = config;
|
|
service.allowLocalhostRequest = YES;
|
|
|
|
XCTestExpectation *expectReceiveResponse = [self expectationWithDescription:@"Received response"];
|
|
NSURL *gettysburgFileURL = [_testServer localURLForFile:kValidFileName];
|
|
NSURLComponents *validFileURLComponents = [[NSURLComponents alloc] initWithURL:gettysburgFileURL
|
|
resolvingAgainstBaseURL:NO];
|
|
validFileURLComponents.query = @"?echo-headers=true";
|
|
NSURL *validFileURL = validFileURLComponents.URL;
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURL:validFileURL];
|
|
|
|
[fetcher beginFetchWithCompletionHandler:^(NSData *fetchData, NSError *fetchError) {
|
|
// fetchData should contain the JSON representation of the dictionary of HTTP headers that were
|
|
// sent with the request to the server.
|
|
XCTAssertNotNil(fetchData);
|
|
XCTAssertNil(fetchError);
|
|
|
|
NSError *jsonError;
|
|
NSDictionary *requestHeaders = [NSJSONSerialization JSONObjectWithData:fetchData
|
|
options:0
|
|
error:&jsonError];
|
|
NSString *userAgent = requestHeaders[kUserAgentHeader];
|
|
|
|
XCTAssertEqualObjects(userAgent, kUserAgentValue, @"%@", jsonError);
|
|
|
|
[expectReceiveResponse fulfill];
|
|
}];
|
|
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
|
|
WAIT_FOR_START_STOP_NOTIFICATION_EXPECTATIONS();
|
|
}
|
|
|
|
- (void)testFetcherShouldUseStandardUserAgent {
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
XCTAssertNil([fetcher.request valueForHTTPHeaderField:@"User-Agent"]);
|
|
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
|
|
NSString *userAgent = [fetcher.request valueForHTTPHeaderField:@"User-Agent"];
|
|
NSError *error;
|
|
// Example: com.apple.dt.xctest.tool/14.2 MacOSX/13.3.1
|
|
// Another example: com.apple.dt.xctest.tool/14.2 Apple_TV/16.1 hw/sim
|
|
NSRegularExpression *standardUserAgentRegularExpression =
|
|
[NSRegularExpression regularExpressionWithPattern:@"^.+?/[0-9.]+ .+?/[0-9.]+( .+?/.+?)?$"
|
|
options:0
|
|
error:&error];
|
|
XCTAssertNotNil(standardUserAgentRegularExpression, @"Couldn't parse regex: %@", error);
|
|
NSUInteger numMatches =
|
|
[standardUserAgentRegularExpression numberOfMatchesInString:userAgent
|
|
options:0
|
|
range:NSMakeRange(0, userAgent.length)];
|
|
XCTAssertEqual(numMatches, 1UL,
|
|
@"Standard User-Agent should match expected pattern [Foo/1.2 Bar/1.2]: [%@]",
|
|
userAgent);
|
|
}
|
|
|
|
- (void)testUserAgentFromProviderShouldFetchFromProviderOffMainQueueWhenNotCached {
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
service.userAgentProvider = [[TestUserAgentBlockProvider alloc] initWithUserAgentBlock:^{
|
|
dispatch_assert_queue_not(dispatch_get_main_queue());
|
|
return @"NotMainQueue";
|
|
}];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher beginFetchWithCompletionHandler:^(__unused NSData *fetchData,
|
|
__unused NSError *fetchError) {
|
|
XCTAssertEqualObjects([fetcher.request valueForHTTPHeaderField:@"User-Agent"], @"NotMainQueue");
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
}
|
|
|
|
- (void)testUserAgentFromProviderShouldNotBeCalledWhenAlreadyCached {
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
TestUserAgentBlockProvider *userAgentProvider =
|
|
[[TestUserAgentBlockProvider alloc] initWithUserAgentBlock:^{
|
|
XCTFail(@"With a cached value, the provider should not be invoked.");
|
|
return @"NotUsed";
|
|
}];
|
|
userAgentProvider.cachedUserAgent = @"MainQueue";
|
|
service.userAgentProvider = userAgentProvider;
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher beginFetchWithCompletionHandler:^(__unused NSData *fetchData,
|
|
__unused NSError *fetchError) {
|
|
XCTAssertEqualObjects([fetcher.request valueForHTTPHeaderField:@"User-Agent"], @"MainQueue");
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
}
|
|
|
|
- (void)testStoppingFetchWhileUserAgentProviderInProgressShouldNotInvokeCompletionHandler {
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
XCTestExpectation *userAgentProvidedExpectation =
|
|
[self expectationWithDescription:@"User agent provided"];
|
|
service.userAgentProvider =
|
|
[[TestUserAgentBlockProvider alloc] initWithBlockedTimeout:1
|
|
userAgentBlock:^{
|
|
[userAgentProvidedExpectation fulfill];
|
|
return @"NotUsed";
|
|
}];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
[fetcher
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
XCTFail(@"Completion handler should not be invoked");
|
|
}];
|
|
[fetcher stopFetching];
|
|
[(TestUserAgentBlockProvider *)fetcher.userAgentProvider unblock];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
}
|
|
|
|
- (void)
|
|
testStoppingFetchWhileUserAgentProviderInProgressShouldInvokeCompletionHandlerIfPropertySet {
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
XCTestExpectation *userAgentProvidedExpectation =
|
|
[self expectationWithDescription:@"User agent provided"];
|
|
service.userAgentProvider =
|
|
[[TestUserAgentBlockProvider alloc] initWithBlockedTimeout:5
|
|
userAgentBlock:^{
|
|
[userAgentProvidedExpectation fulfill];
|
|
return @"NotUsed";
|
|
}];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
fetcher.stopFetchingTriggersCompletionHandler = YES;
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
[fetcher stopFetching];
|
|
[(TestUserAgentBlockProvider *)fetcher.userAgentProvider unblock];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
}
|
|
|
|
- (void)testResetSharedSessionWhileUserAgentProviderBlocked {
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
service.reuseSession = YES;
|
|
|
|
XCTestExpectation *userAgentEnteredExpectation =
|
|
[self expectationWithDescription:@"User agent provider entered"];
|
|
dispatch_semaphore_t userAgentBlockSemaphore = dispatch_semaphore_create(0);
|
|
TestUserAgentBlockProvider *provider =
|
|
[[TestUserAgentBlockProvider alloc] initWithUserAgentBlock:^{
|
|
[userAgentEnteredExpectation fulfill];
|
|
intptr_t waitResult = dispatch_semaphore_wait(
|
|
userAgentBlockSemaphore, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC));
|
|
XCTAssertEqual(0, waitResult, @"Timed out waiting to unblock user agent provider");
|
|
return @"BlockedUA";
|
|
}];
|
|
service.userAgentProvider = provider;
|
|
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
|
|
__block NSError *completionError;
|
|
[fetcher beginFetchWithCompletionHandler:^(NSData *fetchData, NSError *fetchError) {
|
|
(void)fetchData;
|
|
completionError = fetchError;
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
|
|
[self waitForExpectations:@[ userAgentEnteredExpectation ] timeout:_timeoutInterval];
|
|
|
|
[service resetSession];
|
|
dispatch_semaphore_signal(userAgentBlockSemaphore);
|
|
|
|
[self waitForExpectations:@[ fetchCompleteExpectation ] timeout:_timeoutInterval];
|
|
|
|
XCTAssertNotNil(service.session);
|
|
XCTAssertNil(completionError);
|
|
}
|
|
|
|
- (void)testSingleDecoratorSynchronous {
|
|
GTMSessionFetcherTestDecorator *decorator = [[GTMSessionFetcherTestDecorator alloc]
|
|
initWithHeadersSynchronous:@{@"foo" : @"bar", @"baz" : @"blech"}];
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
[service addDecorator:decorator];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
// Ensure this test always runs the decorators synchronously -- if it's the first
|
|
// test to run, then the `GTMStandardUserAgentProvider` will asynchronously add
|
|
// the User-Agent.
|
|
fetcher.userAgentProvider = [[GTMUserAgentStringProvider alloc] initWithUserAgentString:@"Lynx"];
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
NSDictionary<NSString *, NSString *> *expectedHeaders = @{@"foo" : @"bar", @"baz" : @"blech"};
|
|
XCTAssertEqualObjects(FetcherHeadersWithoutUserAgent(fetcher), expectedHeaders);
|
|
// The wait is intentionally after the assert, as we expect the header decorator to complete its
|
|
// work synchronously (but this test still needs to wait, as otherwise the NSNotifications posted
|
|
// asynchronously can affect other tests).
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
}
|
|
|
|
- (void)testSingleDecoratorSynchronousAddHeaderOnRetry {
|
|
GTMSessionFetcherService *service = [[GTMSessionFetcherService alloc] init];
|
|
NSURL *url = [NSURL URLWithString:@"https://www.html5zombo.com"];
|
|
service.testBlock =
|
|
^(GTMSessionFetcher *fetcher, GTMSessionFetcherTestResponse testResponseBlock) {
|
|
NSHTTPURLResponse *response;
|
|
NSData *data;
|
|
NSError *error;
|
|
if (fetcher.retryCount == 0) {
|
|
response = [[NSHTTPURLResponse alloc] initWithURL:url
|
|
statusCode:504
|
|
HTTPVersion:@"HTTP/1.1"
|
|
headerFields:@{}];
|
|
data = nil;
|
|
error = [NSError errorWithDomain:kGTMSessionFetcherErrorDomain code:504 userInfo:nil];
|
|
} else {
|
|
response = [[NSHTTPURLResponse alloc] initWithURL:url
|
|
statusCode:200
|
|
HTTPVersion:@"HTTP/1.1"
|
|
headerFields:@{}];
|
|
data = [@"Welcome to ZomboCom" dataUsingEncoding:NSUTF8StringEncoding];
|
|
error = nil;
|
|
}
|
|
testResponseBlock(response, data, error);
|
|
};
|
|
|
|
GTMSessionFetcherTestDecorator *decorator = [[GTMSessionFetcherTestDecorator alloc]
|
|
initWithFetcherWillStartBlock:^(
|
|
GTMSessionFetcher *fetcher,
|
|
GTMFetcherDecoratorFetcherWillStartCompletionHandler completion) {
|
|
if (!fetcher.retryCount) {
|
|
completion(/*request=*/nil, /*error=*/nil);
|
|
return;
|
|
}
|
|
// Add a `retry=1` header when the decorator is invoked on a retry.
|
|
NSMutableURLRequest *request = [fetcher.request mutableCopy];
|
|
[request setValue:@"1" forHTTPHeaderField:@"retry"];
|
|
completion(request, /*error=*/nil);
|
|
}
|
|
synchronous:YES];
|
|
|
|
[service addDecorator:decorator];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
fetcher.retryEnabled = YES;
|
|
fetcher.minRetryInterval = 0.01;
|
|
fetcher.maxRetryInterval = 0.05;
|
|
fetcher.userAgentProvider = [[GTMUserAgentStringProvider alloc] initWithUserAgentString:@"Lynx"];
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
NSDictionary<NSString *, NSString *> *expectedHeaders = @{@"retry" : @"1"};
|
|
XCTAssertEqualObjects(FetcherHeadersWithoutUserAgent(fetcher), expectedHeaders);
|
|
}
|
|
|
|
- (void)testSingleDecoratorSynchronousWithError {
|
|
NSError *error = [NSError errorWithDomain:@"TestDomain" code:12345 userInfo:nil];
|
|
GTMSessionFetcherTestDecorator *decorator = [[GTMSessionFetcherTestDecorator alloc]
|
|
initWithFetcherWillStartBlock:^(
|
|
GTMSessionFetcher *fetcher,
|
|
GTMFetcherDecoratorFetcherWillStartCompletionHandler completion) {
|
|
completion(/*request=*/nil, error);
|
|
}
|
|
synchronous:YES];
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
[service addDecorator:decorator];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
fetcher.userAgentProvider = [[GTMUserAgentStringProvider alloc] initWithUserAgentString:@"Lynx"];
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher beginFetchWithCompletionHandler:^(__unused NSData *fetchData, NSError *fetchError) {
|
|
XCTAssertEqualObjects(fetchError, error);
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
XCTAssertNil(decorator.fetchedData);
|
|
XCTAssertEqualObjects(decorator.fetchError, error);
|
|
}
|
|
|
|
- (void)testMultipleDecoratorsSynchronousWithErrorShouldNotCallSubsequentDecorators {
|
|
NSError *error = [NSError errorWithDomain:@"TestDomain" code:12345 userInfo:nil];
|
|
GTMSessionFetcherTestDecorator *decoratorA = [[GTMSessionFetcherTestDecorator alloc]
|
|
initWithFetcherWillStartBlock:^(
|
|
GTMSessionFetcher *fetcher,
|
|
GTMFetcherDecoratorFetcherWillStartCompletionHandler completion) {
|
|
completion(/*request=*/nil, error);
|
|
}
|
|
synchronous:YES];
|
|
GTMSessionFetcherTestDecorator *decoratorB = [[GTMSessionFetcherTestDecorator alloc]
|
|
initWithFetcherWillStartBlock:^(
|
|
GTMSessionFetcher *fetcher,
|
|
GTMFetcherDecoratorFetcherWillStartCompletionHandler completion) {
|
|
XCTFail(@"Subsequent decorator should not be invoked");
|
|
}
|
|
synchronous:YES];
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
[service addDecorator:decoratorA];
|
|
[service addDecorator:decoratorB];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
fetcher.userAgentProvider = [[GTMUserAgentStringProvider alloc] initWithUserAgentString:@"Lynx"];
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher beginFetchWithCompletionHandler:^(__unused NSData *fetchData, NSError *fetchError) {
|
|
XCTAssertEqualObjects(fetchError, error);
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
XCTAssertNil(decoratorA.fetchedData);
|
|
XCTAssertNil(decoratorB.fetchedData);
|
|
XCTAssertEqualObjects(decoratorA.fetchError, error);
|
|
XCTAssertEqualObjects(decoratorB.fetchError, error);
|
|
}
|
|
|
|
- (void)testMultipleDecoratorsSynchronous {
|
|
GTMSessionFetcherTestDecorator *decoratorA =
|
|
[[GTMSessionFetcherTestDecorator alloc] initWithHeadersSynchronous:@{@"foo" : @"bar"}];
|
|
GTMSessionFetcherTestDecorator *decoratorB = [[GTMSessionFetcherTestDecorator alloc]
|
|
initWithHeadersSynchronous:@{@"baz" : @"blech", @"quux" : @"xyzzy"}];
|
|
GTMSessionFetcherTestDecorator *decoratorC =
|
|
[[GTMSessionFetcherTestDecorator alloc] initWithHeadersSynchronous:@{@"quux" : @"corge"}];
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
[service addDecorator:decoratorA];
|
|
[service addDecorator:decoratorB];
|
|
[service addDecorator:decoratorC];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
fetcher.userAgentProvider = [[GTMUserAgentStringProvider alloc] initWithUserAgentString:@"Lynx"];
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
NSDictionary<NSString *, NSString *> *expectedHeaders =
|
|
@{@"foo" : @"bar", @"baz" : @"blech", @"quux" : @"corge"};
|
|
XCTAssertEqualObjects(FetcherHeadersWithoutUserAgent(fetcher), expectedHeaders);
|
|
// The wait is intentionally after the assert, as we expect the header decorator to complete its
|
|
// work synchronously (but this test still needs to wait, as otherwise the NSNotifications posted
|
|
// asynchronously can affect other tests).
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
}
|
|
|
|
- (void)testMultipleDecoratorsSynchronousFetchedDataAndError {
|
|
GTMSessionFetcherTestDecorator *decoratorA =
|
|
[[GTMSessionFetcherTestDecorator alloc] initWithHeadersSynchronous:@{}];
|
|
GTMSessionFetcherTestDecorator *decoratorB =
|
|
[[GTMSessionFetcherTestDecorator alloc] initWithHeadersSynchronous:@{}];
|
|
NSData *data = [@"hello world" dataUsingEncoding:NSUTF8StringEncoding];
|
|
NSError *error = [NSError errorWithDomain:@"TestErrorDomain" code:12345 userInfo:nil];
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:data fakedError:error];
|
|
[service addDecorator:decoratorA];
|
|
[service addDecorator:decoratorB];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
fetcher.userAgentProvider = [[GTMUserAgentStringProvider alloc] initWithUserAgentString:@"Lynx"];
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
XCTAssertEqualObjects(decoratorA.fetchedData, data);
|
|
XCTAssertEqualObjects(decoratorB.fetchedData, data);
|
|
XCTAssertEqualObjects(decoratorA.fetchError, error);
|
|
XCTAssertEqualObjects(decoratorB.fetchError, error);
|
|
}
|
|
|
|
- (void)testSingleDecoratorSynchronousWithDifferentHeadersForEachRequest {
|
|
__block int i = 0;
|
|
GTMSessionFetcherTestDecorator *decorator = [[GTMSessionFetcherTestDecorator alloc]
|
|
initWithFetcherWillStartBlock:^(
|
|
GTMSessionFetcher *fetcher,
|
|
GTMFetcherDecoratorFetcherWillStartCompletionHandler completion) {
|
|
NSMutableURLRequest *request = [fetcher.request mutableCopy];
|
|
[request setValue:[NSString stringWithFormat:@"%d", i++] forHTTPHeaderField:@"foo"];
|
|
completion(request, /*error=*/nil);
|
|
}
|
|
synchronous:YES];
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
[service addDecorator:decorator];
|
|
GTMSessionFetcher *fetcherA = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
fetcherA.userAgentProvider = [[GTMUserAgentStringProvider alloc] initWithUserAgentString:@"Lynx"];
|
|
|
|
XCTestExpectation *fetchCompleteExpectationA =
|
|
[self expectationWithDescription:@"Fetch complete"];
|
|
[fetcherA
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
[fetchCompleteExpectationA fulfill];
|
|
}];
|
|
GTMSessionFetcher *fetcherB = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
fetcherB.userAgentProvider = [[GTMUserAgentStringProvider alloc] initWithUserAgentString:@"Lynx"];
|
|
XCTestExpectation *fetchCompleteExpectationB =
|
|
[self expectationWithDescription:@"Fetch complete"];
|
|
[fetcherB
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
[fetchCompleteExpectationB fulfill];
|
|
}];
|
|
GTMSessionFetcher *fetcherC = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
fetcherC.userAgentProvider = [[GTMUserAgentStringProvider alloc] initWithUserAgentString:@"Lynx"];
|
|
XCTestExpectation *fetchCompleteExpectationC =
|
|
[self expectationWithDescription:@"Fetch complete"];
|
|
[fetcherC
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
[fetchCompleteExpectationC fulfill];
|
|
}];
|
|
|
|
NSDictionary<NSString *, NSString *> *expectedHeadersA = @{@"foo" : @"0"};
|
|
XCTAssertEqualObjects(FetcherHeadersWithoutUserAgent(fetcherA), expectedHeadersA);
|
|
NSDictionary<NSString *, NSString *> *expectedHeadersB = @{@"foo" : @"1"};
|
|
XCTAssertEqualObjects(FetcherHeadersWithoutUserAgent(fetcherB), expectedHeadersB);
|
|
NSDictionary<NSString *, NSString *> *expectedHeadersC = @{@"foo" : @"2"};
|
|
XCTAssertEqualObjects(FetcherHeadersWithoutUserAgent(fetcherC), expectedHeadersC);
|
|
// The wait is intentionally after the assert, as we expect the header decorator to complete its
|
|
// work synchronously (but this test still needs to wait, as otherwise the NSNotifications posted
|
|
// asynchronously can affect other tests).
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
}
|
|
|
|
- (void)testEmptyDecoratorAsynchronous {
|
|
GTMSessionFetcherTestDecorator *decorator =
|
|
[[GTMSessionFetcherTestDecorator alloc] initWithHeaders:@{}];
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
[service addDecorator:decorator];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
XCTAssertEqualObjects(FetcherHeadersWithoutUserAgent(fetcher), @{});
|
|
}
|
|
|
|
- (void)testSingleDecoratorAsynchronous {
|
|
GTMSessionFetcherTestDecorator *decorator = [[GTMSessionFetcherTestDecorator alloc]
|
|
initWithHeaders:@{@"foo" : @"bar", @"baz" : @"blech"}];
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
[service addDecorator:decorator];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
NSDictionary<NSString *, NSString *> *expectedHeaders = @{@"foo" : @"bar", @"baz" : @"blech"};
|
|
XCTAssertEqualObjects(FetcherHeadersWithoutUserAgent(fetcher), expectedHeaders);
|
|
}
|
|
|
|
- (void)testSingleDecoratorAsynchronousWithDifferentHeadersForEachRequest {
|
|
__block int i = 0;
|
|
GTMSessionFetcherTestDecorator *decorator = [[GTMSessionFetcherTestDecorator alloc]
|
|
initWithFetcherWillStartBlock:^(
|
|
GTMSessionFetcher *fetcher,
|
|
GTMFetcherDecoratorFetcherWillStartCompletionHandler completion) {
|
|
NSMutableURLRequest *request = [fetcher.request mutableCopy];
|
|
[request setValue:[NSString stringWithFormat:@"%d", i++] forHTTPHeaderField:@"foo"];
|
|
completion(request, /*error=*/nil);
|
|
}
|
|
synchronous:NO];
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
[service addDecorator:decorator];
|
|
GTMSessionFetcher *fetcherA = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
XCTestExpectation *fetchCompleteExpectationA =
|
|
[self expectationWithDescription:@"Fetch complete"];
|
|
[fetcherA
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
[fetchCompleteExpectationA fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
GTMSessionFetcher *fetcherB = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
XCTestExpectation *fetchCompleteExpectationB =
|
|
[self expectationWithDescription:@"Fetch complete"];
|
|
[fetcherB
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
[fetchCompleteExpectationB fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
GTMSessionFetcher *fetcherC = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
XCTestExpectation *fetchCompleteExpectationC =
|
|
[self expectationWithDescription:@"Fetch complete"];
|
|
[fetcherC
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
[fetchCompleteExpectationC fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
|
|
NSDictionary<NSString *, NSString *> *expectedHeadersA = @{@"foo" : @"0"};
|
|
XCTAssertEqualObjects(FetcherHeadersWithoutUserAgent(fetcherA), expectedHeadersA);
|
|
NSDictionary<NSString *, NSString *> *expectedHeadersB = @{@"foo" : @"1"};
|
|
XCTAssertEqualObjects(FetcherHeadersWithoutUserAgent(fetcherB), expectedHeadersB);
|
|
NSDictionary<NSString *, NSString *> *expectedHeadersC = @{@"foo" : @"2"};
|
|
XCTAssertEqualObjects(FetcherHeadersWithoutUserAgent(fetcherC), expectedHeadersC);
|
|
}
|
|
|
|
- (void)testRemoveSingleDecoratorAsynchronous {
|
|
GTMSessionFetcherTestDecorator *decorator = [[GTMSessionFetcherTestDecorator alloc]
|
|
initWithHeaders:@{@"foo" : @"bar", @"baz" : @"blech"}];
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
[service addDecorator:decorator];
|
|
[service removeDecorator:decorator];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
XCTAssertEqualObjects(FetcherHeadersWithoutUserAgent(fetcher), @{});
|
|
}
|
|
|
|
- (void)testSingleDecoratorAsynchronousWeakReferenceReleasedBeforeFetcherCreated {
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
{
|
|
// Reference should be released after this block exits.
|
|
NS_VALID_UNTIL_END_OF_SCOPE GTMSessionFetcherTestDecorator *decorator =
|
|
[[GTMSessionFetcherTestDecorator alloc]
|
|
initWithHeaders:@{@"foo" : @"bar", @"baz" : @"blech"}];
|
|
[service addDecorator:decorator];
|
|
}
|
|
|
|
// The header decorator should no longer add its headers after this point.
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
|
|
XCTAssertEqualObjects(FetcherHeadersWithoutUserAgent(fetcher), @{});
|
|
}
|
|
|
|
- (void)testSingleDecoratorAsynchronousUserAgent {
|
|
GTMSessionFetcherTestDecorator *decorator =
|
|
[[GTMSessionFetcherTestDecorator alloc] initWithHeaders:@{@"User-Agent" : @"My User Agent"}];
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
[service addDecorator:decorator];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
NSDictionary<NSString *, NSString *> *expectedHeaders = @{@"User-Agent" : @"My User Agent"};
|
|
XCTAssertEqualObjects(fetcher.request.allHTTPHeaderFields, expectedHeaders);
|
|
}
|
|
|
|
- (void)testMultipleDecoratorsAsynchronousDifferentFields {
|
|
GTMSessionFetcherTestDecorator *decoratorA =
|
|
[[GTMSessionFetcherTestDecorator alloc] initWithHeaders:@{@"foo" : @"bar"}];
|
|
GTMSessionFetcherTestDecorator *decoratorB =
|
|
[[GTMSessionFetcherTestDecorator alloc] initWithHeaders:@{@"baz" : @"blech"}];
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
[service addDecorator:decoratorA];
|
|
[service addDecorator:decoratorB];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
NSDictionary<NSString *, NSString *> *expectedHeaders = @{@"foo" : @"bar", @"baz" : @"blech"};
|
|
XCTAssertEqualObjects(FetcherHeadersWithoutUserAgent(fetcher), expectedHeaders);
|
|
}
|
|
|
|
- (void)testMultipleDecoratorsAsynchronousOneRemoved {
|
|
GTMSessionFetcherTestDecorator *decoratorA =
|
|
[[GTMSessionFetcherTestDecorator alloc] initWithHeaders:@{@"foo" : @"bar"}];
|
|
GTMSessionFetcherTestDecorator *decoratorB =
|
|
[[GTMSessionFetcherTestDecorator alloc] initWithHeaders:@{@"baz" : @"blech"}];
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
[service addDecorator:decoratorA];
|
|
[service addDecorator:decoratorB];
|
|
[service removeDecorator:decoratorA];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
NSDictionary<NSString *, NSString *> *expectedHeaders = @{@"baz" : @"blech"};
|
|
XCTAssertEqualObjects(FetcherHeadersWithoutUserAgent(fetcher), expectedHeaders);
|
|
}
|
|
|
|
- (void)testMultipleDecoratorsAsynchronousOneWeakReferenceReleased {
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
GTMSessionFetcherTestDecorator *decoratorB =
|
|
[[GTMSessionFetcherTestDecorator alloc] initWithHeaders:@{@"baz" : @"blech"}];
|
|
{
|
|
// Reference should be released after this block exits.
|
|
NS_VALID_UNTIL_END_OF_SCOPE GTMSessionFetcherTestDecorator *decoratorA =
|
|
[[GTMSessionFetcherTestDecorator alloc] initWithHeaders:@{@"foo" : @"bar"}];
|
|
[service addDecorator:decoratorA];
|
|
[service addDecorator:decoratorB];
|
|
}
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
NSDictionary<NSString *, NSString *> *expectedHeaders = @{@"baz" : @"blech"};
|
|
XCTAssertEqualObjects(FetcherHeadersWithoutUserAgent(fetcher), expectedHeaders);
|
|
}
|
|
|
|
- (void)testMultipleDecoratorsAsynchronousSameFields {
|
|
GTMSessionFetcherTestDecorator *decoratorA = [[GTMSessionFetcherTestDecorator alloc]
|
|
initWithHeaders:@{@"foo" : @"bar", @"baz" : @"blech"}];
|
|
GTMSessionFetcherTestDecorator *decoratorB = [[GTMSessionFetcherTestDecorator alloc]
|
|
initWithHeaders:@{@"foo" : @"quux", @"baz" : @"xyzzy"}];
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
[service addDecorator:decoratorA];
|
|
[service addDecorator:decoratorB];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
NSDictionary<NSString *, NSString *> *expectedHeaders = @{@"foo" : @"quux", @"baz" : @"xyzzy"};
|
|
XCTAssertEqualObjects(FetcherHeadersWithoutUserAgent(fetcher), expectedHeaders);
|
|
}
|
|
|
|
- (void)testMultipleDecoratorsAsynchronousSomeFieldsSame {
|
|
GTMSessionFetcherTestDecorator *decoratorA = [[GTMSessionFetcherTestDecorator alloc]
|
|
initWithHeaders:@{@"foo" : @"bar", @"baz" : @"blech"}];
|
|
GTMSessionFetcherTestDecorator *decoratorB = [[GTMSessionFetcherTestDecorator alloc]
|
|
initWithHeaders:@{@"baz" : @"xyzzy", @"quux" : @"corge"}];
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
[service addDecorator:decoratorA];
|
|
[service addDecorator:decoratorB];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
NSDictionary<NSString *, NSString *> *expectedHeaders =
|
|
@{@"foo" : @"bar", @"baz" : @"xyzzy", @"quux" : @"corge"};
|
|
XCTAssertEqualObjects(FetcherHeadersWithoutUserAgent(fetcher), expectedHeaders);
|
|
}
|
|
|
|
- (void)testMultipleDecoratorsAsyncFetchedDataAndError {
|
|
GTMSessionFetcherTestDecorator *decoratorA =
|
|
[[GTMSessionFetcherTestDecorator alloc] initWithHeaders:@{}];
|
|
GTMSessionFetcherTestDecorator *decoratorB =
|
|
[[GTMSessionFetcherTestDecorator alloc] initWithHeaders:@{}];
|
|
NSData *data = [@"hello world" dataUsingEncoding:NSUTF8StringEncoding];
|
|
NSError *error = [NSError errorWithDomain:@"TestErrorDomain" code:12345 userInfo:nil];
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:data fakedError:error];
|
|
[service addDecorator:decoratorA];
|
|
[service addDecorator:decoratorB];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher
|
|
beginFetchWithCompletionHandler:^(__unused NSData *fetchData, __unused NSError *fetchError) {
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
XCTAssertEqualObjects(decoratorA.fetchedData, data);
|
|
XCTAssertEqualObjects(decoratorB.fetchedData, data);
|
|
XCTAssertEqualObjects(decoratorA.fetchError, error);
|
|
XCTAssertEqualObjects(decoratorB.fetchError, error);
|
|
}
|
|
|
|
- (void)testSingleDecoderStopFetchingBeforeBeginNeverInvoked {
|
|
// Note: This test actually results if the per host limits enforcement first noticing the
|
|
// `-stopFetching`, to really trigger the check when entering the delayed state for decorators
|
|
// would take multiple threads and there is no way to synchronize things to set the flag at just
|
|
// the right point.
|
|
GTMSessionFetcherTestDecorator *decorator = [[GTMSessionFetcherTestDecorator alloc]
|
|
initWithFetcherWillStartBlock:^(
|
|
GTMSessionFetcher *fetcher,
|
|
GTMFetcherDecoratorFetcherWillStartCompletionHandler completion) {
|
|
XCTFail(@"Should not have been invoked");
|
|
}
|
|
synchronous:YES];
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
[service addDecorator:decorator];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
fetcher.userAgentProvider = [[GTMUserAgentStringProvider alloc] initWithUserAgentString:@"Lynx"];
|
|
fetcher.stopFetchingTriggersCompletionHandler = YES;
|
|
[fetcher stopFetching];
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher beginFetchWithCompletionHandler:^(__unused NSData *fetchData, NSError *fetchError) {
|
|
XCTAssertEqual(fetchError.code, GTMSessionFetcherErrorUserCancelled);
|
|
XCTAssertEqualObjects(fetchError.domain, kGTMSessionFetcherErrorDomain);
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
XCTAssertNil(decorator.fetchedData);
|
|
XCTAssertEqual(decorator.fetchError.code, GTMSessionFetcherErrorUserCancelled);
|
|
XCTAssertEqualObjects(decorator.fetchError.domain, kGTMSessionFetcherErrorDomain);
|
|
}
|
|
|
|
- (void)testSingleDecoderStopFetchingTriggersCallback {
|
|
GTMSessionFetcherTestDecorator *decorator = [[GTMSessionFetcherTestDecorator alloc]
|
|
initWithFetcherWillStartBlock:^(
|
|
GTMSessionFetcher *fetcher,
|
|
GTMFetcherDecoratorFetcherWillStartCompletionHandler completion) {
|
|
[fetcher stopFetching];
|
|
completion(/*request=*/nil, /*error*/ nil);
|
|
}
|
|
synchronous:YES];
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
[service addDecorator:decorator];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
fetcher.userAgentProvider = [[GTMUserAgentStringProvider alloc] initWithUserAgentString:@"Lynx"];
|
|
fetcher.stopFetchingTriggersCompletionHandler = YES;
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher beginFetchWithCompletionHandler:^(__unused NSData *fetchData, NSError *fetchError) {
|
|
XCTAssertEqual(fetchError.code, GTMSessionFetcherErrorUserCancelled);
|
|
XCTAssertEqualObjects(fetchError.domain, kGTMSessionFetcherErrorDomain);
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
XCTAssertNil(decorator.fetchedData);
|
|
XCTAssertEqual(decorator.fetchError.code, GTMSessionFetcherErrorUserCancelled);
|
|
XCTAssertEqualObjects(decorator.fetchError.domain, kGTMSessionFetcherErrorDomain);
|
|
}
|
|
|
|
- (void)testMultipleDecodersStopFetchingInFirstPreventsRestTriggersCallback {
|
|
GTMSessionFetcherTestDecorator *decoratorA = [[GTMSessionFetcherTestDecorator alloc]
|
|
initWithFetcherWillStartBlock:^(
|
|
GTMSessionFetcher *fetcher,
|
|
GTMFetcherDecoratorFetcherWillStartCompletionHandler completion) {
|
|
[fetcher stopFetching];
|
|
completion(/*request=*/nil, /*error*/ nil);
|
|
}
|
|
synchronous:YES];
|
|
GTMSessionFetcherTestDecorator *decoratorB = [[GTMSessionFetcherTestDecorator alloc]
|
|
initWithFetcherWillStartBlock:^(
|
|
GTMSessionFetcher *fetcher,
|
|
GTMFetcherDecoratorFetcherWillStartCompletionHandler completion) {
|
|
XCTFail(@"Subsequent decorator should not be invoked");
|
|
}
|
|
synchronous:YES];
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
[service addDecorator:decoratorA];
|
|
[service addDecorator:decoratorB];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
fetcher.userAgentProvider = [[GTMUserAgentStringProvider alloc] initWithUserAgentString:@"Lynx"];
|
|
fetcher.stopFetchingTriggersCompletionHandler = YES;
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher beginFetchWithCompletionHandler:^(__unused NSData *fetchData, NSError *fetchError) {
|
|
XCTAssertEqual(fetchError.code, GTMSessionFetcherErrorUserCancelled);
|
|
XCTAssertEqualObjects(fetchError.domain, kGTMSessionFetcherErrorDomain);
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
XCTAssertNil(decoratorA.fetchedData);
|
|
XCTAssertNil(decoratorB.fetchedData);
|
|
XCTAssertEqual(decoratorA.fetchError.code, GTMSessionFetcherErrorUserCancelled);
|
|
XCTAssertEqualObjects(decoratorA.fetchError.domain, kGTMSessionFetcherErrorDomain);
|
|
XCTAssertEqual(decoratorB.fetchError.code, GTMSessionFetcherErrorUserCancelled);
|
|
XCTAssertEqualObjects(decoratorB.fetchError.domain, kGTMSessionFetcherErrorDomain);
|
|
}
|
|
|
|
- (void)testSingleDecoderStopFetchingAndContinueErrorTriggersCallbackWithRightError {
|
|
NSError *error = [NSError errorWithDomain:@"TestDomain" code:12345 userInfo:nil];
|
|
GTMSessionFetcherTestDecorator *decorator = [[GTMSessionFetcherTestDecorator alloc]
|
|
initWithFetcherWillStartBlock:^(
|
|
GTMSessionFetcher *fetcher,
|
|
GTMFetcherDecoratorFetcherWillStartCompletionHandler completion) {
|
|
[fetcher stopFetching];
|
|
completion(/*request=*/nil, error);
|
|
}
|
|
synchronous:YES];
|
|
GTMSessionFetcherService *service =
|
|
[GTMSessionFetcherService mockFetcherServiceWithFakedData:nil fakedError:nil];
|
|
[service addDecorator:decorator];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURLString:@"https://www.html5zombo.com"];
|
|
fetcher.userAgentProvider = [[GTMUserAgentStringProvider alloc] initWithUserAgentString:@"Lynx"];
|
|
fetcher.stopFetchingTriggersCompletionHandler = YES;
|
|
XCTestExpectation *fetchCompleteExpectation = [self expectationWithDescription:@"Fetch complete"];
|
|
[fetcher beginFetchWithCompletionHandler:^(__unused NSData *fetchData, NSError *fetchError) {
|
|
XCTAssertEqual(fetchError.code, GTMSessionFetcherErrorUserCancelled);
|
|
XCTAssertEqualObjects(fetchError.domain, kGTMSessionFetcherErrorDomain);
|
|
[fetchCompleteExpectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
XCTAssertNil(decorator.fetchedData);
|
|
XCTAssertNotEqualObjects(decorator.fetchError, error);
|
|
XCTAssertEqual(decorator.fetchError.code, GTMSessionFetcherErrorUserCancelled);
|
|
XCTAssertEqualObjects(decorator.fetchError.domain, kGTMSessionFetcherErrorDomain);
|
|
}
|
|
|
|
- (void)testDelegateDispatcherForFetcher {
|
|
GTMSessionFetcherService *service = [[GTMSessionFetcherService alloc] init];
|
|
GTMSessionFetcher *fetcher;
|
|
NSURLSession *session;
|
|
NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
|
|
|
|
// When the NSURLSession delegate is the fetcher itself, the returned delegate dispatcher
|
|
// should be nil.
|
|
fetcher = [service fetcherWithURLString:@"https://www.example.com"];
|
|
session = [NSURLSession sessionWithConfiguration:config
|
|
delegate:fetcher
|
|
delegateQueue:fetcher.sessionDelegateQueue];
|
|
|
|
fetcher.session = session;
|
|
XCTAssertNil([service delegateDispatcherForFetcher:fetcher],
|
|
@"dispatcher should be nil when fetcher is the session delegate");
|
|
[session invalidateAndCancel];
|
|
|
|
// When the NSURLSession delegate is a proxy for a GTMSessionFetcher, the returned delegate
|
|
// dispatcher should be nil.
|
|
fetcher = [service fetcherWithURLString:@"https://www.example.com"];
|
|
GTMSessionFetcherServiceTestObjectProxy *fetcherProxy =
|
|
[GTMSessionFetcherServiceTestObjectProxy proxyForObject:fetcher];
|
|
session = [NSURLSession sessionWithConfiguration:config
|
|
delegate:(id<NSURLSessionDelegate>)fetcherProxy
|
|
delegateQueue:fetcher.sessionDelegateQueue];
|
|
fetcher.session = session;
|
|
|
|
XCTAssertNil([service delegateDispatcherForFetcher:fetcher],
|
|
@"dispatcher should be nil when session delegate is a proxy for GTMSessionFetcher");
|
|
[session invalidateAndCancel];
|
|
|
|
// When the NSURLSession delegate is the delegate dispatcher, the returned delegate dispatcher
|
|
// should be non-nil.
|
|
fetcher = [service fetcherWithURLString:@"https://www.example.com"];
|
|
session = [NSURLSession sessionWithConfiguration:config
|
|
delegate:service.sessionDelegate
|
|
delegateQueue:fetcher.sessionDelegateQueue];
|
|
|
|
fetcher.session = session;
|
|
XCTAssertNotNil([service delegateDispatcherForFetcher:fetcher],
|
|
@"dispatcher should be non-nil when fetcher is the session delegate");
|
|
[session invalidateAndCancel];
|
|
|
|
// When the NSURLSession delegate is a proxy for a the delegate dispatcher, the returned delegate
|
|
// dispatcher should be non-nil.
|
|
fetcher = [service fetcherWithURLString:@"https://www.example.com"];
|
|
GTMSessionFetcherServiceTestObjectProxy *dispatcherProxy =
|
|
[GTMSessionFetcherServiceTestObjectProxy proxyForObject:service.sessionDelegate];
|
|
session = [NSURLSession sessionWithConfiguration:config
|
|
delegate:(id<NSURLSessionDelegate>)dispatcherProxy
|
|
delegateQueue:fetcher.sessionDelegateQueue];
|
|
fetcher.session = session;
|
|
|
|
XCTAssertNotNil(
|
|
[service delegateDispatcherForFetcher:fetcher],
|
|
@"dispatcher should be non-nil when session delegate is a proxy for the dispatcher");
|
|
[session invalidateAndCancel];
|
|
}
|
|
|
|
- (void)testFetcherUsingMetricsCollectionBlockFromFetcherService {
|
|
if (!_isServerRunning) return;
|
|
|
|
CREATE_START_STOP_NOTIFICATION_EXPECTATIONS(1, 1);
|
|
|
|
__block NSURLSessionTaskMetrics *collectedMetrics = nil;
|
|
|
|
GTMSessionFetcherService *service = [[GTMSessionFetcherService alloc] init];
|
|
service.allowLocalhostRequest = YES;
|
|
service.metricsCollectionBlock = ^(NSURLSessionTaskMetrics *_Nonnull metrics) {
|
|
collectedMetrics = metrics;
|
|
};
|
|
|
|
NSURL *fetchURL = [_testServer localURLForFile:kValidFileName];
|
|
GTMSessionFetcher *fetcher = [service fetcherWithURL:fetchURL];
|
|
XCTestExpectation *expectation =
|
|
[self expectationWithDescription:(id _Nonnull)fetchURL.absoluteString];
|
|
[fetcher beginFetchWithCompletionHandler:^(NSData *fetchData, NSError *fetchError) {
|
|
XCTAssertNotNil(fetchData);
|
|
XCTAssertNil(fetchError);
|
|
[expectation fulfill];
|
|
}];
|
|
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
|
|
WAIT_FOR_START_STOP_NOTIFICATION_EXPECTATIONS();
|
|
|
|
XCTAssertNotNil(collectedMetrics);
|
|
}
|
|
|
|
- (void)testFetcherUsesStopFetchingTriggersCompletionHandlerFromFetcherService {
|
|
if (!_isServerRunning) return;
|
|
|
|
GTMSessionFetcherService *service = [[GTMSessionFetcherService alloc] init];
|
|
service.allowLocalhostRequest = YES;
|
|
service.stopFetchingTriggersCompletionHandler = YES;
|
|
service.maxRunningFetchersPerHost = 1;
|
|
|
|
NSURL *fetchURL = [_testServer localURLForFile:kValidFileName];
|
|
|
|
// Use 5 fetchers:
|
|
// 1. Blocking auth, will complete successfully. When it completes, it will stop the 4th fetcher.
|
|
// 2. Uses an auth failing helper, will be stopped, so it shouldn't get to auth.
|
|
// 3. Uses an auth failing helper, will be stopped, so it shouldn't get to auth, also don't
|
|
// enable the callback.
|
|
// 4. No auth, just complete successfully.
|
|
// 5. No auth, just a complete, but will be stopped.
|
|
// This ensure they get started after each other and get to the point they should.
|
|
|
|
GTMSessionFetcher *fetcher1 = [service fetcherWithURL:fetchURL];
|
|
XCTestExpectation *authExpect = [self expectationWithDescription:@"Expect for auth block"];
|
|
fetcher1.authorizer = [TestAuthorizer asyncWithBlockedTimeout:1 testExpectation:authExpect];
|
|
|
|
GTMSessionFetcher *fetcher2 = [service fetcherWithURL:fetchURL];
|
|
fetcher2.authorizer = [TestAuthorizer syncAuthorizer];
|
|
((TestAuthorizer *)fetcher2.authorizer).workBlock = ^{
|
|
XCTFail(@"Fetcher2: Should not get here since it was stopped.");
|
|
};
|
|
|
|
GTMSessionFetcher *fetcher3 = [service fetcherWithURL:fetchURL];
|
|
fetcher3.stopFetchingTriggersCompletionHandler = NO; // Reset back to no
|
|
fetcher3.authorizer = [TestAuthorizer syncAuthorizer];
|
|
((TestAuthorizer *)fetcher3.authorizer).workBlock = ^{
|
|
XCTFail(@"Fetcher3: Should not get here since it was stopped.");
|
|
};
|
|
|
|
GTMSessionFetcher *fetcher4 = [service fetcherWithURL:fetchURL];
|
|
|
|
GTMSessionFetcher *fetcher5 = [service fetcherWithURL:fetchURL];
|
|
|
|
// Fetchers 1 and 3 will actually run, 2 and 4 should never fully start, so there should be two
|
|
// started/stopped notifications, but all 4 should get completion invoked.
|
|
XCTestExpectation *fetcherStartedExpectation =
|
|
[self expectationForNotification:kGTMSessionFetcherStartedNotification
|
|
object:nil
|
|
handler:nil];
|
|
fetcherStartedExpectation.expectedFulfillmentCount = 2;
|
|
XCTestExpectation *fetcherStoppedExpectation =
|
|
[self expectationForNotification:kGTMSessionFetcherStoppedNotification
|
|
object:nil
|
|
handler:nil];
|
|
fetcherStoppedExpectation.expectedFulfillmentCount = 2;
|
|
XCTestExpectation *fetcherCompletionInvokedExpecation =
|
|
[self expectationForNotification:kGTMSessionFetcherCompletionInvokedNotification
|
|
object:nil
|
|
handler:nil];
|
|
fetcherCompletionInvokedExpecation.expectedFulfillmentCount = 4;
|
|
|
|
// Nothing started
|
|
XCTAssertEqual(service.runningFetchersByHost.count, (NSUInteger)0);
|
|
XCTAssertEqual(service.delayedFetchersByHost.count, (NSUInteger)0);
|
|
|
|
XCTestExpectation *completionExpectation =
|
|
[self expectationWithDescription:@"Completion expection"];
|
|
completionExpectation.expectedFulfillmentCount = 4;
|
|
|
|
[fetcher1 beginFetchWithCompletionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
|
|
XCTAssertNotNil(data);
|
|
XCTAssertNil(error);
|
|
// and stop the 4th one.
|
|
[fetcher5 stopFetching];
|
|
[completionExpectation fulfill];
|
|
}];
|
|
|
|
[fetcher2 beginFetchWithCompletionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
|
|
XCTAssertNil(data);
|
|
XCTAssertNotNil(error);
|
|
XCTAssertEqual(error.domain, kGTMSessionFetcherErrorDomain);
|
|
XCTAssertEqual(error.code, GTMSessionFetcherErrorUserCancelled);
|
|
[completionExpectation fulfill];
|
|
}];
|
|
|
|
[fetcher3 beginFetchWithCompletionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
|
|
XCTFail(@"Should not get invoked for stopFetcher -- data: %@, error: %@", data, error);
|
|
}];
|
|
|
|
[fetcher4 beginFetchWithCompletionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
|
|
XCTAssertNotNil(data);
|
|
XCTAssertNil(error);
|
|
[completionExpectation fulfill];
|
|
}];
|
|
|
|
[fetcher5 beginFetchWithCompletionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
|
|
XCTAssertNil(data);
|
|
XCTAssertNotNil(error);
|
|
XCTAssertEqual(error.domain, kGTMSessionFetcherErrorDomain);
|
|
XCTAssertEqual(error.code, GTMSessionFetcherErrorUserCancelled);
|
|
[completionExpectation fulfill];
|
|
}];
|
|
|
|
// There should be one running for localhost, and four delayed for localhost.
|
|
XCTAssertEqual(service.runningFetchersByHost.count, (NSUInteger)1);
|
|
XCTAssertEqual(service.runningFetchersByHost[@"localhost"].count, 1);
|
|
XCTAssertEqual(service.delayedFetchersByHost.count, (NSUInteger)1, );
|
|
XCTAssertEqual(service.delayedFetchersByHost[@"localhost"].count, 4);
|
|
|
|
// Stop first, and then unblock the authorizer to let the rest happen.
|
|
[fetcher2 stopFetching];
|
|
[fetcher3 stopFetching];
|
|
[(TestAuthorizer *)fetcher1.authorizer unblock];
|
|
|
|
[self waitForExpectationsWithTimeout:_timeoutInterval handler:nil];
|
|
}
|
|
|
|
@end
|
|
|
|
#endif // !TARGET_OS_WATCH
|