Skip to content

Commit 609f645

Browse files
authored
Remediation failure handling (#1843)
* Job to handle when remediation fails by creating a ticket in LibAnswers and storing the failed_at time on the file resource. * Adds tests * More tests * Moved the timestamp storing step when failed out of the job and in the two fail outcomes in the controller. I think it makes sense to store the timestamp outside of the jobs since the job is run async, so the time might not be at the same time the request come back from the API. * Made the test more realistic
1 parent ea7cb82 commit 609f645

File tree

10 files changed

+132
-6
lines changed

10 files changed

+132
-6
lines changed

app/controllers/webhooks/pdf_accessibility_api_controller.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,14 @@ def handle_success(job_data)
2525
BuildAutoRemediatedWorkVersionJob.perform_later(job_data[:uuid], job_data[:output_url])
2626
render json: { message: 'Update successful' }, status: :ok
2727
rescue StandardError => e
28+
store_failure(job_data[:uuid])
2829
render json: { error: e.message }, status: :internal_server_error
2930
end
3031

3132
def handle_failure(job_data)
3233
Rails.logger.error("Auto-remediation job failed: #{job_data[:processing_error_message]}")
34+
AutoRemediationFailedJob.perform_later(job_data[:uuid])
35+
store_failure(job_data[:uuid])
3336
render json: { message: job_data[:processing_error_message] }, status: :ok
3437
end
3538

@@ -45,4 +48,9 @@ def authenticate_request
4548

4649
head(:unauthorized) unless request.headers['X-API-KEY'] == ENV['PDF_REMEDIATION_WEBHOOK_SECRET']
4750
end
51+
52+
def store_failure(job_uuid)
53+
file_resource = FileResource.find_by(remediation_job_uuid: job_uuid)
54+
file_resource.update(auto_remediation_failed_at: Time.current)
55+
end
4856
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# frozen_string_literal: true
2+
3+
class AutoRemediationFailedJob < ApplicationJob
4+
queue_as :default
5+
6+
def perform(job_uuid)
7+
work = FileResource.find_by!(remediation_job_uuid: job_uuid)
8+
.work_versions
9+
&.first
10+
&.work
11+
12+
LibanswersApiService.new
13+
.admin_create_ticket(work.id,
14+
'work_remediation_failed')
15+
end
16+
end

app/services/libanswers_api_service.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def get_depositor(id, type)
4343
'work_curation' => Work,
4444
'work_accessibility_check' => Work,
4545
'work_remediation' => Work,
46+
'work_remediation_failed' => Work,
4647
'collection' => Collection
4748
}
4849
deposit = deposit_types[type].find(id)
@@ -60,7 +61,7 @@ def get_admin_subject(id, type)
6061
when 'work_accessibility_check'
6162
work = Work.find(id)
6263
"ScholarSphere Deposit Accessibility Curation: #{work.latest_version.title}"
63-
when 'work_remediation'
64+
when 'work_remediation', 'work_remediation_failed'
6465
work = Work.find(id)
6566
"ScholarSphere PDF Auto-remediation Result: #{work.latest_version.title}"
6667
end
@@ -87,12 +88,17 @@ def get_ticket_details(id, type, admin_subject, base_url = '')
8788
(accessibility_check_results.empty? ? '' : "pdetails=#{accessibility_check_results}&") +
8889
"pname=#{work.display_name}&" +
8990
"pemail=#{work.email}"
90-
when 'work_remediation'
91+
when 'work_remediation', 'work_remediation_failed'
9192
@work = Work.find(id)
9293
"quid=#{ACCESSIBILITY_QUEUE_ID}&" +
9394
"pquestion=#{admin_subject}&" +
9495
"pname=#{work.display_name}&" +
95-
"pemail=#{work.email}"
96+
"pemail=#{work.email}" +
97+
(if type == 'work_remediation_failed'
98+
'&pdetails=A PDF associated with this work failed to auto-remediate and requires manual review.'
99+
else
100+
''
101+
end)
96102
end
97103
end
98104

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class AddAutoRemediationFailedAtToFileResource < ActiveRecord::Migration[7.2]
2+
def change
3+
add_column :file_resources, :auto_remediation_failed_at, :datetime
4+
end
5+
end

db/schema.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
#
1111
# It's strongly recommended that you check this file into your version control system.
1212

13-
ActiveRecord::Schema[7.2].define(version: 2025_10_07_200037) do
13+
ActiveRecord::Schema[7.2].define(version: 2025_10_30_184453) do
1414
# These are extensions that must be enabled in order to support this database
1515
enable_extension "pg_stat_statements"
1616
enable_extension "plpgsql"
@@ -155,6 +155,7 @@
155155
t.uuid "uuid", default: -> { "uuid_generate_v4()" }
156156
t.string "remediation_job_uuid"
157157
t.boolean "auto_remediated_version", default: false, null: false
158+
t.datetime "auto_remediation_failed_at"
158159
end
159160

160161
create_table "file_version_memberships", force: :cascade do |t|

spec/controllers/webhooks/pdf_accessibility_api_controller_spec.rb

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
@original_secret = ENV['PDF_REMEDIATION_WEBHOOK_SECRET']
1818
ENV['PDF_REMEDIATION_WEBHOOK_SECRET'] = secret
1919
allow(BuildAutoRemediatedWorkVersionJob).to receive(:perform_later).and_return(nil)
20+
allow(AutoRemediationFailedJob).to receive(:perform_later).and_return(nil)
2021
end
2122

