1# Copyright 2017 gRPC authors.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15require 'spec_helper'
16require_relative '../lib/grpc/google_rpc_status_utils'
17require_relative '../pb/src/proto/grpc/testing/messages_pb'
18require_relative '../pb/src/proto/grpc/testing/messages_pb'
19require 'google/protobuf/well_known_types'
20
21include GRPC::Core
22include GRPC::Spec::Helpers
23
24describe 'conversion from a status struct to a google protobuf status' do
25  it 'fails if the input is not a status struct' do
26    begin
27      GRPC::GoogleRpcStatusUtils.extract_google_rpc_status('string')
28    rescue => e
29      exception = e
30    end
31    expect(exception.is_a?(ArgumentError)).to be true
32    expect(exception.message.include?('bad type')).to be true
33  end
34
35  it 'returns nil if the header key is missing' do
36    status = Struct::Status.new(1, 'details', key: 'val')
37    expect(status.metadata.nil?).to be false
38    expect(GRPC::GoogleRpcStatusUtils.extract_google_rpc_status(
39             status)).to be(nil)
40  end
41
42  it 'fails with some error if the header key fails to deserialize' do
43    status = Struct::Status.new(1, 'details',
44                                'grpc-status-details-bin' => 'string_val')
45    expect do
46      GRPC::GoogleRpcStatusUtils.extract_google_rpc_status(status)
47    end.to raise_error(StandardError)
48  end
49
50  it 'silently ignores erroneous mismatch between messages in '\
51    'status struct and protobuf status' do
52    proto = Google::Rpc::Status.new(code: 1, message: 'proto message')
53    encoded_proto = Google::Rpc::Status.encode(proto)
54    status = Struct::Status.new(1, 'struct message',
55                                'grpc-status-details-bin' => encoded_proto)
56    rpc_status = GRPC::GoogleRpcStatusUtils.extract_google_rpc_status(status)
57    expect(rpc_status).to eq(proto)
58  end
59
60  it 'silently ignores erroneous mismatch between codes in status struct '\
61    'and protobuf status' do
62    proto = Google::Rpc::Status.new(code: 1, message: 'matching message')
63    encoded_proto = Google::Rpc::Status.encode(proto)
64    status = Struct::Status.new(2, 'matching message',
65                                'grpc-status-details-bin' => encoded_proto)
66    rpc_status = GRPC::GoogleRpcStatusUtils.extract_google_rpc_status(status)
67    expect(rpc_status).to eq(proto)
68  end
69
70  it 'can succesfully convert a status struct into a google protobuf status '\
71    'when there are no rpcstatus details' do
72    proto = Google::Rpc::Status.new(code: 1, message: 'matching message')
73    encoded_proto = Google::Rpc::Status.encode(proto)
74    status = Struct::Status.new(1, 'matching message',
75                                'grpc-status-details-bin' => encoded_proto)
76    out = GRPC::GoogleRpcStatusUtils.extract_google_rpc_status(status)
77    expect(out.code).to eq(1)
78    expect(out.message).to eq('matching message')
79    expect(out.details).to eq([])
80  end
81
82  it 'can succesfully convert a status struct into a google protobuf '\
83    'status when there are multiple rpcstatus details' do
84    simple_request_any = Google::Protobuf::Any.new
85    simple_request = Grpc::Testing::SimpleRequest.new(
86      payload: Grpc::Testing::Payload.new(body: 'request'))
87    simple_request_any.pack(simple_request)
88    simple_response_any = Google::Protobuf::Any.new
89    simple_response = Grpc::Testing::SimpleResponse.new(
90      payload: Grpc::Testing::Payload.new(body: 'response'))
91    simple_response_any.pack(simple_response)
92    payload_any = Google::Protobuf::Any.new
93    payload = Grpc::Testing::Payload.new(body: 'payload')
94    payload_any.pack(payload)
95    proto = Google::Rpc::Status.new(code: 1,
96                                    message: 'matching message',
97                                    details: [
98                                      simple_request_any,
99                                      simple_response_any,
100                                      payload_any
101                                    ])
102    encoded_proto = Google::Rpc::Status.encode(proto)
103    status = Struct::Status.new(1, 'matching message',
104                                'grpc-status-details-bin' => encoded_proto)
105    out = GRPC::GoogleRpcStatusUtils.extract_google_rpc_status(status)
106    expect(out.code).to eq(1)
107    expect(out.message).to eq('matching message')
108    expect(out.details[0].unpack(
109             Grpc::Testing::SimpleRequest)).to eq(simple_request)
110    expect(out.details[1].unpack(
111             Grpc::Testing::SimpleResponse)).to eq(simple_response)
112    expect(out.details[2].unpack(
113             Grpc::Testing::Payload)).to eq(payload)
114  end
115end
116
117# A test service that fills in the "reserved" grpc-status-details-bin trailer,
118# for client-side testing of GoogleRpcStatus protobuf extraction from trailers.
119class GoogleRpcStatusTestService
120  include GRPC::GenericService
121  rpc :an_rpc, EchoMsg, EchoMsg
122
123  def initialize(encoded_rpc_status)
124    @encoded_rpc_status = encoded_rpc_status
125  end
126
127  def an_rpc(_, _)
128    # TODO: create a server-side utility API for sending a google rpc status.
129    # Applications are not expected to set the grpc-status-details-bin
130    # ("grpc"-fixed and reserved for library use) manually.
131    # Doing so here is only for testing of the client-side api for extracting
132    # a google rpc status, which is useful
133    # when the interacting with a server that does fill in this trailer.
134    fail GRPC::Unknown.new('test message',
135                           'grpc-status-details-bin' => @encoded_rpc_status)
136  end
137end
138
139GoogleRpcStatusTestStub = GoogleRpcStatusTestService.rpc_stub_class
140
141describe 'receving a google rpc status from a remote endpoint' do
142  def start_server(encoded_rpc_status)
143    @srv = new_rpc_server_for_testing(pool_size: 1)
144    @server_port = @srv.add_http2_port('localhost:0',
145                                       :this_port_is_insecure)
146    @srv.handle(GoogleRpcStatusTestService.new(encoded_rpc_status))
147    @server_thd = Thread.new { @srv.run }
148    @srv.wait_till_running
149  end
150
151  def stop_server
152    expect(@srv.stopped?).to be(false)
153    @srv.stop
154    @server_thd.join
155    expect(@srv.stopped?).to be(true)
156  end
157
158  before(:each) do
159    simple_request_any = Google::Protobuf::Any.new
160    simple_request = Grpc::Testing::SimpleRequest.new(
161      payload: Grpc::Testing::Payload.new(body: 'request'))
162    simple_request_any.pack(simple_request)
163    simple_response_any = Google::Protobuf::Any.new
164    simple_response = Grpc::Testing::SimpleResponse.new(
165      payload: Grpc::Testing::Payload.new(body: 'response'))
166    simple_response_any.pack(simple_response)
167    payload_any = Google::Protobuf::Any.new
168    payload = Grpc::Testing::Payload.new(body: 'payload')
169    payload_any.pack(payload)
170    @expected_proto = Google::Rpc::Status.new(
171      code: StatusCodes::UNKNOWN,
172      message: 'test message',
173      details: [simple_request_any, simple_response_any, payload_any])
174    start_server(Google::Rpc::Status.encode(@expected_proto))
175  end
176
177  after(:each) do
178    stop_server
179  end
180
181  it 'should receive be able to extract a google rpc status from the '\
182    'status struct taken from a BadStatus exception' do
183    stub = GoogleRpcStatusTestStub.new("localhost:#{@server_port}",
184                                       :this_channel_is_insecure)
185    begin
186      stub.an_rpc(EchoMsg.new)
187    rescue GRPC::BadStatus => e
188      rpc_status = GRPC::GoogleRpcStatusUtils.extract_google_rpc_status(
189        e.to_status)
190    end
191    expect(rpc_status).to eq(@expected_proto)
192  end
193
194  it 'should receive be able to extract a google rpc status from the '\
195    'status struct taken from the op view of a call' do
196    stub = GoogleRpcStatusTestStub.new("localhost:#{@server_port}",
197                                       :this_channel_is_insecure)
198    op = stub.an_rpc(EchoMsg.new, return_op: true)
199    begin
200      op.execute
201    rescue GRPC::BadStatus => e
202      status_from_exception = e.to_status
203    end
204    rpc_status = GRPC::GoogleRpcStatusUtils.extract_google_rpc_status(
205      op.status)
206    expect(rpc_status).to eq(@expected_proto)
207    # "to_status" on the bad status should give the same result
208    # as "status" on the "op view".
209    expect(GRPC::GoogleRpcStatusUtils.extract_google_rpc_status(
210             status_from_exception)).to eq(rpc_status)
211  end
212end
213
214# A test service that fails without explicitly setting the
215# grpc-status-details-bin trailer. Tests assumptions about value
216# of grpc-status-details-bin on the client side when the trailer wasn't
217# set explicitly.
218class NoStatusDetailsBinTestService
219  include GRPC::GenericService
220  rpc :an_rpc, EchoMsg, EchoMsg
221
222  def an_rpc(_, _)
223    fail GRPC::Unknown
224  end
225end
226
227NoStatusDetailsBinTestServiceStub = NoStatusDetailsBinTestService.rpc_stub_class
228
229describe 'when the endpoint doesnt send grpc-status-details-bin' do
230  def start_server
231    @srv = new_rpc_server_for_testing(pool_size: 1)
232    @server_port = @srv.add_http2_port('localhost:0',
233                                       :this_port_is_insecure)
234    @srv.handle(NoStatusDetailsBinTestService)
235    @server_thd = Thread.new { @srv.run }
236    @srv.wait_till_running
237  end
238
239  def stop_server
240    expect(@srv.stopped?).to be(false)
241    @srv.stop
242    @server_thd.join
243    expect(@srv.stopped?).to be(true)
244  end
245
246  before(:each) do
247    start_server
248  end
249
250  after(:each) do
251    stop_server
252  end
253
254  it 'should receive nil when we extract try to extract a google '\
255    'rpc status from a BadStatus exception that didnt have it' do
256    stub = NoStatusDetailsBinTestServiceStub.new("localhost:#{@server_port}",
257                                                 :this_channel_is_insecure)
258    begin
259      stub.an_rpc(EchoMsg.new)
260    rescue GRPC::Unknown => e
261      rpc_status = GRPC::GoogleRpcStatusUtils.extract_google_rpc_status(
262        e.to_status)
263    end
264    expect(rpc_status).to be(nil)
265  end
266
267  it 'should receive nil when we extract try to extract a google '\
268    'rpc status from an op views status object that didnt have it' do
269    stub = NoStatusDetailsBinTestServiceStub.new("localhost:#{@server_port}",
270                                                 :this_channel_is_insecure)
271    op = stub.an_rpc(EchoMsg.new, return_op: true)
272    begin
273      op.execute
274    rescue GRPC::Unknown => e
275      status_from_exception = e.to_status
276    end
277    expect(GRPC::GoogleRpcStatusUtils.extract_google_rpc_status(
278             status_from_exception)).to be(nil)
279    expect(GRPC::GoogleRpcStatusUtils.extract_google_rpc_status(
280             op.status)).to be nil
281  end
282end
283