Skip to content

Commit ce956de

Browse files
committed
Add dedicated ErrorHandler concern spec
1 parent d67bee1 commit ce956de

File tree

1 file changed

+140
-0
lines changed

1 file changed

+140
-0
lines changed
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
require 'datadog'
5+
require 'unified_health_data/service'
6+
require 'support/shared_contexts/uhd_security_endpoint'
7+
8+
RSpec.describe MyHealth::V2::Concerns::ErrorHandler, :skip_json_api_validation, type: :request do
9+
include_context 'uhd legacy security endpoint'
10+
11+
let(:current_user) { build(:user, :mhv) }
12+
let(:path) { '/my_health/v2/medical_records/allergies' }
13+
let(:active_span) { instance_double(Datadog::Tracing::SpanOperation, set_error: nil, service: nil) }
14+
let(:rack_span) { instance_double(Datadog::Tracing::SpanOperation, set_error: nil) }
15+
16+
before do
17+
Timecop.freeze('2025-06-02T08:00:00Z')
18+
sign_in_as(current_user)
19+
allow(Flipper).to receive(:enabled?).with(:mhv_accelerated_delivery_uhd_enabled,
20+
instance_of(User)).and_return(true)
21+
allow(Flipper).to receive(:enabled?).with(:mhv_accelerated_delivery_allergies_enabled,
22+
instance_of(User)).and_return(true)
23+
allow(Datadog::Tracing).to receive(:active_span).and_return(active_span)
24+
allow(active_span).to receive(:service=)
25+
end
26+
27+
after do
28+
Timecop.return
29+
end
30+
31+
# Helper to stub service and make request
32+
def stub_and_request(error)
33+
allow_any_instance_of(UnifiedHealthData::Service).to receive(:get_allergies).and_raise(error)
34+
VCR.use_cassette('unified_health_data/get_allergies_200') do
35+
get path, headers: { 'X-Key-Inflection' => 'camel' }
36+
end
37+
end
38+
39+
describe 'status code mapping' do
40+
context 'when upstream times out (GatewayTimeout)' do
41+
it 'returns 504' do
42+
stub_and_request(Common::Exceptions::GatewayTimeout.new('Faraday::TimeoutError'))
43+
expect(response).to have_http_status(:gateway_timeout)
44+
end
45+
end
46+
47+
context 'when raw Net::HTTP times out (Timeout::Error)' do
48+
it 'returns 504 for Net::OpenTimeout' do
49+
stub_and_request(Net::OpenTimeout.new('execution expired'))
50+
expect(response).to have_http_status(:gateway_timeout)
51+
end
52+
53+
it 'returns 504 for Net::ReadTimeout' do
54+
stub_and_request(Net::ReadTimeout.new('Net::ReadTimeout'))
55+
expect(response).to have_http_status(:gateway_timeout)
56+
end
57+
end
58+
59+
context 'when connection fails (ClientError with nil status)' do
60+
it 'returns 503' do
61+
stub_and_request(Common::Client::Errors::ClientError.new('Connection refused', nil))
62+
expect(response).to have_http_status(:service_unavailable)
63+
end
64+
end
65+
66+
context 'when upstream returns HTTP error (ClientError with status)' do
67+
it 'returns 502' do
68+
stub_and_request(Common::Client::Errors::ClientError.new('Internal Server Error', 500))
69+
expect(response).to have_http_status(:bad_gateway)
70+
end
71+
end
72+
73+
context 'when BackendServiceException is raised' do
74+
it 'returns 502' do
75+
stub_and_request(Common::Exceptions::BackendServiceException.new('VA900', {}, 502, 'Backend failure'))
76+
expect(response).to have_http_status(:bad_gateway)
77+
end
78+
end
79+
80+
context 'when circuit breaker is open (Breakers::OutageException)' do
81+
it 'returns 503' do
82+
mock_service = instance_double(Breakers::Service, name: 'UHD')
83+
outage = instance_double(Breakers::Outage, start_time: Time.zone.now, end_time: nil, service: mock_service)
84+
stub_and_request(Breakers::OutageException.new(outage, mock_service))
85+
expect(response).to have_http_status(:service_unavailable)
86+
end
87+
end
88+
89+
context 'when DNS resolution fails (SocketError)' do
90+
it 'returns 503' do
91+
stub_and_request(SocketError.new('getaddrinfo: nodename nor servname provided'))
92+
expect(response).to have_http_status(:service_unavailable)
93+
end
94+
end
95+
96+
context 'when TLS handshake fails (OpenSSL::SSL::SSLError)' do
97+
it 'returns 503' do
98+
stub_and_request(OpenSSL::SSL::SSLError.new('SSL_connect returned=1 errno=0'))
99+
expect(response).to have_http_status(:service_unavailable)
100+
end
101+
end
102+
103+
context 'when an unexpected StandardError is raised' do
104+
it 'returns 500' do
105+
stub_and_request(StandardError.new('something unexpected'))
106+
expect(response).to have_http_status(:internal_server_error)
107+
end
108+
end
109+
end
110+
111+
describe 'Datadog error reporting' do
112+
it 'sets error on active span' do
113+
error = Common::Client::Errors::ClientError.new('Connection refused', nil)
114+
stub_and_request(error)
115+
expect(active_span).to have_received(:set_error).with(error)
116+
end
117+
118+
it 'sets error on rack span' do
119+
error = Common::Client::Errors::ClientError.new('Connection refused', nil)
120+
121+
allow_any_instance_of(UnifiedHealthData::Service).to receive(:get_allergies).and_raise(error)
122+
VCR.use_cassette('unified_health_data/get_allergies_200') do
123+
# Inject a mock rack span into the request env before the request
124+
allow_any_instance_of(ActionDispatch::Request).to receive(:env).and_wrap_original do |original|
125+
env = original.call
126+
env[Datadog::Tracing::Contrib::Rack::Ext::RACK_ENV_REQUEST_SPAN] ||= rack_span
127+
env
128+
end
129+
get path, headers: { 'X-Key-Inflection' => 'camel' }
130+
end
131+
expect(rack_span).to have_received(:set_error).with(error)
132+
end
133+
134+
it 'handles nil spans gracefully' do
135+
allow(Datadog::Tracing).to receive(:active_span).and_return(nil)
136+
stub_and_request(StandardError.new('test'))
137+
expect(response).to have_http_status(:internal_server_error)
138+
end
139+
end
140+
end

0 commit comments

Comments
 (0)