From 58825f7f1395f00ab9253332639fe5c67648d805 Mon Sep 17 00:00:00 2001 From: James S Date: Thu, 20 Feb 2020 17:28:16 -0800 Subject: [PATCH] fix: Integration test for NodeJS Typescript client (#17) * Revert to using a fresh rack app instance on every test * Working node client * Basic working integration spec, still need to add auth * Add node compilation to the rakefile * Update apparition gem to avoid warning during spec suite * Rubocop fixes * Rubocop (non auto) * Revert "Update apparition gem to avoid warning during spec suite" This reverts commit 12cf4e82dc149b755393053b5063b5d28ee2d5dd. * Add basic auth for node client * Remove unneeded ts-node dependency * ANY_CONTENT_TYPES -> UNSPECIFIED_CONTENT_TYPES * Learning --- .gitignore | 3 +- Rakefile | 38 +++++++++- lib/grpc_web/content_types.rb | 2 +- lib/grpc_web/server/grpc_request_processor.rb | 2 +- lib/grpc_web/server/rack_handler.rb | 2 +- .../ruby_server_nodejs_client_spec.rb | 73 +++++++++++++++++++ spec/node-client/client.ts | 34 +++++++++ spec/node-client/package.json | 19 +++++ spec/node-client/pb-ts | 1 + spec/node-client/tsconfig.json | 10 +++ spec/node-client/yarn.lock | 40 ++++++++++ spec/pb-ts/.gitkeep | 0 spec/support/test_grpc_web_app.rb | 5 +- 13 files changed, 222 insertions(+), 7 deletions(-) create mode 100644 spec/integration/ruby_server_nodejs_client_spec.rb create mode 100644 spec/node-client/client.ts create mode 100644 spec/node-client/package.json create mode 120000 spec/node-client/pb-ts create mode 100644 spec/node-client/tsconfig.json create mode 100644 spec/node-client/yarn.lock create mode 100644 spec/pb-ts/.gitkeep diff --git a/.gitignore b/.gitignore index 75fcb42..1b23ec9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ spec/pb-ruby/*.rb +spec/pb-ts/*.[j|t]s spec/pb-js-grpc-web/*.js spec/pb-js-grpc-web-text/*.js -spec/js-client-src/node_modules spec/js-client/main.js +spec/node-client/dist coverage node_modules diff --git a/Rakefile b/Rakefile index 07892c4..77920d5 100644 --- a/Rakefile +++ b/Rakefile @@ -8,7 +8,10 @@ RSpec::Core::RakeTask.new(:spec) CLEAN.include('spec/pb-ruby/*.rb') CLEAN.include('spec/pb-js-grpc-web/*.js') CLEAN.include('spec/pb-js-grpc-web-text/*.js') +CLEAN.include('spec/pb-ts/*.js') +CLEAN.include('spec/pb-ts/*.ts') CLEAN.include('spec/js-client/main.js') +CLEAN.include('spec/node-client/dist/*') module RakeHelpers def self.compile_protos_js_cmd(mode, output_dir) @@ -41,6 +44,23 @@ task :compile_protos_ruby do ].join(' ') end +task :compile_protos_ts do + defs_dir = File.expand_path('spec', __dir__) + proto_files = Dir[File.join(defs_dir, 'pb-src/**/*.proto')] + proto_input_files = proto_files.map { |f| f.gsub(defs_dir, '/defs') } + sh [ + 'docker run', + "-v \"#{defs_dir}:/defs\"", + '--entrypoint protoc', + 'namely/protoc-all', + '--plugin=protoc-gen-ts=/usr/bin/protoc-gen-ts', + '--js_out=import_style=commonjs,binary:/defs/pb-ts', + '--ts_out=service=grpc-web:/defs/pb-ts', + '-I /defs/pb-src', + proto_input_files.join(' '), + ].join(' ') +end + task compile_js_client: [:compile_protos_js] do compile_js_cmd = '"cd spec/js-client-src; yarn install; yarn run webpack"' sh [ @@ -51,6 +71,16 @@ task compile_js_client: [:compile_protos_js] do ].join(' && ') end +task compile_node_client: [:compile_protos_ts] do + compile_node_cmd = '"cd spec/node-client; yarn install; yarn build"' + sh [ + 'docker-compose down', + 'docker-compose build', + "docker-compose run --use-aliases ruby #{compile_node_cmd}", + 'docker-compose down', + ].join(' && ') +end + task :run_specs_in_docker do sh [ 'docker-compose down', @@ -60,4 +90,10 @@ task :run_specs_in_docker do ].join(' && ') end -task default: %i[clean compile_protos_ruby compile_js_client run_specs_in_docker] +task default: %i[ + clean + compile_protos_ruby + compile_js_client + compile_node_client + run_specs_in_docker +] diff --git a/lib/grpc_web/content_types.rb b/lib/grpc_web/content_types.rb index b6d107a..183b075 100644 --- a/lib/grpc_web/content_types.rb +++ b/lib/grpc_web/content_types.rb @@ -15,5 +15,5 @@ module GRPCWeb::ContentTypes TEXT_CONTENT_TYPE, TEXT_PROTO_CONTENT_TYPE, ].freeze - ANY_CONTENT_TYPES = ['*/*', ''].freeze + UNSPECIFIED_CONTENT_TYPES = ['*/*', '', nil].freeze end diff --git a/lib/grpc_web/server/grpc_request_processor.rb b/lib/grpc_web/server/grpc_request_processor.rb index 649cca4..0ff16b2 100644 --- a/lib/grpc_web/server/grpc_request_processor.rb +++ b/lib/grpc_web/server/grpc_request_processor.rb @@ -42,7 +42,7 @@ def execute_request(request) # Use Accept header value if specified, otherwise use request content type def response_content_type(request) - if request.accept.nil? || ANY_CONTENT_TYPES.include?(request.accept) + if UNSPECIFIED_CONTENT_TYPES.include?(request.accept) request.content_type else request.accept diff --git a/lib/grpc_web/server/rack_handler.rb b/lib/grpc_web/server/rack_handler.rb index e3443e2..818019a 100644 --- a/lib/grpc_web/server/rack_handler.rb +++ b/lib/grpc_web/server/rack_handler.rb @@ -43,7 +43,7 @@ def valid_content_types?(rack_request) return false unless ALL_CONTENT_TYPES.include?(rack_request.content_type) accept = rack_request.get_header(ACCEPT_HEADER) - return true if ANY_CONTENT_TYPES.include?(accept) + return true if UNSPECIFIED_CONTENT_TYPES.include?(accept) ALL_CONTENT_TYPES.include?(accept) end diff --git a/spec/integration/ruby_server_nodejs_client_spec.rb b/spec/integration/ruby_server_nodejs_client_spec.rb new file mode 100644 index 0000000..af257d9 --- /dev/null +++ b/spec/integration/ruby_server_nodejs_client_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'json' +require 'hello_services_pb' + +RSpec.describe 'connecting to a ruby server from a nodejs client', type: :feature do + subject(:json_result) { `#{node_cmd}` } + + let(:node_client) { File.expand_path('../node-client/dist/client.js', __dir__) } + let(:node_cmd) do + [ + 'node', + node_client, + server_url, + name, + basic_username, + basic_password, + ].join(' ') + end + let(:result) { JSON.parse(json_result) } + + let(:basic_password) { 'supersecretpassword' } + let(:basic_username) { 'supermanuser' } + let(:service) { TestHelloService } + let(:rack_app) do + app = TestGRPCWebApp.build(service) + app.use Rack::Auth::Basic do |username, password| + [username, password] == [basic_username, basic_password] + end + app + end + + let(:browser) { Capybara::Session.new(Capybara.default_driver, rack_app) } + let(:server) { browser.server } + let(:server_url) { "http://#{server.host}:#{server.port}" } + let(:name) { "James\u1f61d" } + + it 'returns the expected response from the service' do + expect(result['response']).to eq('message' => "Hello #{name}") + end + + context 'with a service that raises a standard gRPC error' do + let(:service) do + Class.new(TestHelloService) do + def say_hello(_request, _metadata = nil) + raise ::GRPC::InvalidArgument, 'Test message' + end + end + end + + it 'raises an error' do + expect(result['error']).to include('grpc-message' => ['Test message'], 'grpc-status' => ['3']) + end + end + + context 'with a service that raises a custom error' do + let(:service) do + Class.new(TestHelloService) do + def say_hello(_request, _metadata = nil) + raise 'Some random error' + end + end + end + + it 'raises an error' do + expect(result['error']).to include( + 'grpc-message' => ['RuntimeError: Some random error'], + 'grpc-status' => ['2'], + ) + end + end +end diff --git a/spec/node-client/client.ts b/spec/node-client/client.ts new file mode 100644 index 0000000..69f41c4 --- /dev/null +++ b/spec/node-client/client.ts @@ -0,0 +1,34 @@ +import { grpc } from "@improbable-eng/grpc-web"; +import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport"; + +import {HelloServiceClient} from './pb-ts/hello_pb_service'; +import {HelloRequest} from './pb-ts/hello_pb'; + +// Required for grpc-web in a NodeJS environment (vs. browser) +grpc.setDefaultTransport(NodeHttpTransport()); + +// Usage: node client.js http://server:port nameParam username password +const serverUrl = process.argv[2]; +const helloName = process.argv[3]; +const username = process.argv[4]; +const password = process.argv[5]; + +const client = new HelloServiceClient(serverUrl); +const headers = new grpc.Metadata(); + +if (username && password) { + const encodedCredentials = Buffer.from(`${username}:${password}`).toString("base64"); + headers.set("Authorization", `Basic ${encodedCredentials}`); +} + +const req = new HelloRequest(); +req.setName(helloName); + +client.sayHello(req, headers, (err, resp) => { + var result = { + response: resp && resp.toObject(), + error: err && err.metadata && err.metadata.headersMap + } + // Emit response and/or error as JSON so it can be parsed from Ruby + console.log(JSON.stringify(result)); +}); diff --git a/spec/node-client/package.json b/spec/node-client/package.json new file mode 100644 index 0000000..6f0b97a --- /dev/null +++ b/spec/node-client/package.json @@ -0,0 +1,19 @@ +{ + "name": "grpc-web-node-ts-client", + "version": "0.1.0", + "description": "gRPC-Web Node Typescript client example", + "license": "Apache-2.0", + "scripts": { + "build": "tsc --pretty" + }, + "dependencies": { + "@improbable-eng/grpc-web": "0.12.0", + "@improbable-eng/grpc-web-node-http-transport": "0.12.0", + "@types/google-protobuf": "^3.7.2", + "@types/node": "^13.7.4", + "google-protobuf": "^3.8.0" + }, + "devDependencies": { + "typescript": "^3.7.2" + } +} diff --git a/spec/node-client/pb-ts b/spec/node-client/pb-ts new file mode 120000 index 0000000..5db8608 --- /dev/null +++ b/spec/node-client/pb-ts @@ -0,0 +1 @@ +../pb-ts/ \ No newline at end of file diff --git a/spec/node-client/tsconfig.json b/spec/node-client/tsconfig.json new file mode 100644 index 0000000..8664926 --- /dev/null +++ b/spec/node-client/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + "outDir": "dist", /* Redirect output structure to the directory. */ + "strict": true, /* Enable all strict type-checking options. */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + "allowJs": true, /* Allow javascript files to be compiled. */ + } +} diff --git a/spec/node-client/yarn.lock b/spec/node-client/yarn.lock new file mode 100644 index 0000000..173fae1 --- /dev/null +++ b/spec/node-client/yarn.lock @@ -0,0 +1,40 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@improbable-eng/grpc-web-node-http-transport@0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@improbable-eng/grpc-web-node-http-transport/-/grpc-web-node-http-transport-0.12.0.tgz#827138160d2f945620e103473042025529c00c8e" + integrity sha512-+Kjz+Dktfz5LKTZA9ZW/Vlww6HF9KaKz4x2mVe1O8CJdOP2WfzC+KY8L6EWMqVLrV4MvdBuQdSgDmvSJz+OGuA== + +"@improbable-eng/grpc-web@0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@improbable-eng/grpc-web/-/grpc-web-0.12.0.tgz#9b10a7edf2a1d7672f8997e34a60e7b70e49738f" + integrity sha512-uJjgMPngreRTYPBuo6gswMj1gK39Wbqre/RgE0XnSDXJRg6ST7ZhuS53dFE6Vc2CX4jxgl+cO+0B3op8LA4Q0Q== + dependencies: + browser-headers "^0.4.0" + +"@types/google-protobuf@^3.7.2": + version "3.7.2" + resolved "https://registry.yarnpkg.com/@types/google-protobuf/-/google-protobuf-3.7.2.tgz#cd8a360c193ce4d672575a20a79f49ba036d38d2" + integrity sha512-ifFemzjNchFBCtHS6bZNhSZCBu7tbtOe0e8qY0z2J4HtFXmPJjm6fXSaQsTG7yhShBEZtt2oP/bkwu5k+emlkQ== + +"@types/node@^13.7.4": + version "13.7.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.4.tgz#76c3cb3a12909510f52e5dc04a6298cdf9504ffd" + integrity sha512-oVeL12C6gQS/GAExndigSaLxTrKpQPxewx9bOcwfvJiJge4rr7wNaph4J+ns5hrmIV2as5qxqN8YKthn9qh0jw== + +browser-headers@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/browser-headers/-/browser-headers-0.4.1.tgz#4308a7ad3b240f4203dbb45acedb38dc2d65dd02" + integrity sha512-CA9hsySZVo9371qEHjHZtYxV2cFtVj5Wj/ZHi8ooEsrtm4vOnl9Y9HmyYWk9q+05d7K3rdoAE0j3MVEFVvtQtg== + +google-protobuf@^3.8.0: + version "3.11.4" + resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.11.4.tgz#598ca405a3cfa917a2132994d008b5932ef42014" + integrity sha512-lL6b04rDirurUBOgsY2+LalI6Evq8eH5TcNzi7TYQ3BsIWelT0KSOQSBsXuavEkNf+odQU6c0lgz3UsZXeNX9Q== + +typescript@^3.7.2: + version "3.7.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae" + integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw== diff --git a/spec/pb-ts/.gitkeep b/spec/pb-ts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/spec/support/test_grpc_web_app.rb b/spec/support/test_grpc_web_app.rb index 2a6bddc..f9c44a3 100644 --- a/spec/support/test_grpc_web_app.rb +++ b/spec/support/test_grpc_web_app.rb @@ -9,7 +9,8 @@ # Used to build a Rack app hosting the HelloService for integration testing. module TestGRPCWebApp def self.build(service_class = TestHelloService) - GRPCWeb.handle(service_class) + grpc_app = GRPCWeb::RackApp.new + grpc_app.handle(service_class) Rack::Builder.new do use Rack::Cors do @@ -20,7 +21,7 @@ def self.build(service_class = TestHelloService) end use Rack::Lint - run GRPCWeb.rack_app + run grpc_app end end end