diff --git a/.gitignore b/.gitignore index e0c687051..7490fb93b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,6 @@ build .vscode package-lock.json .system-test-run/ +.kitchen-sink/ __pycache__ +doc/ diff --git a/package.json b/package.json index cbce7a9b8..435669f18 100644 --- a/package.json +++ b/package.json @@ -26,9 +26,11 @@ "devDependencies": { "@compodoc/compodoc": "^1.1.7", "@types/chai": "^4.1.3", + "@types/execa": "^0.9.0", "@types/lodash.at": "^4.6.4", "@types/lodash.has": "^4.5.4", "@types/mocha": "^5.2.1", + "@types/ncp": "^2.0.1", "@types/node": "^10.3.2", "@types/proxyquire": "^1.3.28", "@types/pumpify": "^1.4.1", @@ -44,10 +46,12 @@ "eslint-config-prettier": "^4.0.0", "eslint-plugin-node": "^8.0.0", "eslint-plugin-prettier": "^3.0.0", + "execa": "^1.0.0", "gts": "^0.9.0", "intelli-espower-loader": "^1.0.1", "linkinator": "^1.1.2", "mocha": "^6.1.0", + "ncp": "^2.0.0", "nyc": "^14.0.0", "pegjs": "~0.10.0", "prettier": "^1.15.2", @@ -72,7 +76,7 @@ "fix": "gts fix && eslint --fix samples/*.js samples/**/*.js", "prepare": "npm run compile", "posttest": "npm run lint", - "system-test": "nyc mocha build/system-test --timeout 120000", + "system-test": "nyc mocha build/system-test/test.*.js --timeout 120000", "samples-test": "echo no sample tests 😱", "docs-test": "linkinator docs -r --skip www.googleapis.com", "predocs-test": "npm run docs" diff --git a/system-test/fixtures/google-gax-packaging-test-app/.eslintignore b/system-test/fixtures/google-gax-packaging-test-app/.eslintignore new file mode 100644 index 000000000..f6fac98b0 --- /dev/null +++ b/system-test/fixtures/google-gax-packaging-test-app/.eslintignore @@ -0,0 +1,3 @@ +node_modules/* +samples/node_modules/* +src/**/doc/* diff --git a/system-test/fixtures/google-gax-packaging-test-app/.eslintrc.yml b/system-test/fixtures/google-gax-packaging-test-app/.eslintrc.yml new file mode 100644 index 000000000..53f3165e1 --- /dev/null +++ b/system-test/fixtures/google-gax-packaging-test-app/.eslintrc.yml @@ -0,0 +1,14 @@ +--- +extends: + - 'eslint:recommended' + - 'plugin:node/recommended' + - prettier +plugins: + - node + - prettier +rules: + prettier/prettier: error + block-scoped-var: error + eqeqeq: error + no-warning-comments: warn + no-console: off diff --git a/system-test/fixtures/google-gax-packaging-test-app/.prettierignore b/system-test/fixtures/google-gax-packaging-test-app/.prettierignore new file mode 100644 index 000000000..f6fac98b0 --- /dev/null +++ b/system-test/fixtures/google-gax-packaging-test-app/.prettierignore @@ -0,0 +1,3 @@ +node_modules/* +samples/node_modules/* +src/**/doc/* diff --git a/system-test/fixtures/google-gax-packaging-test-app/.prettierrc b/system-test/fixtures/google-gax-packaging-test-app/.prettierrc new file mode 100644 index 000000000..df6eac074 --- /dev/null +++ b/system-test/fixtures/google-gax-packaging-test-app/.prettierrc @@ -0,0 +1,8 @@ +--- +bracketSpacing: false +printWidth: 80 +semi: true +singleQuote: true +tabWidth: 2 +trailingComma: es5 +useTabs: false diff --git a/system-test/fixtures/google-gax-packaging-test-app/README.md b/system-test/fixtures/google-gax-packaging-test-app/README.md new file mode 100644 index 000000000..d3611445a --- /dev/null +++ b/system-test/fixtures/google-gax-packaging-test-app/README.md @@ -0,0 +1,19 @@ +# Test application for google-gax + +This test application is based on GAPIC Showcase API Client (automatically +generated by [gapic-generator](https://github.com/googleapis/gapic-generator). + +[GAPIC Showcase](https://github.com/googleapis/gapic-showcase) Echo service is +the sample GAPIC service that shows all the features of generated API clients: + +- retries +- timeouts +- long running operations +- client streaming +- server streaming +- bi-directional streaming +- automatic pagination + +Since all these features are supported by GAX, the Showcase library is a great +use case for these features. + diff --git a/system-test/fixtures/google-gax-packaging-test-app/package.json b/system-test/fixtures/google-gax-packaging-test-app/package.json new file mode 100644 index 000000000..c6265aeb3 --- /dev/null +++ b/system-test/fixtures/google-gax-packaging-test-app/package.json @@ -0,0 +1,37 @@ +{ + "name": "google-gax-packaging-test-app", + "version": "0.0.1", + "description": "Test application for google-gax", + "main": "src/index.js", + "files": [ + "src", + "protos" + ], + "license": "Apache-2.0", + "keywords": [], + "scripts": { + "lint": "eslint **/*.js", + "prettier": "prettier --write **/*.js", + "start": "node src/index.js", + "test": "mocha test/*.js" + }, + "devDependencies": { + "eslint": "^5.16.0", + "eslint-config-prettier": "^4.1.0", + "eslint-plugin-node": "^8.0.1", + "eslint-plugin-prettier": "^3.0.1", + "mocha": "^6.1.4", + "prettier": "^1.17.0", + "through2": "^3.0.1" + }, + "dependencies": { + "@grpc/grpc-js": "^0.3.6", + "google-gax": "file:./google-gax.tgz", + "grpc": "1.20.0", + "lodash.merge": "^4.6.1", + "protobufjs": "^6.8.8" + }, + "engines": { + "node": ">=10.0.0" + } +} diff --git a/system-test/fixtures/google-gax-packaging-test-app/protos/google/showcase/v1beta1/echo.proto b/system-test/fixtures/google-gax-packaging-test-app/protos/google/showcase/v1beta1/echo.proto new file mode 100644 index 000000000..afe4ff0b9 --- /dev/null +++ b/system-test/fixtures/google-gax-packaging-test-app/protos/google/showcase/v1beta1/echo.proto @@ -0,0 +1,176 @@ +// 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. + +syntax = "proto3"; + +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/api/field_behavior.proto"; +import "google/longrunning/operations.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; +import "google/rpc/status.proto"; + +package google.showcase.v1beta1; + +option go_package = "github.com/googleapis/gapic-showcase/server/genproto"; +option java_package = "com.google.showcase.v1beta1"; +option java_multiple_files = true; + +// This service is used showcase the four main types of rpcs - unary, server +// side streaming, client side streaming, and bidirectional streaming. This +// service also exposes methods that explicitly implement server delay, and +// paginated calls. +service Echo { + // This service is meant to only run locally on the port 7469 (keypad digits + // for "show"). + option (google.api.default_host) = "localhost:7469"; + + // This method simply echos the request. This method is showcases unary rpcs. + rpc Echo(EchoRequest) returns (EchoResponse) { + option (google.api.http) = { + post: "/v1beta1/echo:echo" + body: "*" + }; + } + + // This method split the given content into words and will pass each word back + // through the stream. This method showcases server-side streaming rpcs. + rpc Expand(ExpandRequest) returns (stream EchoResponse) { + option (google.api.http) = { + post: "/v1beta1/echo:expand" + body: "*" + }; + // TODO(landrito): change this to be `fields: ["content", "error"]` once + // github.com/dcodeIO/protobuf.js/issues/1094 has been resolved. + option (google.api.method_signature) = "content,error"; + } + + // This method will collect the words given to it. When the stream is closed + // by the client, this method will return the a concatenation of the strings + // passed to it. This method showcases client-side streaming rpcs. + rpc Collect(stream EchoRequest) returns (EchoResponse) { + option (google.api.http) = { + post: "/v1beta1/echo:collect" + body: "*" + }; + } + + // This method, upon receiving a request on the stream, the same content will + // be passed back on the stream. This method showcases bidirectional + // streaming rpcs. + rpc Chat(stream EchoRequest) returns (stream EchoResponse); + + // This is similar to the Expand method but instead of returning a stream of + // expanded words, this method returns a paged list of expanded words. + rpc PagedExpand(PagedExpandRequest) returns (PagedExpandResponse) { + option (google.api.http) = { + post: "/v1beta1/echo:pagedExpand" + body: "*" + }; + } + + // This method will wait the requested amount of and then return. + // This method showcases how a client handles a request timing out. + rpc Wait(WaitRequest) returns (google.longrunning.Operation) { + option (google.api.http) = { + post: "/v1beta1/echo:wait" + body: "*" + }; + option (google.longrunning.operation_info) = { + response_type: "WaitResponse" + metadata_type: "WaitMetadata" + }; + } +} + +// The request message used for the Echo, Collect and Chat methods. If content +// is set in this message then the request will succeed. If a status is +message EchoRequest { + oneof response { + // The content to be echoed by the server. + string content = 1; + + // The error to be thrown by the server. + google.rpc.Status error = 2; + } +} + +// The response message for the Echo methods. +message EchoResponse { + // The content specified in the request. + string content = 1; +} + +// The request message for the Expand method. +message ExpandRequest { + // The content that will be split into words and returned on the stream. + string content = 1; + + // The error that is thrown after all words are sent on the stream. + google.rpc.Status error = 2; +} + +// The request for the PagedExpand method. +message PagedExpandRequest { + // The string to expand. + string content = 1 [(google.api.field_behavior) = REQUIRED]; + + // The amount of words to returned in each page. + int32 page_size = 2; + + // The position of the page to be returned. + string page_token = 3; +} + +// The response for the PagedExpand method. +message PagedExpandResponse { + // The words that were expanded. + repeated EchoResponse responses = 1; + + // The next page token. + string next_page_token = 2; +} + +// The request for Wait method. +message WaitRequest { + oneof end { + // The time that this operation will complete. + google.protobuf.Timestamp end_time = 1; + + // The duration of this operation. + google.protobuf.Duration ttl = 4; + } + + oneof response { + // The error that will be returned by the server. If this code is specified + // to be the OK rpc code, an empty response will be returned. + google.rpc.Status error = 2; + + // The response to be returned on operation completion. + WaitResponse success = 3; + } +} + +// The result of the Wait operation. +message WaitResponse { + // This content of the result. + string content = 1; +} + +// The metadata for Wait operation. +message WaitMetadata { + // The time that this operation will complete. + google.protobuf.Timestamp end_time =1; +} diff --git a/system-test/fixtures/google-gax-packaging-test-app/protos/google/showcase/v1beta1/identity.proto b/system-test/fixtures/google-gax-packaging-test-app/protos/google/showcase/v1beta1/identity.proto new file mode 100644 index 000000000..2cc91003e --- /dev/null +++ b/system-test/fixtures/google-gax-packaging-test-app/protos/google/showcase/v1beta1/identity.proto @@ -0,0 +1,162 @@ +// 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. + +syntax = "proto3"; + +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/api/field_behavior.proto"; +import "google/api/resource.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/field_mask.proto"; +import "google/protobuf/timestamp.proto"; + +package google.showcase.v1beta1; + +option go_package = "github.com/googleapis/gapic-showcase/server/genproto"; +option java_package = "com.google.showcase.v1beta1"; +option java_multiple_files = true; + +// A simple identity service. +service Identity { + // This service is meant to only run locally on the port 7469 (keypad digits + // for "show"). + option (google.api.default_host) = "localhost:7469"; + + // Creates a user. + rpc CreateUser(CreateUserRequest) returns (User) { + option (google.api.http) = { + post: "/v1beta1/users" + body: "*" + }; + option (google.api.method_signature) = "user.display_name,user.email"; + } + + // Retrieves the User with the given uri. + rpc GetUser(GetUserRequest) returns (User) { + option (google.api.http) = { + get: "/v1beta1/{name=users/*}" + }; + option (google.api.method_signature) = "name"; + } + + // Updates a user. + rpc UpdateUser(UpdateUserRequest) returns (User) { + option (google.api.http) = { + patch: "/v1beta1/{user.name=users/*}" + body: "*" + }; + } + + // Deletes a user, their profile, and all of their authored messages. + rpc DeleteUser(DeleteUserRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + delete: "/v1beta1/{name=users/*}" + }; + option (google.api.method_signature) = "name"; + } + + // Lists all users. + rpc ListUsers(ListUsersRequest) returns (ListUsersResponse) { + option (google.api.http) = { + get: "/v1beta1/users" + }; + } +} + +// A user. +message User { + // The resource name of the user. + string name = 1 [(google.api.resource).pattern = "users/{user_id}"]; + + // The display_name of the user. + string display_name = 2 [(google.api.field_behavior) = REQUIRED]; + + // The email address of the user. + string email = 3 [(google.api.field_behavior) = REQUIRED]; + + // The timestamp at which the user was created. + google.protobuf.Timestamp create_time = 4 [ + (google.api.field_behavior) = OUTPUT_ONLY + ]; + + // The latest timestamp at which the user was updated. + google.protobuf.Timestamp update_time = 5 [ + (google.api.field_behavior) = OUTPUT_ONLY + ]; +} + +// The request message for the google.showcase.v1beta1.Identity\CreateUser +// method. +message CreateUserRequest { + // The user to create. + User user = 1; +} + +// The request message for the google.showcase.v1beta1.Identity\GetUser +// method. +message GetUserRequest { + // The resource name of the requested user. + string name = 1 [ + (google.api.resource_reference) = "User", + (google.api.field_behavior) = REQUIRED + ]; +} + +// The request message for the google.showcase.v1beta1.Identity\UpdateUser +// method. +message UpdateUserRequest { + // The user to update. + User user = 1; + + // The field mask to determine wich fields are to be updated. If empty, the + // server will assume all fields are to be updated. + google.protobuf.FieldMask update_mask = 2; +} + +// The request message for the google.showcase.v1beta1.Identity\DeleteUser +// method. +message DeleteUserRequest { + // The resource name of the user to delete. + string name = 1 [ + (google.api.resource_reference) = "User", + (google.api.field_behavior) = REQUIRED + ]; +} + +// The request message for the google.showcase.v1beta1.Identity\ListUsers +// method. +message ListUsersRequest { + // The maximum number of users to return. Server may return fewer users + // than requested. If unspecified, server will pick an appropriate default. + int32 page_size = 1; + + // The value of google.showcase.v1beta1.ListUsersResponse.next_page_token + // returned from the previous call to + // `google.showcase.v1beta1.Identity\ListUsers` method. + string page_token = 2; +} + +// The response message for the google.showcase.v1beta1.Identity\ListUsers +// method. +message ListUsersResponse { + // The list of users. + repeated User users = 1; + + // A token to retrieve next page of results. + // Pass this value in ListUsersRequest.page_token field in the subsequent + // call to `google.showcase.v1beta1.Message\ListUsers` method to retrieve the + // next page of results. + string next_page_token = 2; +} diff --git a/system-test/fixtures/google-gax-packaging-test-app/protos/google/showcase/v1beta1/messaging.proto b/system-test/fixtures/google-gax-packaging-test-app/protos/google/showcase/v1beta1/messaging.proto new file mode 100644 index 000000000..c223e2991 --- /dev/null +++ b/system-test/fixtures/google-gax-packaging-test-app/protos/google/showcase/v1beta1/messaging.proto @@ -0,0 +1,503 @@ +// 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. + +syntax = "proto3"; + +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/api/field_behavior.proto"; +import "google/api/resource.proto"; +import "google/longrunning/operations.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/field_mask.proto"; +import "google/protobuf/timestamp.proto"; +import "google/rpc/error_details.proto"; + +package google.showcase.v1beta1; + +option go_package = "github.com/googleapis/gapic-showcase/server/genproto"; +option java_package = "com.google.showcase.v1beta1"; +option java_multiple_files = true; + +// This resource is defined in google/showcase/v1beta1/user.proto. Since +// google/showcase/v1beta1/user.proto is not imported from this file, the +// resource needs to be defined here to be used in this API. +option (google.api.resource_definition) = { + symbol: "User", + pattern: "users/{user_id}" +}; + +// A simple messaging service that implements chat rooms and profile posts. +// +// This messaging service showcases the features that API clients +// generated by gapic-generators implement. +service Messaging { + // This service is meant to only run locally on the port 7469 (keypad digits + // for "show"). + option (google.api.default_host) = "localhost:7469"; + + // Creates a room. + rpc CreateRoom(CreateRoomRequest) returns (Room) { + option (google.api.http) = { + post: "/v1beta1/rooms" + body: "*" + }; + option (google.api.method_signature) = "room.display_name,room.description"; + } + + // Retrieves the Room with the given resource name. + rpc GetRoom(GetRoomRequest) returns (Room) { + option (google.api.http) = { + get: "/v1beta1/{name=rooms/*}" + }; + option (google.api.method_signature) = "name"; + } + + // Updates a room. + rpc UpdateRoom(UpdateRoomRequest) returns (Room) { + option (google.api.http) = { + patch: "/v1beta1/{room.name=rooms/*}" + body: "*" + }; + } + + // Deletes a room and all of its blurbs. + rpc DeleteRoom(DeleteRoomRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + delete: "/v1beta1/{name=rooms/*}" + }; + option (google.api.method_signature) = "name"; + } + + // Lists all chat rooms. + rpc ListRooms(ListRoomsRequest) returns (ListRoomsResponse) { + option (google.api.http) = { + get: "/v1beta1/rooms" + }; + } + + // Creates a blurb. If the parent is a room, the blurb is understood to be a + // message in that room. If the parent is a profile, the blurb is understood + // to be a post on the profile. + rpc CreateBlurb(CreateBlurbRequest) returns (Blurb) { + option (google.api.http) = { + post: "/v1beta1/{parent=rooms/*}/blurbs" + body: "*" + additional_bindings: { + post: "/v1beta1/{parent=users/*/profile}/blurbs" + body: "*" + } + }; + option (google.api.method_signature) = "parent,blurb.user,blurb.text"; + option (google.api.method_signature) = "parent,blurb.user,blurb.image"; + } + + // Retrieves the Blurb with the given resource name. + rpc GetBlurb(GetBlurbRequest) returns (Blurb) { + option (google.api.http) = { + get: "/v1beta1/{name=rooms/*/blurbs/*}" + additional_bindings: { + get: "/v1beta1/{name=users/*/profile/blurbs/*}" + } + }; + option (google.api.method_signature) = "name"; + } + + // Updates a blurb. + rpc UpdateBlurb(UpdateBlurbRequest) returns (Blurb) { + option (google.api.http) = { + patch: "/v1beta1/{blurb.name=rooms/*/blurbs/*}" + body: "*" + additional_bindings: { + patch: "/v1beta1/{blurb.name=users/*/profile/blurbs/*}" + body: "*" + } + }; + } + + // Deletes a blurb. + rpc DeleteBlurb(DeleteBlurbRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + delete: "/v1beta1/{name=rooms/*/blurbs/*}" + additional_bindings: { + delete: "/v1beta1/{name=users/*/profile/blurbs/*}" + } + }; + option (google.api.method_signature) = "name"; + } + + // Lists blurbs for a specific chat room or user profile depending on the + // parent resource name. + rpc ListBlurbs(ListBlurbsRequest) returns (ListBlurbsResponse) { + option (google.api.http) = { + get: "/v1beta1/{parent=rooms/*}/blurbs" + additional_bindings: { + get: "/v1beta1/{parent=users/*/profile}/blurbs" + } + }; + option (google.api.method_signature) = "parent"; + } + + // This method searches through all blurbs across all rooms and profiles + // for blurbs containing to words found in the query. Only posts that + // contain an exact match of a queried word will be returned. + rpc SearchBlurbs(SearchBlurbsRequest) returns (google.longrunning.Operation) { + option (google.api.http) = { + post: "/v1beta1/{parent=rooms/-}/blurbs:search" + body: "*" + additional_bindings: { + post: "/v1beta1/{parent=users/-/profile}/blurbs:search" + } + }; + option (google.longrunning.operation_info) = { + response_type: "SearchBlurbsResponse" + metadata_type: "SearchBlurbsMetadata" + }; + option (google.api.method_signature) = "query"; + } + + // This returns a stream that emits the blurbs that are created for a + // particular chat room or user profile. + rpc StreamBlurbs(StreamBlurbsRequest) returns (stream StreamBlurbsResponse) { + option (google.api.http) = { + post: "/v1beta1/{name=rooms/*}/blurbs:stream" + body: "*" + additional_bindings: { + post: "/v1beta1/{name=users/*/profile}/blurbs:stream" + body: "*" + } + }; + } + + // This is a stream to create multiple blurbs. If an invalid blurb is + // requested to be created, the stream will close with an error. + rpc SendBlurbs(stream CreateBlurbRequest) returns (SendBlurbsResponse) { + option (google.api.http) = { + post: "/v1beta1/{parent=rooms/*}/blurbs:send" + body: "*" + additional_bindings: { + post: "/v1beta1/{parent=users/*/profile}/blurbs:send" + body: "*" + } + }; + } + + // This method starts a bidirectional stream that receives all blurbs that + // are being created after the stream has started and sends requests to create + // blurbs. If an invalid blurb is requested to be created, the stream will + // close with an error. + rpc Connect(stream ConnectRequest) returns (stream StreamBlurbsResponse); +} + +// A chat room. +message Room { + // The resource name of the chat room. + string name = 1 [(google.api.resource).pattern = "rooms/{room_id}"]; + + // The human readable name of the chat room. + string display_name = 2 [(google.api.field_behavior) = REQUIRED]; + + // The description of the chat room. + string description = 3; + + // The timestamp at which the room was created. + google.protobuf.Timestamp create_time = 4 [ + (google.api.field_behavior) = OUTPUT_ONLY + ]; + + // The latest timestamp at which the room was updated. + google.protobuf.Timestamp update_time = 5 [ + (google.api.field_behavior) = OUTPUT_ONLY + ]; +} + +// The request message for the google.showcase.v1beta1.Messaging\CreateRoom +// method. +message CreateRoomRequest { + // The room to create. + Room room = 1; +} + +// The request message for the google.showcase.v1beta1.Messaging\GetRoom +// method. +message GetRoomRequest { + // The resource name of the requested room. + string name = 1 [ + (google.api.resource_reference) = "Room", + (google.api.field_behavior) = REQUIRED + ]; +} + +// The request message for the google.showcase.v1beta1.Messaging\UpdateRoom +// method. +message UpdateRoomRequest { + // The room to update. + Room room = 1; + + // The field mask to determine wich fields are to be updated. If empty, the + // server will assume all fields are to be updated. + google.protobuf.FieldMask update_mask = 2; +} + +// The request message for the google.showcase.v1beta1.Messaging\DeleteRoom +// method. +message DeleteRoomRequest { + // The resource name of the requested room. + string name = 1 [ + (google.api.resource_reference) = "Room", + (google.api.field_behavior) = REQUIRED + ]; +} + +// The request message for the google.showcase.v1beta1.Messaging\ListRooms +// method. +message ListRoomsRequest { + // The maximum number of rooms return. Server may return fewer rooms + // than requested. If unspecified, server will pick an appropriate default. + int32 page_size = 1; + + // The value of google.showcase.v1beta1.ListRoomsResponse.next_page_token + // returned from the previous call to + // `google.showcase.v1beta1.Messaging\ListRooms` method. + string page_token = 2; +} + +// The response message for the google.showcase.v1beta1.Messaging\ListRooms +// method. +message ListRoomsResponse { + // The list of rooms. + repeated Room rooms = 1; + + // A token to retrieve next page of results. + // Pass this value in ListRoomsRequest.page_token field in the subsequent + // call to `google.showcase.v1beta1.Messaging\ListRooms` method to retrieve the + // next page of results. + string next_page_token = 2; +} + +// This protocol buffer message represents a blurb sent to a chat room or +// posted on a user profile. +message Blurb { + // The resource name of the chat room. + string name = 1; + + // The resource name of the blurb's author. + string user = 2 [ + (google.api.resource_reference) = "User", + (google.api.field_behavior) = REQUIRED]; + + oneof content { + // The textual content of this blurb. + string text = 3; + + // The image content of this blurb. + bytes image = 4; + } + + // The timestamp at which the blurb was created. + google.protobuf.Timestamp create_time = 5 [ + (google.api.field_behavior) = OUTPUT_ONLY + ]; + + // The latest timestamp at which the blurb was updated. + google.protobuf.Timestamp update_time = 6 [ + (google.api.field_behavior) = OUTPUT_ONLY + ]; +} + +// The request message for the google.showcase.v1beta1.Messaging\CreateBlurb +// method. +message CreateBlurbRequest { + // The resource name of the chat room or user profile that this blurb will + // be tied to. + string parent = 1 [ + (google.api.resource_reference) = "BlurbParent", + (google.api.field_behavior) = REQUIRED + ]; + + // The blurb to create. + Blurb blurb = 2; +} + +// The request message for the google.showcase.v1beta1.Messaging\GetBlurb +// method. +message GetBlurbRequest { + // The resource name of the requested blurb. + string name = 1 [ + (google.api.resource_reference) = "Blurb", + (google.api.field_behavior) = REQUIRED + ]; +} + +// The request message for the google.showcase.v1beta1.Messaging\UpdateBlurb +// method. +message UpdateBlurbRequest { + // The blurb to update. + Blurb blurb = 1; + + // The field mask to determine wich fields are to be updated. If empty, the + // server will assume all fields are to be updated. + google.protobuf.FieldMask update_mask = 2; +} + +// The request message for the google.showcase.v1beta1.Messaging\DeleteBlurb +// method. +message DeleteBlurbRequest { + // The resource name of the requested blurb. + string name = 1 [ + (google.api.resource_reference) = "Blurb", + (google.api.field_behavior) = REQUIRED + ]; +} + +// The request message for the google.showcase.v1beta1.Messaging\ListBlurbs +// method. +message ListBlurbsRequest { + // The resource name of the requested room or profile whos blurbs to list. + string parent = 1 [ + (google.api.resource_reference) = "BlurbParent", + (google.api.field_behavior) = REQUIRED + ]; + + + // The maximum number of blurbs to return. Server may return fewer + // blurbs than requested. If unspecified, server will pick an appropriate + // default. + int32 page_size = 2; + + // The value of google.showcase.v1beta1.ListBlurbsResponse.next_page_token + // returned from the previous call to + // `google.showcase.v1beta1.Messaging\ListBlurbs` method. + string page_token = 3; +} + +// The response message for the google.showcase.v1beta1.Messaging\ListBlurbs +// method. +message ListBlurbsResponse { + // The list of blurbs. + repeated Blurb blurbs = 1; + + // A token to retrieve next page of results. + // Pass this value in ListBlurbsRequest.page_token field in the subsequent + // call to `google.showcase.v1beta1.Blurb\ListBlurbs` method to retrieve + // the next page of results. + string next_page_token = 2; +} + +// The request message for the google.showcase.v1beta1.Messaging\SearchBlurbs +// method. +message SearchBlurbsRequest { + // The query used to search for blurbs containing to words of this string. + // Only posts that contain an exact match of a queried word will be returned. + string query = 1 [(google.api.field_behavior) = REQUIRED]; + + // The rooms or profiles to search. If unset, `SearchBlurbs` will search all + // rooms and all profiles. + string parent = 2 [(google.api.resource_reference) = "BlurbParent"]; + + // The maximum number of blurbs return. Server may return fewer + // blurbs than requested. If unspecified, server will pick an appropriate + // default. + int32 page_size = 3; + + // The value of + // google.showcase.v1beta1.SearchBlurbsResponse.next_page_token + // returned from the previous call to + // `google.showcase.v1beta1.Messaging\SearchBlurbs` method. + string page_token = 4; +} + +// The operation metadata message for the +// google.showcase.v1beta1.Messaging\SearchBlurbs method. +message SearchBlurbsMetadata { + // This signals to the client when to next poll for response. + google.rpc.RetryInfo retry_info = 1; +} + +// The operation response message for the +// google.showcase.v1beta1.Messaging\SearchBlurbs method. +message SearchBlurbsResponse { + // Blurbs that matched the search query. + repeated Blurb blurbs = 1; + + // A token to retrieve next page of results. + // Pass this value in SearchBlurbsRequest.page_token field in the subsequent + // call to `google.showcase.v1beta1.Blurb\SearchBlurbs` method to + // retrieve the next page of results. + string next_page_token = 2; +} + +// The request message for the google.showcase.v1beta1.Messaging\StreamBlurbs +// method. +message StreamBlurbsRequest { + // The resource name of a chat room or user profile whose blurbs to stream. + string name = 1 [ + (google.api.resource_reference) = "BlurbParent", + (google.api.field_behavior) = REQUIRED + ]; + + // The time at which this stream will close. + google.protobuf.Timestamp expire_time = 2 [ + (google.api.field_behavior) = REQUIRED + ]; +} + +// The response message for the google.showcase.v1beta1.Messaging\StreamBlurbs +// method. +message StreamBlurbsResponse { + // The blurb that was either created, updated, or deleted. + Blurb blurb = 1; + + // The action that triggered the blurb to be returned. + enum Action { + ACTION_UNSPECIFIED = 0; + + // Specifies that the blurb was created. + CREATE = 1; + + // Specifies that the blurb was updated. + UPDATE = 2; + + // Specifies that the blurb was deleted. + DELETE = 3; + } + + // The action that triggered the blurb to be returned. + Action action = 2; +} + +// The response message for the google.showcase.v1beta1.Messaging\SendBlurbs +// method. +message SendBlurbsResponse { + // The names of successful blurb creations. + repeated string names = 1; +} + +// The request message for the google.showcase.v1beta1.Messaging\Connect +// method. +message ConnectRequest { + message ConnectConfig { + // The room or profile to follow and create messages for. + string parent = 1 [(google.api.resource_reference) = "BlurbParent"]; + } + + oneof request { + // Provides information that specifies how to process subsequent requests. + // The first `ConnectRequest` message must contain a `config` message. + ConnectConfig config = 1; + + // The blurb to be created. + Blurb blurb = 2; + } +} diff --git a/system-test/fixtures/google-gax-packaging-test-app/protos/google/showcase/v1beta1/testing.proto b/system-test/fixtures/google-gax-packaging-test-app/protos/google/showcase/v1beta1/testing.proto new file mode 100644 index 000000000..1574cda98 --- /dev/null +++ b/system-test/fixtures/google-gax-packaging-test-app/protos/google/showcase/v1beta1/testing.proto @@ -0,0 +1,363 @@ +// 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. + +syntax = "proto3"; + +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/api/resource.proto"; +import "google/protobuf/empty.proto"; + +package google.showcase.v1beta1; + +option go_package = "github.com/googleapis/gapic-showcase/server/genproto"; +option java_package = "com.google.showcase.v1beta1"; +option java_multiple_files = true; + +// A service to facilitate running discrete sets of tests +// against Showcase. +service Testing { + // This service is meant to only run locally on the port 7469 (keypad digits + // for "show"). + option (google.api.default_host) = "localhost:7469"; + + // Creates a new testing session. + rpc CreateSession(CreateSessionRequest) returns (Session) { + option (google.api.http) = { + post: "/v1beta1/sessions" + body: "session" + }; + } + + // Gets a testing session. + rpc GetSession(GetSessionRequest) returns (Session) { + option (google.api.http) = { + get: "/v1beta1/{name=sessions/*}" + }; + } + + // Lists the current test sessions. + rpc ListSessions(ListSessionsRequest) returns (ListSessionsResponse) { + option (google.api.http) = { + get: "/v1beta1/sessions" + }; + } + + // Delete a test session. + rpc DeleteSession(DeleteSessionRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + delete: "/v1beta1/{name=sessions/*}" + }; + } + + // Report on the status of a session. + // This generates a report detailing which tests have been completed, + // and an overall rollup. + rpc ReportSession(ReportSessionRequest) returns (ReportSessionResponse) { + option (google.api.http) = { + post: "/v1beta1/{name=sessions/*}:report" + }; + } + + // List the tests of a sessesion. + rpc ListTests(ListTestsRequest) returns (ListTestsResponse) { + option (google.api.http) = { + get: "/v1beta1/{parent=sessions/*}/tests" + }; + } + + // Explicitly decline to implement a test. + // + // This removes the test from subsequent `ListTests` calls, and + // attempting to do the test will error. + // + // This method will error if attempting to delete a required test. + rpc DeleteTest(DeleteTestRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + delete: "/v1beta1/{name=sessions/*/tests/*}" + }; + } + + // Register a response to a test. + // + // In cases where a test involves registering a final answer at the + // end of the test, this method provides the means to do so. + rpc VerifyTest(VerifyTestRequest) returns (VerifyTestResponse) { + option (google.api.http) = { + post: "/v1beta1/{name=sessions/*/tests/*}:check" + }; + } +} + +// A session is a suite of tests, generally being made in the context +// of testing code generation. +// +// A session defines tests it may expect, based on which version of the +// code generation spec is in use. +message Session { + // The name of the session. The ID must conform to ^[a-z]+$ + // If this is not provided, Showcase chooses one at random. + string name = 1 [(google.api.resource).pattern = "sessions/{session}"]; + + // The specification versions understood by Showcase. + enum Version { + // Unspecified version. If passed on creation, the session will default + // to using the latest stable release. + VERSION_UNSPECIFIED = 0; + + // The latest v1. Currently, this is v1.0. + V1_LATEST = 1; + + // v1.0. (Until the spec is "GA", this will be a moving target.) + V1_0 = 2; + } + + // Required. The version this session is using. + Version version = 2; +} + +// The request for the CreateSession method. +message CreateSessionRequest { + // The session to be created. + // Sessions are immutable once they are created (although they can + // be deleted). + Session session = 1; +} + +// The request for the GetSession method. +message GetSessionRequest { + // The session to be retrieved. + string name = 1 [(google.api.resource_reference) = "Session"]; +} + +// The request for the ListSessions method. +message ListSessionsRequest { + // The maximum number of sessions to return per page. + int32 page_size = 1; + + // The page token, for retrieving subsequent pages. + string page_token = 2; +} + +// Response for the ListSessions method. +message ListSessionsResponse { + // The sessions being returned. + repeated Session sessions = 1; + + // The next page token, if any. + // An empty value here means the last page has been reached. + string next_page_token = 2; +} + +// Request for the DeleteSession method. +message DeleteSessionRequest { + // The session to be deleted. + string name = 1 [(google.api.resource_reference) = "Session"]; +} + +// Request message for reporting on a session. +message ReportSessionRequest { + // The session to be reported on. + string name = 1 [(google.api.resource_reference) = "Session"]; +} + +// Response message for reporting on a session. +message ReportSessionResponse { + // The topline state of the report. + enum Result { + RESULT_UNSPECIFIED = 0; + + // The session is complete, and everything passed. + PASSED = 1; + + // The session had an explicit failure. + FAILED = 2; + + // The session is incomplete. This is a failure response. + INCOMPLETE = 3; + } + + // The state of the report. + Result result = 1; + + // The test runs of this session. + repeated TestRun test_runs = 2; +} + +message Test { + // The name of the test. + // The tests/* portion of the names are hard-coded, and do not change + // from session to session. + string name = 1 [(google.api.resource).pattern = "sessions/{session}/tests/{test}"]; + + // Whether or not a test is required, recommended, or optional. + enum ExpectationLevel { + EXPECTATION_LEVEL_UNSPECIFIED = 0; + + // This test is strictly required. + REQUIRED = 1; + + // This test is recommended. + // + // If a generator explicitly ignores a recommended test (see `DeleteTest`), + // then the report may still pass, but with a warning. + // + // If a generator skips a recommended test and does not explicitly + // express that intention, the report will fail. + RECOMMENDED = 2; + + // This test is optional. + // + // If a generator explicitly ignores an optional test (see `DeleteTest`), + // then the report may still pass, and no warning will be issued. + // + // If a generator skips an optional test and does not explicitly + // express that intention, the report may still pass, but with a + // warning. + OPTIONAL = 3; + } + + // The expectation level for this test. + ExpectationLevel expectation_level = 2; + + // A description of the test. + string description = 3; + + // A blueprint is an explicit definition of methods and requests that are needed + // to be made to test this specific test case. Ideally this would be represented + // by something more robust like CEL, but as of writing this, I am unsure if CEL + // is ready. + message Blueprint { + // The name of this blueprint. + string name = 1 [(google.api.resource).pattern = "sessions/{session}/tests/{test}/blueprints/{blueprint}"]; + + // A description of this blueprint. + string description = 2; + + // A message representing a method invocation. + message Invocation { + // The fully qualified name of the showcase method to be invoked. + string method = 1; + + // The request to be made if a specific request is necessary. + bytes serialized_request = 2; + } + + // The initial request to trigger this test. + Invocation request = 3; + + // An ordered list of method calls that can be called to trigger this test. + repeated Invocation additional_requests = 4; + } + + // The blueprints that will satisfy this test. There may be multiple blueprints + // that can signal to the server that this test case is being exercised. Although + // multiple blueprints are specified, only a single blueprint needs to be run to + // signal that the test case was exercised. + repeated Blueprint blueprints = 4; +} + +// An issue found in the test. +message Issue { + // The different potential types of issues. + enum Type { + TYPE_UNSPECIFIED = 0; + + // The test was never instrumented. + SKIPPED = 1; + + // The test was started but never confirmed. + PENDING = 2; + + // The test was instrumented, but Showcase got an unexpected + // value when the generator tried to confirm success. + INCORRECT_CONFIRMATION = 3; + } + + // The type of the issue. + Type type = 1; + + // Severity levels. + enum Severity { + SEVERITY_UNSPECIFIED = 0; + + // Errors. + ERROR = 1; + + // Warnings. + WARNING = 2; + } + + // The severity of the issue. + Severity severity = 2; + + // A description of the issue. + string description = 3; +} + +// The request for the ListTests method. +message ListTestsRequest { + // The session. + string parent = 1 [(google.api.resource_reference) = "Session"]; + + // The maximum number of tests to return per page. + int32 page_size = 2; + + // The page token, for retrieving subsequent pages. + string page_token = 3; +} + +// The response for the ListTests method. +message ListTestsResponse { + // The tests being returned. + repeated Test tests = 1; + + // The next page token, if any. + // An empty value here means the last page has been reached. + string next_page_token = 2; +} + +// A TestRun is the result of running a Test. +message TestRun { + // The name of the test. + // The tests/* portion of the names are hard-coded, and do not change + // from session to session. + string test = 1 [(google.api.resource_reference) = "Test"]; + + + // An issue found with the test run. If empty, this test run was successful. + Issue issue = 2; +} + +// Request message for deleting a test. +message DeleteTestRequest { + // The test to be deleted. + string name = 1 [(google.api.resource_reference) = "Test"]; +} + +message VerifyTestRequest { + // The test to have an answer registered to it. + string name = 1 [(google.api.resource_reference) = "Test"]; + + // The answer from the test. + bytes answer = 2; + + // The answers from the test if multiple are to be checked + repeated bytes answers = 3; +} + +message VerifyTestResponse { + // An issue if check answer was unsuccessful. This will be empty if the check answer succeeded. + Issue issue = 1; +} diff --git a/system-test/fixtures/google-gax-packaging-test-app/src/index.js b/system-test/fixtures/google-gax-packaging-test-app/src/index.js new file mode 100644 index 000000000..8434e6cc5 --- /dev/null +++ b/system-test/fixtures/google-gax-packaging-test-app/src/index.js @@ -0,0 +1,162 @@ +// Copyright 2019 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. + +'use strict'; + +const assert = require('assert'); + +let grpc; +if (process.env['GOOGLE_CLOUD_USE_GRPC_JS']) { + grpc = require('@grpc/grpc-js'); +} else { + grpc = require('grpc'); +} + +// Import the clients for each version supported by this package. +const gapic = Object.freeze({ + v1beta1: require('./v1beta1'), +}); + +module.exports.v1beta1 = gapic.v1beta1; + +// Alias `module.exports` as `module.exports.default`, for future-proofing. +module.exports.default = Object.assign({}, module.exports); + +if (require.main === module) { + testShowcase().then( + () => { + console.log('It works!'); + }, + err => { + process.exitCode = 1; + console.log(err); + } + ); +} + +async function testShowcase() { + const client = new gapic.v1beta1.EchoClient({ + grpc, + sslCreds: grpc.credentials.createInsecure(), + }); + + // assuming gRPC server is started locally + await testEcho(client); + await testExpand(client); + await testPagedExpand(client); + await testCollect(client); + await testChat(client); + await testWait(client); +} + +async function testEcho(client) { + const request = { + content: 'test', + }; + const [response] = await client.echo(request); + assert.deepStrictEqual(request.content, response.content); +} + +async function testExpand(client) { + const words = ['nobody', 'ever', 'reads', 'test', 'input']; + const request = { + content: words.join(' '), + }; + const result = await new Promise((resolve, reject) => { + const stream = client.expand(request); + const result = []; + stream.on('data', response => { + result.push(response.content); + }); + stream.on('end', () => { + resolve(result); + }); + stream.on('error', reject); + }); + assert.deepStrictEqual(words, result); +} + +async function testPagedExpand(client) { + const words = ['nobody', 'ever', 'reads', 'test', 'input']; + const request = { + content: words.join(' '), + pageSize: 2, + }; + const [response] = await client.pagedExpand(request); + const result = response.map(r => r.content); + assert.deepStrictEqual(words, result); +} + +async function testCollect(client) { + const words = ['nobody', 'ever', 'reads', 'test', 'input']; + const result = await new Promise((resolve, reject) => { + const stream = client.collect((err, result) => { + if (err) { + reject(err); + return; + } + resolve(result); + }); + for (const word of words) { + const request = {content: word}; + stream.write(request); + } + stream.end(); + }); + assert.deepStrictEqual(result.content, words.join(' ')); +} + +async function testChat(client) { + const words = [ + 'nobody', + 'ever', + 'reads', + 'test', + 'input', + 'especially', + 'this', + 'one', + ]; + const result = await new Promise((resolve, reject) => { + const result = []; + const stream = client.chat(); + stream.on('data', response => { + result.push(response.content); + }); + stream.on('end', () => { + resolve(result); + }); + stream.on('error', reject); + for (const word of words) { + stream.write({content: word}); + } + stream.end(); + }); + assert.deepStrictEqual(result, words); +} + +async function testWait(client) { + const request = { + ttl: { + seconds: 5, + nanos: 0, + }, + success: { + content: 'done', + }, + }; + const [operation] = await client.wait(request); + const [response] = await operation.promise(); + assert.deepStrictEqual(response.content, request.success.content); +} diff --git a/system-test/fixtures/google-gax-packaging-test-app/src/v1beta1/echo_client.js b/system-test/fixtures/google-gax-packaging-test-app/src/v1beta1/echo_client.js new file mode 100644 index 000000000..6ccfa593c --- /dev/null +++ b/system-test/fixtures/google-gax-packaging-test-app/src/v1beta1/echo_client.js @@ -0,0 +1,660 @@ +// Copyright 2019 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. + +'use strict'; + +const gapicConfig = require('./echo_client_config.json'); +const gax = require('google-gax'); +const merge = require('lodash.merge'); +const path = require('path'); +const protobuf = require('protobufjs'); + +const VERSION = require('../../package.json').version; + +/** + * This service is used showcase the four main types of rpcs - unary, server + * side streaming, client side streaming, and bidirectional streaming. This + * service also exposes methods that explicitly implement server delay, and + * paginated calls. + * + * @class + * @memberof v1beta1 + */ +class EchoClient { + /** + * Construct an instance of EchoClient. + * + * @param {object} [options] - The configuration object. See the subsequent + * parameters for more details. + * @param {object} [options.credentials] - Credentials object. + * @param {string} [options.credentials.client_email] + * @param {string} [options.credentials.private_key] + * @param {string} [options.email] - Account email address. Required when + * using a .pem or .p12 keyFilename. + * @param {string} [options.keyFilename] - Full path to the a .json, .pem, or + * .p12 key downloaded from the Google Developers Console. If you provide + * a path to a JSON file, the projectId option below is not necessary. + * NOTE: .pem and .p12 require you to specify options.email as well. + * @param {number} [options.port] - The port on which to connect to + * the remote host. + * @param {string} [options.projectId] - The project ID from the Google + * Developer's Console, e.g. 'grape-spaceship-123'. We will also check + * the environment variable GCLOUD_PROJECT for your project ID. If your + * app is running in an environment which supports + * {@link https://developers.google.com/identity/protocols/application-default-credentials Application Default Credentials}, + * your project ID will be detected automatically. + * @param {function} [options.promise] - Custom promise module to use instead + * of native Promises. + * @param {string} [options.servicePath] - The domain name of the + * API remote host. + */ + constructor(opts) { + this._descriptors = {}; + + // Ensure that options include the service address and port. + opts = Object.assign( + { + clientConfig: {}, + port: this.constructor.port, + servicePath: this.constructor.servicePath, + }, + opts + ); + + // Create a `gaxGrpc` object, with any grpc-specific options + // sent to the client. + opts.scopes = this.constructor.scopes; + const gaxGrpc = new gax.GrpcClient(opts); + + // Save the auth object to the client, for use by other methods. + this.auth = gaxGrpc.auth; + + // Determine the client header string. + const clientHeader = [ + `gl-node/${process.version}`, + `grpc/${gaxGrpc.grpcVersion}`, + `gax/${gax.version}`, + `gapic/${VERSION}`, + ]; + if (opts.libName && opts.libVersion) { + clientHeader.push(`${opts.libName}/${opts.libVersion}`); + } + + // Load the applicable protos. + const protos = merge( + {}, + gaxGrpc.loadProto( + path.join(__dirname, '..', '..', 'protos'), + 'google/showcase/v1beta1/echo.proto' + ) + ); + + // Some of the methods on this service return "paged" results, + // (e.g. 50 results at a time, with tokens to get subsequent + // pages). Denote the keys used for pagination and results. + this._descriptors.page = { + pagedExpand: new gax.PageDescriptor( + 'pageToken', + 'nextPageToken', + 'responses' + ), + }; + + // Some of the methods on this service provide streaming responses. + // Provide descriptors for these. + this._descriptors.stream = { + expand: new gax.StreamDescriptor(gax.StreamType.SERVER_STREAMING), + collect: new gax.StreamDescriptor(gax.StreamType.CLIENT_STREAMING), + chat: new gax.StreamDescriptor(gax.StreamType.BIDI_STREAMING), + }; + let protoFilesRoot = new gax.GoogleProtoFilesRoot(); + protoFilesRoot = protobuf.loadSync( + path.join( + __dirname, + '..', + '..', + 'protos', + 'google/showcase/v1beta1/echo.proto' + ), + protoFilesRoot + ); + + // This API contains "long-running operations", which return a + // an Operation object that allows for tracking of the operation, + // rather than holding a request open. + this.operationsClient = new gax.lro({ + auth: gaxGrpc.auth, + grpc: gaxGrpc.grpc, + }).operationsClient(opts); + + const waitResponse = protoFilesRoot.lookup( + 'google.showcase.v1beta1.WaitResponse' + ); + const waitMetadata = protoFilesRoot.lookup( + 'google.showcase.v1beta1.WaitMetadata' + ); + + this._descriptors.longrunning = { + wait: new gax.LongrunningDescriptor( + this.operationsClient, + waitResponse.decode.bind(waitResponse), + waitMetadata.decode.bind(waitMetadata) + ), + }; + + // Put together the default options sent with requests. + const defaults = gaxGrpc.constructSettings( + 'google.showcase.v1beta1.Echo', + gapicConfig, + opts.clientConfig, + {'x-goog-api-client': clientHeader.join(' ')} + ); + + // Set up a dictionary of "inner API calls"; the core implementation + // of calling the API is handled in `google-gax`, with this code + // merely providing the destination and request information. + this._innerApiCalls = {}; + + // Put together the "service stub" for + // google.showcase.v1beta1.Echo. + const echoStub = gaxGrpc.createStub( + protos.google.showcase.v1beta1.Echo, + opts + ); + + // Iterate over each of the methods that the service provides + // and create an API call method for each. + const echoStubMethods = [ + 'echo', + 'expand', + 'pagedExpand', + 'collect', + 'chat', + 'wait', + ]; + for (const methodName of echoStubMethods) { + this._innerApiCalls[methodName] = gax.createApiCall( + echoStub.then( + stub => + function() { + const args = Array.prototype.slice.call(arguments, 0); + return stub[methodName].apply(stub, args); + }, + err => + function() { + throw err; + } + ), + defaults[methodName], + this._descriptors.page[methodName] || + this._descriptors.stream[methodName] || + this._descriptors.longrunning[methodName] + ); + } + } + + /** + * The DNS address for this API service. + */ + static get servicePath() { + return 'localhost'; + } + + /** + * The port for this API service. + */ + static get port() { + return 7469; + } + + /** + * The scopes needed to make gRPC calls for every method defined + * in this service. + */ + static get scopes() { + return ['https://www.googleapis.com/auth/cloud-platform']; + } + + /** + * Return the project ID used by this class. + * @param {function(Error, string)} callback - the callback to + * be called with the current project Id. + */ + getProjectId(callback) { + return this.auth.getProjectId(callback); + } + + // ------------------- + // -- Service calls -- + // ------------------- + + /** + * This method simply echos the request. This method is showcases unary rpcs. + * + * @param {Object} request + * The request object that will be sent. + * @param {string} [request.content] + * The content to be echoed by the server. + * @param {Object} [request.error] + * The error to be thrown by the server. + * + * This object should have the same structure as [Status]{@link google.rpc.Status} + * @param {Object} [options] + * Optional parameters. You can override the default settings for this call, e.g, timeout, + * retries, paginations, etc. See [gax.CallOptions]{@link https://googleapis.github.io/gax-nodejs/global.html#CallOptions} for the details. + * @param {function(?Error, ?Object)} [callback] + * The function which will be called with the result of the API call. + * + * The second parameter to the callback is an object representing [EchoResponse]{@link google.showcase.v1beta1.EchoResponse}. + * @returns {Promise} - The promise which resolves to an array. + * The first element of the array is an object representing [EchoResponse]{@link google.showcase.v1beta1.EchoResponse}. + * The promise has a method named "cancel" which cancels the ongoing API call. + * + * @example + * + * const showcase = require('showcase.v1beta1'); + * + * const client = new showcase.v1beta1.EchoClient({ + * // optional auth parameters. + * }); + * + * + * client.echo({}) + * .then(responses => { + * const response = responses[0]; + * // doThingsWith(response) + * }) + * .catch(err => { + * console.error(err); + * }); + */ + echo(request, options, callback) { + if (options instanceof Function && callback === undefined) { + callback = options; + options = {}; + } + options = options || {}; + + return this._innerApiCalls.echo(request, options, callback); + } + + /** + * This method split the given content into words and will pass each word back + * through the stream. This method showcases server-side streaming rpcs. + * + * @param {Object} request + * The request object that will be sent. + * @param {string} [request.content] + * The content that will be split into words and returned on the stream. + * @param {Object} [request.error] + * The error that is thrown after all words are sent on the stream. + * + * This object should have the same structure as [Status]{@link google.rpc.Status} + * @param {Object} [options] + * Optional parameters. You can override the default settings for this call, e.g, timeout, + * retries, paginations, etc. See [gax.CallOptions]{@link https://googleapis.github.io/gax-nodejs/global.html#CallOptions} for the details. + * @returns {Stream} + * An object stream which emits [EchoResponse]{@link google.showcase.v1beta1.EchoResponse} on 'data' event. + * + * @example + * + * const showcase = require('showcase.v1beta1'); + * + * const client = new showcase.v1beta1.EchoClient({ + * // optional auth parameters. + * }); + * + * + * client.expand({}).on('data', response => { + * // doThingsWith(response) + * }); + */ + expand(request, options) { + options = options || {}; + + return this._innerApiCalls.expand(request, options); + } + + /** + * This is similar to the Expand method but instead of returning a stream of + * expanded words, this method returns a paged list of expanded words. + * + * @param {Object} request + * The request object that will be sent. + * @param {string} [request.content] + * The string to expand. + * @param {number} [request.pageSize] + * The maximum number of resources contained in the underlying API + * response. If page streaming is performed per-resource, this + * parameter does not affect the return value. If page streaming is + * performed per-page, this determines the maximum number of + * resources in a page. + * @param {Object} [options] + * Optional parameters. You can override the default settings for this call, e.g, timeout, + * retries, paginations, etc. See [gax.CallOptions]{@link https://googleapis.github.io/gax-nodejs/global.html#CallOptions} for the details. + * @param {function(?Error, ?Array, ?Object, ?Object)} [callback] + * The function which will be called with the result of the API call. + * + * The second parameter to the callback is Array of [EchoResponse]{@link google.showcase.v1beta1.EchoResponse}. + * + * When autoPaginate: false is specified through options, it contains the result + * in a single response. If the response indicates the next page exists, the third + * parameter is set to be used for the next request object. The fourth parameter keeps + * the raw response object of an object representing [PagedExpandResponse]{@link google.showcase.v1beta1.PagedExpandResponse}. + * @returns {Promise} - The promise which resolves to an array. + * The first element of the array is Array of [EchoResponse]{@link google.showcase.v1beta1.EchoResponse}. + * + * When autoPaginate: false is specified through options, the array has three elements. + * The first element is Array of [EchoResponse]{@link google.showcase.v1beta1.EchoResponse} in a single response. + * The second element is the next request object if the response + * indicates the next page exists, or null. The third element is + * an object representing [PagedExpandResponse]{@link google.showcase.v1beta1.PagedExpandResponse}. + * + * The promise has a method named "cancel" which cancels the ongoing API call. + * + * @example + * + * const showcase = require('showcase.v1beta1'); + * + * const client = new showcase.v1beta1.EchoClient({ + * // optional auth parameters. + * }); + * + * // Iterate over all elements. + * client.pagedExpand({}) + * .then(responses => { + * const resources = responses[0]; + * for (const resource of resources) { + * // doThingsWith(resource) + * } + * }) + * .catch(err => { + * console.error(err); + * }); + * + * // Or obtain the paged response. + * + * const options = {autoPaginate: false}; + * const callback = responses => { + * // The actual resources in a response. + * const resources = responses[0]; + * // The next request if the response shows that there are more responses. + * const nextRequest = responses[1]; + * // The actual response object, if necessary. + * // const rawResponse = responses[2]; + * for (const resource of resources) { + * // doThingsWith(resource); + * } + * if (nextRequest) { + * // Fetch the next page. + * return client.pagedExpand(nextRequest, options).then(callback); + * } + * } + * client.pagedExpand({}, options) + * .then(callback) + * .catch(err => { + * console.error(err); + * }); + */ + pagedExpand(request, options, callback) { + if (options instanceof Function && callback === undefined) { + callback = options; + options = {}; + } + options = options || {}; + + return this._innerApiCalls.pagedExpand(request, options, callback); + } + + /** + * Equivalent to {@link pagedExpand}, but returns a NodeJS Stream object. + * + * This fetches the paged responses for {@link pagedExpand} continuously + * and invokes the callback registered for 'data' event for each element in the + * responses. + * + * The returned object has 'end' method when no more elements are required. + * + * autoPaginate option will be ignored. + * + * @see {@link https://nodejs.org/api/stream.html} + * + * @param {Object} request + * The request object that will be sent. + * @param {string} [request.content] + * The string to expand. + * @param {number} [request.pageSize] + * The maximum number of resources contained in the underlying API + * response. If page streaming is performed per-resource, this + * parameter does not affect the return value. If page streaming is + * performed per-page, this determines the maximum number of + * resources in a page. + * @param {Object} [options] + * Optional parameters. You can override the default settings for this call, e.g, timeout, + * retries, paginations, etc. See [gax.CallOptions]{@link https://googleapis.github.io/gax-nodejs/global.html#CallOptions} for the details. + * @returns {Stream} + * An object stream which emits an object representing [EchoResponse]{@link google.showcase.v1beta1.EchoResponse} on 'data' event. + * + * @example + * + * const showcase = require('showcase.v1beta1'); + * + * const client = new showcase.v1beta1.EchoClient({ + * // optional auth parameters. + * }); + * + * + * client.pagedExpandStream({}) + * .on('data', element => { + * // doThingsWith(element) + * }).on('error', err => { + * console.log(err); + * }); + */ + pagedExpandStream(request, options) { + options = options || {}; + + return this._descriptors.page.pagedExpand.createStream( + this._innerApiCalls.pagedExpand, + request, + options + ); + } + + /** + * This method will collect the words given to it. When the stream is closed + * by the client, this method will return the a concatenation of the strings + * passed to it. This method showcases client-side streaming rpcs. + * + * @param {Object} [options] + * Optional parameters. You can override the default settings for this call, e.g, timeout, + * retries, paginations, etc. See [gax.CallOptions]{@link https://googleapis.github.io/gax-nodejs/global.html#CallOptions} for the details. + * @param {function(?Error, ?Object)} [callback] + * The function which will be called with the result of the API call. + * + * The second parameter to the callback is an object representing [EchoResponse]{@link google.showcase.v1beta1.EchoResponse}. + * @returns {Stream} - A writable stream which accepts objects representing + * [EchoRequest]{@link google.showcase.v1beta1.EchoRequest} for write() method. + * + * @example + * + * const showcase = require('showcase.v1beta1'); + * + * const client = new showcase.v1beta1.EchoClient({ + * // optional auth parameters. + * }); + * + * const stream = client.collect((err, response) => { + * if (err) { + * console.error(err); + * return; + * } + * // doThingsWith(response) + * }); + * const request = {}; + * // Write request objects. + * stream.write(request); + */ + collect(options, callback) { + if (options instanceof Function && callback === undefined) { + callback = options; + options = {}; + } + options = options || {}; + + return this._innerApiCalls.collect(null, options, callback); + } + + /** + * This method, upon receiving a request on the stream, the same content will + * be passed back on the stream. This method showcases bidirectional + * streaming rpcs. + * + * @param {Object} [options] + * Optional parameters. You can override the default settings for this call, e.g, timeout, + * retries, paginations, etc. See [gax.CallOptions]{@link https://googleapis.github.io/gax-nodejs/global.html#CallOptions} for the details. + * @returns {Stream} + * An object stream which is both readable and writable. It accepts objects + * representing [EchoRequest]{@link google.showcase.v1beta1.EchoRequest} for write() method, and + * will emit objects representing [EchoResponse]{@link google.showcase.v1beta1.EchoResponse} on 'data' event asynchronously. + * + * @example + * + * const showcase = require('showcase.v1beta1'); + * + * const client = new showcase.v1beta1.EchoClient({ + * // optional auth parameters. + * }); + * + * const stream = client.chat().on('data', response => { + * // doThingsWith(response) + * }); + * const request = {}; + * // Write request objects. + * stream.write(request); + */ + chat(options) { + options = options || {}; + + return this._innerApiCalls.chat(options); + } + + /** + * This method will wait the requested amount of and then return. + * This method showcases how a client handles a request timing out. + * + * @param {Object} request + * The request object that will be sent. + * @param {Object} [request.endTime] + * The time that this operation will complete. + * + * This object should have the same structure as [Timestamp]{@link google.protobuf.Timestamp} + * @param {Object} [request.ttl] + * The duration of this operation. + * + * This object should have the same structure as [Duration]{@link google.protobuf.Duration} + * @param {Object} [request.error] + * The error that will be returned by the server. If this code is specified + * to be the OK rpc code, an empty response will be returned. + * + * This object should have the same structure as [Status]{@link google.rpc.Status} + * @param {Object} [request.success] + * The response to be returned on operation completion. + * + * This object should have the same structure as [WaitResponse]{@link google.showcase.v1beta1.WaitResponse} + * @param {Object} [options] + * Optional parameters. You can override the default settings for this call, e.g, timeout, + * retries, paginations, etc. See [gax.CallOptions]{@link https://googleapis.github.io/gax-nodejs/global.html#CallOptions} for the details. + * @param {function(?Error, ?Object)} [callback] + * The function which will be called with the result of the API call. + * + * The second parameter to the callback is a [gax.Operation]{@link https://googleapis.github.io/gax-nodejs/Operation} object. + * @returns {Promise} - The promise which resolves to an array. + * The first element of the array is a [gax.Operation]{@link https://googleapis.github.io/gax-nodejs/Operation} object. + * The promise has a method named "cancel" which cancels the ongoing API call. + * + * @example + * + * const showcase = require('showcase.v1beta1'); + * + * const client = new showcase.v1beta1.EchoClient({ + * // optional auth parameters. + * }); + * + * + * + * // Handle the operation using the promise pattern. + * client.wait({}) + * .then(responses => { + * const [operation, initialApiResponse] = responses; + * + * // Operation#promise starts polling for the completion of the LRO. + * return operation.promise(); + * }) + * .then(responses => { + * const result = responses[0]; + * const metadata = responses[1]; + * const finalApiResponse = responses[2]; + * }) + * .catch(err => { + * console.error(err); + * }); + * + * + * + * // Handle the operation using the event emitter pattern. + * client.wait({}) + * .then(responses => { + * const [operation, initialApiResponse] = responses; + * + * // Adding a listener for the "complete" event starts polling for the + * // completion of the operation. + * operation.on('complete', (result, metadata, finalApiResponse) => { + * // doSomethingWith(result); + * }); + * + * // Adding a listener for the "progress" event causes the callback to be + * // called on any change in metadata when the operation is polled. + * operation.on('progress', (metadata, apiResponse) => { + * // doSomethingWith(metadata) + * }); + * + * // Adding a listener for the "error" event handles any errors found during polling. + * operation.on('error', err => { + * // throw(err); + * }); + * }) + * .catch(err => { + * console.error(err); + * }); + * + * + * + * // Handle the operation using the await pattern. + * const [operation] = await client.wait({}); + * + * const [response] = await operation.promise(); + */ + wait(request, options, callback) { + if (options instanceof Function && callback === undefined) { + callback = options; + options = {}; + } + options = options || {}; + + return this._innerApiCalls.wait(request, options, callback); + } +} + +module.exports = EchoClient; diff --git a/system-test/fixtures/google-gax-packaging-test-app/src/v1beta1/echo_client_config.json b/system-test/fixtures/google-gax-packaging-test-app/src/v1beta1/echo_client_config.json new file mode 100644 index 000000000..905281613 --- /dev/null +++ b/system-test/fixtures/google-gax-packaging-test-app/src/v1beta1/echo_client_config.json @@ -0,0 +1,56 @@ +{ + "interfaces": { + "google.showcase.v1beta1.Echo": { + "retry_codes": { + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [] + }, + "retry_params": { + "default": { + "initial_retry_delay_millis": 100, + "retry_delay_multiplier": 1.3, + "max_retry_delay_millis": 60000, + "initial_rpc_timeout_millis": 20000, + "rpc_timeout_multiplier": 1.0, + "max_rpc_timeout_millis": 20000, + "total_timeout_millis": 600000 + } + }, + "methods": { + "Echo": { + "timeout_millis": 60000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "Expand": { + "timeout_millis": 60000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "PagedExpand": { + "timeout_millis": 60000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "Collect": { + "timeout_millis": 60000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "Chat": { + "timeout_millis": 60000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "Wait": { + "timeout_millis": 60000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + } + } + } + } +} diff --git a/system-test/fixtures/google-gax-packaging-test-app/src/v1beta1/index.js b/system-test/fixtures/google-gax-packaging-test-app/src/v1beta1/index.js new file mode 100644 index 000000000..aeceecb1b --- /dev/null +++ b/system-test/fixtures/google-gax-packaging-test-app/src/v1beta1/index.js @@ -0,0 +1,19 @@ +// Copyright 2019 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. + +'use strict'; + +const EchoClient = require('./echo_client'); + +module.exports.EchoClient = EchoClient; diff --git a/system-test/fixtures/google-gax-packaging-test-app/test/.eslintrc.yml b/system-test/fixtures/google-gax-packaging-test-app/test/.eslintrc.yml new file mode 100644 index 000000000..73f7bbc94 --- /dev/null +++ b/system-test/fixtures/google-gax-packaging-test-app/test/.eslintrc.yml @@ -0,0 +1,5 @@ +--- +env: + mocha: true +rules: + node/no-unpublished-require: off diff --git a/system-test/fixtures/google-gax-packaging-test-app/test/gapic-v1beta1.js b/system-test/fixtures/google-gax-packaging-test-app/test/gapic-v1beta1.js new file mode 100644 index 000000000..8a356c9d7 --- /dev/null +++ b/system-test/fixtures/google-gax-packaging-test-app/test/gapic-v1beta1.js @@ -0,0 +1,409 @@ +// Copyright 2019 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. + +'use strict'; + +const assert = require('assert'); +const through2 = require('through2'); + +const showcaseModule = require('../src'); + +const FAKE_STATUS_CODE = 1; +const error = new Error(); +error.code = FAKE_STATUS_CODE; + +describe('EchoClient', () => { + describe('echo', () => { + it('invokes echo without error', done => { + const client = new showcaseModule.v1beta1.EchoClient({ + credentials: {client_email: 'bogus', private_key: 'bogus'}, + projectId: 'bogus', + }); + + // Mock request + const request = {}; + + // Mock response + const content = 'content951530617'; + const expectedResponse = { + content: content, + }; + + // Mock Grpc layer + client._innerApiCalls.echo = mockSimpleGrpcMethod( + request, + expectedResponse + ); + + client.echo(request, (err, response) => { + assert.ifError(err); + assert.deepStrictEqual(response, expectedResponse); + done(); + }); + }); + + it('invokes echo with error', done => { + const client = new showcaseModule.v1beta1.EchoClient({ + credentials: {client_email: 'bogus', private_key: 'bogus'}, + projectId: 'bogus', + }); + + // Mock request + const request = {}; + + // Mock Grpc layer + client._innerApiCalls.echo = mockSimpleGrpcMethod(request, null, error); + + client.echo(request, (err, response) => { + assert(err instanceof Error); + assert.strictEqual(err.code, FAKE_STATUS_CODE); + assert(typeof response === 'undefined'); + done(); + }); + }); + }); + + describe('expand', () => { + it('invokes expand without error', done => { + const client = new showcaseModule.v1beta1.EchoClient({ + credentials: {client_email: 'bogus', private_key: 'bogus'}, + projectId: 'bogus', + }); + + // Mock request + const request = {}; + + // Mock response + const content = 'content951530617'; + const expectedResponse = { + content: content, + }; + + // Mock Grpc layer + client._innerApiCalls.expand = mockServerStreamingGrpcMethod( + request, + expectedResponse + ); + + const stream = client.expand(request); + stream.on('data', response => { + assert.deepStrictEqual(response, expectedResponse); + done(); + }); + stream.on('error', err => { + done(err); + }); + + stream.write(); + }); + + it('invokes expand with error', done => { + const client = new showcaseModule.v1beta1.EchoClient({ + credentials: {client_email: 'bogus', private_key: 'bogus'}, + projectId: 'bogus', + }); + + // Mock request + const request = {}; + + // Mock Grpc layer + client._innerApiCalls.expand = mockServerStreamingGrpcMethod( + request, + null, + error + ); + + const stream = client.expand(request); + stream.on('data', () => { + assert.fail(); + }); + stream.on('error', err => { + assert(err instanceof Error); + assert.strictEqual(err.code, FAKE_STATUS_CODE); + done(); + }); + + stream.write(); + }); + }); + + describe('pagedExpand', () => { + it('invokes pagedExpand without error', done => { + const client = new showcaseModule.v1beta1.EchoClient({ + credentials: {client_email: 'bogus', private_key: 'bogus'}, + projectId: 'bogus', + }); + + // Mock request + const request = {}; + + // Mock response + const nextPageToken = ''; + const responsesElement = {}; + const responses = [responsesElement]; + const expectedResponse = { + nextPageToken: nextPageToken, + responses: responses, + }; + + // Mock Grpc layer + client._innerApiCalls.pagedExpand = ( + actualRequest, + options, + callback + ) => { + assert.deepStrictEqual(actualRequest, request); + callback(null, expectedResponse.responses); + }; + + client.pagedExpand(request, (err, response) => { + assert.ifError(err); + assert.deepStrictEqual(response, expectedResponse.responses); + done(); + }); + }); + + it('invokes pagedExpand with error', done => { + const client = new showcaseModule.v1beta1.EchoClient({ + credentials: {client_email: 'bogus', private_key: 'bogus'}, + projectId: 'bogus', + }); + + // Mock request + const request = {}; + + // Mock Grpc layer + client._innerApiCalls.pagedExpand = mockSimpleGrpcMethod( + request, + null, + error + ); + + client.pagedExpand(request, (err, response) => { + assert(err instanceof Error); + assert.strictEqual(err.code, FAKE_STATUS_CODE); + assert(typeof response === 'undefined'); + done(); + }); + }); + }); + + describe('chat', () => { + it('invokes chat without error', done => { + const client = new showcaseModule.v1beta1.EchoClient({ + credentials: {client_email: 'bogus', private_key: 'bogus'}, + projectId: 'bogus', + }); + + // Mock request + const request = {}; + + // Mock response + const content = 'content951530617'; + const expectedResponse = { + content: content, + }; + + // Mock Grpc layer + client._innerApiCalls.chat = mockBidiStreamingGrpcMethod( + request, + expectedResponse + ); + + const stream = client + .chat() + .on('data', response => { + assert.deepStrictEqual(response, expectedResponse); + done(); + }) + .on('error', err => { + done(err); + }); + + stream.write(request); + }); + + it('invokes chat with error', done => { + const client = new showcaseModule.v1beta1.EchoClient({ + credentials: {client_email: 'bogus', private_key: 'bogus'}, + projectId: 'bogus', + }); + + // Mock request + const request = {}; + + // Mock Grpc layer + client._innerApiCalls.chat = mockBidiStreamingGrpcMethod( + request, + null, + error + ); + + const stream = client + .chat() + .on('data', () => { + assert.fail(); + }) + .on('error', err => { + assert(err instanceof Error); + assert.strictEqual(err.code, FAKE_STATUS_CODE); + done(); + }); + + stream.write(request); + }); + }); + + describe('wait', function() { + it('invokes wait without error', done => { + const client = new showcaseModule.v1beta1.EchoClient({ + credentials: {client_email: 'bogus', private_key: 'bogus'}, + projectId: 'bogus', + }); + + // Mock request + const request = {}; + + // Mock response + const content = 'content951530617'; + const expectedResponse = { + content: content, + }; + + // Mock Grpc layer + client._innerApiCalls.wait = mockLongRunningGrpcMethod( + request, + expectedResponse + ); + + client + .wait(request) + .then(responses => { + const operation = responses[0]; + return operation.promise(); + }) + .then(responses => { + assert.deepStrictEqual(responses[0], expectedResponse); + done(); + }) + .catch(err => { + done(err); + }); + }); + + it('invokes wait with error', done => { + const client = new showcaseModule.v1beta1.EchoClient({ + credentials: {client_email: 'bogus', private_key: 'bogus'}, + projectId: 'bogus', + }); + + // Mock request + const request = {}; + + // Mock Grpc layer + client._innerApiCalls.wait = mockLongRunningGrpcMethod( + request, + null, + error + ); + + client + .wait(request) + .then(responses => { + const operation = responses[0]; + return operation.promise(); + }) + .then(() => { + assert.fail(); + }) + .catch(err => { + assert(err instanceof Error); + assert.strictEqual(err.code, FAKE_STATUS_CODE); + done(); + }); + }); + + it('has longrunning decoder functions', () => { + const client = new showcaseModule.v1beta1.EchoClient({ + credentials: {client_email: 'bogus', private_key: 'bogus'}, + projectId: 'bogus', + }); + assert( + client._descriptors.longrunning.wait.responseDecoder instanceof Function + ); + assert( + client._descriptors.longrunning.wait.metadataDecoder instanceof Function + ); + }); + }); +}); + +function mockSimpleGrpcMethod(expectedRequest, response, error) { + return function(actualRequest, options, callback) { + assert.deepStrictEqual(actualRequest, expectedRequest); + if (error) { + callback(error); + } else if (response) { + callback(null, response); + } else { + callback(null); + } + }; +} + +function mockServerStreamingGrpcMethod(expectedRequest, response, error) { + return actualRequest => { + assert.deepStrictEqual(actualRequest, expectedRequest); + const mockStream = through2.obj((chunk, enc, callback) => { + if (error) { + callback(error); + } else { + callback(null, response); + } + }); + return mockStream; + }; +} + +function mockBidiStreamingGrpcMethod(expectedRequest, response, error) { + return () => { + const mockStream = through2.obj((chunk, enc, callback) => { + assert.deepStrictEqual(chunk, expectedRequest); + if (error) { + callback(error); + } else { + callback(null, response); + } + }); + return mockStream; + }; +} + +function mockLongRunningGrpcMethod(expectedRequest, response, error) { + return request => { + assert.deepStrictEqual(request, expectedRequest); + const mockOperation = { + promise: function() { + return new Promise((resolve, reject) => { + if (error) { + reject(error); + } else { + resolve([response]); + } + }); + }, + }; + return Promise.resolve([mockOperation]); + }; +} diff --git a/system-test/system.ts b/system-test/test.clientlibs.ts similarity index 71% rename from system-test/system.ts rename to system-test/test.clientlibs.ts index 9c4c073db..64f6149be 100644 --- a/system-test/system.ts +++ b/system-test/test.clientlibs.ts @@ -29,8 +29,9 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -import * as cp from 'child_process'; +import * as execa from 'execa'; import * as fs from 'fs'; +import {ncp} from 'ncp'; import * as path from 'path'; import * as rimraf from 'rimraf'; import * as util from 'util'; @@ -41,50 +42,10 @@ const rmrf = util.promisify(rimraf); const baseRepoUrl = 'https://github.com/googleapis/'; const testDir = path.join(process.cwd(), '.system-test-run'); -interface ExecuteResult { - stdout: string; - stderr: string; -} - -async function execute(command: string, cwd?: string): Promise { - cwd = cwd || process.cwd(); - const maxBuffer = 10 * 1024 * 1024; - console.log(`Execute: ${command} [cwd: ${cwd}]`); - return new Promise((resolve, reject) => { - cp.exec(command, {cwd, maxBuffer}, (err, stdout, stderr) => { - if (err) { - reject(new Error(`Command ${command} terminated with error ${err}`)); - } else { - resolve({stdout, stderr}); - } - }); - }); -} - -async function spawn( - command: string, args?: string[], cwd?: string): Promise { - cwd = cwd || process.cwd(); - args = args || []; - console.log(`Execute: ${command} ${args.join(' ')} [cwd: ${cwd}]`); - return new Promise((resolve, reject) => { - const child = cp.spawn(command, args || [], { - cwd, - stdio: 'inherit' - }).on('close', (code: number|null, signal: string|null) => { - if (code === 0) { - resolve(); - } else { - reject(new Error(`Command ${command} terminated with code ${ - code}, signal ${signal}`)); - } - }); - }); -} - async function latestRelease(cwd: string): Promise { - const gitTagOutput = (await execute('git tag --list', cwd)).stdout; + const {stdout} = await execa('git', ['tag', '--list'], {cwd}); const tags = - gitTagOutput.split('\n') + stdout.split('\n') .filter(str => str.match(/^v\d+\.\d+\.\d+$/)) .sort((tag1: string, tag2: string): number => { const match1 = tag1.match(/^v(\d+)\.(\d+)\.(\d+)$/); @@ -106,17 +67,19 @@ async function latestRelease(cwd: string): Promise { } async function preparePackage(packageName: string): Promise { - await spawn( - 'git', ['clone', `${baseRepoUrl}${packageName}.git`, packageName]); + await execa( + 'git', ['clone', `${baseRepoUrl}${packageName}.git`, packageName], + {stdio: 'inherit'}); const tag = await latestRelease(packageName); - await spawn('git', ['checkout', tag], packageName); - await spawn('npm', ['link', '../../'], packageName); - await spawn('npm', ['install'], packageName); - await spawn('npm', ['link', '../../'], packageName); + await execa('git', ['checkout', tag], {cwd: packageName, stdio: 'inherit'}); + await execa('npm', ['link', '../../'], {cwd: packageName, stdio: 'inherit'}); + await execa('npm', ['install'], {cwd: packageName, stdio: 'inherit'}); + await execa('npm', ['link', '../../'], {cwd: packageName, stdio: 'inherit'}); } async function runSystemTest(packageName: string): Promise { - await spawn('npm', ['run', 'system-test'], packageName); + await execa( + 'npm', ['run', 'system-test'], {cwd: packageName, stdio: 'inherit'}); } describe('Run system tests for some libraries', () => { diff --git a/system-test/test.endtoend.ts b/system-test/test.endtoend.ts new file mode 100644 index 000000000..33e2a0555 --- /dev/null +++ b/system-test/test.endtoend.ts @@ -0,0 +1,112 @@ +/* + * Copyright 2019 Google LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import * as execa from 'execa'; +import * as fs from 'fs'; +import {ncp} from 'ncp'; +import * as path from 'path'; +import * as rimraf from 'rimraf'; +import * as util from 'util'; + +const mkdir = util.promisify(fs.mkdir); +const rmrf = util.promisify(rimraf); +const ncpp = util.promisify(ncp); + +// gapic-showcase contains sample gRPC server written in TypeScript that we will +// use in this test +const showcaseRepoUrl = 'https://github.com/googleapis/gapic-showcase'; +const testDir = path.join(process.cwd(), '.kitchen-sink'); +const gaxDir = path.resolve(__dirname, '..', '..'); +const fixturesDir = path.join(gaxDir, 'system-test', 'fixtures'); + +// We will pack google-gax using `npm pack`, defining some constants to make it +// easier to consume that tarball +const pkg = require('../../package.json'); +const gaxTarball = path.join(gaxDir, `${pkg.name}-${pkg.version}.tgz`); + +// We have a test gRPC client in `system-test/fixtures` that we'll copy to a +// temporary folder and use it from there +const testAppName = 'google-gax-packaging-test-app'; +const testAppSource = path.join(fixturesDir, testAppName); +const testAppDestination = path.join(testDir, testAppName); + +describe('Run end-to-end test', () => { + let grpcServer: execa.ExecaChildProcess; + before(async () => { + console.log('Packing google-gax...'); + await execa('npm', ['pack'], {cwd: gaxDir, stdio: 'inherit'}); + + if (!fs.existsSync(gaxTarball)) { + throw new Error(`npm pack tarball ${gaxTarball} does not exist`); + } + await rmrf(testDir); + await mkdir(testDir); + process.chdir(testDir); + console.log(`Running tests in ${testDir}.`); + + const showcasePath = path.join(testDir, 'gapic-showcase'); + const grpcServerPath = path.join(showcasePath, 'nodejs-server'); + await execa( + 'git', ['clone', showcaseRepoUrl, 'gapic-showcase'], + {cwd: testDir, stdio: 'inherit'}); + await execa('npm', ['install'], {cwd: grpcServerPath, stdio: 'inherit'}); + grpcServer = execa( + 'node', ['build/src/index.js'], + {cwd: grpcServerPath, stdio: 'inherit'}); + console.log('gRPC server is started.'); + grpcServer.then( + () => { + throw new Error('gRPC server is not supposed to exit normally!'); + }, + () => { + console.log('gRPC server is terminated.'); + }); + }); + + it('should be able to prepare test app', async () => { + await ncpp(testAppSource, testAppDestination); + await ncpp(gaxTarball, path.join(testAppDestination, 'google-gax.tgz')); + await execa( + 'npm', ['install'], {cwd: testAppDestination, stdio: 'inherit'}); + }); + + it('should be able to run unit tests of test app', async () => { + await execa('npm', ['test'], {cwd: testAppDestination, stdio: 'inherit'}); + }); + + it('should be able to run tests against gRPC server', async () => { + await execa('npm', ['start'], {cwd: testAppDestination, stdio: 'inherit'}); + }); + + after(async () => { + grpcServer.kill(); + }); +});