/* * * 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 #include #import "../version.h" // The server address is derived from preprocessor macro, which is // in turn derived from environment variable of the same name. #define NSStringize_helper(x) #x #define NSStringize(x) @NSStringize_helper(x) static NSString *const kHostAddress = NSStringize(HOST_PORT_LOCAL); static NSString *const kRemoteSSLHost = NSStringize(HOST_PORT_REMOTE); // 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 *kFullDuplexCallMethod; static const int kSimpleDataLength = 100; static const NSTimeInterval kTestTimeout = 16; // Reveal the _class ivar for testing access @interface GRPCCall2 () { @public GRPCCall *_call; } @end // Convenience class to use blocks as callbacks @interface ClientTestsBlockCallbacks : NSObject - (instancetype)initWithInitialMetadataCallback:(void (^)(NSDictionary *))initialMetadataCallback messageCallback:(void (^)(id))messageCallback closeCallback:(void (^)(NSDictionary *, NSError *))closeCallback; @end @implementation ClientTestsBlockCallbacks { void (^_initialMetadataCallback)(NSDictionary *); void (^_messageCallback)(id); void (^_closeCallback)(NSDictionary *, NSError *); dispatch_queue_t _dispatchQueue; } - (instancetype)initWithInitialMetadataCallback:(void (^)(NSDictionary *))initialMetadataCallback messageCallback:(void (^)(id))messageCallback closeCallback:(void (^)(NSDictionary *, NSError *))closeCallback { if ((self = [super init])) { _initialMetadataCallback = initialMetadataCallback; _messageCallback = messageCallback; _closeCallback = closeCallback; _dispatchQueue = dispatch_queue_create(nil, DISPATCH_QUEUE_SERIAL); } return self; } - (void)didReceiveInitialMetadata:(NSDictionary *)initialMetadata { if (self->_initialMetadataCallback) { self->_initialMetadataCallback(initialMetadata); } } - (void)didReceiveRawMessage:(GPBMessage *)message { if (self->_messageCallback) { self->_messageCallback(message); } } - (void)didCloseWithTrailingMetadata:(NSDictionary *)trailingMetadata error:(NSError *)error { if (self->_closeCallback) { self->_closeCallback(trailingMetadata, error); } } - (dispatch_queue_t)dispatchQueue { return _dispatchQueue; } @end @interface CallAPIv2Tests : XCTestCase @end @implementation CallAPIv2Tests - (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"]; kFullDuplexCallMethod = [[GRPCProtoMethod alloc] initWithPackage:kPackage service:kService method:@"FullDuplexCall"]; } - (void)testMetadata { __weak XCTestExpectation *expectation = [self expectationWithDescription:@"RPC unauthorized."]; RMTSimpleRequest *request = [RMTSimpleRequest message]; request.fillUsername = YES; request.fillOauthScope = YES; GRPCRequestOptions *callRequest = [[GRPCRequestOptions alloc] initWithHost:(NSString *)kRemoteSSLHost path:kUnaryCallMethod.HTTPPath safety:GRPCCallSafetyDefault]; __block NSDictionary *init_md; __block NSDictionary *trailing_md; GRPCMutableCallOptions *options = [[GRPCMutableCallOptions alloc] init]; options.oauth2AccessToken = @"bogusToken"; GRPCCall2 *call = [[GRPCCall2 alloc] initWithRequestOptions:callRequest responseHandler:[[ClientTestsBlockCallbacks alloc] initWithInitialMetadataCallback:^(NSDictionary *initialMetadata) { init_md = initialMetadata; } messageCallback:^(id message) { XCTFail(@"Received unexpected response."); } closeCallback:^(NSDictionary *trailingMetadata, NSError *error) { trailing_md = trailingMetadata; if (error) { XCTAssertEqual(error.code, 16, @"Finished with unexpected error: %@", error); XCTAssertEqualObjects(init_md, error.userInfo[kGRPCHeadersKey]); XCTAssertEqualObjects(trailing_md, error.userInfo[kGRPCTrailersKey]); NSString *challengeHeader = init_md[@"www-authenticate"]; XCTAssertGreaterThan(challengeHeader.length, 0, @"No challenge in response headers %@", init_md); [expectation fulfill]; } }] callOptions:options]; [call start]; [call writeData:[request data]]; [call finish]; [self waitForExpectationsWithTimeout:kTestTimeout handler:nil]; } - (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:kHostAddress 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:[[ClientTestsBlockCallbacks alloc] initWithInitialMetadataCallback:^( NSDictionary *initialMetadata) { NSString *userAgent = initialMetadata[@"x-grpc-test-echo-useragent"]; // Test the regex is correct NSString *expectedUserAgent = @"Foo grpc-objc/"; expectedUserAgent = [expectedUserAgent stringByAppendingString:GRPC_OBJC_VERSION_STRING]; expectedUserAgent = [expectedUserAgent stringByAppendingString:@" grpc-c/"]; expectedUserAgent = [expectedUserAgent stringByAppendingString:GRPC_C_VERSION_STRING]; expectedUserAgent = [expectedUserAgent stringByAppendingString:@" (ios; chttp2; "]; expectedUserAgent = [expectedUserAgent stringByAppendingString:[NSString stringWithUTF8String:grpc_g_stands_for()]]; expectedUserAgent = [expectedUserAgent stringByAppendingString:@")"]; 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:kHostAddress path:kEmptyCallMethod.HTTPPath safety:GRPCCallSafetyDefault]; GRPCMutableCallOptions *options = [[GRPCMutableCallOptions alloc] init]; options.transportType = GRPCTransportTypeInsecure; options.authTokenProvider = self; __block GRPCCall2 *call = [[GRPCCall2 alloc] initWithRequestOptions:requestOptions responseHandler:[[ClientTestsBlockCallbacks 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:kHostAddress 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:[[ClientTestsBlockCallbacks 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)testIdempotentProtoRPC { __weak XCTestExpectation *response = [self expectationWithDescription:@"Expected response."]; __weak XCTestExpectation *completion = [self expectationWithDescription:@"RPC completed."]; RMTSimpleRequest *request = [RMTSimpleRequest message]; request.responseSize = kSimpleDataLength; request.fillUsername = YES; request.fillOauthScope = YES; GRPCRequestOptions *requestOptions = [[GRPCRequestOptions alloc] initWithHost:kHostAddress path:kUnaryCallMethod.HTTPPath safety:GRPCCallSafetyIdempotentRequest]; GRPCMutableCallOptions *options = [[GRPCMutableCallOptions alloc] init]; options.transportType = GRPCTransportTypeInsecure; GRPCCall2 *call = [[GRPCCall2 alloc] initWithRequestOptions:requestOptions responseHandler:[[ClientTestsBlockCallbacks alloc] initWithInitialMetadataCallback:nil messageCallback:^(id message) { NSData *data = (NSData *)message; XCTAssertNotNil(data, @"nil value received as response."); XCTAssertGreaterThan(data.length, 0, @"Empty response received."); RMTSimpleResponse *responseProto = [RMTSimpleResponse parseFromData:data error:NULL]; // We expect empty strings, not nil: XCTAssertNotNil(responseProto.username, @"Response's username is nil."); XCTAssertNotNil(responseProto.oauthScope, @"Response's OAuth scope is nil."); [response fulfill]; } closeCallback:^(NSDictionary *trailingMetadata, NSError *error) { XCTAssertNil(error, @"Finished with unexpected error: %@", error); [completion fulfill]; }] callOptions:options]; [call start]; [call writeData:[request data]]; [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; GRPCRequestOptions *requestOptions = [[GRPCRequestOptions alloc] initWithHost:kHostAddress path:kFullDuplexCallMethod.HTTPPath safety:GRPCCallSafetyDefault]; GRPCCall2 *call = [[GRPCCall2 alloc] initWithRequestOptions:requestOptions responseHandler: [[ClientTestsBlockCallbacks 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."); 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 kDummyAddress = [NSString stringWithFormat:@"127.0.0.1:10000"]; GRPCRequestOptions *requestOptions = [[GRPCRequestOptions alloc] initWithHost:kDummyAddress path:@"/dummy/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:[[ClientTestsBlockCallbacks 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:kHostAddress path:kUnaryCallMethod.HTTPPath safety:GRPCCallSafetyDefault]; GRPCMutableCallOptions *options = [[GRPCMutableCallOptions alloc] init]; options.transportType = GRPCTransportTypeInsecure; options.compressionAlgorithm = GRPCCompressGzip; GRPCCall2 *call = [[GRPCCall2 alloc] initWithRequestOptions:requestOptions responseHandler:[[ClientTestsBlockCallbacks 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]; } @end