/* * * Copyright 2018 gRPC authors. * * 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 #import "src/objective-c/tests/RemoteTestClient/Messages.pbobjc.h" #include #include #import "../Common/GRPCBlockCallbackResponseHandler.h" #import "../Common/TestUtils.h" #import "../version.h" // Package and service name of test server static NSString *const kPackage = @"grpc.testing"; static NSString *const kService = @"TestService"; static GRPCProtoMethod *kInexistentMethod; static GRPCProtoMethod *kEmptyCallMethod; static GRPCProtoMethod *kUnaryCallMethod; static GRPCProtoMethod *kOutputStreamingCallMethod; static GRPCProtoMethod *kFullDuplexCallMethod; static const int kSimpleDataLength = 100; static const NSTimeInterval kTestTimeout = 8; static const NSTimeInterval kInvertedTimeout = 2; // Reveal the _class ivar for testing access @interface GRPCCall2 () { @public GRPCCall *_call; } @end @interface CallAPIv2Tests : XCTestCase @end @implementation CallAPIv2Tests + (void)setUp { GRPCPrintInteropTestServerDebugInfo(); } - (void)setUp { // This method isn't implemented by the remote server. kInexistentMethod = [[GRPCProtoMethod alloc] initWithPackage:kPackage service:kService method:@"Inexistent"]; kEmptyCallMethod = [[GRPCProtoMethod alloc] initWithPackage:kPackage service:kService method:@"EmptyCall"]; kUnaryCallMethod = [[GRPCProtoMethod alloc] initWithPackage:kPackage service:kService method:@"UnaryCall"]; kOutputStreamingCallMethod = [[GRPCProtoMethod alloc] initWithPackage:kPackage service:kService method:@"StreamingOutputCall"]; kFullDuplexCallMethod = [[GRPCProtoMethod alloc] initWithPackage:kPackage service:kService method:@"FullDuplexCall"]; } - (void)testUserAgentPrefix { __weak XCTestExpectation *completion = [self expectationWithDescription:@"Empty RPC completed."]; __weak XCTestExpectation *recvInitialMd = [self expectationWithDescription:@"Did not receive initial md."]; GRPCRequestOptions *request = [[GRPCRequestOptions alloc] initWithHost:GRPCGetLocalInteropTestServerAddressPlainText() path:kEmptyCallMethod.HTTPPath safety:GRPCCallSafetyDefault]; NSDictionary *headers = [NSDictionary dictionaryWithObjectsAndKeys:@"", @"x-grpc-test-echo-useragent", nil]; GRPCMutableCallOptions *options = [[GRPCMutableCallOptions alloc] init]; options.transportType = GRPCTransportTypeInsecure; options.userAgentPrefix = @"Foo"; options.initialMetadata = headers; GRPCCall2 *call = [[GRPCCall2 alloc] initWithRequestOptions:request responseHandler: [[GRPCBlockCallbackResponseHandler alloc] initWithInitialMetadataCallback:^(NSDictionary *initialMetadata) { NSString *userAgent = initialMetadata[@"x-grpc-test-echo-useragent"]; // Test the regex is correct NSString *expectedUserAgent = @"Foo grpc-objc-cfstream/"; expectedUserAgent = [expectedUserAgent stringByAppendingString:GRPC_OBJC_VERSION_STRING]; expectedUserAgent = [expectedUserAgent stringByAppendingString:@" grpc-c/"]; expectedUserAgent = [expectedUserAgent stringByAppendingString:GRPC_C_VERSION_STRING]; expectedUserAgent = [expectedUserAgent stringByAppendingString:@" ("]; expectedUserAgent = [expectedUserAgent stringByAppendingString:@GPR_PLATFORM_STRING]; expectedUserAgent = [expectedUserAgent stringByAppendingString:@"; chttp2)"]; XCTAssertEqualObjects(userAgent, expectedUserAgent); NSError *error = nil; // Change in format of user-agent field in a direction that does not match // the regex will likely cause problem for certain gRPC users. For details, // refer to internal doc https://goo.gl/c2diBc NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern: @" grpc-[a-zA-Z0-9]+(-[a-zA-Z0-9]+)?/[^ ,]+( \\([^)]*\\))?" options:0 error:&error]; NSString *customUserAgent = [regex stringByReplacingMatchesInString:userAgent options:0 range:NSMakeRange(0, [userAgent length]) withTemplate:@""]; XCTAssertEqualObjects(customUserAgent, @"Foo"); [recvInitialMd fulfill]; } messageCallback:^(id message) { XCTAssertNotNil(message); XCTAssertEqual([message length], 0, @"Non-empty response received: %@", message); } closeCallback:^(NSDictionary *trailingMetadata, NSError *error) { if (error) { XCTFail(@"Finished with unexpected error: %@", error); } else { [completion fulfill]; } }] callOptions:options]; [call writeData:[NSData data]]; [call start]; [self waitForExpectationsWithTimeout:kTestTimeout handler:nil]; } - (void)getTokenWithHandler:(void (^)(NSString *token))handler { dispatch_queue_t queue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL); dispatch_sync(queue, ^{ handler(@"test-access-token"); }); } - (void)testOAuthToken { __weak XCTestExpectation *completion = [self expectationWithDescription:@"RPC completed."]; GRPCRequestOptions *requestOptions = [[GRPCRequestOptions alloc] initWithHost:GRPCGetLocalInteropTestServerAddressPlainText() path:kEmptyCallMethod.HTTPPath safety:GRPCCallSafetyDefault]; GRPCMutableCallOptions *options = [[GRPCMutableCallOptions alloc] init]; options.transportType = GRPCTransportTypeInsecure; options.authTokenProvider = self; __block GRPCCall2 *call = [[GRPCCall2 alloc] initWithRequestOptions:requestOptions responseHandler:[[GRPCBlockCallbackResponseHandler alloc] initWithInitialMetadataCallback:nil messageCallback:nil closeCallback:^(NSDictionary *trailingMetadata, NSError *error) { [completion fulfill]; }] callOptions:options]; [call writeData:[NSData data]]; [call start]; [call finish]; [self waitForExpectationsWithTimeout:kTestTimeout handler:nil]; } - (void)testResponseSizeLimitExceeded { __weak XCTestExpectation *completion = [self expectationWithDescription:@"RPC completed."]; GRPCRequestOptions *requestOptions = [[GRPCRequestOptions alloc] initWithHost:GRPCGetLocalInteropTestServerAddressPlainText() path:kUnaryCallMethod.HTTPPath safety:GRPCCallSafetyDefault]; GRPCMutableCallOptions *options = [[GRPCMutableCallOptions alloc] init]; options.responseSizeLimit = kSimpleDataLength; options.transportType = GRPCTransportTypeInsecure; RMTSimpleRequest *request = [RMTSimpleRequest message]; request.payload.body = [NSMutableData dataWithLength:options.responseSizeLimit]; request.responseSize = (int32_t)(options.responseSizeLimit * 2); GRPCCall2 *call = [[GRPCCall2 alloc] initWithRequestOptions:requestOptions responseHandler:[[GRPCBlockCallbackResponseHandler alloc] initWithInitialMetadataCallback:nil messageCallback:nil closeCallback:^(NSDictionary *trailingMetadata, NSError *error) { XCTAssertNotNil(error, @"Expecting non-nil error"); XCTAssertEqual(error.code, GRPCErrorCodeResourceExhausted); [completion fulfill]; }] callOptions:options]; [call writeData:[request data]]; [call start]; [call finish]; [self waitForExpectationsWithTimeout:kTestTimeout handler:nil]; } - (void)testTimeout { __weak XCTestExpectation *completion = [self expectationWithDescription:@"RPC completed."]; GRPCMutableCallOptions *options = [[GRPCMutableCallOptions alloc] init]; options.timeout = 0.001; options.transportType = GRPCTransportTypeInsecure; GRPCRequestOptions *requestOptions = [[GRPCRequestOptions alloc] initWithHost:GRPCGetLocalInteropTestServerAddressPlainText() path:kFullDuplexCallMethod.HTTPPath safety:GRPCCallSafetyDefault]; GRPCCall2 *call = [[GRPCCall2 alloc] initWithRequestOptions:requestOptions responseHandler: [[GRPCBlockCallbackResponseHandler alloc] initWithInitialMetadataCallback:nil messageCallback:^(NSData *data) { XCTFail(@"Failure: response received; Expect: no response received."); } closeCallback:^(NSDictionary *trailingMetadata, NSError *error) { XCTAssertNotNil(error, @"Failure: no error received; Expect: receive " @"deadline exceeded."); if (error.code != GRPCErrorCodeDeadlineExceeded) { NSLog(@"Unexpected error: %@", error); } XCTAssertEqual(error.code, GRPCErrorCodeDeadlineExceeded); [completion fulfill]; }] callOptions:options]; [call start]; [self waitForExpectationsWithTimeout:kTestTimeout handler:nil]; } - (void)testTimeoutBackoffWithTimeout:(double)timeout Backoff:(double)backoff { const double maxConnectTime = timeout > backoff ? timeout : backoff; const double kMargin = 0.1; __weak XCTestExpectation *completion = [self expectationWithDescription:@"Timeout in a second."]; NSString *const kPhonyAddress = [NSString stringWithFormat:@"127.0.0.1:10000"]; GRPCRequestOptions *requestOptions = [[GRPCRequestOptions alloc] initWithHost:kPhonyAddress path:@"/phony/path" safety:GRPCCallSafetyDefault]; GRPCMutableCallOptions *options = [[GRPCMutableCallOptions alloc] init]; options.connectMinTimeout = timeout; options.connectInitialBackoff = backoff; options.connectMaxBackoff = 0; NSDate *startTime = [NSDate date]; GRPCCall2 *call = [[GRPCCall2 alloc] initWithRequestOptions:requestOptions responseHandler:[[GRPCBlockCallbackResponseHandler alloc] initWithInitialMetadataCallback:nil messageCallback:^(NSData *data) { XCTFail(@"Received message. Should not reach here."); } closeCallback:^(NSDictionary *trailingMetadata, NSError *error) { XCTAssertNotNil(error, @"Finished with no error; expecting error"); XCTAssertLessThan( [[NSDate date] timeIntervalSinceDate:startTime], maxConnectTime + kMargin); [completion fulfill]; }] callOptions:options]; [call start]; [self waitForExpectationsWithTimeout:kTestTimeout handler:nil]; } - (void)testTimeoutBackoff1 { [self testTimeoutBackoffWithTimeout:0.7 Backoff:0.4]; } - (void)testTimeoutBackoff2 { [self testTimeoutBackoffWithTimeout:0.3 Backoff:0.8]; } - (void)testCompression { __weak XCTestExpectation *completion = [self expectationWithDescription:@"RPC completed."]; RMTSimpleRequest *request = [RMTSimpleRequest message]; request.expectCompressed = [RMTBoolValue message]; request.expectCompressed.value = YES; request.responseCompressed = [RMTBoolValue message]; request.expectCompressed.value = YES; request.responseSize = kSimpleDataLength; request.payload.body = [NSMutableData dataWithLength:kSimpleDataLength]; GRPCRequestOptions *requestOptions = [[GRPCRequestOptions alloc] initWithHost:GRPCGetLocalInteropTestServerAddressPlainText() path:kUnaryCallMethod.HTTPPath safety:GRPCCallSafetyDefault]; GRPCMutableCallOptions *options = [[GRPCMutableCallOptions alloc] init]; options.transportType = GRPCTransportTypeInsecure; options.compressionAlgorithm = GRPCCompressGzip; GRPCCall2 *call = [[GRPCCall2 alloc] initWithRequestOptions:requestOptions responseHandler:[[GRPCBlockCallbackResponseHandler alloc] initWithInitialMetadataCallback:nil messageCallback:^(NSData *data) { NSError *error; RMTSimpleResponse *response = [RMTSimpleResponse parseFromData:data error:&error]; XCTAssertNil(error, @"Error when parsing response: %@", error); XCTAssertEqual(response.payload.body.length, kSimpleDataLength); } closeCallback:^(NSDictionary *trailingMetadata, NSError *error) { XCTAssertNil(error, @"Received failure: %@", error); [completion fulfill]; }] callOptions:options]; [call start]; [call writeData:[request data]]; [call finish]; [self waitForExpectationsWithTimeout:kTestTimeout handler:nil]; } - (void)testFlowControlWrite { __weak XCTestExpectation *expectWriteData = [self expectationWithDescription:@"Reported write data"]; RMTStreamingOutputCallRequest *request = [RMTStreamingOutputCallRequest message]; RMTResponseParameters *parameters = [RMTResponseParameters message]; parameters.size = kSimpleDataLength; [request.responseParametersArray addObject:parameters]; request.payload.body = [NSMutableData dataWithLength:kSimpleDataLength]; GRPCRequestOptions *callRequest = [[GRPCRequestOptions alloc] initWithHost:GRPCGetLocalInteropTestServerAddressPlainText() path:kUnaryCallMethod.HTTPPath safety:GRPCCallSafetyDefault]; GRPCMutableCallOptions *options = [[GRPCMutableCallOptions alloc] init]; options.transportType = GRPCTransportTypeInsecure; options.flowControlEnabled = YES; GRPCCall2 *call = [[GRPCCall2 alloc] initWithRequestOptions:callRequest responseHandler:[[GRPCBlockCallbackResponseHandler alloc] initWithInitialMetadataCallback:nil messageCallback:nil closeCallback:nil writeDataCallback:^{ [expectWriteData fulfill]; }] callOptions:options]; [call start]; [call receiveNextMessages:1]; [call writeData:[request data]]; // Wait for 3 seconds and make sure we do not receive the response [self waitForExpectationsWithTimeout:kTestTimeout handler:nil]; [call finish]; } - (void)testFlowControlRead { __weak __block XCTestExpectation *expectBlockedMessage = [self expectationWithDescription:@"Message not delivered without recvNextMessage"]; __weak __block XCTestExpectation *expectPassedMessage = nil; __weak __block XCTestExpectation *expectBlockedClose = [self expectationWithDescription:@"Call not closed with pending message"]; __weak __block XCTestExpectation *expectPassedClose = nil; expectBlockedMessage.inverted = YES; expectBlockedClose.inverted = YES; RMTSimpleRequest *request = [RMTSimpleRequest message]; request.responseSize = kSimpleDataLength; request.payload.body = [NSMutableData dataWithLength:kSimpleDataLength]; GRPCRequestOptions *callRequest = [[GRPCRequestOptions alloc] initWithHost:GRPCGetLocalInteropTestServerAddressPlainText() path:kUnaryCallMethod.HTTPPath safety:GRPCCallSafetyDefault]; GRPCMutableCallOptions *options = [[GRPCMutableCallOptions alloc] init]; options.transportType = GRPCTransportTypeInsecure; options.flowControlEnabled = YES; __block int unblocked = NO; GRPCCall2 *call = [[GRPCCall2 alloc] initWithRequestOptions:callRequest responseHandler:[[GRPCBlockCallbackResponseHandler alloc] initWithInitialMetadataCallback:nil messageCallback:^(NSData *message) { if (!unblocked) { [expectBlockedMessage fulfill]; } else { [expectPassedMessage fulfill]; } } closeCallback:^(NSDictionary *trailers, NSError *error) { if (!unblocked) { [expectBlockedClose fulfill]; } else { [expectPassedClose fulfill]; } }] callOptions:options]; [call start]; [call writeData:[request data]]; [call finish]; // Wait to make sure we do not receive the response [self waitForExpectationsWithTimeout:kInvertedTimeout handler:nil]; expectPassedMessage = [self expectationWithDescription:@"Message delivered with receiveNextMessage"]; expectPassedClose = [self expectationWithDescription:@"Close delivered after receiveNextMessage"]; unblocked = YES; [call receiveNextMessages:1]; [self waitForExpectationsWithTimeout:kTestTimeout handler:nil]; } - (void)testFlowControlMultipleMessages { __weak XCTestExpectation *expectPassedMessage = [self expectationWithDescription:@"two messages delivered with receiveNextMessage"]; expectPassedMessage.expectedFulfillmentCount = 2; __weak XCTestExpectation *expectBlockedMessage = [self expectationWithDescription:@"Message 3 not delivered"]; expectBlockedMessage.inverted = YES; __weak XCTestExpectation *expectWriteTwice = [self expectationWithDescription:@"Write 2 messages done"]; expectWriteTwice.expectedFulfillmentCount = 2; RMTStreamingOutputCallRequest *request = [RMTStreamingOutputCallRequest message]; RMTResponseParameters *parameters = [RMTResponseParameters message]; parameters.size = kSimpleDataLength; [request.responseParametersArray addObject:parameters]; request.payload.body = [NSMutableData dataWithLength:kSimpleDataLength]; GRPCRequestOptions *callRequest = [[GRPCRequestOptions alloc] initWithHost:GRPCGetLocalInteropTestServerAddressPlainText() path:kFullDuplexCallMethod.HTTPPath safety:GRPCCallSafetyDefault]; GRPCMutableCallOptions *options = [[GRPCMutableCallOptions alloc] init]; options.transportType = GRPCTransportTypeInsecure; options.flowControlEnabled = YES; __block NSUInteger messageId = 0; __block GRPCCall2 *call = [[GRPCCall2 alloc] initWithRequestOptions:callRequest responseHandler:[[GRPCBlockCallbackResponseHandler alloc] initWithInitialMetadataCallback:nil messageCallback:^(NSData *message) { if (messageId <= 1) { [expectPassedMessage fulfill]; } else { [expectBlockedMessage fulfill]; } messageId++; } closeCallback:nil writeDataCallback:^{ [expectWriteTwice fulfill]; }] callOptions:options]; [call receiveNextMessages:2]; [call start]; [call writeData:[request data]]; [call writeData:[request data]]; [self waitForExpectationsWithTimeout:kInvertedTimeout handler:nil]; } - (void)testFlowControlReadReadyBeforeStart { __weak XCTestExpectation *expectPassedMessage = [self expectationWithDescription:@"Message delivered with receiveNextMessage"]; __weak XCTestExpectation *expectPassedClose = [self expectationWithDescription:@"Close delivered with receiveNextMessage"]; RMTSimpleRequest *request = [RMTSimpleRequest message]; request.responseSize = kSimpleDataLength; request.payload.body = [NSMutableData dataWithLength:kSimpleDataLength]; GRPCRequestOptions *callRequest = [[GRPCRequestOptions alloc] initWithHost:GRPCGetLocalInteropTestServerAddressPlainText() path:kUnaryCallMethod.HTTPPath safety:GRPCCallSafetyDefault]; GRPCMutableCallOptions *options = [[GRPCMutableCallOptions alloc] init]; options.transportType = GRPCTransportTypeInsecure; options.flowControlEnabled = YES; __block BOOL closed = NO; GRPCCall2 *call = [[GRPCCall2 alloc] initWithRequestOptions:callRequest responseHandler:[[GRPCBlockCallbackResponseHandler alloc] initWithInitialMetadataCallback:nil messageCallback:^(NSData *message) { [expectPassedMessage fulfill]; XCTAssertFalse(closed); } closeCallback:^(NSDictionary *ttrailers, NSError *error) { closed = YES; [expectPassedClose fulfill]; }] callOptions:options]; [call receiveNextMessages:1]; [call start]; [call writeData:[request data]]; [call finish]; [self waitForExpectationsWithTimeout:kInvertedTimeout handler:nil]; } - (void)testFlowControlReadReadyAfterStart { __weak XCTestExpectation *expectPassedMessage = [self expectationWithDescription:@"Message delivered with receiveNextMessage"]; __weak XCTestExpectation *expectPassedClose = [self expectationWithDescription:@"Close delivered with receiveNextMessage"]; RMTStreamingOutputCallRequest *request = [RMTStreamingOutputCallRequest message]; RMTResponseParameters *parameters = [RMTResponseParameters message]; parameters.size = kSimpleDataLength; [request.responseParametersArray addObject:parameters]; request.payload.body = [NSMutableData dataWithLength:kSimpleDataLength]; GRPCRequestOptions *callRequest = [[GRPCRequestOptions alloc] initWithHost:GRPCGetLocalInteropTestServerAddressPlainText() path:kUnaryCallMethod.HTTPPath safety:GRPCCallSafetyDefault]; GRPCMutableCallOptions *options = [[GRPCMutableCallOptions alloc] init]; options.transportType = GRPCTransportTypeInsecure; options.flowControlEnabled = YES; __block BOOL closed = NO; GRPCCall2 *call = [[GRPCCall2 alloc] initWithRequestOptions:callRequest responseHandler:[[GRPCBlockCallbackResponseHandler alloc] initWithInitialMetadataCallback:nil messageCallback:^(NSData *message) { [expectPassedMessage fulfill]; XCTAssertFalse(closed); } closeCallback:^(NSDictionary *trailers, NSError *error) { closed = YES; [expectPassedClose fulfill]; }] callOptions:options]; [call start]; [call receiveNextMessages:1]; [call writeData:[request data]]; [call finish]; [self waitForExpectationsWithTimeout:kTestTimeout handler:nil]; } - (void)testFlowControlReadNonBlockingFailure { __weak XCTestExpectation *completion = [self expectationWithDescription:@"RPC completed."]; GRPCRequestOptions *requestOptions = [[GRPCRequestOptions alloc] initWithHost:GRPCGetLocalInteropTestServerAddressPlainText() path:kUnaryCallMethod.HTTPPath safety:GRPCCallSafetyDefault]; GRPCMutableCallOptions *options = [[GRPCMutableCallOptions alloc] init]; options.flowControlEnabled = YES; options.transportType = GRPCTransportTypeInsecure; RMTSimpleRequest *request = [RMTSimpleRequest message]; request.payload.body = [NSMutableData dataWithLength:options.responseSizeLimit]; RMTEchoStatus *status = [RMTEchoStatus message]; status.code = 2; status.message = @"test"; request.responseStatus = status; GRPCCall2 *call = [[GRPCCall2 alloc] initWithRequestOptions:requestOptions responseHandler:[[GRPCBlockCallbackResponseHandler alloc] initWithInitialMetadataCallback:nil messageCallback:^(NSData *data) { XCTFail(@"Received unexpected message"); } closeCallback:^(NSDictionary *trailingMetadata, NSError *error) { XCTAssertNotNil(error, @"Expecting non-nil error"); XCTAssertEqual(error.code, 2); [completion fulfill]; }] callOptions:options]; [call writeData:[request data]]; [call start]; [call finish]; [self waitForExpectationsWithTimeout:kTestTimeout handler:nil]; } @end