From 7e70a6dd0bb208fcd17c2a43dd9b840bd0401abf Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Fri, 16 Oct 2020 16:11:35 -0400 Subject: [PATCH] GoogleUtilities: NSURLSession promise extension (#6753) * GoogleUtilities: NSURLSession promise extension * Imports fix * Changelog * API and API docs * style * Changelog fix --- GoogleUtilities.podspec | 5 +- GoogleUtilities/CHANGELOG.md | 5 +- .../GULURLSessionDataResponse.h | 31 ++++++ .../GULURLSessionDataResponse.m | 30 ++++++ .../NSURLSession+GULPromises.h | 37 +++++++ .../NSURLSession+GULPromises.m | 46 ++++++++ .../NSURLSession+GULPromisesTests.m | 100 ++++++++++++++++++ .../URLSession/FIRURLSessionOCMockStub.h | 35 ++++++ .../URLSession/FIRURLSessionOCMockStub.m | 65 ++++++++++++ 9 files changed, 352 insertions(+), 2 deletions(-) create mode 100644 GoogleUtilities/Environment/URLSessionPromiseWrapper/GULURLSessionDataResponse.h create mode 100644 GoogleUtilities/Environment/URLSessionPromiseWrapper/GULURLSessionDataResponse.m create mode 100644 GoogleUtilities/Environment/URLSessionPromiseWrapper/NSURLSession+GULPromises.h create mode 100644 GoogleUtilities/Environment/URLSessionPromiseWrapper/NSURLSession+GULPromises.m create mode 100644 GoogleUtilities/Tests/Unit/Environment/NSURLSession+GULPromisesTests.m create mode 100644 SharedTestUtilities/URLSession/FIRURLSessionOCMockStub.h create mode 100644 SharedTestUtilities/URLSession/FIRURLSessionOCMockStub.m diff --git a/GoogleUtilities.podspec b/GoogleUtilities.podspec index db3652c3668..de8f44d70f4 100644 --- a/GoogleUtilities.podspec +++ b/GoogleUtilities.podspec @@ -118,7 +118,10 @@ other Google CocoaPods. They're not intended for direct public usage. s.test_spec 'unit' do |unit_tests| # All tests require arc except Tests/Network/third_party/GTMHTTPServer.m unit_tests.platforms = {:ios => '8.0', :osx => '10.11', :tvos => '10.0'} - unit_tests.source_files = 'GoogleUtilities/Tests/Unit/**/*.[mh]' + unit_tests.source_files = [ + 'GoogleUtilities/Tests/Unit/**/*.[mh]', + 'SharedTestUtilities/URLSession/*.[mh]', + ] unit_tests.requires_arc = 'GoogleUtilities/Tests/Unit/*/*.[mh]' unit_tests.requires_app_host = true unit_tests.dependency 'OCMock' diff --git a/GoogleUtilities/CHANGELOG.md b/GoogleUtilities/CHANGELOG.md index 880a33c3b56..48c0ab17ed7 100644 --- a/GoogleUtilities/CHANGELOG.md +++ b/GoogleUtilities/CHANGELOG.md @@ -1,6 +1,9 @@ +# 7.1.0 -- Unreleased +- Added `NSURLSession` promise extension. (#6753) + # 7.0.0 - All APIs are now public. All CocoaPods private headers are transitioned to public. Note that -- GoogleUtilities may have frequent breaking changes than Firebase. (#6588) + GoogleUtilities may have more frequent breaking changes than Firebase. (#6588) - Fixed writing heartbeat to disk on tvOS devices. (#6658) - Refactor `GULSwizzledObject` to ARC to unblock SwiftPM support. (#5862) diff --git a/GoogleUtilities/Environment/URLSessionPromiseWrapper/GULURLSessionDataResponse.h b/GoogleUtilities/Environment/URLSessionPromiseWrapper/GULURLSessionDataResponse.h new file mode 100644 index 00000000000..e88eb67ba3e --- /dev/null +++ b/GoogleUtilities/Environment/URLSessionPromiseWrapper/GULURLSessionDataResponse.h @@ -0,0 +1,31 @@ +/* + * Copyright 2020 Google LLC + * + * 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 + +NS_ASSUME_NONNULL_BEGIN + +/** The class represents HTTP response received from `NSURLSession`. */ +@interface GULURLSessionDataResponse : NSObject + +@property(nonatomic, readonly) NSHTTPURLResponse *HTTPResponse; +@property(nonatomic, nullable, readonly) NSData *HTTPBody; + +- (instancetype)initWithResponse:(NSHTTPURLResponse *)response HTTPBody:(nullable NSData *)body; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleUtilities/Environment/URLSessionPromiseWrapper/GULURLSessionDataResponse.m b/GoogleUtilities/Environment/URLSessionPromiseWrapper/GULURLSessionDataResponse.m new file mode 100644 index 00000000000..b362562cc08 --- /dev/null +++ b/GoogleUtilities/Environment/URLSessionPromiseWrapper/GULURLSessionDataResponse.m @@ -0,0 +1,30 @@ +/* + * Copyright 2020 Google LLC + * + * 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 "GoogleUtilities/Environment/URLSessionPromiseWrapper/GULURLSessionDataResponse.h" + +@implementation GULURLSessionDataResponse + +- (instancetype)initWithResponse:(NSHTTPURLResponse *)response HTTPBody:(NSData *)body { + self = [super init]; + if (self) { + _HTTPResponse = response; + _HTTPBody = body; + } + return self; +} + +@end diff --git a/GoogleUtilities/Environment/URLSessionPromiseWrapper/NSURLSession+GULPromises.h b/GoogleUtilities/Environment/URLSessionPromiseWrapper/NSURLSession+GULPromises.h new file mode 100644 index 00000000000..7bed005ea35 --- /dev/null +++ b/GoogleUtilities/Environment/URLSessionPromiseWrapper/NSURLSession+GULPromises.h @@ -0,0 +1,37 @@ +/* + * Copyright 2020 Google LLC + * + * 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 + +@class FBLPromise; +@class GULURLSessionDataResponse; + +NS_ASSUME_NONNULL_BEGIN + +/** Promise based API for `NSURLSession`. */ +@interface NSURLSession (GULPromises) + +/** Creates a promise wrapping `-[NSURLSession dataTaskWithRequest:completionHandler:]` method. + * @param URLRequest The request to create a data task with. + * @return A promise that is fulfilled when an HTTP response is received (with any response code), + * or is rejected with the error passed to the task completion. + */ +- (FBLPromise *)gul_dataTaskPromiseWithRequest: + (NSURLRequest *)URLRequest; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleUtilities/Environment/URLSessionPromiseWrapper/NSURLSession+GULPromises.m b/GoogleUtilities/Environment/URLSessionPromiseWrapper/NSURLSession+GULPromises.m new file mode 100644 index 00000000000..88e43bf26a7 --- /dev/null +++ b/GoogleUtilities/Environment/URLSessionPromiseWrapper/NSURLSession+GULPromises.m @@ -0,0 +1,46 @@ +/* + * Copyright 2020 Google LLC + * + * 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 "GoogleUtilities/Environment/URLSessionPromiseWrapper/NSURLSession+GULPromises.h" + +#if __has_include() +#import +#else +#import "FBLPromises.h" +#endif + +#import "GoogleUtilities/Environment/URLSessionPromiseWrapper/GULURLSessionDataResponse.h" + +@implementation NSURLSession (GULPromises) + +- (FBLPromise *)gul_dataTaskPromiseWithRequest: + (NSURLRequest *)URLRequest { + return [FBLPromise async:^(FBLPromiseFulfillBlock fulfill, FBLPromiseRejectBlock reject) { + [[self dataTaskWithRequest:URLRequest + completionHandler:^(NSData *_Nullable data, NSURLResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + reject(error); + } else { + fulfill([[GULURLSessionDataResponse alloc] + initWithResponse:(NSHTTPURLResponse *)response + HTTPBody:data]); + } + }] resume]; + }]; +} + +@end diff --git a/GoogleUtilities/Tests/Unit/Environment/NSURLSession+GULPromisesTests.m b/GoogleUtilities/Tests/Unit/Environment/NSURLSession+GULPromisesTests.m new file mode 100644 index 00000000000..86f670d8c8f --- /dev/null +++ b/GoogleUtilities/Tests/Unit/Environment/NSURLSession+GULPromisesTests.m @@ -0,0 +1,100 @@ +/* + * Copyright 2020 Google LLC + * + * 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 + +#import "FBLPromise+Testing.h" +#import "OCMock.h" +#import "SharedTestUtilities/URLSession/FIRURLSessionOCMockStub.h" + +#import "GoogleUtilities/Environment/URLSessionPromiseWrapper/GULURLSessionDataResponse.h" +#import "GoogleUtilities/Environment/URLSessionPromiseWrapper/NSURLSession+GULPromises.h" + +@interface NSURLSession_GULPromisesTests : XCTestCase +@property(nonatomic) NSURLSession *URLSession; +@property(nonatomic) id URLSessionMock; +@end + +@implementation NSURLSession_GULPromisesTests + +- (void)setUp { + self.URLSession = [NSURLSession + sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration]]; + self.URLSessionMock = OCMPartialMock(self.URLSession); +} + +- (void)tearDown { + [self.URLSessionMock stopMocking]; + self.URLSessionMock = nil; + self.URLSession = nil; +} + +- (void)testDataTaskPromiseWithRequestSuccess { + NSURL *url = [NSURL URLWithString:@"https://localhost"]; + NSURLRequest *request = [NSURLRequest requestWithURL:url]; + + NSHTTPURLResponse *expectedResponse = [[NSHTTPURLResponse alloc] initWithURL:url + statusCode:200 + HTTPVersion:@"1.1" + headerFields:nil]; + NSData *expectedBody = [@"body" dataUsingEncoding:NSUTF8StringEncoding]; + + [FIRURLSessionOCMockStub + stubURLSessionDataTaskWithResponse:expectedResponse + body:expectedBody + error:nil + URLSessionMock:self.URLSessionMock + requestValidationBlock:^BOOL(NSURLRequest *_Nonnull sentRequest) { + return [sentRequest isEqual:request]; + }]; + + __auto_type taskPromise = [self.URLSessionMock gul_dataTaskPromiseWithRequest:request]; + + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); + + XCTAssertTrue(taskPromise.isFulfilled); + XCTAssertNil(taskPromise.error); + XCTAssertEqualObjects(taskPromise.value.HTTPResponse, expectedResponse); + XCTAssertEqualObjects(taskPromise.value.HTTPBody, expectedBody); +} + +- (void)testDataTaskPromiseWithRequestError { + NSURL *url = [NSURL URLWithString:@"https://localhost"]; + NSURLRequest *request = [NSURLRequest requestWithURL:url]; + + NSError *expectedError = [NSError errorWithDomain:@"testDataTaskPromiseWithRequestError" + code:-1 + userInfo:nil]; + + [FIRURLSessionOCMockStub + stubURLSessionDataTaskWithResponse:nil + body:nil + error:expectedError + URLSessionMock:self.URLSessionMock + requestValidationBlock:^BOOL(NSURLRequest *_Nonnull sentRequest) { + return [sentRequest isEqual:request]; + }]; + + __auto_type taskPromise = [self.URLSessionMock gul_dataTaskPromiseWithRequest:request]; + + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); + + XCTAssertTrue(taskPromise.isRejected); + XCTAssertEqualObjects(taskPromise.error, expectedError); + XCTAssertNil(taskPromise.value); +} + +@end diff --git a/SharedTestUtilities/URLSession/FIRURLSessionOCMockStub.h b/SharedTestUtilities/URLSession/FIRURLSessionOCMockStub.h new file mode 100644 index 00000000000..2895b03c71b --- /dev/null +++ b/SharedTestUtilities/URLSession/FIRURLSessionOCMockStub.h @@ -0,0 +1,35 @@ +/* + * Copyright 2020 Google LLC + * + * 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 + +NS_ASSUME_NONNULL_BEGIN + +typedef BOOL (^FIRRequestValidationBlock)(NSURLRequest *request); + +@interface FIRURLSessionOCMockStub : NSObject + ++ (id)stubURLSessionDataTaskWithResponse:(nullable NSHTTPURLResponse *)response + body:(nullable NSData *)body + error:(nullable NSError *)error + URLSessionMock:(id)URLSessionMock + requestValidationBlock:(nullable FIRRequestValidationBlock)requestValidationBlock; + ++ (NSHTTPURLResponse *)HTTPResponseWithCode:(NSInteger)statusCode; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SharedTestUtilities/URLSession/FIRURLSessionOCMockStub.m b/SharedTestUtilities/URLSession/FIRURLSessionOCMockStub.m new file mode 100644 index 00000000000..2d0c9d8f31c --- /dev/null +++ b/SharedTestUtilities/URLSession/FIRURLSessionOCMockStub.m @@ -0,0 +1,65 @@ +/* + * Copyright 2020 Google LLC + * + * 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 "SharedTestUtilities/URLSession/FIRURLSessionOCMockStub.h" + +#import "OCMock.h" + +@implementation FIRURLSessionOCMockStub + ++ (id)stubURLSessionDataTaskWithResponse:(NSHTTPURLResponse *)response + body:(NSData *)body + error:(NSError *)error + URLSessionMock:(id)URLSessionMock + requestValidationBlock:(FIRRequestValidationBlock)requestValidationBlock { + id mockDataTask = OCMStrictClassMock([NSURLSessionDataTask class]); + + // Validate request content. + FIRRequestValidationBlock nonOptionalRequestValidationBlock = + requestValidationBlock ?: ^BOOL(id request) { + return YES; + }; + + id URLRequestValidationArg = [OCMArg checkWithBlock:nonOptionalRequestValidationBlock]; + + // Save task completion to be called on the `[NSURLSessionDataTask resume]` + __block void (^taskCompletion)(NSData *, NSURLResponse *, NSError *); + id completionArg = [OCMArg checkWithBlock:^BOOL(id obj) { + taskCompletion = obj; + return YES; + }]; + + // Expect `dataTaskWithRequest` to be called. + OCMExpect([URLSessionMock dataTaskWithRequest:URLRequestValidationArg + completionHandler:completionArg]) + .andReturn(mockDataTask); + + // Expect the task to be resumed and call the task completion. + OCMExpect([(NSURLSessionDataTask *)mockDataTask resume]).andDo(^(NSInvocation *invocation) { + taskCompletion(body, response, error); + }); + + return mockDataTask; +} + ++ (NSHTTPURLResponse *)HTTPResponseWithCode:(NSInteger)statusCode { + return [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://localhost"] + statusCode:statusCode + HTTPVersion:@"HTTP/1.1" + headerFields:nil]; +} + +@end