Skip to content

Commit

Permalink
Merge branch 'master' into DR-90265-add-error-body-for-422-logs
Browse files Browse the repository at this point in the history
  • Loading branch information
anniebtran authored Sep 23, 2024
2 parents 6208115 + f378056 commit ccc4e10
Show file tree
Hide file tree
Showing 49 changed files with 1,336 additions and 128 deletions.
7 changes: 6 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -512,9 +512,14 @@ GEM
thor (>= 0.20, < 2.a)
google-cloud-env (2.1.1)
faraday (>= 1.0, < 3.a)
google-protobuf (4.28.1)
google-protobuf (4.28.2)
bigdecimal
rake (>= 13)
google-protobuf (4.28.2-java)
bigdecimal
ffi (~> 1)
ffi-compiler (~> 1)
rake (>= 13)
googleauth (1.11.0)
faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.1)
Expand Down
9 changes: 9 additions & 0 deletions config/betamocks/services_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,15 @@
:path: "/api/v2/search/i14y"
:file_path: "search/default"

# Search GSA
- :name: 'Search GSA'
:base_uri: <%= "#{URI(Settings.search.gsa_url).host}:#{URI(Settings.search.gsa_url).port}" %>
:endpoints:
# Search results
- :method: :get
:path: "/technology/searchgov/v2/results/i14y"
:file_path: "search/default"

#GIS
- :name: 'GIS'
:base_uri: <%= "#{URI(Settings.locators.gis_base_path).host}:#{URI(Settings.locators.gis_base_path).port}" %>
Expand Down
4 changes: 4 additions & 0 deletions config/features.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1694,3 +1694,7 @@ features:
mgib_verifications_maintenance:
actor_type: user
description: Used to show maintenance alert for MGIB Verifications
search_use_v2_gsa:
actor_type: cookie_id
description: Swaps the Search Service's configuration url with an updated api.gsa.gov address
enabled_in_development: true
24 changes: 24 additions & 0 deletions config/locales/exceptions.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,30 @@ en:
code: 'SEARCH_504'
detail: 'Did not receive a timely response from Search.gov'
status: 504
SEARCH_GSA_400:
<<: *external_defaults
title: Bad Request
code: 'SEARCH_GSA_400'
detail: 'api.gsa.gov service responded with a Bad Request'
status: 400
SEARCH_GSA_429:
<<: *external_defaults
title: Exceeded rate limit
code: 'SEARCH_GSA_429'
detail: 'Exceeded api.gsa.gov rate limit'
status: 429
SEARCH_GSA_503:
<<: *external_defaults
title: Service Unavailable
code: 'SEARCH_GSA_503'
detail: 'api.gsa.gov service is currently unavailable'
status: 503
SEARCH_GSA_504:
<<: *external_defaults
title: Gateway Timeout
code: 'SEARCH_GSA_504'
detail: 'Did not receive a timely response from api.gsa.gov'
status: 504
SEARCH_TYPEAHEAD_400:
<<: *external_defaults
title: Bad Request
Expand Down
5 changes: 3 additions & 2 deletions config/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -900,15 +900,16 @@ search:
access_key: SEARCH_GOV_ACCESS_KEY
affiliate: va
mock_search: false
url: https://search.usa.gov/api/v2
gsa_url: https://api.gsa.gov/technology/searchgov/v2/results/i14y
url: https://search.usa.gov/api/v2/search/i14y

# Settings for search-typeahead
search_typeahead:
api_key: API_GOV_ACCESS_KEY
name: va
url: https://api.gsa.gov/technology/searchgov/v1

# Settings for search-click-tracking
# Settings for search-click-tracking
search_click_tracking:
access_key: SEARCH_GOV_ACCESS_KEY
affiliate: va
Expand Down
3 changes: 2 additions & 1 deletion config/settings/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ directory:
search:
access_key: TESTKEY
affiliate: va
url: https://search.usa.gov/api/v2
gsa_url: https://api.gsa.gov/technology/searchgov/v2/results/i14y
url: https://search.usa.gov/api/v2/search/i14y

search_typeahead:
api_key: TEST_KEY
Expand Down
11 changes: 10 additions & 1 deletion lib/search/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,16 @@ def mock_enabled?
end

def base_path
"#{Settings.search.url}/search/i14y"
flipper_enabled? ? Settings.search.gsa_url : Settings.search.url
end

# Breakers initialization requires this configuration which means the #base_path
# is required when building the DBs in CI that flipper uses for checking toggles.
# The NoDatabaseError rescue handles times we're building new DBs.
def flipper_enabled?
Flipper.enabled?(:search_use_v2_gsa)
rescue ActiveRecord::NoDatabaseError
false
end

def service_name
Expand Down
22 changes: 13 additions & 9 deletions lib/search/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
require 'search/configuration'

