From e4cdb22c27c974dc8acaa5d3e2869c5ab3d7fb47 Mon Sep 17 00:00:00 2001 From: Brandon Cooper Date: Mon, 25 Nov 2024 13:11:30 -0500 Subject: [PATCH] [10-10CG] Add ability to retry schema validation (#19571) * add ability to retry schema validation * rename some spec for clarity * Add log for successful validation call --- .../caregivers_assistance_claim.rb | 42 ++++++ config/features.yml | 3 + .../caregivers_assistance_claim_spec.rb | 135 ++++++++++++++++++ 3 files changed, 180 insertions(+) diff --git a/app/models/saved_claim/caregivers_assistance_claim.rb b/app/models/saved_claim/caregivers_assistance_claim.rb index f21b0a0a783..65b5dc6e222 100644 --- a/app/models/saved_claim/caregivers_assistance_claim.rb +++ b/app/models/saved_claim/caregivers_assistance_claim.rb @@ -33,6 +33,24 @@ def to_pdf(filename = nil, **) raise end + def form_matches_schema + super unless Flipper.enabled?(:caregiver_retry_form_validation) + + return unless form_is_string + + schema = VetsJsonSchema::SCHEMAS[self.class::FORM] + validation_errors = validate_form_with_retries(schema) + + validation_errors.each do |e| + errors.add(e[:fragment], e[:message]) + e[:errors]&.flatten(2)&.each { |nested| errors.add(nested[:fragment], nested[:message]) if nested.is_a? Hash } + end + + unless validation_errors.empty? + Rails.logger.error('SavedClaim form did not pass validation', { guid:, errors: validation_errors }) + end + end + # SavedClaims require regional_office to be defined, CaregiversAssistanceClaim has no purpose for it. # # CaregiversAssistanceClaims are not processed regional VA offices. @@ -75,4 +93,28 @@ def destroy_attachment Form1010cg::Attachment.find_by(guid: parsed_form['poaAttachmentId'])&.destroy! end + + def validate_form_with_retries(schema) + attempts = 0 + max_attempts = 3 + + begin + attempts += 1 + errors_array = JSON::Validator.fully_validate(schema, parsed_form, { errors_as_objects: true }) + Rails.logger.info("Form validation succeeded on attempt #{attempts}/#{max_attempts}") if attempts > 1 + errors_array + rescue => e + if attempts <= max_attempts + Rails.logger.warn("Retrying form validation due to error: #{e.message} (Attempt #{attempts}/#{max_attempts})") + sleep(1) # Delay 1 second in between attempts + retry + else + PersonalInformationLog.create(data: { schema:, parsed_form:, params: { errors_as_objects: true } }, + error_class: 'SavedClaim FormValidationError') + Rails.logger.error('Error during form validation after maximimum retries', + { error: e.message, backtrace: e.backtrace, schema: }) + raise + end + end + end end diff --git a/config/features.yml b/config/features.yml index fba8c0b8e2a..fdeee335e78 100644 --- a/config/features.yml +++ b/config/features.yml @@ -80,6 +80,9 @@ features: actor_type: user description: Send 10-10CG submission failure email to Veteran using VANotify. enable_in_development: true + caregiver_retry_form_validation: + actor_type: user + description: Enables 1010CG to retry schema validation hca_browser_monitoring_enabled: actor_type: user description: Enables browser monitoring for the health care application. diff --git a/spec/models/saved_claim/caregivers_assistance_claim_spec.rb b/spec/models/saved_claim/caregivers_assistance_claim_spec.rb index 8f31817c321..131cdd68d64 100644 --- a/spec/models/saved_claim/caregivers_assistance_claim_spec.rb +++ b/spec/models/saved_claim/caregivers_assistance_claim_spec.rb @@ -95,6 +95,141 @@ end end + describe 'validations' do + let(:claim) { build(:caregivers_assistance_claim) } + + before do + allow(Flipper).to receive(:enabled?).and_call_original + end + + context 'caregiver_retry_form_validation disabled' do + before do + allow(Flipper).to receive(:enabled?).with(:caregiver_retry_form_validation).and_return(false) + end + + context 'no validation errors' do + before do + allow(JSON::Validator).to receive(:fully_validate).and_return([]) + end + + it 'returns true' do + expect(claim.validate).to eq true + end + end + + context 'validation errors' do + it 'calls the parent method when the toggle is off' do + allow(claim).to receive(:form_matches_schema).and_call_original + + claim.validate + + expect(claim).to have_received(:form_matches_schema) + end + end + end + + context 'caregiver_retry_form_validation enabled' do + before do + allow(Flipper).to receive(:enabled?).with(:caregiver_retry_form_validation).and_return(true) + end + + context 'no validation errors' do + before do + allow(JSON::Validator).to receive(:fully_validate).and_return([]) + end + + it 'returns true' do + expect(Rails.logger).not_to receive(:info) + .with('Form validation succeeded on attempt 1/3') + + expect(claim.validate).to eq true + end + end + + context 'validation errors' do + let(:schema_errors) { [{ fragment: 'error' }] } + + context 'when JSON:Validator.fully_validate returns errors' do + before do + allow(JSON::Validator).to receive(:fully_validate).and_return(schema_errors) + end + + it 'adds validation errors to the form' do + expect(JSON::Validator).not_to receive(:fully_validate_schema) + + expect(Rails.logger).not_to receive(:info) + .with('Form validation succeeded on attempt 1/3') + + claim.validate + expect(claim.errors.full_messages).not_to be_empty + end + end + + context 'when JSON:Validator.fully_validate throws an exception' do + let(:exception_text) { 'Some exception' } + let(:exception) { StandardError.new(exception_text) } + + context '3 times' do + let(:schema) { 'schema_content' } + + before do + allow(VetsJsonSchema::SCHEMAS).to receive(:[]).and_return(schema) + allow(JSON::Validator).to receive(:fully_validate).and_raise(exception) + end + + it 'logs exceptions and raises exception' do + expect(Rails.logger).to receive(:warn) + .with("Retrying form validation due to error: #{exception_text} (Attempt 1/3)").once + expect(Rails.logger).not_to receive(:info) + .with('Form validation succeeded on attempt 1/3') + expect(Rails.logger).to receive(:warn) + .with("Retrying form validation due to error: #{exception_text} (Attempt 2/3)").once + expect(Rails.logger).to receive(:warn) + .with("Retrying form validation due to error: #{exception_text} (Attempt 3/3)").once + + expect(Rails.logger).to receive(:error) + .with('Error during form validation after maximimum retries', { error: exception.message, + backtrace: anything, schema: }) + + expect(PersonalInformationLog).to receive(:create).with( + data: { schema: schema, + parsed_form: claim.parsed_form, + params: { errors_as_objects: true } }, + error_class: 'SavedClaim FormValidationError' + ) + + expect { claim.validate }.to raise_error(exception.class, exception.message) + end + end + + context '1 time but succeeds after retrying' do + before do + # Throws exception the first time, returns empty array on subsequent calls + call_count = 0 + allow(JSON::Validator).to receive(:fully_validate).and_wrap_original do + call_count += 1 + if call_count == 1 + raise exception + else + [] + end + end + end + + it 'logs exception and validates succesfully after the retry' do + expect(Rails.logger).to receive(:warn) + .with("Retrying form validation due to error: #{exception_text} (Attempt 1/3)").once + expect(Rails.logger).to receive(:info) + .with('Form validation succeeded on attempt 2/3').once + + expect(claim.validate).to eq true + end + end + end + end + end + end + describe '#process_attachments!' do it 'raises a NotImplementedError' do expect { subject.process_attachments! }.to raise_error(NotImplementedError)