aboutsummaryrefslogtreecommitdiffhomepage
path: root/GoogleUtilities/Example/Tests
diff options
context:
space:
mode:
authorGravatar Paul Beusterien <paulbeusterien@google.com>2018-07-12 12:03:50 -0700
committerGravatar GitHub <noreply@github.com>2018-07-12 12:03:50 -0700
commitc586dc8747882770973b6488c9f5f9e6e3f08d6c (patch)
tree511cda1bd0c67b94ab7bbb8ba22201fecf89909b /GoogleUtilities/Example/Tests
parent49f2493e14cd68ecc0e08ad2d9fc75739e419a3b (diff)
Separate Xcode project and tests for GoogleUtilities (#1521)
Diffstat (limited to 'GoogleUtilities/Example/Tests')
-rw-r--r--GoogleUtilities/Example/Tests/Environment/GULAppEnvironmentUtilTest.m62
-rw-r--r--GoogleUtilities/Example/Tests/Logger/GULLoggerTest.m210
-rw-r--r--GoogleUtilities/Example/Tests/Network/GULMutableDictionaryTest.m87
-rw-r--r--GoogleUtilities/Example/Tests/Network/GULNetworkTest.m998
-rw-r--r--GoogleUtilities/Example/Tests/Network/third_party/GTMHTTPServer.h173
-rw-r--r--GoogleUtilities/Example/Tests/Network/third_party/GTMHTTPServer.m630
-rw-r--r--GoogleUtilities/Example/Tests/Reachability/GULReachabilityCheckerTest.m358
-rw-r--r--GoogleUtilities/Example/Tests/Tests-Info.plist22
8 files changed, 2540 insertions, 0 deletions
diff --git a/GoogleUtilities/Example/Tests/Environment/GULAppEnvironmentUtilTest.m b/GoogleUtilities/Example/Tests/Environment/GULAppEnvironmentUtilTest.m
new file mode 100644
index 0000000..62a7bf8
--- /dev/null
+++ b/GoogleUtilities/Example/Tests/Environment/GULAppEnvironmentUtilTest.m
@@ -0,0 +1,62 @@
+// Copyright 2018 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import <OCMock/OCMock.h>
+#import <XCTest/XCTest.h>
+
+#import <GoogleUtilities/GULAppEnvironmentUtil.h>
+
+@interface GULAppEnvironmentUtilTest : XCTestCase
+
+@property(nonatomic) id processInfoMock;
+
+@end
+
+@implementation GULAppEnvironmentUtilTest
+
+- (void)setUp {
+ [super setUp];
+
+ _processInfoMock = OCMPartialMock([NSProcessInfo processInfo]);
+}
+
+- (void)tearDown {
+ [super tearDown];
+
+ [_processInfoMock stopMocking];
+}
+
+- (void)testSystemVersionInfoMajorOnly {
+ NSOperatingSystemVersion osTen = {.majorVersion = 10, .minorVersion = 0, .patchVersion = 0};
+ OCMStub([self.processInfoMock operatingSystemVersion]).andReturn(osTen);
+
+ XCTAssertTrue([[GULAppEnvironmentUtil systemVersion] isEqualToString:@"10.0"]);
+}
+
+- (void)testSystemVersionInfoMajorMinor {
+ NSOperatingSystemVersion osTenTwo = {.majorVersion = 10, .minorVersion = 2, .patchVersion = 0};
+ OCMStub([self.processInfoMock operatingSystemVersion]).andReturn(osTenTwo);
+
+ XCTAssertTrue([[GULAppEnvironmentUtil systemVersion] isEqualToString:@"10.2"]);
+}
+
+- (void)testSystemVersionInfoMajorMinorPatch {
+ NSOperatingSystemVersion osTenTwoOne = {.majorVersion = 10, .minorVersion = 2, .patchVersion = 1};
+ OCMStub([self.processInfoMock operatingSystemVersion]).andReturn(osTenTwoOne);
+
+ XCTAssertTrue([[GULAppEnvironmentUtil systemVersion] isEqualToString:@"10.2.1"]);
+}
+
+@end
diff --git a/GoogleUtilities/Example/Tests/Logger/GULLoggerTest.m b/GoogleUtilities/Example/Tests/Logger/GULLoggerTest.m
new file mode 100644
index 0000000..f65c06b
--- /dev/null
+++ b/GoogleUtilities/Example/Tests/Logger/GULLoggerTest.m
@@ -0,0 +1,210 @@
+// Copyright 2018 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifdef DEBUG
+// The tests depend upon library methods only built with #ifdef DEBUG
+
+#import <OCMock/OCMock.h>
+#import <XCTest/XCTest.h>
+
+#import <GoogleUtilities/GULLogger.h>
+
+#import <asl.h>
+
+extern NSString *const kGULPersistedDebugModeKey;
+
+extern const char *kGULLoggerASLClientFacilityName;
+
+extern void GULResetLogger(void);
+
+extern aslclient getGULLoggerClient(void);
+
+extern dispatch_queue_t getGULClientQueue(void);
+
+extern BOOL getGULLoggerDebugMode(void);
+
+static NSString *const kMessageCode = @"I-COR000001";
+
+@interface GULLoggerTest : XCTestCase
+
+@property(nonatomic) NSString *randomLogString;
+
+@property(nonatomic, strong) NSUserDefaults *defaults;
+
+@end
+
+@implementation GULLoggerTest
+
+- (void)setUp {
+ [super setUp];
+ GULResetLogger();
+
+ // Stub NSUserDefaults for cleaner testing.
+ _defaults = [[NSUserDefaults alloc] initWithSuiteName:@"com.google.logger_test"];
+}
+
+- (void)tearDown {
+ [super tearDown];
+
+ _defaults = nil;
+}
+
+- (void)testInitializeASLForDebugModeWithUserDefaults {
+ // Stub.
+ NSNumber *debugMode = @YES;
+ [self.defaults setBool:debugMode.boolValue forKey:kGULPersistedDebugModeKey];
+
+ // Test.
+ GULLogError(@"my service", NO, kMessageCode, @"Some error.");
+
+ // Assert.
+ debugMode = [self.defaults objectForKey:kGULPersistedDebugModeKey];
+ XCTAssertTrue(debugMode.boolValue);
+}
+
+- (void)testMessageCodeFormat {
+ // Valid case.
+ XCTAssertNoThrow(GULLogError(@"my service", NO, @"I-APP000001", @"Message."));
+
+ // An extra dash or missing dash should fail.
+ XCTAssertThrows(GULLogError(@"my service", NO, @"I-APP-000001", @"Message."));
+ XCTAssertThrows(GULLogError(@"my service", NO, @"IAPP000001", @"Message."));
+
+ // Wrong number of digits should fail.
+ XCTAssertThrows(GULLogError(@"my service", NO, @"I-APP00001", @"Message."));
+ XCTAssertThrows(GULLogError(@"my service", NO, @"I-APP0000001", @"Message."));
+
+ // Lowercase should fail.
+ XCTAssertThrows(GULLogError(@"my service", NO, @"I-app000001", @"Message."));
+
+// nil or empty message code should fail.
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnonnull"
+ XCTAssertThrows(GULLogError(@"my service", NO, nil, @"Message."));
+#pragma clang diagnostic pop
+
+ XCTAssertThrows(GULLogError(@"my service", NO, @"", @"Message."));
+
+ // Android message code should fail.
+ XCTAssertThrows(GULLogError(@"my service", NO, @"A-APP000001", @"Message."));
+}
+
+- (void)testLoggerInterface {
+ XCTAssertNoThrow(GULLogError(@"my service", NO, kMessageCode, @"Message."));
+ XCTAssertNoThrow(GULLogError(@"my service", NO, kMessageCode, @"Configure %@.", @"blah"));
+
+ XCTAssertNoThrow(GULLogWarning(@"my service", NO, kMessageCode, @"Message."));
+ XCTAssertNoThrow(GULLogWarning(@"my service", NO, kMessageCode, @"Configure %@.", @"blah"));
+
+ XCTAssertNoThrow(GULLogNotice(@"my service", NO, kMessageCode, @"Message."));
+ XCTAssertNoThrow(GULLogNotice(@"my service", NO, kMessageCode, @"Configure %@.", @"blah"));
+
+ XCTAssertNoThrow(GULLogInfo(@"my service", NO, kMessageCode, @"Message."));
+ XCTAssertNoThrow(GULLogInfo(@"my service", NO, kMessageCode, @"Configure %@.", @"blah"));
+
+ XCTAssertNoThrow(GULLogDebug(@"my service", NO, kMessageCode, @"Message."));
+ XCTAssertNoThrow(GULLogDebug(@"my service", NO, kMessageCode, @"Configure %@.", @"blah"));
+}
+
+// asl_set_filter does not perform as expected in unit test environment with simulator. The
+// following test only checks whether the logs have been sent to system with the default settings in
+// the unit test environment.
+- (void)testSystemLogWithDefaultStatus {
+#if !(BUG128) // Disable until https://github.com/firebase/firebase-ios-sdk/issues/128 is fixed
+ // Test fails on device and iOS 9 simulators - b/38130372
+ return;
+#else
+ // Sets the time interval that we need to wait in order to fetch all the logs.
+ NSTimeInterval timeInterval = 0.1f;
+ // Generates a random string each time and check whether it has been logged.
+ // Log messages with Notice level and below should be logged to system/device by default.
+ self.randomLogString = [NSUUID UUID].UUIDString;
+ GULLogError(@"my service", NO, kMessageCode, @"%@", self.randomLogString);
+ [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:timeInterval]];
+ XCTAssertTrue([self logExists]);
+
+ self.randomLogString = [NSUUID UUID].UUIDString;
+ GULLogWarning(@"my service", kMessageCode, @"%@", self.randomLogString);
+ [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:timeInterval]];
+ XCTAssertTrue([self logExists]);
+
+ self.randomLogString = [NSUUID UUID].UUIDString;
+ GULLogNotice(@"my service", kMessageCode, @"%@", self.randomLogString);
+ [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:timeInterval]];
+ XCTAssertTrue([self logExists]);
+
+ // Log messages with Info level and above should NOT be logged to system/device by default.
+ self.randomLogString = [NSUUID UUID].UUIDString;
+ GULLogInfo(@"my service", kMessageCode, @"%@", self.randomLogString);
+ [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:timeInterval]];
+ XCTAssertFalse([self logExists]);
+
+ self.randomLogString = [NSUUID UUID].UUIDString;
+ GULLogDebug(@"my service", kMessageCode, @"%@", self.randomLogString);
+ [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:timeInterval]];
+ XCTAssertFalse([self logExists]);
+#endif
+}
+
+// The GULLoggerLevel enum must match the ASL_LEVEL_* constants, but we manually redefine
+// them in GULLoggerLevel.h since we cannot include <asl.h> (see b/34976089 for more details).
+// This test ensures the constants match.
+- (void)testGULLoggerLevelValues {
+ XCTAssertEqual(GULLoggerLevelError, ASL_LEVEL_ERR);
+ XCTAssertEqual(GULLoggerLevelWarning, ASL_LEVEL_WARNING);
+ XCTAssertEqual(GULLoggerLevelNotice, ASL_LEVEL_NOTICE);
+ XCTAssertEqual(GULLoggerLevelInfo, ASL_LEVEL_INFO);
+ XCTAssertEqual(GULLoggerLevelDebug, ASL_LEVEL_DEBUG);
+}
+
+// Helper functions.
+- (BOOL)logExists {
+ [self drainGULClientQueue];
+ NSString *correctMsg =
+ [NSString stringWithFormat:@"%@[%@] %@", @"my service", kMessageCode, self.randomLogString];
+ return [self messageWasLogged:correctMsg];
+}
+
+- (void)drainGULClientQueue {
+ dispatch_semaphore_t workerSemaphore = dispatch_semaphore_create(0);
+ dispatch_async(getGULClientQueue(), ^{
+ dispatch_semaphore_signal(workerSemaphore);
+ });
+ dispatch_semaphore_wait(workerSemaphore, DISPATCH_TIME_FOREVER);
+}
+
+- (BOOL)messageWasLogged:(NSString *)message {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ aslmsg query = asl_new(ASL_TYPE_QUERY);
+ asl_set_query(query, ASL_KEY_FACILITY, kGULLoggerASLClientFacilityName, ASL_QUERY_OP_EQUAL);
+ aslresponse r = asl_search(getGULLoggerClient(), query);
+ asl_free(query);
+ aslmsg m;
+ const char *val;
+ NSMutableArray *allMsg = [[NSMutableArray alloc] init];
+ while ((m = asl_next(r)) != NULL) {
+ val = asl_get(m, ASL_KEY_MSG);
+ if (val) {
+ [allMsg addObject:[NSString stringWithUTF8String:val]];
+ }
+ }
+ asl_free(m);
+ asl_release(r);
+ return [allMsg containsObject:message];
+#pragma clang pop
+}
+
+@end
+#endif
diff --git a/GoogleUtilities/Example/Tests/Network/GULMutableDictionaryTest.m b/GoogleUtilities/Example/Tests/Network/GULMutableDictionaryTest.m
new file mode 100644
index 0000000..6378618
--- /dev/null
+++ b/GoogleUtilities/Example/Tests/Network/GULMutableDictionaryTest.m
@@ -0,0 +1,87 @@
+// Copyright 2018 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <XCTest/XCTest.h>
+
+#import <GoogleUtilities/GULMutableDictionary.h>
+
+const static NSString *const kKey = @"testKey1";
+const static NSString *const kValue = @"testValue1";
+const static NSString *const kKey2 = @"testKey2";
+const static NSString *const kValue2 = @"testValue2";
+
+@interface GULMutableDictionaryTest : XCTestCase
+@property(nonatomic) GULMutableDictionary *dictionary;
+@end
+
+@implementation GULMutableDictionaryTest
+
+- (void)setUp {
+ [super setUp];
+ self.dictionary = [[GULMutableDictionary alloc] init];
+}
+
+- (void)tearDown {
+ self.dictionary = nil;
+ [super tearDown];
+}
+
+- (void)testSetGetAndRemove {
+ XCTAssertNil([self.dictionary objectForKey:kKey]);
+ [self.dictionary setObject:kValue forKey:kKey];
+ XCTAssertEqual(kValue, [self.dictionary objectForKey:kKey]);
+ [self.dictionary removeObjectForKey:kKey];
+ XCTAssertNil([self.dictionary objectForKey:kKey]);
+}
+
+- (void)testSetGetAndRemoveKeyed {
+ XCTAssertNil(self.dictionary[kKey]);
+ self.dictionary[kKey] = kValue;
+ XCTAssertEqual(kValue, self.dictionary[kKey]);
+ [self.dictionary removeObjectForKey:kKey];
+ XCTAssertNil(self.dictionary[kKey]);
+}
+
+- (void)testRemoveAll {
+ XCTAssertNil(self.dictionary[kKey]);
+ XCTAssertNil(self.dictionary[kKey2]);
+ self.dictionary[kKey] = kValue;
+ self.dictionary[kKey2] = kValue2;
+ [self.dictionary removeAllObjects];
+ XCTAssertNil(self.dictionary[kKey]);
+ XCTAssertNil(self.dictionary[kKey2]);
+}
+
+- (void)testCount {
+ XCTAssertEqual([self.dictionary count], 0);
+ self.dictionary[kKey] = kValue;
+ XCTAssertEqual([self.dictionary count], 1);
+ self.dictionary[kKey2] = kValue2;
+ XCTAssertEqual([self.dictionary count], 2);
+ [self.dictionary removeAllObjects];
+ XCTAssertEqual([self.dictionary count], 0);
+}
+
+- (void)testUnderlyingDictionary {
+ XCTAssertEqual([self.dictionary count], 0);
+ self.dictionary[kKey] = kValue;
+ self.dictionary[kKey2] = kValue2;
+
+ NSDictionary *dict = self.dictionary.dictionary;
+ XCTAssertEqual([dict count], 2);
+ XCTAssertEqual(dict[kKey], kValue);
+ XCTAssertEqual(dict[kKey2], kValue2);
+}
+
+@end
diff --git a/GoogleUtilities/Example/Tests/Network/GULNetworkTest.m b/GoogleUtilities/Example/Tests/Network/GULNetworkTest.m
new file mode 100644
index 0000000..4d31503
--- /dev/null
+++ b/GoogleUtilities/Example/Tests/Network/GULNetworkTest.m
@@ -0,0 +1,998 @@
+// Copyright 2018 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "GTMHTTPServer.h"
+
+#import <OCMock/OCMock.h>
+#import <XCTest/XCTest.h>
+
+#import <GoogleUtilities/GULNSData+zlib.h>
+#import <GoogleUtilities/GULNetwork.h>
+#import <GoogleUtilities/GULReachabilityChecker.h>
+
+@interface GULNetwork ()
+
+- (void)reachability:(GULReachabilityChecker *)reachability
+ statusChanged:(GULReachabilityStatus)status;
+
+@end
+
+@interface GULNetworkURLSession ()
+
+- (void)maybeRemoveTempFilesAtURL:(NSURL *)tempFile expiringTime:(NSTimeInterval)expiringTime;
+
+@end
+
+@interface GULNetworkTest : XCTestCase <GULNetworkReachabilityDelegate>
+@end
+
+@implementation GULNetworkTest {
+ dispatch_queue_t _backgroundQueue;
+ GULNetwork *_network;
+
+ /// Fake Server.
+ GTMHTTPServer *_httpServer;
+ GTMHTTPRequestMessage *_request;
+ int _statusCode;
+
+ // For network reachability test.
+ BOOL _fakeNetworkIsReachable;
+ BOOL _currentNetworkStatus;
+ GULReachabilityStatus _fakeReachabilityStatus;
+}
+
+#pragma mark - Setup and teardown
+
+- (void)setUp {
+ [super setUp];
+
+ _fakeNetworkIsReachable = YES;
+ _statusCode = 200;
+ _request = nil;
+
+ _httpServer = [[GTMHTTPServer alloc] initWithDelegate:self];
+
+ // Start the server.
+ NSError *error = nil;
+ XCTAssertTrue([_httpServer start:&error], @"Failed to start HTTP server: %@", error);
+
+ _network = [[GULNetwork alloc] init];
+ _backgroundQueue = dispatch_queue_create("Test queue", DISPATCH_QUEUE_SERIAL);
+
+ _request = nil;
+}
+
+- (void)tearDown {
+ _network = nil;
+ _backgroundQueue = nil;
+ _request = nil;
+
+ [_httpServer stop];
+ _httpServer = nil;
+
+ [super tearDown];
+}
+
+#pragma mark - Test reachability
+
+- (void)testReachability {
+ _network.reachabilityDelegate = self;
+
+ id reachability = [_network valueForKey:@"_reachability"];
+ XCTAssertNotNil(reachability);
+
+ id reachabilityMock = OCMPartialMock(reachability);
+ [[[reachabilityMock stub] andCall:@selector(reachabilityStatus) onObject:self]
+ reachabilityStatus];
+
+ // Fake scenario with connectivity.
+ _fakeNetworkIsReachable = YES;
+ _fakeReachabilityStatus = kGULReachabilityViaWifi;
+ [_network reachability:reachabilityMock statusChanged:[reachabilityMock reachabilityStatus]];
+ XCTAssertTrue([_network isNetworkConnected]);
+ XCTAssertEqual(_currentNetworkStatus, _fakeNetworkIsReachable);
+
+ // Fake scenario without connectivity.
+ _fakeNetworkIsReachable = NO;
+ _fakeReachabilityStatus = kGULReachabilityNotReachable;
+ [_network reachability:reachabilityMock statusChanged:[reachabilityMock reachabilityStatus]];
+ XCTAssertFalse([_network isNetworkConnected]);
+ XCTAssertEqual(_currentNetworkStatus, _fakeNetworkIsReachable);
+
+ [reachabilityMock stopMocking];
+ reachabilityMock = nil;
+}
+
+#pragma mark - Test POST Foreground
+
+- (void)testSessionNetwork_POST_foreground {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+
+ NSData *uncompressedData = [@"Google" dataUsingEncoding:NSUTF8StringEncoding];
+
+ NSURL *url =
+ [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%d/2", _httpServer.port]];
+ _statusCode = 200;
+
+ [_network postURL:url
+ payload:uncompressedData
+ queue:_backgroundQueue
+ usingBackgroundSession:NO
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ [self verifyResponse:response error:error];
+ [self verifyRequest];
+ XCTAssertFalse(self->_network.hasUploadInProgress, "There must be no pending request");
+ [expectation fulfill];
+ }];
+ XCTAssertTrue(self->_network.hasUploadInProgress, "There must be a pending request");
+ // Wait a little bit so the server has enough time to respond.
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testSessionNetworkShouldReturnError_POST_foreground {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+
+ NSData *uncompressedData = [@"Google" dataUsingEncoding:NSUTF8StringEncoding];
+ NSURL *url =
+ [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%d/3", _httpServer.port]];
+ _statusCode = 500;
+
+ [_network postURL:url
+ payload:uncompressedData
+ queue:_backgroundQueue
+ usingBackgroundSession:NO
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ XCTAssertEqual(((NSHTTPURLResponse *)response).statusCode, 500);
+ XCTAssertFalse(self->_network.hasUploadInProgress, "There must be no pending request");
+ [expectation fulfill];
+ }];
+ XCTAssertTrue(self->_network.hasUploadInProgress, "There must be a pending request");
+ // Wait a little bit so the server has enough time to respond.
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testNilURLNSURLSession_POST_foreground {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+
+ NSData *uncompressedData = [@"Google" dataUsingEncoding:NSUTF8StringEncoding];
+ _statusCode = 200;
+
+ [_network postURL:nil
+ payload:uncompressedData
+ queue:_backgroundQueue
+ usingBackgroundSession:NO
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ XCTAssertEqual(error.code, GULErrorCodeNetworkInvalidURL);
+ XCTAssertFalse(self->_network.hasUploadInProgress, "There must be no pending request");
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testEmptyURLNSURLSession_POST_foreground {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+ NSData *uncompressedData = [@"Google" dataUsingEncoding:NSUTF8StringEncoding];
+ _statusCode = 200;
+
+ [_network postURL:[NSURL URLWithString:@""]
+ payload:uncompressedData
+ queue:_backgroundQueue
+ usingBackgroundSession:NO
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ XCTAssertEqual(error.code, GULErrorCodeNetworkInvalidURL);
+ XCTAssertFalse(self->_network.hasUploadInProgress, "There must be no pending request");
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testEmptyPayloadNSURLSession_POST_foreground {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+ NSData *uncompressedData = [[NSData alloc] init];
+ NSURL *url =
+ [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%d/2", _httpServer.port]];
+ _statusCode = 200;
+
+ [_network postURL:url
+ payload:uncompressedData
+ queue:_backgroundQueue
+ usingBackgroundSession:NO
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertNotNil(self->_request);
+ XCTAssertEqualObjects([self->_request.URL absoluteString], [url absoluteString]);
+ XCTAssertFalse(self->_network.hasUploadInProgress, "There must be no pending request");
+ [expectation fulfill];
+ }];
+ XCTAssertTrue(self->_network.hasUploadInProgress, "There must be a pending request");
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testNilQueueNSURLSession_POST_foreground {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+
+ NSData *uncompressedData = [@"Google" dataUsingEncoding:NSUTF8StringEncoding];
+ NSURL *url =
+ [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%d/1", _httpServer.port]];
+ _statusCode = 200;
+
+ [_network postURL:url
+ payload:uncompressedData
+ queue:nil
+ usingBackgroundSession:NO
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ [self verifyResponse:response error:error];
+ [self verifyRequest];
+ [expectation fulfill];
+ }];
+
+ // Wait a little bit so the server has enough time to respond.
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testHasRequestPendingNSURLSession_POST_foreground {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+ NSData *uncompressedData = [@"Google" dataUsingEncoding:NSUTF8StringEncoding];
+ NSURL *url =
+ [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%d/hasRequestPending",
+ _httpServer.port]];
+ _statusCode = 200;
+
+ [_network postURL:url
+ payload:uncompressedData
+ queue:_backgroundQueue
+ usingBackgroundSession:NO
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ [self verifyResponse:response error:error];
+ [self verifyRequest];
+
+ XCTAssertFalse(self->_network.hasUploadInProgress,
+ @"hasUploadInProgress must be false");
+ [expectation fulfill];
+ }];
+
+ XCTAssertTrue(self->_network.hasUploadInProgress, @"hasUploadInProgress must be true");
+ // Wait a little bit so the server has enough time to respond.
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+#pragma mark - Test POST Background
+
+- (void)testSessionNetwork_POST_background {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+
+ NSData *uncompressedData = [@"Google" dataUsingEncoding:NSUTF8StringEncoding];
+ NSURL *url =
+ [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%d/2", _httpServer.port]];
+ _statusCode = 200;
+
+ [_network postURL:url
+ payload:uncompressedData
+ queue:_backgroundQueue
+ usingBackgroundSession:YES
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ [self verifyResponse:response error:error];
+ [self verifyRequest];
+ XCTAssertFalse(self->_network.hasUploadInProgress, "There must be no pending request");
+ [expectation fulfill];
+ }];
+ XCTAssertTrue(self->_network.hasUploadInProgress, "There must be a pending request");
+ // Wait a little bit so the server has enough time to respond.
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testSessionNetworkShouldReturnError_POST_background {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+
+ NSData *uncompressedData = [@"Google" dataUsingEncoding:NSUTF8StringEncoding];
+ NSURL *url =
+ [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%d/3", _httpServer.port]];
+ _statusCode = 500;
+
+ [_network postURL:url
+ payload:uncompressedData
+ queue:_backgroundQueue
+ usingBackgroundSession:YES
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ XCTAssertEqual(((NSHTTPURLResponse *)response).statusCode, 500);
+ XCTAssertFalse(self->_network.hasUploadInProgress, "There must be no pending request");
+ [expectation fulfill];
+ }];
+ XCTAssertTrue(self->_network.hasUploadInProgress, "There must be a pending request");
+ // Wait a little bit so the server has enough time to respond.
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testNilURLNSURLSession_POST_background {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+
+ NSData *uncompressedData = [@"Google" dataUsingEncoding:NSUTF8StringEncoding];
+ _statusCode = 200;
+
+ [_network postURL:nil
+ payload:uncompressedData
+ queue:_backgroundQueue
+ usingBackgroundSession:YES
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ XCTAssertEqual(error.code, GULErrorCodeNetworkInvalidURL);
+ XCTAssertFalse(self->_network.hasUploadInProgress, "There must be no pending request");
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testEmptyURLNSURLSession_POST_background {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+ NSData *uncompressedData = [@"Google" dataUsingEncoding:NSUTF8StringEncoding];
+ _statusCode = 200;
+
+ [_network postURL:[NSURL URLWithString:@""]
+ payload:uncompressedData
+ queue:_backgroundQueue
+ usingBackgroundSession:YES
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ XCTAssertEqual(error.code, GULErrorCodeNetworkInvalidURL);
+ XCTAssertFalse(self->_network.hasUploadInProgress, "There must be no pending request");
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testEmptyPayloadNSURLSession_POST_background {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+ NSData *uncompressedData = [[NSData alloc] init];
+ NSURL *url =
+ [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%d/2", _httpServer.port]];
+ _statusCode = 200;
+
+ [_network postURL:url
+ payload:uncompressedData
+ queue:_backgroundQueue
+ usingBackgroundSession:YES
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertNotNil(self->_request);
+ XCTAssertEqualObjects([self->_request.URL absoluteString], [url absoluteString]);
+ XCTAssertFalse(self->_network.hasUploadInProgress, "There must be no pending request");
+ [expectation fulfill];
+ }];
+ XCTAssertTrue(self->_network.hasUploadInProgress, "There must be a pending request");
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testNilQueueNSURLSession_POST_background {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+
+ NSData *uncompressedData = [@"Google" dataUsingEncoding:NSUTF8StringEncoding];
+ NSURL *url =
+ [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%d/1", _httpServer.port]];
+ _statusCode = 200;
+
+ [_network postURL:url
+ payload:uncompressedData
+ queue:nil
+ usingBackgroundSession:YES
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ [self verifyResponse:response error:error];
+ [self verifyRequest];
+ [expectation fulfill];
+ }];
+
+ // Wait a little bit so the server has enough time to respond.
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testHasRequestPendingNSURLSession_POST_background {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+ NSData *uncompressedData = [@"Google" dataUsingEncoding:NSUTF8StringEncoding];
+ NSURL *url =
+ [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%d/hasRequestPending",
+ _httpServer.port]];
+ _statusCode = 200;
+
+ [_network postURL:url
+ payload:uncompressedData
+ queue:_backgroundQueue
+ usingBackgroundSession:YES
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ [self verifyResponse:response error:error];
+ [self verifyRequest];
+
+ XCTAssertFalse(self->_network.hasUploadInProgress,
+ @"hasUploadInProgress must be false");
+ [expectation fulfill];
+ }];
+
+ XCTAssertTrue(self->_network.hasUploadInProgress, @"hasUploadInProgress must be true");
+ // Wait a little bit so the server has enough time to respond.
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+#pragma mark - GET Methods Foreground
+
+- (void)testSessionNetworkAsync_GET_foreground {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+
+ NSURL *url =
+ [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%d/2", _httpServer.port]];
+ _statusCode = 200;
+
+ [_network getURL:url
+ headers:nil
+ queue:_backgroundQueue
+ usingBackgroundSession:NO
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ XCTAssertNotNil(data);
+ NSString *responseBody =
+ [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ XCTAssertEqualObjects(responseBody, @"<html><body>Hello, World!</body></html>");
+ XCTAssertNil(error);
+ XCTAssertFalse(self->_network.hasUploadInProgress, "There must be no pending request");
+ [expectation fulfill];
+ }];
+ XCTAssertTrue(self->_network.hasUploadInProgress, "There must be a pending request");
+ // Wait a little bit so the server has enough time to respond.
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testSessionNetworkShouldReturnError_GET_foreground {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+ NSURL *url =
+ [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%d/3", _httpServer.port]];
+ _statusCode = 500;
+
+ [_network getURL:url
+ headers:nil
+ queue:_backgroundQueue
+ usingBackgroundSession:NO
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ XCTAssertEqual(((NSHTTPURLResponse *)response).statusCode, 500);
+ XCTAssertFalse(self->_network.hasUploadInProgress, "There must be no pending request");
+ [expectation fulfill];
+ }];
+ XCTAssertTrue(self->_network.hasUploadInProgress, "There must be a pending request");
+ // Wait a little bit so the server has enough time to respond.
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testNilURLNSURLSession_GET_foreground {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+ _statusCode = 200;
+
+ [_network getURL:nil
+ headers:nil
+ queue:_backgroundQueue
+ usingBackgroundSession:NO
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ XCTAssertEqual(error.code, GULErrorCodeNetworkInvalidURL);
+ XCTAssertFalse(self->_network.hasUploadInProgress, "There must be no pending request");
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testEmptyURLNSURLSession_GET_foreground {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+ _statusCode = 200;
+
+ [_network getURL:[NSURL URLWithString:@""]
+ headers:nil
+ queue:_backgroundQueue
+ usingBackgroundSession:NO
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ XCTAssertEqual(error.code, GULErrorCodeNetworkInvalidURL);
+ XCTAssertFalse(self->_network.hasUploadInProgress, "There must be no pending request");
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testNilQueueNSURLSession_GET_foreground {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+
+ NSURL *url =
+ [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%d/1", _httpServer.port]];
+ _statusCode = 200;
+
+ [_network getURL:url
+ headers:nil
+ queue:nil
+ usingBackgroundSession:NO
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ XCTAssertNotNil(data);
+ NSString *responseBody =
+ [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ XCTAssertEqualObjects(responseBody, @"<html><body>Hello, World!</body></html>");
+ XCTAssertNil(error);
+ XCTAssertFalse(self->_network.hasUploadInProgress, "There must be no pending request");
+ [expectation fulfill];
+ }];
+ XCTAssertTrue(self->_network.hasUploadInProgress, "There must be a pending request");
+ // Wait a little bit so the server has enough time to respond.
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testHasRequestPendingNSURLSession_GET_foreground {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+ NSURL *url =
+ [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%d/hasRequestPending",
+ _httpServer.port]];
+ _statusCode = 200;
+
+ [_network getURL:url
+ headers:nil
+ queue:_backgroundQueue
+ usingBackgroundSession:NO
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ XCTAssertNotNil(data);
+ NSString *responseBody =
+ [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ XCTAssertEqualObjects(responseBody, @"<html><body>Hello, World!</body></html>");
+ XCTAssertNil(error);
+ XCTAssertFalse(self->_network.hasUploadInProgress,
+ @"hasUploadInProgress must be false");
+ [expectation fulfill];
+ }];
+
+ XCTAssertTrue(self->_network.hasUploadInProgress, @"hasUploadInProgress must be true");
+ // Wait a little bit so the server has enough time to respond.
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testHeaders_foreground {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+
+ NSURL *url =
+ [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%d/3", _httpServer.port]];
+ _statusCode = 200;
+
+ NSDictionary *headers = @{@"Version" : @"123"};
+
+ [_network getURL:url
+ headers:headers
+ queue:_backgroundQueue
+ usingBackgroundSession:NO
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ XCTAssertNotNil(data);
+ NSString *responseBody =
+ [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ XCTAssertEqualObjects(responseBody, @"<html><body>Hello, World!</body></html>");
+ XCTAssertNil(error);
+
+ NSString *version = [self->_request.allHeaderFieldValues valueForKey:@"Version"];
+ XCTAssertEqualObjects(version, @"123");
+
+ [expectation fulfill];
+ }];
+
+ // Wait a little bit so the server has enough time to respond.
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+#pragma mark - GET Methods Background
+
+- (void)testSessionNetworkAsync_GET_background {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+
+ NSURL *url =
+ [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%d/2", _httpServer.port]];
+ _statusCode = 200;
+
+ [_network getURL:url
+ headers:nil
+ queue:_backgroundQueue
+ usingBackgroundSession:YES
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ XCTAssertNotNil(data);
+ NSString *responseBody =
+ [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ XCTAssertEqualObjects(responseBody, @"<html><body>Hello, World!</body></html>");
+ XCTAssertNil(error);
+ XCTAssertFalse(self->_network.hasUploadInProgress, "There must be no pending request");
+ [expectation fulfill];
+ }];
+ XCTAssertTrue(self->_network.hasUploadInProgress, "There must be a pending request");
+ // Wait a little bit so the server has enough time to respond.
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testSessionNetworkShouldReturnError_GET_background {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+ NSURL *url =
+ [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%d/3", _httpServer.port]];
+ _statusCode = 500;
+
+ [_network getURL:url
+ headers:nil
+ queue:_backgroundQueue
+ usingBackgroundSession:YES
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ XCTAssertEqual(((NSHTTPURLResponse *)response).statusCode, 500);
+ XCTAssertFalse(self->_network.hasUploadInProgress, "There must be no pending request");
+ [expectation fulfill];
+ }];
+ XCTAssertTrue(self->_network.hasUploadInProgress, "There must be a pending request");
+ // Wait a little bit so the server has enough time to respond.
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testNilURLNSURLSession_GET_background {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+ _statusCode = 200;
+
+ [_network getURL:nil
+ headers:nil
+ queue:_backgroundQueue
+ usingBackgroundSession:YES
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ XCTAssertEqual(error.code, GULErrorCodeNetworkInvalidURL);
+ XCTAssertFalse(self->_network.hasUploadInProgress, "There must be no pending request");
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testEmptyURLNSURLSession_GET_background {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+ _statusCode = 200;
+
+ [_network getURL:[NSURL URLWithString:@""]
+ headers:nil
+ queue:_backgroundQueue
+ usingBackgroundSession:YES
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ XCTAssertEqual(error.code, GULErrorCodeNetworkInvalidURL);
+ XCTAssertFalse(self->_network.hasUploadInProgress, "There must be no pending request");
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testNilQueueNSURLSession_GET_background {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+
+ NSURL *url =
+ [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%d/1", _httpServer.port]];
+ _statusCode = 200;
+
+ [_network getURL:url
+ headers:nil
+ queue:nil
+ usingBackgroundSession:YES
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ XCTAssertNotNil(data);
+ NSString *responseBody =
+ [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ XCTAssertEqualObjects(responseBody, @"<html><body>Hello, World!</body></html>");
+ XCTAssertNil(error);
+ XCTAssertFalse(self->_network.hasUploadInProgress, "There must be no pending request");
+ [expectation fulfill];
+ }];
+ XCTAssertTrue(self->_network.hasUploadInProgress, "There must be a pending request");
+ // Wait a little bit so the server has enough time to respond.
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testHasRequestPendingNSURLSession_GET_background {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+ NSURL *url =
+ [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%d/hasRequestPending",
+ _httpServer.port]];
+ _statusCode = 200;
+
+ [_network getURL:url
+ headers:nil
+ queue:_backgroundQueue
+ usingBackgroundSession:YES
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ XCTAssertNotNil(data);
+ NSString *responseBody =
+ [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ XCTAssertEqualObjects(responseBody, @"<html><body>Hello, World!</body></html>");
+ XCTAssertNil(error);
+ XCTAssertFalse(self->_network.hasUploadInProgress,
+ @"hasUploadInProgress must be false");
+ [expectation fulfill];
+ }];
+
+ XCTAssertTrue(self->_network.hasUploadInProgress, @"hasUploadInProgress must be true");
+ // Wait a little bit so the server has enough time to respond.
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+- (void)testHeaders_background {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Expect block is called"];
+
+ NSURL *url =
+ [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%d/3", _httpServer.port]];
+ _statusCode = 200;
+
+ NSDictionary *headers = @{@"Version" : @"123"};
+
+ [_network getURL:url
+ headers:headers
+ queue:_backgroundQueue
+ usingBackgroundSession:YES
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
+ XCTAssertNotNil(data);
+ NSString *responseBody =
+ [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ XCTAssertEqualObjects(responseBody, @"<html><body>Hello, World!</body></html>");
+ XCTAssertNil(error);
+
+ NSString *version = [self->_request.allHeaderFieldValues valueForKey:@"Version"];
+ XCTAssertEqualObjects(version, @"123");
+
+ [expectation fulfill];
+ }];
+
+ // Wait a little bit so the server has enough time to respond.
+ [self waitForExpectationsWithTimeout:10
+ handler:^(NSError *error) {
+ if (error) {
+ XCTFail(@"Timeout Error: %@", error);
+ }
+ }];
+}
+
+#pragma mark - Test clean up files
+
+- (void)testRemoveExpiredFiles {
+ NSError *writeError = nil;
+ NSFileManager *fileManager = [NSFileManager defaultManager];
+
+ GULNetworkURLSession *session = [[GULNetworkURLSession alloc]
+ initWithNetworkLoggerDelegate:(id<GULNetworkLoggerDelegate>)_network];
+ NSArray *paths =
+ NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
+ NSString *applicationSupportDirectory = paths.firstObject;
+ NSArray *tempPathComponents = @[
+ applicationSupportDirectory, kGULNetworkApplicationSupportSubdirectory,
+ @"GULNetworkTemporaryDirectory"
+ ];
+ NSURL *folderURL = [NSURL fileURLWithPathComponents:tempPathComponents];
+ [fileManager createDirectoryAtURL:folderURL
+ withIntermediateDirectories:YES
+ attributes:nil
+ error:&writeError];
+
+ NSURL *tempFile1 = [folderURL URLByAppendingPathComponent:@"FIRUpload_temp_123"];
+ [self createTempFileAtURL:tempFile1];
+ NSURL *tempFile2 = [folderURL URLByAppendingPathComponent:@"FIRUpload_temp_456"];
+ [self createTempFileAtURL:tempFile2];
+
+ XCTAssertTrue([fileManager fileExistsAtPath:tempFile1.path]);
+ XCTAssertTrue([fileManager fileExistsAtPath:tempFile2.path]);
+
+ NSDate *now =
+ [[NSDate date] dateByAddingTimeInterval:1]; // Start mocking the clock to avoid flakiness.
+ id mockDate = OCMStrictClassMock([NSDate class]);
+ [[[mockDate stub] andReturn:now] date];
+
+ // The file should not be removed since it is not expired yet.
+ [session maybeRemoveTempFilesAtURL:folderURL expiringTime:20];
+ XCTAssertTrue([fileManager fileExistsAtPath:tempFile1.path]);
+ XCTAssertTrue([fileManager fileExistsAtPath:tempFile2.path]);
+
+ [mockDate stopMocking];
+ mockDate = nil;
+
+ now = [[NSDate date] dateByAddingTimeInterval:100]; // Move forward in time 100s.
+ mockDate = OCMStrictClassMock([NSDate class]);
+ [[[mockDate stub] andReturn:now] date];
+
+ [session maybeRemoveTempFilesAtURL:folderURL expiringTime:20];
+ XCTAssertFalse([fileManager fileExistsAtPath:tempFile1.path]);
+ XCTAssertFalse([fileManager fileExistsAtPath:tempFile2.path]);
+ [mockDate stopMocking];
+ mockDate = nil;
+}
+
+#pragma mark - Internal Methods
+
+- (void)createTempFileAtURL:(NSURL *)fileURL {
+ // Create a dictionary and write it to file.
+ NSDictionary *someContent = @{@"object" : @"key"};
+ [someContent writeToURL:fileURL atomically:YES];
+}
+
+- (void)verifyResponse:(NSHTTPURLResponse *)response error:(NSError *)error {
+ XCTAssertNil(error, @"Error is not expected");
+ XCTAssertNotNil(response, @"Error is not expected");
+}
+
+- (void)verifyRequest {
+ XCTAssertNotNil(_request, @"Request cannot be nil");
+
+ // Test whether the request is compressed correctly.
+ NSData *requestBody = [_request body];
+ NSData *decompressedRequestData = [NSData gul_dataByInflatingGzippedData:requestBody error:NULL];
+ NSString *requestString =
+ [[NSString alloc] initWithData:decompressedRequestData encoding:NSUTF8StringEncoding];
+ XCTAssertEqualObjects(requestString, @"Google", @"Request is not compressed correctly.");
+
+ // The request has to be a POST.
+ XCTAssertEqualObjects([_request method], @"POST", @"Request method has to be POST");
+
+ // Content length has to be set correctly.
+ NSString *contentLength = [_request.allHeaderFieldValues valueForKey:@"Content-Length"];
+ XCTAssertEqualObjects(contentLength, @"26", @"Content Length is incorrect");
+
+ NSString *contentEncoding = [_request.allHeaderFieldValues valueForKey:@"Content-Encoding"];
+ XCTAssertEqualObjects(contentEncoding, @"gzip", @"Content Encoding is incorrect");
+}
+
+#pragma mark - Helper Methods
+
+- (GTMHTTPResponseMessage *)httpServer:(GTMHTTPServer *)server
+ handleRequest:(GTMHTTPRequestMessage *)request {
+ _request = request;
+
+ NSData *html =
+ [@"<html><body>Hello, World!</body></html>" dataUsingEncoding:NSUTF8StringEncoding];
+ return [GTMHTTPResponseMessage responseWithBody:html
+ contentType:@"text/html; charset=UTF-8"
+ statusCode:_statusCode];
+}
+
+- (BOOL)isReachable {
+ return _fakeNetworkIsReachable;
+}
+
+- (GULReachabilityStatus)reachabilityStatus {
+ return _fakeReachabilityStatus;
+}
+
+#pragma mark - FIRReachabilityDelegate
+
+- (void)reachabilityDidChange {
+ _currentNetworkStatus = _fakeNetworkIsReachable;
+}
+
+@end
diff --git a/GoogleUtilities/Example/Tests/Network/third_party/GTMHTTPServer.h b/GoogleUtilities/Example/Tests/Network/third_party/GTMHTTPServer.h
new file mode 100644
index 0000000..cc51207
--- /dev/null
+++ b/GoogleUtilities/Example/Tests/Network/third_party/GTMHTTPServer.h
@@ -0,0 +1,173 @@
+/* Copyright 2010 Google Inc.
+ *
+ * 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.
+ */
+
+//
+// GTMHTTPServer.h
+//
+// This is a *very* *simple* webserver that can be built into something, it is
+// not meant to stand up a site, it sends all requests to its delegate for
+// processing on the main thread. It does not support pipelining, etc. It's
+// great for places where you need a simple webserver to unittest some code
+// that hits a server.
+//
+// NOTE: there are several TODOs left in here as markers for things that could
+// be done if one wanted to add more to this class.
+//
+// Based a little on HTTPServer, part of the CocoaHTTPServer sample code found at
+// https://opensource.apple.com/source/HTTPServer/HTTPServer-11/CocoaHTTPServer/
+// License for the CocoaHTTPServer sample code:
+//
+// Software License Agreement (BSD License)
+//
+// Copyright (c) 2011, Deusty, LLC
+// All rights reserved.
+//
+// Redistribution and use of this software in source and binary forms,
+// with or without modification, are permitted provided that the following conditions are met:
+//
+// * Redistributions of source code must retain the above
+// copyright notice, this list of conditions and the
+// following disclaimer.
+//
+// * Neither the name of Deusty nor the names of its
+// contributors may be used to endorse or promote products
+// derived from this software without specific prior
+// written permission of Deusty, LLC.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+#import <Foundation/Foundation.h>
+
+#if GTM_IPHONE_SDK
+#import <CFNetwork/CFNetwork.h>
+#endif // GTM_IPHONE_SDK
+
+// Global contants needed for errors from start
+
+#undef _EXTERN
+#undef _INITIALIZE_AS
+#ifdef GTMHTTPSERVER_DEFINE_GLOBALS
+#define _EXTERN
+#define _INITIALIZE_AS(x) = x
+#else
+#define _EXTERN extern
+#define _INITIALIZE_AS(x)
+#endif
+
+_EXTERN NSString *const kGTMHTTPServerErrorDomain
+ _INITIALIZE_AS(@"com.google.mactoolbox.HTTPServerDomain");
+enum {
+ kGTMHTTPServerSocketCreateFailedError = -100,
+ kGTMHTTPServerBindFailedError = -101,
+ kGTMHTTPServerListenFailedError = -102,
+ kGTMHTTPServerHandleCreateFailedError = -103,
+};
+
+@class GTMHTTPRequestMessage, GTMHTTPResponseMessage;
+
+// ----------------------------------------------------------------------------
+
+// See comment at top of file for the intened use of this class.
+@interface GTMHTTPServer : NSObject {
+ @private
+ id delegate_; // WEAK
+ uint16_t port_;
+ BOOL reusePort_;
+ BOOL localhostOnly_;
+ NSFileHandle *listenHandle_;
+ NSMutableArray *connections_;
+}
+
+// The delegate must support the httpServer:handleRequest: method in
+// NSObject(GTMHTTPServerDelegateMethods) below.
+- (id)initWithDelegate:(id)delegate;
+
+- (id)delegate;
+
+// Passing port zero will let one get assigned.
+- (uint16_t)port;
+- (void)setPort:(uint16_t)port;
+
+// Controls listening socket behavior: SO_REUSEADDR vs SO_REUSEPORT.
+// The default is NO (SO_REUSEADDR)
+- (BOOL)reusePort;
+- (void)setReusePort:(BOOL)reusePort;
+
+// Receive connections on the localHost loopback address only or on all
+// interfaces for this machine. The default is to only listen on localhost.
+- (BOOL)localhostOnly;
+- (void)setLocalhostOnly:(BOOL)yesno;
+
+// Start/Stop the web server. If there is an error starting up the server, |NO|
+// is returned, and the specific startup failure can be returned in |error| (see
+// above for the error domain and error codes). If the server is started, |YES|
+// is returned and the server's delegate is called for any requests that come
+// in.
+- (BOOL)start:(NSError **)error;
+- (void)stop;
+
+// returns the number of requests currently active in the server (i.e.-being
+// read in, sent replies).
+- (NSUInteger)activeRequestCount;
+
+@end
+
+@interface NSObject (GTMHTTPServerDelegateMethods)
+- (GTMHTTPResponseMessage *)httpServer:(GTMHTTPServer *)server
+ handleRequest:(GTMHTTPRequestMessage *)request;
+@end
+
+// ----------------------------------------------------------------------------
+
+// Encapsulates an http request, one of these is sent to the server's delegate
+// for each request.
+@interface GTMHTTPRequestMessage : NSObject {
+ @private
+ CFHTTPMessageRef message_;
+}
+- (NSString *)version;
+- (NSURL *)URL;
+- (NSString *)method;
+- (NSData *)body;
+- (NSDictionary *)allHeaderFieldValues;
+@end
+
+// ----------------------------------------------------------------------------
+
+// Encapsulates an http response, the server's delegate should return one for
+// each request received.
+@interface GTMHTTPResponseMessage : NSObject {
+ @private
+ CFHTTPMessageRef message_;
+}
++ (instancetype)responseWithString:(NSString *)plainText;
++ (instancetype)responseWithHTMLString:(NSString *)htmlString;
++ (instancetype)responseWithBody:(NSData *)body
+ contentType:(NSString *)contentType
+ statusCode:(int)statusCode;
++ (instancetype)emptyResponseWithCode:(int)statusCode;
+// TODO: class method for redirections?
+// TODO: add helper for expire/no-cache
+- (void)setValue:(NSString *)value forHeaderField:(NSString *)headerField;
+- (void)setHeaderValuesFromDictionary:(NSDictionary *)dict;
+@end
diff --git a/GoogleUtilities/Example/Tests/Network/third_party/GTMHTTPServer.m b/GoogleUtilities/Example/Tests/Network/third_party/GTMHTTPServer.m
new file mode 100644
index 0000000..526d8c5
--- /dev/null
+++ b/GoogleUtilities/Example/Tests/Network/third_party/GTMHTTPServer.m
@@ -0,0 +1,630 @@
+/* Copyright 2010 Google Inc.
+ *
+ * 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.
+ */
+
+//
+// Based a little on HTTPServer, part of the CocoaHTTPServer sample code found at
+// https://opensource.apple.com/source/HTTPServer/HTTPServer-11/CocoaHTTPServer/
+// License for the CocoaHTTPServer sample code:
+//
+// Software License Agreement (BSD License)
+//
+// Copyright (c) 2011, Deusty, LLC
+// All rights reserved.
+//
+// Redistribution and use of this software in source and binary forms,
+// with or without modification, are permitted provided that the following conditions are met:
+//
+// * Redistributions of source code must retain the above
+// copyright notice, this list of conditions and the
+// following disclaimer.
+//
+// * Neither the name of Deusty nor the names of its
+// contributors may be used to endorse or promote products
+// derived from this software without specific prior
+// written permission of Deusty, LLC.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+//
+
+#import <netinet/in.h>
+#import <sys/socket.h>
+#import <unistd.h>
+
+#define GTMHTTPSERVER_DEFINE_GLOBALS
+#import "GTMHTTPServer.h"
+
+// avoid some of GTM's promiscuous dependencies
+#ifndef _GTMDevLog
+#define _GTMDevLog NSLog
+#endif
+
+#ifndef GTM_STATIC_CAST
+#define GTM_STATIC_CAST(type, object) ((type *)(object))
+#endif
+
+#ifndef GTMCFAutorelease
+#define GTMCFAutorelease(x) ([(id)x autorelease])
+#endif
+
+@interface GTMHTTPServer (PrivateMethods)
+- (void)acceptedConnectionNotification:(NSNotification *)notification;
+- (NSMutableDictionary *)connectionWithFileHandle:(NSFileHandle *)fileHandle;
+- (void)dataAvailableNotification:(NSNotification *)notification;
+- (NSMutableDictionary *)lookupConnection:(NSFileHandle *)fileHandle;
+- (void)closeConnection:(NSMutableDictionary *)connDict;
+- (void)sendResponseOnNewThread:(NSMutableDictionary *)connDict;
+- (void)sentResponse:(NSMutableDictionary *)connDict;
+@end
+
+// keys for our connection dictionaries
+static NSString *kFileHandle = @"FileHandle";
+static NSString *kRequest = @"Request";
+static NSString *kResponse = @"Response";
+
+@interface GTMHTTPRequestMessage (PrivateHelpers)
+- (BOOL)isHeaderComplete;
+- (BOOL)appendData:(NSData *)data;
+- (NSString *)headerFieldValueForKey:(NSString *)key;
+- (UInt32)contentLength;
+- (void)setBody:(NSData *)body;
+@end
+
+@interface GTMHTTPResponseMessage ()
+- (id)initWithBody:(NSData *)body contentType:(NSString *)contentType statusCode:(int)statusCode;
+- (NSData *)serializedData;
+@end
+
+@implementation GTMHTTPServer
+
+- (id)init {
+ return [self initWithDelegate:nil];
+}
+
+- (id)initWithDelegate:(id)delegate {
+ self = [super init];
+ if (self) {
+ if (!delegate) {
+ _GTMDevLog(@"missing delegate");
+ [self release];
+ return nil;
+ }
+ delegate_ = delegate;
+
+#ifndef NS_BLOCK_ASSERTIONS
+ BOOL isDelegateOK = [delegate_ respondsToSelector:@selector(httpServer:handleRequest:)];
+ NSAssert(isDelegateOK, @"GTMHTTPServer delegate lacks handleRequest sel");
+#endif
+
+ localhostOnly_ = YES;
+ connections_ = [[NSMutableArray alloc] init];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [self stop];
+ [connections_ release];
+ [super dealloc];
+}
+
+#if !TARGET_OS_IPHONE
+- (void)finalize {
+ [self stop];
+ [super finalize];
+}
+#endif
+
+- (id)delegate {
+ return delegate_;
+}
+
+- (uint16_t)port {
+ return port_;
+}
+
+- (void)setPort:(uint16_t)port {
+ port_ = port;
+}
+
+- (BOOL)reusePort {
+ return reusePort_;
+}
+
+- (void)setReusePort:(BOOL)yesno {
+ reusePort_ = yesno;
+}
+
+- (BOOL)localhostOnly {
+ return localhostOnly_;
+}
+
+- (void)setLocalhostOnly:(BOOL)yesno {
+ localhostOnly_ = yesno;
+}
+
+- (BOOL)start:(NSError **)error {
+ NSAssert(listenHandle_ == nil, @"start called when we already have a listenHandle_");
+
+ if (error) *error = NULL;
+
+ NSInteger startFailureCode = 0;
+ int fd = socket(AF_INET, SOCK_STREAM, 0);
+ if (fd <= 0) {
+ // COV_NF_START - we'd need to use up *all* sockets to test this?
+ startFailureCode = kGTMHTTPServerSocketCreateFailedError;
+ goto startFailed;
+ // COV_NF_END
+ }
+
+ // enable address reuse quicker after we are done w/ our socket
+ int yes = 1;
+ int sock_opt = reusePort_ ? SO_REUSEPORT : SO_REUSEADDR;
+ if (setsockopt(fd, SOL_SOCKET, sock_opt, (void *)&yes, (socklen_t)sizeof(yes)) != 0) {
+ _GTMDevLog(@"failed to mark the socket as reusable"); // COV_NF_LINE
+ }
+
+ // bind
+ struct sockaddr_in addr;
+ bzero(&addr, sizeof(addr));
+ addr.sin_len = sizeof(addr);
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons(port_);
+ if (localhostOnly_) {
+ addr.sin_addr.s_addr = htonl(0x7F000001);
+ } else {
+ // COV_NF_START - testing this could cause a leopard firewall prompt during tests.
+ addr.sin_addr.s_addr = htonl(INADDR_ANY);
+ // COV_NF_END
+ }
+ if (bind(fd, (struct sockaddr *)(&addr), (socklen_t)sizeof(addr)) != 0) {
+ startFailureCode = kGTMHTTPServerBindFailedError;
+ goto startFailed;
+ }
+
+ // collect the port back out
+ if (port_ == 0) {
+ socklen_t len = (socklen_t)sizeof(addr);
+ if (getsockname(fd, (struct sockaddr *)(&addr), &len) == 0) {
+ port_ = ntohs(addr.sin_port);
+ }
+ }
+
+ // tell it to listen for connections
+ if (listen(fd, 5) != 0) {
+ // COV_NF_START
+ startFailureCode = kGTMHTTPServerListenFailedError;
+ goto startFailed;
+ // COV_NF_END
+ }
+
+ // now use a filehandle to accept connections
+ listenHandle_ = [[NSFileHandle alloc] initWithFileDescriptor:fd closeOnDealloc:YES];
+ if (listenHandle_ == nil) {
+ // COV_NF_START - we'd need to run out of memory to test this?
+ startFailureCode = kGTMHTTPServerHandleCreateFailedError;
+ goto startFailed;
+ // COV_NF_END
+ }
+
+ // setup notifications for connects
+ NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+ [center addObserver:self
+ selector:@selector(acceptedConnectionNotification:)
+ name:NSFileHandleConnectionAcceptedNotification
+ object:listenHandle_];
+ [listenHandle_ acceptConnectionInBackgroundAndNotify];
+
+ // TODO: maybe hit the delegate incase it wants to register w/ NSNetService,
+ // or just know we're up and running?
+
+ return YES;
+
+startFailed:
+ if (error) {
+ *error = [[[NSError alloc] initWithDomain:kGTMHTTPServerErrorDomain
+ code:startFailureCode
+ userInfo:nil] autorelease];
+ }
+ if (fd > 0) {
+ close(fd);
+ }
+ return NO;
+}
+
+- (void)stop {
+ if (listenHandle_) {
+ NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+ [center removeObserver:self
+ name:NSFileHandleConnectionAcceptedNotification
+ object:listenHandle_];
+ [listenHandle_ release];
+ listenHandle_ = nil;
+ // TODO: maybe hit the delegate in case it wants to unregister w/
+ // NSNetService, or just know we've stopped running?
+ }
+ [connections_ removeAllObjects];
+}
+
+- (NSUInteger)activeRequestCount {
+ return [connections_ count];
+}
+
+- (NSString *)description {
+ NSString *result =
+ [NSString stringWithFormat:@"%@<%p>{ port=%d localHostOnly=%@ status=%@ }", [self class],
+ self, port_, (localhostOnly_ ? @"YES" : @"NO"),
+ (listenHandle_ != nil ? @"Started" : @"Stopped")];
+ return result;
+}
+
+@end
+
+@implementation GTMHTTPServer (PrivateMethods)
+
+- (void)acceptedConnectionNotification:(NSNotification *)notification {
+ NSDictionary *userInfo = [notification userInfo];
+ NSFileHandle *newConnection = [userInfo objectForKey:NSFileHandleNotificationFileHandleItem];
+ NSAssert1(newConnection != nil, @"failed to get the connection in the notification: %@",
+ notification);
+
+ // make sure we accept more...
+ [listenHandle_ acceptConnectionInBackgroundAndNotify];
+
+ // TODO: could let the delegate look at the address, before we start working
+ // on it.
+
+ NSMutableDictionary *connDict = [self connectionWithFileHandle:newConnection];
+ [connections_ addObject:connDict];
+}
+
+- (NSMutableDictionary *)connectionWithFileHandle:(NSFileHandle *)fileHandle {
+ NSMutableDictionary *result = [NSMutableDictionary dictionary];
+
+ [result setObject:fileHandle forKey:kFileHandle];
+
+ GTMHTTPRequestMessage *request = [[[GTMHTTPRequestMessage alloc] init] autorelease];
+ [result setObject:request forKey:kRequest];
+
+ // setup for data notifications
+ NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+ [center addObserver:self
+ selector:@selector(dataAvailableNotification:)
+ name:NSFileHandleReadCompletionNotification
+ object:fileHandle];
+ [fileHandle readInBackgroundAndNotify];
+
+ return result;
+}
+
+- (void)dataAvailableNotification:(NSNotification *)notification {
+ NSFileHandle *connectionHandle = GTM_STATIC_CAST(NSFileHandle, [notification object]);
+ NSMutableDictionary *connDict = [self lookupConnection:connectionHandle];
+ if (connDict == nil) return; // we are no longer tracking this one
+
+ NSDictionary *userInfo = [notification userInfo];
+ NSData *readData = [userInfo objectForKey:NSFileHandleNotificationDataItem];
+ if ([readData length] == 0) {
+ // remote side closed
+ [self closeConnection:connDict];
+ return;
+ }
+
+ // Use a local pool to keep memory down incase the runloop we're in doesn't
+ // drain until it gets a UI event.
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ @try {
+ // Like Apple's sample, we just keep adding data until we get a full header
+ // and any referenced body.
+
+ GTMHTTPRequestMessage *request = [connDict objectForKey:kRequest];
+ [request appendData:readData];
+
+ // Is the header complete yet?
+ if (![request isHeaderComplete]) {
+ // more data...
+ [connectionHandle readInBackgroundAndNotify];
+ } else {
+ // Do we have all the body?
+ UInt32 contentLength = [request contentLength];
+ NSData *body = [request body];
+ NSUInteger bodyLength = [body length];
+ if (contentLength > bodyLength) {
+ // need more data...
+ [connectionHandle readInBackgroundAndNotify];
+ } else {
+ if (contentLength < bodyLength) {
+ // We got extra (probably someone trying to pipeline on us), trim
+ // and let the extra data go...
+ NSData *newBody = [NSData dataWithBytes:[body bytes] length:contentLength];
+ [request setBody:newBody];
+ _GTMDevLog(@"Got %lu extra bytes on http request, ignoring them",
+ (unsigned long)(bodyLength - contentLength));
+ }
+
+ GTMHTTPResponseMessage *response = nil;
+ @try {
+ // Off to the delegate
+ response = [delegate_ httpServer:self handleRequest:request];
+ } @catch (NSException *e) {
+ _GTMDevLog(@"Exception trying to handle http request: %@", e);
+ } // COV_NF_LINE - radar 5851992 only reachable w/ an uncaught exception which isn't
+ // testable
+
+ if (response) {
+ // We don't support connection reuse, so we add (force) the header to
+ // close every connection.
+ [response setValue:@"close" forHeaderField:@"Connection"];
+
+ // spawn thread to send reply (since we do a blocking send)
+ [connDict setObject:response forKey:kResponse];
+ [NSThread detachNewThreadSelector:@selector(sendResponseOnNewThread:)
+ toTarget:self
+ withObject:connDict];
+ } else {
+ // No response, shut it down
+ [self closeConnection:connDict];
+ }
+ }
+ }
+ } @catch (NSException *e) { // COV_NF_START
+ _GTMDevLog(@"exception while read data: %@", e);
+ // exception while dealing with the connection, close it
+ } // COV_NF_END
+ @finally {
+ [pool drain];
+ }
+}
+
+- (NSMutableDictionary *)lookupConnection:(NSFileHandle *)fileHandle {
+ NSMutableDictionary *result = nil;
+ for (NSMutableDictionary *connDict in connections_) {
+ if (fileHandle == [connDict objectForKey:kFileHandle]) {
+ result = connDict;
+ break;
+ }
+ }
+ return result;
+}
+
+- (void)closeConnection:(NSMutableDictionary *)connDict {
+ // remove the notification
+ NSFileHandle *connectionHandle = [connDict objectForKey:kFileHandle];
+ NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+ [center removeObserver:self name:NSFileHandleReadCompletionNotification object:connectionHandle];
+ // in a non GC world, we're fine just letting the connect get closed when
+ // the object is release when it comes out of connections_, but in a GC world
+ // it won't get cleaned up
+ [connectionHandle closeFile];
+
+ // remove it from the list
+ [connections_ removeObject:connDict];
+}
+
+- (void)sendResponseOnNewThread:(NSMutableDictionary *)connDict {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ @try {
+ GTMHTTPResponseMessage *response = [connDict objectForKey:kResponse];
+ NSFileHandle *connectionHandle = [connDict objectForKey:kFileHandle];
+ NSData *serialized = [response serializedData];
+ [connectionHandle writeData:serialized];
+ } @catch (NSException *e) { // COV_NF_START - causing an exception here is to hard in a test
+ // TODO: let the delegate know about the exception (but do it on the main
+ // thread)
+ _GTMDevLog(@"exception while sending reply: %@", e);
+ } // COV_NF_END
+
+ // back to the main thread to close things down
+ [self performSelectorOnMainThread:@selector(sentResponse:) withObject:connDict waitUntilDone:NO];
+
+ [pool release];
+}
+
+- (void)sentResponse:(NSMutableDictionary *)connDict {
+ // make sure we're still tracking this connection (in case server was stopped)
+ NSFileHandle *connection = [connDict objectForKey:kFileHandle];
+ NSMutableDictionary *connDict2 = [self lookupConnection:connection];
+ if (connDict != connDict2) return;
+
+ // TODO: message the delegate that it was sent
+
+ // close it down
+ [self closeConnection:connDict];
+}
+
+@end
+
+#pragma mark -
+
+@implementation GTMHTTPRequestMessage
+
+- (id)init {
+ self = [super init];
+ if (self) {
+ message_ = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, YES);
+ }
+ return self;
+}
+
+- (void)dealloc {
+ if (message_) {
+ CFRelease(message_);
+ }
+ [super dealloc];
+}
+
+- (NSString *)version {
+ return GTMCFAutorelease(CFHTTPMessageCopyVersion(message_));
+}
+
+- (NSURL *)URL {
+ return GTMCFAutorelease(CFHTTPMessageCopyRequestURL(message_));
+}
+
+- (NSString *)method {
+ return GTMCFAutorelease(CFHTTPMessageCopyRequestMethod(message_));
+}
+
+- (NSData *)body {
+ return GTMCFAutorelease(CFHTTPMessageCopyBody(message_));
+}
+
+- (NSDictionary *)allHeaderFieldValues {
+ return GTMCFAutorelease(CFHTTPMessageCopyAllHeaderFields(message_));
+}
+
+- (NSString *)description {
+ CFStringRef desc = CFCopyDescription(message_);
+ NSString *result = [NSString stringWithFormat:@"%@<%p>{ message=%@ }", [self class], self, desc];
+ CFRelease(desc);
+ return result;
+}
+
+@end
+
+@implementation GTMHTTPRequestMessage (PrivateHelpers)
+
+- (BOOL)isHeaderComplete {
+ return CFHTTPMessageIsHeaderComplete(message_) ? YES : NO;
+}
+
+- (BOOL)appendData:(NSData *)data {
+ return CFHTTPMessageAppendBytes(message_, [data bytes], (CFIndex)[data length]) ? YES : NO;
+}
+
+- (NSString *)headerFieldValueForKey:(NSString *)key {
+ CFStringRef value = NULL;
+ if (key) {
+ value = CFHTTPMessageCopyHeaderFieldValue(message_, (CFStringRef)key);
+ }
+ return GTMCFAutorelease(value);
+}
+
+- (UInt32)contentLength {
+ return (UInt32)[[self headerFieldValueForKey:@"Content-Length"] intValue];
+}
+
+- (void)setBody:(NSData *)body {
+ if (!body) {
+ body = [NSData data]; // COV_NF_LINE - can only happen in we fail to make the new data object
+ }
+ CFHTTPMessageSetBody(message_, (CFDataRef)body);
+}
+
+@end
+
+#pragma mark -
+
+@implementation GTMHTTPResponseMessage
+
+- (id)init {
+ return [self initWithBody:nil contentType:nil statusCode:0];
+}
+
+- (id)initWithBody:(NSData *)body contentType:(NSString *)contentType statusCode:(int)statusCode {
+ self = [super init];
+ if (self) {
+ if ((statusCode < 100) || (statusCode > 599)) {
+ [self release];
+ return nil;
+ }
+ message_ =
+ CFHTTPMessageCreateResponse(kCFAllocatorDefault, statusCode, NULL, kCFHTTPVersion1_0);
+ if (!message_) {
+ // COV_NF_START
+ [self release];
+ return nil;
+ // COV_NF_END
+ }
+ NSUInteger bodyLength = 0;
+ if (body) {
+ bodyLength = [body length];
+ CFHTTPMessageSetBody(message_, (CFDataRef)body);
+ }
+ if ([contentType length] == 0) {
+ contentType = @"text/html";
+ }
+ NSString *bodyLenStr = [NSString stringWithFormat:@"%lu", (unsigned long)bodyLength];
+ [self setValue:bodyLenStr forHeaderField:@"Content-Length"];
+ [self setValue:contentType forHeaderField:@"Content-Type"];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ if (message_) {
+ CFRelease(message_);
+ }
+ [super dealloc];
+}
+
++ (instancetype)responseWithString:(NSString *)plainText {
+ NSData *body = [plainText dataUsingEncoding:NSUTF8StringEncoding];
+ return [self responseWithBody:body contentType:@"text/plain; charset=UTF-8" statusCode:200];
+}
+
++ (instancetype)responseWithHTMLString:(NSString *)htmlString {
+ return [self responseWithBody:[htmlString dataUsingEncoding:NSUTF8StringEncoding]
+ contentType:@"text/html; charset=UTF-8"
+ statusCode:200];
+}
+
++ (instancetype)responseWithBody:(NSData *)body
+ contentType:(NSString *)contentType
+ statusCode:(int)statusCode {
+ return [[[[self class] alloc] initWithBody:body contentType:contentType statusCode:statusCode]
+ autorelease];
+}
+
++ (instancetype)emptyResponseWithCode:(int)statusCode {
+ return
+ [[[[self class] alloc] initWithBody:nil contentType:nil statusCode:statusCode] autorelease];
+}
+
+- (void)setValue:(NSString *)value forHeaderField:(NSString *)headerField {
+ if ([headerField length] == 0) return;
+ if (value == nil) {
+ value = @"";
+ }
+ CFHTTPMessageSetHeaderFieldValue(message_, (CFStringRef)headerField, (CFStringRef)value);
+}
+
+- (void)setHeaderValuesFromDictionary:(NSDictionary *)dict {
+ for (id key in dict) {
+ id value = [dict valueForKey:key];
+ [self setValue:value forHeaderField:key];
+ }
+}
+
+- (NSString *)description {
+ CFStringRef desc = CFCopyDescription(message_);
+ NSString *result = [NSString stringWithFormat:@"%@<%p>{ message=%@ }", [self class], self, desc];
+ CFRelease(desc);
+ return result;
+}
+
+- (NSData *)serializedData {
+ return GTMCFAutorelease(CFHTTPMessageCopySerializedMessage(message_));
+}
+
+@end
diff --git a/GoogleUtilities/Example/Tests/Reachability/GULReachabilityCheckerTest.m b/GoogleUtilities/Example/Tests/Reachability/GULReachabilityCheckerTest.m
new file mode 100644
index 0000000..da9fbf1
--- /dev/null
+++ b/GoogleUtilities/Example/Tests/Reachability/GULReachabilityCheckerTest.m
@@ -0,0 +1,358 @@
+// Copyright 2018 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <GoogleUtilities/GULReachabilityChecker.h>
+
+#import <XCTest/XCTest.h>
+
+#import "GULReachabilityChecker+Internal.h"
+
+@interface GULReachabilityCheckerTest : XCTestCase <GULReachabilityDelegate> {
+ @private
+ GULReachabilityChecker *checker_;
+ NSMutableArray *statuses_;
+ BOOL createFail_;
+ BOOL setCallbackFail_;
+ BOOL scheduleUnscheduleFail_;
+}
+
+- (void *)createReachabilityWithAllocator:(CFAllocatorRef)allocator withName:(const char *)hostname;
+- (BOOL)reachability:(const void *)reachability
+ setCallback:(SCNetworkReachabilityCallBack)callback
+ withContext:(SCNetworkReachabilityContext *)context;
+- (BOOL)scheduleReachability:(const void *)reachability
+ runLoop:(CFRunLoopRef)runLoop
+ runLoopMode:(CFStringRef)runLoopMode;
+- (BOOL)unscheduleReachability:(const void *)reachability
+ runLoop:(CFRunLoopRef)runLoop
+ runLoopMode:(CFStringRef)runLoopMode;
+- (void)releaseReachability:(const void *)reachability;
+@end
+
+static NSString *const kHostname = @"www.google.com";
+static const void *kFakeReachabilityObject = (const void *)0x8badf00d;
+
+static GULReachabilityCheckerTest *FakeReachabilityTest = nil;
+
+static struct {
+ int callsMade;
+ int createCall;
+ int setCallbackCall;
+ int scheduleCall;
+ int unscheduleCall;
+ int releaseCall;
+ void (*callback)(SCNetworkReachabilityRef, SCNetworkReachabilityFlags, void *info);
+ void *callbackInfo;
+} FakeReachability;
+
+static SCNetworkReachabilityRef ReachabilityCreateWithName(CFAllocatorRef allocator,
+ const char *hostname) {
+ return (SCNetworkReachabilityRef)
+ [FakeReachabilityTest createReachabilityWithAllocator:allocator withName:hostname];
+}
+
+static Boolean ReachabilitySetCallback(SCNetworkReachabilityRef reachability,
+ SCNetworkReachabilityCallBack callback,
+ SCNetworkReachabilityContext *context) {
+ return [FakeReachabilityTest reachability:reachability setCallback:callback withContext:context];
+}
+
+static Boolean ReachabilityScheduleWithRunLoop(SCNetworkReachabilityRef reachability,
+ CFRunLoopRef runLoop,
+ CFStringRef runLoopMode) {
+ return [FakeReachabilityTest scheduleReachability:reachability
+ runLoop:runLoop
+ runLoopMode:runLoopMode];
+}
+
+static Boolean ReachabilityUnscheduleFromRunLoop(SCNetworkReachabilityRef reachability,
+ CFRunLoopRef runLoop,
+ CFStringRef runLoopMode) {
+ return [FakeReachabilityTest unscheduleReachability:reachability
+ runLoop:runLoop
+ runLoopMode:runLoopMode];
+}
+
+static void ReachabilityRelease(CFTypeRef reachability) {
+ [FakeReachabilityTest releaseReachability:reachability];
+}
+
+static const struct GULReachabilityApi kTestReachabilityApi = {
+ ReachabilityCreateWithName, ReachabilitySetCallback, ReachabilityScheduleWithRunLoop,
+ ReachabilityUnscheduleFromRunLoop, ReachabilityRelease,
+};
+
+@implementation GULReachabilityCheckerTest
+
+- (void)resetFakeReachability {
+ FakeReachabilityTest = self;
+ FakeReachability.callsMade = 0;
+ FakeReachability.createCall = -1;
+ FakeReachability.setCallbackCall = -1;
+ FakeReachability.scheduleCall = -1;
+ FakeReachability.unscheduleCall = -1;
+ FakeReachability.releaseCall = -1;
+ FakeReachability.callback = NULL;
+ FakeReachability.callbackInfo = NULL;
+}
+
+- (void)setUp {
+ [super setUp];
+
+ [self resetFakeReachability];
+ createFail_ = NO;
+ setCallbackFail_ = NO;
+ scheduleUnscheduleFail_ = NO;
+
+ checker_ = [[GULReachabilityChecker alloc] initWithReachabilityDelegate:self withHost:kHostname];
+ statuses_ = [[NSMutableArray alloc] init];
+}
+
+- (void *)createReachabilityWithAllocator:(CFAllocatorRef)allocator
+ withName:(const char *)hostname {
+ XCTAssertTrue(allocator == kCFAllocatorDefault, @"");
+ XCTAssertEqual(strcmp(hostname, [kHostname UTF8String]), 0, @"");
+ XCTAssertEqual(FakeReachability.callsMade, 0, @"create call must always come first.");
+ XCTAssertEqual(FakeReachability.createCall, -1, @"create call must only be called once.");
+ FakeReachability.createCall = ++FakeReachability.callsMade;
+ return createFail_ ? NULL : (void *)kFakeReachabilityObject;
+}
+
+- (BOOL)reachability:(const void *)reachability
+ setCallback:(SCNetworkReachabilityCallBack)callback
+ withContext:(SCNetworkReachabilityContext *)context {
+ XCTAssertEqual(reachability, kFakeReachabilityObject, @"got bad object");
+ XCTAssertEqual((int)context->version, 0, @"");
+ XCTAssertEqual(context->info, (__bridge void *)checker_, @"");
+ XCTAssertEqual((void *)context->retain, NULL, @"");
+ XCTAssertEqual((void *)context->release, NULL, @"");
+ XCTAssertEqual((void *)context->copyDescription, NULL, @"");
+ XCTAssertEqual(FakeReachability.setCallbackCall, -1, @"setCallback should only be called once.");
+ FakeReachability.setCallbackCall = ++FakeReachability.callsMade;
+ XCTAssertTrue(callback != NULL, @"");
+ FakeReachability.callback = callback;
+ XCTAssertTrue(context->info != NULL, @"");
+ FakeReachability.callbackInfo = context->info;
+ return setCallbackFail_ ? NO : YES;
+}
+
+- (BOOL)scheduleReachability:(const void *)reachability
+ runLoop:(CFRunLoopRef)runLoop
+ runLoopMode:(CFStringRef)runLoopMode {
+ XCTAssertEqual(reachability, kFakeReachabilityObject, @"got bad object");
+ XCTAssertEqual(runLoop, CFRunLoopGetMain(), @"bad run loop");
+ XCTAssertEqualObjects((__bridge NSString *)runLoopMode,
+ (__bridge NSString *)kCFRunLoopCommonModes, @"bad run loop mode");
+ XCTAssertEqual(FakeReachability.scheduleCall, -1,
+ @"scheduleWithRunLoop should only be called once.");
+ FakeReachability.scheduleCall = ++FakeReachability.callsMade;
+ return scheduleUnscheduleFail_ ? NO : YES;
+}
+
+- (BOOL)unscheduleReachability:(const void *)reachability
+ runLoop:(CFRunLoopRef)runLoop
+ runLoopMode:(CFStringRef)runLoopMode {
+ XCTAssertEqual(reachability, kFakeReachabilityObject, @"got bad object");
+ XCTAssertEqual(runLoop, CFRunLoopGetMain(), @"bad run loop");
+ XCTAssertEqualObjects((__bridge NSString *)runLoopMode,
+ (__bridge NSString *)kCFRunLoopCommonModes, @"bad run loop mode");
+ XCTAssertEqual(FakeReachability.unscheduleCall, -1,
+ @"unscheduleFromRunLoop should only be called once.");
+ FakeReachability.unscheduleCall = ++FakeReachability.callsMade;
+ return scheduleUnscheduleFail_ ? NO : YES;
+}
+
+- (void)releaseReachability:(const void *)reachability {
+ XCTAssertEqual(reachability, kFakeReachabilityObject, @"got bad object");
+ XCTAssertEqual(FakeReachability.releaseCall, -1, @"release should only be called once.");
+ FakeReachability.releaseCall = ++FakeReachability.callsMade;
+}
+
+- (void)reachability:(GULReachabilityChecker *)reachability
+ statusChanged:(GULReachabilityStatus)status {
+ [statuses_ addObject:[NSNumber numberWithInt:(int)status]];
+}
+
+#pragma mark - Test
+
+- (void)testApiHappyPath {
+ [checker_ setReachabilityApi:&kTestReachabilityApi];
+ XCTAssertEqual([checker_ reachabilityApi], &kTestReachabilityApi, @"");
+
+ XCTAssertFalse(checker_.isActive, @"");
+ XCTAssertEqual(checker_.reachabilityStatus, kGULReachabilityUnknown, @"");
+
+ XCTAssertTrue([checker_ start], @"");
+
+ XCTAssertTrue(checker_.isActive, @"");
+ XCTAssertEqual([statuses_ count], (NSUInteger)0, @"");
+ XCTAssertEqual(checker_.reachabilityStatus, kGULReachabilityUnknown, @"");
+
+ FakeReachability.callback(kFakeReachabilityObject, 0, FakeReachability.callbackInfo);
+
+ XCTAssertEqual([statuses_ count], (NSUInteger)1, @"");
+ XCTAssertEqual([(NSNumber *)[statuses_ objectAtIndex:0] intValue],
+ (int)kGULReachabilityNotReachable, @"");
+ XCTAssertEqual(checker_.reachabilityStatus, kGULReachabilityNotReachable, @"");
+
+ FakeReachability.callback(kFakeReachabilityObject, kSCNetworkReachabilityFlagsReachable,
+ FakeReachability.callbackInfo);
+
+ XCTAssertEqual([statuses_ count], (NSUInteger)2, @"");
+ XCTAssertEqual([(NSNumber *)[statuses_ objectAtIndex:1] intValue], (int)kGULReachabilityViaWifi,
+ @"");
+ XCTAssertEqual(checker_.reachabilityStatus, kGULReachabilityViaWifi, @"");
+
+ FakeReachability.callback(
+ kFakeReachabilityObject,
+ kSCNetworkReachabilityFlagsReachable | kSCNetworkReachabilityFlagsConnectionRequired,
+ FakeReachability.callbackInfo);
+
+ XCTAssertEqual([statuses_ count], (NSUInteger)3, @"");
+ XCTAssertEqual([(NSNumber *)[statuses_ objectAtIndex:2] intValue],
+ (int)kGULReachabilityNotReachable, @"");
+ XCTAssertEqual(checker_.reachabilityStatus, kGULReachabilityNotReachable, @"");
+
+#if TARGET_OS_IOS || TARGET_OS_TV
+ FakeReachability.callback(
+ kFakeReachabilityObject,
+ kSCNetworkReachabilityFlagsReachable | kSCNetworkReachabilityFlagsIsWWAN,
+ FakeReachability.callbackInfo);
+
+ XCTAssertEqual([statuses_ count], (NSUInteger)4, @"");
+ XCTAssertEqual([(NSNumber *)[statuses_ objectAtIndex:3] intValue],
+ (int)kGULReachabilityViaCellular, @"");
+ XCTAssertEqual(checker_.reachabilityStatus, kGULReachabilityViaCellular, @"");
+
+ FakeReachability.callback(kFakeReachabilityObject, kSCNetworkReachabilityFlagsIsWWAN,
+ FakeReachability.callbackInfo);
+
+ XCTAssertEqual([statuses_ count], (NSUInteger)5, @"");
+ XCTAssertEqual([(NSNumber *)[statuses_ objectAtIndex:4] intValue],
+ (int)kGULReachabilityNotReachable, @"");
+ XCTAssertEqual(checker_.reachabilityStatus, kGULReachabilityNotReachable, @"");
+#endif
+
+ [checker_ stop];
+
+ XCTAssertFalse(checker_.isActive, @"");
+ XCTAssertEqual(checker_.reachabilityStatus, kGULReachabilityUnknown, @"");
+
+ XCTAssertEqual(FakeReachability.callsMade, 5, @"");
+
+ XCTAssertEqual(FakeReachability.createCall, 1, @"");
+ XCTAssertEqual(FakeReachability.setCallbackCall, 2, @"");
+ XCTAssertEqual(FakeReachability.scheduleCall, 3, @"");
+ XCTAssertEqual(FakeReachability.unscheduleCall, 4, @"");
+ XCTAssertEqual(FakeReachability.releaseCall, 5, @"");
+}
+
+- (void)testApiCreateFail {
+ [checker_ setReachabilityApi:&kTestReachabilityApi];
+ XCTAssertEqual([checker_ reachabilityApi], &kTestReachabilityApi, @"");
+
+ createFail_ = YES;
+
+ XCTAssertFalse(checker_.isActive, @"");
+ XCTAssertEqual(checker_.reachabilityStatus, kGULReachabilityUnknown, @"");
+
+ XCTAssertFalse([checker_ start], @"");
+
+ XCTAssertFalse(checker_.isActive, @"");
+ XCTAssertEqual(checker_.reachabilityStatus, kGULReachabilityUnknown, @"");
+
+ [checker_ stop];
+
+ XCTAssertFalse(checker_.isActive, @"");
+ XCTAssertEqual(checker_.reachabilityStatus, kGULReachabilityUnknown, @"");
+
+ XCTAssertEqual(FakeReachability.callsMade, 1, @"");
+
+ XCTAssertEqual(FakeReachability.createCall, 1, @"");
+ XCTAssertEqual(FakeReachability.setCallbackCall, -1, @"");
+ XCTAssertEqual(FakeReachability.scheduleCall, -1, @"");
+ XCTAssertEqual(FakeReachability.unscheduleCall, -1, @"");
+ XCTAssertEqual(FakeReachability.releaseCall, -1, @"");
+
+ XCTAssertEqual([statuses_ count], (NSUInteger)0, @"");
+}
+
+- (void)testApiCallbackFail {
+ [checker_ setReachabilityApi:&kTestReachabilityApi];
+ XCTAssertEqual([checker_ reachabilityApi], &kTestReachabilityApi, @"");
+
+ setCallbackFail_ = YES;
+
+ XCTAssertFalse(checker_.isActive, @"");
+ XCTAssertEqual(checker_.reachabilityStatus, kGULReachabilityUnknown, @"");
+
+ XCTAssertFalse([checker_ start], @"");
+
+ XCTAssertFalse(checker_.isActive, @"");
+ XCTAssertEqual(checker_.reachabilityStatus, kGULReachabilityUnknown, @"");
+
+ [checker_ stop];
+
+ XCTAssertFalse(checker_.isActive, @"");
+ XCTAssertEqual(checker_.reachabilityStatus, kGULReachabilityUnknown, @"");
+
+ XCTAssertEqual(FakeReachability.callsMade, 3, @"");
+
+ XCTAssertEqual(FakeReachability.createCall, 1, @"");
+ XCTAssertEqual(FakeReachability.setCallbackCall, 2, @"");
+ XCTAssertEqual(FakeReachability.scheduleCall, -1, @"");
+ XCTAssertEqual(FakeReachability.unscheduleCall, -1, @"");
+ XCTAssertEqual(FakeReachability.releaseCall, 3, @"");
+
+ XCTAssertEqual([statuses_ count], (NSUInteger)0, @"");
+}
+
+- (void)testApiScheduleFail {
+ [checker_ setReachabilityApi:&kTestReachabilityApi];
+ XCTAssertEqual([checker_ reachabilityApi], &kTestReachabilityApi, @"");
+
+ scheduleUnscheduleFail_ = YES;
+
+ XCTAssertFalse(checker_.isActive, @"");
+ XCTAssertEqual(checker_.reachabilityStatus, kGULReachabilityUnknown, @"");
+
+ XCTAssertFalse([checker_ start], @"");
+
+ XCTAssertFalse(checker_.isActive, @"");
+ XCTAssertEqual(checker_.reachabilityStatus, kGULReachabilityUnknown, @"");
+
+ [checker_ stop];
+
+ XCTAssertFalse(checker_.isActive, @"");
+ XCTAssertEqual(checker_.reachabilityStatus, kGULReachabilityUnknown, @"");
+
+ XCTAssertEqual(FakeReachability.callsMade, 4, @"");
+
+ XCTAssertEqual(FakeReachability.createCall, 1, @"");
+ XCTAssertEqual(FakeReachability.setCallbackCall, 2, @"");
+ XCTAssertEqual(FakeReachability.scheduleCall, 3, @"");
+ XCTAssertEqual(FakeReachability.unscheduleCall, -1, @"");
+ XCTAssertEqual(FakeReachability.releaseCall, 4, @"");
+
+ XCTAssertEqual([statuses_ count], (NSUInteger)0, @"");
+}
+
+- (void)testBadHost {
+ XCTAssertNil([[GULReachabilityChecker alloc] initWithReachabilityDelegate:self withHost:nil],
+ @"Creating a checker with nil hostname must fail.");
+ XCTAssertNil([[GULReachabilityChecker alloc] initWithReachabilityDelegate:self withHost:@""],
+ @"Creating a checker with empty hostname must fail.");
+}
+
+@end
diff --git a/GoogleUtilities/Example/Tests/Tests-Info.plist b/GoogleUtilities/Example/Tests/Tests-Info.plist
new file mode 100644
index 0000000..169b6f7
--- /dev/null
+++ b/GoogleUtilities/Example/Tests/Tests-Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundlePackageType</key>
+ <string>BNDL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>