module Search
# This class builds a wrapper around Search.gov web results API. Creating a new instance of class
# This class builds a wrapper around Search.gov or api.gsa.gov web results API. Creating a new instance of class
# will and calling #results will return a ResultsResponse upon success or an exception upon failure.
#
# @see https://search.usa.gov/sites/7378/api_instructions
# @see https://open.gsa.gov/api/searchgov-results/
#
class Service < Common::Client::Base
include Common::Client::Concerns::Monitoring
Expand Down Expand Up @@ -48,6 +49,7 @@ def results_url
# Optional params [enable_highlighting, limit, offset, sort_by]
#
# @see https://search.usa.gov/sites/7378/api_instructions
# @see https://open.gsa.gov/api/searchgov-results/
#
def query_params
{
Expand Down Expand Up @@ -92,7 +94,7 @@ def handle_error(error)
message = parse_messages(error).first
save_error_details(message)
handle_429!(error)
raise_backend_exception('SEARCH_400', self.class, error) if error.status >= 400
raise_backend_exception(error_code_name(400), self.class, error) if error.status >= 400
else
raise error
end
Expand All @@ -114,20 +116,22 @@ def handle_429!(error)
return unless error.status == 429

StatsD.increment("#{Search::Service::STATSD_KEY_PREFIX}.exceptions", tags: ['exception:429'])
raise_backend_exception('SEARCH_429', self.class, error)
raise_backend_exception(error_code_name(error.status), self.class, error)
end

def handle_server_error!(error)
return unless [503, 504].include?(error.status)

exceptions = {
503 => 'SEARCH_503',
504 => 'SEARCH_504'
}
# Catch when the error's structure doesn't match what's usually expected.
message = error.body.is_a?(Hash) ? parse_messages(error).first : 'Search.gov is down'
message = error.body.is_a?(Hash) ? parse_messages(error).first : 'Search API is down'
save_error_details(message)
raise_backend_exception(exceptions[error.status], self.class, error)
raise_backend_exception(error_code_name(error.status), self.class, error)
end

def error_code_name(error_status)
error_code_prefix = self.class.configuration.flipper_enabled? ? 'SEARCH_GSA' : 'SEARCH'

"#{error_code_prefix}_#{error_status}"
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,17 @@ class ApplicationController < SignIn::ApplicationController
validates_access_token_audience Settings.sign_in.arp_client_id

before_action :verify_pilot_enabled_for_user
around_action :handle_exceptions

private

def handle_exceptions
yield
rescue => e
Rails.logger.error("ARP: Unexpected error occurred for user with user_uuid=#{@current_user&.uuid} - #{e.message}")
raise e
end

def verify_pilot_enabled_for_user
unless Flipper.enabled?(:accredited_representative_portal_pilot, @current_user)
message = 'The accredited_representative_portal_pilot feature flag is disabled ' \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,10 @@ class Form21aController < ApplicationController
# Parses the request body and submits the form.
# Renders the appropriate response based on the service's outcome.
def submit
response = AccreditationService.submit_form21a(parsed_request_body)
response = AccreditationService.submit_form21a(parsed_request_body, @current_user&.uuid)

InProgressForm.form_for_user(FORM_ID, @current_user)&.destroy if response.success?
render_ogc_service_response(response)
rescue => e
Rails.logger.error("Form21aController: Unexpected error occurred - #{e.message}")
render json: { errors: 'Unexpected error' }, status: :internal_server_error
end

private
Expand All @@ -30,19 +27,30 @@ def submit
def parse_request_body
@parsed_request_body = JSON.parse(request.raw_post)
rescue JSON::ParserError
Rails.logger.error('Form21aController: Invalid JSON in request body')
Rails.logger.error(
"Form21aController: Invalid JSON in request body for user with user_uuid=#{@current_user&.uuid}"
)
render json: { errors: 'Invalid JSON' }, status: :bad_request
end

# Renders the response based on the service call's success or failure.
def render_ogc_service_response(response)
if response.success?
Rails.logger.info(
'Form21aController: Form 21a successfully submitted to OGC service ' \
"by user with user_uuid=#{@current_user&.uuid} - Response: #{response.body}"
)
render json: response.body, status: response.status
elsif response.body.blank?
Rails.logger.info('Form21aController: Blank response from OGC service')
Rails.logger.info(
"Form21aController: Blank response from OGC service for user with user_uuid=#{@current_user&.uuid}"
)
render status: :no_content
else
Rails.logger.error('Form21aController: Failed to parse response from external OGC service')
Rails.logger.error(
'Form21aController: Failed to parse response from external OGC service ' \
"for user with user_uuid=#{@current_user&.uuid}"
)
render json: { errors: 'Failed to parse response' }, status: :bad_gateway
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,20 @@ class AccreditationService
# self.submit_form21a(parsed_body): Submits the given parsed body as JSON to the accreditation service.
# - Parameters:
# - parsed_body: A Hash representing the parsed form data.
# - user_uuid: A String representing the user's UUID, which is also stored in the in_progress_forms DB entry.
# - Returns: A Faraday::Response object containing the service response.
def self.submit_form21a(parsed_body)
def self.submit_form21a(parsed_body, user_uuid)
Rails.logger.info("Accreditation Service attempting submit_form21a with service_url: #{service_url}")
connection.post do |req|
req.body = parsed_body.to_json
end
rescue Faraday::ConnectionFailed => e
Rails.logger.error("Accreditation Service connection failed: #{e.message}, URL: #{service_url}")
Rails.logger.error(
"Accreditation Service connection failed for user with user_uuid=#{user_uuid}: #{e.message}, URL: #{service_url}"
)
Faraday::Response.new(status: :service_unavailable, body: { errors: 'Accreditation Service unavailable' }.to_json)
rescue Faraday::TimeoutError => e
Rails.logger.error("Accreditation Service request timed out: #{e.message}")
Rails.logger.error("Accreditation Service request timed out for user with user_uuid=#{user_uuid}: #{e.message}")
Faraday::Response.new(status: :request_timeout, body: { errors: 'Accreditation Service request timed out' }.to_json)
end

Expand All @@ -39,7 +43,7 @@ def self.service_url
case Rails.env
when 'development', 'test'
# NOTE: the below is a temporary URL for development purposes only.
# TODO: Update this once ESECC request goes through. See: https://github.com/department-of-veterans-affairs/va.gov-team/
# TODO: Update this once ESECC request goes through. See: https://github.com/department-of-veterans-affairs/va.gov-team/issues/88288
'http://localhost:5000/api/v1/accreditation/applications/form21a'
when 'production'
# TODO: Update this once MOU has been signed and the ESECC request has gone through. See:
Expand Down
7 changes: 0 additions & 7 deletions modules/accredited_representative_portal/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,6 @@

AccreditedRepresentativePortal::Engine.routes.draw do
namespace :v0, defaults: { format: :json } do
resources :power_of_attorney_requests, only: [:index] do
member do
post :accept
post :decline
end
end

get 'user', to: 'representative_users#show'

post 'form21a', to: 'form21a#submit'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
context 'with valid JSON' do
let!(:in_progress_form) { create(:in_progress_form, form_id: '21a', user_uuid: representative_user.uuid) }

it 'returns a successful response from the service and destroys in progress form' do
it 'logs a successful submission and destroys in-progress form' do
get('/accredited_representative_portal/v0/in_progress_forms/21a')
expect(response).to have_http_status(:ok)
expect(parsed_response.keys).to contain_exactly('formData', 'metadata')
Expand All @@ -25,6 +25,11 @@
instance_double(Faraday::Response, success?: true, body: { result: 'success' }.to_json, status: 200)
)

