aboutsummaryrefslogtreecommitdiffhomepage
path: root/GoogleUtilities/Example/Tests/Network
diff options
context:
space:
mode:
Diffstat (limited to 'GoogleUtilities/Example/Tests/Network')
-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
4 files changed, 1888 insertions, 0 deletions
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