blob: 2a169800a0971ef67fa1fd40d2a966768390ce29 [file] [log] [blame]
/*
*
* Copyright 2015 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 <UIKit/UIKit.h>
#import <XCTest/XCTest.h>
#import <grpc/grpc.h>
#import <GRPCClient/GRPCCall+ChannelArg.h>
#import <GRPCClient/GRPCCall+OAuth2.h>
#import <GRPCClient/GRPCCall+Tests.h>
#import <GRPCClient/GRPCCall.h>
#import <GRPCClient/internal_testing/GRPCCall+InternalTests.h>
#import <ProtoRPC/ProtoMethod.h>
#import <RemoteTest/Messages.pbobjc.h>
#import <RxLibrary/GRXBufferedPipe.h>
#import <RxLibrary/GRXWriteable.h>
#import <RxLibrary/GRXWriter+Immediate.h>
#include <netinet/in.h>
#import "version.h"
#define TEST_TIMEOUT 16
static NSString *const kHostAddress = @"localhost:5050";
static NSString *const kPackage = @"grpc.testing";
static NSString *const kService = @"TestService";
static NSString *const kRemoteSSLHost = @"grpc-test.sandbox.googleapis.com";
static GRPCProtoMethod *kInexistentMethod;
static GRPCProtoMethod *kEmptyCallMethod;
static GRPCProtoMethod *kUnaryCallMethod;
static GRPCProtoMethod *kFullDuplexCallMethod;
/** Observer class for testing that responseMetadata is KVO-compliant */
@interface PassthroughObserver : NSObject
- (instancetype)initWithCallback:(void (^)(NSString *, id, NSDictionary *))callback
NS_DESIGNATED_INITIALIZER;
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context;
@end
@implementation PassthroughObserver {
void (^_callback)(NSString *, id, NSDictionary *);
}
- (instancetype)init {
return [self initWithCallback:nil];
}
- (instancetype)initWithCallback:(void (^)(NSString *, id, NSDictionary *))callback {
if (!callback) {
return nil;
}
if ((self = [super init])) {
_callback = callback;
}
return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
_callback(keyPath, object, change);
[object removeObserver:self forKeyPath:keyPath];
}
@end
#pragma mark Tests
/**
* A few tests similar to InteropTests, but which use the generic gRPC client (GRPCCall) rather than
* a generated proto library on top of it. Its RPCs are sent to a local cleartext server.
*
* TODO(jcanizales): Run them also against a local SSL server and against a remote server.
*/
@interface GRPCClientTests : XCTestCase
@end
@implementation GRPCClientTests
+ (void)setUp {
NSLog(@"GRPCClientTests Started");
}
- (void)setUp {
// Add a custom user agent prefix that will be used in test
[GRPCCall setUserAgentPrefix:@"Foo" forHost:kHostAddress];
// Register test server as non-SSL.
[GRPCCall useInsecureConnectionsForHost:kHostAddress];
// 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)testConnectionToRemoteServer {
__weak XCTestExpectation *expectation = [self expectationWithDescription:@"Server reachable."];
GRPCCall *call = [[GRPCCall alloc] initWithHost:kHostAddress
path:kInexistentMethod.HTTPPath
requestsWriter:[GRXWriter writerWithValue:[NSData data]]];
id<GRXWriteable> responsesWriteable =
[[GRXWriteable alloc] initWithValueHandler:^(NSData *value) {
XCTFail(@"Received unexpected response: %@", value);
}
completionHandler:^(NSError *errorOrNil) {
XCTAssertNotNil(errorOrNil, @"Finished without error!");
XCTAssertEqual(errorOrNil.code, 12, @"Finished with unexpected error: %@", errorOrNil);
[expectation fulfill];
}];
[call startWithWriteable:responsesWriteable];
[self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil];
}
- (void)testEmptyRPC {
__weak XCTestExpectation *response =
[self expectationWithDescription:@"Empty response received."];
__weak XCTestExpectation *completion = [self expectationWithDescription:@"Empty RPC completed."];
GRPCCall *call = [[GRPCCall alloc] initWithHost:kHostAddress
path:kEmptyCallMethod.HTTPPath
requestsWriter:[GRXWriter writerWithValue:[NSData data]]];
id<GRXWriteable> responsesWriteable =
[[GRXWriteable alloc] initWithValueHandler:^(NSData *value) {
XCTAssertNotNil(value, @"nil value received as response.");
XCTAssertEqual([value length], 0, @"Non-empty response received: %@", value);
[response fulfill];
}
completionHandler:^(NSError *errorOrNil) {
XCTAssertNil(errorOrNil, @"Finished with unexpected error: %@", errorOrNil);
[completion fulfill];
}];
[call startWithWriteable:responsesWriteable];
[self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil];
}
- (void)testSimpleProtoRPC {
__weak XCTestExpectation *response = [self expectationWithDescription:@"Expected response."];
__weak XCTestExpectation *completion = [self expectationWithDescription:@"RPC completed."];
RMTSimpleRequest *request = [RMTSimpleRequest message];
request.responseSize = 100;
request.fillUsername = YES;
request.fillOauthScope = YES;
GRXWriter *requestsWriter = [GRXWriter writerWithValue:[request data]];
GRPCCall *call = [[GRPCCall alloc] initWithHost:kHostAddress
path:kUnaryCallMethod.HTTPPath
requestsWriter:requestsWriter];
id<GRXWriteable> responsesWriteable =
[[GRXWriteable alloc] initWithValueHandler:^(NSData *value) {
XCTAssertNotNil(value, @"nil value received as response.");
XCTAssertGreaterThan(value.length, 0, @"Empty response received.");
RMTSimpleResponse *responseProto = [RMTSimpleResponse parseFromData:value 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];
}
completionHandler:^(NSError *errorOrNil) {
XCTAssertNil(errorOrNil, @"Finished with unexpected error: %@", errorOrNil);
[completion fulfill];
}];
[call startWithWriteable:responsesWriteable];
[self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil];
}
- (void)testMetadata {
__weak XCTestExpectation *expectation = [self expectationWithDescription:@"RPC unauthorized."];
RMTSimpleRequest *request = [RMTSimpleRequest message];
request.fillUsername = YES;
request.fillOauthScope = YES;
GRXWriter *requestsWriter = [GRXWriter writerWithValue:[request data]];
GRPCCall *call = [[GRPCCall alloc] initWithHost:kRemoteSSLHost
path:kUnaryCallMethod.HTTPPath
requestsWriter:requestsWriter];
call.oauth2AccessToken = @"bogusToken";
id<GRXWriteable> responsesWriteable =
[[GRXWriteable alloc] initWithValueHandler:^(NSData *value) {
XCTFail(@"Received unexpected response: %@", value);
}
completionHandler:^(NSError *errorOrNil) {
XCTAssertNotNil(errorOrNil, @"Finished without error!");
XCTAssertEqual(errorOrNil.code, 16, @"Finished with unexpected error: %@", errorOrNil);
XCTAssertEqualObjects(call.responseHeaders, errorOrNil.userInfo[kGRPCHeadersKey],
@"Headers in the NSError object and call object differ.");
XCTAssertEqualObjects(call.responseTrailers, errorOrNil.userInfo[kGRPCTrailersKey],
@"Trailers in the NSError object and call object differ.");
NSString *challengeHeader = call.oauth2ChallengeHeader;
XCTAssertGreaterThan(challengeHeader.length, 0, @"No challenge in response headers %@",
call.responseHeaders);
[expectation fulfill];
}];
[call startWithWriteable:responsesWriteable];
[self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil];
}
- (void)testResponseMetadataKVO {
__weak XCTestExpectation *response =
[self expectationWithDescription:@"Empty response received."];
__weak XCTestExpectation *completion = [self expectationWithDescription:@"Empty RPC completed."];
__weak XCTestExpectation *metadata = [self expectationWithDescription:@"Metadata changed."];
GRPCCall *call = [[GRPCCall alloc] initWithHost:kHostAddress
path:kEmptyCallMethod.HTTPPath
requestsWriter:[GRXWriter writerWithValue:[NSData data]]];
PassthroughObserver *observer = [[PassthroughObserver alloc]
initWithCallback:^(NSString *keypath, id object, NSDictionary *change) {
if ([keypath isEqual:@"responseHeaders"]) {
[metadata fulfill];
}
}];
[call addObserver:observer forKeyPath:@"responseHeaders" options:0 context:NULL];
id<GRXWriteable> responsesWriteable =
[[GRXWriteable alloc] initWithValueHandler:^(NSData *value) {
XCTAssertNotNil(value, @"nil value received as response.");
XCTAssertEqual([value length], 0, @"Non-empty response received: %@", value);
[response fulfill];
}
completionHandler:^(NSError *errorOrNil) {
XCTAssertNil(errorOrNil, @"Finished with unexpected error: %@", errorOrNil);
[completion fulfill];
}];
[call startWithWriteable:responsesWriteable];
[self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil];
}
- (void)testUserAgentPrefix {
__weak XCTestExpectation *response =
[self expectationWithDescription:@"Empty response received."];
__weak XCTestExpectation *completion = [self expectationWithDescription:@"Empty RPC completed."];
GRPCCall *call = [[GRPCCall alloc] initWithHost:kHostAddress
path:kEmptyCallMethod.HTTPPath
requestsWriter:[GRXWriter writerWithValue:[NSData data]]];
// Setting this special key in the header will cause the interop server to echo back the
// user-agent value, which we confirm.
call.requestHeaders[@"x-grpc-test-echo-useragent"] = @"";
id<GRXWriteable> responsesWriteable =
[[GRXWriteable alloc] initWithValueHandler:^(NSData *value) {
XCTAssertNotNil(value, @"nil value received as response.");
XCTAssertEqual([value length], 0, @"Non-empty response received: %@", value);
NSString *userAgent = call.responseHeaders[@"x-grpc-test-echo-useragent"];
NSError *error = nil;
// 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);
// 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");
[response fulfill];
}
completionHandler:^(NSError *errorOrNil) {
XCTAssertNil(errorOrNil, @"Finished with unexpected error: %@", errorOrNil);
[completion fulfill];
}];
[call startWithWriteable:responsesWriteable];
[self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil];
}
- (void)testTrailers {
__weak XCTestExpectation *response =
[self expectationWithDescription:@"Empty response received."];
__weak XCTestExpectation *completion = [self expectationWithDescription:@"Empty RPC completed."];
GRPCCall *call = [[GRPCCall alloc] initWithHost:kHostAddress
path:kEmptyCallMethod.HTTPPath
requestsWriter:[GRXWriter writerWithValue:[NSData data]]];
// Setting this special key in the header will cause the interop server to echo back the
// trailer data.
const unsigned char raw_bytes[] = {1, 2, 3, 4};
NSData *trailer_data = [NSData dataWithBytes:raw_bytes length:sizeof(raw_bytes)];
call.requestHeaders[@"x-grpc-test-echo-trailing-bin"] = trailer_data;
id<GRXWriteable> responsesWriteable =
[[GRXWriteable alloc] initWithValueHandler:^(NSData *value) {
XCTAssertNotNil(value, @"nil value received as response.");
XCTAssertEqual([value length], 0, @"Non-empty response received: %@", value);
[response fulfill];
}
completionHandler:^(NSError *errorOrNil) {
XCTAssertNil(errorOrNil, @"Finished with unexpected error: %@", errorOrNil);
XCTAssertEqualObjects((NSData *)call.responseTrailers[@"x-grpc-test-echo-trailing-bin"],
trailer_data, @"Did not receive expected trailer");
[completion fulfill];
}];
[call startWithWriteable:responsesWriteable];
[self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil];
}
// TODO(makarandd): Move to a different file that contains only unit tests
- (void)testExceptions {
// Try to set parameters to nil for GRPCCall. This should cause an exception
@try {
(void)[[GRPCCall alloc] initWithHost:nil path:nil requestsWriter:nil];
XCTFail(@"Did not receive an exception when parameters are nil");
} @catch (NSException *theException) {
NSLog(@"Received exception as expected: %@", theException.name);
}
// Set state to Finished by force
GRXWriter *requestsWriter = [GRXWriter emptyWriter];
[requestsWriter finishWithError:nil];
@try {
(void)[[GRPCCall alloc] initWithHost:kHostAddress
path:kUnaryCallMethod.HTTPPath
requestsWriter:requestsWriter];
XCTFail(@"Did not receive an exception when GRXWriter has incorrect state.");
} @catch (NSException *theException) {
NSLog(@"Received exception as expected: %@", theException.name);
}
}
- (void)testIdempotentProtoRPC {
__weak XCTestExpectation *response = [self expectationWithDescription:@"Expected response."];
__weak XCTestExpectation *completion = [self expectationWithDescription:@"RPC completed."];
RMTSimpleRequest *request = [RMTSimpleRequest message];
request.responseSize = 100;
request.fillUsername = YES;
request.fillOauthScope = YES;
GRXWriter *requestsWriter = [GRXWriter writerWithValue:[request data]];
GRPCCall *call = [[GRPCCall alloc] initWithHost:kHostAddress
path:kUnaryCallMethod.HTTPPath
requestsWriter:requestsWriter];
[GRPCCall setCallSafety:GRPCCallSafetyIdempotentRequest
host:kHostAddress
path:kUnaryCallMethod.HTTPPath];
id<GRXWriteable> responsesWriteable =
[[GRXWriteable alloc] initWithValueHandler:^(NSData *value) {
XCTAssertNotNil(value, @"nil value received as response.");
XCTAssertGreaterThan(value.length, 0, @"Empty response received.");
RMTSimpleResponse *responseProto = [RMTSimpleResponse parseFromData:value 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];
}
completionHandler:^(NSError *errorOrNil) {
XCTAssertNil(errorOrNil, @"Finished with unexpected error: %@", errorOrNil);
[completion fulfill];
}];
[call startWithWriteable:responsesWriteable];
[self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil];
}
- (void)testAlternateDispatchQueue {
const int32_t kPayloadSize = 100;
RMTSimpleRequest *request = [RMTSimpleRequest message];
request.responseSize = kPayloadSize;
__weak XCTestExpectation *expectation1 =
[self expectationWithDescription:@"AlternateDispatchQueue1"];
// Use default (main) dispatch queue
NSString *main_queue_label =
[NSString stringWithUTF8String:dispatch_queue_get_label(dispatch_get_main_queue())];
GRXWriter *requestsWriter1 = [GRXWriter writerWithValue:[request data]];
GRPCCall *call1 = [[GRPCCall alloc] initWithHost:kHostAddress
path:kUnaryCallMethod.HTTPPath
requestsWriter:requestsWriter1];
id<GRXWriteable> responsesWriteable1 =
[[GRXWriteable alloc] initWithValueHandler:^(NSData *value) {
NSString *label =
[NSString stringWithUTF8String:dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)];
XCTAssert([label isEqualToString:main_queue_label]);
[expectation1 fulfill];
}
completionHandler:^(NSError *errorOrNil){
}];
[call1 startWithWriteable:responsesWriteable1];
[self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil];
// Use a custom queue
__weak XCTestExpectation *expectation2 =
[self expectationWithDescription:@"AlternateDispatchQueue2"];
NSString *queue_label = @"test.queue1";
dispatch_queue_t queue = dispatch_queue_create([queue_label UTF8String], DISPATCH_QUEUE_SERIAL);
GRXWriter *requestsWriter2 = [GRXWriter writerWithValue:[request data]];
GRPCCall *call2 = [[GRPCCall alloc] initWithHost:kHostAddress
path:kUnaryCallMethod.HTTPPath
requestsWriter:requestsWriter2];
[call2 setResponseDispatchQueue:queue];
id<GRXWriteable> responsesWriteable2 =
[[GRXWriteable alloc] initWithValueHandler:^(NSData *value) {
NSString *label =
[NSString stringWithUTF8String:dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)];
XCTAssert([label isEqualToString:queue_label]);
[expectation2 fulfill];
}
completionHandler:^(NSError *errorOrNil){
}];
[call2 startWithWriteable:responsesWriteable2];
[self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil];
}
- (void)testTimeout {
__weak XCTestExpectation *completion = [self expectationWithDescription:@"RPC completed."];
GRXBufferedPipe *pipe = [GRXBufferedPipe pipe];
GRPCCall *call = [[GRPCCall alloc] initWithHost:kHostAddress
path:kFullDuplexCallMethod.HTTPPath
requestsWriter:pipe];
id<GRXWriteable> responsesWriteable =
[[GRXWriteable alloc] initWithValueHandler:^(NSData *value) {
XCTAssert(0, @"Failure: response received; Expect: no response received.");
}
completionHandler:^(NSError *errorOrNil) {
XCTAssertNotNil(errorOrNil,
@"Failure: no error received; Expect: receive deadline exceeded.");
XCTAssertEqual(errorOrNil.code, GRPCErrorCodeDeadlineExceeded);
[completion fulfill];
}];
call.timeout = 0.001;
[call startWithWriteable:responsesWriteable];
[self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil];
}
- (int)findFreePort {
struct sockaddr_in addr;
unsigned int addr_len = sizeof(addr);
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
int fd = socket(AF_INET, SOCK_STREAM, 0);
XCTAssertEqual(bind(fd, (struct sockaddr *)&addr, sizeof(addr)), 0);
XCTAssertEqual(getsockname(fd, (struct sockaddr *)&addr, &addr_len), 0);
XCTAssertEqual(addr_len, sizeof(addr));
close(fd);
return addr.sin_port;
}
- (void)testErrorCode {
int port = [self findFreePort];
NSString *const kDummyAddress = [NSString stringWithFormat:@"localhost:%d", port];
__weak XCTestExpectation *completion =
[self expectationWithDescription:@"Received correct error code."];
GRPCCall *call = [[GRPCCall alloc] initWithHost:kDummyAddress
path:kEmptyCallMethod.HTTPPath
requestsWriter:[GRXWriter writerWithValue:[NSData data]]];
id<GRXWriteable> responsesWriteable =
[[GRXWriteable alloc] initWithValueHandler:^(NSData *value) {
// Should not reach here
XCTAssert(NO);
}
completionHandler:^(NSError *errorOrNil) {
XCTAssertNotNil(errorOrNil, @"Finished with no error");
XCTAssertEqual(errorOrNil.code, GRPC_STATUS_UNAVAILABLE);
[completion fulfill];
}];
[call startWithWriteable:responsesWriteable];
[self waitForExpectationsWithTimeout:TEST_TIMEOUT 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:@"8.8.8.8:1"];
GRPCCall *call = [[GRPCCall alloc] initWithHost:kDummyAddress
path:@""
requestsWriter:[GRXWriter writerWithValue:[NSData data]]];
[GRPCCall setMinConnectTimeout:timeout * 1000
initialBackoff:backoff * 1000
maxBackoff:0
forHost:kDummyAddress];
NSDate *startTime = [NSDate date];
id<GRXWriteable> responsesWriteable = [[GRXWriteable alloc] initWithValueHandler:^(id value) {
XCTAssert(NO, @"Received message. Should not reach here");
}
completionHandler:^(NSError *errorOrNil) {
XCTAssertNotNil(errorOrNil, @"Finished with no error");
// The call must fail before maxConnectTime. However there is no lower bound on the time
// taken for connection. A shorter time happens when connection is actively refused
// by 8.8.8.8:1 before maxConnectTime elapsed.
XCTAssertLessThan([[NSDate date] timeIntervalSinceDate:startTime],
maxConnectTime + kMargin);
[completion fulfill];
}];
[call startWithWriteable:responsesWriteable];
[self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil];
}
// The numbers of the following three tests are selected to be smaller than the default values of
// initial backoff (1s) and min_connect_timeout (20s), so that if they fail we know the default
// values fail to be overridden by the channel args.
- (void)testTimeoutBackoff2 {
[self testTimeoutBackoffWithTimeout:0.7 Backoff:0.3];
}
- (void)testTimeoutBackoff3 {
[self testTimeoutBackoffWithTimeout:0.3 Backoff:0.7];
}
@end