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

Jmax/lg 15676 passport api health check infrastructure #11891

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
14 changes: 14 additions & 0 deletions app/services/analytics_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6232,6 +6232,20 @@ def otp_phone_validation_failed(error:, message:, context:, country:, **extra)
)
end

# Tracks the health of the DoS Passports API
# @param [Boolean] success Whether the passport api health check succeeded.
# @param [Hash] body The health check body, if present.
# @param [String] error Any additional error information we have
def passport_api_health_check(success:, body: nil, error: nil, **extra)
track_event(
:passport_api_health_check,
success:,
body:,
error:,
**extra,
)
end

# @param [Boolean] success Whether form validation was successful
# @param [Hash] error_details Details for errors that occurred in unsuccessful submission
# @param [Boolean] active_profile_present Whether active profile existed at time of change
Expand Down
35 changes: 35 additions & 0 deletions app/services/doc_auth/dos/requests/health_check_request.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

module DocAuth
module Dos
module Requests
class HealthCheckRequest
def fetch(analytics)
begin
faraday_response = connection.get
response = Responses::HealthCheckSuccess.new(faraday_response)
rescue Faraday::Error => faraday_error
response = Responses::HealthCheckFailure.new(faraday_error)
end
ensure
analytics.passport_api_health_check(
success: response.success?,
**response.extra,
)
end

private

attr_reader :analytics

def connection
@connection ||= Faraday::Connection.new(
url: IdentityConfig.store.passports_api_health_check_endpoint,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are 2 healthcheck endpoints for which we need to be able to check ...
there is also a composite healthcheck endpoint

) do |builder|
builder.response :raise_error
end
end
end
end
end
end
25 changes: 25 additions & 0 deletions app/services/doc_auth/dos/responses/health_check_failure.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

module DocAuth
module Dos
module Responses
class HealthCheckFailure < DocAuth::Response
def initialize(faraday_error)
errors =
if faraday_error.respond_to?(:status) # some subclasses don't
{ network: faraday_error.status }
else
{ network: true }
end

super(
success: false,
errors:,
exception: faraday_error,
extra: { error: faraday_error.inspect }
)
end
end
end
end
end
23 changes: 23 additions & 0 deletions app/services/doc_auth/dos/responses/health_check_success.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

module DocAuth
module Dos
module Responses
class HealthCheckSuccess < DocAuth::Response
def initialize(faraday_response)
extra =
if faraday_response.body && !faraday_response.body.empty?
{ body: faraday_response.body }
else
{}
end

super(
success: faraday_response.success?,
extra:
)
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

module DocAuth
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this file to be deleted after namespace change?

module Passports
module Dos
module Requests
class HealthCheckRequest
def initialize(analytics:)
@analytics = analytics
end

def fetch
begin
faraday_response = connection.get
response = Responses::HealthCheckSuccess.new(faraday_response)
rescue Faraday::Error => faraday_error
response = Responses::HealthCheckFailure.new(faraday_error)
end
ensure
analytics.passport_api_health_check(
success: response.success?,
**response.extra,
)
end

private

attr_reader :analytics

def connection
@connection ||= Faraday::Connection.new(
url: IdentityConfig.store.passports_api_health_check_endpoint,
) do |builder|
builder.response :raise_error
end
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module DocAuth
module Passports
module Dos
module Responses
class HealthCheckFailure < DocAuth::Response
def initialize(faraday_error)
errors =
if faraday_error.respond_to?(:status) # some subclasses don't
{ network: faraday_error.status }
else
{ network: true }
end

super(
success: false,
errors:,
exception: faraday_error,
extra: { error: faraday_error.inspect }
)
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

module DocAuth
module Passports
module Dos
module Responses
class HealthCheckSuccess < DocAuth::Response
def initialize(faraday_response)
extra =
if faraday_response.body && !faraday_response.body.empty?
{ body: faraday_response.body }
else
{}
end

