Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LocalBGS cleanup #16412

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions config/features.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ features:
actor_type: user
description: Enables users to access the claim letters page
enable_in_development: true
claims_api_local_bgs_refactor:
actor_type: user
description: Diverts codepath to LocalBGSRefactored
enable_in_development: true
cst_use_lighthouse_5103:
actor_type: user
description: When enabled, claims status tool uses the Lighthouse API for the 5103 endpoint
Expand Down
4 changes: 2 additions & 2 deletions modules/claims_api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ ssh -L 4447:localhost:4447 {{aws-url}}
ssh -L 4431:localhost:4431 {{aws-url}}

## Testing
### Unit testing BGS service operation wrappers
### Unit testing BGS service action wrappers
If using cassettes, make sure to only make or use ones under [spec/support/vcr_cassettes/claims_api](spec/support/vcr_cassettes/claims_api)
Check out documentation in comments for the spec helper `BGSClientHelpers#use_bgs_cassette`
Check out documentation in comments for the spec helper `BGSClientSpecHelpers#use_bgs_cassette`

## OpenApi/Swagger Doc Generation
This api uses [rswag](https://github.com/rswag/rswag) to build the OpenApi/Swagger docs that are displayed in the [VA|Lighthouse APIs Documentation](https://developer.va.gov/explore/benefits/docs/claims?version=current). To generate/update the docs for this api, navigate to the root directory of `vets-api` and run the following command ::
Expand Down
117 changes: 117 additions & 0 deletions modules/claims_api/app/clients/claims_api/bgs_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# frozen_string_literal: true

module ClaimsApi
module BGSClient
class << self
##
# Invokes the given BGS SOAP service action with the given payload and
# returns a result containing a success payload or a fault.
#
# @example Perform a request to BGS at:
# /VDC/ManageRepresentativeService(readPOARequest)
#
# body = <<~EOXML
# <data:SecondaryStatusList>
# <SecondaryStatus>New</SecondaryStatus>
# </data:SecondaryStatusList>
# <data:POACodeList>
# <POACode>012</POACode>
# </data:POACodeList>
# EOXML
#
# definition =
# BGSClient::ServiceAction::Definition::
# ManageRepresentativeService::
# ReadPoaRequest
#
# BGSClient.perform_request(
# definition:,
# body:
# )
#
# @param definition [BGSClient::ServiceAction::Definition] a value object
# that identifies a particular BGS SOAP service action by way of:
# `{.service_path, .service_namespaces, .action_name}`
#
# @param body [String, #to_xml, #to_s] the action payload
#
# @param external_id [BGSClient::ServiceAction::ExternalId] a value object
# that arbitrarily self-identifies ourselves to BGS as its caller by:
# `{.external_uid, .external_key}`
#
# @return [BGSClient::ServiceAction::Request::Result<Hash, BGSClient::ServiceAction::Request::Fault>]
# the response payload of a successful request, or the fault object of a
# failed request
def perform_request(
definition:, body:,
external_id: ServiceAction::ExternalId::DEFAULT
)
ServiceAction
.const_get(:Request)
.new(definition:, external_id:)
.perform(body)
end

##
# Reveals the momemtary health of a BGS service by attempting to request
# its WSDL and returning the HTTP status code of the response.
#
# @example
# definition =
# BGSClient::ServiceAction::Definition::
# ManageRepresentativeService::
# ReadPoaRequest
#
# BGSClient.healthcheck(definition)
#
# @param definition [BGSClient::ServiceAction::Definition] a value object
# that identifies a particular BGS SOAP service action by way of:
# `{.service_path, .service_namespaces, .action_name}`
#
# @return [Integer] HTTP status code
def healthcheck(definition)
connection = build_connection
path = definition.service_path
response = fetch_wsdl(connection, path)
response.status
end

def breakers_service
url = URI.parse(Settings.bgs.url)
request_matcher =
proc do |request_env|
request_env.url.host == url.host &&
request_env.url.port == url.port &&
request_env.url.path =~ /^#{url.path}/
end

Breakers::Service.new(
name: 'BGS/Claims',
request_matcher:
)
end

private

def fetch_wsdl(connection, path)
connection.get(path) do |req|
req.params['WSDL'] = nil
end
end

def build_connection
ssl_verify_mode =
if Settings.bgs.ssl_verify_mode == 'none'
OpenSSL::SSL::VERIFY_NONE
else
OpenSSL::SSL::VERIFY_PEER
end

Faraday.new(Settings.bgs.url) do |conn|
conn.ssl.verify_mode = ssl_verify_mode
yield(conn) if block_given?
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

module ClaimsApi
module BGSClient
module ServiceAction
class Definition <
Data.define(
:service_path,
:service_namespaces,
:action_name
)

module ManageRepresentativeService
service = {
service_path: 'VDC/ManageRepresentativeService',
service_namespaces: { 'data' => '/data' }
}

ReadPoaRequest =
Definition.new(
action_name: 'readPOARequest',
**service
)
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module ClaimsApi
module BGSClient
module ServiceAction
class ExternalId < Data.define(:external_uid, :external_key)
DEFAULT =
new(
external_uid: Settings.bgs.external_uid,
external_key: Settings.bgs.external_key
)
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# frozen_string_literal: true

require 'claims_api/claim_logger'

module ClaimsApi
module BGSClient
module ServiceAction
# `private_constant` is used to prevent inheritance that could eventually
# tempt someone to add extraneous behavior to this, the parent class.
# Consumers should instead directly interface with
# `BGSClient.perform_request`, which maintains the sole responsibility of
# making a request to BGS.
private_constant :Request

class Request
attr_reader :external_id

def initialize(definition:, external_id:)
@definition = definition
@external_id = external_id
end

def perform(body) # rubocop:disable Metrics/MethodLength
begin
wsdl =
log_duration('connection_wsdl_get') do
BGSClient.send(
:fetch_wsdl,
connection,
@definition.service_path
).body
end

request_body =
log_duration('built_request') do
wsdl_body = Hash.from_xml(wsdl)
namespace = wsdl_body.dig('definitions', 'targetNamespace').to_s
build_request_body(body, namespace:)
end

response =
log_duration('connection_post') do
connection.post(@definition.service_path) do |req|
req.body = request_body
req.headers.merge!(
'Soapaction' => %("#{@definition.action_name}"),
'Content-Type' => 'text/xml;charset=UTF-8',
'Host' => "#{Settings.bgs.env}.vba.va.gov"
)
end
end
rescue Faraday::TimeoutError, Faraday::ConnectionFailed => e
detail = "local BGS Faraday Timeout: #{e.message}"
ClaimsApi::Logger.log('local_bgs', retry: true, detail:)
raise ::Common::Exceptions::BadGateway
end

log_duration('parsed_response') do
response_body = Hash.from_xml(response.body)
action_body = response_body.dig('Envelope', 'Body').to_h
fault = get_fault(action_body)

if fault
Result.new(
success: false,
value: fault
)
else
key = "#{@definition.action_name}Response"
value = action_body[key].to_h

Result.new(
success: true,
value:
)
end
end
end

private

def build_request_body(body, namespace:) # rubocop:disable Metrics/MethodLength
namespaces =
{}.tap do |value|
namespace = URI(namespace)
value['tns'] = namespace

@definition.service_namespaces.to_h.each do |aliaz, path|
uri = namespace.clone
uri.path = path
value[aliaz] = uri
end
end

client_ip =
if Rails.env.test?
# For all intents and purposes, BGS behaves identically no matter
# what IP we provide it. So in a test environment, let's just give
# it a fake IP so that cassette matching isn't defeated on CI and
# everyone's computer.
'127.0.0.1'
else
Socket
.ip_address_list
.detect(&:ipv4_private?)
.ip_address
end

headers =
Envelope::Headers.new(
ip: client_ip,
username: Settings.bgs.client_username,
station_id: Settings.bgs.client_station_id,
application_name: Settings.bgs.application,
external_id:
)

action = @definition.action_name

Envelope.generate(
namespaces:,
headers:,
action:,
body:
)
end

def get_fault(body)
fault = body['Fault'].to_h
return if fault.blank?

message =
fault.dig('detail', 'MessageException') ||
fault.dig('detail', 'MessageFaultException')

Fault.new(
code: fault['faultcode'].to_s.split(':').last,
string: fault['faultstring'],
message:
)
end

def connection
@connection ||=
BGSClient.send(:build_connection) do |conn|
# Should all of this connection configuration really not be
# involved in the BGS service healthcheck performed by
# `BGSClient.healthcheck`? Under the hood, that just fetches WSDL
# which we also do here but with "smarter" connection config.
conn.options.timeout = Settings.bgs.timeout || 120
conn.adapter Faraday.default_adapter
conn.use :breakers
end
end

def log_duration(event_name)
start = now
result = yield
finish = now

duration = (finish - start).round(4)
event = {
# event should be first key in log, duration last
event: event_name,
endpoint: @definition.service_path,
action: @definition.action_name,
duration:
}

ClaimsApi::Logger.log('local_bgs', **event)
metric = "api.claims_api.local_bgs.#{event_name}.duration"
StatsD.measure(metric, duration, tags: {})

result
end

def now
::Process.clock_gettime(
::Process::CLOCK_MONOTONIC
)
end

Fault =
Data.define(
:code,
:string,
:message
)

# Tiny subset of the API for `Dry::Monads[:result]`. Chose this
# particularly because some SOAP `500` really isn't error-like, and it
# is awkward to wrap exception handling for non-exceptional cases.
class Result
attr_reader :value

def initialize(value:, success:)
@value = value
@success = success
end

def success?
@success
end

def failure?
!success?
end
end
end
end
end
end
Loading
Loading