From a9b919a5e999d6aa8deb4c6e7c8b39e0dde82a0c Mon Sep 17 00:00:00 2001 From: Jakob Andersen Date: Wed, 27 Sep 2017 13:57:24 +0200 Subject: [PATCH] Added client-side interop tests. (#32) --- interop/bin/client.dart | 99 ++++ interop/ca.pem | 15 + interop/lib/src/client.dart | 1029 +++++++++++++++++++++++++++++++++++ interop/pubspec.yaml | 1 + 4 files changed, 1144 insertions(+) create mode 100644 interop/bin/client.dart create mode 100644 interop/ca.pem create mode 100644 interop/lib/src/client.dart diff --git a/interop/bin/client.dart b/interop/bin/client.dart new file mode 100644 index 0000000..712ebe6 --- /dev/null +++ b/interop/bin/client.dart @@ -0,0 +1,99 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:args/args.dart'; + +import 'package:interop/src/client.dart'; + +const _serverHostArgument = 'server_host'; +const _serverHostOverrideArgument = 'server_host_override'; +const _serverPortArgument = 'server_port'; +const _testCaseArgument = 'test_case'; +const _useTLSArgument = 'use_tls'; +const _useTestCAArgument = 'use_test_ca'; +const _defaultServiceAccountArgument = 'default_service_account'; +const _oauthScopeArgument = 'oauth_scope'; +const _serviceAccountKeyFileArgument = 'service_account_key_file'; + +/// Clients implement test cases that test certain functionally. Each client is +/// provided the test case it is expected to run as a command-line parameter. +/// Names should be lowercase and without spaces. +/// +/// Clients should accept these arguments: +/// +/// * --server_host=HOSTNAME +/// * The server host to connect to. For example, "localhost" or "127.0.0.1" +/// * --server_host_override=HOSTNAME +/// * The server host to claim to be connecting to, for use in TLS and +/// HTTP/2 :authority header. If unspecified, the value of --server_host +/// will be used +/// * --server_port=PORT +/// * The server port to connect to. For example, "8080" +/// * --test_case=TESTCASE +/// * The name of the test case to execute. For example, "empty_unary" +/// * --use_tls=BOOLEAN +/// * Whether to use a plaintext or encrypted connection +/// * --use_test_ca=BOOLEAN +/// * Whether to replace platform root CAs with ca.pem as the CA root +/// * --default_service_account=ACCOUNT_EMAIL +/// * Email of the GCE default service account. +/// * --oauth_scope=SCOPE +/// * OAuth scope. For example, "https://www.googleapis.com/auth/xapi.zoo" +/// * --service_account_key_file=PATH +/// * The path to the service account JSON key file generated from GCE +/// developer console. +/// +/// Clients must support TLS with ALPN. Clients must not disable certificate +/// checking. +Future main(List args) async { + final argumentParser = new ArgParser(); + argumentParser.addOption(_serverHostArgument, + help: 'The server host to connect to. For example, "localhost" or ' + '"127.0.0.1".'); + argumentParser.addOption(_serverHostOverrideArgument, + help: 'The server host to claim to be connecting to, for use in TLS and ' + 'HTTP/2 :authority header. If unspecified, the value of ' + '--server_host will be used.'); + argumentParser.addOption(_serverPortArgument, + help: 'The server port to connect to. For example, "8080".'); + argumentParser.addOption(_testCaseArgument, + help: + 'The name of the test case to execute. For example, "empty_unary".'); + argumentParser.addOption(_useTLSArgument, + defaultsTo: 'false', + help: 'Whether to use a plaintext or encrypted connection.'); + argumentParser.addOption(_useTestCAArgument, + help: 'Whether to replace platform root CAs with ca.pem as the CA root.'); + argumentParser.addOption(_defaultServiceAccountArgument, + help: 'Email of the GCE default service account.'); + argumentParser.addOption(_oauthScopeArgument, + help: 'OAuth scope. For example, ' + '"https://www.googleapis.com/auth/xapi.zoo".'); + argumentParser.addOption(_serviceAccountKeyFileArgument, + help: 'The path to the service account JSON key file generated from GCE ' + 'developer console.'); + final arguments = argumentParser.parse(args); + + final testClient = new Tester(); + + testClient.serverHost = arguments[_serverHostArgument]; + testClient.serverHostOverride = arguments[_serverHostOverrideArgument]; + testClient.serverPort = arguments[_serverPortArgument]; + testClient.testCase = arguments[_testCaseArgument]; + testClient.useTls = arguments[_useTLSArgument]; + testClient.useTestCA = arguments[_useTestCAArgument]; + testClient.defaultServiceAccount = arguments[_defaultServiceAccountArgument]; + testClient.oauthScope = arguments[_oauthScopeArgument]; + testClient.serviceAccountKeyFile = arguments[_serviceAccountKeyFileArgument]; + + if (!testClient.validate()) { + print(argumentParser.usage); + return -1; + } + await testClient.runTest(); + print('Passed.'); + return 0; +} diff --git a/interop/ca.pem b/interop/ca.pem new file mode 100644 index 0000000..6c8511a --- /dev/null +++ b/interop/ca.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla +Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0 +YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT +BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7 ++L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu +g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd +Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau +sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m +oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG +Dfcog5wrJytaQ6UA0wE= +-----END CERTIFICATE----- diff --git a/interop/lib/src/client.dart b/interop/lib/src/client.dart new file mode 100644 index 0000000..ae25c36 --- /dev/null +++ b/interop/lib/src/client.dart @@ -0,0 +1,1029 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:collection/collection.dart'; +import 'package:grpc/grpc.dart'; + +import 'package:interop/src/generated/empty.pb.dart'; +import 'package:interop/src/generated/messages.pb.dart'; +import 'package:interop/src/generated/test.pbgrpc.dart'; + +const _headerEchoKey = 'x-grpc-test-echo-initial'; +const _headerEchoData = 'test_initial_metadata_value'; + +const _trailerEchoKey = 'x-grpc-test-echo-trailing-bin'; +const _trailerEchoData = 'q6ur'; // 0xababab in base64 + +class Tester { + String serverHost; + String serverHostOverride; + int _serverPort; + String testCase; + bool _useTls; + bool _useTestCA; + String defaultServiceAccount; + String oauthScope; + String serviceAccountKeyFile; + + void set serverPort(String value) { + if (value == null) { + _serverPort = null; + return; + } + try { + _serverPort = int.parse(value); + } catch (e) { + print('Invalid port "$value": $e'); + } + } + + void set useTls(String value) { + _useTls = value != 'false'; + } + + void set useTestCA(String value) { + _useTestCA = value == 'true'; + } + + TestServiceClient client; + UnimplementedServiceClient unimplementedServiceClient; + + bool validate() { + if (serverHost == null) { + print('Must specify --server_host'); + return false; + } + if (_serverPort == null) { + print('Must specify --server_port'); + return false; + } + + return true; + } + + Future runTest() async { + ChannelOptions options; + if (_useTls) { + List trustedRoot; + if (_useTestCA) { + trustedRoot = new File('ca.pem').readAsBytesSync(); + } + options = new ChannelOptions.secure( + certificate: trustedRoot, authority: serverHostOverride); + } else { + options = new ChannelOptions.insecure(); + } + + final channel = + new ClientChannel(serverHost, port: _serverPort, options: options); + client = new TestServiceClient(channel); + unimplementedServiceClient = new UnimplementedServiceClient(channel); + await runTestCase(); + await channel.shutdown(); + } + + Future runTestCase() async { + switch (testCase) { + case 'empty_unary': + return emptyUnary(); + case 'cacheable_unary': + return cacheableUnary(); + case 'large_unary': + return largeUnary(); + case 'client_compressed_unary': + return clientCompressedUnary(); + case 'server_compressed_unary': + return serverCompressedUnary(); + case 'client_streaming': + return clientStreaming(); + case 'client_compressed_streaming': + return clientCompressedStreaming(); + case 'server_streaming': + return serverStreaming(); + case 'server_compressed_streaming': + return serverCompressedStreaming(); + case 'ping_pong': + return pingPong(); + case 'empty_stream': + return emptyStream(); + case 'compute_engine_creds': + return computeEngineCreds(); + case 'jwt_token_creds': + return jwtTokenCreds(); + case 'oauth2_auth_token': + return oauth2AuthToken(); + case 'per_rpc_creds': + return perRpcCreds(); + case 'custom_metadata': + return customMetadata(); + case 'status_code_and_message': + return statusCodeAndMessage(); + case 'unimplemented_method': + return unimplementedMethod(); + case 'unimplemented_service': + return unimplementedService(); + case 'cancel_after_begin': + return cancelAfterBegin(); + case 'cancel_after_first_response': + return cancelAfterFirstResponse(); + case 'timeout_on_sleeping_server': + return timeoutOnSleepingServer(); + default: + print('Unknown test case: $testCase'); + } + } + + /// This test verifies that implementations support zero-size messages. + /// Ideally, client implementations would verify that the request and response + /// were zero bytes serialized, but this is generally prohibitive to perform, + /// so is not required. + /// + /// Procedure: + /// 1. Client calls EmptyCall with the default Empty message + /// + /// Client asserts: + /// * call was successful + /// * response is non-null + Future emptyUnary() async { + final response = await client.emptyCall(new Empty()); + if (response == null) throw 'Expected non-null response.'; + if (response is! Empty) throw 'Expected Empty response.'; + } + + /// This test verifies that gRPC requests marked as cacheable use GET verb + /// instead of POST, and that server sets appropriate cache control headers + /// for the response to be cached by a proxy. This test requires that the + /// server is behind a caching proxy. Use of current timestamp in the request + /// prevents accidental cache matches left over from previous tests. + /// + /// Procedure: + /// 1. Client calls CacheableUnaryCall with `SimpleRequest` request with + /// payload set to current timestamp. Timestamp format is irrelevant, and + /// resolution is in nanoseconds. + /// Client adds a `x-user-ip` header with value `1.2.3.4` to the request. + /// This is done since some proxys such as GFE will not cache requests from + /// localhost. + /// Client marks the request as cacheable by setting the cacheable flag in + /// the request context. Longer term this should be driven by the method + /// option specified in the proto file itself. + /// 2. Client calls CacheableUnaryCall again immediately with the same request + /// and configuration as the previous call. + /// + /// Client asserts: + /// * Both calls were successful + /// * The payload body of both responses is the same. + Future cacheableUnary() async { + throw 'Not implemented'; + } + + /// This test verifies unary calls succeed in sending messages, and touches on + /// flow control (even if compression is enabled on the channel). + /// + /// Procedure: + /// 1. Client calls UnaryCall with: + /// { + /// response_size: 314159 + /// payload: { + /// body: 271828 bytes of zeros + /// } + /// } + /// + /// Client asserts: + /// * call was successful + /// * response payload body is 314159 bytes in size + /// * clients are free to assert that the response payload body contents are + /// zero and comparing the entire response message against a golden response + Future largeUnary() async { + final payload = new Payload()..body = new Uint8List(271828); + final request = new SimpleRequest() + ..responseSize = 314159 + ..payload = payload; + final response = await client.unaryCall(request); + final receivedBytes = response.payload.body.length; + if (receivedBytes != 314159) { + throw 'Response payload mismatch. Expected 314159 bytes, ' + 'got ${receivedBytes}.'; + } + } + + /// This test verifies the client can compress unary messages by sending two + /// unary calls, for compressed and uncompressed payloads. It also sends an + /// initial probing request to verify whether the server supports the + /// CompressedRequest feature by checking if the probing call fails with an + /// `INVALID_ARGUMENT` status. + /// + /// Procedure: + /// 1. Client calls UnaryCall with the feature probe, an *uncompressed* + /// message: + /// { + /// expect_compressed: { + /// value: true + /// } + /// response_size: 314159 + /// payload: { + /// body: 271828 bytes of zeros + /// } + /// } + /// 1. Client calls UnaryCall with the *compressed* message: + /// { + /// expect_compressed: { + /// value: true + /// } + /// response_size: 314159 + /// payload: { + /// body: 271828 bytes of zeros + /// } + /// } + /// 1. Client calls UnaryCall with the *uncompressed* message: + /// { + /// expect_compressed: { + /// value: false + /// } + /// response_size: 314159 + /// payload: { + /// body: 271828 bytes of zeros + /// } + /// } + /// + /// Client asserts: + /// * First call failed with `INVALID_ARGUMENT` status. + /// * Subsequent calls were successful. + /// * Response payload body is 314159 bytes in size. + /// * Clients are free to assert that the response payload body contents are + /// zeros and comparing the entire response message against a golden + /// response. + Future clientCompressedUnary() async { + throw 'Not implemented'; + } + + /// This test verifies the server can compress unary messages. It sends two + /// unary requests, expecting the server's response to be compressed or not + /// according to the `response_compressed` boolean. + /// + /// Whether compression was actually performed is determined by the + /// compression bit in the response's message flags. *Note that some languages + /// may not have access to the message flags, in which case the client will be + /// unable to verify that the `response_compressed` boolean is obeyed by the + /// server*. + /// + /// Procedure: + /// 1. Client calls UnaryCall with `SimpleRequest`: + /// { + /// response_compressed: { + /// value: true + /// } + /// response_size: 314159 + /// payload: { + /// body: 271828 bytes of zeros + /// } + /// } + /// { + /// response_compressed: { + /// value: false + /// } + /// response_size: 314159 + /// payload: { + /// body: 271828 bytes of zeros + /// } + /// } + /// + /// Client asserts: + /// * call was successful + /// * if supported by the implementation, when `response_compressed` is true, + /// the response MUST have the compressed message flag set. + /// * if supported by the implementation, when `response_compressed` is false, + /// the response MUST NOT have the compressed message flag set. + /// * response payload body is 314159 bytes in size in both cases. + /// * clients are free to assert that the response payload body contents are + /// zero and comparing the entire response message against a golden response + Future serverCompressedUnary() async { + throw 'Not implemented'; + } + + /// This test verifies that client-only streaming succeeds. + /// + /// Procedure: + /// 1. Client calls StreamingInputCall + /// 2. Client sends: + /// { + /// payload: { + /// body: 27182 bytes of zeros + /// } + /// } + /// 3. Client then sends: + /// { + /// payload: { + /// body: 8 bytes of zeros + /// } + /// } + /// 4. Client then sends: + /// { + /// payload: { + /// body: 1828 bytes of zeros + /// } + /// } + /// 5. Client then sends: + /// { + /// payload: { + /// body: 45904 bytes of zeros + /// } + /// } + /// 6. Client half-closes + /// + /// Client asserts: + /// * call was successful + /// * response aggregated_payload_size is 74922 + Future clientStreaming() async { + StreamingInputCallRequest createRequest(int bytes) { + final request = new StreamingInputCallRequest()..payload = new Payload(); + request.payload.body = new Uint8List(bytes); + return request; + } + + Stream requests() async* { + yield createRequest(27182); + yield createRequest(8); + yield createRequest(1828); + yield createRequest(45904); + } + + final response = await client.streamingInputCall(requests()); + if (response.aggregatedPayloadSize != 74922) { + throw 'Response mismatch. Expected 74922, ' + 'got ${response.aggregatedPayloadSize}'; + } + } + + /// This test verifies the client can compress requests on per-message basis + /// by performing a two-request streaming call. It also sends an initial + /// probing request to verify whether the server supports the + /// CompressedRequest feature by checking if the probing call fails with an + /// `INVALID_ARGUMENT` status. + /// + /// Procedure: + /// 1. Client calls `StreamingInputCall` and sends the following + /// feature-probing *uncompressed* `StreamingInputCallRequest` message + /// { + /// expect_compressed: { + /// value: true + /// } + /// payload: { + /// body: 27182 bytes of zeros + /// } + /// } + /// If the call does not fail with `INVALID_ARGUMENT`, the test fails. + /// Otherwise, we continue. + /// 1. Client calls `StreamingInputCall` again, sending the *compressed* + /// message + /// { + /// expect_compressed: { + /// value: true + /// } + /// payload: { + /// body: 27182 bytes of zeros + /// } + /// } + /// 1. And finally, the *uncompressed* message + /// { + /// expect_compressed: { + /// value: false + /// } + /// payload: { + /// body: 45904 bytes of zeros + /// } + /// } + /// 1. Client half-closes + /// + /// Client asserts: + /// * First call fails with `INVALID_ARGUMENT`. + /// * Next calls succeeds. + /// * Response aggregated payload size is 73086. + Future clientCompressedStreaming() async { + throw 'Not implemented'; + } + + /// This test verifies that server-only streaming succeeds. + /// + /// Procedure: + /// 1. Client calls StreamingOutputCall with `StreamingOutputCallRequest`: + /// { + /// response_parameters: { + /// size: 31415 + /// } + /// response_parameters: { + /// size: 9 + /// } + /// response_parameters: { + /// size: 2653 + /// } + /// response_parameters: { + /// size: 58979 + /// } + /// } + /// + /// Client asserts: + /// * call was successful + /// * exactly four responses + /// * response payload bodies are sized (in order): 31415, 9, 2653, 58979 + /// * clients are free to assert that the response payload body contents are + /// zero and comparing the entire response messages against golden responses + Future serverStreaming() async { + final expectedResponses = [31415, 9, 2653, 58979]; + + final request = new StreamingOutputCallRequest() + ..responseParameters.addAll(expectedResponses + .map((size) => new ResponseParameters()..size = size)); + + final responses = await client.streamingOutputCall(request).toList(); + if (responses.length != 4) { + throw 'Incorrect number of responses (${responses.length}).'; + } + final responseLengths = + responses.map((response) => response.payload.body.length).toList(); + + if (!new ListEquality().equals(responseLengths, expectedResponses)) { + throw 'Incorrect response lengths received (${responseLengths.join(', ')} != ${expectedResponses.join(', ')})'; + } + } + + /// This test verifies that the server can compress streaming messages and + /// disable compression on individual messages, expecting the server's + /// response to be compressed or not according to the `response_compressed` + /// boolean. + /// + /// Whether compression was actually performed is determined by the + /// compression bit in the response's message flags. *Note that some languages + /// may not have access to the message flags, in which case the client will be + /// unable to verify that the `response_compressed` boolean is obeyed by the + /// server*. + /// + /// Procedure: + /// 1. Client calls StreamingOutputCall with `StreamingOutputCallRequest`: + /// { + /// response_parameters: { + /// compressed: { + /// value: true + /// } + /// size: 31415 + /// } + /// response_parameters: { + /// compressed: { + /// value: false + /// } + /// size: 92653 + /// } + /// } + /// + /// Client asserts: + /// * call was successful + /// * exactly two responses + /// * if supported by the implementation, when `response_compressed` is false, + /// the response's messages MUST NOT have the compressed message flag set. + /// * if supported by the implementation, when `response_compressed` is true, + /// the response's messages MUST have the compressed message flag set. + /// * response payload bodies are sized (in order): 31415, 92653 + /// * clients are free to assert that the response payload body contents are + /// zero and comparing the entire response messages against golden responses + Future serverCompressedStreaming() async { + throw 'Not implemented'; + } + + /// This test verifies that full duplex bidi is supported. + /// + /// Procedure: + /// 1. Client calls FullDuplexCall with: + /// { + /// response_parameters: { + /// size: 31415 + /// } + /// payload: { + /// body: 27182 bytes of zeros + /// } + /// } + /// 2. After getting a reply, it sends: + /// { + /// response_parameters: { + /// size: 9 + /// } + /// payload: { + /// body: 8 bytes of zeros + /// } + /// } + /// 3. After getting a reply, it sends: + /// { + /// response_parameters: { + /// size: 2653 + /// } + /// payload: { + /// body: 1828 bytes of zeros + /// } + /// } + /// 4. After getting a reply, it sends: + /// { + /// response_parameters: { + /// size: 58979 + /// } + /// payload: { + /// body: 45904 bytes of zeros + /// } + /// } + /// 5. After getting a reply, client half-closes + /// + /// Client asserts: + /// * call was successful + /// * exactly four responses + /// * response payload bodies are sized (in order): 31415, 9, 2653, 58979 + /// * clients are free to assert that the response payload body contents are + /// zero and comparing the entire response messages against golden responses + Future pingPong() async { + final requestSizes = [27182, 8, 1828, 45904]; + final expectedResponses = [31415, 9, 2653, 58979]; + + StreamingOutputCallRequest createRequest(int index) { + final payload = new Payload()..body = new Uint8List(requestSizes[index]); + final request = new StreamingOutputCallRequest() + ..payload = payload + ..responseParameters + .add(new ResponseParameters()..size = expectedResponses[index]); + return request; + } + + var index = 0; + final requests = new StreamController(); + + final responses = client.fullDuplexCall(requests.stream.map(createRequest)); + requests.add(index); + await for (final response in responses) { + if (index >= expectedResponses.length) { + throw 'Received too many responses. $index > ${expectedResponses.length}.'; + } + if (response.payload.body.length != expectedResponses[index]) { + throw 'Response mismatch for response $index: ' + '${response.payload.body.length} != ${expectedResponses[index]}.'; + } + index++; + if (index == requestSizes.length) { + requests.close(); + } else { + requests.add(index); + } + } + } + + /// This test verifies that streams support having zero-messages in both + /// directions. + /// + /// Procedure: + /// 1. Client calls FullDuplexCall and then half-closes + /// + /// Client asserts: + /// * call was successful + /// * exactly zero responses + Future emptyStream() async { + final requests = new StreamController(); + final call = client.fullDuplexCall(requests.stream); + requests.close(); + final responses = await call.toList(); + if (responses.length != 0) { + throw 'Received too many responses. ${responses.length} != 0'; + } + } + + /// This test is only for cloud-to-prod path. + /// + /// This test verifies unary calls succeed in sending messages while using + /// Service Credentials from GCE metadata server. The client instance needs to + /// be created with desired oauth scope. + /// + /// The test uses `--default_service_account` with GCE service account email + /// and `--oauth_scope` with the OAuth scope to use. For testing against + /// grpc-test.sandbox.googleapis.com, + /// "https://www.googleapis.com/auth/xapi.zoo" should be passed in as + /// `--oauth_scope`. + /// + /// Procedure: + /// 1. Client configures channel to use GCECredentials + /// 2. Client calls UnaryCall on the channel with: + /// { + /// response_size: 314159 + /// payload: { + /// body: 271828 bytes of zeros + /// } + /// fill_username: true + /// fill_oauth_scope: true + /// } + /// + /// Client asserts: + /// * call was successful + /// * received SimpleResponse.username equals the value of + /// `--default_service_account` flag + /// * received SimpleResponse.oauth_scope is in `--oauth_scope` + /// * response payload body is 314159 bytes in size + /// * clients are free to assert that the response payload body contents are + /// zero and comparing the entire response message against a golden response + Future computeEngineCreds() async { + throw 'Not implemented'; + } + + /// This test is only for cloud-to-prod path. + /// + /// This test verifies unary calls succeed in sending messages while using JWT + /// token (created by the project's key file) + /// + /// Test caller should set flag `--service_account_key_file` with the path to + /// json key file downloaded from https://console.developers.google.com. + /// Alternately, if using a usable auth implementation, she may specify the + /// file location in the environment variable GOOGLE_APPLICATION_CREDENTIALS. + /// + /// Procedure: + /// 1. Client configures the channel to use JWTTokenCredentials + /// 2. Client calls UnaryCall with: + /// { + /// response_size: 314159 + /// payload: { + /// body: 271828 bytes of zeros + /// } + /// fill_username: true + /// } + /// + /// Client asserts: + /// * call was successful + /// * received SimpleResponse.username is not empty and is in the json key + /// file used by the auth library. The client can optionally check the + /// username matches the email address in the key file or equals the value + /// of `--default_service_account` flag. + /// * response payload body is 314159 bytes in size + /// * clients are free to assert that the response payload body contents are + /// zero and comparing the entire response message against a golden response + Future jwtTokenCreds() async { + throw 'Not implemented'; + } + + /// This test is only for cloud-to-prod path and some implementations may run + /// in GCE only. + /// + /// This test verifies unary calls succeed in sending messages using an OAuth2 + /// token that is obtained out of band. For the purpose of the test, the + /// OAuth2 token is actually obtained from a service account credentials or + /// GCE credentials via the language-specific authorization library. + /// + /// The difference between this test and the other auth tests is that it first + /// uses the authorization library to obtain an authorization token. + /// + /// The test + /// * uses the flag `--service_account_key_file` with the path to a json key + /// file downloaded from https://console.developers.google.com. Alternately, + /// if using a usable auth implementation, it may specify the file location + /// in the environment variable GOOGLE_APPLICATION_CREDENTIALS, *OR* if GCE + /// credentials is used to fetch the token, `--default_service_account` can + /// be used to pass in GCE service account email. + /// * uses the flag `--oauth_scope` for the oauth scope. For testing against + /// grpc-test.sandbox.googleapis.com, + /// "https://www.googleapis.com/auth/xapi.zoo" should be passed as the + /// `--oauth_scope`. + /// + /// Procedure: + /// 1. Client uses the auth library to obtain an authorization token + /// 2. Client configures the channel to use AccessTokenCredentials with the + /// access token obtained in step 1 + /// 3. Client calls UnaryCall with the following message + /// { + /// fill_username: true + /// fill_oauth_scope: true + /// } + /// + /// Client asserts: + /// * call was successful + /// * received SimpleResponse.username is valid. Depending on whether a + /// service account key file or GCE credentials was used, client should + /// check against the json key file or GCE default service account email. + /// * received SimpleResponse.oauth_scope is in `--oauth_scope` + Future oauth2AuthToken() async { + throw 'Not implemented'; + } + + /// Similar to the other auth tests, this test is only for cloud-to-prod path. + /// + /// This test verifies unary calls succeed in sending messages using a JWT or + /// a service account credentials set on the RPC. + /// + /// The test + /// * uses the flag `--service_account_key_file` with the path to a json key + /// file downloaded from https://console.developers.google.com. Alternately, + /// if using a usable auth implementation, it may specify the file location + /// in the environment variable GOOGLE_APPLICATION_CREDENTIALS + /// * optionally uses the flag `--oauth_scope` for the oauth scope if + /// implementator wishes to use service account credential instead of JWT + /// credential. For testing against grpc-test.sandbox.googleapis.com, oauth + /// scope "https://www.googleapis.com/auth/xapi.zoo" should be used. + /// + /// Procedure: + /// 1. Client configures the channel with just SSL credentials + /// 2. Client calls UnaryCall, setting per-call credentials to + /// JWTTokenCredentials. The request is the following message + /// { + /// fill_username: true + /// } + /// + /// Client asserts: + /// * call was successful + /// * received SimpleResponse.username is not empty and is in the json key + /// file used by the auth library. The client can optionally check the + /// username matches the email address in the key file. + Future perRpcCreds() async { + throw 'Not implemented'; + } + + /// This test verifies that custom metadata in either binary or ascii format + /// can be sent as initial-metadata by the client and as both initial- and + /// trailing-metadata by the server. + /// + /// Procedure: + /// 1. The client attaches custom metadata with the following keys and values: + /// key: "x-grpc-test-echo-initial", value: "test_initial_metadata_value" + /// key: "x-grpc-test-echo-trailing-bin", value: 0xababab + /// to a UnaryCall with request: + /// { + /// response_size: 314159 + /// payload: { + /// body: 271828 bytes of zeros + /// } + /// } + /// + /// 2. The client attaches custom metadata with the following keys and values: + /// key: "x-grpc-test-echo-initial", value: "test_initial_metadata_value" + /// key: "x-grpc-test-echo-trailing-bin", value: 0xababab + /// to a FullDuplexCall with request: + /// { + /// response_parameters: { + /// size: 314159 + /// } + /// payload: { + /// body: 271828 bytes of zeros + /// } + /// } + /// and then half-closes + /// + /// Client asserts: + /// * call was successful + /// * metadata with key `"x-grpc-test-echo-initial"` and value + /// `"test_initial_metadata_value"`is received in the initial metadata for + /// calls in Procedure steps 1 and 2. + /// * metadata with key `"x-grpc-test-echo-trailing-bin"` and value `0xababab` + /// is received in the trailing metadata for calls in Procedure steps 1 and + /// 2. + Future customMetadata() async { + void validate(Map headers, Map trailers) { + if (headers[_headerEchoKey] != _headerEchoData) { + throw 'Invalid header data received.'; + } + if (trailers[_trailerEchoKey] != _trailerEchoData) { + throw 'Invalid trailer data received.'; + } + } + + final options = new CallOptions(metadata: { + _headerEchoKey: _headerEchoData, + _trailerEchoKey: _trailerEchoData, + }); + final unaryCall = client.unaryCall( + new SimpleRequest() + ..responseSize = 314159 + ..payload = (new Payload()..body = new Uint8List(271828)), + options: options); + var headers = await unaryCall.headers; + var trailers = await unaryCall.trailers; + await unaryCall; + validate(headers, trailers); + + Stream requests() async* { + yield new StreamingOutputCallRequest() + ..responseParameters.add(new ResponseParameters()..size = 314159) + ..payload = (new Payload()..body = new Uint8List(271828)); + } + + final fullDuplexCall = client.fullDuplexCall(requests(), options: options); + final drain = fullDuplexCall.drain(); + headers = await fullDuplexCall.headers; + trailers = await fullDuplexCall.trailers; + await drain; + validate(headers, trailers); + } + + /// This test verifies unary calls succeed in sending messages, and propagate + /// back status code and message sent along with the messages. + /// + /// Procedure: + /// 1. Client calls UnaryCall with: + /// { + /// response_status: { + /// code: 2 + /// message: "test status message" + /// } + /// } + /// + /// 2. Client calls FullDuplexCall with: + /// { + /// response_status: { + /// code: 2 + /// message: "test status message" + /// } + /// } + /// + /// and then half-closes + /// + /// Client asserts: + /// * received status code is the same as the sent code for both Procedure + /// steps 1 and 2 + /// * received status message is the same as the sent message for both + /// Procedure steps 1 and 2 + Future statusCodeAndMessage() async { + final expectedStatus = new GrpcError.custom(2, 'test status message'); + final responseStatus = new EchoStatus() + ..code = expectedStatus.code + ..message = expectedStatus.message; + try { + await client + .unaryCall(new SimpleRequest()..responseStatus = responseStatus); + throw 'Did not receive correct status code.'; + } on GrpcError catch (e) { + if (e != expectedStatus) { + throw 'Received incorrect status: $e.'; + } + } + Stream requests() async* { + yield new StreamingOutputCallRequest()..responseStatus = responseStatus; + } + + try { + await for (final _ in client.fullDuplexCall(requests())) { + throw 'Received unexpected response.'; + } + throw 'Did not receive correct status code.'; + } on GrpcError catch (e) { + if (e != expectedStatus) { + throw 'Received incorrect status: $e.'; + } + } + } + + /// This test verifies that calling an unimplemented RPC method returns the + /// UNIMPLEMENTED status code. + /// + /// Procedure: + /// * Client calls `grpc.testing.TestService/UnimplementedCall` with an empty + /// request (defined as `grpc.testing.Empty`): + /// { + /// } + /// + /// Client asserts: + /// * received status code is 12 (UNIMPLEMENTED) + Future unimplementedMethod() async { + try { + await client.unimplementedCall(new Empty()); + throw 'Did not throw.'; + } on GrpcError catch (e) { + if (e.code != StatusCode.unimplemented) { + throw 'Unexpected status code ${e.code} - ${e.message}.'; + } + } + } + + /// This test verifies calling an unimplemented server returns the + /// UNIMPLEMENTED status code. + /// + /// Procedure: + /// * Client calls `grpc.testing.UnimplementedService/UnimplementedCall` with + /// an empty request (defined as `grpc.testing.Empty`) + /// + /// Client asserts: + /// * received status code is 12 (UNIMPLEMENTED) + Future unimplementedService() async { + try { + await unimplementedServiceClient.unimplementedCall(new Empty()); + throw 'Did not throw.'; + } on GrpcError catch (e) { + if (e.code != StatusCode.unimplemented) { + throw 'Unexpected status code ${e.code} - ${e.message}.'; + } + } + } + + /// This test verifies that a request can be cancelled after metadata has been + /// sent but before payloads are sent. + /// + /// Procedure: + /// 1. Client starts StreamingInputCall + /// 2. Client immediately cancels request + /// + /// Client asserts: + /// * Call completed with status CANCELLED + Future cancelAfterBegin() async { + final requests = new StreamController(); + final call = client.streamingInputCall(requests.stream); + scheduleMicrotask(call.cancel); + try { + await call; + throw 'Expected exception.'; + } on GrpcError catch (e) { + if (e.code != StatusCode.cancelled) { + throw 'Unexpected status code ${e.code} - ${e.message}'; + } + } + requests.close(); + } + + /// This test verifies that a request can be cancelled after receiving a + /// message from the server. + /// + /// Procedure: + /// 1. Client starts FullDuplexCall with + /// { + /// response_parameters: { + /// size: 31415 + /// } + /// payload: { + /// body: 27182 bytes of zeros + /// } + /// } + /// + /// 2. After receiving a response, client cancels request + /// + /// Client asserts: + /// * Call completed with status CANCELLED + Future cancelAfterFirstResponse() async { + final requests = new StreamController(); + final call = client.fullDuplexCall(requests.stream); + final completer = new Completer(); + + var receivedResponse = false; + call.listen((response) { + if (receivedResponse) { + completer.completeError('Received too many responses.'); + return; + } + receivedResponse = true; + if (response.payload.body.length != 31415) { + completer.completeError('Invalid response length: ' + '${response.payload.body.length} != 31415.'); + } + call.cancel(); + }, onError: (e) { + if (e is! GrpcError) completer.completeError('Unexpected error: $e.'); + if (e.code != StatusCode.cancelled) { + completer + .completeError('Unexpected status code ${e.code}: ${e.message}.'); + } + completer.complete(true); + }, onDone: () { + if (!completer.isCompleted) completer.completeError('Expected error.'); + }); + + requests.add(new StreamingOutputCallRequest() + ..responseParameters.add(new ResponseParameters()..size = 31415) + ..payload = (new Payload()..body = new Uint8List(27182))); + await completer.future; + requests.close(); + } + + /// This test verifies that an RPC request whose lifetime exceeds its + /// configured timeout value will end with the DeadlineExceeded status. + /// + /// Procedure: + /// 1. Client calls FullDuplexCall with the following request and sets its + /// timeout to 1ms + /// { + /// payload: { + /// body: 27182 bytes of zeros + /// } + /// } + /// + /// 2. Client waits + /// + /// Client asserts: + /// * Call completed with status DEADLINE_EXCEEDED. + Future timeoutOnSleepingServer() async { + final requests = new StreamController(); + final call = client.fullDuplexCall(requests.stream, + options: new CallOptions(timeout: new Duration(milliseconds: 1))); + requests.add(new StreamingOutputCallRequest() + ..payload = (new Payload()..body = new Uint8List(27182))); + try { + await for (final _ in call) { + throw 'Unexpected response received.'; + } + throw 'Expected exception.'; + } on GrpcError catch (e) { + if (e.code != StatusCode.deadlineExceeded) { + throw 'Unexpected status code ${e.code} - ${e.message}.'; + } + } finally { + requests.close(); + } + } +} diff --git a/interop/pubspec.yaml b/interop/pubspec.yaml index 16baafa..a264777 100644 --- a/interop/pubspec.yaml +++ b/interop/pubspec.yaml @@ -9,6 +9,7 @@ environment: dependencies: args: ^0.13.0 async: ^1.13.3 + collection: ^1.14.2 grpc: path: ../ protobuf: ^0.5.4