/* Copyright (c) 2011 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. */ #import #import #import "GTLRService.h" #import "GTLRUtilities.h" #import "GTMMIMEDocument.h" #import "GTLRTestingSvc.h" #import "GTMSessionFetcherService.h" @interface GTLRServiceTest : XCTestCase @end // Surrogates for testing. @interface GTLRTestingSvc_File_Surrogate : GTLRTestingSvc_File @end @interface GTLRTestingSvc_FileList_Surrogate : GTLRTestingSvc_FileList @end @interface GTLRTestingSvc_FileList_Surrogate2 : GTLRTestingSvc_FileList @end @implementation GTLRTestingSvc_File_Surrogate @end @implementation GTLRTestingSvc_FileList_Surrogate @end @implementation GTLRTestingSvc_FileList_Surrogate2 @end // Internal methods redeclared for testing. @interface GTLRBatchResponsePart : NSObject @property(nonatomic, strong) NSDictionary *headers; @end @interface GTLRService (InternalMethods) + (NSURL *)URLWithString:(NSString *)urlString queryParameters:(NSDictionary *)queryParameters; - (GTLRBatchResponsePart *)responsePartWithMIMEPart:(GTMMIMEDocumentPart *)mimePart; @end // // Simple authorizer class for testing // @interface GTLRTestAuthorizer : NSObject // The value string will be added to the authorization header, like // Authorization: Bearer value + (GTLRTestAuthorizer *)authorizerWithValue:(NSString *)value; @property(atomic, copy) NSString *value; @property(atomic, retain) NSError *error; @end // GTLRTestLifetimeObject fulfills the expectation when the provided object deallocates. // // This allows dealloc to occur asynchronously and the test code to just wait for it. @interface GTLRTestLifetimeObject : NSObject + (instancetype)trackLifetimeOfObject:(id)object expectation:(XCTestExpectation *)expectation; @end #if GTM_BACKGROUND_TASK_FETCHING // An implementation of a substitute UIApplication to track invocations of begin // and end. It doesn't attempt to match them up. @interface CountingUIApplication : NSObject @property(atomic, readonly) NSCountedSet *beginTaskIDs; @property(atomic, readonly) NSCountedSet *endTaskIDs; // Optionally, expire tasks immediately (on the next main thread cycle.) @property(atomic, assign) BOOL shouldExpireTasks; @property(atomic, readonly) NSUInteger numberOfExpiredTasks; @end #endif // GTM_BACKGROUND_TASK_FETCHING @implementation GTLRServiceTest { NSURL *_tempFileURLToDelete; } - (void)tearDown { if (_tempFileURLToDelete) { NSError *deleteError; XCTAssert([[NSFileManager defaultManager] removeItemAtURL:_tempFileURLToDelete error:&deleteError], @"%@", deleteError); } [self clearCountingUIApp]; [super tearDown]; } #pragma mark - + (NSData *)dataForTestFileName:(NSString *)fileName { // Make an URL for the named file in the unit test bundle. NSBundle *testBundle = [NSBundle bundleForClass:[self class]]; NSURL *fileURL = [testBundle URLForResource:fileName withExtension:nil]; if (!fileURL) { NSLog(@"Cannot find \"%@\" in test runtime resources at %@", fileName, testBundle.resourcePath); return nil; } NSError *error; NSData *data = [NSData dataWithContentsOfURL:fileURL options:0 error:&error]; if (!data) { NSLog(@"Cannot read data for %@: %@", fileName, error); } return data; } static NSHTTPURLResponse *QueryResponseWithURL(NSURL *url, NSInteger status, NSString *contentType) { // Make a succesful API response to feed to the fetcher testBlock. return [[NSHTTPURLResponse alloc] initWithURL:url statusCode:status HTTPVersion:@"HTTP/1.1" headerFields:@{ @"Content-Type" : contentType }]; } - (GTMSessionFetcherTestBlock)fetcherTestBlockWithResponseForFileName:(NSString *)fileName status:(NSInteger)status { return [self fetcherTestBlockWithResponseForFileName:fileName status:status numberOfStatusErrors:0 multipartBoundary:nil]; } - (GTMSessionFetcherTestBlock)fetcherTestBlockWithResponseForFileName:(NSString *)fileName status:(NSInteger)initialStatus numberOfStatusErrors:(NSInteger)numberOfStatusErrors multipartBoundary:(NSString *)multipartBoundary { GTMSessionFetcherTestBlock testBlock = ^(GTMSessionFetcher *fetcherToTest, GTMSessionFetcherTestResponse testResponse) { NSString *topContentType = @"application/json; charset=UTF-8"; if (multipartBoundary) { topContentType = [NSString stringWithFormat:@"multipart/mixed; boundary=%@", multipartBoundary]; } NSInteger status = initialStatus; if (numberOfStatusErrors > 0 && (NSInteger)fetcherToTest.retryCount >= numberOfStatusErrors) { status = 200; } NSHTTPURLResponse *response = QueryResponseWithURL(fetcherToTest.request.URL, status, topContentType); NSData *responseData; if (status != 204) { if (fileName) { responseData = [[self class] dataForTestFileName:fileName]; XCTAssertNotNil(responseData, @"%@", fileName); } } else { // We expect 204 "No content" on delete, for example. } NSError *responseError = nil; if (status >= 400) { responseError = [NSError errorWithDomain:kGTMSessionFetcherStatusDomain code:status userInfo:nil]; } testResponse(response, responseData, responseError); }; return testBlock; } static NSString *QueryValueForURLItem(NSURL *url, NSString *itemName) { NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:YES]; NSArray *queryItems = components.queryItems; for (NSURLQueryItem *thisItem in queryItems) { if ([thisItem.name isEqual:itemName]) { return thisItem.value; } } return nil; } // This matches GTLRURITemplate's version. static NSString *EscapeString(NSString *str, BOOL allowReserved) { NSMutableCharacterSet *cs = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy]; NSString * const kReservedChars = @":/?#[]@!$&'()*+,;="; if (allowReserved) { [cs addCharactersInString:kReservedChars]; } else { [cs removeCharactersInString:kReservedChars]; } NSString *resultStr = [str stringByAddingPercentEncodingWithAllowedCharacters:cs]; return resultStr; } static BOOL WWWFormDataHasValue(NSData *wwwFormData, NSString *itemName, NSString *itemValue) { NSString *escapedItemValue = EscapeString(itemValue, NO); NSRange fullRange = NSMakeRange(0, wwwFormData.length); NSString *needle = [NSString stringWithFormat:@"&%@=%@&", itemName, escapedItemValue]; NSData *needleData = [needle dataUsingEncoding:NSUTF8StringEncoding]; NSRange range = [wwwFormData rangeOfData:needleData options:0 range:fullRange]; if (range.location != NSNotFound) { return YES; } // It could be the first or last, so check those cases. needle = [NSString stringWithFormat:@"%@=%@&", itemName, escapedItemValue]; needleData = [needle dataUsingEncoding:NSUTF8StringEncoding]; range = [wwwFormData rangeOfData:needleData options:NSDataSearchAnchored range:fullRange]; if (range.location == 0) { return YES; } needle = [NSString stringWithFormat:@"&%@=%@", itemName, escapedItemValue]; needleData = [needle dataUsingEncoding:NSUTF8StringEncoding]; range = [wwwFormData rangeOfData:needleData options:(NSDataSearchAnchored | NSDataSearchBackwards) range:fullRange]; if (range.location != NSNotFound) { return YES; } // Last case, the data was just the one pair. needle = [NSString stringWithFormat:@"%@=%@", itemName, escapedItemValue]; needleData = [needle dataUsingEncoding:NSUTF8StringEncoding]; if ([wwwFormData isEqual:needleData]) { return YES; } return NO; } static BOOL IsCurrentQueue(dispatch_queue_t targetQueue) { const char *targetQueueLabel = dispatch_queue_get_label(targetQueue); const char *currentQueueLabel = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL); int result = strcmp(currentQueueLabel, targetQueueLabel); return result == 0; } - (GTLRService *)driveServiceForTest { // Has valid authorization. GTLRService *service = [[GTLRTestingSvcService alloc] init]; service.authorizer = [GTLRTestAuthorizer authorizerWithValue:@"catpaws"]; return service; } - (BOOL)service:(GTLRService *)service waitForTicket:(GTLRServiceTicket *)ticket { BOOL finishedInTime = [service waitForTicket:ticket timeout:10.0]; return finishedInTime; } - (void)expectTicketNotifications { [self expectationForNotification:kGTLRServiceTicketStartedNotification object:nil handler:nil]; [self expectationForNotification:kGTLRServiceTicketStoppedNotification object:nil handler:nil]; } - (void)expectTicketAndParsingNotifications { [self expectTicketNotifications]; // Parsing notifications are expected whenever the fetcher returns a nil error // and non-empty data. [self expectationForNotification:kGTLRServiceTicketParsingStartedNotification object:nil handler:nil]; [self expectationForNotification:kGTLRServiceTicketParsingStoppedNotification object:nil handler:nil]; } - (void)setCountingUIAppWithExpirations:(BOOL)shouldExpireTasks { #if GTM_BACKGROUND_TASK_FETCHING CountingUIApplication *countingUIApp = [[CountingUIApplication alloc] init]; countingUIApp.shouldExpireTasks = shouldExpireTasks; [GTMSessionFetcher setSubstituteUIApplication:countingUIApp]; #endif } - (void)verifyCountingUIAppWithExpectedCount:(NSUInteger)expectedCount expectedExpirations:(NSUInteger)expectedExpirations { #if GTM_BACKGROUND_TASK_FETCHING CountingUIApplication *countingUIApp = [GTMSessionFetcher substituteUIApplication]; XCTAssertNotNil(countingUIApp); XCTAssertEqual(countingUIApp.beginTaskIDs.count, expectedCount); XCTAssertEqualObjects(countingUIApp.beginTaskIDs, countingUIApp.endTaskIDs); XCTAssertEqual(countingUIApp.numberOfExpiredTasks, expectedExpirations); [self clearCountingUIApp]; #endif } - (void)clearCountingUIApp { #if GTM_BACKGROUND_TASK_FETCHING [GTMSessionFetcher setSubstituteUIApplication:nil]; #endif } #pragma mark - Query Execution Tests - (void)testService_SingleQuery { // Successful request with valid authorization. // // Response is file Drive1.response.txt GTLRService *service = [self driveServiceForTest]; service.fetcherService.testBlock = [self fetcherTestBlockWithResponseForFileName:@"Drive1.response.txt" status:200]; service.serviceProperties = @{ @"Marsupial" : @"Koala", @"Dolphin" : @"Spinner" }; [self expectTicketAndParsingNotifications]; [self setCountingUIAppWithExpirations:NO]; NSString *timeParam = @"2011-05-04T23:28:20.888Z"; GTLRTestingSvcQuery_FilesList *query = [GTLRTestingSvcQuery_FilesList query]; query.fields = @"kind,nextPageToken,files(mimeType,id,kind,name,webViewLink,thumbnailLink,trashed)"; query.pageSize = 10; query.requestID = @"gtlr_1234"; query.timeParamForTesting = [GTLRDateTime dateTimeWithRFC3339String:timeParam]; query.additionalHTTPHeaders = @{ @"X-Feline": @"Fluffy", @"X-Canine": @"Spot" }; query.additionalURLQueryParameters = @{ @"meowParam": @"Meow" }; query.executionParameters.retryBlock = ^(GTLRServiceTicket *ticket, BOOL suggestedWillRetry, NSError *error) { XCTFail(@"No retry expected."); return NO; }; query.executionParameters.uploadProgressBlock = ^(GTLRServiceTicket *ticket, unsigned long long numberOfBytesRead, unsigned long long dataLength) { XCTFail(@"No body upload expected."); }; query.executionParameters.ticketProperties = @{ @"Bird" : @"Kookaburra", @"Dolphin" : @"Tucuxi" }; XCTestExpectation *queryFinished = [self expectationWithDescription:@"queryFinished"]; __block GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_FileList *object, NSError *error) { // Verify the top-level object and one of its items. XCTAssertEqualObjects([object class], [GTLRTestingSvc_FileList class]); XCTAssertNil(error); XCTAssertEqualObjects(object.kind, @"drive#fileList"); XCTAssertEqual(object.files.count, 2U, @"%@", object.files); GTLRTestingSvc_File *item0 = object.files[0]; XCTAssertEqualObjects([item0 class], [GTLRTestingSvc_File class]); XCTAssertEqualObjects(item0.kind, @"drive#file"); XCTAssertEqualObjects(object.timeFieldForTesting.RFC3339String, @"2011-01-02T03:04:05.067Z"); XCTAssertEqualObjects(callbackTicket, queryTicket); // The ticket's query should be a copy of the original; query execution leaves // the original unmolested so the client can modify and reuse it. GTLRQuery *ticketQuery = callbackTicket.executingQuery; XCTAssertNotEqual(ticketQuery, query); XCTAssertEqualObjects(ticketQuery.requestID, query.requestID); // Service properties should be copied to the ticket. XCTAssertEqualObjects(callbackTicket.ticketProperties[@"Marsupial"], @"Koala"); XCTAssertEqualObjects(callbackTicket.ticketProperties[@"Bird"], @"Kookaburra"); XCTAssertEqualObjects(callbackTicket.ticketProperties[@"Dolphin"], @"Tucuxi"); XCTAssert([NSThread isMainThread]); [queryFinished fulfill]; }]; XCTAssertFalse(queryTicket.hasCalledCallback); XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // GTLRQuery query parameters. NSURLRequest *fetcherRequest = queryTicket.objectFetcher.request; XCTAssertEqualObjects(QueryValueForURLItem(fetcherRequest.URL, @"fields"), query.fields); XCTAssertEqualObjects(QueryValueForURLItem(fetcherRequest.URL, @"pageSize"), @"10"); XCTAssertEqualObjects(QueryValueForURLItem(fetcherRequest.URL, @"prettyPrint"), @"false"); XCTAssertEqualObjects(QueryValueForURLItem(fetcherRequest.URL, @"timeParamForTesting"), timeParam); // Test things added to the request: // Authorization header, additionalHTTPHeaders, and additionalURLQueryParameters // // Headers. XCTAssertEqualObjects([fetcherRequest valueForHTTPHeaderField:@"Authorization"], @"Bearer catpaws"); XCTAssertEqualObjects([fetcherRequest valueForHTTPHeaderField:@"X-Feline"], @"Fluffy"); // URL query parameters. XCTAssertEqualObjects(QueryValueForURLItem(fetcherRequest.URL, @"meowParam"), @"Meow"); // The fetcher releases its authorizer upon completion, so to test the request, // we'll use the service's authorizer. id authorizer = service.authorizer; XCTAssertNotNil(authorizer); XCTAssert([authorizer isAuthorizedRequest:fetcherRequest], @"%@", fetcherRequest.allHTTPHeaderFields); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; // The fetcher and GTLRService both start and end background tasks, so we expect 2 invocations of // beginBackgroundTask and endBackgroundTask. [self verifyCountingUIAppWithExpectedCount:2 expectedExpirations:0]; } - (void)testService_SingleQuery_LongURLToFormPost { // Successful request with valid authorization but with URL query arguments // forcing it into a www forms post. // // Response is file Drive1.response.txt GTLRService *service = [self driveServiceForTest]; service.fetcherService.testBlock = [self fetcherTestBlockWithResponseForFileName:@"Drive1.response.txt" status:200]; service.serviceProperties = @{ @"Marsupial" : @"Koala", @"Dolphin" : @"Spinner" }; [self expectTicketAndParsingNotifications]; [self setCountingUIAppWithExpirations:NO]; NSString *timeParam = @"2011-05-04T23:28:20.888Z"; GTLRTestingSvcQuery_FilesList *query = [GTLRTestingSvcQuery_FilesList query]; query.fields = @"kind,nextPageToken,files(mimeType,id,kind,name,webViewLink,thumbnailLink,trashed)"; query.pageSize = 10; query.requestID = @"gtlr_1234"; query.timeParamForTesting = [GTLRDateTime dateTimeWithRFC3339String:timeParam]; // Add a bunch of entries to ensure we go over the URL size limit. NSMutableArray *extras = [NSMutableArray array]; for (NSUInteger x = 0 ; x < 1024; ++x) { [extras addObject:[@(x) description]]; } query.extras = extras; query.additionalHTTPHeaders = @{ @"X-Feline": @"Fluffy", @"X-Canine": @"Spot" }; query.additionalURLQueryParameters = @{ @"meowParam": @"Meow" }; query.executionParameters.retryBlock = ^(GTLRServiceTicket *ticket, BOOL suggestedWillRetry, NSError *error) { XCTFail(@"No retry expected."); return NO; }; XCTestExpectation *uploadedSomeBytes = [self expectationWithDescription:@"uploadedSomeBytes"]; query.executionParameters.uploadProgressBlock = ^(GTLRServiceTicket *ticket, unsigned long long numberOfBytesRead, unsigned long long dataLength) { // The arg will go in the body, so uploadProgressBlock will see them. if (numberOfBytesRead == dataLength) { [uploadedSomeBytes fulfill]; } XCTAssert([NSThread isMainThread]); }; query.executionParameters.ticketProperties = @{ @"Bird" : @"Kookaburra", @"Dolphin" : @"Tucuxi" }; XCTestExpectation *queryFinished = [self expectationWithDescription:@"queryFinished"]; __block GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_FileList *object, NSError *error) { // Verify the top-level object and one of its items. XCTAssertEqualObjects([object class], [GTLRTestingSvc_FileList class]); XCTAssertNil(error); XCTAssertEqualObjects(object.kind, @"drive#fileList"); XCTAssertEqual(object.files.count, 2U, @"%@", object.files); GTLRTestingSvc_File *item0 = object.files[0]; XCTAssertEqualObjects([item0 class], [GTLRTestingSvc_File class]); XCTAssertEqualObjects(item0.kind, @"drive#file"); XCTAssertEqualObjects(object.timeFieldForTesting.RFC3339String, @"2011-01-02T03:04:05.067Z"); XCTAssertEqualObjects(callbackTicket, queryTicket); // The ticket's query should be a copy of the original; query execution leaves // the original unmolested so the client can modify and reuse it. GTLRQuery *ticketQuery = callbackTicket.executingQuery; XCTAssertNotEqual(ticketQuery, query); XCTAssertEqualObjects(ticketQuery.requestID, query.requestID); // Service properties should be copied to the ticket. XCTAssertEqualObjects(callbackTicket.ticketProperties[@"Marsupial"], @"Koala"); XCTAssertEqualObjects(callbackTicket.ticketProperties[@"Bird"], @"Kookaburra"); XCTAssertEqualObjects(callbackTicket.ticketProperties[@"Dolphin"], @"Tucuxi"); XCTAssert([NSThread isMainThread]); [queryFinished fulfill]; }]; XCTAssertFalse(queryTicket.hasCalledCallback); XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); NSURLRequest *fetcherRequest = queryTicket.objectFetcher.request; // Should be a POST with the X-HTTP-Method-Override header and correct // Content-Type. XCTAssertEqualObjects(fetcherRequest.HTTPMethod, @"POST"); XCTAssertEqualObjects([fetcherRequest valueForHTTPHeaderField:@"X-HTTP-Method-Override"], @"GET"); XCTAssertEqualObjects([fetcherRequest valueForHTTPHeaderField:@"Content-Type"], @"application/x-www-form-urlencoded"); // GTLRQuery query parameters. XCTAssertNil(fetcherRequest.URL.query); // Should be in the body instead. NSData *dataPosted = queryTicket.objectFetcher.bodyData; XCTAssertNotNil(dataPosted); XCTAssertTrue(WWWFormDataHasValue(dataPosted, @"fields", query.fields)); XCTAssertTrue(WWWFormDataHasValue(dataPosted, @"pageSize", @"10")); XCTAssertTrue(WWWFormDataHasValue(dataPosted, @"prettyPrint", @"false")); XCTAssertTrue(WWWFormDataHasValue(dataPosted, @"timeParamForTesting", timeParam)); // Test things added to the request: // Authorization header, additionalHTTPHeaders, and additionalURLQueryParameters // // Headers. XCTAssertEqualObjects([fetcherRequest valueForHTTPHeaderField:@"Authorization"], @"Bearer catpaws"); XCTAssertEqualObjects([fetcherRequest valueForHTTPHeaderField:@"X-Feline"], @"Fluffy"); // URL query parameters. XCTAssertTrue(WWWFormDataHasValue(dataPosted, @"meowParam", @"Meow")); // The fetcher releases its authorizer upon completion, so to test the request, // we'll use the service's authorizer. id authorizer = service.authorizer; XCTAssertNotNil(authorizer); XCTAssert([authorizer isAuthorizedRequest:fetcherRequest], @"%@", fetcherRequest.allHTTPHeaderFields); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; // The fetcher and GTLRService both start and end background tasks, so we expect 2 invocations of // beginBackgroundTask and endBackgroundTask. [self verifyCountingUIAppWithExpectedCount:2 expectedExpirations:0]; } - (void)testService_SingleQuery_SelectorCallback { // Successful request with selector callback. GTLRService *service = [self driveServiceForTest]; service.fetcherService.testBlock = [self fetcherTestBlockWithResponseForFileName:@"Drive1.response.txt" status:200]; [self expectTicketAndParsingNotifications]; GTLRTestingSvcQuery_FilesList *query = [GTLRTestingSvcQuery_FilesList query]; query.fields = @"kind,nextPageToken,files(mimeType,id,kind,name)"; XCTestExpectation *queryFinished = [self expectationWithDescription:@"queryFinished"]; // We'll put the callback tests in a block in the ticket properties. query.executionParameters.ticketProperties = @{ @"callback tests" : ^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_FileList *object, NSError *error){ // Verify the top-level object and one of its items. XCTAssertEqualObjects([object class], [GTLRTestingSvc_FileList class]); XCTAssertNil(error); XCTAssertEqualObjects(object.kind, @"drive#fileList"); XCTAssertEqual(object.files.count, 2U, @"%@", object.files); // The ticket's query should be a copy of the original; query execution leaves // the original unmolested so the client can modify and reuse it. GTLRQuery *ticketQuery = callbackTicket.executingQuery; XCTAssertNotEqual(ticketQuery, query); XCTAssertEqualObjects(ticketQuery.requestID, query.requestID); XCTAssert([NSThread isMainThread]); [queryFinished fulfill]; }}; GTLRServiceTicket *queryTicket = [service executeQuery:query delegate:self didFinishSelector:@selector(serviceTicket:finishedWithObject:error:)]; XCTAssertFalse(queryTicket.hasCalledCallback); XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } // Callback method for use in testService_SingleQuery_SelectorCallback // // The ticket properties have a block to be executed. - (void)serviceTicket:(GTLRServiceTicket *)callbackTicket finishedWithObject:(GTLRTestingSvc_FileList *)object error:(NSError *)callbackError { GTLRServiceCompletionHandler block = callbackTicket.ticketProperties[@"callback tests"]; block(callbackTicket, object, callbackError); } - (void)testService_SingleQuery_QueryWithResourceURL { // Successful based on fixed URL. GTLRService *service = [self driveServiceForTest]; service.fetcherService.testBlock = [self fetcherTestBlockWithResponseForFileName:@"Drive1.response.txt" status:200]; [self expectTicketAndParsingNotifications]; GTLRTestingSvcQuery_FilesList *templateQuery = [GTLRTestingSvcQuery_FilesList query]; templateQuery.fields = @"kind,files(id,kind,name)"; // Set a specific request URL by getting the actual query URL. NSURLRequest *request = [service requestForQuery:templateQuery]; NSURL *requestURL = request.URL; XCTestExpectation *queryFinished = [self expectationWithDescription:@"queryFinished"]; 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); XCTAssert([NSThread isMainThread]); [queryFinished fulfill]; }]; XCTAssertFalse(queryTicket.hasCalledCallback); XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; NSURL *fetcherRequestURL = queryTicket.objectFetcher.request.URL; XCTAssertEqualObjects(fetcherRequestURL.host, @"www.googleapis.com"); XCTAssertEqualObjects(fetcherRequestURL.path, @"/drive/v3/files"); } - (void)testService_SingleQuery_ExpiredBackgroundTasks { // Successful request with expired background tasks. [self setCountingUIAppWithExpirations:YES]; GTLRService *service = [self driveServiceForTest]; service.fetcherService.testBlock = [self fetcherTestBlockWithResponseForFileName:@"Drive1.response.txt" status:456 numberOfStatusErrors:1 multipartBoundary:nil]; GTLRTestingSvcQuery_FilesList *query = [GTLRTestingSvcQuery_FilesList query]; query.fields = @"kind,nextPageToken,files(mimeType,id,kind)"; XCTestExpectation *completionExp = [self expectationWithDescription:@"completionExp"]; GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_FileList *object, NSError *error) { [completionExp fulfill]; }]; XCTAssert([self service:service waitForTicket:queryTicket]); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; // The fetcher and GTLRService both start and end background tasks, so we expect 2 invocations of // beginBackgroundTask and endBackgroundTask, both expired. [self verifyCountingUIAppWithExpectedCount:2 expectedExpirations:2]; } - (void)testService_SingleQuery_Paging { // Successful request with 3 pages of results, 8 total result items. // // Response is file Drive1.response.txt GTLRService *service = [self driveServiceForTest]; [self setCountingUIAppWithExpirations:NO]; __block GTLRServiceTicket *queryTicket; __block int pageCounter = 0; // 1-based as page requests come in to the fetcher. void (^checkRequestParamsAndHeaders)(NSURLRequest *) = ^(NSURLRequest *request){ // GTLRQuery query parameters. XCTAssertEqualObjects(QueryValueForURLItem(request.URL, @"pageSize"), @"3"); if (pageCounter == 0) { XCTAssertNil(QueryValueForURLItem(request.URL, @"pageToken")); } else { XCTAssertEqualObjects(QueryValueForURLItem(request.URL, @"pageToken"), @(pageCounter + 1).stringValue); } // All queries should have all header fields and added params. XCTAssertEqualObjects([request valueForHTTPHeaderField:@"Authorization"], @"Bearer catpaws"); XCTAssertEqualObjects([request valueForHTTPHeaderField:@"X-Feline"], @"Fluffy"); XCTAssertEqualObjects(QueryValueForURLItem(request.URL, @"meowParam"), @"Meow"); }; service.fetcherService.testBlock = ^(GTMSessionFetcher *fetcherToTest, GTMSessionFetcherTestResponse testResponse) { checkRequestParamsAndHeaders(fetcherToTest.request); XCTAssertEqual(queryTicket.pagesFetchedCounter, (NSUInteger)pageCounter); ++pageCounter; NSHTTPURLResponse *response = QueryResponseWithURL(fetcherToTest.request.URL, 200, @"application/json"); NSString *fileName = [NSString stringWithFormat:@"Drive1Paging%d.response.txt", pageCounter]; NSData *responseData = [[self class] dataForTestFileName:fileName];; XCTAssertNotNil(responseData, @"%@", fileName); testResponse(response, responseData, nil); }; [self expectTicketAndParsingNotifications]; GTLRTestingSvcQuery_FilesList *query = [GTLRTestingSvcQuery_FilesList query]; query.fields = @"kind,nextPageToken,files(mimeType,id,kind,name,trashed)"; query.pageSize = 3; query.additionalHTTPHeaders = @{ @"X-Feline": @"Fluffy", @"X-Canine": @"Spot" }; query.additionalURLQueryParameters = @{ @"meowParam": @"Meow" }; query.executionParameters.shouldFetchNextPages = @YES; query.executionParameters.retryBlock = ^(GTLRServiceTicket *ticket, BOOL suggestedWillRetry, NSError *error) { XCTFail(@"No retry expected."); return NO; }; query.executionParameters.uploadProgressBlock = ^(GTLRServiceTicket *ticket, unsigned long long numberOfBytesRead, unsigned long long dataLength) { XCTFail(@"No body upload expected."); }; XCTestExpectation *queryFinished = [self expectationWithDescription:@"queryFinished"]; queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_FileList *object, NSError *error) { XCTAssertEqual(pageCounter, 3); // Verify the top-level object and one of its items. XCTAssertEqualObjects([object class], [GTLRTestingSvc_FileList class]); XCTAssertNil(error); XCTAssertEqualObjects(object.kind, @"drive#fileList"); XCTAssertEqual(object.files.count, 8U, @"%@", object.files); GTLRTestingSvc_File *item0 = object.files[0]; XCTAssertEqualObjects([item0 class], [GTLRTestingSvc_File class]); XCTAssertEqualObjects(item0.kind, @"drive#file"); XCTAssertEqualObjects(item0.identifier, @"1_tKaUhjk3YkF3RYk1d"); GTLRTestingSvc_File *item7 = object.files[7]; XCTAssertEqualObjects([item7 class], [GTLRTestingSvc_File class]); XCTAssertEqualObjects(item7.kind, @"drive#file"); XCTAssertEqualObjects(item7.identifier, @"1M0XYjGhEbY1BzHs5srOpQ"); XCTAssert([NSThread isMainThread]); [queryFinished fulfill]; }]; XCTAssertFalse(queryTicket.hasCalledCallback); XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; // We expect six calls to beginBackgroundTask and endBackgroundTask, one for // fetcher and GTLRService for each page. [self verifyCountingUIAppWithExpectedCount:6 expectedExpirations:0]; } - (void)testService_SingleQuery_SkipAuth { // Disallow authorizing request. GTLRService *service = [self driveServiceForTest]; service.fetcherService.testBlock = [self fetcherTestBlockWithResponseForFileName:@"Drive1.response.txt" status:200]; // Request with an auth error. GTLRTestingSvcQuery_FilesList *query = [GTLRTestingSvcQuery_FilesList query]; query.fields = @"kind,nextPageToken,files(id,kind,name)"; query.requestID = @"gtlr_1234"; query.shouldSkipAuthorization = YES; XCTestExpectation *queryFinished = [self expectationWithDescription:@"queryFinished"]; GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_FileList *object, NSError *error) { XCTAssertEqualObjects([object class], [GTLRTestingSvc_FileList class]); XCTAssertNil(error); [queryFinished fulfill]; }]; XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Authorization should have been skipped. id authorizer = service.authorizer; NSURLRequest *fetcherRequest = queryTicket.objectFetcher.request; XCTAssertNotNil(authorizer); XCTAssertFalse([authorizer isAuthorizedRequest:fetcherRequest], @"%@", fetcherRequest.allHTTPHeaderFields); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)performTestService_SingleQuery_InvalidParam:(NSString *)responseFileName { GTLRService *service = [self driveServiceForTest]; service.fetcherService.testBlock = [self fetcherTestBlockWithResponseForFileName:responseFileName status:400]; // Request an invalid page. GTLRTestingSvcQuery_FilesList *query = [GTLRTestingSvcQuery_FilesList query]; query.fields = @"kind,nextPageToken,files(id,kind,name)"; query.pageToken = @"NotARealToken"; query.requestID = @"gtlr_1234"; XCTestExpectation *queryFinishedExp = [self expectationWithDescription:@"queryFinished"]; XCTestExpectation *retryExp = [self expectationWithDescription:@"queryFinished"]; query.executionParameters.retryBlock = ^(GTLRServiceTicket *ticket, BOOL suggestedWillRetry, NSError *error) { [retryExp fulfill]; return NO; }; query.executionParameters.uploadProgressBlock = ^(GTLRServiceTicket *ticket, unsigned long long numberOfBytesRead, unsigned long long dataLength) { XCTFail(@"No body upload expected."); }; GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_FileList *object, NSError *error) { XCTAssertNil(object); XCTAssertEqualObjects(error.domain, kGTLRErrorObjectDomain); XCTAssertEqual(error.code, 400); GTLRErrorObject *errorObject = [error.userInfo objectForKey:kGTLRStructuredErrorKey]; XCTAssertEqual(errorObject.code.intValue, 400); XCTAssertEqualObjects(errorObject.message, @"Invalid Value"); XCTAssertEqual(errorObject.errors.count, (NSUInteger)1); XCTAssertEqualObjects(errorObject.errors[0].reason, @"invalid"); XCTAssertEqualObjects(errorObject.errors[0].message, @"Invalid Value"); [queryFinishedExp fulfill]; }]; XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); NSURLRequest *fetcherRequest = queryTicket.objectFetcher.request; XCTAssertEqualObjects(QueryValueForURLItem(fetcherRequest.URL, @"pageToken"), @"NotARealToken"); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)testService_SingleQuery_InvalidParam { // Unsuccessful request (invalid page token). // // Response is file Drive1ParamError.response.txt [self performTestService_SingleQuery_InvalidParam:@"Drive1ParamError.response.txt"]; } - (void)testService_SingleQuery_InvalidParam_ListResult { // Unsuccessful request (invalid page token). // // This is the same as testService_SingleQuery_InvalidParam, expect the response // is what happens if the server sees this as a HTTP Streaming call, meaning the // error will come back in an array. // // Response is file Drive1ParamErrorAsList.response.txt [self performTestService_SingleQuery_InvalidParam:@"Drive1ParamErrorAsList.response.txt"]; } - (void)testService_SingleQuery_InvalidAuth { // Unsuccessful request (invalid auth). // // Response is file Drive1AuthError.response.txt GTLRService *service = [self driveServiceForTest]; service.fetcherService.testBlock = [self fetcherTestBlockWithResponseForFileName:@"Drive1AuthError.response.txt" status:401]; // Request with an auth error. GTLRTestingSvcQuery_FilesList *query = [GTLRTestingSvcQuery_FilesList query]; query.fields = @"kind,nextPageToken,files(id,kind,name)"; query.requestID = @"gtlr_1234"; XCTestExpectation *queryFinished = [self expectationWithDescription:@"queryFinished"]; GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_FileList *object, NSError *error) { XCTAssertNil(object); XCTAssertEqualObjects(error.domain, kGTLRErrorObjectDomain); XCTAssertEqual(error.code, 401); [queryFinished fulfill]; }]; XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)testService_SingleQuery_CorruptResponse { // Successful request with invalid JSON in the response. // // Response is file Drive1Corrupt.response.txt GTLRService *service = [self driveServiceForTest]; service.fetcherService.testBlock = [self fetcherTestBlockWithResponseForFileName:@"Drive1Corrupt.response.txt" status:200]; GTLRTestingSvcQuery_FilesList *query = [GTLRTestingSvcQuery_FilesList query]; query.fields = @"kind,nextPageToken,files(mimeType,id,kind,name)"; query.requestID = @"gtlr_1234"; query.executionParameters.retryBlock = ^(GTLRServiceTicket *ticket, BOOL suggestedWillRetry, NSError * error) { XCTFail(@"No retry expected."); return NO; }; XCTestExpectation *queryFinished = [self expectationWithDescription:@"queryFinished"]; __block GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_FileList *object, NSError *error) { // Verify the top-level object and one of its items. XCTAssertNil(object); // NSPropertyListReadCorruptError = 3840 XCTAssertEqual(error.code, NSPropertyListReadCorruptError, @"%@", error); [queryFinished fulfill]; }]; XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)testService_SingleQuery_ServiceCallbackQueue { [self performCallbackQueueTestUsingExecutionParams:NO]; } - (void)testService_SingleQuery_ExecutionParamsCallbackQueue { [self performCallbackQueueTestUsingExecutionParams:YES]; } - (void)performCallbackQueueTestUsingExecutionParams:(BOOL)useExecutionParams { // Retry once then successful request with callbacks on a specific queue. // // Response is file Drive1.response.txt dispatch_queue_t myCallbackQueue = dispatch_queue_create("myCallbackQueue", DISPATCH_QUEUE_SERIAL); GTLRService *service = [self driveServiceForTest]; service.fetcherService.testBlock = [self fetcherTestBlockWithResponseForFileName:@"Drive1.response.txt" status:456 numberOfStatusErrors:1 multipartBoundary:nil]; GTLRTestingSvcQuery_FilesList *query = [GTLRTestingSvcQuery_FilesList query]; query.fields = @"kind,nextPageToken,files(mimeType,id,kind)"; if (useExecutionParams) { query.executionParameters.callbackQueue = myCallbackQueue; } else { service.callbackQueue = myCallbackQueue; } XCTestExpectation *completionExp = [self expectationWithDescription:@"completionExp"]; XCTestExpectation *retryBlockExp = [self expectationWithDescription:@"retryBlockExp"]; service.retryBlock = ^(GTLRServiceTicket *ticket, BOOL suggestedWillRetry, NSError *error) { XCTAssert(IsCurrentQueue(myCallbackQueue)); [retryBlockExp fulfill]; return YES; }; service.retryEnabled = YES; GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_FileList *object, NSError *error) { XCTAssert(IsCurrentQueue(myCallbackQueue)); [completionExp fulfill]; }]; // Changing the callback queue doesn't affect tickets already issued. service.callbackQueue = dispatch_get_main_queue(); XCTAssert([self service:service waitForTicket:queryTicket]); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)testService_SingleQuery_NoCallbacks { // Successful request but without any callback blocks provided. // // Response is file Drive1.response.txt GTLRService *service = [self driveServiceForTest]; service.fetcherService.testBlock = [self fetcherTestBlockWithResponseForFileName:@"Drive1.response.txt" status:200]; [self expectTicketAndParsingNotifications]; GTLRTestingSvcQuery_FilesList *query = [GTLRTestingSvcQuery_FilesList query]; query.fields = @"kind,nextPageToken,files(mimeType,id,kind,name,webViewLink,thumbnailLink,trashed)"; query.pageSize = 10; query.requestID = @"gtlr_1234"; GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:nil]; XCTAssertFalse(queryTicket.hasCalledCallback); XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)testService_SingleQuery_CancelDuringFetch { // Successful request, canceled before the fetch returns. GTLRService *service = [self driveServiceForTest]; XCTestExpectation *fetchExp = [self expectationWithDescription:@"fetch happened"]; __block GTLRServiceTicket *queryTicket; service.fetcherService.testBlock = ^(GTMSessionFetcher *fetcherToTest, GTMSessionFetcherTestResponse testResponse) { // Cancel the ticket during the fetch. This must be async so the ticket variable is non-nil. dispatch_async(dispatch_get_main_queue(), ^{ XCTAssertNotNil(queryTicket); [queryTicket cancelTicket]; NSHTTPURLResponse *response = QueryResponseWithURL(fetcherToTest.request.URL, 200, @"text/plain"); NSData *responseData = [NSData data]; testResponse(response, responseData, nil); [fetchExp fulfill]; }); }; GTLRTestingSvcQuery_FilesList *query = [GTLRTestingSvcQuery_FilesList query]; query.fields = @"kind,nextPageToken,files(mimeType,id,kind,name)"; query.completionBlock = ^(GTLRServiceTicket *callbackTicket, GTLRObject *callbackObj, NSError *callbackError) { XCTFail(@"Cancel should skip callbacks"); }; query.executionParameters.retryBlock = ^(GTLRServiceTicket *ticket, BOOL suggestedWillRetry, NSError *error) { XCTFail(@"No retry expected."); return NO; }; queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_FileList *object, NSError *error) { XCTFail(@"Cancel should skip callbacks"); }]; XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssertFalse(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)testService_SingleQuery_CancelDuringParse { // Successful request, canceled after the fetch returns. GTLRService *service = [self driveServiceForTest]; XCTestExpectation *parseExp = [self expectationWithDescription:@"parse notification"]; __block GTLRServiceTicket *queryTicket; service.fetcherService.testBlock = [self fetcherTestBlockWithResponseForFileName:@"Drive1.response.txt" status:200]; // Cancel the ticket when we're notified synchronously during parsing. NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; id observer = [nc addObserverForName:@"kGTLRServiceTicketParsingStartedForTestNotification" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) { XCTAssertNotNil(queryTicket); XCTAssertEqualObjects(queryTicket, note.object); [queryTicket cancelTicket]; [parseExp fulfill]; }]; GTLRTestingSvcQuery_FilesList *query = [GTLRTestingSvcQuery_FilesList query]; query.fields = @"kind,nextPageToken,files(mimeType,id,kind,name)"; query.completionBlock = ^(GTLRServiceTicket *callbackTicket, GTLRObject *callbackObj, NSError *callbackError) { XCTFail(@"Cancel should skip callbacks"); }; query.executionParameters.retryBlock = ^(GTLRServiceTicket *ticket, BOOL suggestedWillRetry, NSError *error) { XCTFail(@"No retry expected."); return NO; }; queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_FileList *object, NSError *error) { XCTFail(@"Cancel should skip callbacks"); }]; XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssertFalse(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; [nc removeObserver:observer]; } - (void)testService_Delete { // Successful deletion. // // Response is empty with status 204 "No content" [self expectTicketNotifications]; GTLRService *service = [self driveServiceForTest]; service.fetcherService.testBlock = [self fetcherTestBlockWithResponseForFileName:nil status:204]; GTLRTestingSvcQuery_FilesDelete *query = [GTLRTestingSvcQuery_FilesDelete queryWithFileId:@"1234"]; query.fields = @"kind,nextPageToken"; query.requestID = @"gtlr_1234"; XCTestExpectation *queryFinished = [self expectationWithDescription:@"queryFinished"]; GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRObject *object, NSError *error) { // Verify that there was no error, but no object returned. XCTAssertNil(object); XCTAssertNil(error); [queryFinished fulfill]; }]; XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // File ID should be the tail of the query URL. NSURLRequest *fetcherRequest = queryTicket.objectFetcher.request; XCTAssertEqualObjects(fetcherRequest.URL.lastPathComponent, @"1234"); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)performBodyObjectTestWithQuery:(GTLRTestingSvcQuery_PermissionsCreate *)query expectedBody:(NSDictionary *)expectedJSONBody { XCTestExpectation *fetchExp = [self expectationWithDescription:@"Fetched"]; XCTestExpectation *executeCompletionExp = [self expectationWithDescription:@"Execute block"]; GTLRService *service = [self driveServiceForTest]; service.fetcherService.testBlock = ^(GTMSessionFetcher *fetcherToTest, GTMSessionFetcherTestResponse testResponse) { NSData *fetchBody = fetcherToTest.bodyData; NSJSONSerialization *fetchJSONBody = nil; if (fetchBody) { NSError *parseError; fetchJSONBody = [NSJSONSerialization JSONObjectWithData:fetchBody options:NSJSONReadingMutableContainers error:&parseError]; XCTAssertNil(parseError); } XCTAssertEqualObjects(fetchJSONBody, expectedJSONBody); NSHTTPURLResponse *response = QueryResponseWithURL(fetcherToTest.request.URL, 200, @"text/plain"); testResponse(response, [NSData data], nil); [fetchExp fulfill]; }; GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_File *uploadedFile, NSError *error) { [executeCompletionExp fulfill]; }]; XCTAssert([self service:service waitForTicket:queryTicket]); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)testService_BodyObject { GTLRTestingSvc_Permission *permissionObj = [GTLRTestingSvc_Permission object]; permissionObj.displayName = @"Rex the Tyrannosaurus"; permissionObj.type = @"reader"; GTLRTestingSvcQuery_PermissionsCreate *query = [GTLRTestingSvcQuery_PermissionsCreate queryWithObject:permissionObj fileId:@"12345"]; NSDictionary *expectedJSON = @{ @"type" : @"reader", @"displayName" : @"Rex the Tyrannosaurus" }; [self performBodyObjectTestWithQuery:query expectedBody:expectedJSON]; } - (void)testService_EmptyBodyObject { // Verify that a non-nil but empty body object creates an empty JSON request body. GTLRTestingSvc_Permission *emptyPermissionObj = [GTLRTestingSvc_Permission object]; GTLRTestingSvcQuery_PermissionsCreate *query = [GTLRTestingSvcQuery_PermissionsCreate queryWithObject:emptyPermissionObj fileId:@"12345"]; NSDictionary *expectedJSON = [NSDictionary dictionary]; [self performBodyObjectTestWithQuery:query expectedBody:expectedJSON]; } - (void)testService_NilBodyObject { // Verify that a nil body object creates a nil request body. // We need to force a query to have a nil body object, so create a query with a non-nil // body object and then remove it. GTLRTestingSvc_Permission *emptyPermissionObj = [GTLRTestingSvc_Permission object]; GTLRTestingSvcQuery_PermissionsCreate *query = [GTLRTestingSvcQuery_PermissionsCreate queryWithObject:emptyPermissionObj fileId:@"12345"]; query.bodyObject = nil; [self performBodyObjectTestWithQuery:query expectedBody:nil]; } - (void)testService_SingleQuery_NoFields { // Successful request with valid authorization. // // Response is file Drive1Empty.response GTLRService *service = [self driveServiceForTest]; service.fetcherService.testBlock = [self fetcherTestBlockWithResponseForFileName:@"Drive1Empty.response.txt" status:200]; GTLRTestingSvcQuery_FilesList *query = [GTLRTestingSvcQuery_FilesList query]; query.fields = @""; XCTestExpectation *queryFinished = [self expectationWithDescription:@"queryFinished"]; GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_FileList *object, NSError *error) { // Verify a bare object with no JSON was returned. XCTAssertEqualObjects([object class], [GTLRTestingSvc_FileList class]); XCTAssertNil(object.JSON); XCTAssertNil(error); [queryFinished fulfill]; }]; XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)testService_MediaQuery_UsingMediaRedirect { [self performMediaQueryTestUsingMediaService:NO]; } - (void)testService_MediaQuery_UsingMediaService { [self performMediaQueryTestUsingMediaService:YES]; } - (void)performMediaQueryTestUsingMediaService:(BOOL)useMediaService { // Successful request with data object response. [self expectTicketAndParsingNotifications]; NSData *expectedData = [self tempDataForUploading]; NSString *expectedContentType = @"text/plain"; GTLRService *service = [self driveServiceForTest]; service.fetcherService.testBlock = ^(GTMSessionFetcher *fetcherToTest, GTMSessionFetcherTestResponse testResponse) { NSHTTPURLResponse *response = QueryResponseWithURL(fetcherToTest.request.URL, 200, expectedContentType); NSData *responseData = expectedData; testResponse(response, responseData, nil); }; GTLRTestingSvcQuery_FilesGet *query = [GTLRTestingSvcQuery_FilesGet queryForMediaWithFileId:@"abcde"]; query.useMediaDownloadService = useMediaService; XCTestExpectation *queryFinished = [self expectationWithDescription:@"queryFinished"]; GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRDataObject *object, NSError *error) { NSURL *fetcherURL = callbackTicket.fetchRequest.URL; if (useMediaService) { XCTAssertEqualObjects(fetcherURL.path, @"/download/drive/v3/files/abcde"); } else { XCTAssertEqualObjects(fetcherURL.path, @"/drive/v3/files/abcde"); } XCTAssertEqualObjects(QueryValueForURLItem(fetcherURL, @"alt"), @"media"); XCTAssertEqualObjects([object class], [GTLRDataObject class]); XCTAssertNil(error); XCTAssertEqualObjects(object.data, expectedData); XCTAssertEqualObjects(object.contentType, expectedContentType); XCTAssert([NSThread isMainThread]); [queryFinished fulfill]; }]; XCTAssertFalse(queryTicket.hasCalledCallback); XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)testService_BatchQuery { [self performBatchQueryTestSkippingAuthorization:NO]; } - (void)testService_BatchQuery_SkipAuth { [self performBatchQueryTestSkippingAuthorization:YES]; } - (void)performBatchQueryTestSkippingAuthorization:(BOOL)shouldSkipAuthorization { // Mixed failure and success batch request with valid authorization. // // Response is file Drive1Batch1.response.txt // // WARNING: Batch response file must be saved with CRLF line endings and checked in as a binary // file to be valid multipart MIME. (Edit it with TextWrangler/BBEdit set to "Windows (CRLF)" // for the document.) [self expectTicketAndParsingNotifications]; [self setCountingUIAppWithExpirations:NO]; GTLRService *service = [self driveServiceForTest]; service.fetcherService.testBlock = [self fetcherTestBlockWithResponseForFileName:@"Drive1Batch.response.txt" status:200 numberOfStatusErrors:0 multipartBoundary:@"batch_3ajN40YpXZQ_ABf5ww_gxyg"]; XCTestExpectation *permissionExp = [self expectationWithDescription:@"permissionCallback"]; XCTestExpectation *childExp = [self expectationWithDescription:@"childCallback"]; XCTestExpectation *parentsExp = [self expectationWithDescription:@"parentsCallback"]; XCTestExpectation *batchFinished = [self expectationWithDescription:@"batchFinished"]; GTLRTestingSvcQuery_PermissionsList *permissionQuery = [GTLRTestingSvcQuery_PermissionsList queryWithFileId:@"badID"]; permissionQuery.requestID = @"gtlr_4"; permissionQuery.completionBlock = ^(GTLRServiceTicket *ticket, GTLRTestingSvc_PermissionList *obj, NSError *permissionError) { XCTAssertNil(obj); XCTAssertEqual(permissionError.code, 404); GTLRErrorObject *errorObject = permissionError.userInfo[kGTLRStructuredErrorKey]; XCTAssertEqual(errorObject.code.intValue, 404); XCTAssert([NSThread isMainThread]); [permissionExp fulfill]; }; GTLRTestingSvcQuery_FilesList *childQuery = [GTLRTestingSvcQuery_FilesList query]; childQuery.q = [NSString stringWithFormat:@"'0B7svZDDwtKrhS2FDS2JZclU1U0E' in parents"]; childQuery.requestID = @"gtlr_5"; childQuery.completionBlock = ^(GTLRServiceTicket *ticket, GTLRTestingSvc_FileList *fileList, NSError *childError) { XCTAssertEqualObjects([fileList class], [GTLRTestingSvc_FileList class]); XCTAssertEqualObjects(fileList.kind, @"drive#fileList"); XCTAssertNil(childError); XCTAssert([NSThread isMainThread]); [childExp fulfill]; }; GTLRTestingSvcQuery_FilesGet *parentsQuery = [GTLRTestingSvcQuery_FilesGet queryWithFileId:@"0B7svZDDwtKrhS2FDS2JZclU1U0E"]; parentsQuery.fields = @"parents"; parentsQuery.requestID = @"gtlr_6"; parentsQuery.completionBlock = ^(GTLRServiceTicket *ticket, GTLRTestingSvc_File *file, NSError *parentsError) { XCTAssertEqualObjects([file class], [GTLRTestingSvc_File class]); XCTAssertEqualObjects(file.parents, @[ @"0ALsvZDDwtKrhUk9PVA" ]); XCTAssertNil(parentsError); XCTAssert([NSThread isMainThread]); [parentsExp fulfill]; }; parentsQuery.additionalHTTPHeaders = @{ @"Tiger" : @"Siberian" }; // Combine the separate queries into one batch. GTLRBatchQuery *batchQuery = [GTLRBatchQuery batchQuery]; [batchQuery addQuery:permissionQuery]; [batchQuery addQuery:childQuery]; [batchQuery addQuery:parentsQuery]; // Test adding http headers to the query batchQuery.additionalHTTPHeaders = @{ @"X-Feline": @"Fluffy", @"X-Canine": @"Spot" }; batchQuery.executionParameters.retryBlock = ^(GTLRServiceTicket *ticket, BOOL suggestedWillRetry, NSError *error) { XCTFail(@"No retry expected."); return NO; }; XCTestExpectation *uploadedSomeBytes = [self expectationWithDescription:@"uploadedSomeBytes"]; batchQuery.executionParameters.uploadProgressBlock = ^(GTLRServiceTicket *ticket, unsigned long long numberOfBytesRead, unsigned long long dataLength) { if (numberOfBytesRead == dataLength) { [uploadedSomeBytes fulfill]; } XCTAssert([NSThread isMainThread]); }; batchQuery.shouldSkipAuthorization = shouldSkipAuthorization; __block GTLRServiceTicket *queryTicket = [service executeQuery:batchQuery completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRBatchResult *batchResult, NSError *error) { // Verify the batch result and each success and failure result. XCTAssertEqualObjects([batchResult class], [GTLRBatchResult class]); XCTAssertNil(error); XCTAssertEqual(batchResult.successes.count, 2U); XCTAssertEqual(batchResult.failures.count, 1U); XCTAssertEqual(batchResult.responseHeaders.count, 3U); GTLRErrorObject *failureError = batchResult.failures[@"gtlr_4"]; XCTAssertEqualObjects([failureError class], [GTLRErrorObject class]); XCTAssertEqual(failureError.code.intValue, 404); NSDictionary *responseHeaderGTLR_4 = batchResult.responseHeaders[@"gtlr_4"]; XCTAssertEqualObjects(responseHeaderGTLR_4[@"X-Rejected-Reason"], @"Failed to remove Excalibur from stone"); GTLRTestingSvc_FileList *fileList = batchResult.successes[@"gtlr_5"]; XCTAssertEqualObjects([fileList class], [GTLRTestingSvc_FileList class]); XCTAssertEqualObjects(fileList.kind, @"drive#fileList"); NSDictionary *responseHeaderGTLR_5 = batchResult.responseHeaders[@"gtlr_5"]; XCTAssertEqual(((NSNumber *)responseHeaderGTLR_5[@"Retry-After"]).intValue, 300); GTLRTestingSvc_File *file = batchResult.successes[@"gtlr_6"]; XCTAssertEqualObjects([file class], [GTLRTestingSvc_File class]); XCTAssertEqualObjects(file.parents, @[ @"0ALsvZDDwtKrhUk9PVA" ]); // As with a single query, the batch ticket's query should be a copy of the original; // query execution leaves the original unmolested so the client can modify and reuse it. GTLRBatchQuery *ticketQuery = callbackTicket.executingQuery; XCTAssertNotEqual(ticketQuery, batchQuery); // Since the batch has a copy of the original query, all request IDs of both batch // queries should be the same. XCTAssertEqualObjects([ticketQuery.queries valueForKey:@"requestID"], [batchQuery.queries valueForKey:@"requestID"]); NSDictionary *responseHeaderGTLR_6 = batchResult.responseHeaders[@"gtlr_6"]; XCTAssertNil(responseHeaderGTLR_6[@"X-Rejected-Reason"]); XCTAssertNil(responseHeaderGTLR_6[@"Retry-After"]); XCTAssert([NSThread isMainThread]); [batchFinished fulfill]; }]; XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // The request is to a fixed URL. NSURLRequest *fetcherRequest = queryTicket.objectFetcher.request; XCTAssertEqualObjects(fetcherRequest.URL.absoluteString, @"https://www.googleapis.com/batch"); // Test additionalHTTPHeaders. XCTAssertEqualObjects([fetcherRequest valueForHTTPHeaderField:@"X-Feline"], @"Fluffy"); XCTAssertEqualObjects([fetcherRequest valueForHTTPHeaderField:@"X-Canine"], @"Spot"); // Verify the payload was the expected multipart MIME. NSData *requestBody = fetcherRequest.HTTPBody; NSString *requestBoundary = ((GTLRBatchQuery *)queryTicket.executingQuery).boundary; NSArray *requestMIMEParts = [GTMMIMEDocument MIMEPartsWithBoundary:requestBoundary data:requestBody]; XCTAssertEqual(requestMIMEParts.count, 3U); GTMMIMEDocumentPart *part0 = requestMIMEParts[0]; NSString *body0Str = [[NSString alloc] initWithData:part0.body encoding:NSUTF8StringEncoding]; XCTAssertEqualObjects(part0.headers[@"Content-ID"], @"gtlr_4"); XCTAssert([body0Str hasPrefix:@"GET /drive/v3/files/badID/permissions"], @"%@", body0Str); // Verify the additional HTTP headers were added to the third query. // // We'll parse the request MIME part as a response part since request parts also have outer // and inner headers, similar to response parts. GTMMIMEDocumentPart *part2 = requestMIMEParts[2]; XCTAssertEqualObjects(part2.headers[@"Content-ID"], @"gtlr_6"); GTLRBatchResponsePart *requestPart = [service responsePartWithMIMEPart:part2]; NSDictionary *requestPartHeaders = requestPart.headers; XCTAssertEqualObjects(requestPartHeaders[@"Tiger"], @"Siberian"); NSString *part2BodyStr = [[NSString alloc] initWithData:part2.body encoding:NSUTF8StringEncoding]; NSString *expectedBodyGet = @"GET /drive/v3/files/0B7svZDDwtKrhS2FDS2JZclU1U0E?fields=parents"; XCTAssert([part2BodyStr hasPrefix:expectedBodyGet], @"%@", part2BodyStr); // Verify authorization. id authorizer = service.authorizer; XCTAssertNotNil(authorizer); bool didAuthorize = [authorizer isAuthorizedRequest:fetcherRequest]; XCTAssertEqual(didAuthorize, !shouldSkipAuthorization, @"%@", fetcherRequest.allHTTPHeaderFields); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; // We expect two calls to beginBackgroundTask and endBackgroundTask, one each for // fetcher and GTLRService. [self verifyCountingUIAppWithExpectedCount:2 expectedExpirations:0]; } - (GTMSessionFetcherTestBlock)fetcherTestBlockForBatchPaging { __block int pageCounter = 0; GTMSessionFetcherTestBlock testBlock = ^(GTMSessionFetcher *fetcherToTest, GTMSessionFetcherTestResponse testResponse) { // Not currently done here (like it is above in the batch test) but to be thorough, // we could inspect inside the batch request MIME parts to ensure any additional headers // and query parameters were passed through. ++pageCounter; NSString *contentType = @"multipart/mixed; boundary=batch_3ajN40YpXZQ_ABf5ww_gxyg"; NSHTTPURLResponse *response = QueryResponseWithURL(fetcherToTest.request.URL, 200, contentType); NSString *fileName = [NSString stringWithFormat:@"Drive1BatchPaging%d.response.txt", pageCounter]; NSData *responseData = [[self class] dataForTestFileName:fileName];; XCTAssertNotNil(responseData, @"%@", fileName); testResponse(response, responseData, nil); }; return testBlock; } - (void)testService_BatchQuery_Paging { // WARNING: Batch response file must be saved with CRLF line endings and checked in as a binary // file to be valid multipart MIME. [self expectTicketAndParsingNotifications]; [self setCountingUIAppWithExpirations:NO]; GTLRService *service = [self driveServiceForTest]; service.fetcherService.testBlock = [self fetcherTestBlockForBatchPaging]; XCTestExpectation *query1Exp = [self expectationWithDescription:@"query1Callback"]; XCTestExpectation *query2Exp = [self expectationWithDescription:@"query2Callback"]; XCTestExpectation *query3Exp = [self expectationWithDescription:@"query3Callback"]; XCTestExpectation *query4Exp = [self expectationWithDescription:@"query4Callback"]; XCTestExpectation *batchFinished = [self expectationWithDescription:@"batchFinished"]; // First query succeeds in three pages. // Second query fails on first page. // Third query succeeds on first page but fails on second. // Fourth query succeeds on first page. GTLRTestingSvcQuery_FilesList *childQuery1 = [GTLRTestingSvcQuery_FilesList query]; childQuery1.q = [NSString stringWithFormat:@"'0B7svZDDwtKrhS2FDS2JZclU1U0E' in parents"]; childQuery1.requestID = @"gtlr_5"; childQuery1.completionBlock = ^(GTLRServiceTicket *ticket, GTLRTestingSvc_FileList *fileList, NSError *childError) { XCTAssertEqualObjects([fileList class], [GTLRTestingSvc_FileList class]); XCTAssertEqualObjects(fileList.kind, @"drive#fileList"); XCTAssertEqual(fileList.files.count, (NSUInteger)8); XCTAssertEqualObjects(fileList.files[7].name, @"Mouse Types.pdf"); XCTAssertEqualObjects(fileList.files[7].identifier, @"0B7svZDDwtKr1BVVk3N1k"); XCTAssertNil(childError); XCTAssert([NSThread isMainThread]); [query1Exp fulfill]; }; GTLRTestingSvcQuery_FilesList *childQuery2 = [GTLRTestingSvcQuery_FilesList query]; childQuery2.q = [NSString stringWithFormat:@"'0B7svZABCtKrhS2FDS2JZclU1U0E' in parents"]; childQuery2.requestID = @"gtlr_6"; childQuery2.completionBlock = ^(GTLRServiceTicket *ticket, GTLRTestingSvc_FileList *fileList, NSError *childError) { XCTAssertNil(fileList); XCTAssertEqual(childError.code, 404); XCTAssert([NSThread isMainThread]); [query2Exp fulfill]; }; GTLRTestingSvcQuery_FilesList *childQuery3 = [GTLRTestingSvcQuery_FilesList query]; childQuery3.q = [NSString stringWithFormat:@"'0B7svZDEFtKrhS2FDS2JZclU1U0E' in parents"]; childQuery3.requestID = @"gtlr_7"; childQuery3.completionBlock = ^(GTLRServiceTicket *ticket, GTLRTestingSvc_FileList *fileList, NSError *childError) { XCTAssertNil(fileList); XCTAssertEqual(childError.code, 400); XCTAssert([NSThread isMainThread]); [query3Exp fulfill]; }; GTLRTestingSvcQuery_FilesGet *parentsQuery = [GTLRTestingSvcQuery_FilesGet queryWithFileId:@"0B7svZDDwtKrhS2FDS2JZclU1U0E"]; parentsQuery.fields = @"parents"; parentsQuery.requestID = @"gtlr_8"; parentsQuery.completionBlock = ^(GTLRServiceTicket *ticket, GTLRTestingSvc_File *file, NSError *parentsError) { XCTAssertEqualObjects([file class], [GTLRTestingSvc_File class]); XCTAssertEqualObjects(file.parents, @[ @"0ALsvZDDwtKrhUk9PVA" ]); XCTAssertNil(parentsError); XCTAssert([NSThread isMainThread]); [query4Exp fulfill]; }; // Combine the separate queries into one batch. NSArray *queries = @[ childQuery1, childQuery2, childQuery3, parentsQuery ]; GTLRBatchQuery *batchQuery = [GTLRBatchQuery batchQueryWithQueries:queries]; batchQuery.executionParameters.shouldFetchNextPages = @YES; GTLRServiceTicket *queryTicket = [service executeQuery:batchQuery completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRBatchResult *batchResult, NSError *error) { // Verify the batch result and each success and failure result. XCTAssertEqualObjects([batchResult class], [GTLRBatchResult class]); XCTAssertNil(error); XCTAssertEqual(batchResult.successes.count, (NSUInteger)2); XCTAssertEqual(batchResult.failures.count, (NSUInteger)2); XCTAssertEqual(batchResult.responseHeaders.count, (NSUInteger)4); XCTAssertEqual(callbackTicket.pagesFetchedCounter, 3U); // The first query, gtlr_5, requires all three pages to fetch the full file list. GTLRTestingSvc_FileList *fileList = batchResult.successes[@"gtlr_5"]; XCTAssertEqualObjects([fileList class], [GTLRTestingSvc_FileList class]); XCTAssertEqualObjects(fileList.kind, @"drive#fileList"); XCTAssertEqual(fileList.files.count, (NSUInteger)8); GTLRErrorObject *errorObj = batchResult.failures[@"gtlr_6"]; errorObj.code = @404; errorObj = batchResult.failures[@"gtlr_7"]; errorObj.code = @400; GTLRTestingSvc_File *file = batchResult.successes[@"gtlr_8"]; XCTAssertEqualObjects([file class], [GTLRTestingSvc_File class]); XCTAssertEqualObjects(file.parents, @[ @"0ALsvZDDwtKrhUk9PVA" ]); // As with a single query, the batch ticket's query should be a copy of the original; // query execution leaves the original unmolested so the client can modify and reuse it. GTLRBatchQuery *ticketQuery = callbackTicket.executingQuery; XCTAssertNotEqual(ticketQuery, batchQuery); // Since the batch has a copy of the original query, all request IDs of both batch // queries should be the same. XCTAssertEqualObjects([ticketQuery.queries valueForKey:@"requestID"], [batchQuery.queries valueForKey:@"requestID"]); XCTAssert([NSThread isMainThread]); [batchFinished fulfill]; }]; XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; // We expect six calls to beginBackgroundTask and endBackgroundTask, one each for // fetcher and GTLRService for each page. [self verifyCountingUIAppWithExpectedCount:6 expectedExpirations:0]; } - (void)testService_BatchQuery_CorruptResponse { // Mixed failure and success batch request with on corrupt JSON resonse. // // Response is file Drive1BatchCorrupt.response.txt // // WARNING: Batch response file must be saved with CRLF line endings and checked in as a binary // file to be valid multipart MIME. (Edit it with TextWrangler/BBEdit set to "Windows (CRLF)" // for the document.) GTLRService *service = [self driveServiceForTest]; service.fetcherService.testBlock = [self fetcherTestBlockWithResponseForFileName:@"Drive1BatchCorrupt.response.txt" status:200 numberOfStatusErrors:0 multipartBoundary:@"batch_3ajN40YpXZQ_ABf5ww_gxyg"]; XCTestExpectation *permissionExp = [self expectationWithDescription:@"permissionCallback"]; XCTestExpectation *childExp = [self expectationWithDescription:@"childCallback"]; XCTestExpectation *parentsExp = [self expectationWithDescription:@"parentsCallback"]; XCTestExpectation *batchFinished = [self expectationWithDescription:@"batchFinished"]; GTLRTestingSvcQuery_PermissionsList *permissionQuery = [GTLRTestingSvcQuery_PermissionsList queryWithFileId:@"badID"]; permissionQuery.requestID = @"gtlr_4"; permissionQuery.completionBlock = ^(GTLRServiceTicket *ticket, GTLRTestingSvc_PermissionList *obj, NSError *permissionError) { XCTAssertNil(obj); XCTAssertEqual(permissionError.code, 404); GTLRErrorObject *errorObject = permissionError.userInfo[kGTLRStructuredErrorKey]; XCTAssertEqual(errorObject.code.intValue, 404); [permissionExp fulfill]; }; // The second part has corrupt JSON. GTLRTestingSvcQuery_FilesList *childQuery = [GTLRTestingSvcQuery_FilesList query]; childQuery.q = [NSString stringWithFormat:@"'0B7svZDDwtKrhS2FDS2JZclU1U0E' in parents"]; childQuery.requestID = @"gtlr_5"; childQuery.completionBlock = ^(GTLRServiceTicket *ticket, GTLRTestingSvc_FileList *fileList, NSError *childError) { XCTAssertNil(fileList); XCTAssertEqual(childError.code, NSPropertyListReadCorruptError); [childExp fulfill]; }; GTLRTestingSvcQuery_FilesGet *parentsQuery = [GTLRTestingSvcQuery_FilesGet queryWithFileId:@"0B7svZDDwtKrhS2FDS2JZclU1U0E"]; parentsQuery.fields = @"parents"; parentsQuery.requestID = @"gtlr_6"; parentsQuery.completionBlock = ^(GTLRServiceTicket *ticket, GTLRTestingSvc_File *file, NSError *parentsError) { XCTAssertEqualObjects([file class], [GTLRTestingSvc_File class]); XCTAssertEqualObjects(file.parents, @[ @"0ALsvZDDwtKrhUk9PVA" ]); XCTAssertNil(parentsError); [parentsExp fulfill]; }; // Combine the separate queries into one batch. GTLRBatchQuery *batchQuery = [GTLRBatchQuery batchQuery]; [batchQuery addQuery:permissionQuery]; [batchQuery addQuery:childQuery]; [batchQuery addQuery:parentsQuery]; batchQuery.executionParameters.retryBlock = ^(GTLRServiceTicket *ticket, BOOL suggestedWillRetry, NSError *error) { XCTFail(@"No retry expected."); return NO; }; __block GTLRServiceTicket *queryTicket = [service executeQuery:batchQuery completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRBatchResult *batchResult, NSError *error) { // Verify the batch result and each success and failure result. XCTAssertEqualObjects([batchResult class], [GTLRBatchResult class]); XCTAssertNil(error); XCTAssertEqual(batchResult.successes.count, 1U); XCTAssertEqual(batchResult.failures.count, 2U); XCTAssertEqual(batchResult.responseHeaders.count, 3U); GTLRErrorObject *failureError = batchResult.failures[@"gtlr_4"]; XCTAssertEqualObjects([failureError class], [GTLRErrorObject class]); XCTAssertEqual(failureError.code.intValue, 404); // This object has corrupt JSON. GTLRErrorObject *fileListError = batchResult.failures[@"gtlr_5"]; XCTAssertEqual(fileListError.code.intValue, NSPropertyListReadCorruptError); GTLRTestingSvc_File *file = batchResult.successes[@"gtlr_6"]; XCTAssertEqualObjects([file class], [GTLRTestingSvc_File class]); XCTAssertEqualObjects(file.parents, @[ @"0ALsvZDDwtKrhUk9PVA" ]); // As with a single query, the batch ticket's query should be a copy of the original; // query execution leaves the original unmolested so the client can modify and reuse it. GTLRBatchQuery *ticketQuery = callbackTicket.executingQuery; XCTAssertNotEqual(ticketQuery, batchQuery); // Since the batch has a copy of the original query, all request IDs of both batch // queries should be the same. XCTAssertEqualObjects([ticketQuery.queries valueForKey:@"requestID"], [batchQuery.queries valueForKey:@"requestID"]); [batchFinished fulfill]; }]; XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)testService_BatchQuery_CallbackQueue { // Batch with callbacks on a specified GCD queue. // // Response is file Drive1Batch1.response.txt dispatch_queue_t myCallbackQueue = dispatch_queue_create("myCallbackQueue", DISPATCH_QUEUE_SERIAL); GTLRService *service = [self driveServiceForTest]; service.callbackQueue = myCallbackQueue; service.fetcherService.testBlock = [self fetcherTestBlockWithResponseForFileName:@"Drive1Batch.response.txt" status:200 numberOfStatusErrors:0 multipartBoundary:@"batch_3ajN40YpXZQ_ABf5ww_gxyg"]; XCTestExpectation *permissionExp = [self expectationWithDescription:@"permissionCallback"]; XCTestExpectation *childExp = [self expectationWithDescription:@"childCallback"]; XCTestExpectation *parentsExp = [self expectationWithDescription:@"parentsCallback"]; XCTestExpectation *batchFinished = [self expectationWithDescription:@"batchFinished"]; GTLRTestingSvcQuery_PermissionsList *permissionQuery = [GTLRTestingSvcQuery_PermissionsList queryWithFileId:@"badID"]; permissionQuery.requestID = @"gtlr_4"; permissionQuery.completionBlock = ^(GTLRServiceTicket *ticket, GTLRTestingSvc_PermissionList *obj, NSError *permissionError) { XCTAssert(IsCurrentQueue(myCallbackQueue)); [permissionExp fulfill]; }; GTLRTestingSvcQuery_FilesList *childQuery = [GTLRTestingSvcQuery_FilesList query]; childQuery.q = [NSString stringWithFormat:@"'0B7svZDDwtKrhS2FDS2JZclU1U0E' in parents"]; childQuery.requestID = @"gtlr_5"; childQuery.completionBlock = ^(GTLRServiceTicket *ticket, GTLRTestingSvc_FileList *fileList, NSError *childError) { XCTAssert(IsCurrentQueue(myCallbackQueue)); [childExp fulfill]; }; GTLRTestingSvcQuery_FilesGet *parentsQuery = [GTLRTestingSvcQuery_FilesGet queryWithFileId:@"0B7svZDDwtKrhS2FDS2JZclU1U0E"]; parentsQuery.fields = @"parents"; parentsQuery.requestID = @"gtlr_6"; parentsQuery.completionBlock = ^(GTLRServiceTicket *ticket, GTLRTestingSvc_File *file, NSError *parentsError) { XCTAssert(IsCurrentQueue(myCallbackQueue)); [parentsExp fulfill]; }; // Combine the separate queries into one batch. GTLRBatchQuery *batchQuery = [GTLRBatchQuery batchQuery]; [batchQuery addQuery:permissionQuery]; [batchQuery addQuery:childQuery]; [batchQuery addQuery:parentsQuery]; // Test adding http headers to the query batchQuery.additionalHTTPHeaders = @{ @"X-Feline": @"Fluffy", @"X-Canine": @"Spot" }; batchQuery.executionParameters.retryBlock = ^(GTLRServiceTicket *ticket, BOOL suggestedWillRetry, NSError *error) { XCTFail(@"No retry expected."); return NO; }; XCTestExpectation *uploadedSomeBytes = [self expectationWithDescription:@"uploadedSomeBytes"]; batchQuery.executionParameters.uploadProgressBlock = ^(GTLRServiceTicket *ticket, unsigned long long numberOfBytesRead, unsigned long long dataLength) { if (numberOfBytesRead == dataLength) { [uploadedSomeBytes fulfill]; } XCTAssert(IsCurrentQueue(myCallbackQueue)); }; __block GTLRServiceTicket *queryTicket = [service executeQuery:batchQuery completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRBatchResult *batchResult, NSError *error) { XCTAssert(IsCurrentQueue(myCallbackQueue)); [batchFinished fulfill]; }]; // Changing the callback queue doesn't affect tickets already issued. service.callbackQueue = dispatch_get_main_queue(); XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)testService_SingleQuery_Surrogates { // Successful request with surrogates. // // Response is file Drive1.response.txt GTLRService *service = [self driveServiceForTest]; service.fetcherService.testBlock = [self fetcherTestBlockWithResponseForFileName:@"Drive1.response.txt" status:200]; GTLRTestingSvcQuery_FilesList *query = [GTLRTestingSvcQuery_FilesList query]; query.fields = @"kind,nextPageToken,files(mimeType,id,kind)"; NSDictionary *serviceSurrogates = @{ (id)[GTLRTestingSvc_File class] : [GTLRTestingSvc_File_Surrogate class], (id)[GTLRTestingSvc_FileList class] : [GTLRTestingSvc_FileList_Surrogate class] }; [service setSurrogates:serviceSurrogates]; XCTestExpectation *queryFinished = [self expectationWithDescription:@"queryFinished"]; // Override a resolver in the query's executionParameters. NSDictionary *querySurrogates = @{ (id)[GTLRTestingSvc_File class] : [GTLRTestingSvc_File_Surrogate class], (id)[GTLRTestingSvc_FileList class] : [GTLRTestingSvc_FileList_Surrogate2 class] }; NSDictionary *kindMap = [[service class] kindStringToClassMap]; query.executionParameters.objectClassResolver = [GTLRObjectClassResolver resolverWithKindMap:kindMap surrogates:querySurrogates]; __block GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_FileList *object, NSError *error) { // Verify the top-level object and one of its items. XCTAssertEqualObjects([object class], [GTLRTestingSvc_FileList_Surrogate2 class]); XCTAssertNil(error); XCTAssertEqualObjects(object.kind, @"drive#fileList"); XCTAssertEqual(object.files.count, 2U, @"%@", object.files); [queryFinished fulfill]; }]; XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)testService_BatchQuery_Surrogates { // Mixed failure and success batch request with surrogates. // // Response is file Drive1Batch1.response.txt // // See warning above about batch file needing CRLF line endings. GTLRService *service = [self driveServiceForTest]; service.fetcherService.testBlock = [self fetcherTestBlockWithResponseForFileName:@"Drive1Batch.response.txt" status:200 numberOfStatusErrors:0 multipartBoundary:@"batch_3ajN40YpXZQ_ABf5ww_gxyg"]; XCTestExpectation *batchFinished = [self expectationWithDescription:@"batchFinished"]; GTLRTestingSvcQuery_PermissionsList *permissionQuery = [GTLRTestingSvcQuery_PermissionsList queryWithFileId:@"badID"]; permissionQuery.requestID = @"gtlr_4"; GTLRTestingSvcQuery_FilesList *childQuery = [GTLRTestingSvcQuery_FilesList query]; childQuery.q = [NSString stringWithFormat:@"'0B7svZDDwtKrhS2FDS2JZclU1U0E' in parents"]; childQuery.requestID = @"gtlr_5"; GTLRTestingSvcQuery_FilesGet *parentsQuery = [GTLRTestingSvcQuery_FilesGet queryWithFileId:@"0B7svZDDwtKrhS2FDS2JZclU1U0E"]; parentsQuery.fields = @"parents"; parentsQuery.requestID = @"gtlr_6"; // Combine the separate queries into one batch. GTLRBatchQuery *batchQuery = [GTLRBatchQuery batchQuery]; [batchQuery addQuery:permissionQuery]; [batchQuery addQuery:childQuery]; [batchQuery addQuery:parentsQuery]; NSDictionary *kindMap = [[service class] kindStringToClassMap]; NSDictionary *surrogates = @{ (id)[GTLRTestingSvc_File class] : [GTLRTestingSvc_File_Surrogate class], (id)[GTLRTestingSvc_FileList class] : [GTLRTestingSvc_FileList_Surrogate class] }; batchQuery.executionParameters.objectClassResolver = [GTLRObjectClassResolver resolverWithKindMap:kindMap surrogates:surrogates]; __block GTLRServiceTicket *queryTicket = [service executeQuery:batchQuery completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRBatchResult *batchResult, NSError *error) { // Verify the batch result and each success and failure result. XCTAssertEqualObjects([batchResult class], [GTLRBatchResult class]); XCTAssertNil(error); XCTAssertEqual(batchResult.successes.count, 2U); XCTAssertEqual(batchResult.failures.count, 1U); XCTAssertEqual(batchResult.responseHeaders.count, 3U); GTLRErrorObject *failureError = batchResult.failures[@"gtlr_4"]; XCTAssertEqualObjects([failureError class], [GTLRErrorObject class]); XCTAssertEqual(failureError.code.intValue, 404); GTLRTestingSvc_FileList *fileList = batchResult.successes[@"gtlr_5"]; XCTAssertEqualObjects([fileList class], [GTLRTestingSvc_FileList_Surrogate class]); XCTAssertEqualObjects(fileList.kind, @"drive#fileList"); GTLRTestingSvc_File *file = batchResult.successes[@"gtlr_6"]; XCTAssertEqualObjects([file class], [GTLRTestingSvc_File_Surrogate class]); XCTAssertEqualObjects(file.parents, @[ @"0ALsvZDDwtKrhUk9PVA" ]); // As with a single query, the batch ticket's query should be a copy of the original; // query execution leaves the original unmolested so the client can modify and reuse it. GTLRBatchQuery *ticketQuery = callbackTicket.executingQuery; XCTAssertNotEqual(ticketQuery, batchQuery); // Since the batch has a copy of the original query, all request IDs of both batch // queries should be the same. XCTAssertEqualObjects([ticketQuery.queries valueForKey:@"requestID"], [batchQuery.queries valueForKey:@"requestID"]); [batchFinished fulfill]; }]; XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)testService_SingleQuery_RetrySucceeding { // Successful request with retry. // // Response is file Drive1.response.txt GTLRService *service = [self driveServiceForTest]; [self setCountingUIAppWithExpirations:NO]; service.fetcherService.testBlock = [self fetcherTestBlockWithResponseForFileName:@"Drive1.response.txt" status:456 numberOfStatusErrors:1 multipartBoundary:nil]; GTLRTestingSvcQuery_FilesList *query = [GTLRTestingSvcQuery_FilesList query]; query.fields = @"kind,nextPageToken,files(mimeType,id,kind)"; XCTestExpectation *queryFinished = [self expectationWithDescription:@"queryFinished"]; XCTestExpectation *retryBlockExp = [self expectationWithDescription:@"retryBlock"]; service.retryBlock = ^(GTLRServiceTicket *ticket, BOOL suggestedWillRetry, NSError *error) { XCTAssertEqual(error.code, 456); XCTAssertEqual(suggestedWillRetry, NO); XCTAssert([NSThread isMainThread]); [retryBlockExp fulfill]; return YES; }; service.retryEnabled = YES; GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_FileList *object, NSError *error) { // Verify the top-level object and one of its items. XCTAssertEqualObjects([object class], [GTLRTestingSvc_FileList class]); XCTAssertNil(error); XCTAssertEqualObjects(object.kind, @"drive#fileList"); XCTAssertEqual(object.files.count, 2U, @"%@", object.files); [queryFinished fulfill]; }]; XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; // We expect three calls to beginBackgroundTask and endBackgroundTask, two for // fetchers and one for GTLRService. [self verifyCountingUIAppWithExpectedCount:3 expectedExpirations:0]; } - (void)testService_SingleQuery_RetryFailing { // Failed request with a retry attempt. // // Response is file Drive1.response.txt GTLRService *service = [self driveServiceForTest]; [self setCountingUIAppWithExpirations:NO]; service.fetcherService.testBlock = [self fetcherTestBlockWithResponseForFileName:nil status:456 numberOfStatusErrors:100 multipartBoundary:nil]; GTLRTestingSvcQuery_FilesList *query = [GTLRTestingSvcQuery_FilesList query]; query.fields = @"kind,nextPageToken,files(mimeType,id,kind)"; XCTestExpectation *queryFinished = [self expectationWithDescription:@"queryFinished"]; XCTestExpectation *retryBlockExp = [self expectationWithDescription:@"retryBlock"]; service.retryBlock = ^(GTLRServiceTicket *ticket, BOOL suggestedWillRetry, NSError *error) { XCTAssertEqual(error.code, 456); XCTAssertEqual(suggestedWillRetry, NO); if (ticket.objectFetcher.retryCount == 0) { return YES; } // Give up after one retry. [retryBlockExp fulfill]; return NO; }; service.retryEnabled = YES; GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_FileList *object, NSError *error) { XCTAssertNil(object); XCTAssertEqual(error.code, 456); [queryFinished fulfill]; }]; XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; // We expect three calls to beginBackgroundTask and endBackgroundTask, two for // fetchers and one for GTLRService. [self verifyCountingUIAppWithExpectedCount:3 expectedExpirations:0]; } #pragma mark - Lifetime Tests - (void)testService_SingleQuery_Retry_ObjectLifetimes { // This test is based on a fallacy, that we can assume objects are dealloc'd at // a certain point in time, // // If this test turns out to be a source of headaches, we can just give up and do // these checks manually on occasion. XCTestExpectation *ticketDealloc = [self expectationWithDescription:@"ticketDealloc"]; XCTestExpectation *initialQueryDealloc = [self expectationWithDescription:@"initialQueryDealloc"]; XCTestExpectation *executedQueryDealloc = [self expectationWithDescription:@"execdQueryDealloc"]; XCTestExpectation *resultObjectDealloc = [self expectationWithDescription:@"resultObjectDealloc"]; XCTestExpectation *fetcherDealloc = [self expectationWithDescription:@"fetcherDealloc"]; @autoreleasepool { // Successful request with one retry. GTLRService *service = [self driveServiceForTest]; service.fetcherService.testBlock = [self fetcherTestBlockWithResponseForFileName:@"Drive1.response.txt" status:456 numberOfStatusErrors:1 multipartBoundary:nil]; GTLRTestingSvcQuery_FilesList *query = [GTLRTestingSvcQuery_FilesList query]; query.fields = @"kind,nextPageToken,files(mimeType,id,kind)"; // Create a retain cycle in the query callback. id queryHolder = query; query.completionBlock = ^(GTLRServiceTicket *callbackTicket, id _Nullable object, NSError * _Nullable callbackError) { XCTAssertNotNil(queryHolder); }; XCTestExpectation *queryFinished = [self expectationWithDescription:@"queryFinished"]; XCTestExpectation *retryBlockExp = [self expectationWithDescription:@"retryBlock"]; service.retryBlock = ^(GTLRServiceTicket *ticket, BOOL suggestedWillRetry, NSError *error) { [retryBlockExp fulfill]; return YES; }; service.retryEnabled = YES; [GTLRTestLifetimeObject trackLifetimeOfObject:query expectation:initialQueryDealloc]; __block GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_FileList *resultObject, NSError *error) { // Retain the original query and original ticket in the completion handler // to try to create a cycle. XCTAssertNotNil(query); XCTAssertNotNil(queryTicket); [GTLRTestLifetimeObject trackLifetimeOfObject:callbackTicket expectation:ticketDealloc]; [GTLRTestLifetimeObject trackLifetimeOfObject:callbackTicket.executingQuery expectation:executedQueryDealloc]; [GTLRTestLifetimeObject trackLifetimeOfObject:resultObject expectation:resultObjectDealloc]; [GTLRTestLifetimeObject trackLifetimeOfObject:callbackTicket.objectFetcher expectation:fetcherDealloc]; [queryFinished fulfill]; }]; XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); } // @autoreleasepool // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)testService_BatchQuery_Paging_Lifetime { // Object lifetime expectations. XCTestExpectation *ticketDealloc = [self expectationWithDescription:@"ticketDealloc"]; XCTestExpectation *initialBatchQueryDealloc = [self expectationWithDescription:@"initialBatchQueryDealloc"]; XCTestExpectation *initialQuery1Dealloc = [self expectationWithDescription:@"initialQuery1Dealloc"]; XCTestExpectation *initialQuery2Dealloc = [self expectationWithDescription:@"initialQuery2Dealloc"]; XCTestExpectation *initialQuery3Dealloc = [self expectationWithDescription:@"initialQuery3Dealloc"]; XCTestExpectation *initialQuery4Dealloc = [self expectationWithDescription:@"initialQuery4Dealloc"]; XCTestExpectation *executedBatchQueryDealloc = [self expectationWithDescription:@"execdBatchQueryDealloc"]; XCTestExpectation *executedQuery1Dealloc = [self expectationWithDescription:@"execdQuery1Dealloc"]; XCTestExpectation *executedQuery2Dealloc = [self expectationWithDescription:@"execdQuery2Dealloc"]; XCTestExpectation *executedQuery3Dealloc = [self expectationWithDescription:@"execdQuery3Dealloc"]; XCTestExpectation *executedQuery4Dealloc = [self expectationWithDescription:@"execdQuery4Dealloc"]; XCTestExpectation *resultObjectDealloc = [self expectationWithDescription:@"resultObjectDealloc"]; XCTestExpectation *fetcherDealloc = [self expectationWithDescription:@"fetcherDealloc"]; @autoreleasepool { __block GTLRServiceTicket *queryTicket; GTLRService *service = [self driveServiceForTest]; service.fetcherService.testBlock = [self fetcherTestBlockForBatchPaging]; // Callback expectations. XCTestExpectation *query1Exp = [self expectationWithDescription:@"query1Callback"]; XCTestExpectation *query2Exp = [self expectationWithDescription:@"query2Callback"]; XCTestExpectation *query3Exp = [self expectationWithDescription:@"query3Callback"]; XCTestExpectation *query4Exp = [self expectationWithDescription:@"query4Callback"]; XCTestExpectation *batchFinished = [self expectationWithDescription:@"batchFinished"]; // First query succeeds in three pages. // Second query fails on first page. // Third query succeeds on first page but fails on second. // Fourth query succeeds on first page. GTLRTestingSvcQuery_FilesList *childQuery1 = [GTLRTestingSvcQuery_FilesList query]; childQuery1.q = [NSString stringWithFormat:@"'0B7svZDDwtKrhS2FDS2JZclU1U0E' in parents"]; childQuery1.requestID = @"gtlr_5"; childQuery1.completionBlock = ^(GTLRServiceTicket *ticket, GTLRTestingSvc_FileList *fileList, NSError *childError) { XCTAssertNotNil(fileList); XCTAssertNil(childError); // Force a reference to the original ticket. XCTAssertNotNil(queryTicket); [query1Exp fulfill]; }; GTLRTestingSvcQuery_FilesList *childQuery2 = [GTLRTestingSvcQuery_FilesList query]; childQuery2.q = [NSString stringWithFormat:@"'0B7svZABCtKrhS2FDS2JZclU1U0E' in parents"]; childQuery2.requestID = @"gtlr_6"; childQuery2.completionBlock = ^(GTLRServiceTicket *ticket, GTLRTestingSvc_FileList *fileList, NSError *childError) { XCTAssertNil(fileList); XCTAssertNotNil(childError); [query2Exp fulfill]; }; GTLRTestingSvcQuery_FilesList *childQuery3 = [GTLRTestingSvcQuery_FilesList query]; childQuery3.q = [NSString stringWithFormat:@"'0B7svZDEFtKrhS2FDS2JZclU1U0E' in parents"]; childQuery3.requestID = @"gtlr_7"; childQuery3.completionBlock = ^(GTLRServiceTicket *ticket, GTLRTestingSvc_FileList *fileList, NSError *childError) { XCTAssertNil(fileList); XCTAssertNotNil(childError); [query3Exp fulfill]; }; GTLRTestingSvcQuery_FilesGet *parentsQuery = [GTLRTestingSvcQuery_FilesGet queryWithFileId:@"0B7svZDDwtKrhS2FDS2JZclU1U0E"]; parentsQuery.fields = @"parents"; parentsQuery.requestID = @"gtlr_8"; parentsQuery.completionBlock = ^(GTLRServiceTicket *ticket, GTLRTestingSvc_File *file, NSError *parentsError) { XCTAssertNotNil(file); XCTAssertNil(parentsError); [query4Exp fulfill]; }; [GTLRTestLifetimeObject trackLifetimeOfObject:childQuery1 expectation:initialQuery1Dealloc]; [GTLRTestLifetimeObject trackLifetimeOfObject:childQuery2 expectation:initialQuery2Dealloc]; [GTLRTestLifetimeObject trackLifetimeOfObject:childQuery3 expectation:initialQuery3Dealloc]; [GTLRTestLifetimeObject trackLifetimeOfObject:parentsQuery expectation:initialQuery4Dealloc]; // Combine the separate queries into one batch. NSArray *queries = @[ childQuery1, childQuery2, childQuery3, parentsQuery ]; GTLRBatchQuery *batchQuery = [GTLRBatchQuery batchQueryWithQueries:queries]; batchQuery.executionParameters.shouldFetchNextPages = @YES; [GTLRTestLifetimeObject trackLifetimeOfObject:batchQuery expectation:initialBatchQueryDealloc]; queryTicket = [service executeQuery:batchQuery completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRBatchResult *batchResult, NSError *error) { // Force a reference to the original ticket. XCTAssertNotNil(queryTicket); // Verify the batch result and each success and failure result. XCTAssertEqualObjects([batchResult class], [GTLRBatchResult class]); XCTAssertNil(error); XCTAssertEqual(batchResult.successes.count, (NSUInteger)2); XCTAssertEqual(batchResult.failures.count, (NSUInteger)2); XCTAssertEqual(callbackTicket.pagesFetchedCounter, 3U); GTLRTestingSvc_FileList *fileList = batchResult.successes[@"gtlr_5"]; XCTAssertEqual(fileList.files.count, (NSUInteger)8); [GTLRTestLifetimeObject trackLifetimeOfObject:callbackTicket expectation:ticketDealloc]; // The ticket has a copy of the original batch query, which has copies of the individual // queries. GTLRBatchQuery *ticketQuery = callbackTicket.executingQuery; [GTLRTestLifetimeObject trackLifetimeOfObject:ticketQuery expectation:executedBatchQueryDealloc]; [GTLRTestLifetimeObject trackLifetimeOfObject:[ticketQuery queryForRequestID:@"gtlr_5"] expectation:executedQuery1Dealloc]; [GTLRTestLifetimeObject trackLifetimeOfObject:[ticketQuery queryForRequestID:@"gtlr_6"] expectation:executedQuery2Dealloc]; [GTLRTestLifetimeObject trackLifetimeOfObject:[ticketQuery queryForRequestID:@"gtlr_7"] expectation:executedQuery3Dealloc]; [GTLRTestLifetimeObject trackLifetimeOfObject:[ticketQuery queryForRequestID:@"gtlr_8"] expectation:executedQuery4Dealloc]; [GTLRTestLifetimeObject trackLifetimeOfObject:batchResult expectation:resultObjectDealloc]; [GTLRTestLifetimeObject trackLifetimeOfObject:callbackTicket.objectFetcher expectation:fetcherDealloc]; [batchFinished fulfill]; }]; XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); } // @autoreleasepool // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } #pragma mark - Upload Tests - (NSData *)tempDataForUploading { NSMutableString *string = [NSMutableString string]; for (int index = 0; index < 10000; index++) { [string appendFormat:@"%c", 'A' + (index % 26)]; } return [string dataUsingEncoding:NSUTF8StringEncoding]; } - (NSURL *)tempFileURLForUploading { // Write a file that we can test uploading. NSURL *tempDir = [NSURL fileURLWithPath:NSTemporaryDirectory()]; NSURL *tempFileURL = [tempDir URLByAppendingPathComponent:@"tempFileURLForUploading" isDirectory:NO]; NSError *writeError; NSData *data = [self tempDataForUploading]; BOOL didWrite = [data writeToURL:tempFileURL options:NSDataWritingAtomic error:&writeError]; XCTAssert(didWrite, @"%@", writeError); if (didWrite) { _tempFileURLToDelete = tempFileURL; } return tempFileURL; } - (void)testService_Upload_FileHandle { NSURL *fileURL = [self tempFileURLForUploading]; NSError *readingError; NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingFromURL:fileURL error:&readingError]; XCTAssertNotNil(fileHandle, @"%@", readingError); GTLRUploadParameters *uploadParameters = [GTLRUploadParameters uploadParametersWithFileHandle:fileHandle MIMEType:@"text/plain"]; uploadParameters.useBackgroundSession = NO; [self performServiceUploadTestWithParameters:uploadParameters]; } - (void)testService_Upload_FileURL { NSURL *fileURL = [self tempFileURLForUploading]; NSError *fileError; XCTAssert([fileURL checkResourceIsReachableAndReturnError:&fileError], @"%@", fileError); GTLRUploadParameters *uploadParameters = [GTLRUploadParameters uploadParametersWithFileURL:fileURL MIMEType:@"text/plain"]; uploadParameters.useBackgroundSession = YES; [self performServiceUploadTestWithParameters:uploadParameters]; } - (void)testService_Upload_FileURL_WithoutMetadata { NSURL *fileURL = [self tempFileURLForUploading]; NSError *fileError; XCTAssert([fileURL checkResourceIsReachableAndReturnError:&fileError], @"%@", fileError); GTLRUploadParameters *uploadParameters = [GTLRUploadParameters uploadParametersWithFileURL:fileURL MIMEType:@"text/plain"]; uploadParameters.shouldSendUploadOnly = YES; uploadParameters.useBackgroundSession = YES; [self performServiceUploadTestWithParameters:uploadParameters]; } - (void)testService_Upload_NSData { NSData *uploadData = [self tempDataForUploading]; XCTAssert(uploadData.length > 1000); GTLRUploadParameters *uploadParameters = [GTLRUploadParameters uploadParametersWithData:uploadData MIMEType:@"text/plain"]; uploadParameters.useBackgroundSession = NO; [self performServiceUploadTestWithParameters:uploadParameters]; } - (void)testService_Upload_NSData_WithoutMetadata { NSData *uploadData = [self tempDataForUploading]; GTLRUploadParameters *uploadParameters = [GTLRUploadParameters uploadParametersWithData:uploadData MIMEType:@"text/plain"]; uploadParameters.shouldSendUploadOnly = YES; uploadParameters.useBackgroundSession = NO; [self performServiceUploadTestWithParameters:uploadParameters]; } - (void)testService_Upload_NSData_SingleRequest { NSData *uploadData = [self tempDataForUploading]; GTLRUploadParameters *uploadParameters = [GTLRUploadParameters uploadParametersWithData:uploadData MIMEType:@"text/plain"]; uploadParameters.shouldUploadWithSingleRequest = YES; uploadParameters.useBackgroundSession = NO; [self performServiceUploadTestWithParameters:uploadParameters]; } - (void)testService_Upload_NSData_SingleRequest_WithoutMetadata { NSData *uploadData = [self tempDataForUploading]; GTLRUploadParameters *uploadParameters = [GTLRUploadParameters uploadParametersWithData:uploadData MIMEType:@"text/plain"]; uploadParameters.shouldUploadWithSingleRequest = YES; uploadParameters.shouldSendUploadOnly = YES; uploadParameters.useBackgroundSession = NO; [self performServiceUploadTestWithParameters:uploadParameters]; } - (void)performServiceUploadTestWithParameters:(GTLRUploadParameters *)uploadParameters { [self expectTicketAndParsingNotifications]; XCTestExpectation *executeCompletionExp = [self expectationWithDescription:@"Execute block"]; XCTestExpectation *queryCompletionExp = [self expectationWithDescription:@"Query block"]; XCTestExpectation *progressExp; if (uploadParameters.shouldUploadWithSingleRequest || !uploadParameters.shouldSendUploadOnly) { // A request body is present except when doing chunked upload without metadata. progressExp = [self expectationWithDescription:@"Upload progress"]; } GTLRTestingSvc_File *newFile = [GTLRTestingSvc_File object]; newFile.name = @"File de Feline"; GTLRService *service = [self driveServiceForTest]; service.fetcherService.testBlock = ^(GTMSessionFetcher *fetcherToTest, GTMSessionFetcherTestResponse testResponse) { NSURL *fetchURL = fetcherToTest.request.URL; // Upload fetcher testBlock doesn't do chunk fetches, so this must be the initial upload fetch, // without an upload_id in the URL. NSString *uploadID = QueryValueForURLItem(fetchURL, @"upload_id"); XCTAssertNil(uploadID); NSError *parseError; NSData *fetchBody = fetcherToTest.bodyData; if (uploadParameters.shouldUploadWithSingleRequest) { if (uploadParameters.shouldSendUploadOnly) { // Single part, no metadata. XCTAssertEqualObjects(fetchBody, [self tempDataForUploading]); } else { // Multipart. NSArray *bodyParts = [GTMMIMEDocument MIMEPartsWithBoundary:@"END_OF_PART" data:fetchBody]; XCTAssertEqual(bodyParts.count, 2U); XCTAssertEqualObjects(bodyParts[0].headers[@"Content-Type"], @"application/json; charset=utf-8"); XCTAssertEqualObjects(bodyParts[1].headers[@"Content-Type"], @"text/plain"); XCTAssertEqualObjects(bodyParts[1].body, [self tempDataForUploading]); } } else { // Chunked, resumable upload. if (uploadParameters.shouldSendUploadOnly) { // No metadata, so no initial fetch body. XCTAssertNil(fetchBody); } else { // The initial fetch body is the metadata. NSDictionary *jsonBody = [NSJSONSerialization JSONObjectWithData:fetchBody options:0 error:&parseError]; XCTAssertEqualObjects(jsonBody, newFile.JSON); } } // This is the initial request from the client. uploadID = @"AEnB2Uqzt_QIERV0PsZrB"; NSDictionary *responseHeaders = @{ @"X-Goog-Upload-Status" : @"final", @"Content-Type" : @"application/json; charset=UTF-8" }; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:(NSURL * _Nonnull)fetcherToTest.request.URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:responseHeaders]; NSDictionary *responseBodyDict = @{ @"mimeType" : @"text/plain", @"id" : @"0svZDDwtKrhcHh2dmcyZ05MZWc", @"kind" : @"drive#file", @"name" : @"abcdefg.txt" }; NSData *responseData = [NSJSONSerialization dataWithJSONObject:responseBodyDict options:0 error:NULL]; NSError *responseError = nil; testResponse(response, responseData, responseError); }; GTLRTestingSvcQuery_FilesCreate *query = [GTLRTestingSvcQuery_FilesCreate queryWithObject:newFile uploadParameters:uploadParameters]; query.completionBlock = ^(GTLRServiceTicket *ticket, GTLRTestingSvc_File *uploadedFile, NSError *permissionError) { XCTAssertEqualObjects(uploadedFile.name, @"abcdefg.txt"); XCTAssertEqualObjects(uploadedFile.kind, @"drive#file"); [queryCompletionExp fulfill]; }; query.executionParameters.uploadProgressBlock = ^(GTLRServiceTicket *progressTicket, unsigned long long numberOfBytesRead, unsigned long long dataLength) { if (numberOfBytesRead == dataLength) { [progressExp fulfill]; } }; GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_File *uploadedFile, NSError *error) { XCTAssertEqualObjects(uploadedFile.name, @"abcdefg.txt"); XCTAssertEqualObjects(uploadedFile.kind, @"drive#file"); [executeCompletionExp fulfill]; }]; XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } #pragma mark - testBlock Tests - (GTLRTestingSvc_FileList *)fileListObjectForTest { GTLRTestingSvc_File *item0 = [GTLRTestingSvc_File object]; item0.kind = @"drive#file"; item0.name = @"frogfile"; GTLRTestingSvc_File *item1 = [GTLRTestingSvc_File object]; item1.kind = @"drive#file"; item1.name = @"possumfile"; GTLRTestingSvc_FileList *object = [GTLRTestingSvc_FileList object]; object.kind = @"drive#fileList"; object.files = @[ item0, item1 ]; return object; } - (void)testService_MockService_Succeeding { GTLRTestingSvcQuery_FilesList *query = [GTLRTestingSvcQuery_FilesList query]; query.fields = @"kind,nextPageToken,files(id,kind,name)"; GTLRTestingSvc_FileList *fileObj = [self fileListObjectForTest]; GTLRService *service = [GTLRService mockServiceWithFakedObject:fileObj fakedError:nil]; XCTestExpectation *queryFinishedExp = [self expectationWithDescription:@"queryFinished"]; GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_FileList *object, NSError *error) { // Verify the top-level object and one of its items. XCTAssertEqualObjects([object class], [GTLRTestingSvc_FileList class]); XCTAssertNil(error); XCTAssertEqualObjects(object.kind, @"drive#fileList"); XCTAssertEqual(object.files.count, 2U, @"%@", object.files); [queryFinishedExp fulfill]; }]; XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)testService_MockService_Failing { GTLRTestingSvcQuery_FilesList *query = [GTLRTestingSvcQuery_FilesList query]; query.fields = @"kind,nextPageToken,files(id,kind,name)"; NSError *expectedError = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorResourceUnavailable userInfo:nil]; GTLRService *service = [GTLRService mockServiceWithFakedObject:nil fakedError:expectedError]; XCTestExpectation *queryFinishedExp = [self expectationWithDescription:@"queryFinished"]; GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_FileList *object, NSError *error) { XCTAssertNil(object); XCTAssertEqual(error.code, NSURLErrorResourceUnavailable); [queryFinishedExp fulfill]; }]; XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)testService_testBlock_Succeeding { // No need for a fetcher testBlock. [self expectTicketAndParsingNotifications]; GTLRTestingSvcQuery_FilesList *query = [GTLRTestingSvcQuery_FilesList query]; query.fields = @"kind,nextPageToken,files(id,kind,name)"; query.requestID = @"gtlr_1234"; GTLRService *service = [self driveServiceForTest]; service.rootURLString = @"https://example.invalid/"; service.testBlock = ^(GTLRServiceTicket *ticket, GTLRServiceTestResponse testResponse) { XCTAssertEqualObjects(((GTLRQuery *)ticket.originalQuery).requestID, query.requestID); XCTAssertEqualObjects(((GTLRQuery *)ticket.executingQuery).requestID, query.requestID); XCTAssertEqualObjects(ticket.fetchRequest.URL.absoluteString, @"https://example.invalid/drive/v3/files?" @"fields=kind%2CnextPageToken%2Cfiles%28id%2Ckind%2Cname%29&" @"prettyPrint=false"); GTLRTestingSvc_FileList *obj = [self fileListObjectForTest]; testResponse(obj, nil); }; query.executionParameters.retryBlock = ^(GTLRServiceTicket *ticket, BOOL suggestedWillRetry, NSError *error) { XCTFail(@"No retry expected."); return NO; }; query.executionParameters.uploadProgressBlock = ^(GTLRServiceTicket *ticket, unsigned long long numberOfBytesRead, unsigned long long dataLength) { XCTFail(@"No body upload expected."); }; XCTestExpectation *queryFinished = [self expectationWithDescription:@"queryFinished"]; __block GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_FileList *object, NSError *error) { // Verify the top-level object and one of its items. XCTAssertEqualObjects([object class], [GTLRTestingSvc_FileList class]); XCTAssertNil(error); XCTAssertEqualObjects(object.kind, @"drive#fileList"); XCTAssertEqual(object.files.count, 2U, @"%@", object.files); GTLRTestingSvc_File *item0 = object.files[0]; XCTAssertEqualObjects([item0 class], [GTLRTestingSvc_File class]); XCTAssertEqualObjects(item0.kind, @"drive#file"); XCTAssertEqualObjects(callbackTicket, queryTicket); // The ticket's query should be a copy of the original; query execution leaves // the original unmolested so the client can modify and reuse it. GTLRQuery *ticketQuery = callbackTicket.executingQuery; XCTAssertNotEqual(ticketQuery, query); XCTAssertEqualObjects(ticketQuery.requestID, query.requestID); XCTAssert([NSThread isMainThread]); [queryFinished fulfill]; }]; XCTAssertFalse(queryTicket.hasCalledCallback); XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)testService_testBlock_Failing { [self expectTicketNotifications]; GTLRTestingSvcQuery_FilesList *query = [GTLRTestingSvcQuery_FilesList query]; query.fields = @"kind,nextPageToken,files(id,kind,name)"; query.requestID = @"gtlr_1234"; NSError *expectedError = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorResourceUnavailable userInfo:nil]; GTLRService *service = [self driveServiceForTest]; service.rootURLString = @"https://example.invalid/"; service.testBlock = ^(GTLRServiceTicket *ticket, GTLRServiceTestResponse testResponse) { testResponse(nil, expectedError); }; XCTestExpectation *queryFinishedExp = [self expectationWithDescription:@"queryFinishedExp"]; XCTestExpectation *retryBlockExp = [self expectationWithDescription:@"retryBlockExp"]; query.executionParameters.retryBlock = ^(GTLRServiceTicket *ticket, BOOL suggestedWillRetry, NSError *error) { XCTAssertEqual(suggestedWillRetry, NO); [retryBlockExp fulfill]; return NO; }; GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_FileList *object, NSError *error) { // Verify the top-level object and one of its items. XCTAssertNil(object); XCTAssertEqualObjects(error, expectedError); [queryFinishedExp fulfill]; }]; XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)testService_testBlock_CallbackQueue { // No need for a fetcher testBlock. dispatch_queue_t myCallbackQueue = dispatch_queue_create("myCallbackQueue", DISPATCH_QUEUE_SERIAL); GTLRTestingSvcQuery_FilesList *query = [GTLRTestingSvcQuery_FilesList query]; query.fields = @"kind,nextPageToken,files(id,kind,name)"; query.requestID = @"gtlr_1234"; GTLRService *service = [self driveServiceForTest]; service.rootURLString = @"https://example.invalid/"; service.testBlock = ^(GTLRServiceTicket *ticket, GTLRServiceTestResponse testResponse) { GTLRTestingSvc_FileList *obj = [self fileListObjectForTest]; testResponse(obj, nil); }; service.callbackQueue = myCallbackQueue; XCTestExpectation *queryFinished = [self expectationWithDescription:@"queryFinished"]; GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_FileList *object, NSError *error) { // Verify the top-level object and one of its items. XCTAssertEqualObjects([object class], [GTLRTestingSvc_FileList class]); XCTAssertNil(error); XCTAssert(IsCurrentQueue(myCallbackQueue)); [queryFinished fulfill]; }]; service.callbackQueue = dispatch_get_main_queue(); XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)testService_testBlock_MediaQuery { // Successful request with data object response. NSData *expectedData = [self tempDataForUploading]; NSString *expectedContentType = @"text/plain"; GTLRService *service = [self driveServiceForTest]; GTLRTestingSvcQuery_FilesGet *query = [GTLRTestingSvcQuery_FilesGet queryForMediaWithFileId:@"abcde"]; // For variety, we'll attach the testBlock to the query's service execution parameters. query.executionParameters.testBlock = ^(GTLRServiceTicket *ticket, GTLRServiceTestResponse testResponse) { GTLRDataObject *obj = [GTLRDataObject object]; obj.data = expectedData; obj.contentType = expectedContentType; testResponse(obj, nil); }; XCTestExpectation *queryFinished = [self expectationWithDescription:@"queryFinished"]; GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRDataObject *object, NSError *error) { NSURL *fetcherURL = callbackTicket.fetchRequest.URL; XCTAssertEqualObjects(QueryValueForURLItem(fetcherURL, @"alt"), @"media"); XCTAssertEqualObjects([object class], [GTLRDataObject class]); XCTAssertNil(error); XCTAssertEqualObjects(object.data, expectedData); XCTAssertEqualObjects(object.contentType, expectedContentType); XCTAssert([NSThread isMainThread]); [queryFinished fulfill]; }]; XCTAssertFalse(queryTicket.hasCalledCallback); XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)testService_testBlock_BatchQuery { // The test block will return a batch with one success and one failure. GTLRService *service = [self driveServiceForTest]; service.rootURLString = @"https://example.invalid/"; XCTestExpectation *childExp = [self expectationWithDescription:@"childCallback"]; XCTestExpectation *parentsExp = [self expectationWithDescription:@"parentsCallback"]; XCTestExpectation *batchFinished = [self expectationWithDescription:@"batchFinished"]; GTLRTestingSvcQuery_FilesList *childQuery = [GTLRTestingSvcQuery_FilesList query]; childQuery.q = [NSString stringWithFormat:@"'ABCDE' in parents"]; childQuery.requestID = @"gtlr_5"; childQuery.completionBlock = ^(GTLRServiceTicket *ticket, GTLRTestingSvc_FileList *fileList, NSError *childError) { XCTAssertEqualObjects([fileList class], [GTLRTestingSvc_FileList class]); XCTAssertEqualObjects(fileList.kind, @"drive#fileList"); XCTAssertNil(childError); [childExp fulfill]; }; NSError *expectedError = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorResourceUnavailable userInfo:nil]; GTLRErrorObject *expectedErrorObject = [GTLRErrorObject objectWithFoundationError:expectedError]; GTLRTestingSvcQuery_FilesGet *parentsQuery = [GTLRTestingSvcQuery_FilesGet queryWithFileId:@"0B7svZDDwtKrhS2FDS2JZclU1U0E"]; parentsQuery.fields = @"parents"; parentsQuery.requestID = @"gtlr_6"; parentsQuery.completionBlock = ^(GTLRServiceTicket *ticket, GTLRTestingSvc_File *file, NSError *parentsError) { XCTAssertNil(file); XCTAssertEqualObjects(parentsError.domain, expectedError.domain); XCTAssertEqual(parentsError.code, expectedError.code); [parentsExp fulfill]; }; parentsQuery.additionalHTTPHeaders = @{ @"Tiger" : @"Siberian" }; // Combine the separate queries into one batch. GTLRBatchQuery *batchQuery = [GTLRBatchQuery batchQuery]; [batchQuery addQuery:childQuery]; [batchQuery addQuery:parentsQuery]; // Test adding http headers to the query batchQuery.additionalHTTPHeaders = @{ @"X-Feline": @"Fluffy", @"X-Canine": @"Spot" }; service.testBlock = ^(GTLRServiceTicket *ticket, GTLRServiceTestResponse testResponse) { XCTAssert([ticket.fetchRequest.URL.absoluteString hasPrefix:@"https://example.invalid/"], @"%@", ticket.fetchRequest); GTLRTestingSvc_FileList *fileList = [self fileListObjectForTest]; GTLRBatchResult *batchResult = [GTLRBatchResult object]; batchResult.successes = @{ childQuery.requestID : fileList }; batchResult.failures = @{ parentsQuery.requestID : expectedErrorObject }; batchResult.responseHeaders = @{ childQuery.requestID : @{} , parentsQuery.requestID : @{} }; testResponse(batchResult, nil); }; batchQuery.executionParameters.retryBlock = ^(GTLRServiceTicket *ticket, BOOL suggestedWillRetry, NSError *error) { XCTFail(@"No retry expected."); return NO; }; XCTestExpectation *uploadedSomeBytes = [self expectationWithDescription:@"uploadedSomeBytes"]; batchQuery.executionParameters.uploadProgressBlock = ^(GTLRServiceTicket *ticket, unsigned long long numberOfBytesRead, unsigned long long dataLength) { if (numberOfBytesRead == dataLength) { [uploadedSomeBytes fulfill]; } XCTAssert([NSThread isMainThread]); }; GTLRServiceTicket *queryTicket = [service executeQuery:batchQuery completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRBatchResult *batchResult, NSError *batchError) { // Verify the batch result and each success and failure result. XCTAssertEqualObjects([batchResult class], [GTLRBatchResult class]); XCTAssertNil(batchError); XCTAssertEqual(batchResult.successes.count, 1U); XCTAssertEqual(batchResult.failures.count, 1U); XCTAssertEqual(batchResult.responseHeaders.count, 2U); GTLRTestingSvc_FileList *fileList = batchResult.successes[@"gtlr_5"]; XCTAssertEqualObjects([fileList class], [GTLRTestingSvc_FileList class]); XCTAssertEqualObjects(fileList.kind, @"drive#fileList"); GTLRErrorObject *failureObject = batchResult.failures[@"gtlr_6"]; XCTAssertEqualObjects([failureObject class], [GTLRErrorObject class]); XCTAssertEqual(failureObject.code.intValue, NSURLErrorResourceUnavailable); // As with a single query, the batch ticket's query should be a copy of the original; // query execution leaves the original unmolested so the client can modify and reuse it. GTLRBatchQuery *ticketQuery = callbackTicket.executingQuery; XCTAssertNotEqual(ticketQuery, batchQuery); // Since the batch has a copy of the original query, all request IDs of both batch // queries should be the same. XCTAssertEqualObjects([ticketQuery.queries valueForKey:@"requestID"], [batchQuery.queries valueForKey:@"requestID"]); XCTAssert([NSThread isMainThread]); [batchFinished fulfill]; }]; XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // The request is to a fixed URL. XCTAssertEqualObjects(queryTicket.fetchRequest.URL.absoluteString, @"https://example.invalid/batch"); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)testService_testBlock_Upload_FileHandle { NSURL *fileURL = [self tempFileURLForUploading]; NSError *readingError; NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingFromURL:fileURL error:&readingError]; XCTAssertNotNil(fileHandle, @"%@", readingError); GTLRUploadParameters *uploadParameters = [GTLRUploadParameters uploadParametersWithFileHandle:fileHandle MIMEType:@"text/plain"]; [self performServiceUploadTestBlockTestWithParameters:uploadParameters]; } - (void)testService_testBlock_Upload_FileURL { NSURL *fileURL = [self tempFileURLForUploading]; GTLRUploadParameters *uploadParameters = [GTLRUploadParameters uploadParametersWithFileURL:fileURL MIMEType:@"text/plain"]; [self performServiceUploadTestBlockTestWithParameters:uploadParameters]; } - (void)testService_testBlock_Upload_NSData { NSData *uploadData = [self tempDataForUploading]; XCTAssert(uploadData.length > 1000); GTLRUploadParameters *uploadParameters = [GTLRUploadParameters uploadParametersWithData:uploadData MIMEType:@"text/plain"]; [self performServiceUploadTestBlockTestWithParameters:uploadParameters]; } - (void)performServiceUploadTestBlockTestWithParameters:(GTLRUploadParameters *)uploadParameters { XCTestExpectation *executeCompletionExp = [self expectationWithDescription:@"Execute block"]; XCTestExpectation *queryCompletionExp = [self expectationWithDescription:@"Query block"]; XCTestExpectation *progressExp = [self expectationWithDescription:@"Upload progress"]; XCTestExpectation *testBlockExp = [self expectationWithDescription:@"testBlock"]; GTLRTestingSvc_File *newFile = [GTLRTestingSvc_File object]; newFile.name = @"File de Feline"; GTLRService *service = [self driveServiceForTest]; service.rootURLString = @"https://example.invalid/"; service.testBlock = ^(GTLRServiceTicket *ticket, GTLRServiceTestResponse testResponse) { NSString *uploadID = QueryValueForURLItem(ticket.fetchRequest.URL, @"upload_id"); XCTAssertNil(uploadID); // Below we attach an ETag header to the upload object, and force the http method to PUT, // so the ETag should be present in the initial request header. NSString *headerETag = ticket.fetchRequest.allHTTPHeaderFields[@"If-Match"]; XCTAssertEqualObjects(headerETag, newFile.ETag); GTLRTestingSvc_File *fileObj = [GTLRTestingSvc_File object]; fileObj.kind = @"drive#file"; fileObj.name = @"abcdefg.txt"; fileObj.identifier = @"0svZDDwtKrhcHh2dmcyZ05MZWc"; testResponse(fileObj, nil); [testBlockExp fulfill]; }; GTLRTestingSvcQuery_FilesCreate *query = [GTLRTestingSvcQuery_FilesCreate queryWithObject:newFile uploadParameters:uploadParameters]; query.completionBlock = ^(GTLRServiceTicket *ticket, GTLRTestingSvc_File *uploadedFile, NSError *permissionError) { XCTAssertEqualObjects(uploadedFile.name, @"abcdefg.txt"); XCTAssertEqualObjects(uploadedFile.kind, @"drive#file"); [queryCompletionExp fulfill]; }; query.executionParameters.uploadProgressBlock = ^(GTLRServiceTicket *progressTicket, unsigned long long numberOfBytesRead, unsigned long long dataLength) { if (numberOfBytesRead == dataLength) { [progressExp fulfill]; } }; // To test that the ETag is copied from the query's object, we'll change the // request from POST to PUT, which will be ignored by the test block simulation // of the upload server, but will cause the ETag to be added as an If-Match request // header. newFile.ETag = @"wheeTag"; [query setValue:@"PUT" forKey:@"httpMethod"]; GTLRServiceTicket *queryTicket = [service executeQuery:query completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRTestingSvc_File *uploadedFile, NSError *error) { XCTAssertEqualObjects(uploadedFile.name, @"abcdefg.txt"); XCTAssertEqualObjects(uploadedFile.kind, @"drive#file"); [executeCompletionExp fulfill]; }]; XCTAssert([self service:service waitForTicket:queryTicket]); XCTAssert(queryTicket.hasCalledCallback); // Ensure all expectations were satisfied. [self waitForExpectationsWithTimeout:10 handler:nil]; } #pragma mark - Utility Method Tests - (void)testRequestForQuery { GTLRService *service = [[GTLRService alloc] init]; service.rootURLString = @"https://www.test.com/"; service.servicePath = @"api/"; // Basic query. GTLRQuery *query = [[GTLRQuery alloc] initWithPathURITemplate:@"path/{path_part}" HTTPMethod:nil pathParameterNames:@[ @"path_part" ]]; query.JSON = [@{ @"path_part" : @"foo", @"arg" : @"mumble", } mutableCopy]; NSString *userAgent = service.requestUserAgent; NSDictionary *baseHTTPHeaders = @{ @"User-Agent" : userAgent }; NSMutableDictionary *expectedHTTPHeaders = [baseHTTPHeaders mutableCopy]; 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); // Extra query arg and HTTP header. query.additionalURLQueryParameters = @{ @"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); // With a service arg and HTTP header. service.additionalURLQueryParameters = @{ @"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); // Overlap between the query and service for an arg and HTTP header (query // wins). query.additionalURLQueryParameters = @{ @"arg1" : @"query", @"arg2" : @"query" }; query.additionalHTTPHeaders = @{ @"X-1" : @"Query", @"X-2" : @"Query" }; service.additionalURLQueryParameters = @{ @"arg1" : @"service", @"arg3" : @"service" }; service.additionalHTTPHeaders = @{ @"X-1" : @"Service", @"X-3" : @"Service" }; expectedURLString = @"https://www.test.com/api/path/foo?arg=mumble&arg1=query&arg2=query&arg3=service"; expectedHTTPHeaders = [baseHTTPHeaders mutableCopy]; [expectedHTTPHeaders addEntriesFromDictionary:@{ @"X-1" : @"Query", @"X-2" : @"Query", @"X-3" : @"Service", }]; result = [service requestForQuery:query]; XCTAssertEqualObjects(result.URL.absoluteString, expectedURLString); XCTAssertEqualObjects(result.HTTPMethod, @"GET"); XCTAssertEqualObjects(result.allHTTPHeaderFields, expectedHTTPHeaders); // Different HTTPMethod. query = [[GTLRQuery alloc] initWithPathURITemplate:@"blah" HTTPMethod:@"POST" pathParameterNames:nil]; expectedURLString = @"https://www.test.com/api/blah"; 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); // 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); // 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); } - (void)testRequestForQuery_MediaDownload { GTLRTestingSvcQuery_FilesGet *query = [GTLRTestingSvcQuery_FilesGet queryForMediaWithFileId:@"abcde"]; query.useMediaDownloadService = NO; GTLRService *service = [self driveServiceForTest]; // Without download service. NSURLRequest *result = [service requestForQuery:query]; NSString *expectedURLString = @"https://www.googleapis.com/drive/v3/files/abcde?alt=media"; XCTAssertEqualObjects(result.URL.absoluteString, expectedURLString); // With download service. query.useMediaDownloadService = YES; result = [service requestForQuery:query]; expectedURLString = @"https://www.googleapis.com/download/drive/v3/files/abcde?alt=media"; XCTAssertEqualObjects(result.URL.absoluteString, expectedURLString); } #pragma mark - Internal Utility Method Tests - (void)testFullURLFromStringQueryParameters { NSString *inputStr; NSDictionary *inputDict; NSURL *output; NSString *expectedStr; NSURL *expected; inputStr = nil; inputDict = nil; output = [GTLRService URLWithString:inputStr queryParameters:inputDict]; XCTAssertNil(output); inputStr = @""; output = [GTLRService URLWithString:inputStr queryParameters:inputDict]; XCTAssertNil(output); inputStr = nil; inputDict = @{ @"b" : @"1" }; output = [GTLRService URLWithString:inputStr queryParameters:inputDict]; XCTAssertNil(output); inputStr = @""; output = [GTLRService URLWithString:inputStr queryParameters:inputDict]; XCTAssertNil(output); inputStr = @"http://www.google.com"; inputDict = nil; output = [GTLRService URLWithString:inputStr queryParameters:inputDict]; expected = [NSURL URLWithString:@"http://www.google.com"]; XCTAssertEqualObjects(output, expected); inputDict = @{ }; output = [GTLRService URLWithString:inputStr queryParameters:inputDict]; XCTAssertEqualObjects(output, expected); // Keys will be sorted, but order within any arrays is preserved. inputDict = @{ @"g" : @42, @"f" : @[ @YES, @NO ], @"e" : @[ @1, @2, @3 ], @"d" : @[ @"d1", @"d3", @"d2" ], @"c" : @YES, @"b" : @"&", @"a" : @"1", }; output = [GTLRService URLWithString:inputStr queryParameters:inputDict]; expectedStr = @"http://www.google.com?a=1&b=%26&c=true&d=d1&d=d3&d=d2&e=1&e=2&e=3&f=true&f=false&g=42"; expected = [NSURL URLWithString:expectedStr]; XCTAssertEqualObjects(output, expected); inputStr = @"http://www.google.com?q=spam"; output = [GTLRService URLWithString:inputStr queryParameters:inputDict]; expectedStr = @"http://www.google.com?q=spam&a=1&b=%26&c=true&d=d1&d=d3&d=d2&e=1&e=2&e=3&f=true&f=false&g=42"; expected = [NSURL URLWithString:expectedStr]; XCTAssertEqualObjects(output, expected); } @end // // Simple authorizer object for testing - implementation // @implementation GTLRTestAuthorizer @synthesize value = _value, error = _error; + (GTLRTestAuthorizer *)authorizerWithValue:(NSString *)value { GTLRTestAuthorizer *obj = [[self alloc] init]; obj.value = value; return obj; } - (NSString *)authorizationValue { NSString *str = [NSString stringWithFormat:@"Bearer %@", _value]; return str; } - (void)authorizeRequest:(NSMutableURLRequest *)request delegate:(id)delegate didFinishSelector:(SEL)sel { NSString *str = [self authorizationValue]; [request setValue:str forHTTPHeaderField:@"Authorization"]; id localSelf = self; NSMethodSignature *sig = [delegate methodSignatureForSelector:sel]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; [invocation setSelector:sel]; [invocation setTarget:delegate]; [invocation setArgument:&localSelf atIndex:2]; [invocation setArgument:&request atIndex:3]; [invocation setArgument:&_error atIndex:4]; [invocation invoke]; } - (void)stopAuthorization { } - (void)stopAuthorizationForRequest:(NSURLRequest *)request { } - (BOOL)isAuthorizingRequest:(NSURLRequest *)request { return NO; } - (BOOL)isAuthorizedRequest:(NSURLRequest *)request { NSString *requestValue = [request valueForHTTPHeaderField:@"Authorization"]; NSString *str = [self authorizationValue]; return GTLR_AreEqualOrBothNil(requestValue, str); } - (NSString *)userEmail { return @"test@example.com"; } @end @implementation GTLRTestLifetimeObject { XCTestExpectation *_expectation; } + (instancetype)trackLifetimeOfObject:(id)object expectation:(XCTestExpectation *)expectation { NSAssert(expectation != nil, @"missing expectation for %@", object); NSAssert(object != nil, @"missing object for %@", expectation); GTLRTestLifetimeObject *lifetime = [[self alloc] initWithExpectation:expectation]; objc_setAssociatedObject(object, "GTLRTestLifetimeObject", lifetime, OBJC_ASSOCIATION_RETAIN); return lifetime; } - (instancetype)initWithExpectation:(XCTestExpectation *)expectation { self = [super init]; if (self) { _expectation = expectation; } return self; } - (void)dealloc { [_expectation fulfill]; } @end #if GTM_BACKGROUND_TASK_FETCHING @implementation CountingUIApplication @synthesize beginTaskIDs = _beginTaskIDs, endTaskIDs = _endTaskIDs, shouldExpireTasks = _shouldExpireTasks, numberOfExpiredTasks = _numberOfExpiredTasks; UIBackgroundTaskIdentifier gTaskID = 1000; - (instancetype)init { self = [super init]; if (self) { _beginTaskIDs = [NSCountedSet set]; _endTaskIDs = [NSCountedSet set]; } return self; } - (UIBackgroundTaskIdentifier)beginBackgroundTaskWithName:(NSString *)taskName expirationHandler:(dispatch_block_t)handler { UIBackgroundTaskIdentifier taskID; @synchronized(self) { taskID = ++gTaskID; [self.beginTaskIDs addObject:@(taskID)]; } if (_shouldExpireTasks) { dispatch_async(dispatch_get_main_queue(), ^{ handler(); self->_numberOfExpiredTasks++; }); } return taskID; } - (void)endBackgroundTask:(UIBackgroundTaskIdentifier)taskID { @synchronized(self) { [self.endTaskIDs addObject:@(taskID)]; } } @end #endif // GTM_BACKGROUND_TASK_FETCHING