super(
success: faraday_response.success?,
extra:
)
end
end
end
end
end
end
1 change: 1 addition & 0 deletions config/application.yml.default
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ outbound_connection_check_retry_count: 2
outbound_connection_check_timeout: 5
outbound_connection_check_url: 'https://checkip.amazonaws.com'
participate_in_dap: false
passports_api_health_check_endpoint: 'https://caapinpe.state.gov/api/passport-match-prc-api/v1/html'
Copy link
Contributor

@amirbey amirbey Feb 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
passports_api_health_check_endpoint: 'https://caapinpe.state.gov/api/passport-match-prc-api/v1/html'

password_max_attempts: 3
password_pepper:
personal_key_retired: true
Expand Down
1 change: 1 addition & 0 deletions lib/identity_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ def self.store
config.add(:outbound_connection_check_timeout, type: :integer)
config.add(:outbound_connection_check_url)
config.add(:participate_in_dap, type: :boolean)
config.add(:passports_api_health_check_endpoint, type: :string)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
config.add(:passports_api_health_check_endpoint, type: :string)

config.add(:password_max_attempts, type: :integer)
config.add(:password_pepper, type: :string)
config.add(:personal_key_retired, type: :boolean)
Expand Down
95 changes: 95 additions & 0 deletions spec/services/doc_auth/dos/requests/health_check_request_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
require 'rails_helper'

RSpec.describe DocAuth::Dos::Requests::HealthCheckRequest do
include PassportApiHelpers

subject(:health_check_request) { described_class.new }

let(:analytics) { FakeAnalytics.new }

let(:health_check_endpoint) do
IdentityConfig.store.passports_api_health_check_endpoint
end

describe '#fetch' do
let(:result) { health_check_request.fetch(analytics) }

context 'happy path' do
before do
stub_api_up
end

it 'hits the endpoint' do
result
expect(WebMock).to have_requested(:get, health_check_endpoint)
end

it 'logs the request' do
result
expect(analytics).to have_logged_event(
:passport_api_health_check,
success: true,
body: successful_api_health_check_body.to_json,
)
end

describe 'the #fetch result' do
it 'succeeds' do
expect(result).to be_success
end
end
end

context 'when Faraday raises an error' do
before do
stub_request(:get, health_check_endpoint).to_raise(Faraday::Error)
end

it 'hits the endpoint' do
result
expect(WebMock).to have_requested(:get, health_check_endpoint)
end

it 'logs the request' do
result
expect(analytics).to have_logged_event(
:passport_api_health_check,
success: false,
error: /Faraday::Error/,
)
end

describe 'the #fetch result' do
it 'does not succeed' do
expect(result).not_to be_success
end
end
end

context 'when Faraday returns an HTTP error' do
before do
stub_request(:get, health_check_endpoint).to_return(status: 500)
end

it 'hits the endpoint' do
result
expect(WebMock).to have_requested(:get, health_check_endpoint)
end

it 'logs the request' do
result
expect(analytics).to have_logged_event(
:passport_api_health_check,
success: false,
error: /Faraday::ServerError/,
)
end

describe 'the #fetch result' do
it 'does not succeed' do
expect(result).not_to be_success
end
end
end
end
end
36 changes: 36 additions & 0 deletions spec/services/doc_auth/dos/responses/health_check_failure_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
require 'rails_helper'

RSpec.describe DocAuth::Dos::Responses::HealthCheckFailure do
subject(:health_check_result) { described_class.new(faraday_error) }

let(:health_check_endpoint) do
IdentityConfig.store.passports_api_health_check_endpoint
end

def make_faraday_error(status:)
stub_request(:get, health_check_endpoint).to_return(status:)
begin
Faraday.get(health_check_endpoint)
rescue FaradayError => faraday_error
faraday_error
end
end

[403, 404, 500].each do |http_status|
context "when initialized from an HTTP #{http_status} error" do
let(:faraday_error) { make_faraday_error(status: http_status) }

it 'is not successful' do
expect(health_check_result).not_to be_success
end

it 'has the correct errors hash' do
expect(health_check_result.errors).to eq({ network: http_status })
end

it 'has the faraday exception' do
expect(health_check_result.exception).to eq(faraday_error)
end
end
end
end
Loading