2223
after do
@@ -70,6 +71,21 @@
7071
expect(BuildAutoRemediatedWorkVersionJob).to have_received(:perform_later)
7172
.with(file.remediation_job_uuid, 'https://example.com/out.pdf')
7273
end
74+
75+
context 'when an error occurs enqueuing the job' do
76+
before do
77+
allow(BuildAutoRemediatedWorkVersionJob).to receive(:perform_later).and_raise(StandardError.new('Redis error'))
78+
allow(Rails.logger).to receive(:error)
79+
end
80+
81+
it 'stores the failure timestamp and returns 500 with the error message' do
82+
post :create, params: params, as: :json
83+
84+
expect(response).to have_http_status(:internal_server_error)
85+
expect(response.parsed_body).to include('error' => 'Redis error')
86+
expect(file.reload.auto_remediation_failed_at).not_to be_nil
87+
end
88+
end
7389
end
7490

7591
describe 'job.failed handling' do
@@ -82,13 +98,15 @@
8298
allow(Rails.logger).to receive(:error)
8399
end
84100

85-
it 'logs the failure and returns 200 with the processing message' do
101+
it 'logs the failure, enqueues the failed job, stores the failure timestamp and returns 200 with the processing message' do
86102
post :create, params: params, as: :json
87103

88104
expect(response).to have_http_status(:ok)
89105
expect(response.parsed_body).to include('message' => error_message)
90106
expect(Rails.logger).to have_received(:error)
91107
.with("Auto-remediation job failed: #{error_message}")
108+
expect(AutoRemediationFailedJob).to have_received(:perform_later).with(file.remediation_job_uuid)
109+
expect(file.reload.auto_remediation_failed_at).not_to be_nil
92110
end
93111
end
94112
end
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe AutoRemediationFailedJob do
6+
describe '#perform' do
7+
let(:existing_job_uuid) { SecureRandom.uuid }
8+
let!(:file_resource) do
9+
create(:file_resource,
10+
remediation_job_uuid: existing_job_uuid,
11+
work_versions: [create(:work_version)])
12+
end
13+
let(:work) { file_resource.work_versions.first.work }
14+
let(:service) do
15+
instance_double(LibanswersApiService,
16+
admin_create_ticket: 'https://psu.libanswers.com/fake-ticket')
17+
end
18+
19+
before do
20+
allow(LibanswersApiService).to receive(:new).and_return(service)
21+
end
22+
23+
context 'when the file resource with the given remediation_job_uuid exists' do
24+
it 'creates a LibAnswers ticket' do
25+
described_class.perform_now(existing_job_uuid)
26+
27+
expect(LibanswersApiService).to have_received(:new)
28+
expect(service).to have_received(:admin_create_ticket).with(work.id, 'work_remediation_failed')
29+
end
30+
end
31+
32+
context 'when the file resource with the given remediation_job_uuid does not exist' do
33+
it 'raises ActiveRecord::RecordNotFound' do
34+
expect {
35+
described_class.perform_now('nonexistent-uuid')
36+
}.to raise_error(ActiveRecord::RecordNotFound)
37+
end
38+
end
39+
end
40+
end

spec/models/file_resource_spec.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
it { is_expected.to have_db_column(:file_data).of_type(:jsonb) }
1818
it { is_expected.to have_db_column(:remediation_job_uuid).of_type(:string) }
1919
it { is_expected.to have_db_column(:auto_remediated_version).of_type(:boolean) }
20+
it { is_expected.to have_db_column(:auto_remediation_failed_at).of_type(:datetime) }
2021
end
2122

2223
describe 'factories' do
@@ -269,6 +270,7 @@
269270
thumbnail_url_ssi
270271
remediation_job_uuid_tesim
271272
auto_remediated_version_tesim
273+
auto_remediation_failed_at_dtsi
272274
)
273275
end
274276

spec/services/libanswers_api_service_spec.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,36 @@
184184
end
185185
end
186186
end
187+
188+
context 'when the ticket is a Work Remediation failed ticket' do
189+
it 'uses id 2590 for quid and ScholarSphere PDF Auto-remediation Result for question but includes details' do
190+
described_class.new.admin_create_ticket(work.id, 'work_remediation_failed')
191+
expect(mock_faraday_connection).to have_received(:post).with(
192+
'/api/1.1/ticket/create',
193+
"quid=#{accessibility_quid}&pquestion=ScholarSphere PDF Auto-remediation Result: #{
194+
work.latest_version.title}&pname=#{work.display_name}&pemail=#{work.email}" \
195+
'&pdetails=A PDF associated with this work failed to auto-remediate and requires manual review.'
196+
)
197+
end
198+
199+
context 'when the user is not an active member' do
200+
let!(:inactive_member) { object_double(PsuIdentity::SearchService::Person.new, affiliation: ['MEMBER']) }
201+
202+
before do
203+
allow(mock_identity_search).to receive(:userid).and_return(inactive_member)
204+
end
205+
206+
it 'still creates a ticket' do
207+
described_class.new.admin_create_ticket(work.id, 'work_remediation_failed')
208+
expect(mock_faraday_connection).to have_received(:post).with(
209+
'/api/1.1/ticket/create',
210+
"quid=#{accessibility_quid}&pquestion=ScholarSphere PDF Auto-remediation Result: #{
211+
work.latest_version.title}&pname=#{work.display_name}&pemail=#{work.email}" \
212+
'&pdetails=A PDF associated with this work failed to auto-remediate and requires manual review.'
213+
)
214+
end
215+
end
216+
end
187217
end
188218
end
189219

spec/support/vcr.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
WebMock.allow_net_connect!(net_http_connect_on_start: true)
1010

1111
# Allow connections to Docker images and webriver update urls
12-
allowed_hosts = %w(selenium minio solr)
12+
allowed_hosts = %w(selenium minio solr docker.for.mac.localhost)
1313

1414
VCR.configure do |c|
1515
c.cassette_library_dir = 'spec/fixtures/vcr_cassettes'

0 commit comments

Comments
 (0)