Skip to content

Commit c3774ac

Browse files
authored
TBE-241: make mef requests async and add async webhook callbacks, and adjust tests to match new behavior (#52)
1 parent 076e25a commit c3774ac

29 files changed

+524
-91
lines changed

Gemfile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ gem "puma", ">= 5.0"
1515
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
1616
gem "tzinfo-data", platforms: %i[windows jruby]
1717

18+
gem "solid_queue"
19+
1820
# Reduces boot times through caching; required in config/boot.rb
1921
gem "bootsnap", require: false
2022

@@ -33,17 +35,15 @@ gem "jwt"
3335
gem "aws-sdk-s3"
3436
gem "aws-sdk-secretsmanager"
3537
gem "open3"
38+
gem "faraday"
3639

3740
group :development, :test do
3841
gem "dotenv-rails"
39-
4042
gem "rspec-rails", "~> 8.0"
41-
4243
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
4344
gem "debug", platforms: %i[mri windows], require: "debug/prelude"
44-
4545
# Static analysis for security vulnerabilities [https://brakemanscanner.org/]
4646
gem "brakeman", require: false
47-
4847
gem "standard", require: false
48+
gem "webmock"
4949
end

Gemfile.lock

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ GEM
7272
securerandom (>= 0.3)
7373
tzinfo (~> 2.0, >= 2.0.5)
7474
uri (>= 0.13.1)
75+
addressable (2.8.7)
76+
public_suffix (>= 2.0.2, < 7.0)
7577
ast (2.4.3)
7678
aws-eventstream (1.4.0)
7779
aws-partitions (1.1156.0)
@@ -105,6 +107,9 @@ GEM
105107
builder (3.3.0)
106108
concurrent-ruby (1.3.5)
107109
connection_pool (2.5.4)
110+
crack (1.0.0)
111+
bigdecimal
112+
rexml
108113
crass (1.0.6)
109114
date (3.4.1)
110115
debug (1.11.0)
@@ -118,8 +123,20 @@ GEM
118123
drb (2.2.3)
119124
erb (5.0.2)
120125
erubi (1.13.1)
126+
et-orbi (1.3.0)
127+
tzinfo
128+
faraday (2.13.4)
129+
faraday-net_http (>= 2.0, < 3.5)
130+
json
131+
logger
132+
faraday-net_http (3.4.1)
133+
net-http (>= 0.5.0)
134+
fugit (1.11.2)
135+
et-orbi (~> 1, >= 1.2.11)
136+
raabro (~> 1.4)
121137
globalid (1.2.1)
122138
activesupport (>= 6.1)
139+
hashdiff (1.2.0)
123140
i18n (1.14.7)
124141
concurrent-ruby (~> 1.0)
125142
io-console (0.8.1)
@@ -146,6 +163,8 @@ GEM
146163
mini_mime (1.1.5)
147164
minitest (5.25.5)
148165
msgpack (1.8.0)
166+
net-http (0.6.0)
167+
uri
149168
net-imap (0.5.10)
150169
date
151170
net-protocol
@@ -191,8 +210,10 @@ GEM
191210
psych (5.2.6)
192211
date
193212
stringio
213+
public_suffix (6.0.2)
194214
puma (7.0.3)
195215
nio4r (~> 2.0)
216+
raabro (1.4.0)
196217
racc (1.8.1)
197218
rack (3.2.1)
198219
rack-session (2.1.1)
@@ -239,6 +260,7 @@ GEM
239260
regexp_parser (2.11.2)
240261
reline (0.6.2)
241262
io-console (~> 0.5)
263+
rexml (3.4.2)
242264
rspec-core (3.13.5)
243265
rspec-support (~> 3.13.0)
244266
rspec-expectations (3.13.5)
@@ -277,6 +299,13 @@ GEM
277299
ruby-progressbar (1.13.0)
278300
rubyzip (3.1.0)
279301
securerandom (0.4.1)
302+
solid_queue (1.2.1)
303+
activejob (>= 7.1)
304+
activerecord (>= 7.1)
305+
concurrent-ruby (>= 1.3.1)
306+
fugit (~> 1.11.0)
307+
railties (>= 7.1)
308+
thor (>= 1.3.1)
280309
standard (1.51.1)
281310
language_server-protocol (~> 3.17.0.2)
282311
lint_roller (~> 1.0)
@@ -304,6 +333,10 @@ GEM
304333
unicode-emoji (4.1.0)
305334
uri (1.0.3)
306335
useragent (0.16.11)
336+
webmock (3.25.1)
337+
addressable (>= 2.8.0)
338+
crack (>= 0.3.2)
339+
hashdiff (>= 0.4.0, < 2.0.0)
307340
websocket-driver (0.8.0)
308341
base64
309342
websocket-extensions (>= 0.1.0)
@@ -329,6 +362,7 @@ DEPENDENCIES
329362
brakeman
330363
debug
331364
dotenv-rails
365+
faraday
332366
jwt
333367
nokogiri
334368
open3
@@ -337,9 +371,11 @@ DEPENDENCIES
337371
rails (~> 8.0.2)
338372
rspec-rails (~> 8.0)
339373
rubyzip
374+
solid_queue
340375
standard
341376
thruster
342377
tzinfo-data
378+
webmock
343379

344380
BUNDLED WITH
345381
2.6.9

Rakefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
33

44
require_relative "config/application"
5+
require "standard/rake"
56

67
Rails.application.load_tasks

app/controllers/api/v0/base_controller.rb

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
class Api::V0::BaseController < ApplicationController
2+
before_action :generate_api_request_id
23
before_action :verify_client_name_and_signature
3-
rescue_from MefService::RetryableError, with: :retryable_mef_error
4+
45
rescue_from ActionController::ParameterMissing, with: :showable_error
56
rescue_from Aws::SecretsManager::Errors::ServiceError, with: :aws_error
67
rescue_from JWT::VerificationError, with: :jwt_error
78

8-
def retryable_mef_error(exception)
9-
Rails.logger.error("Encountered retryable error while contacting MeF: #{exception}")
10-
render json: "Error contacting MeF, please try again", status: :bad_gateway
9+
# A random API Request ID is returned to the client synchronously and also included in all webhook requests.
10+
# This is so that the client can correspond a webhook callback to the originating API request.
11+
# This pattern requires that clients persist records of all API calls that so that they can look them up by request ID
12+
# upon receiving a webhook callback and take appropriate steps to resolve whatever action initiated the API request.
13+
attr_reader :api_request_id
14+
def generate_api_request_id
15+
@api_request_id = SecureRandom.uuid
1116
end
1217

1318
def showable_error(exception)
@@ -29,7 +34,7 @@ def verify_client_name_and_signature
2934
authorization_header = request.headers["HTTP_AUTHORIZATION"]
3035
token = JWT::EncodedToken.new(authorization_header.delete_prefix("Bearer "))
3136

32-
client_credentials = get_api_client_mef_credentials
37+
client_credentials = MefService.get_mef_credentials(api_client_name)
3338
client_public_key_base64 = client_credentials[:efiler_api_public_key]
3439
client_public_key = OpenSSL::PKey::RSA.new(Base64.decode64(client_public_key_base64))
3540
token.verify_signature!(algorithm: "RS256", key: client_public_key)
@@ -40,11 +45,4 @@ def api_client_name
4045
token = JWT::EncodedToken.new(authorization_header.delete_prefix("Bearer "))
4146
token.unverified_payload["iss"]
4247
end
43-
44-
def get_api_client_mef_credentials
45-
aws_client = Aws::SecretsManager::Client.new
46-
environment = Rails.env.production? ? "production" : "demo"
47-
response = aws_client.get_secret_value(secret_id: "efiler-api/#{environment}/efiler-api-client-credentials/#{api_client_name}")
48-
JSON.parse(response.secret_string).transform_keys { |k| k.to_sym }
49-
end
5048
end

app/controllers/api/v0/efile_controller.rb

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,30 @@ module Api::V0
22
class EfileController < BaseController
33
def submit
44
submission_bundle = params.expect(:submission_bundle)
5-
6-
submission_filename = submission_bundle.original_filename
7-
result = Dir.mktmpdir do |dir|
8-
submission_path = File.join(dir, submission_filename)
9-
FileUtils.mv submission_bundle.tempfile.path, submission_path
10-
MefService.run_efiler_command(get_api_client_mef_credentials, "submit", submission_path)
11-
end
12-
13-
doc = Nokogiri::XML(result)
14-
if doc.css("SubmissionReceiptList SubmissionReceiptGrp SubmissionId").text.strip == File.basename(submission_filename, ".zip")
15-
render json: {status: "transmitted", result: result}, status: :created
16-
else
17-
render json: {status: "failed", result: result}, status: :bad_request
18-
end
5+
base64_encoded_submission_bundle = Base64.strict_encode64(submission_bundle.read)
6+
webhook_url = CGI.unescape(params.expect(:webhook_url))
7+
Mef::SubmitJob.perform_later(
8+
api_request_id,
9+
webhook_url,
10+
api_client_name,
11+
submission_bundle.original_filename,
12+
base64_encoded_submission_bundle
13+
)
14+
render json: {api_request_id:}
1915
end
2016

2117
def submissions_status
2218
submission_ids = params.expect(id: [])
23-
response = MefService.run_efiler_command(get_api_client_mef_credentials, "submissions-status", *submission_ids)
24-
render json: Mef::SubmissionsStatus.handle_submission_status_response(response)
19+
webhook_url = CGI.unescape(params.expect(:webhook_url))
20+
Mef::SubmissionsStatusJob.perform_later(api_request_id, webhook_url, api_client_name, submission_ids)
21+
render json: {api_request_id:}
2522
end
2623

2724
def acks
2825
submission_ids = params.expect(id: [])
29-
response = MefService.run_efiler_command(get_api_client_mef_credentials, "acks", *submission_ids)
30-
render json: Mef::Acks.handle_ack_response(response)
26+
webhook_url = CGI.unescape(params.expect(:webhook_url))
27+
Mef::AcksJob.perform_later(api_request_id, webhook_url, api_client_name, submission_ids)
28+
render json: {api_request_id:}
3129
end
3230
end
3331
end

app/jobs/mef/acks_job.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module Mef
2+
class AcksJob < MefJob
3+
def perform_mef_request(submission_ids)
4+
mef_response = MefService.run_efiler_command(mef_credentials, "acks", *submission_ids)
5+
parsed_response = Mef::Acks.parse_acks_response(mef_response)
6+
WebhookCallbackJob.perform_later(api_request_id, webhook_url, {result: parsed_response})
7+
end
8+
end
9+
end

app/jobs/mef/mef_job.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
module Mef
2+
class MefJob < ApplicationJob
3+
attr_accessor :webhook_url, :api_request_id, :mef_credentials
4+
5+
retry_on MefService::RetryableError
6+
7+
after_discard do |job, exception|
8+
log_and_respond_with_error(job, exception)
9+
end
10+
11+
def log_and_respond_with_error(job, exception)
12+
Rails.logger.error("MefJob #{job} has been discarded due to #{exception}")
13+
WebhookCallbackJob.perform_later(
14+
api_request_id,
15+
webhook_url,
16+
{error: exception.detailed_message}
17+
)
18+
end
19+
20+
def perform(api_request_id, webhook_url, api_client_name, *args)
21+
self.api_request_id = api_request_id
22+
self.webhook_url = webhook_url
23+
self.mef_credentials = MefService.get_mef_credentials(api_client_name)
24+
25+
perform_mef_request(*args)
26+
end
27+
end
28+
end
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module Mef
2+
class SubmissionsStatusJob < MefJob
3+
def perform_mef_request(submission_ids)
4+
mef_response = MefService.run_efiler_command(mef_credentials, "submissions-status", *submission_ids)
5+
parsed_response = Mef::SubmissionsStatus.parse_submissions_status_response(mef_response)
6+
WebhookCallbackJob.perform_later(api_request_id, webhook_url, {result: parsed_response})
7+
end
8+
end
9+
end

app/jobs/mef/submit_job.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module Mef
2+
class SubmitJob < MefJob
3+
def perform_mef_request(submission_bundle_filename, submission_bundle_contents_base64)
4+
mef_response = Mef::Submit.send_submission_bundle(
5+
mef_credentials,
6+
submission_bundle_filename,
7+
submission_bundle_contents_base64
8+
)
9+
10+
if Mef::Submit.transmitted?(mef_response, submission_bundle_filename)
11+
WebhookCallbackJob.perform_later(api_request_id, webhook_url, {status: "transmitted", result: mef_response})
12+
else
13+
WebhookCallbackJob.perform_later(api_request_id, webhook_url, {status: "failed", result: mef_response})
14+
end
15+
end
16+
end
17+
end

app/jobs/webhook_callback_job.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
class WebhookCallbackJob < ApplicationJob
2+
def perform(api_request_id, webhook_url, payload)
3+
payload_with_api_request_id = payload.merge({api_request_id:})
4+
uri = URI.parse(webhook_url)
5+
conn = Faraday.new(url: "#{uri.scheme}://#{uri.host}", headers: {"Content-Type" => "application/json"})
6+
conn.post(uri.path) do |req|
7+
req.body = payload_with_api_request_id.to_json
8+
end
9+
end
10+
end

0 commit comments

Comments
 (0)