diff options
author | Benoit St-Pierre <ben@benoitstpierre.com> | 2018-04-19 15:56:11 -0400 |
---|---|---|
committer | Ryan Wilson <wilsonryan@google.com> | 2018-04-19 15:56:11 -0400 |
commit | 9eea8d4fc480f32d57cab344c32ebe631a083e9d (patch) | |
tree | dc8b95409edc50157a364c32121b489e6b40b9ca | |
parent | 0df8378971553a203cc6982a298f342baecae543 (diff) |
Add unit tests for FirebaseCore (#751)
* Add unit tests for FIRMutableDictionary
Basic unit tests to increase overall code coverage.
* Add unit tests for FIRReachabilityChecker
Basic unit tests to increase overall test coverage.
* Add unit tests for FIRNetwork
Basic unit tests to increase overall test coverage.
This also includes a mock HTTP server to serve mock network responses
through the network stack.
* Remove +internal from imports
This was unnecessary.
* Move Example/Tests/Core/Utils files to Example/Tests/Core
This simplifies build files to include the correct dependencies
I had also missed some tvOS test fixes.
* PR Feedback
Fixed some style issues and updated GTMHTTPServer comments.
* Moved GTMHTTPServer.h into third_party directory
* Revert Firebase Xcode project for merging.
* Add tests to project file.
-rw-r--r-- | Example/Core/App/iOS/Core-Info.plist | 5 | ||||
-rw-r--r-- | Example/Core/App/macOS/Core-Info.plist | 5 | ||||
-rw-r--r-- | Example/Core/App/tvOS/Info.plist | 5 | ||||
-rw-r--r-- | Example/Core/Tests/FIRMutableDictionaryTest.m | 87 | ||||
-rw-r--r-- | Example/Core/Tests/FIRNetworkTest.m | 992 | ||||
-rw-r--r-- | Example/Core/Tests/FIRReachabilityCheckerTest.m | 365 | ||||
-rw-r--r-- | Example/Core/Tests/Tests-Info.plist | 5 | ||||
-rw-r--r-- | Example/Core/Tests/third_party/GTMHTTPServer.h | 173 | ||||
-rw-r--r-- | Example/Core/Tests/third_party/GTMHTTPServer.m | 630 | ||||
-rw-r--r-- | Example/Firebase.xcodeproj/project.pbxproj | 42 |
10 files changed, 2309 insertions, 0 deletions
diff --git a/Example/Core/App/iOS/Core-Info.plist b/Example/Core/App/iOS/Core-Info.plist index 7576a0d..fc26896 100644 --- a/Example/Core/App/iOS/Core-Info.plist +++ b/Example/Core/App/iOS/Core-Info.plist @@ -24,6 +24,11 @@ <string>1.0</string> <key>LSRequiresIPhoneOS</key> <true/> + <key>NSAppTransportSecurity</key> + <dict> + <key>NSAllowsArbitraryLoads</key> + <true/> + </dict> <key>UILaunchStoryboardName</key> <string>LaunchScreen</string> <key>UIMainStoryboardFile</key> diff --git a/Example/Core/App/macOS/Core-Info.plist b/Example/Core/App/macOS/Core-Info.plist index 6f7d78e..f5a2636 100644 --- a/Example/Core/App/macOS/Core-Info.plist +++ b/Example/Core/App/macOS/Core-Info.plist @@ -22,6 +22,11 @@ <string>1.0</string> <key>LSMinimumSystemVersion</key> <string>$(MACOSX_DEPLOYMENT_TARGET)</string> + <key>NSAppTransportSecurity</key> + <dict> + <key>NSAllowsArbitraryLoads</key> + <true/> + </dict> <key>NSHumanReadableCopyright</key> <string>Copyright © 2017 Google. All rights reserved.</string> <key>NSMainStoryboardFile</key> diff --git a/Example/Core/App/tvOS/Info.plist b/Example/Core/App/tvOS/Info.plist index 02942a3..d76f80a 100644 --- a/Example/Core/App/tvOS/Info.plist +++ b/Example/Core/App/tvOS/Info.plist @@ -20,6 +20,11 @@ <string>1</string> <key>LSRequiresIPhoneOS</key> <true/> + <key>NSAppTransportSecurity</key> + <dict> + <key>NSAllowsArbitraryLoads</key> + <true/> + </dict> <key>UIMainStoryboardFile</key> <string>Main</string> <key>UIRequiredDeviceCapabilities</key> diff --git a/Example/Core/Tests/FIRMutableDictionaryTest.m b/Example/Core/Tests/FIRMutableDictionaryTest.m new file mode 100644 index 0000000..56c079c --- /dev/null +++ b/Example/Core/Tests/FIRMutableDictionaryTest.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 "FIRTestCase.h" + +#import <FirebaseCore/FIRMutableDictionary.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 FIRMutableDictionaryTest : FIRTestCase +@property(nonatomic) FIRMutableDictionary *dictionary; +@end + +@implementation FIRMutableDictionaryTest + +- (void)setUp { + [super setUp]; + self.dictionary = [[FIRMutableDictionary 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/Example/Core/Tests/FIRNetworkTest.m b/Example/Core/Tests/FIRNetworkTest.m new file mode 100644 index 0000000..ba288d6 --- /dev/null +++ b/Example/Core/Tests/FIRNetworkTest.m @@ -0,0 +1,992 @@ +// 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 "FIRTestCase.h" + +#import "GTMHTTPServer.h" + +#import <FirebaseCore/FIRNetwork.h> +#import <FirebaseCore/FIRReachabilityChecker.h> +#import <GoogleToolboxForMac/GTMNSData+zlib.h> + +@interface FIRNetwork () + +- (void)reachability:(FIRReachabilityChecker *)reachability + statusChanged:(FIRReachabilityStatus)status; + +@end + +@interface FIRNetworkURLSession () + +- (void)maybeRemoveTempFilesAtURL:(NSURL *)tempFile expiringTime:(NSTimeInterval)expiringTime; + +@end + +@interface FIRNetworkTest : FIRTestCase <FIRNetworkReachabilityDelegate> +@end + +@implementation FIRNetworkTest { + dispatch_queue_t _backgroundQueue; + FIRNetwork *_network; + + /// Fake Server. + GTMHTTPServer *_httpServer; + GTMHTTPRequestMessage *_request; + int _statusCode; + + // For network reachability test. + BOOL _fakeNetworkIsReachable; + BOOL _currentNetworkStatus; + FIRReachabilityStatus _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 = [[FIRNetwork 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 = kFIRReachabilityViaWifi; + [_network reachability:reachabilityMock statusChanged:[reachabilityMock reachabilityStatus]]; + XCTAssertTrue([_network isNetworkConnected]); + XCTAssertEqual(_currentNetworkStatus, _fakeNetworkIsReachable); + + // Fake scenario without connectivity. + _fakeNetworkIsReachable = NO; + _fakeReachabilityStatus = kFIRReachabilityNotReachable; + [_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(_network.hasUploadInProgress, "There must be no pending request"); + [expectation fulfill]; + }]; + XCTAssertTrue(_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(_network.hasUploadInProgress, "There must be no pending request"); + [expectation fulfill]; + }]; + XCTAssertTrue(_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, FIRErrorCodeNetworkInvalidURL); + XCTAssertFalse(_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, FIRErrorCodeNetworkInvalidURL); + XCTAssertFalse(_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(_request); + XCTAssertEqualObjects([_request.URL absoluteString], [url absoluteString]); + XCTAssertFalse(_network.hasUploadInProgress, "There must be no pending request"); + [expectation fulfill]; + }]; + XCTAssertTrue(_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(_network.hasUploadInProgress, @"hasUploadInProgress must be false"); + [expectation fulfill]; + }]; + + XCTAssertTrue(_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(_network.hasUploadInProgress, "There must be no pending request"); + [expectation fulfill]; + }]; + XCTAssertTrue(_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(_network.hasUploadInProgress, "There must be no pending request"); + [expectation fulfill]; + }]; + XCTAssertTrue(_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, FIRErrorCodeNetworkInvalidURL); + XCTAssertFalse(_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, FIRErrorCodeNetworkInvalidURL); + XCTAssertFalse(_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(_request); + XCTAssertEqualObjects([_request.URL absoluteString], [url absoluteString]); + XCTAssertFalse(_network.hasUploadInProgress, "There must be no pending request"); + [expectation fulfill]; + }]; + XCTAssertTrue(_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(_network.hasUploadInProgress, @"hasUploadInProgress must be false"); + [expectation fulfill]; + }]; + + XCTAssertTrue(_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(_network.hasUploadInProgress, "There must be no pending request"); + [expectation fulfill]; + }]; + XCTAssertTrue(_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(_network.hasUploadInProgress, "There must be no pending request"); + [expectation fulfill]; + }]; + XCTAssertTrue(_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, FIRErrorCodeNetworkInvalidURL); + XCTAssertFalse(_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, FIRErrorCodeNetworkInvalidURL); + XCTAssertFalse(_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(_network.hasUploadInProgress, "There must be no pending request"); + [expectation fulfill]; + }]; + XCTAssertTrue(_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(_network.hasUploadInProgress, @"hasUploadInProgress must be false"); + [expectation fulfill]; + }]; + + XCTAssertTrue(_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 = [_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(_network.hasUploadInProgress, "There must be no pending request"); + [expectation fulfill]; + }]; + XCTAssertTrue(_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(_network.hasUploadInProgress, "There must be no pending request"); + [expectation fulfill]; + }]; + XCTAssertTrue(_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, FIRErrorCodeNetworkInvalidURL); + XCTAssertFalse(_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, FIRErrorCodeNetworkInvalidURL); + XCTAssertFalse(_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(_network.hasUploadInProgress, "There must be no pending request"); + [expectation fulfill]; + }]; + XCTAssertTrue(_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(_network.hasUploadInProgress, @"hasUploadInProgress must be false"); + [expectation fulfill]; + }]; + + XCTAssertTrue(_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 = [_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]; + + FIRNetworkURLSession *session = [[FIRNetworkURLSession alloc] + initWithNetworkLoggerDelegate:(id<FIRNetworkLoggerDelegate>)_network]; + NSArray *paths = + NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); + NSString *applicationSupportDirectory = paths.firstObject; + NSArray *tempPathComponents = @[ + applicationSupportDirectory, kFIRNetworkApplicationSupportSubdirectory, + @"FIRNetworkTemporaryDirectory" + ]; + 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 gtm_dataByInflatingData: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; +} + +- (FIRReachabilityStatus)reachabilityStatus { + return _fakeReachabilityStatus; +} + +#pragma mark - FIRReachabilityDelegate + +- (void)reachabilityDidChange { + _currentNetworkStatus = _fakeNetworkIsReachable; +} + +@end diff --git a/Example/Core/Tests/FIRReachabilityCheckerTest.m b/Example/Core/Tests/FIRReachabilityCheckerTest.m new file mode 100644 index 0000000..7b6b068 --- /dev/null +++ b/Example/Core/Tests/FIRReachabilityCheckerTest.m @@ -0,0 +1,365 @@ +// 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 "FIRTestCase.h" + +#import <FirebaseCore/FIRReachabilityChecker+Internal.h> +#import <FirebaseCore/FIRReachabilityChecker.h> + +@interface FIRReachabilityCheckerTest : FIRTestCase <FIRReachabilityDelegate> { + @private + FIRReachabilityChecker *checker_; + NSMutableArray *statuses_; + BOOL createFail_; + BOOL setCallbackFail_; + BOOL scheduleUnscheduleFail_; +} + +- (void *)createReachabilityWithAllocator:(CFAllocatorRef)allocator withName:(const char *)hostname; +- (BOOL)reachability:(const void *)reachability + setCallback:(SCNetworkReachabilityCallBack)callback + withContext:(SCNetworkReachabilityContext *)context; +- (BOOL)scheduleReachability:(const void *)reachability + runLoop:(CFRunLoopRef)runLoop + runLoopMode:(CFStringRef)runLoopMode; +- (BOOL)unscheduleReachability:(const void *)reachability + runLoop:(CFRunLoopRef)runLoop + runLoopMode:(CFStringRef)runLoopMode; +- (void)releaseReachability:(const void *)reachability; +@end + +static NSString *const kHostname = @"www.google.com"; +static const void *kFakeReachabilityObject = (const void *)0x8badf00d; + +static FIRReachabilityCheckerTest *FakeReachabilityTest = nil; + +static struct { + int callsMade; + int createCall; + int setCallbackCall; + int scheduleCall; + int unscheduleCall; + int releaseCall; + void (*callback)(SCNetworkReachabilityRef, SCNetworkReachabilityFlags, void *info); + void *callbackInfo; +} FakeReachability; + +static SCNetworkReachabilityRef ReachabilityCreateWithName(CFAllocatorRef allocator, + const char *hostname) { + return (SCNetworkReachabilityRef) + [FakeReachabilityTest createReachabilityWithAllocator:allocator withName:hostname]; +} + +static Boolean ReachabilitySetCallback(SCNetworkReachabilityRef reachability, + SCNetworkReachabilityCallBack callback, + SCNetworkReachabilityContext *context) { + return [FakeReachabilityTest reachability:reachability setCallback:callback withContext:context]; +} + +static Boolean ReachabilityScheduleWithRunLoop(SCNetworkReachabilityRef reachability, + CFRunLoopRef runLoop, + CFStringRef runLoopMode) { + return [FakeReachabilityTest scheduleReachability:reachability + runLoop:runLoop + runLoopMode:runLoopMode]; +} + +static Boolean ReachabilityUnscheduleFromRunLoop(SCNetworkReachabilityRef reachability, + CFRunLoopRef runLoop, + CFStringRef runLoopMode) { + return [FakeReachabilityTest unscheduleReachability:reachability + runLoop:runLoop + runLoopMode:runLoopMode]; +} + +static void ReachabilityRelease(CFTypeRef reachability) { + [FakeReachabilityTest releaseReachability:reachability]; +} + +static const struct FIRReachabilityApi kTestReachabilityApi = { + ReachabilityCreateWithName, ReachabilitySetCallback, ReachabilityScheduleWithRunLoop, + ReachabilityUnscheduleFromRunLoop, ReachabilityRelease, +}; + +@implementation FIRReachabilityCheckerTest + +- (void)resetFakeReachability { + FakeReachabilityTest = self; + FakeReachability.callsMade = 0; + FakeReachability.createCall = -1; + FakeReachability.setCallbackCall = -1; + FakeReachability.scheduleCall = -1; + FakeReachability.unscheduleCall = -1; + FakeReachability.releaseCall = -1; + FakeReachability.callback = NULL; + FakeReachability.callbackInfo = NULL; +} + +- (void)setUp { + [super setUp]; + + [self resetFakeReachability]; + createFail_ = NO; + setCallbackFail_ = NO; + scheduleUnscheduleFail_ = NO; + + checker_ = [[FIRReachabilityChecker alloc] initWithReachabilityDelegate:self + loggerDelegate:nil + withHost:kHostname]; + statuses_ = [[NSMutableArray alloc] init]; +} + +- (void *)createReachabilityWithAllocator:(CFAllocatorRef)allocator + withName:(const char *)hostname { + XCTAssertTrue(allocator == kCFAllocatorDefault, @""); + XCTAssertEqual(strcmp(hostname, [kHostname UTF8String]), 0, @""); + XCTAssertEqual(FakeReachability.callsMade, 0, @"create call must always come first."); + XCTAssertEqual(FakeReachability.createCall, -1, @"create call must only be called once."); + FakeReachability.createCall = ++FakeReachability.callsMade; + return createFail_ ? NULL : (void *)kFakeReachabilityObject; +} + +- (BOOL)reachability:(const void *)reachability + setCallback:(SCNetworkReachabilityCallBack)callback + withContext:(SCNetworkReachabilityContext *)context { + XCTAssertEqual(reachability, kFakeReachabilityObject, @"got bad object"); + XCTAssertEqual((int)context->version, 0, @""); + XCTAssertEqual(context->info, (__bridge void *)checker_, @""); + XCTAssertEqual((void *)context->retain, NULL, @""); + XCTAssertEqual((void *)context->release, NULL, @""); + XCTAssertEqual((void *)context->copyDescription, NULL, @""); + XCTAssertEqual(FakeReachability.setCallbackCall, -1, @"setCallback should only be called once."); + FakeReachability.setCallbackCall = ++FakeReachability.callsMade; + XCTAssertTrue(callback != NULL, @""); + FakeReachability.callback = callback; + XCTAssertTrue(context->info != NULL, @""); + FakeReachability.callbackInfo = context->info; + return setCallbackFail_ ? NO : YES; +} + +- (BOOL)scheduleReachability:(const void *)reachability + runLoop:(CFRunLoopRef)runLoop + runLoopMode:(CFStringRef)runLoopMode { + XCTAssertEqual(reachability, kFakeReachabilityObject, @"got bad object"); + XCTAssertEqual(runLoop, CFRunLoopGetMain(), @"bad run loop"); + XCTAssertEqualObjects((__bridge NSString *)runLoopMode, + (__bridge NSString *)kCFRunLoopCommonModes, @"bad run loop mode"); + XCTAssertEqual(FakeReachability.scheduleCall, -1, + @"scheduleWithRunLoop should only be called once."); + FakeReachability.scheduleCall = ++FakeReachability.callsMade; + return scheduleUnscheduleFail_ ? NO : YES; +} + +- (BOOL)unscheduleReachability:(const void *)reachability + runLoop:(CFRunLoopRef)runLoop + runLoopMode:(CFStringRef)runLoopMode { + XCTAssertEqual(reachability, kFakeReachabilityObject, @"got bad object"); + XCTAssertEqual(runLoop, CFRunLoopGetMain(), @"bad run loop"); + XCTAssertEqualObjects((__bridge NSString *)runLoopMode, + (__bridge NSString *)kCFRunLoopCommonModes, @"bad run loop mode"); + XCTAssertEqual(FakeReachability.unscheduleCall, -1, + @"unscheduleFromRunLoop should only be called once."); + FakeReachability.unscheduleCall = ++FakeReachability.callsMade; + return scheduleUnscheduleFail_ ? NO : YES; +} + +- (void)releaseReachability:(const void *)reachability { + XCTAssertEqual(reachability, kFakeReachabilityObject, @"got bad object"); + XCTAssertEqual(FakeReachability.releaseCall, -1, @"release should only be called once."); + FakeReachability.releaseCall = ++FakeReachability.callsMade; +} + +- (void)reachability:(FIRReachabilityChecker *)reachability + statusChanged:(FIRReachabilityStatus)status { + [statuses_ addObject:[NSNumber numberWithInt:(int)status]]; +} + +#pragma mark - Test + +- (void)testApiHappyPath { + [checker_ setReachabilityApi:&kTestReachabilityApi]; + XCTAssertEqual([checker_ reachabilityApi], &kTestReachabilityApi, @""); + + XCTAssertFalse(checker_.isActive, @""); + XCTAssertEqual(checker_.reachabilityStatus, kFIRReachabilityUnknown, @""); + + XCTAssertTrue([checker_ start], @""); + + XCTAssertTrue(checker_.isActive, @""); + XCTAssertEqual([statuses_ count], (NSUInteger)0, @""); + XCTAssertEqual(checker_.reachabilityStatus, kFIRReachabilityUnknown, @""); + + FakeReachability.callback(kFakeReachabilityObject, 0, FakeReachability.callbackInfo); + + XCTAssertEqual([statuses_ count], (NSUInteger)1, @""); + XCTAssertEqual([(NSNumber *)[statuses_ objectAtIndex:0] intValue], + (int)kFIRReachabilityNotReachable, @""); + XCTAssertEqual(checker_.reachabilityStatus, kFIRReachabilityNotReachable, @""); + + FakeReachability.callback(kFakeReachabilityObject, kSCNetworkReachabilityFlagsReachable, + FakeReachability.callbackInfo); + + XCTAssertEqual([statuses_ count], (NSUInteger)2, @""); + XCTAssertEqual([(NSNumber *)[statuses_ objectAtIndex:1] intValue], (int)kFIRReachabilityViaWifi, + @""); + XCTAssertEqual(checker_.reachabilityStatus, kFIRReachabilityViaWifi, @""); + + FakeReachability.callback( + kFakeReachabilityObject, + kSCNetworkReachabilityFlagsReachable | kSCNetworkReachabilityFlagsConnectionRequired, + FakeReachability.callbackInfo); + + XCTAssertEqual([statuses_ count], (NSUInteger)3, @""); + XCTAssertEqual([(NSNumber *)[statuses_ objectAtIndex:2] intValue], + (int)kFIRReachabilityNotReachable, @""); + XCTAssertEqual(checker_.reachabilityStatus, kFIRReachabilityNotReachable, @""); + +#if TARGET_OS_IOS || TARGET_OS_TV + FakeReachability.callback( + kFakeReachabilityObject, + kSCNetworkReachabilityFlagsReachable | kSCNetworkReachabilityFlagsIsWWAN, + FakeReachability.callbackInfo); + + XCTAssertEqual([statuses_ count], (NSUInteger)4, @""); + XCTAssertEqual([(NSNumber *)[statuses_ objectAtIndex:3] intValue], + (int)kFIRReachabilityViaCellular, @""); + XCTAssertEqual(checker_.reachabilityStatus, kFIRReachabilityViaCellular, @""); + + FakeReachability.callback(kFakeReachabilityObject, kSCNetworkReachabilityFlagsIsWWAN, + FakeReachability.callbackInfo); + + XCTAssertEqual([statuses_ count], (NSUInteger)5, @""); + XCTAssertEqual([(NSNumber *)[statuses_ objectAtIndex:4] intValue], + (int)kFIRReachabilityNotReachable, @""); + XCTAssertEqual(checker_.reachabilityStatus, kFIRReachabilityNotReachable, @""); +#endif + + [checker_ stop]; + + XCTAssertFalse(checker_.isActive, @""); + XCTAssertEqual(checker_.reachabilityStatus, kFIRReachabilityUnknown, @""); + + XCTAssertEqual(FakeReachability.callsMade, 5, @""); + + XCTAssertEqual(FakeReachability.createCall, 1, @""); + XCTAssertEqual(FakeReachability.setCallbackCall, 2, @""); + XCTAssertEqual(FakeReachability.scheduleCall, 3, @""); + XCTAssertEqual(FakeReachability.unscheduleCall, 4, @""); + XCTAssertEqual(FakeReachability.releaseCall, 5, @""); +} + +- (void)testApiCreateFail { + [checker_ setReachabilityApi:&kTestReachabilityApi]; + XCTAssertEqual([checker_ reachabilityApi], &kTestReachabilityApi, @""); + + createFail_ = YES; + + XCTAssertFalse(checker_.isActive, @""); + XCTAssertEqual(checker_.reachabilityStatus, kFIRReachabilityUnknown, @""); + + XCTAssertFalse([checker_ start], @""); + + XCTAssertFalse(checker_.isActive, @""); + XCTAssertEqual(checker_.reachabilityStatus, kFIRReachabilityUnknown, @""); + + [checker_ stop]; + + XCTAssertFalse(checker_.isActive, @""); + XCTAssertEqual(checker_.reachabilityStatus, kFIRReachabilityUnknown, @""); + + XCTAssertEqual(FakeReachability.callsMade, 1, @""); + + XCTAssertEqual(FakeReachability.createCall, 1, @""); + XCTAssertEqual(FakeReachability.setCallbackCall, -1, @""); + XCTAssertEqual(FakeReachability.scheduleCall, -1, @""); + XCTAssertEqual(FakeReachability.unscheduleCall, -1, @""); + XCTAssertEqual(FakeReachability.releaseCall, -1, @""); + + XCTAssertEqual([statuses_ count], (NSUInteger)0, @""); +} + +- (void)testApiCallbackFail { + [checker_ setReachabilityApi:&kTestReachabilityApi]; + XCTAssertEqual([checker_ reachabilityApi], &kTestReachabilityApi, @""); + + setCallbackFail_ = YES; + + XCTAssertFalse(checker_.isActive, @""); + XCTAssertEqual(checker_.reachabilityStatus, kFIRReachabilityUnknown, @""); + + XCTAssertFalse([checker_ start], @""); + + XCTAssertFalse(checker_.isActive, @""); + XCTAssertEqual(checker_.reachabilityStatus, kFIRReachabilityUnknown, @""); + + [checker_ stop]; + + XCTAssertFalse(checker_.isActive, @""); + XCTAssertEqual(checker_.reachabilityStatus, kFIRReachabilityUnknown, @""); + + XCTAssertEqual(FakeReachability.callsMade, 3, @""); + + XCTAssertEqual(FakeReachability.createCall, 1, @""); + XCTAssertEqual(FakeReachability.setCallbackCall, 2, @""); + XCTAssertEqual(FakeReachability.scheduleCall, -1, @""); + XCTAssertEqual(FakeReachability.unscheduleCall, -1, @""); + XCTAssertEqual(FakeReachability.releaseCall, 3, @""); + + XCTAssertEqual([statuses_ count], (NSUInteger)0, @""); +} + +- (void)testApiScheduleFail { + [checker_ setReachabilityApi:&kTestReachabilityApi]; + XCTAssertEqual([checker_ reachabilityApi], &kTestReachabilityApi, @""); + + scheduleUnscheduleFail_ = YES; + + XCTAssertFalse(checker_.isActive, @""); + XCTAssertEqual(checker_.reachabilityStatus, kFIRReachabilityUnknown, @""); + + XCTAssertFalse([checker_ start], @""); + + XCTAssertFalse(checker_.isActive, @""); + XCTAssertEqual(checker_.reachabilityStatus, kFIRReachabilityUnknown, @""); + + [checker_ stop]; + + XCTAssertFalse(checker_.isActive, @""); + XCTAssertEqual(checker_.reachabilityStatus, kFIRReachabilityUnknown, @""); + + XCTAssertEqual(FakeReachability.callsMade, 4, @""); + + XCTAssertEqual(FakeReachability.createCall, 1, @""); + XCTAssertEqual(FakeReachability.setCallbackCall, 2, @""); + XCTAssertEqual(FakeReachability.scheduleCall, 3, @""); + XCTAssertEqual(FakeReachability.unscheduleCall, -1, @""); + XCTAssertEqual(FakeReachability.releaseCall, 4, @""); + + XCTAssertEqual([statuses_ count], (NSUInteger)0, @""); +} + +- (void)testBadHost { + XCTAssertNil([[FIRReachabilityChecker alloc] + initWithReachabilityDelegate:self + loggerDelegate:(id<FIRNetworkLoggerDelegate>)self + withHost:nil], + @"Creating a checker with nil hostname must fail."); + XCTAssertNil([[FIRReachabilityChecker alloc] + initWithReachabilityDelegate:self + loggerDelegate:(id<FIRNetworkLoggerDelegate>)self + withHost:@""], + @"Creating a checker with empty hostname must fail."); +} + +@end diff --git a/Example/Core/Tests/Tests-Info.plist b/Example/Core/Tests/Tests-Info.plist index 169b6f7..94f0bf9 100644 --- a/Example/Core/Tests/Tests-Info.plist +++ b/Example/Core/Tests/Tests-Info.plist @@ -18,5 +18,10 @@ <string>????</string> <key>CFBundleVersion</key> <string>1</string> + <key>NSAppTransportSecurity</key> + <dict> + <key>NSAllowsArbitraryLoads</key> + <true/> + </dict> </dict> </plist> diff --git a/Example/Core/Tests/third_party/GTMHTTPServer.h b/Example/Core/Tests/third_party/GTMHTTPServer.h new file mode 100644 index 0000000..cc51207 --- /dev/null +++ b/Example/Core/Tests/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/Example/Core/Tests/third_party/GTMHTTPServer.m b/Example/Core/Tests/third_party/GTMHTTPServer.m new file mode 100644 index 0000000..526d8c5 --- /dev/null +++ b/Example/Core/Tests/third_party/GTMHTTPServer.m @@ -0,0 +1,630 @@ +/* Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// Based a little on HTTPServer, part of the CocoaHTTPServer sample code found at +// https://opensource.apple.com/source/HTTPServer/HTTPServer-11/CocoaHTTPServer/ +// License for the CocoaHTTPServer sample code: +// +// Software License Agreement (BSD License) +// +// Copyright (c) 2011, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software in source and binary forms, +// with or without modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above +// copyright notice, this list of conditions and the +// following disclaimer. +// +// * Neither the name of Deusty nor the names of its +// contributors may be used to endorse or promote products +// derived from this software without specific prior +// written permission of Deusty, LLC. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// + +#import <netinet/in.h> +#import <sys/socket.h> +#import <unistd.h> + +#define GTMHTTPSERVER_DEFINE_GLOBALS +#import "GTMHTTPServer.h" + +// avoid some of GTM's promiscuous dependencies +#ifndef _GTMDevLog +#define _GTMDevLog NSLog +#endif + +#ifndef GTM_STATIC_CAST +#define GTM_STATIC_CAST(type, object) ((type *)(object)) +#endif + +#ifndef GTMCFAutorelease +#define GTMCFAutorelease(x) ([(id)x autorelease]) +#endif + +@interface GTMHTTPServer (PrivateMethods) +- (void)acceptedConnectionNotification:(NSNotification *)notification; +- (NSMutableDictionary *)connectionWithFileHandle:(NSFileHandle *)fileHandle; +- (void)dataAvailableNotification:(NSNotification *)notification; +- (NSMutableDictionary *)lookupConnection:(NSFileHandle *)fileHandle; +- (void)closeConnection:(NSMutableDictionary *)connDict; +- (void)sendResponseOnNewThread:(NSMutableDictionary *)connDict; +- (void)sentResponse:(NSMutableDictionary *)connDict; +@end + +// keys for our connection dictionaries +static NSString *kFileHandle = @"FileHandle"; +static NSString *kRequest = @"Request"; +static NSString *kResponse = @"Response"; + +@interface GTMHTTPRequestMessage (PrivateHelpers) +- (BOOL)isHeaderComplete; +- (BOOL)appendData:(NSData *)data; +- (NSString *)headerFieldValueForKey:(NSString *)key; +- (UInt32)contentLength; +- (void)setBody:(NSData *)body; +@end + +@interface GTMHTTPResponseMessage () +- (id)initWithBody:(NSData *)body contentType:(NSString *)contentType statusCode:(int)statusCode; +- (NSData *)serializedData; +@end + +@implementation GTMHTTPServer + +- (id)init { + return [self initWithDelegate:nil]; +} + +- (id)initWithDelegate:(id)delegate { + self = [super init]; + if (self) { + if (!delegate) { + _GTMDevLog(@"missing delegate"); + [self release]; + return nil; + } + delegate_ = delegate; + +#ifndef NS_BLOCK_ASSERTIONS + BOOL isDelegateOK = [delegate_ respondsToSelector:@selector(httpServer:handleRequest:)]; + NSAssert(isDelegateOK, @"GTMHTTPServer delegate lacks handleRequest sel"); +#endif + + localhostOnly_ = YES; + connections_ = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)dealloc { + [self stop]; + [connections_ release]; + [super dealloc]; +} + +#if !TARGET_OS_IPHONE +- (void)finalize { + [self stop]; + [super finalize]; +} +#endif + +- (id)delegate { + return delegate_; +} + +- (uint16_t)port { + return port_; +} + +- (void)setPort:(uint16_t)port { + port_ = port; +} + +- (BOOL)reusePort { + return reusePort_; +} + +- (void)setReusePort:(BOOL)yesno { + reusePort_ = yesno; +} + +- (BOOL)localhostOnly { + return localhostOnly_; +} + +- (void)setLocalhostOnly:(BOOL)yesno { + localhostOnly_ = yesno; +} + +- (BOOL)start:(NSError **)error { + NSAssert(listenHandle_ == nil, @"start called when we already have a listenHandle_"); + + if (error) *error = NULL; + + NSInteger startFailureCode = 0; + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd <= 0) { + // COV_NF_START - we'd need to use up *all* sockets to test this? + startFailureCode = kGTMHTTPServerSocketCreateFailedError; + goto startFailed; + // COV_NF_END + } + + // enable address reuse quicker after we are done w/ our socket + int yes = 1; + int sock_opt = reusePort_ ? SO_REUSEPORT : SO_REUSEADDR; + if (setsockopt(fd, SOL_SOCKET, sock_opt, (void *)&yes, (socklen_t)sizeof(yes)) != 0) { + _GTMDevLog(@"failed to mark the socket as reusable"); // COV_NF_LINE + } + + // bind + struct sockaddr_in addr; + bzero(&addr, sizeof(addr)); + addr.sin_len = sizeof(addr); + addr.sin_family = AF_INET; + addr.sin_port = htons(port_); + if (localhostOnly_) { + addr.sin_addr.s_addr = htonl(0x7F000001); + } else { + // COV_NF_START - testing this could cause a leopard firewall prompt during tests. + addr.sin_addr.s_addr = htonl(INADDR_ANY); + // COV_NF_END + } + if (bind(fd, (struct sockaddr *)(&addr), (socklen_t)sizeof(addr)) != 0) { + startFailureCode = kGTMHTTPServerBindFailedError; + goto startFailed; + } + + // collect the port back out + if (port_ == 0) { + socklen_t len = (socklen_t)sizeof(addr); + if (getsockname(fd, (struct sockaddr *)(&addr), &len) == 0) { + port_ = ntohs(addr.sin_port); + } + } + + // tell it to listen for connections + if (listen(fd, 5) != 0) { + // COV_NF_START + startFailureCode = kGTMHTTPServerListenFailedError; + goto startFailed; + // COV_NF_END + } + + // now use a filehandle to accept connections + listenHandle_ = [[NSFileHandle alloc] initWithFileDescriptor:fd closeOnDealloc:YES]; + if (listenHandle_ == nil) { + // COV_NF_START - we'd need to run out of memory to test this? + startFailureCode = kGTMHTTPServerHandleCreateFailedError; + goto startFailed; + // COV_NF_END + } + + // setup notifications for connects + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center addObserver:self + selector:@selector(acceptedConnectionNotification:) + name:NSFileHandleConnectionAcceptedNotification + object:listenHandle_]; + [listenHandle_ acceptConnectionInBackgroundAndNotify]; + + // TODO: maybe hit the delegate incase it wants to register w/ NSNetService, + // or just know we're up and running? + + return YES; + +startFailed: + if (error) { + *error = [[[NSError alloc] initWithDomain:kGTMHTTPServerErrorDomain + code:startFailureCode + userInfo:nil] autorelease]; + } + if (fd > 0) { + close(fd); + } + return NO; +} + +- (void)stop { + if (listenHandle_) { + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center removeObserver:self + name:NSFileHandleConnectionAcceptedNotification + object:listenHandle_]; + [listenHandle_ release]; + listenHandle_ = nil; + // TODO: maybe hit the delegate in case it wants to unregister w/ + // NSNetService, or just know we've stopped running? + } + [connections_ removeAllObjects]; +} + +- (NSUInteger)activeRequestCount { + return [connections_ count]; +} + +- (NSString *)description { + NSString *result = + [NSString stringWithFormat:@"%@<%p>{ port=%d localHostOnly=%@ status=%@ }", [self class], + self, port_, (localhostOnly_ ? @"YES" : @"NO"), + (listenHandle_ != nil ? @"Started" : @"Stopped")]; + return result; +} + +@end + +@implementation GTMHTTPServer (PrivateMethods) + +- (void)acceptedConnectionNotification:(NSNotification *)notification { + NSDictionary *userInfo = [notification userInfo]; + NSFileHandle *newConnection = [userInfo objectForKey:NSFileHandleNotificationFileHandleItem]; + NSAssert1(newConnection != nil, @"failed to get the connection in the notification: %@", + notification); + + // make sure we accept more... + [listenHandle_ acceptConnectionInBackgroundAndNotify]; + + // TODO: could let the delegate look at the address, before we start working + // on it. + + NSMutableDictionary *connDict = [self connectionWithFileHandle:newConnection]; + [connections_ addObject:connDict]; +} + +- (NSMutableDictionary *)connectionWithFileHandle:(NSFileHandle *)fileHandle { + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + + [result setObject:fileHandle forKey:kFileHandle]; + + GTMHTTPRequestMessage *request = [[[GTMHTTPRequestMessage alloc] init] autorelease]; + [result setObject:request forKey:kRequest]; + + // setup for data notifications + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center addObserver:self + selector:@selector(dataAvailableNotification:) + name:NSFileHandleReadCompletionNotification + object:fileHandle]; + [fileHandle readInBackgroundAndNotify]; + + return result; +} + +- (void)dataAvailableNotification:(NSNotification *)notification { + NSFileHandle *connectionHandle = GTM_STATIC_CAST(NSFileHandle, [notification object]); + NSMutableDictionary *connDict = [self lookupConnection:connectionHandle]; + if (connDict == nil) return; // we are no longer tracking this one + + NSDictionary *userInfo = [notification userInfo]; + NSData *readData = [userInfo objectForKey:NSFileHandleNotificationDataItem]; + if ([readData length] == 0) { + // remote side closed + [self closeConnection:connDict]; + return; + } + + // Use a local pool to keep memory down incase the runloop we're in doesn't + // drain until it gets a UI event. + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + @try { + // Like Apple's sample, we just keep adding data until we get a full header + // and any referenced body. + + GTMHTTPRequestMessage *request = [connDict objectForKey:kRequest]; + [request appendData:readData]; + + // Is the header complete yet? + if (![request isHeaderComplete]) { + // more data... + [connectionHandle readInBackgroundAndNotify]; + } else { + // Do we have all the body? + UInt32 contentLength = [request contentLength]; + NSData *body = [request body]; + NSUInteger bodyLength = [body length]; + if (contentLength > bodyLength) { + // need more data... + [connectionHandle readInBackgroundAndNotify]; + } else { + if (contentLength < bodyLength) { + // We got extra (probably someone trying to pipeline on us), trim + // and let the extra data go... + NSData *newBody = [NSData dataWithBytes:[body bytes] length:contentLength]; + [request setBody:newBody]; + _GTMDevLog(@"Got %lu extra bytes on http request, ignoring them", + (unsigned long)(bodyLength - contentLength)); + } + + GTMHTTPResponseMessage *response = nil; + @try { + // Off to the delegate + response = [delegate_ httpServer:self handleRequest:request]; + } @catch (NSException *e) { + _GTMDevLog(@"Exception trying to handle http request: %@", e); + } // COV_NF_LINE - radar 5851992 only reachable w/ an uncaught exception which isn't + // testable + + if (response) { + // We don't support connection reuse, so we add (force) the header to + // close every connection. + [response setValue:@"close" forHeaderField:@"Connection"]; + + // spawn thread to send reply (since we do a blocking send) + [connDict setObject:response forKey:kResponse]; + [NSThread detachNewThreadSelector:@selector(sendResponseOnNewThread:) + toTarget:self + withObject:connDict]; + } else { + // No response, shut it down + [self closeConnection:connDict]; + } + } + } + } @catch (NSException *e) { // COV_NF_START + _GTMDevLog(@"exception while read data: %@", e); + // exception while dealing with the connection, close it + } // COV_NF_END + @finally { + [pool drain]; + } +} + +- (NSMutableDictionary *)lookupConnection:(NSFileHandle *)fileHandle { + NSMutableDictionary *result = nil; + for (NSMutableDictionary *connDict in connections_) { + if (fileHandle == [connDict objectForKey:kFileHandle]) { + result = connDict; + break; + } + } + return result; +} + +- (void)closeConnection:(NSMutableDictionary *)connDict { + // remove the notification + NSFileHandle *connectionHandle = [connDict objectForKey:kFileHandle]; + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center removeObserver:self name:NSFileHandleReadCompletionNotification object:connectionHandle]; + // in a non GC world, we're fine just letting the connect get closed when + // the object is release when it comes out of connections_, but in a GC world + // it won't get cleaned up + [connectionHandle closeFile]; + + // remove it from the list + [connections_ removeObject:connDict]; +} + +- (void)sendResponseOnNewThread:(NSMutableDictionary *)connDict { + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + @try { + GTMHTTPResponseMessage *response = [connDict objectForKey:kResponse]; + NSFileHandle *connectionHandle = [connDict objectForKey:kFileHandle]; + NSData *serialized = [response serializedData]; + [connectionHandle writeData:serialized]; + } @catch (NSException *e) { // COV_NF_START - causing an exception here is to hard in a test + // TODO: let the delegate know about the exception (but do it on the main + // thread) + _GTMDevLog(@"exception while sending reply: %@", e); + } // COV_NF_END + + // back to the main thread to close things down + [self performSelectorOnMainThread:@selector(sentResponse:) withObject:connDict waitUntilDone:NO]; + + [pool release]; +} + +- (void)sentResponse:(NSMutableDictionary *)connDict { + // make sure we're still tracking this connection (in case server was stopped) + NSFileHandle *connection = [connDict objectForKey:kFileHandle]; + NSMutableDictionary *connDict2 = [self lookupConnection:connection]; + if (connDict != connDict2) return; + + // TODO: message the delegate that it was sent + + // close it down + [self closeConnection:connDict]; +} + +@end + +#pragma mark - + +@implementation GTMHTTPRequestMessage + +- (id)init { + self = [super init]; + if (self) { + message_ = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, YES); + } + return self; +} + +- (void)dealloc { + if (message_) { + CFRelease(message_); + } + [super dealloc]; +} + +- (NSString *)version { + return GTMCFAutorelease(CFHTTPMessageCopyVersion(message_)); +} + +- (NSURL *)URL { + return GTMCFAutorelease(CFHTTPMessageCopyRequestURL(message_)); +} + +- (NSString *)method { + return GTMCFAutorelease(CFHTTPMessageCopyRequestMethod(message_)); +} + +- (NSData *)body { + return GTMCFAutorelease(CFHTTPMessageCopyBody(message_)); +} + +- (NSDictionary *)allHeaderFieldValues { + return GTMCFAutorelease(CFHTTPMessageCopyAllHeaderFields(message_)); +} + +- (NSString *)description { + CFStringRef desc = CFCopyDescription(message_); + NSString *result = [NSString stringWithFormat:@"%@<%p>{ message=%@ }", [self class], self, desc]; + CFRelease(desc); + return result; +} + +@end + +@implementation GTMHTTPRequestMessage (PrivateHelpers) + +- (BOOL)isHeaderComplete { + return CFHTTPMessageIsHeaderComplete(message_) ? YES : NO; +} + +- (BOOL)appendData:(NSData *)data { + return CFHTTPMessageAppendBytes(message_, [data bytes], (CFIndex)[data length]) ? YES : NO; +} + +- (NSString *)headerFieldValueForKey:(NSString *)key { + CFStringRef value = NULL; + if (key) { + value = CFHTTPMessageCopyHeaderFieldValue(message_, (CFStringRef)key); + } + return GTMCFAutorelease(value); +} + +- (UInt32)contentLength { + return (UInt32)[[self headerFieldValueForKey:@"Content-Length"] intValue]; +} + +- (void)setBody:(NSData *)body { + if (!body) { + body = [NSData data]; // COV_NF_LINE - can only happen in we fail to make the new data object + } + CFHTTPMessageSetBody(message_, (CFDataRef)body); +} + +@end + +#pragma mark - + +@implementation GTMHTTPResponseMessage + +- (id)init { + return [self initWithBody:nil contentType:nil statusCode:0]; +} + +- (id)initWithBody:(NSData *)body contentType:(NSString *)contentType statusCode:(int)statusCode { + self = [super init]; + if (self) { + if ((statusCode < 100) || (statusCode > 599)) { + [self release]; + return nil; + } + message_ = + CFHTTPMessageCreateResponse(kCFAllocatorDefault, statusCode, NULL, kCFHTTPVersion1_0); + if (!message_) { + // COV_NF_START + [self release]; + return nil; + // COV_NF_END + } + NSUInteger bodyLength = 0; + if (body) { + bodyLength = [body length]; + CFHTTPMessageSetBody(message_, (CFDataRef)body); + } + if ([contentType length] == 0) { + contentType = @"text/html"; + } + NSString *bodyLenStr = [NSString stringWithFormat:@"%lu", (unsigned long)bodyLength]; + [self setValue:bodyLenStr forHeaderField:@"Content-Length"]; + [self setValue:contentType forHeaderField:@"Content-Type"]; + } + return self; +} + +- (void)dealloc { + if (message_) { + CFRelease(message_); + } + [super dealloc]; +} + ++ (instancetype)responseWithString:(NSString *)plainText { + NSData *body = [plainText dataUsingEncoding:NSUTF8StringEncoding]; + return [self responseWithBody:body contentType:@"text/plain; charset=UTF-8" statusCode:200]; +} + ++ (instancetype)responseWithHTMLString:(NSString *)htmlString { + return [self responseWithBody:[htmlString dataUsingEncoding:NSUTF8StringEncoding] + contentType:@"text/html; charset=UTF-8" + statusCode:200]; +} + ++ (instancetype)responseWithBody:(NSData *)body + contentType:(NSString *)contentType + statusCode:(int)statusCode { + return [[[[self class] alloc] initWithBody:body contentType:contentType statusCode:statusCode] + autorelease]; +} + ++ (instancetype)emptyResponseWithCode:(int)statusCode { + return + [[[[self class] alloc] initWithBody:nil contentType:nil statusCode:statusCode] autorelease]; +} + +- (void)setValue:(NSString *)value forHeaderField:(NSString *)headerField { + if ([headerField length] == 0) return; + if (value == nil) { + value = @""; + } + CFHTTPMessageSetHeaderFieldValue(message_, (CFStringRef)headerField, (CFStringRef)value); +} + +- (void)setHeaderValuesFromDictionary:(NSDictionary *)dict { + for (id key in dict) { + id value = [dict valueForKey:key]; + [self setValue:value forHeaderField:key]; + } +} + +- (NSString *)description { + CFStringRef desc = CFCopyDescription(message_); + NSString *result = [NSString stringWithFormat:@"%@<%p>{ message=%@ }", [self class], self, desc]; + CFRelease(desc); + return result; +} + +- (NSData *)serializedData { + return GTMCFAutorelease(CFHTTPMessageCopySerializedMessage(message_)); +} + +@end diff --git a/Example/Firebase.xcodeproj/project.pbxproj b/Example/Firebase.xcodeproj/project.pbxproj index d306fe2..88a03d2 100644 --- a/Example/Firebase.xcodeproj/project.pbxproj +++ b/Example/Firebase.xcodeproj/project.pbxproj @@ -571,6 +571,18 @@ DEF6C33D1FBCE775005D0740 /* FIRVerifyPasswordRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE93151F1E86C6FF0083EDBF /* FIRVerifyPasswordRequestTest.m */; }; DEF6C33E1FBCE775005D0740 /* FIRVerifyPasswordResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315201E86C6FF0083EDBF /* FIRVerifyPasswordResponseTests.m */; }; DEF6C3411FBCE775005D0740 /* OCMStubRecorder+FIRAuthUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315241E86C6FF0083EDBF /* OCMStubRecorder+FIRAuthUnitTests.m */; }; + ED8C81002088EFA20093EB8A /* FIRMutableDictionaryTest.m in Sources */ = {isa = PBXBuildFile; fileRef = ED8C80FA2088EFA10093EB8A /* FIRMutableDictionaryTest.m */; }; + ED8C81012088EFA20093EB8A /* FIRMutableDictionaryTest.m in Sources */ = {isa = PBXBuildFile; fileRef = ED8C80FA2088EFA10093EB8A /* FIRMutableDictionaryTest.m */; }; + ED8C81022088EFA20093EB8A /* FIRMutableDictionaryTest.m in Sources */ = {isa = PBXBuildFile; fileRef = ED8C80FA2088EFA10093EB8A /* FIRMutableDictionaryTest.m */; }; + ED8C81032088EFA20093EB8A /* FIRNetworkTest.m in Sources */ = {isa = PBXBuildFile; fileRef = ED8C80FB2088EFA10093EB8A /* FIRNetworkTest.m */; }; + ED8C81042088EFA20093EB8A /* FIRNetworkTest.m in Sources */ = {isa = PBXBuildFile; fileRef = ED8C80FB2088EFA10093EB8A /* FIRNetworkTest.m */; }; + ED8C81052088EFA20093EB8A /* FIRNetworkTest.m in Sources */ = {isa = PBXBuildFile; fileRef = ED8C80FB2088EFA10093EB8A /* FIRNetworkTest.m */; }; + ED8C81062088EFA20093EB8A /* FIRReachabilityCheckerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = ED8C80FC2088EFA20093EB8A /* FIRReachabilityCheckerTest.m */; }; + ED8C81072088EFA20093EB8A /* FIRReachabilityCheckerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = ED8C80FC2088EFA20093EB8A /* FIRReachabilityCheckerTest.m */; }; + ED8C81082088EFA20093EB8A /* FIRReachabilityCheckerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = ED8C80FC2088EFA20093EB8A /* FIRReachabilityCheckerTest.m */; }; + ED8C81092088EFA20093EB8A /* GTMHTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = ED8C80FF2088EFA20093EB8A /* GTMHTTPServer.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + ED8C810A2088EFA20093EB8A /* GTMHTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = ED8C80FF2088EFA20093EB8A /* GTMHTTPServer.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + ED8C810B2088EFA20093EB8A /* GTMHTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = ED8C80FF2088EFA20093EB8A /* GTMHTTPServer.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1254,6 +1266,11 @@ DEE14D7D1E844677006FA992 /* Tests-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Tests-Info.plist"; sourceTree = "<group>"; }; DEF288401F9AB6E100D480CF /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = "<group>"; }; E2C2834C90DBAB56D568189F /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = "<group>"; }; + ED8C80FA2088EFA10093EB8A /* FIRMutableDictionaryTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRMutableDictionaryTest.m; sourceTree = "<group>"; }; + ED8C80FB2088EFA10093EB8A /* FIRNetworkTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRNetworkTest.m; sourceTree = "<group>"; }; + ED8C80FC2088EFA20093EB8A /* FIRReachabilityCheckerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRReachabilityCheckerTest.m; sourceTree = "<group>"; }; + ED8C80FE2088EFA20093EB8A /* GTMHTTPServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMHTTPServer.h; sourceTree = "<group>"; }; + ED8C80FF2088EFA20093EB8A /* GTMHTTPServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMHTTPServer.m; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -2277,6 +2294,10 @@ DEE14D741E844677006FA992 /* Tests */ = { isa = PBXGroup; children = ( + ED8C80FA2088EFA10093EB8A /* FIRMutableDictionaryTest.m */, + ED8C80FB2088EFA10093EB8A /* FIRNetworkTest.m */, + ED8C80FC2088EFA20093EB8A /* FIRReachabilityCheckerTest.m */, + ED8C80FD2088EFA20093EB8A /* third_party */, DE4B26DE20855F1F0030A38C /* FIRAppEnvironmentUtilTest.m */, DEE14D7B1E844677006FA992 /* FIRTestCase.h */, DEE14D751E844677006FA992 /* FIRAppAssociationRegistrationUnitTests.m */, @@ -2291,6 +2312,15 @@ path = Tests; sourceTree = "<group>"; }; + ED8C80FD2088EFA20093EB8A /* third_party */ = { + isa = PBXGroup; + children = ( + ED8C80FE2088EFA20093EB8A /* GTMHTTPServer.h */, + ED8C80FF2088EFA20093EB8A /* GTMHTTPServer.m */, + ); + path = third_party; + sourceTree = "<group>"; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -3644,12 +3674,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + ED8C81072088EFA20093EB8A /* FIRReachabilityCheckerTest.m in Sources */, + ED8C81042088EFA20093EB8A /* FIRNetworkTest.m in Sources */, D064E6AF1ED9B31C001956DF /* FIRAppAssociationRegistrationUnitTests.m in Sources */, + ED8C81012088EFA20093EB8A /* FIRMutableDictionaryTest.m in Sources */, D064E6B01ED9B31C001956DF /* FIRAppTest.m in Sources */, D064E6B11ED9B31C001956DF /* FIRConfigurationTest.m in Sources */, DE4B26E120855F500030A38C /* FIRAppEnvironmentUtilTest.m in Sources */, D064E6B21ED9B31C001956DF /* FIRLoggerTest.m in Sources */, D064E6B31ED9B31C001956DF /* FIROptionsTest.m in Sources */, + ED8C810A2088EFA20093EB8A /* GTMHTTPServer.m in Sources */, D064E6B41ED9B31C001956DF /* FIRBundleUtilTest.m in Sources */, D064E6B51ED9B31C001956DF /* FIRTestCase.m in Sources */, ); @@ -4099,12 +4133,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + ED8C81082088EFA20093EB8A /* FIRReachabilityCheckerTest.m in Sources */, + ED8C81052088EFA20093EB8A /* FIRNetworkTest.m in Sources */, DEAAD3DA1FBA34250053BF48 /* FIROptionsTest.m in Sources */, + ED8C81022088EFA20093EB8A /* FIRMutableDictionaryTest.m in Sources */, DEAAD3D51FBA34250053BF48 /* FIRAppAssociationRegistrationUnitTests.m in Sources */, DEAAD3D91FBA34250053BF48 /* FIRLoggerTest.m in Sources */, DE4B26E220855F520030A38C /* FIRAppEnvironmentUtilTest.m in Sources */, DEAAD3D61FBA34250053BF48 /* FIRAppTest.m in Sources */, DEAAD3D81FBA34250053BF48 /* FIRConfigurationTest.m in Sources */, + ED8C810B2088EFA20093EB8A /* GTMHTTPServer.m in Sources */, DEAAD3DB1FBA34250053BF48 /* FIRTestCase.m in Sources */, DEAAD3D71FBA34250053BF48 /* FIRBundleUtilTest.m in Sources */, ); @@ -4187,12 +4225,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + ED8C81062088EFA20093EB8A /* FIRReachabilityCheckerTest.m in Sources */, + ED8C81032088EFA20093EB8A /* FIRNetworkTest.m in Sources */, DEE14D8E1E84468D006FA992 /* FIRAppAssociationRegistrationUnitTests.m in Sources */, + ED8C81002088EFA20093EB8A /* FIRMutableDictionaryTest.m in Sources */, DEE14D8F1E84468D006FA992 /* FIRAppTest.m in Sources */, DEE14D911E84468D006FA992 /* FIRConfigurationTest.m in Sources */, DE4B26E020855F4C0030A38C /* FIRAppEnvironmentUtilTest.m in Sources */, DEE14D921E84468D006FA992 /* FIRLoggerTest.m in Sources */, DEE14D931E84468D006FA992 /* FIROptionsTest.m in Sources */, + ED8C81092088EFA20093EB8A /* GTMHTTPServer.m in Sources */, DEE14D901E84468D006FA992 /* FIRBundleUtilTest.m in Sources */, DEE14D941E84468D006FA992 /* FIRTestCase.m in Sources */, ); |