From 81f356c9af3230819098ad74c63fa37739774e5b Mon Sep 17 00:00:00 2001 From: Peri-Ann McLaren <141954992+pmclaren19@users.noreply.github.com> Date: Tue, 4 Feb 2025 13:16:27 -0700 Subject: [PATCH] PR #4 for silent failure work - Add lighthouse files and changes for uploading a document (#20453) --- .github/CODEOWNERS | 10 +- app/sidekiq/lighthouse/document_upload.rb | 99 --- .../evidence_submissions/document_upload.rb | 179 +++++ .../lighthouse/failure_notification.rb | 5 +- lib/lighthouse/benefits_documents/service.rb | 69 +- .../mobile/v0/claim/documents_spec.rb | 703 +++++++++++++----- .../benefits_documents/service_spec.rb | 80 +- spec/sidekiq/evss/document_upload_spec.rb | 6 +- .../lighthouse/document_upload_spec.rb | 75 -- .../document_upload_spec.rb | 220 ++++++ .../lighthouse/failure_notification_spec.rb | 6 +- 11 files changed, 1054 insertions(+), 398 deletions(-) delete mode 100644 app/sidekiq/lighthouse/document_upload.rb create mode 100644 app/sidekiq/lighthouse/evidence_submissions/document_upload.rb delete mode 100644 spec/sidekiq/lighthouse/document_upload_spec.rb create mode 100644 spec/sidekiq/lighthouse/evidence_submissions/document_upload_spec.rb diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f827cd09c3e..185020a9e90 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -664,6 +664,7 @@ app/sidekiq/kms_key_rotation @department-of-veterans-affairs/va-api-engineers @d app/sidekiq/lighthouse @department-of-veterans-affairs/backend-review-group app/sidekiq/lighthouse/submit_career_counseling_job.rb @department-of-veterans-affairs/my-education-benefits @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/sidekiq/lighthouse/create_intent_to_file_job.rb @department-of-veterans-affairs/pension-and-burials @department-of-veterans-affairs/backend-review-group +app/sidekiq/lighthouse/evidence_submissions/document_upload.rb @department-of-veterans-affairs/benefits-management-tools-be @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/va-api-engineers app/sidekiq/lighthouse/income_and_assets_intake_job.rb @department-of-veterans-affairs/pension-and-burials @department-of-veterans-affairs/backend-review-group app/sidekiq/lighthouse/failure_notification.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/benefits-management-tools-be app/sidekiq/load_average_days_for_claim_completion_job.rb @department-of-veterans-affairs/benefits-microservices @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -952,6 +953,9 @@ lib/lighthouse @department-of-veterans-affairs/backend-review-group lib/lighthouse/benefits_claims @department-of-veterans-affairs/disability-experience @department-of-veterans-affairs/benefits-management-tools-be @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group lib/lighthouse/benefits_claims/monitor.rb @department-of-veterans-affairs/pension-and-burials @department-of-veterans-affairs/backend-review-group lib/lighthouse/benefits_documents/constants.rb @department-of-veterans-affairs/benefits-management-tools-be @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +lib/lighthouse/benefits_documents/service.rb @department-of-veterans-affairs/benefits-management-tools-be @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +lib/lighthouse/benefits_documents/update_documents_status_service.rb @department-of-veterans-affairs/benefits-management-tools-be @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/va-api-engineers +lib/lighthouse/benefits_documents/upload_status_updater.rb @department-of-veterans-affairs/benefits-management-tools-be @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/va-api-engineers lib/lighthouse/benefits_documents/utilities/helpers.rb @department-of-veterans-affairs/benefits-management-tools-be @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group lib/lighthouse/benefits_intake @department-of-veterans-affairs/pension-and-burials @department-of-veterans-affairs/backend-review-group lib/lighthouse/letters_generator @department-of-veterans-affairs/benefits-management-tools-be @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -1058,6 +1062,7 @@ modules/ivc_champva @department-of-veterans-affairs/champva-engineering @departm modules/meb_api @department-of-veterans-affairs/my-education-benefits modules/mobile @department-of-veterans-affairs/mobile-api-team modules/mobile/app/assets/translations @department-of-veterans-affairs/flagship-mobile-content @department-of-veterans-affairs/backend-review-group +modules/mobile/spec/requests/mobile/v0/claim/documents_spec.rb @department-of-veterans-affairs/benefits-management-tools-be @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group modules/mocked_authentication @department-of-veterans-affairs/octo-identity modules/my_health @department-of-veterans-affairs/vfs-mhv-secure-messaging @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group modules/my_health/app/controllers/my_health/v1/prescriptions_controller.rb @department-of-veterans-affairs/vfs-mhv-medications @department-of-veterans-affairs/backend-review-group @@ -1403,6 +1408,7 @@ spec/sidekiq/income_limits @department-of-veterans-affairs/vfs-public-websites-f spec/sidekiq/in_progress_form_cleaner_spec.rb @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/sidekiq/kms_key_rotation @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/sidekiq/lighthouse @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +spec/sidekiq/lighthouse/evidence_submissions/document_upload_spec.rb @department-of-veterans-affairs/benefits-management-tools-be @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/sidekiq/lighthouse/failure_notification_spec.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/benefits-management-tools-be spec/sidekiq/load_average_days_for_claim_completion_job_spec.rb @department-of-veterans-affairs/benefits-microservices @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/sidekiq/mhv @department-of-veterans-affairs/vfs-mhv-medical-records @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -1500,7 +1506,9 @@ spec/lib/lighthouse @department-of-veterans-affairs/backend-review-group spec/lib/lighthouse/auth @department-of-veterans-affairs/benefits-management-tools-be @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/lighthouse/benefits_claims @department-of-veterans-affairs/disability-experience @department-of-veterans-affairs/benefits-management-tools-be @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/lighthouse/benefits_documents @department-of-veterans-affairs/backend-review-group -spec/lib/lighthouse/benefits_documents/service_spec.rb @department-of-veterans-affairs/backend-review-group +spec/lib/lighthouse/benefits_documents/service_spec.rb @department-of-veterans-affairs/benefits-management-tools-be @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +spec/lib/lighthouse/benefits_documents/update_documents_status_service_spec.rb @department-of-veterans-affairs/benefits-management-tools-be @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/va-api-engineers +spec/lib/lighthouse/benefits_documents/upload_status_updater_spec.rb @department-of-veterans-affairs/benefits-management-tools-be @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/va-api-engineers spec/lib/lighthouse/benefits_intake @department-of-veterans-affairs/pension-and-burials @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/lighthouse/direct_deposit @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/dbex-trex @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/lighthouse/direct_deposit/payment_account_spec.rb @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group diff --git a/app/sidekiq/lighthouse/document_upload.rb b/app/sidekiq/lighthouse/document_upload.rb deleted file mode 100644 index 4ac67ff7e1c..00000000000 --- a/app/sidekiq/lighthouse/document_upload.rb +++ /dev/null @@ -1,99 +0,0 @@ -# frozen_string_literal: true - -require 'datadog' -require 'timeout' -require 'lighthouse/benefits_documents/worker_service' -require 'lighthouse/failure_notification' - -class Lighthouse::DocumentUpload - include Sidekiq::Job - extend SentryLogging - - FILENAME_EXTENSION_MATCHER = /\.\w*$/ - OBFUSCATED_CHARACTER_MATCHER = /[a-zA-Z\d]/ - - DD_ZSF_TAGS = ['service:claim-status', 'function: evidence upload to Lighthouse'].freeze - - NOTIFY_SETTINGS = Settings.vanotify.services.benefits_management_tools - MAILER_TEMPLATE_ID = NOTIFY_SETTINGS.template_id.evidence_submission_failure_email - - # retry for 2d 1h 47m 12s - # https://github.com/sidekiq/sidekiq/wiki/Error-Handling - sidekiq_options retry: 16, queue: 'low' - # Set minimum retry time to ~1 hour - sidekiq_retry_in do |count, _exception| - rand(3600..3660) if count < 9 - end - - sidekiq_retries_exhausted do |msg, _ex| - # There should be 2 values in msg['args']: - # 1) The ICN of the user - # 2) Document metadata - - next unless Flipper.enabled?('cst_send_evidence_failure_emails') - - icn = msg['args'].first - first_name = msg['args'][1]['first_name'].titleize - filename = obscured_filename(msg['args'][1]['file_name']) - date_submitted = format_issue_instant_for_mailers(msg['created_at']) - date_failed = format_issue_instant_for_mailers(msg['failed_at']) - - Lighthouse::FailureNotification.perform_async(icn, first_name, filename, date_submitted, date_failed) - - ::Rails.logger.info('Lighthouse::DocumentUpload exhaustion handler email queued') - StatsD.increment('silent_failure_avoided_no_confirmation', tags: DD_ZSF_TAGS) - rescue => e - ::Rails.logger.error('Lighthouse::DocumentUpload exhaustion handler email error', - { message: e.message }) - StatsD.increment('silent_failure', tags: DD_ZSF_TAGS) - log_exception_to_sentry(e) - end - - def self.obscured_filename(original_filename) - extension = original_filename[FILENAME_EXTENSION_MATCHER] - filename_without_extension = original_filename.gsub(FILENAME_EXTENSION_MATCHER, '') - - if filename_without_extension.length > 5 - # Obfuscate with the letter 'X'; we cannot obfuscate with special characters such as an asterisk, - # as these filenames appear in VA Notify Mailers and their templating engine uses markdown. - # Therefore, special characters can be interpreted as markdown and introduce formatting issues in the mailer - obfuscated_portion = filename_without_extension[3..-3].gsub(OBFUSCATED_CHARACTER_MATCHER, 'X') - filename_without_extension[0..2] + obfuscated_portion + filename_without_extension[-2..] + extension - else - original_filename - end - end - - def self.format_issue_instant_for_mailers(issue_instant) - # We want to return all times in EDT - timestamp = Time.at(issue_instant).in_time_zone('America/New_York') - - # We display dates in mailers in the format "May 1, 2024 3:01 p.m. EDT" - timestamp.strftime('%B %-d, %Y %-l:%M %P %Z').sub(/([ap])m/, '\1.m.') - end - - def perform(user_icn, document_hash) - client = BenefitsDocuments::WorkerService.new - document, file_body, uploader = nil - - Datadog::Tracing.trace('Config/Initialize Upload Document') do - Sentry.set_tags(source: 'documents-upload') - document = LighthouseDocument.new document_hash - - raise Common::Exceptions::ValidationErrors, document_data unless document.valid? - - uploader = LighthouseDocumentUploader.new(user_icn, document.uploader_ids) - uploader.retrieve_from_store!(document.file_name) - end - Datadog::Tracing.trace('Sidekiq read_for_upload') do - file_body = uploader.read_for_upload - end - Datadog::Tracing.trace('Sidekiq Upload Document') do |span| - span.set_tag('Document File Size', file_body.size) - client.upload_document(file_body, document) - end - Datadog::Tracing.trace('Remove Upload Document') do - uploader.remove! - end - end -end diff --git a/app/sidekiq/lighthouse/evidence_submissions/document_upload.rb b/app/sidekiq/lighthouse/evidence_submissions/document_upload.rb new file mode 100644 index 00000000000..0fcc31fa586 --- /dev/null +++ b/app/sidekiq/lighthouse/evidence_submissions/document_upload.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +require 'datadog' +require 'timeout' +require 'lighthouse/benefits_documents/worker_service' +require 'lighthouse/benefits_documents/constants' +require 'lighthouse/benefits_documents/utilities/helpers' + +module Lighthouse + module EvidenceSubmissions + class DocumentUpload + include Sidekiq::Job + attr_accessor :user_icn, :document_hash + + # retry for 2d 1h 47m 12s + # https://github.com/sidekiq/sidekiq/wiki/Error-Handling + sidekiq_options retry: 16, queue: 'low' + # Set minimum retry time to ~1 hour + sidekiq_retry_in do |count, _exception| + rand(3600..3660) if count < 9 + end + + sidekiq_retries_exhausted do |msg, _ex| + verify_msg(msg) + + if Flipper.enabled?(:cst_send_evidence_submission_failure_emails) + update_evidence_submission_for_failure(msg) + else + call_failure_notification(msg) + end + end + + def perform(user_icn, document_hash) + @user_icn = user_icn + @document_hash = document_hash + + initialize_upload_document + perform_document_upload_to_lighthouse + clean_up! + end + + def self.verify_msg(msg) + raise StandardError, "Missing fields in #{name}" if invalid_msg_fields?(msg) || invalid_msg_args?(msg['args']) + end + + def self.invalid_msg_fields?(msg) + !(%w[jid args created_at failed_at] - msg.keys).empty? + end + + def self.invalid_msg_args?(args) + return true unless args[1].is_a?(Hash) + + !(%w[first_name claim_id document_type file_name tracked_item_id] - args[1].keys).empty? + end + + def self.update_evidence_submission_for_failure(msg) + evidence_submission = EvidenceSubmission.find_by(job_id: msg['jid']) + current_personalisation = JSON.parse(evidence_submission.template_metadata)['personalisation'] + evidence_submission.update( + upload_status: BenefitsDocuments::Constants::UPLOAD_STATUS[:FAILED], + template_metadata: { + personalisation: update_personalisation(current_personalisation, msg['failed_at']) + }.to_json + ) + message = "#{name} EvidenceSubmission updated" + ::Rails.logger.info(message) + StatsD.increment('silent_failure_avoided_no_confirmation', + tags: ['service:claim-status', "function: #{message}"]) + rescue => e + error_message = "#{name} failed to update EvidenceSubmission" + ::Rails.logger.error(error_message, { messsage: e.message }) + StatsD.increment('silent_failure', tags: ['service:claim-status', "function: #{error_message}"]) + end + + def self.call_failure_notification(msg) + return unless Flipper.enabled?(:cst_send_evidence_failure_emails) + + icn = msg['args'].first + + Lighthouse::FailureNotification.perform_async(icn, create_personalisation(msg)) + + ::Rails.logger.info("#{name} exhaustion handler email queued") + StatsD.increment('silent_failure_avoided_no_confirmation', + tags: ['service:claim-status', 'function: evidence upload to Lighthouse']) + rescue => e + ::Rails.logger.error("#{name} exhaustion handler email error", + { message: e.message }) + StatsD.increment('silent_failure', tags: ['service:claim-status', 'function: evidence upload to Lighthouse']) + log_exception_to_sentry(e) + end + + # Update personalisation here since an evidence submission record was previously created + def self.update_personalisation(current_personalisation, failed_at) + personalisation = current_personalisation.clone + personalisation['date_failed'] = BenefitsDocuments::Utilities::Helpers.format_date_for_mailers(failed_at) + personalisation + end + + # This will be used by Lighthouse::FailureNotification + def self.create_personalisation(msg) + first_name = msg['args'][1]['first_name'].titleize unless msg['args'][1]['first_name'].nil? + document_type = msg['args'][1]['document_type'] + # Obscure the file name here since this will be used to generate a failed email + # NOTE: the template that we use for va_notify.send_email uses `filename` but we can also pass in `file_name` + filename = BenefitsDocuments::Utilities::Helpers.generate_obscured_file_name(msg['args'][1]['file_name']) + date_submitted = BenefitsDocuments::Utilities::Helpers.format_date_for_mailers(msg['created_at']) + date_failed = BenefitsDocuments::Utilities::Helpers.format_date_for_mailers(msg['failed_at']) + + { first_name:, document_type:, filename:, date_submitted:, date_failed: } + end + + private + + def initialize_upload_document + Datadog::Tracing.trace('Config/Initialize Upload Document') do + Sentry.set_tags(source: 'documents-upload') + validate_document! + uploader.retrieve_from_store!(document.file_name) + end + end + + def validate_document! + raise Common::Exceptions::ValidationErrors, document unless document.valid? + end + + def perform_document_upload_to_lighthouse + Datadog::Tracing.trace('Sidekiq Upload Document') do |span| + span.set_tag('Document File Size', file_body.size) + response = client.upload_document(file_body, document) # returns upload response which includes requestId + if Flipper.enabled?(:cst_send_evidence_submission_failure_emails) + update_evidence_submission_for_success(jid, response) + end + end + end + + def clean_up! + Datadog::Tracing.trace('Remove Upload Document') do + uploader.remove! + end + end + + def client + @client ||= BenefitsDocuments::WorkerService.new + end + + def document + @document ||= LighthouseDocument.new(document_hash) + end + + def uploader + @uploader ||= LighthouseDocumentUploader.new(user_icn, document.uploader_ids) + end + + def perform_initial_file_read + Datadog::Tracing.trace('Sidekiq read_for_upload') do + uploader.read_for_upload + end + end + + def file_body + @file_body ||= perform_initial_file_read + end + + def update_evidence_submission_for_success(job_id, response) + evidence_submission = EvidenceSubmission.find_by(job_id:) + request_successful = response.body.dig('data', 'success') + if request_successful + request_id = response.body.dig('data', 'requestId') + evidence_submission.update( + upload_status: BenefitsDocuments::Constants::UPLOAD_STATUS[:SUCCESS], + request_id: + ) + else + raise StandardError + end + end + end + end +end diff --git a/app/sidekiq/lighthouse/failure_notification.rb b/app/sidekiq/lighthouse/failure_notification.rb index 77d6345500e..e29437c369c 100644 --- a/app/sidekiq/lighthouse/failure_notification.rb +++ b/app/sidekiq/lighthouse/failure_notification.rb @@ -22,11 +22,12 @@ def notify_client VaNotify::Service.new(NOTIFY_SETTINGS.api_key) end - def perform(icn, first_name, filename, date_submitted, date_failed) + def perform(icn, personalisation) + # NOTE: The filename in the personalisation that is passed in is obscured notify_client.send_email( recipient_identifier: { id_value: icn, id_type: 'ICN' }, template_id: MAILER_TEMPLATE_ID, - personalisation: { first_name:, filename:, date_submitted:, date_failed: } + personalisation: ) ::Rails.logger.info('Lighthouse::FailureNotification email sent') diff --git a/lib/lighthouse/benefits_documents/service.rb b/lib/lighthouse/benefits_documents/service.rb index a1c75c1a040..c2eb0cce229 100644 --- a/lib/lighthouse/benefits_documents/service.rb +++ b/lib/lighthouse/benefits_documents/service.rb @@ -3,10 +3,13 @@ require 'common/client/base' require 'lighthouse/benefits_documents/configuration' require 'lighthouse/service_exception' +require 'lighthouse/benefits_documents/constants' +require 'lighthouse/benefits_documents/utilities/helpers' module BenefitsDocuments class Service < Common::Client::Base configuration BenefitsDocuments::Configuration + STATSD_KEY_PREFIX = 'api.benefits_documents' STATSD_UPLOAD_LATENCY = 'lighthouse.api.benefits.documents.latency' @@ -22,12 +25,6 @@ def queue_document_upload(params, lighthouse_client_id = nil) Rails.logger.info('Parameters for document upload', loggable_params) start_timer = Time.zone.now - claim_id = params[:claimId] || params[:claim_id] - - unless claim_id - raise Common::Exceptions::InternalServerError, - ArgumentError.new("Claim with id #{claim_id} not found") - end jid = submit_document(params[:file], params, lighthouse_client_id) StatsD.measure(STATSD_UPLOAD_LATENCY, Time.zone.now - start_timer, tags: ['is_multifile:false']) @@ -40,11 +37,6 @@ def queue_multi_image_upload_document(params, lighthouse_client_id = nil) Rails.logger.info('Parameters for document multi image upload', loggable_params) start_timer = Time.zone.now - claim_id = params[:claimId] || params[:claim_id] - unless claim_id - raise Common::Exceptions::InternalServerError, - ArgumentError.new("Claim with id #{claim_id} not found") - end file_to_upload = generate_multi_image_pdf(params[:files]) jid = submit_document(file_to_upload, params, lighthouse_client_id) @@ -61,29 +53,68 @@ def cleanup_after_upload def submit_document(file, file_params, lighthouse_client_id = nil) user_icn = @user.icn document_data = build_lh_doc(file, file_params) + claim_id = file_params[:claimId] || file_params[:claim_id] + + unless claim_id + raise Common::Exceptions::InternalServerError, + ArgumentError.new('Claim id is required') + end raise Common::Exceptions::ValidationErrors, document_data unless document_data.valid? uploader = LighthouseDocumentUploader.new(user_icn, document_data.uploader_ids) uploader.store!(document_data.file_obj) - # the uploader sanitizes the filename before storing, so set our doc to match + # The uploader sanitizes the filename before storing, so set our doc to match document_data.file_name = uploader.final_filename - if Flipper.enabled?(:cst_synchronous_evidence_uploads, @user) - Lighthouse::DocumentUploadSynchronous.upload(user_icn, document_data.to_serializable_hash) - else - Lighthouse::DocumentUpload.perform_async(user_icn, document_data.to_serializable_hash) + job_id = document_upload(user_icn, document_data.to_serializable_hash) + if Flipper.enabled?(:cst_send_evidence_submission_failure_emails) && + !Flipper.enabled?(:cst_synchronous_evidence_uploads, @user) + record_evidence_submission(document_data, job_id) end + job_id rescue CarrierWave::IntegrityError => e handle_error(e, lighthouse_client_id, uploader.store_dir) raise e end + def document_upload(user_icn, document_hash) + if Flipper.enabled?(:cst_synchronous_evidence_uploads, @user) + Lighthouse::DocumentUploadSynchronous.upload(user_icn, document_hash) + else + Lighthouse::EvidenceSubmissions::DocumentUpload.perform_async(user_icn, document_hash) + end + end + + def record_evidence_submission(document, job_id) + user_account = UserAccount.find(@user.user_account_uuid) + EvidenceSubmission.create( + claim_id: document.claim_id, + # Doing `.first` here since document.tracked_item_id is an array with 1 tracked item + # TODO update this and remove the first when the below pr is worked + # Created https://github.com/department-of-veterans-affairs/va.gov-team/issues/101200 for this work + tracked_item_id: document.tracked_item_id&.first, + job_id:, + job_class: self.class, + upload_status: BenefitsDocuments::Constants::UPLOAD_STATUS[:PENDING], + user_account:, + template_metadata: { personalisation: create_personalisation(document) }.to_json + ) + end + + def create_personalisation(document) + { first_name: document.first_name.titleize, + document_type: document.document_type, + file_name: document.file_name, + obfuscated_file_name: BenefitsDocuments::Utilities::Helpers.generate_obscured_file_name(document.file_name), + date_submitted: BenefitsDocuments::Utilities::Helpers.format_date_for_mailers(Time.zone.now), + date_failed: nil } + end + def build_lh_doc(file, file_params) claim_id = file_params[:claimId] || file_params[:claim_id] tracked_item_ids = file_params[:trackedItemIds] || file_params[:tracked_item_ids] document_type = file_params[:documentType] || file_params[:document_type] password = file_params[:password] - LighthouseDocument.new( first_name: @user.first_name, participant_id: @user.participant_id, @@ -91,6 +122,10 @@ def build_lh_doc(file, file_params) file_obj: file, uuid: SecureRandom.uuid, file_name: file.original_filename, + # We should pull the string out of the array for the tracked item since lighthouse gives us an array + # NOTE there will only be one tracked item here + # TODO Update this so that we only pass a tracked item instead of an array of tracked items + # Created https://github.com/department-of-veterans-affairs/va.gov-team/issues/101200 for this work tracked_item_id: tracked_item_ids, document_type:, password: diff --git a/modules/mobile/spec/requests/mobile/v0/claim/documents_spec.rb b/modules/mobile/spec/requests/mobile/v0/claim/documents_spec.rb index 4c2e6b24392..b16d83b2a21 100644 --- a/modules/mobile/spec/requests/mobile/v0/claim/documents_spec.rb +++ b/modules/mobile/spec/requests/mobile/v0/claim/documents_spec.rb @@ -8,8 +8,9 @@ include JsonSchemaMatchers let!(:user) { sis_user(icn: '24811694708759028') } + let(:user_account) { create(:user_account) } let(:file) { fixture_file_upload('doctors-note.pdf', 'application/pdf') } - let(:claim_id) { 33 } + let(:claim_id) { '33' } let(:tracked_item_id) { '12345' } let(:document_type) { 'L023' } let(:json_body_headers) { { 'Content-Type' => 'application/json', 'Accept' => 'application/json' } } @@ -18,249 +19,595 @@ token = 'abcdefghijklmnop' allow_any_instance_of(BGS::People::Response).to receive(:file_number).and_return('12345') allow_any_instance_of(BenefitsDocuments::Configuration).to receive(:access_token).and_return(token) - Flipper.enable_actor(:mobile_lighthouse_document_upload, user) - Flipper.disable(:cst_synchronous_evidence_uploads) + allow(Flipper).to receive(:enabled?).with(:mobile_lighthouse_document_upload, + instance_of(User)).and_return(true) + allow(Flipper).to receive(:enabled?).with(:cst_synchronous_evidence_uploads, + instance_of(User)).and_return(false) + user.user_account_uuid = user_account.id + user.save! FileUtils.rm_rf(Rails.root.join('tmp', 'uploads', 'cache', '*')) end - after do - Flipper.disable(:mobile_lighthouse_document_upload, user) - end - - it 'uploads a file' do - params = { file:, claim_id:, tracked_item_id:, document_type: } - expect do - post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers - end.to change(Lighthouse::DocumentUpload.jobs, :size).by(1) - - expect(response).to have_http_status(:accepted) - expect(response.parsed_body.dig('data', 'jobId')).to eq(Lighthouse::DocumentUpload.jobs.first['jid']) - expect(Lighthouse::DocumentUpload.jobs.first.dig('args', 1, 'tracked_item_id')).to eq([tracked_item_id]) - end - - it 'uploads multiple jpeg files' do - files = [Base64.encode64(File.read('spec/fixtures/files/doctors-note.jpg')), - Base64.encode64(File.read('spec/fixtures/files/marriage-cert.jpg'))] - params = { files:, claim_id:, tracked_item_id:, document_type: } - expect_any_instance_of(BenefitsDocuments::Service).to receive(:cleanup_after_upload) - expect do - post '/mobile/v0/claim/600117255/documents/multi-image', params: params.to_json, - headers: sis_headers(json: true) - end.to change(Lighthouse::DocumentUpload.jobs, :size).by(1) - expect(response).to have_http_status(:accepted) - expect(response.parsed_body.dig('data', 'jobId')).to eq(Lighthouse::DocumentUpload.jobs.first['jid']) - expect(Lighthouse::DocumentUpload.jobs.first.dig('args', 1, 'tracked_item_id')).to eq([tracked_item_id]) - end + context 'when cst_send_evidence_submission_failure_emails is disabled' do + before do + allow(Flipper).to receive(:enabled?).with(:cst_send_evidence_submission_failure_emails).and_return(false) + end - context 'when camel case is used for parameters and camel case header is disabled' do it 'uploads a file' do - params = { file:, claimId: claim_id, trackedItemId: tracked_item_id, documentType: document_type } + params = { file:, claim_id:, tracked_item_id:, document_type: } expect do - post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers(camelize: false) - end.to change(Lighthouse::DocumentUpload.jobs, :size).by(1) + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + end.to change(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs, :size).by(1) + expect(response).to have_http_status(:accepted) - expect(response.parsed_body.dig('data', 'job_id')).to eq(Lighthouse::DocumentUpload.jobs.first['jid']) - expect(Lighthouse::DocumentUpload.jobs.first.dig('args', 1, 'tracked_item_id')).to eq([tracked_item_id]) + expect(response.parsed_body.dig('data', + 'jobId')).to eq(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first['jid']) + expect(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first.dig('args', 1, + 'tracked_item_id')).to eq([tracked_item_id]) + expect(EvidenceSubmission.count).to eq(0) end it 'uploads multiple jpeg files' do files = [Base64.encode64(File.read('spec/fixtures/files/doctors-note.jpg')), Base64.encode64(File.read('spec/fixtures/files/marriage-cert.jpg'))] - params = { files:, claimId: claim_id, trackedItemId: tracked_item_id, documentType: document_type } + params = { files:, claim_id:, + tracked_item_id:, document_type: } expect_any_instance_of(BenefitsDocuments::Service).to receive(:cleanup_after_upload) expect do post '/mobile/v0/claim/600117255/documents/multi-image', params: params.to_json, - headers: sis_headers(camelize: false, json: true) - end.to change(Lighthouse::DocumentUpload.jobs, :size).by(1) + headers: sis_headers(json: true) + end.to change( + Lighthouse::EvidenceSubmissions::DocumentUpload.jobs, :size + ).by(1) + expect(response).to have_http_status(:accepted) + expect(response.parsed_body.dig('data', + 'jobId')).to eq(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first['jid']) + expect(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first.dig('args', 1, + 'tracked_item_id')).to eq([tracked_item_id]) + expect(EvidenceSubmission.count).to eq(0) + end + + context 'when camel case is used for parameters and camel case header is disabled' do + it 'uploads a file' do + params = { file:, claimId: claim_id, + trackedItemId: tracked_item_id, documentType: document_type } + expect do + post '/mobile/v0/claim/600117255/documents', + params:, headers: sis_headers(camelize: false) + end.to change( + Lighthouse::EvidenceSubmissions::DocumentUpload.jobs, :size + ).by(1) + expect(response).to have_http_status(:accepted) + expect(response.parsed_body.dig('data', + 'job_id')) + .to eq(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first['jid']) + expect(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first.dig('args', 1, + 'tracked_item_id')) + .to eq([tracked_item_id]) + expect(EvidenceSubmission.count).to eq(0) + end + + it 'uploads multiple jpeg files' do + files = [Base64.encode64(File.read('spec/fixtures/files/doctors-note.jpg')), + Base64.encode64(File.read('spec/fixtures/files/marriage-cert.jpg'))] + params = { files:, claimId: claim_id, + trackedItemId: tracked_item_id, documentType: document_type } + expect_any_instance_of(BenefitsDocuments::Service).to receive(:cleanup_after_upload) + expect do + post '/mobile/v0/claim/600117255/documents/multi-image', params: params.to_json, + headers: sis_headers(camelize: false, json: true) + end.to change( + Lighthouse::EvidenceSubmissions::DocumentUpload.jobs, :size + ).by(1) + + expect(response).to have_http_status(:accepted) + expect(response.parsed_body.dig('data', + 'job_id')) + .to eq(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first['jid']) + expect(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first.dig('args', 1, + 'tracked_item_id')) + .to eq([tracked_item_id]) + expect(EvidenceSubmission.count).to eq(0) + end + end + + it 'uploads multiple gif files' do + files = [Base64.encode64(File.read('spec/fixtures/files/doctors-note.gif')), + Base64.encode64(File.read('spec/fixtures/files/marriage-cert.gif'))] + params = { files:, claim_id:, tracked_item_id:, document_type: } + expect_any_instance_of(BenefitsDocuments::Service).to receive(:cleanup_after_upload) + expect do + post '/mobile/v0/claim/600117255/documents/multi-image', params: params.to_json, + headers: sis_headers(json: true) + end.to change(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs, :size).by(1) expect(response).to have_http_status(:accepted) - expect(response.parsed_body.dig('data', 'job_id')).to eq(Lighthouse::DocumentUpload.jobs.first['jid']) - expect(Lighthouse::DocumentUpload.jobs.first.dig('args', 1, 'tracked_item_id')).to eq([tracked_item_id]) + expect(response.parsed_body.dig('data', + 'jobId')).to eq(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first['jid']) + expect(EvidenceSubmission.count).to eq(0) end - end - it 'uploads multiple gif files' do - files = [Base64.encode64(File.read('spec/fixtures/files/doctors-note.gif')), - Base64.encode64(File.read('spec/fixtures/files/marriage-cert.gif'))] - params = { files:, claim_id:, tracked_item_id:, document_type: } - expect_any_instance_of(BenefitsDocuments::Service).to receive(:cleanup_after_upload) - - expect do - post '/mobile/v0/claim/600117255/documents/multi-image', params: params.to_json, - headers: sis_headers(json: true) - end.to change(Lighthouse::DocumentUpload.jobs, :size).by(1) - expect(response).to have_http_status(:accepted) - expect(response.parsed_body.dig('data', 'jobId')).to eq(Lighthouse::DocumentUpload.jobs.first['jid']) - end + it 'uploads multiple mixed img files' do + files = [Base64.encode64(File.read('spec/fixtures/files/doctors-note.jpg')), + Base64.encode64(File.read('spec/fixtures/files/marriage-cert.gif'))] + params = { files:, claim_id:, tracked_item_id:, document_type: } + expect_any_instance_of(BenefitsDocuments::Service).to receive(:cleanup_after_upload) + expect do + post '/mobile/v0/claim/600117255/documents/multi-image', params: params.to_json, + headers: sis_headers(json: true) + end.to change(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs, :size).by(1) + expect(response).to have_http_status(:accepted) + expect(response.parsed_body.dig('data', + 'jobId')).to eq(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first['jid']) + expect(EvidenceSubmission.count).to eq(0) + end - it 'uploads multiple mixed img files' do - files = [Base64.encode64(File.read('spec/fixtures/files/doctors-note.jpg')), - Base64.encode64(File.read('spec/fixtures/files/marriage-cert.gif'))] - params = { files:, claim_id:, tracked_item_id:, document_type: } - expect_any_instance_of(BenefitsDocuments::Service).to receive(:cleanup_after_upload) - expect do - post '/mobile/v0/claim/600117255/documents/multi-image', params: params.to_json, - headers: sis_headers(json: true) - end.to change(Lighthouse::DocumentUpload.jobs, :size).by(1) - expect(response).to have_http_status(:accepted) - expect(response.parsed_body.dig('data', 'jobId')).to eq(Lighthouse::DocumentUpload.jobs.first['jid']) - end + it 'rejects files with invalid document_types' do + params = { file:, claim_id:, tracked_item_id:, document_type: 'Invalid Type' } + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + expect(response).to have_http_status(:unprocessable_entity) + expect( + response.parsed_body['errors'].first['title'] + ).to eq(I18n.t('errors.messages.uploads.document_type_unknown')) + expect(EvidenceSubmission.count).to eq(0) + end - it 'rejects files with invalid document_types' do - params = { file:, claim_id:, tracked_item_id:, document_type: 'Invalid Type' } - post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers - expect(response).to have_http_status(:unprocessable_entity) - expect( - response.parsed_body['errors'].first['title'] - ).to eq(I18n.t('errors.messages.uploads.document_type_unknown')) - end + it 'normalizes requests with a null tracked_item_id' do + params = { file:, claim_id:, tracked_item_id: nil, document_type: } + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + args = Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first['args'][1] + expect(response).to have_http_status(:accepted) + expect(response.parsed_body.dig('data', + 'jobId')).to eq(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first['jid']) + expect(args.key?('tracked_item_id')).to be(true) + expect(args['tracked_item_id']).to be_nil + expect(EvidenceSubmission.count).to eq(0) + end + + context 'with a user that has multiple file numbers' do + before do + allow_any_instance_of(BGS::People::Response).to receive(:file_number).and_return(%w[12345 56789]) + end + + it 'uploads a file' do + params = { file:, claim_id:, tracked_item_id:, document_type: } + expect do + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + end.to change(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs, :size).by(1) + + expect(response).to have_http_status(:accepted) + expect(response.parsed_body.dig('data', + 'jobId')) + .to eq(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first['jid']) + expect(EvidenceSubmission.count).to eq(0) + end + end + + context 'with unaccepted file_type' do + let(:file) { fixture_file_upload('invalid_idme_cert.crt', 'application/x-x509-ca-cert') } + + it 'rejects files with invalid document_types' do + params = { file:, claim_id:, tracked_item_id:, document_type: } + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + expect(response).to have_http_status(:unprocessable_entity) + expect(response.parsed_body['errors'].first['title']).to eq('File extension is not included in the list') + expect(EvidenceSubmission.count).to eq(0) + end + end + + context 'with locked PDF and no provided password' do + let(:locked_file) { fixture_file_upload('locked_pdf_password_is_test.pdf', 'application/pdf') } + + it 'rejects locked PDFs if no password is provided' do + params = { file: locked_file, claim_id:, tracked_item_id:, document_type: } + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + expect(response).to have_http_status(:unprocessable_entity) + expect(response.parsed_body['errors'].first['title']).to eq(I18n.t('errors.messages.uploads.pdf.locked')) + expect(EvidenceSubmission.count).to eq(0) + end + + it 'accepts locked PDFs with the correct password' do + params = { file: locked_file, claimId: claim_id, trackedItemId: tracked_item_id, documentType: document_type, + password: 'test' } + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + expect(response).to have_http_status(:accepted) + expect(response.parsed_body.dig('data', + 'jobId')) + .to eq(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first['jid']) + expect(EvidenceSubmission.count).to eq(0) + end + + it 'rejects locked PDFs with the incorrect password' do + params = { file: locked_file, claimId: claim_id, trackedItemId: tracked_item_id, documentType: document_type, + password: 'bad' } + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + expect(response).to have_http_status(:unprocessable_entity) + expect( + response.parsed_body['errors'].first['title'] + ).to eq(I18n.t('errors.messages.uploads.pdf.incorrect_password')) + expect(EvidenceSubmission.count).to eq(0) + end + end + + context 'with a false file extension' do + let(:tempfile) do + f = Tempfile.new(['not-a', '.pdf']) + f.write('I am not a PDF') + f.rewind + fixture_file_upload(f.path, 'application/pdf') + end + + it 'rejects a file that is not really a PDF' do + params = { file: tempfile, claim_id:, tracked_item_id:, document_type: } + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + expect(response).to have_http_status(:unprocessable_entity) + expect( + response.parsed_body['errors'].first['title'] + ).to eq(I18n.t('errors.messages.uploads.malformed_pdf')) + expect(EvidenceSubmission.count).to eq(0) + end + end + + context 'with no body' do + let(:file) { fixture_file_upload('empty_file.txt', 'text/plain') } + + it 'rejects a text file with no body' do + params = { file:, claim_id:, tracked_item_id:, document_type: } + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + expect(response).to have_http_status(:internal_server_error) + expect(response.parsed_body['errors'].first['meta']['exception']).to match(/1 Byte/) + expect(EvidenceSubmission.count).to eq(0) + end + end + + context 'with an emoji in text' do + let(:tempfile) do + f = Tempfile.new(['test', '.txt']) + f.write("I \u2661 Unicode!") + f.rewind + fixture_file_upload(f.path, 'text/plain') + end + + it 'rejects a text file containing untranslatable characters' do + params = { file: tempfile, claim_id:, tracked_item_id:, document_type: } + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + expect(response).to have_http_status(:unprocessable_entity) + expect( + response.parsed_body['errors'].first['title'] + ).to eq(I18n.t('errors.messages.uploads.ascii_encoded')) + expect(EvidenceSubmission.count).to eq(0) + end + end + + context 'with UTF-16 ASCII text' do + let(:tempfile) do + f = Tempfile.new(['test', '.txt'], encoding: 'utf-16be') + f.write('I love nulls') + f.rewind + fixture_file_upload(f.path, 'text/plain') + end + + it 'accepts a text file containing translatable characters' do + params = { file: tempfile, claim_id:, tracked_item_id:, document_type: } + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + expect(response).to have_http_status(:accepted) + expect(response.parsed_body.dig('data', + 'jobId')) + .to eq(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first['jid']) + expect(EvidenceSubmission.count).to eq(0) + end + end - it 'normalizes requests with a null tracked_item_id' do - params = { file:, claim_id:, tracked_item_id: nil, document_type: } - post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers - args = Lighthouse::DocumentUpload.jobs.first['args'][1] - expect(response).to have_http_status(:accepted) - expect(response.parsed_body.dig('data', 'jobId')).to eq(Lighthouse::DocumentUpload.jobs.first['jid']) - expect(args.key?('tracked_item_id')).to be(true) - expect(args['tracked_item_id']).to be_nil + context 'with a PDF pretending to be text' do + let(:tempfile) do + f = Tempfile.new(['test', '.txt'], encoding: 'utf-16be') + pdf = File.open(Rails.root.join(*'/spec/fixtures/files/doctors-note.pdf'.split('/')).to_s, 'rb') + FileUtils.copy_stream(pdf, f) + pdf.close + f.rewind + fixture_file_upload(f.path, 'text/plain') + end + + it 'rejects a text file containing binary data' do + params = { file: tempfile, claim_id:, tracked_item_id:, document_type: } + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + expect(response).to have_http_status(:unprocessable_entity) + expect( + response.parsed_body['errors'].first['title'] + ).to eq(I18n.t('errors.messages.uploads.ascii_encoded')) + expect(EvidenceSubmission.count).to eq(0) + end + end end - context 'with a user that has multiple file numbers' do + context 'when cst_send_evidence_submission_failure_emails is enabled' do before do - allow_any_instance_of(BGS::People::Response).to receive(:file_number).and_return(%w[12345 56789]) + allow(Flipper).to receive(:enabled?).with(:cst_send_evidence_submission_failure_emails).and_return(true) end it 'uploads a file' do params = { file:, claim_id:, tracked_item_id:, document_type: } expect do post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers - end.to change(Lighthouse::DocumentUpload.jobs, :size).by(1) - + end.to change(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs, :size).by(1) expect(response).to have_http_status(:accepted) - expect(response.parsed_body.dig('data', 'jobId')).to eq(Lighthouse::DocumentUpload.jobs.first['jid']) + expect(response.parsed_body.dig('data', + 'jobId')).to eq(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first['jid']) + expect(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first.dig('args', 1, + 'tracked_item_id')).to eq([tracked_item_id]) + expect(EvidenceSubmission.count).to eq(1) end - end - context 'with unaccepted file_type' do - let(:file) { fixture_file_upload('invalid_idme_cert.crt', 'application/x-x509-ca-cert') } + it 'uploads multiple jpeg files' do + files = [Base64.encode64(File.read('spec/fixtures/files/doctors-note.jpg')), + Base64.encode64(File.read('spec/fixtures/files/marriage-cert.jpg'))] + params = { files:, claim_id:, + tracked_item_id:, document_type: } + expect_any_instance_of(BenefitsDocuments::Service).to receive(:cleanup_after_upload) + expect do + post '/mobile/v0/claim/600117255/documents/multi-image', params: params.to_json, + headers: sis_headers(json: true) + end.to change( + Lighthouse::EvidenceSubmissions::DocumentUpload.jobs, :size + ).by(1) + expect(response).to have_http_status(:accepted) + expect(response.parsed_body.dig('data', + 'jobId')).to eq(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first['jid']) + expect(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first.dig('args', 1, + 'tracked_item_id')).to eq([tracked_item_id]) + expect(EvidenceSubmission.count).to eq(1) + end - it 'rejects files with invalid document_types' do - params = { file:, claim_id:, tracked_item_id:, document_type: } - post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers - expect(response).to have_http_status(:unprocessable_entity) - expect(response.parsed_body['errors'].first['title']).to eq('File extension is not included in the list') + context 'when camel case is used for parameters and camel case header is disabled' do + it 'uploads a file' do + params = { file:, claimId: claim_id, + trackedItemId: tracked_item_id, documentType: document_type } + expect do + post '/mobile/v0/claim/600117255/documents', + params:, headers: sis_headers(camelize: false) + end.to change( + Lighthouse::EvidenceSubmissions::DocumentUpload.jobs, :size + ).by(1) + expect(response).to have_http_status(:accepted) + expect(response.parsed_body.dig('data', + 'job_id')) + .to eq(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first['jid']) + expect(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first.dig('args', 1, + 'tracked_item_id')) + .to eq([tracked_item_id]) + expect(EvidenceSubmission.count).to eq(1) + end + + it 'uploads multiple jpeg files' do + files = [Base64.encode64(File.read('spec/fixtures/files/doctors-note.jpg')), + Base64.encode64(File.read('spec/fixtures/files/marriage-cert.jpg'))] + params = { files:, claimId: claim_id, + trackedItemId: tracked_item_id, documentType: document_type } + expect_any_instance_of(BenefitsDocuments::Service).to receive(:cleanup_after_upload) + expect do + post '/mobile/v0/claim/600117255/documents/multi-image', params: params.to_json, + headers: sis_headers(camelize: false, json: true) + end.to change( + Lighthouse::EvidenceSubmissions::DocumentUpload.jobs, :size + ).by(1) + + expect(response).to have_http_status(:accepted) + expect(response.parsed_body.dig('data', + 'job_id')) + .to eq(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first['jid']) + expect(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first.dig('args', 1, + 'tracked_item_id')) + .to eq([tracked_item_id]) + expect(EvidenceSubmission.count).to eq(1) + end end - end - context 'with locked PDF and no provided password' do - let(:locked_file) { fixture_file_upload('locked_pdf_password_is_test.pdf', 'application/pdf') } + it 'uploads multiple gif files' do + files = [Base64.encode64(File.read('spec/fixtures/files/doctors-note.gif')), + Base64.encode64(File.read('spec/fixtures/files/marriage-cert.gif'))] + params = { files:, claim_id:, tracked_item_id:, document_type: } + expect_any_instance_of(BenefitsDocuments::Service).to receive(:cleanup_after_upload) - it 'rejects locked PDFs if no password is provided' do - params = { file: locked_file, claim_id:, tracked_item_id:, document_type: } - post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers - expect(response).to have_http_status(:unprocessable_entity) - expect(response.parsed_body['errors'].first['title']).to eq(I18n.t('errors.messages.uploads.pdf.locked')) + expect do + post '/mobile/v0/claim/600117255/documents/multi-image', params: params.to_json, + headers: sis_headers(json: true) + end.to change(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs, :size).by(1) + expect(response).to have_http_status(:accepted) + expect(response.parsed_body.dig('data', + 'jobId')).to eq(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first['jid']) + expect(EvidenceSubmission.count).to eq(1) end - it 'accepts locked PDFs with the correct password' do - params = { file: locked_file, claimId: claim_id, trackedItemId: tracked_item_id, documentType: document_type, - password: 'test' } - post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + it 'uploads multiple mixed img files' do + files = [Base64.encode64(File.read('spec/fixtures/files/doctors-note.jpg')), + Base64.encode64(File.read('spec/fixtures/files/marriage-cert.gif'))] + params = { files:, claim_id:, tracked_item_id:, document_type: } + expect_any_instance_of(BenefitsDocuments::Service).to receive(:cleanup_after_upload) + expect do + post '/mobile/v0/claim/600117255/documents/multi-image', params: params.to_json, + headers: sis_headers(json: true) + end.to change(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs, :size).by(1) expect(response).to have_http_status(:accepted) - expect(response.parsed_body.dig('data', 'jobId')).to eq(Lighthouse::DocumentUpload.jobs.first['jid']) + expect(response.parsed_body.dig('data', + 'jobId')).to eq(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first['jid']) + expect(EvidenceSubmission.count).to eq(1) end - it 'rejects locked PDFs with the incorrect password' do - params = { file: locked_file, claimId: claim_id, trackedItemId: tracked_item_id, documentType: document_type, - password: 'bad' } + it 'rejects files with invalid document_types' do + params = { file:, claim_id:, tracked_item_id:, document_type: 'Invalid Type' } post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers expect(response).to have_http_status(:unprocessable_entity) expect( response.parsed_body['errors'].first['title'] - ).to eq(I18n.t('errors.messages.uploads.pdf.incorrect_password')) + ).to eq(I18n.t('errors.messages.uploads.document_type_unknown')) + expect(EvidenceSubmission.count).to eq(0) end - end - context 'with a false file extension' do - let(:tempfile) do - f = Tempfile.new(['not-a', '.pdf']) - f.write('I am not a PDF') - f.rewind - fixture_file_upload(f.path, 'application/pdf') + it 'normalizes requests with a null tracked_item_id' do + params = { file:, claim_id:, tracked_item_id: nil, document_type: } + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + args = Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first['args'][1] + expect(response).to have_http_status(:accepted) + expect(response.parsed_body.dig('data', + 'jobId')).to eq(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first['jid']) + expect(args.key?('tracked_item_id')).to be(true) + expect(args['tracked_item_id']).to be_nil + expect(EvidenceSubmission.count).to eq(1) end - it 'rejects a file that is not really a PDF' do - params = { file: tempfile, claim_id:, tracked_item_id:, document_type: } - post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers - expect(response).to have_http_status(:unprocessable_entity) - expect( - response.parsed_body['errors'].first['title'] - ).to eq(I18n.t('errors.messages.uploads.malformed_pdf')) + context 'with a user that has multiple file numbers' do + before do + allow_any_instance_of(BGS::People::Response).to receive(:file_number).and_return(%w[12345 56789]) + end + + it 'uploads a file' do + params = { file:, claim_id:, tracked_item_id:, document_type: } + expect do + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + end.to change(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs, :size).by(1) + + expect(response).to have_http_status(:accepted) + expect(response.parsed_body.dig('data', + 'jobId')) + .to eq(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first['jid']) + expect(EvidenceSubmission.count).to eq(1) + end end - end - context 'with no body' do - let(:file) { fixture_file_upload('empty_file.txt', 'text/plain') } + context 'with unaccepted file_type' do + let(:file) { fixture_file_upload('invalid_idme_cert.crt', 'application/x-x509-ca-cert') } - it 'rejects a text file with no body' do - params = { file:, claim_id:, tracked_item_id:, document_type: } - post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers - expect(response).to have_http_status(:internal_server_error) - expect(response.parsed_body['errors'].first['meta']['exception']).to match(/1 Byte/) + it 'rejects files with invalid document_types' do + params = { file:, claim_id:, tracked_item_id:, document_type: } + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + expect(response).to have_http_status(:unprocessable_entity) + expect(response.parsed_body['errors'].first['title']).to eq('File extension is not included in the list') + expect(EvidenceSubmission.count).to eq(0) + end end - end - context 'with an emoji in text' do - let(:tempfile) do - f = Tempfile.new(['test', '.txt']) - f.write("I \u2661 Unicode!") - f.rewind - fixture_file_upload(f.path, 'text/plain') + context 'with locked PDF and no provided password' do + let(:locked_file) { fixture_file_upload('locked_pdf_password_is_test.pdf', 'application/pdf') } + + it 'rejects locked PDFs if no password is provided' do + params = { file: locked_file, claim_id:, tracked_item_id:, document_type: } + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + expect(response).to have_http_status(:unprocessable_entity) + expect(response.parsed_body['errors'].first['title']).to eq(I18n.t('errors.messages.uploads.pdf.locked')) + expect(EvidenceSubmission.count).to eq(0) + end + + it 'accepts locked PDFs with the correct password' do + params = { file: locked_file, claimId: claim_id, trackedItemId: tracked_item_id, documentType: document_type, + password: 'test' } + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + expect(response).to have_http_status(:accepted) + expect(response.parsed_body.dig('data', + 'jobId')) + .to eq(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first['jid']) + expect(EvidenceSubmission.count).to eq(1) + end + + it 'rejects locked PDFs with the incorrect password' do + params = { file: locked_file, claimId: claim_id, trackedItemId: tracked_item_id, documentType: document_type, + password: 'bad' } + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + expect(response).to have_http_status(:unprocessable_entity) + expect( + response.parsed_body['errors'].first['title'] + ).to eq(I18n.t('errors.messages.uploads.pdf.incorrect_password')) + expect(EvidenceSubmission.count).to eq(0) + end end - it 'rejects a text file containing untranslatable characters' do - params = { file: tempfile, claim_id:, tracked_item_id:, document_type: } - post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers - expect(response).to have_http_status(:unprocessable_entity) - expect( - response.parsed_body['errors'].first['title'] - ).to eq(I18n.t('errors.messages.uploads.ascii_encoded')) + context 'with a false file extension' do + let(:tempfile) do + f = Tempfile.new(['not-a', '.pdf']) + f.write('I am not a PDF') + f.rewind + fixture_file_upload(f.path, 'application/pdf') + end + + it 'rejects a file that is not really a PDF' do + params = { file: tempfile, claim_id:, tracked_item_id:, document_type: } + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + expect(response).to have_http_status(:unprocessable_entity) + expect( + response.parsed_body['errors'].first['title'] + ).to eq(I18n.t('errors.messages.uploads.malformed_pdf')) + expect(EvidenceSubmission.count).to eq(0) + end end - end - context 'with UTF-16 ASCII text' do - let(:tempfile) do - f = Tempfile.new(['test', '.txt'], encoding: 'utf-16be') - f.write('I love nulls') - f.rewind - fixture_file_upload(f.path, 'text/plain') + context 'with no body' do + let(:file) { fixture_file_upload('empty_file.txt', 'text/plain') } + + it 'rejects a text file with no body' do + params = { file:, claim_id:, tracked_item_id:, document_type: } + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + expect(response).to have_http_status(:internal_server_error) + expect(response.parsed_body['errors'].first['meta']['exception']).to match(/1 Byte/) + expect(EvidenceSubmission.count).to eq(0) + end end - it 'accepts a text file containing translatable characters' do - params = { file: tempfile, claim_id:, tracked_item_id:, document_type: } - post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers - expect(response).to have_http_status(:accepted) - expect(response.parsed_body.dig('data', 'jobId')).to eq(Lighthouse::DocumentUpload.jobs.first['jid']) + context 'with an emoji in text' do + let(:tempfile) do + f = Tempfile.new(['test', '.txt']) + f.write("I \u2661 Unicode!") + f.rewind + fixture_file_upload(f.path, 'text/plain') + end + + it 'rejects a text file containing untranslatable characters' do + params = { file: tempfile, claim_id:, tracked_item_id:, document_type: } + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + expect(response).to have_http_status(:unprocessable_entity) + expect( + response.parsed_body['errors'].first['title'] + ).to eq(I18n.t('errors.messages.uploads.ascii_encoded')) + expect(EvidenceSubmission.count).to eq(0) + end end - end - context 'with a PDF pretending to be text' do - let(:tempfile) do - f = Tempfile.new(['test', '.txt'], encoding: 'utf-16be') - pdf = File.open(Rails.root.join(*'/spec/fixtures/files/doctors-note.pdf'.split('/')).to_s, 'rb') - FileUtils.copy_stream(pdf, f) - pdf.close - f.rewind - fixture_file_upload(f.path, 'text/plain') + context 'with UTF-16 ASCII text' do + let(:tempfile) do + f = Tempfile.new(['test', '.txt'], encoding: 'utf-16be') + f.write('I love nulls') + f.rewind + fixture_file_upload(f.path, 'text/plain') + end + + it 'accepts a text file containing translatable characters' do + params = { file: tempfile, claim_id:, tracked_item_id:, document_type: } + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + expect(response).to have_http_status(:accepted) + expect(response.parsed_body.dig('data', + 'jobId')) + .to eq(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs.first['jid']) + expect(EvidenceSubmission.count).to eq(1) + end end - it 'rejects a text file containing binary data' do - params = { file: tempfile, claim_id:, tracked_item_id:, document_type: } - post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers - expect(response).to have_http_status(:unprocessable_entity) - expect( - response.parsed_body['errors'].first['title'] - ).to eq(I18n.t('errors.messages.uploads.ascii_encoded')) + context 'with a PDF pretending to be text' do + let(:tempfile) do + f = Tempfile.new(['test', '.txt'], encoding: 'utf-16be') + pdf = File.open(Rails.root.join(*'/spec/fixtures/files/doctors-note.pdf'.split('/')).to_s, 'rb') + FileUtils.copy_stream(pdf, f) + pdf.close + f.rewind + fixture_file_upload(f.path, 'text/plain') + end + + it 'rejects a text file containing binary data' do + params = { file: tempfile, claim_id:, tracked_item_id:, document_type: } + post '/mobile/v0/claim/600117255/documents', params:, headers: sis_headers + expect(response).to have_http_status(:unprocessable_entity) + expect( + response.parsed_body['errors'].first['title'] + ).to eq(I18n.t('errors.messages.uploads.ascii_encoded')) + expect(EvidenceSubmission.count).to eq(0) + end end end end diff --git a/spec/lib/lighthouse/benefits_documents/service_spec.rb b/spec/lib/lighthouse/benefits_documents/service_spec.rb index 52074527a5e..862fcbc51ef 100644 --- a/spec/lib/lighthouse/benefits_documents/service_spec.rb +++ b/spec/lib/lighthouse/benefits_documents/service_spec.rb @@ -6,7 +6,10 @@ require 'lighthouse/benefits_documents/configuration' RSpec.describe BenefitsDocuments::Service do + subject { service } + let(:user) { create(:user, :loa3) } + let(:user_account) { create(:user_account) } let(:service) { BenefitsDocuments::Service.new(user) } describe '#queue_document_upload' do @@ -14,6 +17,8 @@ allow_any_instance_of(Auth::ClientCredentials::Service).to receive(:get_token).and_return('fake_access_token') token = 'abcd1234' allow_any_instance_of(BenefitsDocuments::Configuration).to receive(:access_token).and_return(token) + user.user_account_uuid = user_account.id + user.save! end describe 'when uploading single file' do @@ -24,38 +29,75 @@ Rack::Test::UploadedFile.new(f.path, 'image/jpeg') end - let(:document) do - LighthouseDocument.new( - claim_id: 1, - file_obj: upload_file, - file_name: File.basename(upload_file.path) - ) - end - let(:params) do { file_number: 'xyz', - claimId: 1, + claimId: '1', file: upload_file, - trackedItemId: [1], + trackedItemIds: ['1'], # Lighthouse expects an array for tracked items documentType: 'L023', password: nil } end - it 'enqueues a job when cst_synchronous_evidence_uploads is false' do - Flipper.disable(:cst_synchronous_evidence_uploads) - expect do - service.queue_document_upload(params) - end.to change(Lighthouse::DocumentUpload.jobs, :size).by(1) + let(:issue_instant) { Time.now.to_i } + let(:submitted_date) do + BenefitsDocuments::Utilities::Helpers.format_date_for_mailers(issue_instant) end - it 'does not enqueue a job when cst_synchronous_evidence_uploads is true' do - VCR.use_cassette('lighthouse/benefits_claims/documents/lighthouse_document_upload_200_pdf') do - Flipper.enable(:cst_synchronous_evidence_uploads) + context 'when cst_synchronous_evidence_uploads is false and cst_send_evidence_submission_failure_emails is true' do # rubocop:disable Layout/LineLength + before do + allow(Flipper).to receive(:enabled?).with(:cst_send_evidence_submission_failure_emails).and_return(true) + allow(Flipper).to receive(:enabled?).with(:cst_synchronous_evidence_uploads, + instance_of(User)).and_return(false) + end + + it 'enqueues a job' do expect do service.queue_document_upload(params) - end.not_to change(Lighthouse::DocumentUpload.jobs, :size) + end.to change(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs, :size).by(1) + end + + it 'records evidence submission with PENDING status' do + subject.queue_document_upload(params) + expect(EvidenceSubmission.count).to eq(1) + evidence_submission = EvidenceSubmission.first + current_personalisation = JSON.parse(evidence_submission.template_metadata)['personalisation'] + expect(evidence_submission.upload_status) + .to eql(BenefitsDocuments::Constants::UPLOAD_STATUS[:PENDING]) + expect(current_personalisation['date_submitted']).to eql(submitted_date) + expect(evidence_submission.tracked_item_id).to be(1) + end + end + + context 'when cst_synchronous_evidence_uploads and cst_send_evidence_submission_failure_emails is disabled' do + before do + allow(Flipper).to receive(:enabled?).with(:cst_send_evidence_submission_failure_emails).and_return(false) + allow(Flipper).to receive(:enabled?).with(:cst_synchronous_evidence_uploads, + instance_of(User)).and_return(false) + end + + it 'does not record an evidence submission' do + expect do + service.queue_document_upload(params) + end.not_to change(EvidenceSubmission, :count) + end + end + + context 'when cst_synchronous_evidence_uploads is true and cst_send_evidence_submission_failure_emails is false' do # rubocop:disable Layout/LineLength + before do + allow(Flipper).to receive(:enabled?).with(:cst_send_evidence_submission_failure_emails).and_return(false) + allow(Flipper).to receive(:enabled?).with(:cst_synchronous_evidence_uploads, + instance_of(User)).and_return(true) + end + + it 'does not enqueue a job' do + VCR.use_cassette('lighthouse/benefits_claims/documents/lighthouse_document_upload_200_pdf') do + expect do + service.queue_document_upload(params) + end.not_to change(Lighthouse::EvidenceSubmissions::DocumentUpload.jobs, :size) + expect(EvidenceSubmission.count).to eq(0) + end end end end diff --git a/spec/sidekiq/evss/document_upload_spec.rb b/spec/sidekiq/evss/document_upload_spec.rb index 5a2c81e3496..8f3dbe882a9 100644 --- a/spec/sidekiq/evss/document_upload_spec.rb +++ b/spec/sidekiq/evss/document_upload_spec.rb @@ -149,11 +149,7 @@ let(:uploader_stub) { instance_double(EVSSClaimDocumentUploader) } let(:formatted_submit_date) do - # We want to return all times in EDT - timestamp = Time.at(issue_instant).in_time_zone('America/New_York') - - # We display dates in mailers in the format "May 1, 2024 3:01 p.m. EDT" - timestamp.strftime('%B %-d, %Y %-l:%M %P %Z').sub(/([ap])m/, '\1.m.') + BenefitsDocuments::Utilities::Helpers.format_date_for_mailers(issue_instant) end it 'retrieves the file and uploads to EVSS' do diff --git a/spec/sidekiq/lighthouse/document_upload_spec.rb b/spec/sidekiq/lighthouse/document_upload_spec.rb deleted file mode 100644 index 819cc5c2b35..00000000000 --- a/spec/sidekiq/lighthouse/document_upload_spec.rb +++ /dev/null @@ -1,75 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -require 'lighthouse/document_upload' -require 'va_notify/service' - -RSpec.describe Lighthouse::DocumentUpload, type: :job do - subject { described_class } - - let(:user_account) { create(:user_account) } - let(:user_account_uuid) { user_account.id } - let(:filename) { 'doctors-note.pdf' } - - let(:issue_instant) { Time.now.to_i } - let(:args) do - { - 'args' => [user_account.icn, { 'file_name' => filename, 'first_name' => 'Bob' }], - 'created_at' => issue_instant, - 'failed_at' => issue_instant - } - end - let(:tags) { subject::DD_ZSF_TAGS } - - before do - allow(Rails.logger).to receive(:info) - allow(StatsD).to receive(:increment) - end - - context 'when cst_send_evidence_failure_emails is enabled' do - before do - Flipper.enable(:cst_send_evidence_failure_emails) - allow(Lighthouse::FailureNotification).to receive(:perform_async) - end - - let(:formatted_submit_date) do - # We want to return all times in EDT - timestamp = Time.at(issue_instant).in_time_zone('America/New_York') - - # We display dates in mailers in the format "May 1, 2024 3:01 p.m. EDT" - timestamp.strftime('%B %-d, %Y %-l:%M %P %Z').sub(/([ap])m/, '\1.m.') - end - - it 'calls Lighthouse::FailureNotification' do - subject.within_sidekiq_retries_exhausted_block(args) do - expect(Lighthouse::FailureNotification).to receive(:perform_async).with( - user_account.icn, - 'Bob', # first_name - 'docXXXX-XXte.pdf', # filename - formatted_submit_date, # date_submitted - formatted_submit_date # date_failed - ) - - expect(Rails.logger) - .to receive(:info) - .with('Lighthouse::DocumentUpload exhaustion handler email queued') - expect(StatsD).to receive(:increment).with('silent_failure_avoided_no_confirmation', tags:) - end - end - end - - context 'when cst_send_evidence_failure_emails is disabled' do - before do - Flipper.disable(:cst_send_evidence_failure_emails) - end - - let(:issue_instant) { Time.now.to_i } - - it 'does not call Lighthouse::Failure Notification' do - subject.within_sidekiq_retries_exhausted_block(args) do - expect(Lighthouse::FailureNotification).not_to receive(:perform_async) - end - end - end -end diff --git a/spec/sidekiq/lighthouse/evidence_submissions/document_upload_spec.rb b/spec/sidekiq/lighthouse/evidence_submissions/document_upload_spec.rb new file mode 100644 index 00000000000..9414f85a341 --- /dev/null +++ b/spec/sidekiq/lighthouse/evidence_submissions/document_upload_spec.rb @@ -0,0 +1,220 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'sidekiq/testing' +Sidekiq::Testing.fake! + +require 'lighthouse/evidence_submissions/document_upload' +require 'va_notify/service' +require 'lighthouse/benefits_documents/constants' +require 'lighthouse/benefits_documents/utilities/helpers' + +RSpec.describe Lighthouse::EvidenceSubmissions::DocumentUpload, type: :job do + subject(:job) do + described_class.perform_async(user_icn, + document_data.to_serializable_hash) + end + + let(:user_icn) { user_account.icn } + let(:claim_id) { 4567 } + let(:file_name) { 'doctors-note.pdf' } + let(:tracked_item_ids) { 1234 } + let(:document_type) { 'L029' } + let(:document_data) do + LighthouseDocument.new( + first_name: 'First Name', + participant_id: '1111', + claim_id:, + uuid: SecureRandom.uuid, + file_extension: 'pdf', + file_name:, + tracked_item_id: tracked_item_ids, + document_type: + ) + end + let(:user_account) { create(:user_account) } + let(:job_id) { job } + + let(:client_stub) { instance_double(BenefitsDocuments::WorkerService) } + let(:job_class) { 'Lighthouse::EvidenceSubmissions::DocumentUpload' } + let(:issue_instant) { Time.now.to_i } + let(:msg) do + { + 'jid' => job_id, + 'args' => [user_account.icn, + { 'first_name' => 'Bob', + 'claim_id' => claim_id, + 'document_type' => document_type, + 'file_name' => file_name, + 'tracked_item_id' => tracked_item_ids }], + 'created_at' => issue_instant, + 'failed_at' => issue_instant + } + end + let(:file) { Rails.root.join('spec', 'fixtures', 'files', file_name).read } + let(:formatted_submit_date) do + BenefitsDocuments::Utilities::Helpers.format_date_for_mailers(issue_instant) + end + + # Create Evidence Submission records from factory + let(:evidence_submission_failed) { create(:bd_evidence_submission_failed) } + let(:evidence_submission_pending) do + create(:bd_evidence_submission_pending, + tracked_item_id: tracked_item_ids, + claim_id:, + job_id:, + job_class: described_class) + end + + def mock_response(status:, body:) + instance_double(Faraday::Response, status:, body:) + end + + context 'when :cst_send_evidence_submission_failure_emails is enabled' do + before do + allow(Flipper).to receive(:enabled?).with(:cst_send_evidence_submission_failure_emails).and_return(true) + end + + context 'when upload succeeds' do + let(:uploader_stub) { instance_double(EVSSClaimDocumentUploader) } + let(:message) { "#{job_class} EvidenceSubmission updated" } + let(:success_response) do + mock_response( + status: 200, + body: { + 'data' => { + 'success' => true, + 'requestId' => 1234 + } + } + ) + end + + it 'retrieves the file, uploads to Lighthouse and returns a success response' do + allow(LighthouseDocumentUploader).to receive(:new) { uploader_stub } + allow(BenefitsDocuments::WorkerService).to receive(:new) { client_stub } + allow(uploader_stub).to receive(:retrieve_from_store!).with(file_name) { file } + allow(uploader_stub).to receive(:read_for_upload) { file } + expect(uploader_stub).to receive(:remove!).once + expect(client_stub).to receive(:upload_document).with(file, document_data).and_return(success_response) + allow(EvidenceSubmission).to receive(:find_by) + .with({ job_id: }) + .and_return(evidence_submission_pending) + described_class.drain # runs all queued jobs of this class + # After running DocumentUpload job, there should be an updated EvidenceSubmission record + # with the response request_id + new_evidence_submission = EvidenceSubmission.find_by(job_id: job_id) + expect(new_evidence_submission.request_id).to eql(success_response.body.dig('data', 'requestId')) + expect(new_evidence_submission.upload_status).to eql(BenefitsDocuments::Constants::UPLOAD_STATUS[:SUCCESS]) + end + end + + context 'when upload fails' do + let(:msg_with_errors) do ## added 'test' so file would error + { + 'jid' => job_id, + 'args' => ['test', user_account.icn, + { 'first_name' => 'Bob', + 'claim_id' => claim_id, + 'document_type' => document_type, + 'file_name' => file_name, + 'tracked_item_id' => tracked_item_ids }], + 'created_at' => issue_instant, + 'failed_at' => issue_instant + } + end + let(:failure_response) do + { + data: { + success: false + } + } + end + let(:error_message) { "#{job_class} failed to create EvidenceSubmission" } + let(:message) { "#{job_class} EvidenceSubmission updated" } + let(:tags) { ['service:claim-status', "function: #{error_message}"] } + let(:failed_date) do + BenefitsDocuments::Utilities::Helpers.format_date_for_mailers(issue_instant) + end + + it 'updates an evidence submission record to a failed status with a failed date' do + Lighthouse::EvidenceSubmissions::DocumentUpload.within_sidekiq_retries_exhausted_block(msg) do + allow(EvidenceSubmission).to receive(:find_by) + .with({ job_id: }) + .and_return(evidence_submission_pending) + expect(Rails.logger) + .to receive(:info) + .with(message) + expect(StatsD).to receive(:increment).with('silent_failure_avoided_no_confirmation', + tags: ['service:claim-status', "function: #{message}"]) + end + expect(EvidenceSubmission.va_notify_email_not_queued.length).to equal(1) + evidence_submission = EvidenceSubmission.find_by(job_id: job_id) + current_personalisation = JSON.parse(evidence_submission.template_metadata)['personalisation'] + expect(evidence_submission.upload_status).to eql(BenefitsDocuments::Constants::UPLOAD_STATUS[:FAILED]) + expect(current_personalisation['date_failed']).to eql(failed_date) + end + + it 'fails to create a failed evidence submission record when args malformed' do + expect do + described_class.within_sidekiq_retries_exhausted_block(msg_with_errors) {} + end.to raise_error(StandardError, "Missing fields in #{job_class}") + end + + it 'raises an error when Lighthouse returns a failure response' do + allow(client_stub).to receive(:upload_document).with(file, document_data).and_return(failure_response) + expect do + job + described_class.drain + end.to raise_error(StandardError) + end + end + end + + context 'when :cst_send_evidence_submission_failure_emails is disabled' do + before do + allow(Lighthouse::FailureNotification).to receive(:perform_async) + allow(Flipper).to receive(:enabled?).with(:cst_send_evidence_submission_failure_emails).and_return(false) + end + + let(:uploader_stub) { instance_double(EVSSClaimDocumentUploader) } + let(:tags) { ['service:claim-status', 'function: evidence upload to Lighthouse'] } + + it 'retrieves the file, uploads to Lighthouse and returns a success response' do + allow(LighthouseDocumentUploader).to receive(:new) { uploader_stub } + allow(BenefitsDocuments::WorkerService).to receive(:new) { client_stub } + allow(uploader_stub).to receive(:retrieve_from_store!).with(file_name) { file } + allow(uploader_stub).to receive(:read_for_upload) { file } + expect(uploader_stub).to receive(:remove!).once + expect(client_stub).to receive(:upload_document).with(file, document_data) + expect(EvidenceSubmission.count).to equal(0) + described_class.new.perform(user_icn, document_data.to_serializable_hash) + end + + context 'when cst_send_evidence_failure_emails is enabled' do + before do + allow(Flipper).to receive(:enabled?).with(:cst_send_evidence_failure_emails).and_return(true) + end + + it 'calls Lighthouse::FailureNotification' do + described_class.within_sidekiq_retries_exhausted_block(msg) do + expect(Lighthouse::FailureNotification).to receive(:perform_async).with( + user_account.icn, + { + first_name: 'Bob', + document_type: document_type, + filename: BenefitsDocuments::Utilities::Helpers.generate_obscured_file_name(file_name), + date_submitted: formatted_submit_date, + date_failed: formatted_submit_date + } + ) + expect(EvidenceSubmission.count).to equal(0) + expect(Rails.logger) + .to receive(:info) + .with("#{job_class} exhaustion handler email queued") + expect(StatsD).to receive(:increment).with('silent_failure_avoided_no_confirmation', tags:) + end + end + end + end +end diff --git a/spec/sidekiq/lighthouse/failure_notification_spec.rb b/spec/sidekiq/lighthouse/failure_notification_spec.rb index 222681ee9bf..13263289fe1 100644 --- a/spec/sidekiq/lighthouse/failure_notification_spec.rb +++ b/spec/sidekiq/lighthouse/failure_notification_spec.rb @@ -10,6 +10,7 @@ let(:notify_client_stub) { instance_double(VaNotify::Service) } let(:user_account) { create(:user_account) } + let(:document_type) { 'L029' } let(:filename) { 'docXXXX-XXte.pdf' } let(:icn) { user_account.icn } let(:first_name) { 'Bob' } @@ -38,8 +39,9 @@ recipient_identifier: { id_value: user_account.icn, id_type: 'ICN' }, template_id: 'fake_template_id', personalisation: { - first_name: 'Bob', - filename: 'docXXXX-XXte.pdf', + first_name: first_name, + document_type: document_type, + filename: file_name, date_submitted: formatted_submit_date, date_failed: formatted_submit_date }