diff --git a/Example/Tests/HttpFileParsingTests.m b/Example/Tests/HttpFileParsingTests.m index 07034ba..549a2cc 100644 --- a/Example/Tests/HttpFileParsingTests.m +++ b/Example/Tests/HttpFileParsingTests.m @@ -9,8 +9,11 @@ #import #import +#import #import +static NSString *const AuthToken = @"Token I_am_a_token"; + @interface HttpFileParsingTests : XCTestCase @end @@ -29,14 +32,35 @@ + (void)tearDown { // Un-register the mock URL protocol class. [NSURLProtocol unregisterClass:[VOKMockUrlProtocol class]]; - + [super tearDown]; +} + +- (void)tearDown +{ + // Make sure the URL protocol is not encoding auth params. + [VOKMockUrlProtocol setHeadersToEncode:nil]; [super tearDown]; } - (void)verifyRequestWithURLString:(NSString *)urlString + additionalHeaders:(NSDictionary *)additionalHeaders + bodyData:(NSData *)bodyData completion:(void (^)(NSData *data, NSHTTPURLResponse *response, NSError *error))completion { - NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString]]; + NSMutableURLRequest *request = [[NSURLRequest requestWithURL:[NSURL URLWithString:urlString]] mutableCopy]; + + if (bodyData) { + request.HTTPMethod = @"POST"; + request.HTTPBody = bodyData; + } + + if (additionalHeaders) { + for (NSString *key in additionalHeaders) { + NSString *value = additionalHeaders[key]; + [request setValue:value forHTTPHeaderField:key]; + } + } + XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)]; NSURLSession *session = [NSURLSession sharedSession]; [[session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *urlResponse, NSError *error) { @@ -49,6 +73,8 @@ - (void)verifyRequestWithURLString:(NSString *)urlString - (void)testNonexistentFileGives404 { [self verifyRequestWithURLString:@"http://example.com/DoesntExist.html" + additionalHeaders:nil + bodyData:nil completion:^(NSData *data, NSHTTPURLResponse *response, NSError *error) { XCTAssertEqual(response.statusCode, kHTTPStatusCodeNotFound); XCTAssertEqual(data.length, 0); @@ -58,6 +84,8 @@ - (void)testNonexistentFileGives404 - (void)testHttpFileEmpty { [self verifyRequestWithURLString:@"http://example.com/empty" + additionalHeaders:nil + bodyData:nil completion:^(NSData *data, NSHTTPURLResponse *response, NSError *error) { if (!data) { XCTFail(); @@ -73,6 +101,8 @@ - (void)testHttpFileEmpty - (void)testHttpFileBodyNoHeaders { [self verifyRequestWithURLString:@"http://example.com/bodyNoHeaders" + additionalHeaders:nil + bodyData:nil completion:^(NSData *data, NSHTTPURLResponse *response, NSError *error) { if (!data) { XCTFail(); @@ -88,6 +118,8 @@ - (void)testHttpFileBodyNoHeaders - (void)testHttpFileHeadersNoBody { [self verifyRequestWithURLString:@"http://example.com/headersNoBody" + additionalHeaders:nil + bodyData:nil completion:^(NSData *data, NSHTTPURLResponse *response, NSError *error) { if (!data) { XCTFail(); @@ -103,6 +135,8 @@ - (void)testHttpFileHeadersNoBody - (void)testHttpLongQueryFile { [self verifyRequestWithURLString:@"http://example.com/details?one=1&two=2&three=3&four=4&five=5&six=6&seven=7&eight=8&nine=9&ten=10&eleven=11&twelve=12&thirteen=13&fourteen=14&fifteen=15&sixteen=16&seventeen=17&eighteen=18&nineteen=19&twenty=20&twentyone=21&twntytwo=22&twentythree=23&twentyfour=24&twentyfive=25" + additionalHeaders:nil + bodyData:nil completion:^(NSData *data, NSHTTPURLResponse *response, NSError *error) { if (!data) { XCTFail(); @@ -115,4 +149,78 @@ - (void)testHttpLongQueryFile }]; } +- (void)testHttpHeadersEncoding +{ + [VOKMockUrlProtocol setHeadersToEncode:@[ + kHTTPHeaderFieldAuthorization, + kHTTPHeaderFieldContentLanguage, + ]]; + [self verifyRequestWithURLString:@"http://example.com/auth" + additionalHeaders: @{ + kHTTPHeaderFieldAuthorization: AuthToken, + kHTTPHeaderFieldContentLanguage: @"en", + } + bodyData:nil + completion:^(NSData *data, NSHTTPURLResponse *response, NSError *error) { + if (!data) { + XCTFail(); + return; + } + XCTAssertNil(error); + XCTAssertEqual(response.statusCode, kHTTPStatusCodeAccepted); + }]; +} + +- (void)testHttpHeadersWithJSONBody +{ + [VOKMockUrlProtocol setHeadersToEncode:@[ + kHTTPHeaderFieldAuthorization, + kHTTPHeaderFieldContentLanguage, + ]]; + + NSDictionary *foobar = @{ @"foo": @"bar" }; + NSError *error = nil; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:foobar + options:0 + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(jsonData); + + [self verifyRequestWithURLString:@"http://example.com/auth" + additionalHeaders: @{ + kHTTPHeaderFieldAuthorization: AuthToken, + kHTTPHeaderFieldContentLanguage: @"en", + kHTTPHeaderFieldContentType: @"application/json", + } + bodyData:jsonData + completion:^(NSData *data, NSHTTPURLResponse *response, NSError *error) { + if (!data) { + XCTFail(); + return; + } + XCTAssertNil(error); + XCTAssertEqual(response.statusCode, kHTTPStatusCodeAccepted); + }]; +} + +- (void)testHttpHeadersIgnoresUnencodedHeader +{ + [VOKMockUrlProtocol setHeadersToEncode:@[kHTTPHeaderFieldAuthorization]]; + [self verifyRequestWithURLString:@"http://example.com/auth" + additionalHeaders: @{ + kHTTPHeaderFieldAuthorization: AuthToken, + kHTTPHeaderFieldContentLanguage: @"en", + } + bodyData:nil + completion:^(NSData *data, NSHTTPURLResponse *response, NSError *error) { + if (!data) { + XCTFail(); + return; + } + XCTAssertNil(error); + XCTAssertEqual(response.statusCode, kHTTPStatusCodeAccepted); + + }]; +} + @end \ No newline at end of file diff --git a/Example/VOKMockData/GET|-auth|Authorization=Token%20I_am_a_token&Content-Language=en.http b/Example/VOKMockData/GET|-auth|Authorization=Token%20I_am_a_token&Content-Language=en.http new file mode 100644 index 0000000..0e20fb6 --- /dev/null +++ b/Example/VOKMockData/GET|-auth|Authorization=Token%20I_am_a_token&Content-Language=en.http @@ -0,0 +1,3 @@ +HTTP/1.1 202 Accepted + +{"favorite_dog_breed": "dogfish"} diff --git a/Example/VOKMockData/GET|-auth|Authorization=Token%20I_am_a_token.http b/Example/VOKMockData/GET|-auth|Authorization=Token%20I_am_a_token.http new file mode 100644 index 0000000..0e20fb6 --- /dev/null +++ b/Example/VOKMockData/GET|-auth|Authorization=Token%20I_am_a_token.http @@ -0,0 +1,3 @@ +HTTP/1.1 202 Accepted + +{"favorite_dog_breed": "dogfish"} diff --git a/Example/VOKMockData/POST|-auth|Authorization=Token%20I_am_a_token&Content-Language=en|d3-foo3-bare.http b/Example/VOKMockData/POST|-auth|Authorization=Token%20I_am_a_token&Content-Language=en|d3-foo3-bare.http new file mode 100644 index 0000000..0e20fb6 --- /dev/null +++ b/Example/VOKMockData/POST|-auth|Authorization=Token%20I_am_a_token&Content-Language=en|d3-foo3-bare.http @@ -0,0 +1,3 @@ +HTTP/1.1 202 Accepted + +{"favorite_dog_breed": "dogfish"} diff --git a/Example/VOKMockUrlProtocol/Images.xcassets/AppIcon.appiconset/Contents.json b/Example/VOKMockUrlProtocol/Images.xcassets/AppIcon.appiconset/Contents.json index f697f61..eeea76c 100644 --- a/Example/VOKMockUrlProtocol/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/Example/VOKMockUrlProtocol/Images.xcassets/AppIcon.appiconset/Contents.json @@ -5,16 +5,31 @@ "size" : "29x29", "scale" : "2x" }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, { "idiom" : "iphone", "size" : "40x40", "scale" : "2x" }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, { "idiom" : "iphone", "size" : "60x60", "scale" : "2x" }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, { "idiom" : "ipad", "size" : "29x29", @@ -44,10 +59,15 @@ "idiom" : "ipad", "size" : "76x76", "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } -} +} \ No newline at end of file diff --git a/README.md b/README.md index e5a59eb..b8e18f7 100644 --- a/README.md +++ b/README.md @@ -86,3 +86,7 @@ In order to switch back and forth between mock and live, you can also take out t ### Using with Frameworks/Swift When `VOKMockUrlProtocol` is built as a framework (usually for use with Swift), make sure to call the `setTestBundle:` class method and pass in your test bundle. Since the default behavior is to fall back to the bundle for the current class, that would look in the Framework's bundle rather than the test bundle, and nothing would work. + +### Using with HTTP Authorization headers + +To verify that authorization headers are being sent properly, make sure to set `setShouldEncodeAuthHeader` to YES. This will verify that the value for the http header "Authorization" is set to an expected value. Otherwise, auth headers will not be verified. diff --git a/VOKMockUrlProtocol.h b/VOKMockUrlProtocol.h index 3bfba73..81c08e3 100644 --- a/VOKMockUrlProtocol.h +++ b/VOKMockUrlProtocol.h @@ -20,4 +20,12 @@ */ + (void)setTestBundle:(NSBundle *)bundle; +/** + * Allows you to encode authorizatation headers if desired. Defaults to nil. + * + * @param headers The names of headers to encode as part of the file name as strings, + * or nil to not encode any headers. + */ ++ (void)setHeadersToEncode:(NSArray*)headers; + @end diff --git a/VOKMockUrlProtocol.m b/VOKMockUrlProtocol.m index d90e33f..adb2bbb 100644 --- a/VOKMockUrlProtocol.m +++ b/VOKMockUrlProtocol.m @@ -11,6 +11,7 @@ #import #import +#import #import #import @@ -55,6 +56,12 @@ + (void)setTestBundle:(NSBundle *)bundle testBundle = bundle; } +static NSArray *headersToEncode = nil; ++ (void)setHeadersToEncode:(NSArray*)headers +{ + headersToEncode = headers; +} + + (BOOL)canInitWithRequest:(NSURLRequest *)request { return YES; @@ -111,6 +118,28 @@ - (NSArray *)resourceNames [resourceName appendFormat:queryFormat, self.request.URL.query]; } + if (headersToEncode) { + NSDictionary *headerDict = self.request.allHTTPHeaderFields; + NSMutableArray *allIncludedHeaders = [NSMutableArray array]; + for (NSString *headerName in headersToEncode) { + NSString *headerValue = headerDict[headerName]; + if (headerValue) { + NSString *headerString = [NSString stringWithFormat:@"%@=%@", headerName, headerValue]; + [allIncludedHeaders addObject:headerString]; + } + } + + NSString *fullHeaderString = [allIncludedHeaders componentsJoinedByString:@"&"]; + if (fullHeaderString.length > 0) { + NSString *encodedHeaderString = [fullHeaderString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + NSData *fullHeaderData = [encodedHeaderString dataUsingEncoding:NSUTF8StringEncoding]; + NSString *hashedHeaderString = [self sha256HexOfData:fullHeaderData]; + + [resourceNames addObject:[[resourceName stringByAppendingFormat:AppendSeparatorFormat, hashedHeaderString] mutableCopy]]; + [resourceName appendFormat:AppendSeparatorFormat, encodedHeaderString]; + } + } + // If the request is one that can have a body... if ([kHTTPMethodPost isEqualToString:self.request.HTTPMethod] || [kHTTPMethodPatch isEqualToString:self.request.HTTPMethod] diff --git a/VOKMockUrlProtocol.podspec b/VOKMockUrlProtocol.podspec index fec5bc3..a5fbd58 100644 --- a/VOKMockUrlProtocol.podspec +++ b/VOKMockUrlProtocol.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "VOKMockUrlProtocol" - s.version = "2.2.0" + s.version = "2.2.1" s.summary = "A url protocol that parses and returns fake responses with mock data." s.homepage = "https://github.com/vokal/VOKMockUrlProtocol" s.license = { :type => "MIT", :file => "LICENSE"}