Skip to content
This repository was archived by the owner on Dec 12, 2019. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 110 additions & 2 deletions Example/Tests/HttpFileParsingTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
#import <XCTest/XCTest.h>

#import <HTTPStatusCodes.h>
#import <HTTPHeaderFields.h>
#import <VOKMockUrlProtocol.h>

static NSString *const AuthToken = @"Token I_am_a_token";

@interface HttpFileParsingTests : XCTestCase

@end
Expand All @@ -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) {
Expand All @@ -49,6 +73,8 @@ - (void)verifyRequestWithURLString:(NSString *)urlString
- (void)testNonexistentFileGives404
{
[self verifyRequestWithURLString:@"http://example.com/DoesntExist.html"
additionalHeaders:nil
bodyData:nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to have a convenience wrapper method like

- (void)verifyRequestWithURLString:(NSString *)urlString
                        completion:(void (^)(NSData *data, NSHTTPURLResponse *response, NSError *error))completion
{
     [self verifyRequestWithURLString:urlString
                    additionalHeaders:nil
                             bodyData:nil
                            completion:completion];
}

to avoid adding these two lines in a bunch of places?

completion:^(NSData *data, NSHTTPURLResponse *response, NSError *error) {
XCTAssertEqual(response.statusCode, kHTTPStatusCodeNotFound);
XCTAssertEqual(data.length, 0);
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
HTTP/1.1 202 Accepted
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't expect a 202 Accepted on a GET request... but 🤷 doesn't actually matter for the purposes of testing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was mostly just to match existing files - 200 OK would probably be more appropriate, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. Like I said, though, doesn't really matter for how it's being used.


{"favorite_dog_breed": "dogfish"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
HTTP/1.1 202 Accepted

{"favorite_dog_breed": "dogfish"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
HTTP/1.1 202 Accepted

{"favorite_dog_breed": "dogfish"}
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -44,10 +59,15 @@
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "83.5x83.5",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
}
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this no longer matches the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true but I wanted to hold off rewriting this until we had a better idea on what the final bit would be.

8 changes: 8 additions & 0 deletions VOKMockUrlProtocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,12 @@
*/
+ (void)setTestBundle:(NSBundle *)bundle;

/**
* Allows you to encode authorizatation headers if desired. Defaults to nil.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Description doesn't match revised method.

*
* @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<NSString *>*)headers;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be inclined to add a getter method, too.


@end
29 changes: 29 additions & 0 deletions VOKMockUrlProtocol.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

#import <ILGHttpConstants/HTTPStatusCodes.h>
#import <ILGHttpConstants/HTTPMethods.h>
#import <ILGHttpConstants/HttpHeaderFields.h>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/Http/HTTP/?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to work fine without it but just in case it breaks in the future

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most OS X filesystems are set up case-insensitive, so it'd probably be okay, except on that rare case-sensitive filesystem.

#import <VOKBenkode/VOKBenkode.h>
#import <sys/syslimits.h>

Expand Down Expand Up @@ -55,6 +56,12 @@ + (void)setTestBundle:(NSBundle *)bundle
testBundle = bundle;
}

static NSArray<NSString *> *headersToEncode = nil;
+ (void)setHeadersToEncode:(NSArray<NSString *>*)headers
{
headersToEncode = headers;
}

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
return YES;
Expand Down Expand Up @@ -111,6 +118,28 @@ - (NSArray *)resourceNames
[resourceName appendFormat:queryFormat, self.request.URL.query];
}

if (headersToEncode) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if there are no headers to encode, we still need to add a separator to distinguish the header segment of the file name.

NSDictionary *headerDict = self.request.allHTTPHeaderFields;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a potential pitfall here in that, at least according to Apple's docs, header names are case-sensitive, but my experience has been that the majority of servers treat them as case-insensitive. Basically, if the app sets AUTHORIZATION: whatever and I ask for the Authorization header, it won't find it.

I'd be tempted to iterate over headerDict, normalize the header name to lower or upper case and compare to a normalized version of headersToEncode. I'm not sure that's a good idea, though—it might be better to just document the potential pitfall.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add a note when I finalize the documentation that this is case-sensitive.

NSMutableArray <NSString *>*allIncludedHeaders = [NSMutableArray array];
for (NSString *headerName in headersToEncode) {
NSString *headerValue = headerDict[headerName];
if (headerValue) {
NSString *headerString = [NSString stringWithFormat:@"%@=%@", headerName, headerValue];
[allIncludedHeaders addObject:headerString];
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of building up a string, how about building up a dictionary mapping header names to values, then bencoding it to put into the resource name?

Maybe that'd also allow taking the all-headers dict and filtering it rather than iterating over it?


NSString *fullHeaderString = [allIncludedHeaders componentsJoinedByString:@"&"];
if (fullHeaderString.length > 0) {
NSString *encodedHeaderString = [fullHeaderString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this line generating a deprecation warning?

NSData *fullHeaderData = [encodedHeaderString dataUsingEncoding:NSUTF8StringEncoding];
NSString *hashedHeaderString = [self sha256HexOfData:fullHeaderData];

[resourceNames addObject:[[resourceName stringByAppendingFormat:AppendSeparatorFormat, hashedHeaderString] mutableCopy]];
[resourceName appendFormat:AppendSeparatorFormat, encodedHeaderString];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There may already be multiple resource names at this point, so I think you have to add your potential two additions to each existing resource name—take a look below, around lines 180–188.

}
}

// If the request is one that can have a body...
if ([kHTTPMethodPost isEqualToString:self.request.HTTPMethod]
|| [kHTTPMethodPatch isEqualToString:self.request.HTTPMethod]
Expand Down
2 changes: 1 addition & 1 deletion VOKMockUrlProtocol.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "VOKMockUrlProtocol"
s.version = "2.2.0"
s.version = "2.2.1"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to need to be a major version bump, since it'll break compatibility with the existing naming format.

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"}
Expand Down