expect(Rails.logger).to receive(:info).with(
'Form21aController: Form 21a successfully submitted to OGC service ' \
"by user with user_uuid=#{representative_user.uuid} - Response: {\"result\":\"success\"}"
)

headers = { 'Content-Type' => 'application/json' }
post('/accredited_representative_portal/v0/form21a', params: valid_json, headers:)

Expand All @@ -38,7 +43,11 @@
end

context 'with invalid JSON' do
it 'returns a bad request status' do
it 'logs the error and returns a bad request status' do
expect(Rails.logger).to receive(:error).with(
"Form21aController: Invalid JSON in request body for user with user_uuid=#{representative_user.uuid}"
)

headers = { 'Content-Type' => 'application/json' }
post('/accredited_representative_portal/v0/form21a', params: invalid_json, headers:)

Expand All @@ -48,11 +57,15 @@
end

context 'when service returns a blank response' do
it 'returns no content status' do
it 'logs the error and returns no content status' do
allow(AccreditationService).to receive(:submit_form21a).and_return(
instance_double(Faraday::Response, success?: false, body: nil, status: 204)
)

expect(Rails.logger).to receive(:info).with(
"Form21aController: Blank response from OGC service for user with user_uuid=#{representative_user.uuid}"
)

headers = { 'Content-Type' => 'application/json' }
post('/accredited_representative_portal/v0/form21a', params: valid_json, headers:)

Expand All @@ -61,12 +74,17 @@
end

context 'when service fails to parse response' do
it 'returns a bad gateway status' do
it 'logs the error and returns a bad gateway status' do
allow(AccreditationService).to receive(:submit_form21a).and_return(
instance_double(Faraday::Response, success?: false, body: { errors: 'Failed to parse response' }.to_json,
status: 502)
)

expect(Rails.logger).to receive(:error).with(
'Form21aController: Failed to parse response from external OGC service ' \
"for user with user_uuid=#{representative_user.uuid}"
)

headers = { 'Content-Type' => 'application/json' }
post('/accredited_representative_portal/v0/form21a', params: valid_json, headers:)

Expand All @@ -76,12 +94,18 @@
end

context 'when an unexpected error occurs' do
it 'returns an internal server error status' do
it 'logs the error and returns an internal server error status' do
allow_any_instance_of(AccreditedRepresentativePortal::V0::Form21aController)
.to receive(:parse_request_body).and_raise(StandardError, 'Unexpected error')

allow(Rails.logger).to receive(:error).and_call_original

post '/accredited_representative_portal/v0/form21a'

expect(Rails.logger).to have_received(:error).with(
"ARP: Unexpected error occurred for user with user_uuid=#{representative_user.uuid} - Unexpected error"
)

expect(response).to have_http_status(:internal_server_error)
expect(parsed_response).to match(
'errors' => [
Expand Down
Loading

0 comments on commit ccc4e10

Please sign in to comment.