diff --git a/advanced.yml b/advanced.yml index 446937f..72f26cb 100644 --- a/advanced.yml +++ b/advanced.yml @@ -95,3 +95,12 @@ services: image: grpcweb/binary-client ports: - "8081:8081" + interop: + build: + context: ./ + dockerfile: ./net/grpc/gateway/docker/interop/Dockerfile + depends_on: + - prereqs + image: grpcweb/interop + ports: + - "8081:8081" diff --git a/interop-test-descriptions.md b/interop-test-descriptions.md new file mode 100644 index 0000000..57a2051 --- /dev/null +++ b/interop-test-descriptions.md @@ -0,0 +1,82 @@ +gRPC-Web Interop Tests +====================== + +This document describes the set of tests any gRPC-Web clients or proxies need +to implement. The proto definition for the messages and RPCs we are using for +the tests can be found +[here](src/proto/grpc/testing/test.proto). + +The canonical set of interop tests was defined in the main +[grpc/grpc repo](https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md). + +Here in gRPC-Web, we will only implement a subset of tests that's relevant to +gRPC-Web. For example, we will not be implementing any tests involving +client-streaming or bidi-streaming for now. On the other hand, there are +gRPC-Web specific tests that we will add here. + +``` + gRPC-Web Client <--> Proxy <--> gRPC Service +``` + +The idea is that we should be able to swap out any of the 3 components above +and all the interop tests should still pass. + +This repository will provide a canonical implementation of the interop test +suite using the Javascript client, Envoy and a gRPC service implemented in +Node. + +For any new gRPC-Web client implementation, you need to swap out the JS +client and make sure all tests still pass. + +For any in-process proxies implementation, you need to swap out the proxy +and the service as a unit and make sure the standard JS client will still +pass the tests. + + +List of Tests +------------- + +| Test Name | grpc-web-text Mode | grpc-web Binary mode | +| --------- |:------------------:|:--------------------:| +| empty_unary | ✓ | ✓ | +| cacheable_unary | TBD | TBD | +| large_unary | ✓ | ✓ | +| client_compressed_unary | TBD | TBD | +| server_compressed_unary | TBD | TBD | +| client_streaming | ✗ | ✗ | +| client_compressed_streaming | ✗ | ✗ | +| server_streaming | ✓ | ✗ | +| server_compressed_streaming | TBD | ✗ | +| ping_pong | ✗ | ✗ | +| empty_stream | ✗ | ✗ | +| compute_engine_creds | TBD | TBD | +| jwt_token_creds | TBD | TBD | +| oauth2_auth_token | TBD | TBD | +| per_rpc_creds | TBD | TBD | +| google_default_credentials | TBD | TBD | +| compute_engine_channel_credentials | TBD | TBD | +| custom_metadata | ✓ | ✓ | +| status_code_and_message | ✓ | ✓ | +| special_status_message | ✓ | ✓ | +| unimplemented_method | ✓ | ✓ | +| unimplemented_service | ✓ | ✓ | +| cancel_after_begin | ✗ | ✗ | +| cancel_after_first_response | ✗ | ✗ | +| timeout_on_sleeping_server | ✗ | ✗ | + + +gRPC-Web specific considerations +-------------------------------- + +### Text vs Binary mode + +As mentioned in the table above, client needs to be tested in both the text +format `application/grpc-web-text` and the binary mode +`application/grpc-web+proto`. The latter we don't need to test any streaming +methods. + +### CORS and other web specific scenarios + +We may add specific tests to account for web-related scenarios like CORS +handling, etc. Mostly these are to test the connection between the browser +client and the proxy. diff --git a/net/grpc/gateway/docker/interop/Dockerfile b/net/grpc/gateway/docker/interop/Dockerfile new file mode 100644 index 0000000..79fa301 --- /dev/null +++ b/net/grpc/gateway/docker/interop/Dockerfile @@ -0,0 +1,35 @@ +# Copyright 2018 Google LLC +# +# 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 +# +# https://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. + +FROM grpcweb/prereqs + +WORKDIR /github/grpc-web/test/interop + +COPY ./test/interop . + +RUN protoc -I=../.. src/proto/grpc/testing/test.proto \ +src/proto/grpc/testing/empty.proto src/proto/grpc/testing/messages.proto \ +--js_out=import_style=commonjs:. \ +--grpc-web_out=import_style=commonjs,mode=grpcwebtext:. + +RUN npm install && \ + npm link grpc-web && \ + npx webpack && \ + cp index.html /var/www/html && \ + cp dist/main.js /var/www/html/dist + +WORKDIR /var/www/html + +EXPOSE 8081 +CMD ["python", "-m", "SimpleHTTPServer", "8081"] diff --git a/src/proto/grpc/testing/empty.proto b/src/proto/grpc/testing/empty.proto new file mode 100644 index 0000000..6a0aa88 --- /dev/null +++ b/src/proto/grpc/testing/empty.proto @@ -0,0 +1,28 @@ + +// 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. + +syntax = "proto3"; + +package grpc.testing; + +// An empty message that you can re-use to avoid defining duplicated empty +// messages in your project. A typical example is to use it as argument or the +// return value of a service API. For instance: +// +// service Foo { +// rpc Bar (grpc.testing.Empty) returns (grpc.testing.Empty) { }; +// }; +// +message Empty {} diff --git a/src/proto/grpc/testing/messages.proto b/src/proto/grpc/testing/messages.proto new file mode 100644 index 0000000..5993bc6 --- /dev/null +++ b/src/proto/grpc/testing/messages.proto @@ -0,0 +1,209 @@ + +// Copyright 2015-2016 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. + +// Message definitions to be used by integration test service definitions. + +syntax = "proto3"; + +package grpc.testing; + +// TODO(dgq): Go back to using well-known types once +// https://github.com/grpc/grpc/issues/6980 has been fixed. +// import "google/protobuf/wrappers.proto"; +message BoolValue { + // The bool value. + bool value = 1; +} + +// The type of payload that should be returned. +enum PayloadType { + // Compressable text format. + COMPRESSABLE = 0; +} + +// A block of data, to simply increase gRPC message size. +message Payload { + // The type of data in body. + PayloadType type = 1; + // Primary contents of payload. + bytes body = 2; +} + +// A protobuf representation for grpc status. This is used by test +// clients to specify a status that the server should attempt to return. +message EchoStatus { + int32 code = 1; + string message = 2; +} + +// The type of route that a client took to reach a server w.r.t. gRPCLB. +// The server must fill in "fallback" if it detects that the RPC reached +// the server via the "gRPCLB fallback" path, and "backend" if it detects +// that the RPC reached the server via "gRPCLB backend" path (i.e. if it got +// the address of this server from the gRPCLB server BalanceLoad RPC). Exactly +// how this detection is done is context and server dependent. +enum GrpclbRouteType { + // Server didn't detect the route that a client took to reach it. + GRPCLB_ROUTE_TYPE_UNKNOWN = 0; + // Indicates that a client reached a server via gRPCLB fallback. + GRPCLB_ROUTE_TYPE_FALLBACK = 1; + // Indicates that a client reached a server as a gRPCLB-given backend. + GRPCLB_ROUTE_TYPE_BACKEND = 2; +} + +// Unary request. +message SimpleRequest { + // Desired payload type in the response from the server. + // If response_type is RANDOM, server randomly chooses one from other formats. + PayloadType response_type = 1; + + // Desired payload size in the response from the server. + int32 response_size = 2; + + // Optional input payload sent along with the request. + Payload payload = 3; + + // Whether SimpleResponse should include username. + bool fill_username = 4; + + // Whether SimpleResponse should include OAuth scope. + bool fill_oauth_scope = 5; + + // Whether to request the server to compress the response. This field is + // "nullable" in order to interoperate seamlessly with clients not able to + // implement the full compression tests by introspecting the call to verify + // the response's compression status. + BoolValue response_compressed = 6; + + // Whether server should return a given status + EchoStatus response_status = 7; + + // Whether the server should expect this request to be compressed. + BoolValue expect_compressed = 8; + + // Whether SimpleResponse should include server_id. + bool fill_server_id = 9; + + // Whether SimpleResponse should include grpclb_route_type. + bool fill_grpclb_route_type = 10; +} + +// Unary response, as configured by the request. +message SimpleResponse { + // Payload to increase message size. + Payload payload = 1; + // The user the request came from, for verifying authentication was + // successful when the client expected it. + string username = 2; + // OAuth scope. + string oauth_scope = 3; + + // Server ID. This must be unique among different server instances, + // but the same across all RPC's made to a particular server instance. + string server_id = 4; + // gRPCLB Path. + GrpclbRouteType grpclb_route_type = 5; + + // Server hostname. + string hostname = 6; +} + +// Client-streaming request. +message StreamingInputCallRequest { + // Optional input payload sent along with the request. + Payload payload = 1; + + // Whether the server should expect this request to be compressed. This field + // is "nullable" in order to interoperate seamlessly with servers not able to + // implement the full compression tests by introspecting the call to verify + // the request's compression status. + BoolValue expect_compressed = 2; + + // Not expecting any payload from the response. +} + +// Client-streaming response. +message StreamingInputCallResponse { + // Aggregated size of payloads received from the client. + int32 aggregated_payload_size = 1; +} + +// Configuration for a particular response. +message ResponseParameters { + // Desired payload sizes in responses from the server. + int32 size = 1; + + // Desired interval between consecutive responses in the response stream in + // microseconds. + int32 interval_us = 2; + + // Whether to request the server to compress the response. This field is + // "nullable" in order to interoperate seamlessly with clients not able to + // implement the full compression tests by introspecting the call to verify + // the response's compression status. + BoolValue compressed = 3; +} + +// Server-streaming request. +message StreamingOutputCallRequest { + // Desired payload type in the response from the server. + // If response_type is RANDOM, the payload from each response in the stream + // might be of different types. This is to simulate a mixed type of payload + // stream. + PayloadType response_type = 1; + + // Configuration for each expected response message. + repeated ResponseParameters response_parameters = 2; + + // Optional input payload sent along with the request. + Payload payload = 3; + + // Whether server should return a given status + EchoStatus response_status = 7; +} + +// Server-streaming response, as configured by the request and parameters. +message StreamingOutputCallResponse { + // Payload to increase response size. + Payload payload = 1; +} + +// For reconnect interop test only. +// Client tells server what reconnection parameters it used. +message ReconnectParams { + int32 max_reconnect_backoff_ms = 1; +} + +// For reconnect interop test only. +// Server tells client whether its reconnects are following the spec and the +// reconnect backoffs it saw. +message ReconnectInfo { + bool passed = 1; + repeated int32 backoff_ms = 2; +} + +message LoadBalancerStatsRequest { + // Request stats for the next num_rpcs sent by client. + int32 num_rpcs = 1; + // If num_rpcs have not completed within timeout_sec, return partial results. + int32 timeout_sec = 2; +} + +message LoadBalancerStatsResponse { + // The number of completed RPCs for each peer. + map rpcs_by_peer = 1; + // The number of RPCs that failed to record a remote peer. + int32 num_failures = 2; +} diff --git a/src/proto/grpc/testing/test.proto b/src/proto/grpc/testing/test.proto new file mode 100644 index 0000000..0b198d8 --- /dev/null +++ b/src/proto/grpc/testing/test.proto @@ -0,0 +1,86 @@ + +// Copyright 2015-2016 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. + +// An integration test service that covers all the method signature permutations +// of unary/streaming requests/responses. + +syntax = "proto3"; + +import "src/proto/grpc/testing/empty.proto"; +import "src/proto/grpc/testing/messages.proto"; + +package grpc.testing; + +// A simple service to test the various types of RPCs and experiment with +// performance with various types of payload. +service TestService { + // One empty request followed by one empty response. + rpc EmptyCall(grpc.testing.Empty) returns (grpc.testing.Empty); + + // One request followed by one response. + rpc UnaryCall(SimpleRequest) returns (SimpleResponse); + + // One request followed by one response. Response has cache control + // headers set such that a caching HTTP proxy (such as GFE) can + // satisfy subsequent requests. + rpc CacheableUnaryCall(SimpleRequest) returns (SimpleResponse); + + // One request followed by a sequence of responses (streamed download). + // The server returns the payload with client desired type and sizes. + rpc StreamingOutputCall(StreamingOutputCallRequest) + returns (stream StreamingOutputCallResponse); + + // A sequence of requests followed by one response (streamed upload). + // The server returns the aggregated size of client payload as the result. + rpc StreamingInputCall(stream StreamingInputCallRequest) + returns (StreamingInputCallResponse); + + // A sequence of requests with each request served by the server immediately. + // As one request could lead to multiple responses, this interface + // demonstrates the idea of full duplexing. + rpc FullDuplexCall(stream StreamingOutputCallRequest) + returns (stream StreamingOutputCallResponse); + + // A sequence of requests followed by a sequence of responses. + // The server buffers all the client requests and then serves them in order. A + // stream of responses are returned to the client when the server starts with + // first request. + rpc HalfDuplexCall(stream StreamingOutputCallRequest) + returns (stream StreamingOutputCallResponse); + + // The test server will not implement this method. It will be used + // to test the behavior when clients call unimplemented methods. + rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty); +} + +// A simple service NOT implemented at servers so clients can test for +// that case. +service UnimplementedService { + // A call that no server should implement + rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty); +} + +// A service used to control reconnect server. +service ReconnectService { + rpc Start(grpc.testing.ReconnectParams) returns (grpc.testing.Empty); + rpc Stop(grpc.testing.Empty) returns (grpc.testing.ReconnectInfo); +} + +// A service used to obtain stats for verifying LB behavior. +service LoadBalancerStatsService { + // Gets the backend distribution for RPCs sent by a test client. + rpc GetClientStats(LoadBalancerStatsRequest) + returns (LoadBalancerStatsResponse) {} +} diff --git a/test/interop/README.md b/test/interop/README.md new file mode 100644 index 0000000..4194aec --- /dev/null +++ b/test/interop/README.md @@ -0,0 +1,45 @@ +gRPC-Web Interop Tests +====================== + +See the +[main doc](https://github.com/grpc/grpc-web/blob/master/interop-test-descriptions.md) +for details about gRPC interop tests in general and the list of test cases. + + +Run interop tests +----------------- + +### Run the Node interop server + +An interop server implemented in Node is hosted in the `grpc/grpc-node` repo. +There might be a bit of set up you need to do before running the command below. + +```sh +$ cd grpc-node/test +$ node --require ./fixtures/native_native interop/interop_server.js --port=7074 +``` + + +### Run the Envoy proxy + +An `envoy.yaml` file is provided in this directory to direct traffic for these +tests. + +```sh +$ cd grpc-web +$ docker run -it --rm -v $(pwd)/test/interop/envoy.yaml:/etc/envoy/envoy.yaml:ro \ + --network=host -p 8080:8080 envoyproxy/envoy:latest +``` + + +### Run the gRPC-Web browser client + + +```sh +$ cd grpc-web +$ docker-compose -f advanced.yml build common prereqs interop +$ docker-compose -f advanced.yml up interop +``` + +Open up the browser and go to `http://localhost:8081/index.html` and open up +the console. diff --git a/test/interop/client.js b/test/interop/client.js new file mode 100644 index 0000000..3c8ad58 --- /dev/null +++ b/test/interop/client.js @@ -0,0 +1,250 @@ +/** + * + * Copyright 2018 Google LLC + * + * 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 + * + * https://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. + * + */ + +const {Empty} = require('./src/proto/grpc/testing/empty_pb.js'); +const {SimpleRequest, + StreamingOutputCallRequest, + EchoStatus, + Payload, + ResponseParameters} = + require('./src/proto/grpc/testing/messages_pb.js'); +const {TestServiceClient} = + require('./src/proto/grpc/testing/test_grpc_web_pb.js'); +const grpc = {}; +grpc.web = require('grpc-web'); + +var testService = new TestServiceClient( + 'http://'+window.location.hostname+':8080', null, null); + +function doEmptyUnary() { + return new Promise((resolve, reject) => { + testService.emptyCall(new Empty(), null, (err, response) => { + if (err) { + reject('EmptyUnary failed: Received unexpected error "'+ + err.message+'"'); + return; + } else { + if (!(response instanceof Empty)) { + reject('EmptyUnary failed: Response not of Empty type'); + return; + } + resolve('EmptyUnary: passed'); + } + }); + }); +} + +function doLargeUnary() { + return new Promise((resolve, reject) => { + var req = new SimpleRequest(); + var size = 314159; + + var payload = new Payload(); + payload.setBody('0'.repeat(271828)); + + req.setPayload(payload); + req.setResponseSize(size); + + testService.unaryCall(req, null, (err, response) => { + if (err) { + reject('LargeUnary failed: Received unexpected error "'+ + err.message+'"'); + return; + } else { + if (response.getPayload().getBody().length != size) { + rejecet('LargeUnary failed: Received incorrect size payload'); + return; + } + resolve('LargeUnary: passed'); + } + }); + }); +} + +function doServerStreaming() { + return new Promise((resolve, reject) => { + var sizes = [31415, 9, 2653, 58979]; + + var responseParams = sizes.map((size, idx) => { + var param = new ResponseParameters(); + param.setSize(size); + param.setIntervalUs(idx * 10); + return param; + }); + + var req = new StreamingOutputCallRequest(); + req.setResponseParametersList(responseParams); + + var stream = testService.streamingOutputCall(req); + + var numCallbacks = 0; + stream.on('data', (response) => { + if (response.getPayload().getBody().length != sizes[numCallbacks]) { + reject('ServerStreaming failed: Received incorrect size payload'); + return; + } + numCallbacks++; + }); + // TODO(stanleycheung): is there a better way to do this? + setTimeout(() => { + if (numCallbacks != sizes.length) { + reject('ServerStreaming failed: data callback called '+ + numCallbacks+' times. Should be '+sizes.length); + return; + } + resolve('ServerStreaming: passed'); + }, 200); + }); +} + +function doCustomMetadata() { + return new Promise((resolve, reject) => { + var req = new SimpleRequest(); + const size = 314159; + const ECHO_INITIAL_KEY = 'x-grpc-test-echo-initial'; + const ECHO_INITIAL_VALUE = 'test_initial_metadata_value'; + const ECHO_TRAILING_KEY = 'x-grpc-test-echo-trailing-bin'; + const ECHO_TRAILING_VALUE = 0xababab; + + var payload = new Payload(); + payload.setBody('0'.repeat(271828)); + + req.setPayload(payload); + req.setResponseSize(size); + + var call = testService.unaryCall(req, { + [ECHO_INITIAL_KEY]: ECHO_INITIAL_VALUE, + [ECHO_TRAILING_KEY]: ECHO_TRAILING_VALUE + }, (err, response) => { + if (err) { + reject('CustomMetadata failed: Received unexpected error "'+ + err.message+'"'); + return; + } else { + if (response.getPayload().getBody().length != size) { + rejecet('CustomMetadata failed: Received incorrect size payload'); + return; + } + } + }); + var metadataCallbackVerified = false; + var statusCallbackVerified = false; + call.on('metadata', (metadata) => { + if (!(ECHO_INITIAL_KEY in metadata)) { + reject('CustomMetadata failed: initial metadata does not '+ + 'contain '+ECHO_INITIAL_KEY); + } else if (metadata[ECHO_INITIAL_KEY] != ECHO_INITIAL_VALUE) { + reject('CustomMetadata failed: initial metadata value for '+ + ECHO_INITIAL_KEY+' is incorrect'); + } else { + metadataCallbackVerified = true; + } + }); + call.on('status', (status) => { + if (!('metadata' in status)) { + reject('CustomMetadata failed: status callback does not '+ + 'contain metadata'); + } else if (!(ECHO_TRAILING_KEY in status.metadata)) { + reject('CustomMetadata failed: trailing metadata does not '+ + 'contain '+ECHO_TRAILING_KEY); + } else if (status.metadata[ECHO_TRAILING_KEY] != ECHO_TRAILING_VALUE) { + reject('CustomMetadata failed: trailing metadata value for '+ + ECHO_TRAILING_KEY+' is incorrect'); + } else { + statusCallbackVerified = true; + } + }); + setTimeout(() => { + if (!metadataCallbackVerified || !statusCallbackVerified) { + reject('CustomMetadata failed: some callback failed to verify'); + } + resolve('CustomMetadta passed'); + }, 200); + }); +} + +function doStatusCodeAndMessage() { + return new Promise((resolve, reject) => { + var req = new SimpleRequest(); + + const TEST_STATUS_MESSAGE = 'test status message'; + const echoStatus = new EchoStatus(); + echoStatus.setCode(2); + echoStatus.setMessage(TEST_STATUS_MESSAGE); + + req.setResponseStatus(echoStatus); + + testService.unaryCall(req, {}, (err, response) => { + if (err) { + if (!('code' in err)) { + reject('StatusCodeAndMessage failed: status callback does not '+ + 'contain code'); + } else if (!('message' in err)) { + reject('StatusCodeAndMessage failed: status callback does not '+ + 'contain message'); + } else if (err.code != 2) { + reject('StatusCodeAndMessage failed: status code is not 2, is '+ + err.code); + } else if (err.message != TEST_STATUS_MESSAGE) { + reject('StatusCodeAndMessage failed: status mesage is not '+ + TEST_STATUS_MESSAGE+', is '+err.message); + } else { + resolve('StatusCodeAndMessage passed'); + } + } else { + reject('StatusCodeAndMessage failed: should not have received a '+ + 'proper response'); + } + }); + }); +} + +function doUnimplementedMethod() { + return new Promise((resolve, reject) => { + testService.unimplementedCall(new Empty(), {}, (err, response) => { + if (err) { + if (!('code' in err)) { + reject('UnimplementedMethod failed: status callback does not '+ + 'contain code'); + } else if (!('message' in err)) { + reject('UnimplementedMethod failed: status callback does not '+ + 'contain message'); + } else if (err.code != 12) { + reject('UnimplementedMethod failed: status code is not 12'+ + '(UNIMPLEMENTED), is '+ err.code); + } else { + resolve('UnimplementedMethod passed'); + } + } else { + reject('UnimplementedMethod failed: should not have received a '+ + 'proper respoonse'); + } + }); + }); +} + + +var testCases = [doEmptyUnary, doLargeUnary, doServerStreaming, + doCustomMetadata, doStatusCodeAndMessage, + doUnimplementedMethod]; + +testCases.reduce((promiseChain, currentTask) => { + return promiseChain.then(() => { + return currentTask().then(console.log); + }).catch(console.error); +}, Promise.resolve()); diff --git a/test/interop/envoy.yaml b/test/interop/envoy.yaml new file mode 100644 index 0000000..c53e633 --- /dev/null +++ b/test/interop/envoy.yaml @@ -0,0 +1,44 @@ +admin: + access_log_path: /tmp/admin_access.log + address: + socket_address: { address: 0.0.0.0, port_value: 9901 } + +static_resources: + listeners: + - name: listener_0 + address: + socket_address: { address: 0.0.0.0, port_value: 8080 } + filter_chains: + - filters: + - name: envoy.http_connection_manager + config: + codec_type: auto + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: ["*"] + routes: + - match: { prefix: "/" } + route: + cluster: interop_service + max_grpc_timeout: 0s + cors: + allow_origin_string_match: + - prefix: "*" + allow_methods: GET, PUT, DELETE, POST, OPTIONS + allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,x-grpc-test-echo-initial,x-grpc-test-echo-trailing-bin,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout + max_age: "1728000" + expose_headers: x-grpc-test-echo-initial,x-grpc-test-echo-trailing-bin,grpc-status,grpc-message + http_filters: + - name: envoy.grpc_web + - name: envoy.cors + - name: envoy.router + clusters: + - name: interop_service + connect_timeout: 0.25s + type: logical_dns + http2_protocol_options: {} + lb_policy: round_robin + hosts: [{ socket_address: { address: localhost, port_value: 7074 }}] diff --git a/test/interop/index.html b/test/interop/index.html new file mode 100644 index 0000000..8f8ec45 --- /dev/null +++ b/test/interop/index.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + +Interop Test + + + + + +
+
+

Please open up the console to see the test results.

+
+
+ + diff --git a/test/interop/package.json b/test/interop/package.json new file mode 100644 index 0000000..a195d9c --- /dev/null +++ b/test/interop/package.json @@ -0,0 +1,15 @@ +{ + "name": "grpc-web-interop-test", + "version": "0.1.0", + "description": "gRPC-Web Interop Test Client", + "license": "Apache-2.0", + "dependencies": { + "google-protobuf": "^3.6.1", + "grpc-web": "^1.0.0" + }, + "devDependencies": { + "browserify": "^16.2.2", + "webpack": "^4.16.5", + "webpack-cli": "^3.1.0" + } +} diff --git a/test/interop/webpack.config.js b/test/interop/webpack.config.js new file mode 100644 index 0000000..05b939a --- /dev/null +++ b/test/interop/webpack.config.js @@ -0,0 +1,4 @@ +module.exports = { + mode: "production", + entry: "./client.js", +};