diff options
Diffstat (limited to 'Foundation/GTMHTTPServerTest.m')
-rw-r--r-- | Foundation/GTMHTTPServerTest.m | 573 |
1 files changed, 573 insertions, 0 deletions
diff --git a/Foundation/GTMHTTPServerTest.m b/Foundation/GTMHTTPServerTest.m new file mode 100644 index 0000000..d96d54e --- /dev/null +++ b/Foundation/GTMHTTPServerTest.m @@ -0,0 +1,573 @@ +// +// GTMHTTPServerTest.m +// +// Copyright 2008 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. +// + +#import <netinet/in.h> +#import <sys/socket.h> +#import <unistd.h> +#import "GTMSenTestCase.h" +#import "GTMUnitTestDevLog.h" +#import "GTMHTTPServer.h" +#import "GTMRegex.h" + +@interface GTMHTTPServerTest : GTMTestCase { + NSData *fetchedData_; +} +@end + +@interface GTMHTTPServerTest (PrivateMethods) +- (NSData *)fetchFromPort:(unsigned short)port + payload:(NSString *)payload + chunkSize:(NSUInteger)chunkSize; +- (NSFileHandle *)fileHandleSendingToPort:(unsigned short)port + payload:(NSString *)payload + chunkSize:(NSUInteger)chunkSize; +- (void)readFinished:(NSNotification *)notification; +@end + +// helper class +@interface TestServerDelegate : NSObject { + NSMutableArray *requests_; + NSMutableArray *responses_; +} ++ (id)testServerDelegate; +- (NSUInteger)requestCount; +- (GTMHTTPRequestMessage *)popRequest; +- (void)pushResponse:(GTMHTTPResponseMessage *)message; +@end + +// helper that throws while handling its request +@interface TestThrowingServerDelegate : TestServerDelegate +// since this method ALWAYS throws, we can mark it as noreturn +- (GTMHTTPResponseMessage *)httpServer:(GTMHTTPServer *)server + handleRequest:(GTMHTTPRequestMessage *)request __attribute__ ((noreturn)); +@end + +// The timings used for waiting for replies +const NSTimeInterval kGiveUpInterval = 5.0; +const NSTimeInterval kRunLoopInterval = 0.01; + +// the size we break writes up into to test the reading code and how long to +// wait between writes. +const NSUInteger kSendChunkSize = 12; +const NSTimeInterval kSendChunkInterval = 0.05; + +// ---------------------------------------------------------------------------- + +@implementation GTMHTTPServerTest + +- (void)testInit { + // bad delegates + [GTMUnitTestDevLog expectString:@"missing delegate"]; + STAssertNil([[GTMHTTPServer alloc] init], nil); + [GTMUnitTestDevLog expectString:@"missing delegate"]; + STAssertNil([[GTMHTTPServer alloc] initWithDelegate:nil], nil); + + TestServerDelegate *delegate = [TestServerDelegate testServerDelegate]; + STAssertNotNil(delegate, nil); + GTMHTTPServer *server = + [[[GTMHTTPServer alloc] initWithDelegate:delegate] autorelease]; + STAssertNotNil(server, nil); + + // some attributes + + STAssertTrue([server delegate] == delegate, nil); + + [server setLocalhostOnly:NO]; + STAssertFalse([server localhostOnly], nil); + [server setLocalhostOnly:YES]; + STAssertTrue([server localhostOnly], nil); + + STAssertEquals([server port], (uint16_t)0, nil); + [server setPort:8080]; + STAssertEquals([server port], (uint16_t)8080, nil); + [server setPort:80]; + STAssertEquals([server port], (uint16_t)80, nil); + + // description (atleast 10 chars) + STAssertGreaterThan([[server description] length], (NSUInteger)10, nil); +} + +- (void)testStartStop { + TestServerDelegate *delegate1 = [TestServerDelegate testServerDelegate]; + STAssertNotNil(delegate1, nil); + GTMHTTPServer *server1 = + [[[GTMHTTPServer alloc] initWithDelegate:delegate1] autorelease]; + STAssertNotNil(server1, nil); + NSError *error = nil; + STAssertTrue([server1 start:&error], @"failed to start (error=%@)", error); + STAssertNil(error, @"error: %@", error); + STAssertGreaterThanOrEqual([server1 port], (uint16_t)1024, + @"how'd we get a reserved port?"); + + TestServerDelegate *delegate2 = [TestServerDelegate testServerDelegate]; + STAssertNotNil(delegate2, nil); + GTMHTTPServer *server2 = + [[[GTMHTTPServer alloc] initWithDelegate:delegate2] autorelease]; + STAssertNotNil(server2, nil); + + // try the reserved port + [server2 setPort:666]; + error = nil; + STAssertFalse([server2 start:&error], nil); + STAssertNotNil(error, nil); + STAssertEqualObjects([error domain], kGTMHTTPServerErrorDomain, nil); + STAssertEquals([error code], (NSInteger)kGTMHTTPServerBindFailedError, + @"port should have been reserved"); + + // try the same port + [server2 setPort:[server1 port]]; + error = nil; + STAssertFalse([server2 start:&error], nil); + STAssertNotNil(error, nil); + STAssertEqualObjects([error domain], kGTMHTTPServerErrorDomain, nil); + STAssertEquals([error code], (NSInteger)kGTMHTTPServerBindFailedError, + @"port should have been in use"); + + // try a random port again so we really start (prove two can run at once) + [server2 setPort:0]; + error = nil; + STAssertTrue([server2 start:&error], @"failed to start (error=%@)", error); + STAssertNil(error, @"error: %@", error); + + // shut them down + [server1 stop]; + [server2 stop]; +} + +- (void)testRequests { + TestServerDelegate *delegate = [TestServerDelegate testServerDelegate]; + STAssertNotNil(delegate, nil); + GTMHTTPServer *server = + [[[GTMHTTPServer alloc] initWithDelegate:delegate] autorelease]; + STAssertNotNil(server, nil); + NSError *error = nil; + STAssertTrue([server start:&error], @"failed to start (error=%@)", error); + STAssertNil(error, @"error: %@", error); + + // a request to test all the fields of a request object + + NSString *payload = + @"PUT /some/server/path HTTP/1.0\r\n" + @"Content-Length: 16\r\n" + @"Custom-Header: Custom_Value\r\n" + @"\r\n" + @"this is the body"; + NSData *reply = + [self fetchFromPort:[server port] payload:payload chunkSize:kSendChunkSize]; + STAssertNotNil(reply, nil); + + GTMHTTPRequestMessage *request = [delegate popRequest]; + STAssertEqualObjects([request version], @"HTTP/1.0", nil); + STAssertEqualObjects([[request URL] absoluteString], @"/some/server/path", nil); + STAssertEqualObjects([request method], @"PUT", nil); + STAssertEqualObjects([request body], + [@"this is the body" dataUsingEncoding:NSUTF8StringEncoding], + nil); + NSDictionary *allHeaders = [request allHeaderFieldValues]; + STAssertNotNil(allHeaders, nil); + STAssertEquals([allHeaders count], (NSUInteger)2, nil); + STAssertEqualObjects([allHeaders objectForKey:@"Content-Length"], + @"16", nil); + STAssertEqualObjects([allHeaders objectForKey:@"Custom-Header"], + @"Custom_Value", nil); + STAssertGreaterThan([[request description] length], (NSUInteger)10, nil); + + // test different request types (in simple form) + + typedef struct { + NSString *method; + NSString *url; + } TestData; + + TestData data[] = { + { @"GET", @"/foo/bar" }, + { @"HEAD", @"/foo/baz" }, + { @"POST", @"/foo" }, + { @"PUT", @"/foo/spam" }, + { @"DELETE", @"/fooby/doo" }, + { @"TRACE", @"/something.html" }, + { @"CONNECT", @"/spam" }, + { @"OPTIONS", @"/wee/doggies" }, + }; + + for (size_t i = 0; i < sizeof(data) / sizeof(TestData); i++) { + payload = [NSString stringWithFormat:@"%@ %@ HTTP/1.0\r\n\r\n", + data[i].method, data[i].url]; + STAssertNotNil(payload, nil); + reply = [self fetchFromPort:[server port] + payload:payload + chunkSize:kSendChunkSize]; + STAssertNotNil(reply, // just want a reply in this test + @"failed of method %@", data[i].method); + request = [delegate popRequest]; + STAssertEqualObjects([[request URL] absoluteString], data[i].url, + @"urls didn't match for index %d", i); + STAssertEqualObjects([request method], data[i].method, + @"methods didn't match for index %d", i); + } + + [server stop]; +} + +- (void)testResponses { + + // some quick init tests for invalid things + STAssertNil([[GTMHTTPResponseMessage alloc] init], nil); + STAssertNil([GTMHTTPResponseMessage responseWithBody:nil + contentType:nil + statusCode:99], + nil); + STAssertNil([GTMHTTPResponseMessage responseWithBody:nil + contentType:nil + statusCode:602], + nil); + + TestServerDelegate *delegate = [TestServerDelegate testServerDelegate]; + STAssertNotNil(delegate, nil); + GTMHTTPServer *server = + [[[GTMHTTPServer alloc] initWithDelegate:delegate] autorelease]; + STAssertNotNil(server, nil); + NSError *error = nil; + STAssertTrue([server start:&error], @"failed to start (error=%@)", error); + STAssertNil(error, @"error: %@", error); + + // test the html helper + + GTMHTTPResponseMessage *expectedResponse = + [GTMHTTPResponseMessage responseWithHTMLString:@"Success!"]; + STAssertNotNil(expectedResponse, nil); + STAssertGreaterThan([[expectedResponse description] length], + (NSUInteger)0, nil); + [delegate pushResponse:expectedResponse]; + NSData *responseData = [self fetchFromPort:[server port] + payload:@"GET /foo HTTP/1.0\r\n\r\n" + chunkSize:kSendChunkSize]; + STAssertNotNil(responseData, nil); + NSString *responseString = + [[[NSString alloc] initWithData:responseData + encoding:NSUTF8StringEncoding] autorelease]; + STAssertNotNil(responseString, nil); + STAssertTrue([responseString hasPrefix:@"HTTP/1.0 200 OK"], nil); + STAssertTrue([responseString hasSuffix:@"Success!"], @"should end w/ our data"); + STAssertNotEquals([responseString rangeOfString:@"Content-Length: 8"].location, + (NSUInteger)NSNotFound, nil); + STAssertNotEquals([responseString rangeOfString:@"Content-Type: text/html; charset=UTF-8"].location, + (NSUInteger)NSNotFound, nil); + + // test the plain code response + + expectedResponse = [GTMHTTPResponseMessage emptyResponseWithCode:299]; + STAssertNotNil(expectedResponse, nil); + STAssertGreaterThan([[expectedResponse description] length], + (NSUInteger)0, nil); + [delegate pushResponse:expectedResponse]; + responseData = [self fetchFromPort:[server port] + payload:@"GET /foo HTTP/1.0\r\n\r\n" + chunkSize:kSendChunkSize]; + STAssertNotNil(responseData, nil); + responseString = + [[[NSString alloc] initWithData:responseData + encoding:NSUTF8StringEncoding] autorelease]; + STAssertNotNil(responseString, nil); + STAssertTrue([responseString hasPrefix:@"HTTP/1.0 299 "], nil); + STAssertNotEquals([responseString rangeOfString:@"Content-Length: 0"].location, + (NSUInteger)NSNotFound, nil); + STAssertNotEquals([responseString rangeOfString:@"Content-Type: text/html"].location, + (NSUInteger)NSNotFound, nil); + + // test the general api w/ extra header add + + expectedResponse = + [GTMHTTPResponseMessage responseWithBody:[@"FOO" dataUsingEncoding:NSUTF8StringEncoding] + contentType:@"some/type" + statusCode:298]; + STAssertNotNil(expectedResponse, nil); + STAssertGreaterThan([[expectedResponse description] length], + (NSUInteger)0, nil); + [expectedResponse setValue:@"Custom_Value" + forHeaderField:@"Custom-Header"]; + [expectedResponse setValue:nil + forHeaderField:@"Custom-Header2"]; + [delegate pushResponse:expectedResponse]; + responseData = [self fetchFromPort:[server port] + payload:@"GET /foo HTTP/1.0\r\n\r\n" + chunkSize:kSendChunkSize]; + STAssertNotNil(responseData, nil); + responseString = + [[[NSString alloc] initWithData:responseData + encoding:NSUTF8StringEncoding] autorelease]; + STAssertNotNil(responseString, nil); + STAssertTrue([responseString hasPrefix:@"HTTP/1.0 298"], nil); + STAssertTrue([responseString hasSuffix:@"FOO"], @"should end w/ our data"); + STAssertNotEquals([responseString rangeOfString:@"Content-Length: 3"].location, + (NSUInteger)NSNotFound, nil); + STAssertNotEquals([responseString rangeOfString:@"Content-Type: some/type"].location, + (NSUInteger)NSNotFound, nil); + STAssertNotEquals([responseString rangeOfString:@"Custom-Header: Custom_Value"].location, + (NSUInteger)NSNotFound, nil); + STAssertNotEquals([responseString rangeOfString:@"Custom-Header2: "].location, + (NSUInteger)NSNotFound, nil); + + [server stop]; +} + +- (void)testRequstEdgeCases { + // test all the odd things about requests + + TestServerDelegate *delegate = [TestServerDelegate testServerDelegate]; + STAssertNotNil(delegate, nil); + GTMHTTPServer *server = + [[[GTMHTTPServer alloc] initWithDelegate:delegate] autorelease]; + STAssertNotNil(server, nil); + NSError *error = nil; + STAssertTrue([server start:&error], @"failed to start (error=%@)", error); + STAssertNil(error, @"error: %@", error); + + // extra data (ie-pipelining) + + NSString *payload = + @"GET /some/server/path HTTP/1.0\r\n" + @"\r\n" + @"GET /some/server/path/too HTTP/1.0\r\n" + @"\r\n"; + // don't chunk this, we want to make sure both requests get to our server + [GTMUnitTestDevLog expectString:@"Got 38 extra bytes on http request, " + "ignoring them"]; + NSData *reply = + [self fetchFromPort:[server port] payload:payload chunkSize:0]; + STAssertNotNil(reply, nil); + STAssertEquals([delegate requestCount], (NSUInteger)1, nil); + + // close w/o full request + { + // local pool so we can force our handle to close + NSAutoreleasePool *localPool = [[NSAutoreleasePool alloc] init]; + NSFileHandle *handle = + [self fileHandleSendingToPort:[server port] + payload:@"GET /some/server/path HTTP/" + chunkSize:kSendChunkSize]; + STAssertNotNil(handle, nil); + // spin the run loop so reads the start of the request + NSDate* loopIntervalDate = + [NSDate dateWithTimeIntervalSinceNow:kRunLoopInterval]; + [[NSRunLoop currentRunLoop] runUntilDate:loopIntervalDate]; + // make sure we see the request at this point + STAssertEquals([server activeRequestCount], (NSUInteger)1, + @"should have started the request by now"); + // drop the pool to close the connection + [localPool release]; + // spin the run loop so it should see the close + loopIntervalDate = [NSDate dateWithTimeIntervalSinceNow:kRunLoopInterval]; + [[NSRunLoop currentRunLoop] runUntilDate:loopIntervalDate]; + // make sure we didn't get a request (1 is from test before) and make sure + // we don't have some in flight. + STAssertEquals([delegate requestCount], (NSUInteger)1, + @"shouldn't have gotten another request"); + STAssertEquals([server activeRequestCount], (NSUInteger)0, + @"should have cleaned up the pending connection"); + } + +} + +- (void)testExceptionDuringRequest { + + TestServerDelegate *delegate = [TestThrowingServerDelegate testServerDelegate]; + STAssertNotNil(delegate, nil); + GTMHTTPServer *server = + [[[GTMHTTPServer alloc] initWithDelegate:delegate] autorelease]; + STAssertNotNil(server, nil); + NSError *error = nil; + STAssertTrue([server start:&error], @"failed to start (error=%@)", error); + STAssertNil(error, @"error: %@", error); + [GTMUnitTestDevLog expectString:@"Exception trying to handle http request: " + "To test our handling"]; + NSData *responseData = [self fetchFromPort:[server port] + payload:@"GET /foo HTTP/1.0\r\n\r\n" + chunkSize:kSendChunkSize]; + STAssertNotNil(responseData, nil); + STAssertEquals([responseData length], (NSUInteger)0, nil); + STAssertEquals([delegate requestCount], (NSUInteger)1, nil); + STAssertEquals([server activeRequestCount], (NSUInteger)0, nil); +} + +@end + +// ---------------------------------------------------------------------------- + +@implementation GTMHTTPServerTest (PrivateMethods) + +- (NSData *)fetchFromPort:(unsigned short)port + payload:(NSString *)payload + chunkSize:(NSUInteger)chunkSize { + fetchedData_ = nil; + + NSFileHandle *handle = [self fileHandleSendingToPort:port + payload:payload + chunkSize:chunkSize]; + + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center addObserver:self + selector:@selector(readFinished:) + name:NSFileHandleReadToEndOfFileCompletionNotification + object:handle]; + [handle readToEndOfFileInBackgroundAndNotify]; + + // wait for our reply + NSDate* giveUpDate = [NSDate dateWithTimeIntervalSinceNow:kGiveUpInterval]; + while (!fetchedData_ && [giveUpDate timeIntervalSinceNow] > 0) { + NSDate* loopIntervalDate = + [NSDate dateWithTimeIntervalSinceNow:kRunLoopInterval]; + [[NSRunLoop currentRunLoop] runUntilDate:loopIntervalDate]; + } + + [center removeObserver:self + name:NSFileHandleReadToEndOfFileCompletionNotification + object:handle]; + + NSData *result = [fetchedData_ autorelease]; + fetchedData_ = nil; + return result; +} + +- (NSFileHandle *)fileHandleSendingToPort:(unsigned short)port + payload:(NSString *)payload + chunkSize:(NSUInteger)chunkSize { + int fd = socket(AF_INET, SOCK_STREAM, 0); + STAssertGreaterThan(fd, 0, @"failed to create socket"); + + struct sockaddr_in addr; + bzero(&addr, sizeof(addr)); + addr.sin_len = sizeof(addr); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + addr.sin_addr.s_addr = htonl(0x7F000001); + int connectResult = + connect(fd, (struct sockaddr*)(&addr), (socklen_t)sizeof(addr)); + STAssertEquals(connectResult, 0, nil); + + NSFileHandle *handle = + [[[NSFileHandle alloc] initWithFileDescriptor:fd + closeOnDealloc:YES] autorelease]; + STAssertNotNil(handle, nil); + + NSData *payloadData = [payload dataUsingEncoding:NSUTF8StringEncoding]; + + // we can send in one block or in chunked mode + if (chunkSize > 0) { + // we don't write the data in one large block, instead of write it out + // in bits to help test the data collection code. + NSUInteger length = [payloadData length]; + for (NSUInteger x = 0 ; x < length ; x += chunkSize) { + NSUInteger dataChunkSize = length - x; + if (dataChunkSize > chunkSize) { + dataChunkSize = chunkSize; + } + NSData *dataChunk + = [payloadData subdataWithRange:NSMakeRange(x, dataChunkSize)]; + [handle writeData:dataChunk]; + // delay after all but the last chunk to give it time to be read. + if ((x + chunkSize) < length) { + NSDate* loopIntervalDate = + [NSDate dateWithTimeIntervalSinceNow:kSendChunkInterval]; + [[NSRunLoop currentRunLoop] runUntilDate:loopIntervalDate]; + } + } + } else { + [handle writeData:payloadData]; + } + + return handle; +} + +- (void)readFinished:(NSNotification *)notification { + NSDictionary *userInfo = [notification userInfo]; + fetchedData_ = + [[userInfo objectForKey:NSFileHandleNotificationDataItem] retain]; +} + +@end + +// ---------------------------------------------------------------------------- + +@implementation TestServerDelegate + +- (id)init { + self = [super init]; + if (self) { + requests_ = [[NSMutableArray alloc] init]; + responses_ = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)dealloc { + [requests_ release]; + [responses_ release]; + [super dealloc]; +} + ++ (id)testServerDelegate { + return [[[[self class] alloc] init] autorelease]; +} + +- (NSUInteger)requestCount { + return [requests_ count]; +} + +- (GTMHTTPRequestMessage *)popRequest { + GTMHTTPRequestMessage *result = [[[requests_ lastObject] retain] autorelease]; + [requests_ removeLastObject]; + return result; +} + +- (void)pushResponse:(GTMHTTPResponseMessage *)message { + [responses_ addObject:message]; +} + +- (GTMHTTPResponseMessage *)httpServer:(GTMHTTPServer *)server + handleRequest:(GTMHTTPRequestMessage *)request { + [requests_ addObject:request]; + + GTMHTTPResponseMessage *result = nil; + if ([responses_ count] > 0) { + result = [[[responses_ lastObject] retain] autorelease]; + [responses_ removeLastObject]; + } else { + result = [GTMHTTPResponseMessage responseWithHTMLString:@"success"]; + } + return result; +} + +@end + +// ---------------------------------------------------------------------------- + +@implementation TestThrowingServerDelegate + +- (GTMHTTPResponseMessage *)httpServer:(GTMHTTPServer *)server + handleRequest:(GTMHTTPRequestMessage *)request { + // let the base do its normal work for counts, etc. + [super httpServer:server handleRequest:request]; + NSException *exception = + [NSException exceptionWithName:@"InternalTestingException" + reason:@"To test our handling" + userInfo:nil]; + @throw exception; +} + +@end |