Skip to content

Commit

Permalink
chore: add tests for rest error wrapping (#27388)
Browse files Browse the repository at this point in the history
  • Loading branch information
viacheslav-rostovtsev authored Oct 7, 2024
1 parent 407dd87 commit b0b4c08
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Copyright 2024 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.

require "helper"
require "google/cloud/errors"
require "grpc/errors"
require "google/gax/errors"
require "google/rpc/status_pb"

# These test confirm that REST exceptions corresponding to various HTTP status codes
# are correctly wrapped
describe Google::Cloud::Error, :rest_errors do
it "identifies CanceledError" do
mapped_error = wrapped_rest_error gapic_rest_error(status_code: 499)
_(mapped_error).must_be_kind_of Google::Cloud::CanceledError
end

it "identifies InvalidArgumentError" do
mapped_error = wrapped_rest_error gapic_rest_error(status_code: 400)
_(mapped_error).must_be_kind_of Google::Cloud::InvalidArgumentError
end

it "identifies DeadlineExceededError" do
mapped_error = wrapped_rest_error gapic_rest_error(status_code: 504)
_(mapped_error).must_be_kind_of Google::Cloud::DeadlineExceededError
end

it "identifies NotFoundError" do
mapped_error = wrapped_rest_error gapic_rest_error(status_code: 404)
_(mapped_error).must_be_kind_of Google::Cloud::NotFoundError
end

it "identifies AlreadyExistsError" do
mapped_error = wrapped_rest_error gapic_rest_error(status_code: 409)
_(mapped_error).must_be_kind_of Google::Cloud::AlreadyExistsError
end

it "identifies PermissionDeniedError" do
mapped_error = wrapped_rest_error gapic_rest_error(status_code: 403)
_(mapped_error).must_be_kind_of Google::Cloud::PermissionDeniedError
end

it "identifies ResourceExhaustedError" do
mapped_error = wrapped_rest_error gapic_rest_error(status_code: 429)
_(mapped_error).must_be_kind_of Google::Cloud::ResourceExhaustedError
end

it "identifies FailedPreconditionError" do
mapped_error = wrapped_rest_error gapic_rest_error(status_code: 412)
_(mapped_error).must_be_kind_of Google::Cloud::FailedPreconditionError
end

it "identifies UnimplementedError" do
mapped_error = wrapped_rest_error gapic_rest_error(status_code: 501)
_(mapped_error).must_be_kind_of Google::Cloud::UnimplementedError
end

it "identifies InternalError" do
mapped_error = wrapped_rest_error gapic_rest_error(status_code: 500)
_(mapped_error).must_be_kind_of Google::Cloud::InternalError
end

it "identifies UnavailableError" do
mapped_error = wrapped_rest_error gapic_rest_error(status_code: 503)
_(mapped_error).must_be_kind_of Google::Cloud::UnavailableError
end

it "identifies unknown error" do
# We don't know what to map this error case to
mapped_error = wrapped_rest_error gapic_rest_error(status_code: 0)
_(mapped_error).must_be_kind_of Google::Cloud::Error
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Copyright 2020 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.

require "helper"
require "google/cloud/errors"
require "grpc/errors"
require "google/gax/errors"
require "google/rpc/status_pb"

describe Google::Cloud::Error, :wrapped_rest_error do
# This test confirms that a whole array of any-wrapped detail messages
# containing various messages from the `google/rpc/error_details.proto`
# will be correctly deserialized and surfaced to the end-user
# in the `status_details` field when wrapping the rest error
it "contains multiple detail messages" do
error = wrapped_rest_error gapic_rest_error(status_code: 404, extended_details: true)

di = error.status_details.find {|entry| entry.is_a?(Google::Rpc::DebugInfo)}
_(di).must_equal debug_info

lm = error.status_details.find {|entry| entry.is_a?(Google::Rpc::LocalizedMessage)}
_(lm).must_equal localized_message

help_detail = error.status_details.find {|entry| entry.is_a?(Google::Rpc::Help)}
end
end
32 changes: 1 addition & 31 deletions google-cloud-errors/test/google/cloud/errors/wrapped_gax_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,43 +19,13 @@
require "google/rpc/status_pb"

describe Google::Cloud::Error, :wrapped_gax do
def debug_info
Google::Rpc::DebugInfo.new detail: "status_detail"
end

def error_info
Google::Rpc::ErrorInfo.new reason: "ErrorInfo reason", domain: "ErrorInfo domain", metadata: {"foo": "bar"}
end

def localized_message
Google::Rpc::LocalizedMessage.new locale: "fr-CH", message: "c'est un message d'erreur"
end

def help
link = Google::Rpc::Help::Link.new description: "example description", url: "https://example.com/error"
Google::Rpc::Help.new links: [link]
end

##
# Construct a new Google::Rpc::Status object and return its binary encoding
#
# @param extended_details [Boolean]
# Whether to encode multiple error details. Default is one DebugInfo message.
def encoded_protobuf extended_details: false
any_debug = Google::Protobuf::Any.new
any_debug.pack debug_info

any_message = Google::Protobuf::Any.new
any_message.pack localized_message

any_help = Google::Protobuf::Any.new
any_help.pack help

status_arr = [any_debug]
status_arr = [any_debug, any_message, any_help] if extended_details

status = Google::Rpc::Status.new details: status_arr

status = google_rpc_status extended_details: extended_details
Google::Rpc::Status.encode status
end

Expand Down
118 changes: 118 additions & 0 deletions google-cloud-errors/test/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,121 @@
require "minitest/rg"
require "json"
require "google/cloud/errors"
require "grpc/errors"
require "google/rpc/status_pb"

def debug_info
Google::Rpc::DebugInfo.new detail: "status_detail"
end

def error_info
Google::Rpc::ErrorInfo.new reason: "ErrorInfo reason", domain: "ErrorInfo domain", metadata: {"foo": "bar"}
end

def localized_message
Google::Rpc::LocalizedMessage.new locale: "fr-CH", message: "c'est un message d'erreur"
end

def help
link = Google::Rpc::Help::Link.new description: "example description", url: "https://example.com/error"
Google::Rpc::Help.new links: [link]
end

def google_rpc_status extended_details: false
any_debug = Google::Protobuf::Any.new
any_debug.pack debug_info

any_message = Google::Protobuf::Any.new
any_message.pack localized_message

any_help = Google::Protobuf::Any.new
any_help.pack help

status_arr = [any_debug]
status_arr = [any_debug, any_message, any_help] if extended_details

Google::Rpc::Status.new details: status_arr
end

def gapic_rest_error status_code:, extended_details: false
status = google_rpc_status(extended_details: extended_details).to_json
jp = JSON.parse(status)
details = jp["details"]

headers = {"content-type" => "application/json; charset=UTF-8", "content-encoding"=>"gzip"}

gapic_rest_err = MockGapicRestError.new(
:status => "err_message for #{status_code}",
:details => details,
:status_code => status_code,
:headers => headers
)

gapic_rest_err
end

def wrapped_rest_error gapic_rest_error
begin
begin
raise gapic_rest_error
rescue => gapic_r_e
raise Google::Cloud::Error.from_error gapic_r_e
end
rescue => e
return e
end
end

##
# A truncated copy of gapic-common's GapicRestError that only keeps the
# parse details decoding implementation.
# Here to avoid referencing gapic-common.
#
class MockGapicRestError < StandardError
attr_reader :status_code, :status, :details, :headers
alias status_details details

def initialize msg = nil
super
end

def initialize status_code:, status:, details:, headers:
@status_code = status_code
@status = status
@msg = status
@details = parse_details details
@headers = headers

super
end

##
# A copy of implementation from gapic-common's GapicRestError
#
def parse_details details
# For rest errors details will contain json representations of `Protobuf.Any`
# decoded into hashes. If it's not an array, of its elements are not hashes,
# it's some other case
return details unless details.is_a? ::Array

details.map do |detail_instance|
next detail_instance unless detail_instance.is_a? ::Hash
# Next, parse detail_instance into a Proto message.
# There are three possible issues for the JSON->Any->message parsing
# - json decoding fails
# - the json belongs to a proto message type we don't know about
# - any unpacking fails
# If we hit any of these three issues we'll just return the original hash
begin
any = ::Google::Protobuf::Any.decode_json detail_instance.to_json
klass = ::Google::Protobuf::DescriptorPool.generated_pool.lookup(any.type_name)&.msgclass
next detail_instance if klass.nil?
unpack = any.unpack klass
next detail_instance if unpack.nil?
unpack
rescue ::Google::Protobuf::ParseError
detail_instance
end
end.compact
end
end

0 comments on commit b0b4c08

Please sign in to comment.