From f4777d8b60ea93c9e5969e981b229d7a8c739749 Mon Sep 17 00:00:00 2001 From: Ben Hamilton Date: Mon, 22 Jan 2024 14:56:18 -0700 Subject: [PATCH] Asynchronously calculate User-Agent --- .../DriveSample/DriveSampleWindowController.m | 22 ++- .../StorageSampleWindowController.m | 66 +++---- Sources/Core/GTLRService.m | 185 +++++++++++++----- .../GoogleAPIClientForREST/GTLRService.h | 27 ++- USING.md | 33 ++-- UnitTests/GTLRServiceTest.m | 167 +++++++++++----- 6 files changed, 338 insertions(+), 162 deletions(-) diff --git a/Examples/DriveSample/DriveSampleWindowController.m b/Examples/DriveSample/DriveSampleWindowController.m index f47700f76..2376864ab 100644 --- a/Examples/DriveSample/DriveSampleWindowController.m +++ b/Examples/DriveSample/DriveSampleWindowController.m @@ -277,19 +277,21 @@ - (void)downloadFile:(GTLRDrive_File *)file // Here's how to download with a GTMSessionFetcher. The fetcher will use the authorizer that's // attached to the GTLR service's fetcherService. // - // NSURLRequest *downloadRequest = [service requestForQuery:query]; - // GTMSessionFetcher *fetcher = [service.fetcherService fetcherWithRequest:downloadRequest]; + // [service requestForQuery:query + // completion:^(NSURLRequest *downloadRequest) { + // GTMSessionFetcher *fetcher = [service.fetcherService fetcherWithRequest:downloadRequest]; // - // [fetcher setCommentWithFormat:@"Downloading %@", file.name]; - // fetcher.destinationFileURL = destinationURL; + // [fetcher setCommentWithFormat:@"Downloading %@", file.name]; + // fetcher.destinationFileURL = destinationURL; // - // [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { - // if (error == nil) { - // NSLog(@"Download succeeded."); + // [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { + // if (error == nil) { + // NSLog(@"Download succeeded."); // - // // With a destinationFileURL property set, the fetcher's callback - // // data parameter here will be nil. - // } + // // With a destinationFileURL property set, the fetcher's callback + // // data parameter here will be nil. + // } + // }]; // }]; [service executeQuery:query diff --git a/Examples/StorageSample/StorageSampleWindowController.m b/Examples/StorageSample/StorageSampleWindowController.m index d637df704..38a2b29bd 100644 --- a/Examples/StorageSample/StorageSampleWindowController.m +++ b/Examples/StorageSample/StorageSampleWindowController.m @@ -193,39 +193,39 @@ - (IBAction)downloadFileClicked:(id)sender { // Having the service execute this query would download the data to a GTLRDataObject. // But for downloads that might be large, we'll use a fetcher, since that offers // better control and monitoring of downloading. - NSURLRequest *request = [storageService requestForQuery:query]; - - // The Storage service's fetcherService will create a fetcher with an appropriate - // authorizer. - GTMSessionFetcher *fetcher = [storageService.fetcherService fetcherWithRequest:request]; - - // The fetcher can save data directly to a file. - fetcher.destinationFileURL = destinationURL; - - // Fetcher logging can include comments. - [fetcher setCommentWithFormat:@"Downloading \"%@/%@\"", - storageObject.bucket, storageObject.name]; - - fetcher.downloadProgressBlock = ^(int64_t bytesWritten, - int64_t totalBytesWritten, - int64_t totalBytesExpectedToWrite) { - // The fetcher will call the download progress block periodically. - }; - - [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { - // Callback - if (error == nil) { - // Successfully saved the file. - // - // Since a downloadPath property was specified, the data argument is - // nil, and the file data has been written to disk. - [self displayAlert:@"Downloaded" - format:@"%@", destinationURL.path]; - } else { - [self displayAlert:@"Error Downloading File" - format:@"%@", error]; - } - }]; + [storageService + requestForQuery:query + completion:^(NSURLRequest *request) { + // The Storage service's fetcherService will create a fetcher with an appropriate + // authorizer. + GTMSessionFetcher *fetcher = + [storageService.fetcherService fetcherWithRequest:request]; + + // The fetcher can save data directly to a file. + fetcher.destinationFileURL = destinationURL; + + // Fetcher logging can include comments. + [fetcher setCommentWithFormat:@"Downloading \"%@/%@\"", storageObject.bucket, + storageObject.name]; + + fetcher.downloadProgressBlock = ^(int64_t bytesWritten, int64_t totalBytesWritten, + int64_t totalBytesExpectedToWrite) { + // The fetcher will call the download progress block periodically. + }; + + [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { + // Callback + if (error == nil) { + // Successfully saved the file. + // + // Since a downloadPath property was specified, the data argument is + // nil, and the file data has been written to disk. + [self displayAlert:@"Downloaded" format:@"%@", destinationURL.path]; + } else { + [self displayAlert:@"Error Downloading File" format:@"%@", error]; + } + }]; + }]; } // result == NSFileHandlingPanelOKButton }]; // beginSheetModalForWindow: } diff --git a/Sources/Core/GTLRService.m b/Sources/Core/GTLRService.m index e55074d2e..4b13d1bc7 100644 --- a/Sources/Core/GTLRService.m +++ b/Sources/Core/GTLRService.m @@ -219,6 +219,7 @@ @implementation GTLRService { NSString *_overrideUserAgent; NSDictionary *_serviceProperties; // Properties retained for the convenience of the client app. NSUInteger _uploadChunkSize; // Only applies to resumable chunked uploads. + dispatch_queue_t _requestCreationQueue; } @synthesize additionalHTTPHeaders = _additionalHTTPHeaders, @@ -250,6 +251,9 @@ - (instancetype)init { if (self) { _parseQueue = dispatch_queue_create("com.google.GTLRServiceParse", DISPATCH_QUEUE_SERIAL); _callbackQueue = dispatch_get_main_queue(); + _requestCreationQueue = + dispatch_queue_create("com.google.GTLRServiceRequestCreation", DISPATCH_QUEUE_SERIAL); + _fetcherService = [[GTMSessionFetcherService alloc] init]; // Make the session fetcher use a background delegate queue instead of bouncing @@ -319,10 +323,26 @@ - (void)setMainBundleIDRestrictionWithAPIKey:(NSString *)apiKey { self.APIKeyRestrictionBundleID = [[NSBundle mainBundle] bundleIdentifier]; } -- (NSMutableURLRequest *)requestForURL:(NSURL *)url - ETag:(NSString *)etag - httpMethod:(NSString *)httpMethod - ticket:(GTLRServiceTicket *)ticket { +- (void)requestForURL:(NSURL *)url + ETag:(NSString *)etag + httpMethod:(NSString *)httpMethod + ticket:(GTLRServiceTicket *)ticket + completion:(void (^)(NSMutableURLRequest *))completion { + dispatch_async(_requestCreationQueue, ^{ + NSMutableURLRequest *request = [self createRequestForURL:url + ETag:etag + httpMethod:httpMethod + ticket:ticket]; + completion(request); + }); +} + +- (NSMutableURLRequest *)createRequestForURL:(NSURL *)url + ETag:(NSString *)etag + httpMethod:(NSString *)httpMethod + ticket:(GTLRServiceTicket *)ticket { + // This method may block, so make sure it's not on the caller's queue when executing a query. + dispatch_assert_queue_debug(_requestCreationQueue); // subclasses may add headers to this NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url @@ -371,19 +391,20 @@ - (NSMutableURLRequest *)requestForURL:(NSURL *)url return request; } -// objectRequestForURL returns an NSMutableURLRequest for a GTLRObject +// objectRequestForURL asynchronously returns an NSMutableURLRequest for a GTLRObject // // the object is the object being sent to the server, or nil; // the http method may be nil for get, or POST, PUT, DELETE -- (NSMutableURLRequest *)objectRequestForURL:(NSURL *)url - object:(GTLRObject *)object - contentType:(NSString *)contentType - contentLength:(NSString *)contentLength - ETag:(NSString *)etag - httpMethod:(NSString *)httpMethod - additionalHeaders:(NSDictionary *)additionalHeaders - ticket:(GTLRServiceTicket *)ticket { +- (void)objectRequestForURL:(NSURL *)url + object:(GTLRObject *)object + contentType:(NSString *)contentType + contentLength:(NSString *)contentLength + ETag:(NSString *)etag + httpMethod:(NSString *)httpMethod + additionalHeaders:(NSDictionary *)additionalHeaders + ticket:(GTLRServiceTicket *)ticket + completion:(void (^)(NSMutableURLRequest *))completion { if (object) { // if the object being sent has an etag, add it to the request header to // avoid retrieving a duplicate or to avoid writing over an updated @@ -396,10 +417,23 @@ - (NSMutableURLRequest *)objectRequestForURL:(NSURL *)url } } - NSMutableURLRequest *request = [self requestForURL:url - ETag:etag - httpMethod:httpMethod - ticket:ticket]; + [self requestForURL:url + ETag:etag + httpMethod:httpMethod + ticket:ticket + completion:^(NSMutableURLRequest *request) { + [self handleRequestCompletion:request + contentType:contentType + contentLength:contentLength + additionalHeaders:additionalHeaders]; + completion(request); + }]; +} + +- (void)handleRequestCompletion:(NSMutableURLRequest *)request + contentType:(NSString *)contentType + contentLength:(NSString *)contentLength + additionalHeaders:(NSDictionary *)additionalHeaders { [request setValue:@"application/json" forHTTPHeaderField:@"Accept"]; [request setValue:contentType forHTTPHeaderField:@"Content-Type"]; @@ -421,17 +455,43 @@ - (NSMutableURLRequest *)objectRequestForURL:(NSURL *)url NSString *value = [headers objectForKey:key]; [request setValue:value forHTTPHeaderField:key]; } - - return request; } #pragma mark - - (NSMutableURLRequest *)requestForQuery:(GTLRQuery *)query { - GTLR_DEBUG_ASSERT(query.bodyObject == nil, - @"requestForQuery: supports only GET methods, but was passed: %@", query); + // This public API will block the calling thread waiting for a completion to be dispatched onto + // this queue, so it cannot be called on this queue. + // + // Thankfully, this queue is an internal implementation detail, so this just guards against + // someone in the future accidentally calling this method on the request creation queue. + dispatch_assert_queue_not_debug(_requestCreationQueue); + + dispatch_semaphore_t requestCompleteSemaphore = dispatch_semaphore_create(0); + + __block NSMutableURLRequest *result; + [self requestForQuery:query + completionQueue:_requestCreationQueue + completion:^(NSMutableURLRequest *request) { + result = request; + dispatch_semaphore_signal(requestCompleteSemaphore); + }]; + + dispatch_semaphore_wait(requestCompleteSemaphore, DISPATCH_TIME_FOREVER); + return result; +} + +- (void)requestForQuery:(GTLRQuery *)query completion:(void (^)(NSMutableURLRequest *))completion { + [self requestForQuery:query completionQueue:self.callbackQueue completion:completion]; +} + +- (void)requestForQuery:(GTLRQuery *)query + completionQueue:(dispatch_queue_t)completionQueue + completion:(void (^)(NSMutableURLRequest *))completion { + GTLR_DEBUG_ASSERT(query.bodyObject == nil, @"%s supports only GET methods, but was passed: %@", + __func__, query); GTLR_DEBUG_ASSERT(query.uploadParameters == nil, - @"requestForQuery: does not support uploads, but was passed: %@", query); + @"%s does not support uploads, but was passed: %@", __func__, query); NSURL *url = [self URLFromQueryObject:query usePartialPaths:NO @@ -446,10 +506,19 @@ - (NSMutableURLRequest *)requestForQuery:(GTLRQuery *)query { queryParameters:queryParameters]; } - NSMutableURLRequest *request = [self requestForURL:url - ETag:nil - httpMethod:query.httpMethod - ticket:nil]; + [self requestForURL:url + ETag:nil + httpMethod:query.httpMethod + ticket:nil + completion:^(NSMutableURLRequest *request) { + [self handleRequestCompletion:request forQuery:query]; + dispatch_async(completionQueue, ^{ + completion(request); + }); + }]; +} + +- (void)handleRequestCompletion:(NSMutableURLRequest *)request forQuery:(GTLRQuery *)query { NSString *apiRestriction = self.APIKeyRestrictionBundleID; if ([apiRestriction length] > 0) { [request setValue:apiRestriction forHTTPHeaderField:kXIosBundleIdHeader]; @@ -466,8 +535,6 @@ - (NSMutableURLRequest *)requestForQuery:(GTLRQuery *)query { NSString *value = [headers objectForKey:key]; [request setValue:value forHTTPHeaderField:key]; } - - return request; } // common fetch starting method @@ -574,14 +641,6 @@ - (GTLRServiceTicket *)fetchObjectWithURL:(NSURL *)targetURL } } - NSURLRequest *request = [self objectRequestForURL:targetURL - object:bodyObject - contentType:contentType - contentLength:contentLength - ETag:etag - httpMethod:httpMethod - additionalHeaders:additionalHeaders - ticket:ticket]; ticket.postedObject = bodyObject; ticket.executingQuery = executingQuery; @@ -591,10 +650,39 @@ - (GTLRServiceTicket *)fetchObjectWithURL:(NSURL *)targetURL ticket.originalQuery = originalQuery; } + [self objectRequestForURL:targetURL + object:bodyObject + contentType:contentType + contentLength:contentLength + ETag:etag + httpMethod:httpMethod + additionalHeaders:additionalHeaders + ticket:ticket + completion:^(NSMutableURLRequest *request) { + [self handleObjectRequestCompletionWithRequest:request + objectClass:objectClass + dataToPost:dataToPost + mayAuthorize:mayAuthorize + completionHandler:completionHandler + executingQuery:executingQuery + ticket:ticket]; + }]; + + return ticket; +} + +- (void)handleObjectRequestCompletionWithRequest:(NSMutableURLRequest *)request + objectClass:(Class)objectClass + dataToPost:(NSData *)dataToPost + mayAuthorize:(BOOL)mayAuthorize + completionHandler:(GTLRServiceCompletionHandler)completionHandler + executingQuery:(id)executingQuery + ticket:(GTLRServiceTicket *)ticket { // Some proxy servers (and some web servers) have issues with GET URLs being // too long, trap that and move the query parameters into the body. The // uploadParams and dataToPost should be nil for a GET, but playing it safe // and confirming. + GTLRUploadParameters *uploadParams = executingQuery.uploadParameters; NSString *requestHTTPMethod = request.HTTPMethod; BOOL isDoingHTTPGet = (requestHTTPMethod == nil @@ -630,7 +718,7 @@ - (GTLRServiceTicket *)fetchObjectWithURL:(NSURL *)targetURL testBlock:testBlock dataToPost:dataToPost completionHandler:completionHandler]; - return ticket; + return; } GTMSessionFetcherService *fetcherService = ticket.fetcherService; @@ -814,24 +902,13 @@ - (GTLRServiceTicket *)fetchObjectWithURL:(NSURL *)targetURL } [self handleParsedObjectForFetcher:fetcher - executingQuery:executingQuery - ticket:ticket - error:error - parsedObject:nil - hasSentParsingStartNotification:NO - completionHandler:completionHandler]; + executingQuery:executingQuery + ticket:ticket + error:error + parsedObject:nil + hasSentParsingStartNotification:NO + completionHandler:completionHandler]; }]; // fetcher completion handler - - // If something weird happens and the networking callbacks have been called - // already synchronously, we don't want to return the ticket since the caller - // will never know when to stop retaining it, so we'll make sure the - // success/failure callbacks have not yet been called by checking the - // ticket - if (ticket.hasCalledCallback) { - return nil; - } - - return ticket; } - (GTMSessionUploadFetcher *)uploadFetcherWithRequest:(NSURLRequest *)request diff --git a/Sources/Core/Public/GoogleAPIClientForREST/GTLRService.h b/Sources/Core/Public/GoogleAPIClientForREST/GTLRService.h index 7b0f30027..919a8bd02 100644 --- a/Sources/Core/Public/GoogleAPIClientForREST/GTLRService.h +++ b/Sources/Core/Public/GoogleAPIClientForREST/GTLRService.h @@ -209,7 +209,8 @@ typedef void (^GTLRServiceTestBlock)(GTLRServiceTicket *testTicket, * A query may only be executed a single time. To reuse a query, make a copy before executing * it. * - * To get a NSURLRequest that represents the query, use @c -[GTLRService requestForQuery:] + * To get a NSURLRequest that represents the query, use + * @c -[GTLRService requestForQuery:completion:]. * * @param query The API query, either a subclass of GTLRQuery, or a GTLRBatchQuery. * @param handler The execution callback block. @@ -378,7 +379,10 @@ typedef void (^GTLRServiceTestBlock)(GTLRServiceTicket *testTicket, * * This works only for GET queries, and only for an individual query, not a batch query. * - * @note @c Unlike executeQuery:, requestForQuery: does not release the query's callback blocks. + * @note This method blocks the calling thread to calculate the User-Agent string if not + * otherwise specified, so do not invoke this on the UI thread / main queue. + * Use @c -requestForQuery:completion: unless calling this on a background queue which + * is OK to block. * * @param query The query used to create the request. * @@ -386,6 +390,25 @@ typedef void (^GTLRServiceTestBlock)(GTLRServiceTicket *testTicket, */ - (NSMutableURLRequest *)requestForQuery:(GTLRQuery *)query; +/** + * Creates a NSURLRequest from the query object and from properties on this service + * (additionalHTTPHeaders, additionalURLQueryParameters, APIKey) without executing + * it. This can be useful for using @c GTMSessionFetcher or @c NSURLSession to + * perform the fetch. + * + * For requests to non-public resources, the request will not yet be authorized; + * that can be done using the GTLR service's authorizer. Creating a @c GTMSessionFetcher + * from the GTLRService's @c fetcherService will take care of authorization as well. + * + * This works only for GET queries, and only for an individual query, not a batch query. + * + * @param query The query used to create the request. + * @param completion Completion asynchronously invoked on the service's + * @c callbackQueue with the URL request, suitable for use with with @c GTMSessionFetcher or + * @c NSURLSession. + */ +- (void)requestForQuery:(GTLRQuery *)query completion:(void (^)(NSMutableURLRequest *))completion; + #pragma mark User Properties /** diff --git a/USING.md b/USING.md index f908e6e5a..55e103c28 100644 --- a/USING.md +++ b/USING.md @@ -552,8 +552,15 @@ GTLRQuery *query = [GTLRDriveQuery_FilesGet queryForMediaWithFileId:fileID]; #### Downloading with a GTMSessionFetcher -A GTMSessionFetcher can download any NSURLRequest. The service method -`requestForQuery:` will convert a library query into an NSURLRequest. +A GTMSessionFetcher can download any NSURLRequest. The service methods +`requestForQuery:completion:` / `requestForQuery:` will convert a library query +into an NSURLRequest. + +NOTE: Because formatting the `User-Agent` header (required to create the +`GTMSessionFetcher`) can block the calling thread, always use +`requestForQuery:completion:` when calling from the UI thread / main queue. If +the code is creating the `GTMSessionFetcher` from a background queue that is OK +to block, then using `requestForQuery:` can be more appropriate. Download of any individual user’s data from Google services requires that the request be authorized. A fetcher created from the `GTLRService` @@ -563,14 +570,16 @@ Here is an example of an authorized file download using a fetcher: ```Objective-C GTLRQuery *query = [GTLRDriveQuery_FilesGet queryForMediaWithFileId:fileID]; -NSURLRequest *downloadRequest = [service requestForQuery:query]; -GTMSessionFetcher *fetcher = - [service.fetcherService fetcherWithRequest:downloadRequest]; - -[fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *fetchError) { - if (fetchError == nil) { - // Download succeeded. - } +[service requestForQuery:query + completion:^(NSURLRequest *downloadRequest) { + GTMSessionFetcher *fetcher = + [service.fetcherService fetcherWithRequest:downloadRequest]; + + [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *fetchError) { + if (fetchError == nil) { + // Download succeeded. + } + }]; }]; ``` @@ -762,8 +771,8 @@ To have callbacks performed on a different queue than the main queue, specify a ### Converting a Query to an NSURLRequest -A service object can convert a GTLRQuery to a plain NSMutableURLRequest with -the service method `requestForQuery:`. That may be useful for downloading +A service object can convert a GTLRQuery to a plain NSMutableURLRequest with the +service method `requestForQuery:completion:`. That may be useful for downloading media files, or for performing an API request without using the service’s `executeQuery:` method. diff --git a/UnitTests/GTLRServiceTest.m b/UnitTests/GTLRServiceTest.m index ffe0e6d09..8b084d126 100644 --- a/UnitTests/GTLRServiceTest.m +++ b/UnitTests/GTLRServiceTest.m @@ -723,33 +723,43 @@ - (void)testService_SingleQuery_QueryWithResourceURL { GTLRTestingSvcQuery_FilesList *templateQuery = [GTLRTestingSvcQuery_FilesList query]; templateQuery.fields = @"kind,files(id,kind,name)"; + XCTestExpectation *requestFinished = [self expectationWithDescription:@"requestFinished"]; + XCTestExpectation *queryFinished = [self expectationWithDescription:@"queryFinished"]; + + __block GTLRServiceTicket *queryTicket; // Set a specific request URL by getting the actual query URL. - NSURLRequest *request = [service requestForQuery:templateQuery]; - NSURL *requestURL = request.URL; + [service + requestForQuery:templateQuery + completion:^(NSURLRequest *request) { + NSURL *requestURL = request.URL; - XCTestExpectation *queryFinished = [self expectationWithDescription:@"queryFinished"]; + queryTicket = [service + fetchObjectWithURL:requestURL + objectClass:[GTLRTestingSvc_FileList class] + executionParameters:nil + completionHandler:^(GTLRServiceTicket *callbackTicket, + GTLRTestingSvc_FileList *object, NSError *callbackError) { + // Verify the top-level object and one of its items. + XCTAssertEqualObjects([object class], [GTLRTestingSvc_FileList class]); + XCTAssertNil(callbackError); - GTLRServiceTicket *queryTicket = - [service fetchObjectWithURL:requestURL - objectClass:[GTLRTestingSvc_FileList class] - executionParameters:nil - completionHandler:^(GTLRServiceTicket *callbackTicket, - GTLRTestingSvc_FileList *object, - NSError *callbackError) { - // Verify the top-level object and one of its items. - XCTAssertEqualObjects([object class], [GTLRTestingSvc_FileList class]); - XCTAssertNil(callbackError); + XCTAssertEqualObjects(object.kind, @"drive#fileList"); + XCTAssertEqual(object.files.count, 2U, @"%@", object.files); - XCTAssertEqualObjects(object.kind, @"drive#fileList"); - XCTAssertEqual(object.files.count, 2U, @"%@", object.files); + XCTAssert([NSThread isMainThread]); - XCTAssert([NSThread isMainThread]); + [queryFinished fulfill]; + }]; - [queryFinished fulfill]; - }]; + [requestFinished fulfill]; + }]; - XCTAssertFalse(queryTicket.hasCalledCallback); + [self waitForExpectations:@[ requestFinished ] timeout:1]; + // This only works because this code runs on the main thread, but the completionHandler: passed to + // -fetchObjectWithURL: can't be invoked until -service:waitForTicket: pumps the main thread + // runloop to invoke the completion handler. + XCTAssertFalse(queryTicket.hasCalledCallback); XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); @@ -3358,31 +3368,46 @@ - (void)testRequestForQuery { NSDictionary *baseHTTPHeaders = @{ @"User-Agent" : userAgent }; NSMutableDictionary *expectedHTTPHeaders = [baseHTTPHeaders mutableCopy]; + XCTestExpectation *requestFinished = [self expectationWithDescription:@"requestFinished"]; NSString *expectedURLString = @"https://www.test.com/api/path/foo?arg=mumble"; - NSMutableURLRequest *result = [service requestForQuery:query]; - XCTAssertEqualObjects(result.URL.absoluteString, expectedURLString); - XCTAssertEqualObjects(result.HTTPMethod, @"GET"); - XCTAssertEqualObjects(result.allHTTPHeaderFields, expectedHTTPHeaders); + [service requestForQuery:query + completion:^(NSURLRequest *result) { + XCTAssertEqualObjects(result.URL.absoluteString, expectedURLString); + XCTAssertEqualObjects(result.HTTPMethod, @"GET"); + XCTAssertEqualObjects(result.allHTTPHeaderFields, expectedHTTPHeaders); + [requestFinished fulfill]; + }]; + [self waitForExpectations:@[ requestFinished ] timeout:1]; // Extra query arg and HTTP header. query.additionalURLQueryParameters = (id)@{ @"queryArg" : @YES }; query.additionalHTTPHeaders = @{ @"X-Query" : @"All Good!" }; expectedURLString = @"https://www.test.com/api/path/foo?arg=mumble&queryArg=true"; [expectedHTTPHeaders setObject:@"All Good!" forKey:@"X-Query"]; - result = [service requestForQuery:query]; - XCTAssertEqualObjects(result.URL.absoluteString, expectedURLString); - XCTAssertEqualObjects(result.HTTPMethod, @"GET"); - XCTAssertEqualObjects(result.allHTTPHeaderFields, expectedHTTPHeaders); + XCTestExpectation *request2Finished = [self expectationWithDescription:@"request2Finished"]; + [service requestForQuery:query + completion:^(NSURLRequest *result) { + XCTAssertEqualObjects(result.URL.absoluteString, expectedURLString); + XCTAssertEqualObjects(result.HTTPMethod, @"GET"); + XCTAssertEqualObjects(result.allHTTPHeaderFields, expectedHTTPHeaders); + [request2Finished fulfill]; + }]; + [self waitForExpectations:@[ request2Finished ] timeout:1]; // With a service arg and HTTP header. service.additionalURLQueryParameters = (id)@{ @"serviceArg" : @42 }; service.additionalHTTPHeaders = @{ @"X-Service" : @"Grumble" }; expectedURLString = @"https://www.test.com/api/path/foo?arg=mumble&queryArg=true&serviceArg=42"; [expectedHTTPHeaders setObject:@"Grumble" forKey:@"X-Service"]; - result = [service requestForQuery:query]; - XCTAssertEqualObjects(result.URL.absoluteString, expectedURLString); - XCTAssertEqualObjects(result.HTTPMethod, @"GET"); - XCTAssertEqualObjects(result.allHTTPHeaderFields, expectedHTTPHeaders); + XCTestExpectation *request3Finished = [self expectationWithDescription:@"request3Finished"]; + [service requestForQuery:query + completion:^(NSURLRequest *result) { + XCTAssertEqualObjects(result.URL.absoluteString, expectedURLString); + XCTAssertEqualObjects(result.HTTPMethod, @"GET"); + XCTAssertEqualObjects(result.allHTTPHeaderFields, expectedHTTPHeaders); + [request3Finished fulfill]; + }]; + [self waitForExpectations:@[ request3Finished ] timeout:1]; // Overlap between the query and service for an arg and HTTP header (query // wins). @@ -3397,10 +3422,15 @@ - (void)testRequestForQuery { @"X-2" : @"Query", @"X-3" : @"Service", }]; - result = [service requestForQuery:query]; - XCTAssertEqualObjects(result.URL.absoluteString, expectedURLString); - XCTAssertEqualObjects(result.HTTPMethod, @"GET"); - XCTAssertEqualObjects(result.allHTTPHeaderFields, expectedHTTPHeaders); + XCTestExpectation *request4Finished = [self expectationWithDescription:@"request4Finished"]; + [service requestForQuery:query + completion:^(NSURLRequest *result) { + XCTAssertEqualObjects(result.URL.absoluteString, expectedURLString); + XCTAssertEqualObjects(result.HTTPMethod, @"GET"); + XCTAssertEqualObjects(result.allHTTPHeaderFields, expectedHTTPHeaders); + [request4Finished fulfill]; + }]; + [self waitForExpectations:@[ request4Finished ] timeout:1]; // Different HTTPMethod. query = [[GTLRQuery alloc] initWithPathURITemplate:@"blah" @@ -3410,26 +3440,41 @@ - (void)testRequestForQuery { service.additionalURLQueryParameters = nil; service.additionalHTTPHeaders = nil; expectedHTTPHeaders = [baseHTTPHeaders mutableCopy]; - result = [service requestForQuery:query]; - XCTAssertEqualObjects(result.URL.absoluteString, expectedURLString); - XCTAssertEqualObjects(result.HTTPMethod, @"POST"); - XCTAssertEqualObjects(result.allHTTPHeaderFields, expectedHTTPHeaders); + XCTestExpectation *request5Finished = [self expectationWithDescription:@"request5Finished"]; + [service requestForQuery:query + completion:^(NSURLRequest *result) { + XCTAssertEqualObjects(result.URL.absoluteString, expectedURLString); + XCTAssertEqualObjects(result.HTTPMethod, @"POST"); + XCTAssertEqualObjects(result.allHTTPHeaderFields, expectedHTTPHeaders); + [request5Finished fulfill]; + }]; + [self waitForExpectations:@[ request5Finished ] timeout:1]; // Add an APIKey. service.APIKey = @"Abracadabra!"; - result = [service requestForQuery:query]; expectedURLString = @"https://www.test.com/api/blah?key=Abracadabra%21"; - XCTAssertEqualObjects(result.URL.absoluteString, expectedURLString); - XCTAssertEqualObjects(result.HTTPMethod, @"POST"); - XCTAssertEqualObjects(result.allHTTPHeaderFields, expectedHTTPHeaders); + XCTestExpectation *request6Finished = [self expectationWithDescription:@"request6Finished"]; + [service requestForQuery:query + completion:^(NSURLRequest *result) { + XCTAssertEqualObjects(result.URL.absoluteString, expectedURLString); + XCTAssertEqualObjects(result.HTTPMethod, @"POST"); + XCTAssertEqualObjects(result.allHTTPHeaderFields, expectedHTTPHeaders); + [request6Finished fulfill]; + }]; + [self waitForExpectations:@[ request6Finished ] timeout:1]; // Add an APIKey Restriction service.APIKeyRestrictionBundleID = @"foo.bar.baz"; expectedHTTPHeaders[kXIosBundleIdHeader] = @"foo.bar.baz"; - result = [service requestForQuery:query]; - XCTAssertEqualObjects(result.URL.absoluteString, expectedURLString); - XCTAssertEqualObjects(result.HTTPMethod, @"POST"); - XCTAssertEqualObjects(result.allHTTPHeaderFields, expectedHTTPHeaders); + XCTestExpectation *request7Finished = [self expectationWithDescription:@"request7Finished"]; + [service requestForQuery:query + completion:^(NSURLRequest *result) { + XCTAssertEqualObjects(result.URL.absoluteString, expectedURLString); + XCTAssertEqualObjects(result.HTTPMethod, @"POST"); + XCTAssertEqualObjects(result.allHTTPHeaderFields, expectedHTTPHeaders); + [request7Finished fulfill]; + }]; + [self waitForExpectations:@[ request7Finished ] timeout:1]; } - (void)testRequestForQuery_MediaDownload { @@ -3440,15 +3485,35 @@ - (void)testRequestForQuery_MediaDownload { GTLRService *service = [self driveServiceForTest]; // Without download service. - NSURLRequest *result = [service requestForQuery:query]; + XCTestExpectation *requestFinished = [self expectationWithDescription:@"requestFinished"]; NSString *expectedURLString = @"https://www.googleapis.com/drive/v3/files/abcde?alt=media"; - XCTAssertEqualObjects(result.URL.absoluteString, expectedURLString); + [service requestForQuery:query + completion:^(NSURLRequest *result) { + XCTAssertEqualObjects(result.URL.absoluteString, expectedURLString); + [requestFinished fulfill]; + }]; + [self waitForExpectations:@[ requestFinished ] timeout:1]; // With download service. query.useMediaDownloadService = YES; - result = [service requestForQuery:query]; + XCTestExpectation *request2Finished = [self expectationWithDescription:@"request2Finished"]; expectedURLString = @"https://www.googleapis.com/download/drive/v3/files/abcde?alt=media"; - XCTAssertEqualObjects(result.URL.absoluteString, expectedURLString); + [service requestForQuery:query + completion:^(NSURLRequest *result) { + XCTAssertEqualObjects(result.URL.absoluteString, expectedURLString); + [request2Finished fulfill]; + }]; + [self waitForExpectations:@[ request2Finished ] timeout:1]; +} + +- (void)testRequestForQuery_synchronousAPI { + GTLRService *service = [self driveServiceForTest]; + GTLRTestingSvcQuery_FilesGet *query = + [GTLRTestingSvcQuery_FilesGet queryForMediaWithFileId:@"abcde"]; + NSURLRequest *result = [service requestForQuery:query]; + NSDictionary *expectedHeaders = + @{@"User-Agent" : service.requestUserAgent}; + XCTAssertEqualObjects(result.allHTTPHeaderFields, expectedHeaders); } #pragma mark - Internal Utility Method Tests