diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2b026f3c301..65d46e7489d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -138,6 +138,7 @@ app/controllers/v0/search_controller.rb @department-of-veterans-affairs/va-api-e app/controllers/v0/search_typeahead_controller.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/controllers/v0/sign_in_controller.rb @department-of-veterans-affairs/octo-identity app/controllers/v0/terms_of_use_agreements_controller.rb @department-of-veterans-affairs/octo-identity +app/controllers/v0/test_account_user_emails_controller.rb @department-of-veterans-affairs/octo-identity app/controllers/v0/trackings_controller.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/qa-standards @department-of-veterans-affairs/backend-review-group app/controllers/v0/triage_teams_controller.rb @department-of-veterans-affairs/vfs-mhv-secure-messaging @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/controllers/v0/upload_supporting_evidences_controller.rb @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -850,6 +851,7 @@ lib/caseflow @department-of-veterans-affairs/lighthouse-banana-peels @department lib/central_mail @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group lib/chip @department-of-veterans-affairs/vsa-healthcare-health-quest-1-backend @department-of-veterans-affairs/patient-check-in @department-of-veterans-affairs/backend-review-group lib/claim_letters @department-of-veterans-affairs/benefits-management-tools-be @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +lib/claim_documents/monitor.rb @department-of-veterans-affairs/pension-and-burials @department-of-veterans-affairs/backend-review-group lib/clamav @department-of-veterans-affairs/backend-review-group lib/common/client/base.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group lib/common/client/concerns/mhv_fhir_session_client.rb @department-of-veterans-affairs/vfs-mhv-medical-records @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -1233,6 +1235,7 @@ spec/factories/message_drafts.rb @department-of-veterans-affairs/vfs-mhv-secure- spec/factories/message_threads.rb @department-of-veterans-affairs/vfs-mhv-secure-messaging @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/factories/messages.rb @department-of-veterans-affairs/vfs-mhv-secure-messaging @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/factories/messaging_preferences.rb @department-of-veterans-affairs/vfs-mhv-secure-messaging @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +spec/factories/mhv_user_accounts.rb @department-of-veterans-affairs/octo-identity spec/factories/military_service_episodes.rb @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/factories/mpi @department-of-veterans-affairs/octo-identity spec/factories/mvi_profile_relationships.rb @department-of-veterans-affairs/octo-identity @@ -1417,6 +1420,7 @@ spec/lib/carma @department-of-veterans-affairs/vfs-10-10 @department-of-veterans spec/lib/caseflow @department-of-veterans-affairs/lighthouse-banana-peels @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/central_mail @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/chip @department-of-veterans-affairs/vsa-healthcare-health-quest-1-backend @department-of-veterans-affairs/patient-check-in @department-of-veterans-affairs/backend-review-group +spec/lib/claim_documents/monitor_spec.rb @department-of-veterans-affairs/pension-and-burials @department-of-veterans-affairs/backend-review-group spec/lib/claim_status_tool @department-of-veterans-affairs/benefits-management-tools-be @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/common/client/concerns/mhv_fhir_session_client_spec.rb @department-of-veterans-affairs/vfs-mhv-medical-records @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/common/client/concerns/mhv_jwt_session_client_spec.rb @department-of-veterans-affairs/vfs-mhv-medical-records @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -1733,6 +1737,7 @@ spec/requests/v0/caregivers_assistance_claims_spec.rb @department-of-veterans-af spec/requests/v0/claim_documents_spec.rb @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/requests/v0/debts_spec.rb @department-of-veterans-affairs/vsa-debt-resolution @department-of-veterans-affairs/backend-review-group spec/requests/v0/disability_compensation_form_spec.rb @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +spec/requests/v0/test_account_user_emails_spec.rb @department-of-veterans-affairs/octo-identity spec/requests/v1/higher_level_reviews @department-of-veterans-affairs/benefits-decision-reviews-be @department-of-veterans-affairs/backend-review-group spec/requests/v1/notice_of_disagreements @department-of-veterans-affairs/benefits-decision-reviews-be @department-of-veterans-affairs/backend-review-group spec/requests/v1/supplemental_claims @department-of-veterans-affairs/benefits-decision-reviews-be @department-of-veterans-affairs/backend-review-group @@ -2124,6 +2129,7 @@ spec/support/vcr_cassettes/spec/support @department-of-veterans-affairs/octo-ide spec/support/vcr_cassettes/staccato @department-of-veterans-affairs/vfs-10-10 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/support/vcr_cassettes/token_validation @department-of-veterans-affairs/lighthouse-banana-peels @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/support/vcr_cassettes/travel_pay @department-of-veterans-affairs/travel-pay-integration @department-of-veterans-affairs/backend-review-group +spec/support/vcr_cassettes/uploads/validate_document.yml @department-of-veterans-affairs/pension-and-burials @department-of-veterans-affairs/backend-review-group spec/spupport/vcr_cassettes/user/get_facilities_empty.yml @department-of-veterans-affairs/vfs-facilities-frontend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/support/vcr_cassettes/va_forms @department-of-veterans-affairs/platform-va-product-forms @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/support/vcr_cassettes/va_notify @department-of-veterans-affairs/va-notify-write @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group diff --git a/Gemfile.lock b/Gemfile.lock index 6efd86aa465..748a97c8e45 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -705,11 +705,9 @@ GEM nio4r (2.7.4-java) nkf (0.2.0) nkf (0.2.0-java) - nokogiri (1.16.8) + nokogiri (1.17.2) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.16.8-java) - racc (~> 1.4) nori (2.7.1) bigdecimal notiffany (0.1.3) @@ -766,8 +764,8 @@ GEM ruby-rc4 ttfunk pg (1.5.9) - pg_query (5.1.0) - google-protobuf (>= 3.22.3) + pg_query (6.0.0) + google-protobuf (>= 3.25.3) pg_search (2.3.7) activerecord (>= 6.1) activesupport (>= 6.1) diff --git a/app/controllers/concerns/exception_handling.rb b/app/controllers/concerns/exception_handling.rb index a365d5a4ed5..a970d57f6cf 100644 --- a/app/controllers/concerns/exception_handling.rb +++ b/app/controllers/concerns/exception_handling.rb @@ -83,7 +83,7 @@ def report_mapped_exception(exception, va_exception) # Add additional user specific context to the logs if exception.is_a?(Common::Exceptions::BackendServiceException) && current_user.present? extra[:icn] = current_user.icn - extra[:mhv_correlation_id] = current_user.mhv_correlation_id + extra[:mhv_credential_uuid] = current_user.mhv_credential_uuid end va_exception_info = { va_exception_errors: va_exception.errors.map(&:to_hash) } log_exception_to_sentry(exception, extra.merge(va_exception_info)) diff --git a/app/controllers/v0/claim_documents_controller.rb b/app/controllers/v0/claim_documents_controller.rb index 86449fe0812..2fef551ea4a 100644 --- a/app/controllers/v0/claim_documents_controller.rb +++ b/app/controllers/v0/claim_documents_controller.rb @@ -2,28 +2,38 @@ require 'pension_burial/tag_sentry' require 'lgy/tag_sentry' +require 'claim_documents/monitor' +require 'lighthouse/benefits_intake/service' +require 'pdf_utilities/datestamp_pdf' module V0 class ClaimDocumentsController < ApplicationController service_tag 'claims-shared' skip_before_action(:authenticate) + before_action :load_user def create - Rails.logger.info "Creating PersistentAttachment FormID=#{form_id}" + uploads_monitor.track_document_upload_attempt(form_id, current_user) - attachment = klass.new(form_id:) + @attachment = klass&.new(form_id:) # add the file after so that we have a form_id and guid for the uploader to use - attachment.file = unlock_file(params['file'], params['password']) + @attachment.file = unlock_file(params['file'], params['password']) - raise Common::Exceptions::ValidationErrors, attachment unless attachment.valid? + if %w[21P-527EZ 21P-530 21P-530V2].include?(form_id) && + Flipper.enabled?(:document_upload_validation_enabled) && !stamped_pdf_valid? - attachment.save + raise Common::Exceptions::ValidationErrors, @attachment + end + + raise Common::Exceptions::ValidationErrors, @attachment unless @attachment.valid? - Rails.logger.info "Success creating PersistentAttachment FormID=#{form_id} AttachmentID=#{attachment.id}" + @attachment.save - render json: PersistentAttachmentSerializer.new(attachment) + uploads_monitor.track_document_upload_success(form_id, @attachment.id, current_user) + + render json: PersistentAttachmentSerializer.new(@attachment) rescue => e - Rails.logger.error "Error creating PersistentAttachment FormID=#{form_id} AttachmentID=#{attachment.id} #{e}" + uploads_monitor.track_document_upload_failed(form_id, @attachment&.id, current_user, e) raise e end @@ -31,7 +41,7 @@ def create def klass case form_id - when '21P-527EZ', '21P-530EZ' + when '21P-527EZ', '21P-530EZ', '21P-530V2' PensionBurial::TagSentry.tag_sentry PersistentAttachments::PensionBurial when '21-686C', '686C-674' @@ -47,7 +57,7 @@ def form_id end def unlock_file(file, file_password) - return file unless File.extname(file) == '.pdf' && file_password + return file unless File.extname(file) == '.pdf' && file_password.present? pdftk = PdfForms.new(Settings.binaries.pdftk) tmpf = Tempfile.new(['decrypted_form_attachment', '.pdf']) @@ -69,5 +79,41 @@ def unlock_file(file, file_password) file.tempfile = tmpf file end + + # rubocop:disable Metrics/MethodLength + def stamped_pdf_valid? + extension = File.extname(@attachment&.file&.id) + allowed_types = PersistentAttachment::ALLOWED_DOCUMENT_TYPES + + if allowed_types.exclude?(extension) + raise Common::Exceptions::UnprocessableEntity.new( + detail: I18n.t('errors.messages.extension_allowlist_error', extension:, allowed_types:), + source: 'PersistentAttachment.stamped_pdf_valid?' + ) + elsif @attachment&.file&.size&.< PersistentAttachment::MINIMUM_FILE_SIZE + raise Common::Exceptions::UnprocessableEntity.new( + detail: 'File size must not be less than 1.0 KB', + source: 'PersistentAttachment.stamped_pdf_valid?' + ) + end + + document = PDFUtilities::DatestampPdf.new(@attachment.to_pdf).run(text: 'VA.GOV', x: 5, y: 5) + intake_service.valid_document?(document:) + rescue BenefitsIntake::Service::InvalidDocumentError => e + @attachment.errors.add(:attachment, e.message) + false + rescue PdfForms::PdftkError + @attachment.errors.add(:attachment, 'File is corrupt and cannot be uploaded') + false + end + # rubocop:enable Metrics/MethodLength + + def intake_service + @intake_service ||= BenefitsIntake::Service.new + end + + def uploads_monitor + @uploads_monitor ||= ClaimDocuments::Monitor.new + end end end diff --git a/app/controllers/v0/test_account_user_emails_controller.rb b/app/controllers/v0/test_account_user_emails_controller.rb new file mode 100644 index 00000000000..b5c537bcc34 --- /dev/null +++ b/app/controllers/v0/test_account_user_emails_controller.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module V0 + class TestAccountUserEmailsController < ApplicationController + service_tag 'identity' + skip_before_action :authenticate + + NAMESPACE = 'test_account_user_email' + TTL = 2_592_000 + + def create + email_redis_key = SecureRandom.uuid + Rails.cache.write(email_redis_key, create_params, namespace: NAMESPACE, expires_in: TTL) + + Rails.logger.info("[V0][TestAccountUserEmailsController] create, key:#{email_redis_key}") + + render json: { test_account_user_email_uuid: email_redis_key }, status: :created + rescue + render json: { errors: 'invalid params' }, status: :bad_request + end + + def create_params + params.require(:email) + end + end +end diff --git a/app/controllers/v1/sessions_controller.rb b/app/controllers/v1/sessions_controller.rb index fdebee29e18..5ef15a03d7c 100644 --- a/app/controllers/v1/sessions_controller.rb +++ b/app/controllers/v1/sessions_controller.rb @@ -374,7 +374,7 @@ def handle_callback_error(exc, status, response, level = :error, context = {}, else exc.message end - conditional_log_message_to_sentry(message, level, context, code) + conditional_log_message_to_sentry(message, level, context) Rails.logger.info("SessionsController version:v1 saml_callback failure, user_uuid=#{@current_user&.uuid}") unless performed? @@ -393,14 +393,11 @@ def handle_callback_error(exc, status, response, level = :error, context = {}, end # rubocop:enable Metrics/ParameterLists - def conditional_log_message_to_sentry(message, level, context, code) - # If our error is that we have multiple mhv ids, this is a case where we won't log in the user, - # but we give them a path to resolve this. So we don't want to throw an error, and we don't want - # to pollute Sentry with this condition, but we will still log in case we want metrics in - # Cloudwatch or any other log aggregator. Additionally, if the user has an invalid message timestamp + def conditional_log_message_to_sentry(message, level, context) + # If the user has an invalid message timestamp # error, this means they have waited too long in the log in page to progress, so it's not really an # appropriate Sentry error - if code == SAML::UserAttributeError::MULTIPLE_MHV_IDS_CODE || invalid_message_timestamp_error?(message) + if invalid_message_timestamp_error?(message) Rails.logger.warn("SessionsController version:v1 context:#{context} message:#{message}") else log_message_to_sentry(message, level, extra_context: context) diff --git a/app/models/mhv_user_account.rb b/app/models/mhv_user_account.rb index 1083b77c8ff..cb036f12451 100644 --- a/app/models/mhv_user_account.rb +++ b/app/models/mhv_user_account.rb @@ -10,6 +10,7 @@ class MHVUserAccount attribute :patient, :boolean attribute :sm_account_created, :boolean attribute :message, :string + alias_attribute :id, :user_profile_id validates :user_profile_id, presence: true validates :premium, :champ_va, :patient, :sm_account_created, inclusion: { in: [true, false] } diff --git a/app/models/persistent_attachment.rb b/app/models/persistent_attachment.rb index f37730ed72b..ac967d3f5af 100644 --- a/app/models/persistent_attachment.rb +++ b/app/models/persistent_attachment.rb @@ -8,6 +8,9 @@ class PersistentAttachment < ApplicationRecord include SetGuid + ALLOWED_DOCUMENT_TYPES = %w[.pdf .jpg .jpeg .png].freeze + MINIMUM_FILE_SIZE = 1.kilobyte.freeze + has_kms_key has_encrypted :file_data, key: :kms_key, **lockbox_options belongs_to :saved_claim, inverse_of: :persistent_attachments, optional: true diff --git a/app/models/user.rb b/app/models/user.rb index e9343ffda99..d7e99dc14d8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -98,6 +98,7 @@ def pciu_alternate_phone delegate :idme_uuid, to: :identity, allow_nil: true delegate :loa3?, to: :identity, allow_nil: true delegate :logingov_uuid, to: :identity, allow_nil: true + delegate :mhv_credential_uuid, to: :identity, allow_nil: true delegate :mhv_icn, to: :identity, allow_nil: true delegate :multifactor, to: :identity, allow_nil: true delegate :sign_in, to: :identity, allow_nil: true, prefix: true @@ -152,14 +153,16 @@ def mhv_account_type end def mhv_correlation_id - identity.mhv_correlation_id || mpi_mhv_correlation_id + return mhv_user_account.id if mhv_user_account.present? + + mpi_mhv_correlation_id if active_mhv_ids&.one? end def mhv_user_account @mhv_user_account ||= MHV::UserAccount::Creator.new(user_verification:).perform - rescue MHV::UserAccount::Errors::UserAccountError => e + rescue => e log_mhv_user_account_error(e.message) - raise + nil end def middle_name @@ -488,7 +491,7 @@ def mpi def get_user_verification case identity_sign_in&.dig(:service_name) when SAML::User::MHV_ORIGINAL_CSID - return UserVerification.find_by(mhv_uuid: mhv_correlation_id) if mhv_correlation_id + return UserVerification.find_by(mhv_uuid: mhv_credential_uuid) if mhv_credential_uuid when SAML::User::DSLOGON_CSID return UserVerification.find_by(dslogon_uuid: identity.edipi) if identity.edipi when SAML::User::LOGINGOV_CSID diff --git a/app/models/user_identity.rb b/app/models/user_identity.rb index f65a91a4ffa..ab40985c8de 100644 --- a/app/models/user_identity.rb +++ b/app/models/user_identity.rb @@ -29,7 +29,7 @@ class UserIdentity < Common::RedisStore attribute :verified_at # Login.gov IAL2 verification timestamp attribute :sec_id attribute :mhv_icn # only needed by B/E not serialized in user_serializer - attribute :mhv_correlation_id # this is the cannonical version of MHV Correlation ID, provided by MHV sign-in users + attribute :mhv_credential_uuid attribute :mhv_account_type # this is only available for MHV sign-in users attribute :edipi # this is only available for dslogon users attribute :sign_in, Hash # original sign_in (see sso_service#mergable_identity_attributes) diff --git a/app/services/login/after_login_actions.rb b/app/services/login/after_login_actions.rb index 18464aa4d6d..728b4c57cc9 100644 --- a/app/services/login/after_login_actions.rb +++ b/app/services/login/after_login_actions.rb @@ -48,7 +48,7 @@ def id_mismatch_validations check_id_mismatch(current_user.identity.icn, current_user.mpi_icn, 'User Identity & MPI ICN values conflict') check_id_mismatch(current_user.identity.edipi, current_user.edipi_mpi, 'User Identity & MPI EDIPI values conflict') - check_id_mismatch(current_user.identity.mhv_correlation_id, current_user.mpi_mhv_correlation_id, + check_id_mismatch(current_user.identity.mhv_credential_uuid, current_user.mpi_mhv_correlation_id, 'User Identity & MPI MHV Correlation ID values conflict') end diff --git a/app/services/login/user_verifier.rb b/app/services/login/user_verifier.rb index c2e6ad2ed63..a001bb30d1f 100644 --- a/app/services/login/user_verifier.rb +++ b/app/services/login/user_verifier.rb @@ -5,7 +5,7 @@ class UserVerifier def initialize(user) @login_type = user.sign_in&.dig(:service_name) @auth_broker = user.sign_in&.dig(:auth_broker) - @mhv_uuid = user.mhv_correlation_id + @mhv_uuid = user.mhv_credential_uuid @idme_uuid = user.idme_uuid @dslogon_uuid = user.edipi @logingov_uuid = user.logingov_uuid diff --git a/app/services/sign_in/attribute_validator.rb b/app/services/sign_in/attribute_validator.rb index bda8269a31a..23bae1e1e9f 100644 --- a/app/services/sign_in/attribute_validator.rb +++ b/app/services/sign_in/attribute_validator.rb @@ -15,7 +15,7 @@ class AttributeValidator :ssn, :mhv_icn, :edipi, - :mhv_correlation_id + :mhv_credential_uuid def initialize(user_attributes:) @idme_uuid = user_attributes[:idme_uuid] @@ -31,7 +31,7 @@ def initialize(user_attributes:) @ssn = user_attributes[:ssn] @mhv_icn = user_attributes[:mhv_icn] @edipi = user_attributes[:edipi] - @mhv_correlation_id = user_attributes[:mhv_correlation_id] + @mhv_credential_uuid = user_attributes[:mhv_credential_uuid] end def perform @@ -59,8 +59,9 @@ def validate_existing_mpi_attributes check_lock_flag(mpi_response_profile.id_theft_flag, 'Theft Flag', Constants::ErrorCode::MPI_LOCKED_ACCOUNT) check_lock_flag(mpi_response_profile.deceased_date, 'Death Flag', Constants::ErrorCode::MPI_LOCKED_ACCOUNT) check_id_mismatch(mpi_response_profile.edipis, 'EDIPI', Constants::ErrorCode::MULTIPLE_EDIPI) - check_id_mismatch(mpi_response_profile.mhv_iens, 'MHV_ID', Constants::ErrorCode::MULTIPLE_MHV_IEN) check_id_mismatch(mpi_response_profile.participant_ids, 'CORP_ID', Constants::ErrorCode::MULTIPLE_CORP_ID) + check_id_mismatch(mpi_response_profile.mhv_iens, 'MHV_ID', Constants::ErrorCode::MULTIPLE_MHV_IEN, + raise_error: false) end def add_mpi_user @@ -108,7 +109,7 @@ def user_attribute_mismatch_checks def validate_credential_attributes if mhv_auth? credential_attribute_check(:icn, mhv_icn) - credential_attribute_check(:mhv_uuid, mhv_correlation_id) + credential_attribute_check(:mhv_uuid, mhv_credential_uuid) else credential_attribute_check(:dslogon_uuid, edipi) if dslogon_auth? credential_attribute_check(:last_name, last_name) unless auto_uplevel @@ -154,26 +155,26 @@ def check_lock_flag(attribute, attribute_description, code) handle_error("#{attribute_description} Detected", code, error: Errors::MPILockedAccountError) if attribute end - def check_id_mismatch(id_array, id_description, code) + def check_id_mismatch(id_array, id_description, code, raise_error: true) if id_array && id_array.compact.uniq.size > 1 handle_error("User attributes contain multiple distinct #{id_description} values", code, - error: Errors::MPIMalformedAccountError) + error: Errors::MPIMalformedAccountError, raise_error:) end end - def handle_error(error_message, error_code, error: nil) + def handle_error(error_message, error_code, error: nil, raise_error: true) sign_in_logger.info('attribute validator error', { errors: error_message, credential_uuid:, mhv_icn:, type: service_name }.compact) - raise error.new message: error_message, code: error_code if error + raise error.new message: error_message, code: error_code if error && raise_error end def mpi_response_profile @mpi_response_profile ||= - if mhv_correlation_id - mpi_service.find_profile_by_identifier(identifier: mhv_correlation_id, + if mhv_credential_uuid + mpi_service.find_profile_by_identifier(identifier: mhv_credential_uuid, identifier_type: MPI::Constants::MHV_UUID)&.profile elsif idme_uuid mpi_service.find_profile_by_identifier(identifier: idme_uuid, diff --git a/app/services/sign_in/user_code_map_creator.rb b/app/services/sign_in/user_code_map_creator.rb index e04561a57f6..bc3de6fb069 100644 --- a/app/services/sign_in/user_code_map_creator.rb +++ b/app/services/sign_in/user_code_map_creator.rb @@ -9,7 +9,7 @@ class UserCodeMapCreator :all_credential_emails, :verified_icn, :edipi, - :mhv_correlation_id, + :mhv_credential_uuid, :request_ip, :first_name, :last_name @@ -21,7 +21,7 @@ def initialize(user_attributes:, state_payload:, verified_icn:, request_ip:) @credential_email = user_attributes[:csp_email] @all_credential_emails = user_attributes[:all_csp_emails] @edipi = user_attributes[:edipi] - @mhv_correlation_id = user_attributes[:mhv_correlation_id] + @mhv_credential_uuid = user_attributes[:mhv_credential_uuid] @verified_icn = verified_icn @request_ip = request_ip @first_name = user_attributes[:first_name] @@ -71,7 +71,7 @@ def user_verifier_object logingov_uuid:, sign_in:, edipi:, - mhv_correlation_id:, + mhv_credential_uuid:, icn: verified_icn }) end diff --git a/app/sidekiq/evss/disability_compensation_form/submit_form0781.rb b/app/sidekiq/evss/disability_compensation_form/submit_form0781.rb index c06e9670303..c7cd83d69c3 100644 --- a/app/sidekiq/evss/disability_compensation_form/submit_form0781.rb +++ b/app/sidekiq/evss/disability_compensation_form/submit_form0781.rb @@ -30,10 +30,12 @@ class SubmitForm0781 < Job FORM_ID_0781 = '21-0781' # form id for PTSD FORM_ID_0781A = '21-0781a' # form id for PTSD Secondary to Personal Assault + FORM_ID_0781V2 = '21-0781V2' # form id for Mental Health Disorder(s) Due to In-Service Traumatic Event(s) FORMS_METADATA = { FORM_ID_0781 => { docType: 'L228' }, - FORM_ID_0781A => { docType: 'L229' } + FORM_ID_0781A => { docType: 'L229' }, + FORM_ID_0781V2 => { docType: 'L228' } }.freeze STATSD_KEY_PREFIX = 'worker.evss.submit_form0781' @@ -145,12 +147,17 @@ def get_docs(submission_id, uuid) @submission = Form526Submission.find_by(id: submission_id) file_type_and_file_objs = [] - { 'form0781' => FORM_ID_0781, 'form0781a' => FORM_ID_0781A }.each do |form_type_key, actual_form_types| - if parsed_forms[form_type_key].present? + { + 'form0781' => FORM_ID_0781, + 'form0781a' => FORM_ID_0781A, + 'form0781v2' => FORM_ID_0781V2 + }.each do |form_type_key, actual_form_types| + form_content = parsed_forms[form_type_key] + + if form_content.present? file_type_and_file_objs << { type: actual_form_types, - file: process_0781(uuid, actual_form_types, parsed_forms[form_type_key], - upload: false) + file: process_0781(uuid, actual_form_types, form_content, upload: false) } end end @@ -172,12 +179,14 @@ def perform(submission_id) super(submission_id) with_tracking('Form0781 Submission', submission.saved_claim_id, submission.id) do - # process 0781 and 0781a - if parsed_forms['form0781'].present? - process_0781(submission.submitted_claim_id, FORM_ID_0781, parsed_forms['form0781']) - end - if parsed_forms['form0781a'].present? - process_0781(submission.submitted_claim_id, FORM_ID_0781A, parsed_forms['form0781a']) + # process 0781, 0781a and 0781v2 + { + 'form0781' => FORM_ID_0781, + 'form0781a' => FORM_ID_0781A, + 'form0781v2' => FORM_ID_0781V2 + }.each do |form_key, form_id| + form_content = parsed_forms[form_key] + process_0781(submission.submitted_claim_id, form_id, form_content) if form_content.present? end end rescue => e diff --git a/app/sidekiq/form1010cg/submission_job.rb b/app/sidekiq/form1010cg/submission_job.rb index 9b8160644d3..81aeba7bc73 100644 --- a/app/sidekiq/form1010cg/submission_job.rb +++ b/app/sidekiq/form1010cg/submission_job.rb @@ -5,10 +5,20 @@ module Form1010cg class SubmissionJob STATSD_KEY_PREFIX = "#{Form1010cg::Auditor::STATSD_KEY_PREFIX}.async.".freeze + DD_ZSF_TAGS = [ - 'caregiver-application', + 'service:caregiver-application', 'function: 10-10CG async form submission' ].freeze + + CALLBACK_METADATA = { + callback_metadata: { + notification_type: 'error', + form_number: '10-10CG', + statsd_tags: DD_ZSF_TAGS + } + }.freeze + include Sidekiq::Job include Sidekiq::MonitoredWorker include SentryLogging @@ -19,7 +29,6 @@ class SubmissionJob sidekiq_retries_exhausted do |msg, _e| StatsD.increment("#{STATSD_KEY_PREFIX}failed_no_retries_left", tags: ["claim_id:#{msg['args'][0]}"]) - StatsD.increment('silent_failure_avoided_no_confirmation', tags: DD_ZSF_TAGS) claim = SavedClaim::CaregiversAssistanceClaim.find(msg['args'][0]) send_failure_email(claim) @@ -49,8 +58,6 @@ def perform(claim_id) end rescue CARMA::Client::MuleSoftClient::RecordParseError StatsD.increment("#{STATSD_KEY_PREFIX}record_parse_error", tags: ["claim_id:#{claim_id}"]) - StatsD.increment('silent_failure_avoided_no_confirmation', tags: DD_ZSF_TAGS) - self.class.send_failure_email(claim) rescue => e log_exception_to_sentry(e) @@ -59,25 +66,38 @@ def perform(claim_id) raise end - def self.send_failure_email(claim) - return unless Flipper.enabled?(:caregiver_use_va_notify_on_submission_failure) - return unless claim.parsed_form.dig('veteran', 'email') - - parsed_form = claim.parsed_form - first_name = parsed_form.dig('veteran', 'fullName', 'first') - email = parsed_form.dig('veteran', 'email') - template_id = Settings.vanotify.services.health_apps_1010.template_id.form1010_cg_failure_email - api_key = Settings.vanotify.services.health_apps_1010.api_key - salutation = first_name ? "Dear #{first_name}," : '' - - VANotify::EmailJob.perform_async( - email, - template_id, - { 'salutation' => salutation }, - api_key - ) - - StatsD.increment("#{STATSD_KEY_PREFIX}submission_failure_email_sent") + class << self + def send_failure_email(claim) + unless can_send_failure_email?(claim) + StatsD.increment('silent_failure', tags: DD_ZSF_TAGS) + return + end + + parsed_form = claim.parsed_form + first_name = parsed_form.dig('veteran', 'fullName', 'first') + email = parsed_form.dig('veteran', 'email') + template_id = Settings.vanotify.services.health_apps_1010.template_id.form1010_cg_failure_email + api_key = Settings.vanotify.services.health_apps_1010.api_key + salutation = first_name ? "Dear #{first_name}," : '' + + VANotify::EmailJob.perform_async( + email, + template_id, + { 'salutation' => salutation }, + api_key, + CALLBACK_METADATA + ) + + StatsD.increment("#{STATSD_KEY_PREFIX}submission_failure_email_sent") + end + + private + + def can_send_failure_email?(claim) + Flipper.enabled?(:caregiver_use_va_notify_on_submission_failure) && claim.parsed_form.dig( + 'veteran', 'email' + ) + end end end end diff --git a/app/sidekiq/form526_submission_failure_email_job.rb b/app/sidekiq/form526_submission_failure_email_job.rb index e53c10de310..65fa491da9f 100644 --- a/app/sidekiq/form526_submission_failure_email_job.rb +++ b/app/sidekiq/form526_submission_failure_email_job.rb @@ -17,8 +17,16 @@ class Form526SubmissionFailureEmailJob 'form4142' => 'VA Form 21-4142', 'form0781' => 'VA Form 21-0781', 'form0781a' => 'VA Form 21-0781a', + 'form0781v2' => 'VA Form 21-0781', 'form8940' => 'VA Form 21-8940' }.freeze + FORM_KEYS = { + 'form4142' => 'form4142', + 'form0781' => 'form0781.form0781', + 'form0781a' => 'form0781.form0781a', + 'form0781v2' => 'form0781.form0781v2', + 'form8940' => 'form8940' + }.freeze # retry for 2d 1h 47m 12s # https://github.com/sidekiq/sidekiq/wiki/Error-Handling @@ -86,11 +94,8 @@ def send_email end def list_forms_submitted - [].tap do |forms| - forms << FORM_DESCRIPTIONS['form4142'] if form['form4142'].present? - forms << FORM_DESCRIPTIONS['form0781'] if form['form0781'].present? - forms << FORM_DESCRIPTIONS['form0781a'] if form.dig('form0781', 'form0781a').present? - forms << FORM_DESCRIPTIONS['form8940'] if form['form8940'].present? + FORM_KEYS.each_with_object([]) do |(key, path), forms| + forms << FORM_DESCRIPTIONS[key] if form.dig(*path.split('.')).present? end end diff --git a/config/features.yml b/config/features.yml index 8336c030af5..45f1bead314 100644 --- a/config/features.yml +++ b/config/features.yml @@ -90,6 +90,10 @@ features: disability_compensation_staging_lighthouse_brd: actor_type: user description: Switches to Lighthouse Staging BRD Service. NEVER ENABLE IN PRODUCTION. + document_upload_validation_enabled: + actor_type: user + description: Enables stamped PDF validation on document upload + enable_in_development: true hca_browser_monitoring_enabled: actor_type: user description: Enables browser monitoring for the health care application. diff --git a/config/routes.rb b/config/routes.rb index c2aac422a66..6991a60729b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -105,6 +105,8 @@ resource :mhv_user_account, only: [:show], controller: 'user/mhv_user_accounts' end + resource :test_account_user_email, only: [:create] + resource :veteran_onboarding, only: %i[show update] resource :education_benefits_claims, only: %i[create show] do diff --git a/lib/claim_documents/monitor.rb b/lib/claim_documents/monitor.rb new file mode 100644 index 00000000000..14c68d398c7 --- /dev/null +++ b/lib/claim_documents/monitor.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'logging/monitor' + +module ClaimDocuments + ## + # Monitor functions for Rails logging and StatsD + # @todo abstract, split logging for controller and sidekiq + # + class Monitor < ::Logging::Monitor + # statsd key for document uploads + DOCUMENT_STATS_KEY = 'api.claim_documents' + + def initialize + super('claim_documents') + end + + def track_document_upload_attempt(form_id, current_user) + additional_context = { + user_account_uuid: current_user&.user_account_uuid, + tags: ["form_id:#{form_id}"] + } + track_request('info', "Creating PersistentAttachment FormID=#{form_id}", "#{DOCUMENT_STATS_KEY}.attempt", + **additional_context) + end + + def track_document_upload_success(form_id, attachment_id, current_user) + additional_context = { + attachment_id:, + user_account_uuid: current_user&.user_account_uuid, + tags: ["form_id:#{form_id}"] + } + track_request('info', "Success creating PersistentAttachment FormID=#{form_id} AttachmentID=#{attachment_id}", + "#{DOCUMENT_STATS_KEY}.success", **additional_context) + end + + def track_document_upload_failed(form_id, attachment_id, current_user, e) + additional_context = { + attachment_id:, + user_account_uuid: current_user&.user_account_uuid, + tags: ["form_id:#{form_id}"], + message: e&.message + } + track_request('error', "Error creating PersistentAttachment FormID=#{form_id} AttachmentID=#{attachment_id} #{e}", + "#{DOCUMENT_STATS_KEY}.failure", **additional_context) + end + end +end diff --git a/lib/evss/disability_compensation_form/form0781.rb b/lib/evss/disability_compensation_form/form0781.rb index 59ab207877d..d2d870ed2ee 100644 --- a/lib/evss/disability_compensation_form/form0781.rb +++ b/lib/evss/disability_compensation_form/form0781.rb @@ -12,6 +12,7 @@ def initialize(user, form_content) @user = user @phone_email = form_content.dig('form526', 'phoneAndEmail') @form_content = form_content.dig('form526', 'form0781') + @sync_modern_0781_flow = form_content.dig('form526', 'syncModern0781Flow') @translated_forms = {} end @@ -22,16 +23,20 @@ def initialize(user, form_content) def translate return nil unless @form_content - # The pdf creation functionality is looking for a single street address - # instead of a hash - @form_content['incidents'].each do |incident| - incident['incidentLocation'] = join_location(incident['incidentLocation']) if incident['incidentLocation'] - end + if @sync_modern_0781_flow + @translated_forms['form0781v2'] = create_form_v2 + else + # The pdf creation functionality is looking for a single street address + # instead of a hash + @form_content['incidents'].each do |incident| + incident['incidentLocation'] = join_location(incident['incidentLocation']) if incident['incidentLocation'] + end - incs0781a, incs0781 = split_incidents(@form_content['incidents']) + incs0781a, incs0781 = split_incidents(@form_content['incidents']) - @translated_forms['form0781'] = create_form(incs0781) if incs0781.present? - @translated_forms['form0781a'] = create_form(incs0781a) if incs0781a.present? + @translated_forms['form0781'] = create_form(incs0781) if incs0781.present? + @translated_forms['form0781a'] = create_form(incs0781a) if incs0781a.present? + end @translated_forms end @@ -39,6 +44,31 @@ def translate private def create_form(incidents) + prepare_veteran_info.merge({ + 'incidents' => incidents, + 'remarks' => @form_content['remarks'], + 'additionalIncidentText' => @form_content['additionalIncidentText'], + 'otherInformation' => @form_content['otherInformation'] + }) + end + + def create_form_v2 + prepare_veteran_info.merge({ + 'eventsDetails' => @form_content['eventsDetails'], + 'reports' => @form_content['reports'], + 'reportsDetails' => @form_content['reportsDetails'], + 'behaviors' => @form_content['behaviors'], + 'behaviorsDetails' => @form_content['behaviorsDetails'], + 'evidence' => @form_content['evidence'], + 'traumaTreatment' => @form_content['traumaTreatment'], + 'treatmentProviders' => @form_content['treatmentProviders'], + 'treatmentProvidersDetails' => @form_content['treatmentProvidersDetails'], + 'optionIndicator' => @form_content['optionIndicator'], + 'additionalInformation' => @form_content['additionalInformation'] + }) + end + + def prepare_veteran_info { 'vaFileNumber' => @user.ssn, 'veteranSocialSecurityNumber' => @user.ssn, @@ -47,11 +77,7 @@ def create_form(incidents) 'email' => @phone_email['emailAddress'], 'veteranPhone' => @phone_email['primaryPhone'], 'veteranSecondaryPhone' => '', # No secondary phone available in 526 PreFill - 'veteranServiceNumber' => '', # No veteran service number available in 526 PreFill - 'incidents' => incidents, - 'remarks' => @form_content['remarks'], - 'additionalIncidentText' => @form_content['additionalIncidentText'], - 'otherInformation' => @form_content['otherInformation'] + 'veteranServiceNumber' => '' # No veteran service number available in 526 PreFill } end diff --git a/lib/pdf_fill/filler.rb b/lib/pdf_fill/filler.rb index 4c80c5166b3..26e48283909 100644 --- a/lib/pdf_fill/filler.rb +++ b/lib/pdf_fill/filler.rb @@ -5,6 +5,7 @@ require 'pdf_fill/forms/va214142' require 'pdf_fill/forms/va210781a' require 'pdf_fill/forms/va210781' +require 'pdf_fill/forms/va210781v2' require 'pdf_fill/forms/va218940' require 'pdf_fill/forms/va1010cg' require 'pdf_fill/forms/va686c674' @@ -51,6 +52,7 @@ def register_form(form_id, form_class) '21-4142' => PdfFill::Forms::Va214142, '21-0781a' => PdfFill::Forms::Va210781a, '21-0781' => PdfFill::Forms::Va210781, + '21-0781V2' => PdfFill::Forms::Va210781v2, '21-8940' => PdfFill::Forms::Va218940, '10-10CG' => PdfFill::Forms::Va1010cg, '686C-674' => PdfFill::Forms::Va686c674, diff --git a/lib/pdf_fill/forms/pdfs/21-0781V2.pdf b/lib/pdf_fill/forms/pdfs/21-0781V2.pdf new file mode 100644 index 00000000000..03215bf3b9a Binary files /dev/null and b/lib/pdf_fill/forms/pdfs/21-0781V2.pdf differ diff --git a/lib/saml/user_attributes/ssoe.rb b/lib/saml/user_attributes/ssoe.rb index 58a86584f03..b78fcfc0d93 100644 --- a/lib/saml/user_attributes/ssoe.rb +++ b/lib/saml/user_attributes/ssoe.rb @@ -11,7 +11,7 @@ class SSOe include Identity::Parsers::GCIds SERIALIZABLE_ATTRIBUTES = %i[email first_name middle_name last_name gender ssn birth_date uuid idme_uuid logingov_uuid verified_at sec_id mhv_icn - mhv_correlation_id mhv_account_type edipi loa sign_in multifactor icn].freeze + mhv_credential_uuid mhv_account_type edipi loa sign_in multifactor icn].freeze INBOUND_AUTHN_CONTEXT = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password' attr_reader :attributes, :authn_context, :tracker_uuid, :warnings @@ -95,7 +95,7 @@ def mhv_icn safe_attr('va_eauth_icn') end - def mhv_correlation_id + def mhv_credential_uuid safe_attr('va_eauth_mhvuuid') || mvi_ids[:mhv_ien] end @@ -215,7 +215,7 @@ def multiple_id_validations check_id_mismatch([safe_attr('va_eauth_icn'), safe_attr('va_eauth_mhvicn')], :mhv_icn_mismatch) check_id_mismatch(mvi_ids[:vba_corp_ids], :multiple_corp_ids) check_id_mismatch(edipi_ids[:edipis], :multiple_edipis) - check_id_mismatch(mhv_iens, :multiple_mhv_ids) + check_id_mismatch(mhv_iens, :multiple_mhv_ids, raise_error: false) if sec_id_mismatch? log_message_to_sentry('User attributes contains multiple sec_id values', 'warn', @@ -259,22 +259,25 @@ def mhv_iens def mhv_outbound_redirect(mismatched_ids_error) return false if mismatched_ids_error[:tag] == :multiple_edipis - @mhv_outbound_redirect ||= %w[mhv myvahealth].include?(tracker&.payload_attr(:application)) + %w[mhv myvahealth].include?(tracker&.payload_attr(:application)) end - def check_id_mismatch(ids, multiple_ids_error_type) + def check_id_mismatch(ids, multiple_ids_error_type, raise_error: true) return if ids.blank? + return if ids.compact.uniq.count <= 1 - if ids.reject(&:nil?).uniq.size > 1 - mismatched_ids_error = SAML::UserAttributeError::ERRORS[multiple_ids_error_type] - error_data = { mismatched_ids: ids, icn: mhv_icn } - Rails.logger.warn("[SAML::UserAttributes::SSOe] #{mismatched_ids_error[:message]}, #{error_data}") - unless mhv_outbound_redirect(mismatched_ids_error) - raise SAML::UserAttributeError.new(message: mismatched_ids_error[:message], - code: mismatched_ids_error[:code], - tag: mismatched_ids_error[:tag]) - end - end + mismatched_ids_error = SAML::UserAttributeError::ERRORS[multiple_ids_error_type] + error_data = { mismatched_ids: ids, icn: mhv_icn } + + Rails.logger.warn("[SAML::UserAttributes::SSOe] #{mismatched_ids_error[:message]}", error_data) + + return if mhv_outbound_redirect(mismatched_ids_error) || !raise_error + + raise SAML::UserAttributeError.new( + message: mismatched_ids_error[:message], + code: mismatched_ids_error[:code], + tag: mismatched_ids_error[:tag] + ) end def sec_id_mismatch? diff --git a/lib/sidekiq/form526_backup_submission_process/processor.rb b/lib/sidekiq/form526_backup_submission_process/processor.rb index ba97939ce46..a4e45b5a90f 100644 --- a/lib/sidekiq/form526_backup_submission_process/processor.rb +++ b/lib/sidekiq/form526_backup_submission_process/processor.rb @@ -51,6 +51,7 @@ class Processor '21-4142' => 'L107', '21-0781' => 'L228', '21-0781a' => 'L229', + '21-0781V2' => 'L228', '21-8940' => 'L149', 'bdd' => 'L023' }.freeze @@ -59,6 +60,7 @@ class Processor 21-4142 21-0781 21-0781a + 21-0781V2 21-8940 ].freeze diff --git a/lib/sign_in/idme/service.rb b/lib/sign_in/idme/service.rb index c5dc062980a..1de85b9cf05 100644 --- a/lib/sign_in/idme/service.rb +++ b/lib/sign_in/idme/service.rb @@ -126,7 +126,7 @@ def dslogon_attributes(user_info) def mhv_attributes(user_info) { - mhv_correlation_id: user_info.mhv_uuid, + mhv_credential_uuid: user_info.mhv_uuid, mhv_icn: user_info.mhv_icn, mhv_assurance: user_info.mhv_assurance } diff --git a/modules/accredited_representative_portal/accredited_representative_portal.gemspec b/modules/accredited_representative_portal/accredited_representative_portal.gemspec index 59ff0751b94..0a163557ead 100644 --- a/modules/accredited_representative_portal/accredited_representative_portal.gemspec +++ b/modules/accredited_representative_portal/accredited_representative_portal.gemspec @@ -20,5 +20,6 @@ Gem::Specification.new do |spec| spec.test_files = Dir['spec/**/*'] spec.add_dependency 'blind_index' + spec.add_development_dependency 'activerecord' spec.add_development_dependency 'rspec-rails' end diff --git a/modules/accredited_representative_portal/app/models/accredited_representative_portal/power_of_attorney_form.rb b/modules/accredited_representative_portal/app/models/accredited_representative_portal/power_of_attorney_form.rb new file mode 100644 index 00000000000..b9d70b73e25 --- /dev/null +++ b/modules/accredited_representative_portal/app/models/accredited_representative_portal/power_of_attorney_form.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module AccreditedRepresentativePortal + class PowerOfAttorneyForm < ApplicationRecord + belongs_to :power_of_attorney_request, + class_name: 'AccreditedRepresentativePortal::PowerOfAttorneyRequest', + inverse_of: :power_of_attorney_form + + has_kms_key + + has_encrypted :data, key: :kms_key, **lockbox_options + + blind_index :city + blind_index :state + blind_index :zipcode + end +end diff --git a/modules/accredited_representative_portal/app/models/accredited_representative_portal/power_of_attorney_request.rb b/modules/accredited_representative_portal/app/models/accredited_representative_portal/power_of_attorney_request.rb new file mode 100644 index 00000000000..18af415b22c --- /dev/null +++ b/modules/accredited_representative_portal/app/models/accredited_representative_portal/power_of_attorney_request.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module AccreditedRepresentativePortal + class PowerOfAttorneyRequest < ApplicationRecord + belongs_to :claimant, class_name: 'UserAccount' + + has_one :power_of_attorney_form, + class_name: 'AccreditedRepresentativePortal::PowerOfAttorneyForm', + inverse_of: :power_of_attorney_request + + has_one :resolution, + class_name: 'AccreditedRepresentativePortal::PowerOfAttorneyRequestResolution', + inverse_of: :power_of_attorney_request + end +end diff --git a/modules/accredited_representative_portal/app/models/accredited_representative_portal/power_of_attorney_request_decision.rb b/modules/accredited_representative_portal/app/models/accredited_representative_portal/power_of_attorney_request_decision.rb new file mode 100644 index 00000000000..6dcc0a03e41 --- /dev/null +++ b/modules/accredited_representative_portal/app/models/accredited_representative_portal/power_of_attorney_request_decision.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module AccreditedRepresentativePortal + class PowerOfAttorneyRequestDecision < ApplicationRecord + include PowerOfAttorneyRequestResolution::Resolving + + self.inheritance_column = nil + + belongs_to :creator, + class_name: 'UserAccount' + end +end diff --git a/modules/accredited_representative_portal/app/models/accredited_representative_portal/power_of_attorney_request_expiration.rb b/modules/accredited_representative_portal/app/models/accredited_representative_portal/power_of_attorney_request_expiration.rb new file mode 100644 index 00000000000..a2a6fd4cd9e --- /dev/null +++ b/modules/accredited_representative_portal/app/models/accredited_representative_portal/power_of_attorney_request_expiration.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module AccreditedRepresentativePortal + class PowerOfAttorneyRequestExpiration < ApplicationRecord + include PowerOfAttorneyRequestResolution::Resolving + end +end diff --git a/modules/accredited_representative_portal/app/models/accredited_representative_portal/power_of_attorney_request_resolution.rb b/modules/accredited_representative_portal/app/models/accredited_representative_portal/power_of_attorney_request_resolution.rb new file mode 100644 index 00000000000..96754cbb86d --- /dev/null +++ b/modules/accredited_representative_portal/app/models/accredited_representative_portal/power_of_attorney_request_resolution.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module AccreditedRepresentativePortal + class PowerOfAttorneyRequestResolution < ApplicationRecord + belongs_to :power_of_attorney_request, + class_name: 'AccreditedRepresentativePortal::PowerOfAttorneyRequest', + inverse_of: :resolution + + RESOLVING_TYPES = [ + 'AccreditedRepresentativePortal::PowerOfAttorneyRequestExpiration', + 'AccreditedRepresentativePortal::PowerOfAttorneyRequestDecision' + ].freeze + + delegated_type :resolving, types: RESOLVING_TYPES + + has_kms_key + + has_encrypted :reason, key: :kms_key, **lockbox_options + + module Resolving + extend ActiveSupport::Concern + + included do + has_one :power_of_attorney_request_resolution, as: :resolving + end + end + end +end diff --git a/modules/accredited_representative_portal/lib/accredited_representative_portal/engine.rb b/modules/accredited_representative_portal/lib/accredited_representative_portal/engine.rb index 9d1284ad819..864b156a223 100644 --- a/modules/accredited_representative_portal/lib/accredited_representative_portal/engine.rb +++ b/modules/accredited_representative_portal/lib/accredited_representative_portal/engine.rb @@ -3,6 +3,16 @@ module AccreditedRepresentativePortal class Engine < ::Rails::Engine isolate_namespace AccreditedRepresentativePortal + + # `isolate_namespace` redefines `table_name_prefix` on load of + # `active_record`, so we append our own callback to redefine it again how we + # want. + ActiveSupport.on_load(:active_record) do + AccreditedRepresentativePortal.redefine_singleton_method(:table_name_prefix) do + 'ar_' + end + end + config.generators.api_only = true # So that the app-wide migration command notices our engine's migrations. diff --git a/modules/accredited_representative_portal/spec/factories/power_of_attorney_decision.rb b/modules/accredited_representative_portal/spec/factories/power_of_attorney_decision.rb new file mode 100644 index 00000000000..fd6f2e65d66 --- /dev/null +++ b/modules/accredited_representative_portal/spec/factories/power_of_attorney_decision.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :power_of_attorney_request_decision, + class: 'AccreditedRepresentativePortal::PowerOfAttorneyRequestDecision' do + id { Faker::Internet.uuid } + association :creator, factory: :user_account + type { 'Approval' } + end +end diff --git a/modules/accredited_representative_portal/spec/factories/power_of_attorney_expiration.rb b/modules/accredited_representative_portal/spec/factories/power_of_attorney_expiration.rb new file mode 100644 index 00000000000..2f294a2a9fb --- /dev/null +++ b/modules/accredited_representative_portal/spec/factories/power_of_attorney_expiration.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :power_of_attorney_request_expiration, + class: 'AccreditedRepresentativePortal::PowerOfAttorneyRequestExpiration' do + id { Faker::Internet.uuid } + end +end diff --git a/modules/accredited_representative_portal/spec/factories/power_of_attorney_form.rb b/modules/accredited_representative_portal/spec/factories/power_of_attorney_form.rb new file mode 100644 index 00000000000..779efce161d --- /dev/null +++ b/modules/accredited_representative_portal/spec/factories/power_of_attorney_form.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :power_of_attorney_form, class: 'AccreditedRepresentativePortal::PowerOfAttorneyForm' do + association :power_of_attorney_request, factory: :power_of_attorney_request + data_ciphertext { 'Test encrypted data' } + city_bidx { Faker::Alphanumeric.alphanumeric(number: 44) } + state_bidx { Faker::Alphanumeric.alphanumeric(number: 44) } + zipcode_bidx { Faker::Alphanumeric.alphanumeric(number: 44) } + encrypted_kms_key { SecureRandom.hex(16) } + end +end diff --git a/modules/accredited_representative_portal/spec/factories/power_of_attorney_request.rb b/modules/accredited_representative_portal/spec/factories/power_of_attorney_request.rb new file mode 100644 index 00000000000..0aa23eea4f6 --- /dev/null +++ b/modules/accredited_representative_portal/spec/factories/power_of_attorney_request.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :power_of_attorney_request, class: 'AccreditedRepresentativePortal::PowerOfAttorneyRequest' do + association :claimant, factory: :user_account + id { Faker::Internet.uuid } + created_at { Time.current } + end +end diff --git a/modules/accredited_representative_portal/spec/factories/power_of_attorney_request_resolution.rb b/modules/accredited_representative_portal/spec/factories/power_of_attorney_request_resolution.rb new file mode 100644 index 00000000000..20798bf1d54 --- /dev/null +++ b/modules/accredited_representative_portal/spec/factories/power_of_attorney_request_resolution.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :power_of_attorney_request_resolution, + class: 'AccreditedRepresentativePortal::PowerOfAttorneyRequestResolution' do + association :power_of_attorney_request, factory: :power_of_attorney_request + resolving_id { SecureRandom.uuid } + reason_ciphertext { 'Encrypted Reason' } + created_at { Time.current } + encrypted_kms_key { SecureRandom.hex(16) } + + trait :with_expiration do + resolving_type { 'AccreditedRepresentativePortal::PowerOfAttorneyRequestExpiration' } + resolving { create(:power_of_attorney_request_expiration) } + end + + trait :with_decision do + resolving_type { 'AccreditedRepresentativePortal::PowerOfAttorneyRequestDecision' } + resolving { create(:power_of_attorney_request_decision) } + end + + trait :with_invalid_type do + resolving_type { 'AccreditedRepresentativePortal::InvalidType' } + resolving { AccreditedRepresentativePortal::InvalidType.new } + end + end +end + +module AccreditedRepresentativePortal + class InvalidType + def method_missing(_method, *_args) = self + + def respond_to_missing?(_method, _include_private = false) = true + + def id = nil + + def self.method_missing(_method, *_args) = NullObject.new + + def self.respond_to_missing?(_method, _include_private = false) = true + end + + class NullObject + def method_missing(_method, *_args) = self + + def respond_to_missing?(*) = true + + def nil? = true + + def to_s = '' + end +end diff --git a/modules/accredited_representative_portal/spec/models/accredited_representative_portal/power_of_attorney_form_spec.rb b/modules/accredited_representative_portal/spec/models/accredited_representative_portal/power_of_attorney_form_spec.rb new file mode 100644 index 00000000000..0767cf0ceca --- /dev/null +++ b/modules/accredited_representative_portal/spec/models/accredited_representative_portal/power_of_attorney_form_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_relative '../../rails_helper' + +RSpec.describe AccreditedRepresentativePortal::PowerOfAttorneyForm, type: :model do + describe 'associations' do + it { is_expected.to belong_to(:power_of_attorney_request) } + end + + describe 'creation' do + it 'creates a valid form' do + form = build(:power_of_attorney_form, data_ciphertext: 'test_data') + expect(form).to be_valid + end + end +end diff --git a/modules/accredited_representative_portal/spec/models/accredited_representative_portal/power_of_attorney_request_decision_spec.rb b/modules/accredited_representative_portal/spec/models/accredited_representative_portal/power_of_attorney_request_decision_spec.rb new file mode 100644 index 00000000000..c2376dc7ecd --- /dev/null +++ b/modules/accredited_representative_portal/spec/models/accredited_representative_portal/power_of_attorney_request_decision_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require_relative '../../rails_helper' + +RSpec.describe AccreditedRepresentativePortal::PowerOfAttorneyRequestDecision, type: :model do + describe 'associations' do + it { is_expected.to belong_to(:creator).class_name('UserAccount') } + it { is_expected.to have_one(:power_of_attorney_request_resolution) } + end +end diff --git a/modules/accredited_representative_portal/spec/models/accredited_representative_portal/power_of_attorney_request_expiration_spec.rb b/modules/accredited_representative_portal/spec/models/accredited_representative_portal/power_of_attorney_request_expiration_spec.rb new file mode 100644 index 00000000000..2213312df20 --- /dev/null +++ b/modules/accredited_representative_portal/spec/models/accredited_representative_portal/power_of_attorney_request_expiration_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_relative '../../rails_helper' + +RSpec.describe AccreditedRepresentativePortal::PowerOfAttorneyRequestExpiration, type: :model do + describe 'associations' do + it { is_expected.to have_one(:power_of_attorney_request_resolution) } + end + + describe 'validations' do + it 'creates a valid record' do + expiration = create(:power_of_attorney_request_expiration) + expect(expiration).to be_valid + end + end +end diff --git a/modules/accredited_representative_portal/spec/models/accredited_representative_portal/power_of_attorney_request_resolution_spec.rb b/modules/accredited_representative_portal/spec/models/accredited_representative_portal/power_of_attorney_request_resolution_spec.rb new file mode 100644 index 00000000000..e7a006aac77 --- /dev/null +++ b/modules/accredited_representative_portal/spec/models/accredited_representative_portal/power_of_attorney_request_resolution_spec.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +require_relative '../../rails_helper' + +mod = AccreditedRepresentativePortal +RSpec.describe mod::PowerOfAttorneyRequestResolution, type: :model do + describe 'associations' do + let(:power_of_attorney_request) { create(:power_of_attorney_request) } + + it { is_expected.to belong_to(:power_of_attorney_request) } + + it 'can resolve to PowerOfAttorneyRequestExpiration' do + expiration = create(:power_of_attorney_request_expiration) + resolution = described_class.create!( + resolving: expiration, + power_of_attorney_request: power_of_attorney_request, + created_at: Time.zone.now, + encrypted_kms_key: SecureRandom.hex(16) + ) + + expect(resolution.resolving).to eq(expiration) + expect(resolution.resolving_type).to eq('AccreditedRepresentativePortal::PowerOfAttorneyRequestExpiration') + end + + it 'can resolve to PowerOfAttorneyRequestDecision' do + decision = create(:power_of_attorney_request_decision) + resolution = described_class.create!( + resolving: decision, + power_of_attorney_request: power_of_attorney_request, + created_at: Time.zone.now, + encrypted_kms_key: SecureRandom.hex(16) + ) + + expect(resolution.resolving).to eq(decision) + expect(resolution.resolving_type).to eq('AccreditedRepresentativePortal::PowerOfAttorneyRequestDecision') + end + end + + describe 'delegated_type resolving' do + it 'is valid with expiration resolving' do + resolution = create(:power_of_attorney_request_resolution, :with_expiration) + expect(resolution).to be_valid + expect(resolution.resolving).to be_a(mod::PowerOfAttorneyRequestExpiration) + end + + it 'is valid with decision resolving' do + resolution = create(:power_of_attorney_request_resolution, :with_decision) + expect(resolution).to be_valid + expect(resolution.resolving).to be_a(mod::PowerOfAttorneyRequestDecision) + end + + it 'is invalid with null resolving_type and resolving_id' do + resolution = build(:power_of_attorney_request_resolution, resolving_type: nil, resolving_id: nil) + expect(resolution).not_to be_valid + end + end + + describe 'heterogeneous list behavior' do + it 'conveniently returns heterogeneous lists' do + travel_to Time.zone.parse('2024-11-25T09:46:24Z') do + creator = create(:user_account) + + ids = [] + + # Persisted resolving records + decision_acceptance = mod::PowerOfAttorneyRequestDecision.create!( + type: 'acceptance', + creator: creator + ) + decision_declination = mod::PowerOfAttorneyRequestDecision.create!( + type: 'declination', + creator: creator + ) + expiration = mod::PowerOfAttorneyRequestExpiration.create! + + # Associate resolving records + ids << described_class.create!( + power_of_attorney_request: create(:power_of_attorney_request), + resolving: decision_acceptance, + encrypted_kms_key: SecureRandom.hex(16), + created_at: Time.current + ).id + + ids << described_class.create!( + power_of_attorney_request: create(:power_of_attorney_request), + resolving: decision_declination, + encrypted_kms_key: SecureRandom.hex(16), + created_at: Time.current + ).id + + ids << described_class.create!( + power_of_attorney_request: create(:power_of_attorney_request), + resolving: expiration, + encrypted_kms_key: SecureRandom.hex(16), + created_at: Time.current + ).id + + resolutions = described_class.includes(:resolving).find(ids) + + # Serialize for comparison + actual = + resolutions.map do |resolution| + serialized = + case resolution.resolving + when mod::PowerOfAttorneyRequestDecision + { + type: 'decision', + decision_type: resolution.resolving.type + } + when mod::PowerOfAttorneyRequestExpiration + { + type: 'expiration' + } + end + + serialized.merge!( + created_at: resolution.created_at.iso8601 + ) + end + + expect(actual).to eq( + [ + { + type: 'decision', + decision_type: 'acceptance', + created_at: '2024-11-25T09:46:24Z' + }, + { + type: 'decision', + decision_type: 'declination', + created_at: '2024-11-25T09:46:24Z' + }, + { + type: 'expiration', + created_at: '2024-11-25T09:46:24Z' + } + ] + ) + end + end + end +end diff --git a/modules/accredited_representative_portal/spec/models/accredited_representative_portal/power_of_attorney_request_spec.rb b/modules/accredited_representative_portal/spec/models/accredited_representative_portal/power_of_attorney_request_spec.rb new file mode 100644 index 00000000000..81b29d90317 --- /dev/null +++ b/modules/accredited_representative_portal/spec/models/accredited_representative_portal/power_of_attorney_request_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require_relative '../../rails_helper' + +RSpec.describe AccreditedRepresentativePortal::PowerOfAttorneyRequest, type: :model do + describe 'associations' do + it { is_expected.to belong_to(:claimant).class_name('UserAccount') } + it { is_expected.to have_one(:power_of_attorney_form) } + it { is_expected.to have_one(:resolution) } + end +end diff --git a/modules/accredited_representative_portal/spec/models/accredited_representatiive_portal/representative_user_spec.rb b/modules/accredited_representative_portal/spec/models/accredited_representative_portal/representative_user_spec.rb similarity index 100% rename from modules/accredited_representative_portal/spec/models/accredited_representatiive_portal/representative_user_spec.rb rename to modules/accredited_representative_portal/spec/models/accredited_representative_portal/representative_user_spec.rb diff --git a/modules/mocked_authentication/spec/lib/credential/service_spec.rb b/modules/mocked_authentication/spec/lib/credential/service_spec.rb index 82be3253faf..984302df925 100644 --- a/modules/mocked_authentication/spec/lib/credential/service_spec.rb +++ b/modules/mocked_authentication/spec/lib/credential/service_spec.rb @@ -322,7 +322,7 @@ credential_aal_highest: 2, credential_ial_highest: 'classic_loa3', email:, - mhv_uuid: mhv_correlation_id, + mhv_uuid: mhv_credential_uuid, mhv_icn:, mhv_assurance:, level_of_assurance: 3, @@ -333,7 +333,7 @@ } ) end - let(:mhv_correlation_id) { 'some-mhv-correlation-id' } + let(:mhv_credential_uuid) { 'some-mhv-credential-uuid' } let(:mhv_icn) { 'some-mhv-icn' } let(:mhv_assurance) { 'some-mhv-assurance' } let(:aud) { 'some-aud' } @@ -351,7 +351,7 @@ authn_context:, auto_uplevel:, mhv_icn:, - mhv_correlation_id:, + mhv_credential_uuid:, mhv_assurance: } end diff --git a/spec/controllers/v1/sessions_controller_spec.rb b/spec/controllers/v1/sessions_controller_spec.rb index 6d8caf3dae8..28e1139fce0 100644 --- a/spec/controllers/v1/sessions_controller_spec.rb +++ b/spec/controllers/v1/sessions_controller_spec.rb @@ -900,7 +900,7 @@ def expect_logger_msg(level, msg) context 'MHV correlation id validation' do let(:mpi_profile) { build(:mpi_profile, mhv_ids: [Faker::Number.number(digits: 11)]) } - let(:expected_identity_value) { user.identity.mhv_correlation_id } + let(:expected_identity_value) { user.identity.mhv_credential_uuid } let(:expected_mpi_value) { user.mpi_mhv_correlation_id } let(:validation_id) { 'MHV Correlation ID' } @@ -1253,41 +1253,6 @@ def expect_logger_msg(level, msg) end end - context 'when MHV user attribute validation fails' do - let(:saml_attributes) do - build(:ssoe_idme_mhv_loa3, - va_eauth_mhvuuid: ['999888'], - va_eauth_mhvien: ['888777']) - end - let(:saml_response) do - build_saml_response( - authn_context: 'myhealthevet', - level_of_assurance: ['3'], - attributes: saml_attributes, - existing_attributes: nil, - issuer: 'https://int.eauth.va.gov/FIM/sps/saml20fedCSP/saml20' - ) - end - let(:saml_user) { SAML::User.new(saml_response) } - let(:expected_error_message) { SAML::UserAttributeError::ERRORS[:multiple_mhv_ids][:message] } - let(:version) { 'v1' } - let(:expected_warn_message) do - "SessionsController version:#{version} context:{} message:#{expected_error_message}" - end - let(:error_code) { '101' } - - before { allow(SAML::User).to receive(:new).and_return(saml_user) } - - it 'logs a generic user validation error', :aggregate_failures do - expect(controller).not_to receive(:log_message_to_sentry) - expect(Rails.logger).to receive(:warn).ordered - expect(Rails.logger).to receive(:warn).ordered.with(expected_warn_message) - expect(call_endpoint).to redirect_to(expected_redirect) - - expect(response).to have_http_status(:found) - end - end - context 'when EDIPI user attribute validation fails' do let(:saml_attributes) do build(:ssoe_idme_mhv_loa3, diff --git a/spec/factories/form526_submissions.rb b/spec/factories/form526_submissions.rb index 499d3d54a74..76e78dbffed 100644 --- a/spec/factories/form526_submissions.rb +++ b/spec/factories/form526_submissions.rb @@ -44,6 +44,12 @@ end end + trait :with_0781v2 do + form_json do + File.read("#{submissions_path}/with_0781v2.json") + end + end + trait :with_everything_toxic_exposure do form_json do File.read("#{submissions_path}/with_everything_toxic_exposure.json") diff --git a/spec/factories/mhv_user_accounts.rb b/spec/factories/mhv_user_accounts.rb new file mode 100644 index 00000000000..18d5ed66052 --- /dev/null +++ b/spec/factories/mhv_user_accounts.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :mhv_user_account do + user_profile_id { '12345678' } + premium { true } + champ_va { true } + patient { true } + sm_account_created { true } + message { 'Success' } + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index f9bfb6538e9..cef808f1587 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -27,9 +27,10 @@ icn { '123498767V234859' } mhv_icn { nil } multifactor { false } - mhv_ids { [] } - active_mhv_ids { [] } - mhv_correlation_id { Faker::Number.number(digits: 9) } + mhv_ids { [mhv_credential_uuid] } + active_mhv_ids { [mhv_credential_uuid] } + mhv_correlation_id { mhv_credential_uuid } + mhv_credential_uuid { Faker::Number.number(digits: 9) } mhv_account_type { nil } edipi { '384759483' } va_patient { nil } @@ -86,7 +87,7 @@ mhv_icn:, loa:, multifactor:, - mhv_correlation_id:, + mhv_credential_uuid:, mhv_account_type:, edipi:, sign_in: } @@ -120,6 +121,10 @@ vet360_id: } build(:mpi_profile, mpi_attributes) end + + mhv_user_account do + build(:mhv_user_account) + end end callback(:after_build, :after_stub, :after_create) do |user, t| diff --git a/spec/fixtures/files/tiny.pdf b/spec/fixtures/files/tiny.pdf new file mode 100755 index 00000000000..593558f9a45 Binary files /dev/null and b/spec/fixtures/files/tiny.pdf differ diff --git a/spec/fixtures/pdf_fill/21-0781V2/kitchen_sink.json b/spec/fixtures/pdf_fill/21-0781V2/kitchen_sink.json index 936646005f7..97d3d991b47 100644 --- a/spec/fixtures/pdf_fill/21-0781V2/kitchen_sink.json +++ b/spec/fixtures/pdf_fill/21-0781V2/kitchen_sink.json @@ -1,125 +1,125 @@ { - "vaFileNumber": "12345678", - "veteranSocialSecurityNumber": "123456789", - "veteranFullName": { - "first": "Testy", - "middle": "Tester", - "last": "Testerson" + "vaFileNumber": "12345678", + "veteranSocialSecurityNumber": "123456789", + "veteranFullName": { + "first": "Testy", + "middle": "Tester", + "last": "Testerson" + }, + "veteranDateOfBirth": "1981-11-05", + "veteranServiceNumber": "987654321", + "email": "testy.testerson@gmail.com", + "veteranPhone": "2123456789", + "veteranSecondaryPhone": "3133456789", + "additionalInformation": "Lorem ipsum dolor sit amet", + "eventTypes": { + "combat": true, + "mst": false, + "nonMst": true, + "other": false + }, + "eventsDetails": [ + { + "details": "Lorem ipsum dolor sit amet.", + "location": "abcdefghijklmn opqrstuvwxyz1234a", + "timing": "Summer of '70" }, - "veteranDateOfBirth": "1981-11-05", - "veteranServiceNumber": "987654321", - "email": "testy.testerson@gmail.com", - "veteranPhone": "2123456789", - "veteranSecondaryPhone": "3133456789", - "additionalInformation": "Lorem ipsum dolor sit amet", - "eventTypes": { - "combat": true, - "mst": false, - "nonMst": true, - "other": false + { + "details": "Lorem ipsum dolor sit amet.", + "location": "abcdefghijklmn opqrstuvwxyz1234a", + "timing": "June 2007" + } + ], + "reports": { + "yes": true, + "no": false, + "restricted": true, + "unrestricted": false, + "neither": false, + "police": true, + "other": true + }, + "reportsDetails": { + "police": { + "agency": "SVI", + "city": "Dallas", + "state": "TX", + "township": "", + "country": "USA" }, - "eventsDetails": [ - { - "details": "Lorem ipsum dolor sit amet.", - "location": "abcdefghijklmn opqrstuvwxyz1234a", - "timing": "Summer of '70" - }, - { - "details": "Lorem ipsum dolor sit amet.", - "location": "abcdefghijklmn opqrstuvwxyz1234a", - "timing": "June 2007" - } - ], - "reports": { - "yes": true, - "no": false, - "restricted": true, - "unrestricted": false, - "neither": false, - "police": true, - "other": true + "other": "incident report" + }, + "behaviors": { + "reassignment": true, + "absences": false, + "performance": true, + "consultations": false, + "episodes": true, + "medications": false, + "selfMedication": true, + "substances": false, + "appetite": true, + "pregnancy": false, + "screenings": true, + "socialEconomic": false, + "relationships": true, + "misconduct": false, + "otherBehavior": "Lorem" + }, + "behaviorsDetails": { + "reassignment": "requested duty assignment", + "performance": "poor evaluations", + "episodes": "severe depression", + "selfMedication": "stopped treating headaches", + "appetite": "skipping meals", + "screenings": "positive tests", + "relationships": "separation", + "otherBehavior": "Lorem ipsum dolor sit amet." + }, + "evidence": { + "crisisCenter": true, + "counseling": true, + "family": true, + "faculty": true, + "police": true, + "medical": true, + "clergy": true, + "peers": true, + "journal": true, + "none": true, + "other": true, + "otherDetails": "photographic evidence" + }, + "traumaTreatment": false, + "treatmentProviders": { + "privateCare": true, + "vetCenter": false, + "communityCare": false, + "vamc": true, + "cboc": false, + "mtf": true, + "none": false + }, + "treatmentProvidersDetails": [ + { + "facilityInfo": "Walter Reed, Bethesda, MD", + "treatmentMonth": "02", + "treatmentYear": "2014" }, - "reportsDetails": { - "police": { - "agency": "SVI", - "city": "Dallas", - "state": "TX", - "township": "", - "country": "USA" - }, - "other": "incident report" + { + "facilityInfo": "Cedarwood Behavioral Health Center, 4321 Oak Ridge Rd, Maplewood, MN", + "treatmentYear": "2024" }, - "behaviors": { - "reassignment": true, - "absences": false, - "performance": true, - "consultations": false, - "episodes": true, - "medications": false, - "selfMedication": true, - "substances": false, - "appetite": true, - "pregnancy": false, - "screenings": true, - "socialEconomic": false, - "relationships": true, - "misconduct": false, - "otherBehavior": "Lorem" - }, - "behaviorsDetails": { - "reassignment": "requested duty assignment", - "performance": "poor evaluations", - "episodes": "severe depression", - "selfMedication": "stopped treating headaches", - "appetite": "skipping meals", - "screenings": "positive tests", - "relationships": "separation", - "otherBehavior": "Lorem ipsum dolor sit amet." - }, - "evidence": { - "crisisCenter": true, - "counseling": true, - "family": true, - "faculty": true, - "police": true, - "medical": true, - "clergy": true, - "peers": true, - "journal": true, - "none": true, - "other": true, - "otherDetails": "photographic evidence" - }, - "traumaTreatment": false, - "treatmentProviders": { - "privateCare": true, - "vetCenter": false, - "communityCare": false, - "vamc": true, - "cboc": false, - "mtf": true, - "none": false - }, - "treatmentProvidersDetails": [ - { - "facilityInfo": "Walter Reed, Bethesda, MD", - "treatmentMonth": "02", - "treatmentYear": "2014" - }, - { - "facilityInfo": "Cedarwood Behavioral Health Center, 4321 Oak Ridge Rd, Maplewood, MN", - "treatmentYear": "2024" - }, - { - "facilityInfo": "Silver Oak Recovery Center, 745 Greenfield Avenue, Clearwater, FL", - "noDates": true - } - ], - "optionIndicator": { - "yes": false, - "no": false, - "revoke": false, - "notEnrolled": true - }, - "signatureDate": "2016-01-31" - } + { + "facilityInfo": "Silver Oak Recovery Center, 745 Greenfield Avenue, Clearwater, FL", + "noDates": true + } + ], + "optionIndicator": { + "yes": false, + "no": false, + "revoke": false, + "notEnrolled": true + }, + "signatureDate": "2016-01-31" +} diff --git a/spec/fixtures/pdf_fill/21-0781V2/kitchen_sink.pdf b/spec/fixtures/pdf_fill/21-0781V2/kitchen_sink.pdf new file mode 100644 index 00000000000..b191ccd4ba3 Binary files /dev/null and b/spec/fixtures/pdf_fill/21-0781V2/kitchen_sink.pdf differ diff --git a/spec/fixtures/pdf_fill/21-0781V2/overflow.json b/spec/fixtures/pdf_fill/21-0781V2/overflow.json new file mode 100644 index 00000000000..114c01ceeb5 --- /dev/null +++ b/spec/fixtures/pdf_fill/21-0781V2/overflow.json @@ -0,0 +1,154 @@ +{ + "vaFileNumber": "12345678", + "veteranSocialSecurityNumber": "123456789", + "veteranFullName": { + "first": "XXXXXXXXXXXXX", + "middle": "Tester", + "last": "XXXXXXXXXXXXXXXXXX" + }, + "veteranDateOfBirth": "1981-11-05", + "veteranServiceNumber": "987654321", + "email": "testy.testerson@gmail.com", + "veteranPhone": "2123456789", + "veteranSecondaryPhone": "3133456789", + "additionalInformation": "Lorem ipsum dolor sit amet", + "eventTypes": { + "combat": true, + "mst": false, + "nonMst": true, + "other": false + }, + "eventsDetails": [ + { + "details": "Lorem ipsum dolor sit amet.", + "location": "abcdefghijklmn opqrstuvwxyz1234a bpqrstuvwxyz1234a", + "timing": "Summer of '70" + }, + { + "details": "Lorem ipsum dolor sit amet.", + "location": "abcdefghijklmn opqrstuvwxyz1234a bpqrstuvwxyz1234a", + "timing": "June 2007" + }, + { + "details": "Lorem ipsum dolor sit amet..", + "location": "abcdefghijklmn opqrstuvwxyz1234a bpqrstuvwxyz1234a", + "timing": "February 14, 2020" + }, + { + "details": "Lorem ipsum dolor sit amet..", + "location": "abcdefghijklmn opqrstuvwxyz1234a bpqrstuvwxyz1234a", + "timing": "Autumn of 1995" + }, + { + "details": "Lorem ipsum dolor sit amet.", + "location": "abcdefghijklmn opqrstuvwxyz1234a bpqrstuvwxyz1234a", + "timing": "Winter of '68" + }, + { + "details": "Lorem ipsum dolor sit amet.", + "location": "abcdefghijklmn opqrstuvwxyz1234a bpqrstuvwxyz1234a", + "timing": "Spring of '72" + }, + { + "details": "Lorem ipsum dolor sit amet.", + "location": "abcdefghijklmn opqrstuvwxyz1234a bpqrstuvwxyz1234a", + "timing": "Summer of '69" + } + ], + "reports": { + "yes": true, + "no": false, + "restricted": true, + "unrestricted": false, + "neither": false, + "police": true, + "other": true + }, + "reportsDetails": { + "police": { + "agency": "SVI", + "city": "Dallas", + "state": "TX", + "township": "", + "country": "USA" + }, + "other": "incident report" + }, + "behaviors": { + "reassignment": true, + "absences": false, + "performance": true, + "consultations": false, + "episodes": true, + "medications": false, + "selfMedication": true, + "substances": false, + "appetite": true, + "pregnancy": false, + "screenings": true, + "socialEconomic": false, + "relationships": true, + "misconduct": false, + "otherBehavior": "Lorem" + }, + "behaviorsDetails": { + "reassignment": "requested duty assignment", + "performance": "poor evaluations", + "episodes": "severe depression", + "selfMedication": "stopped treating headaches", + "appetite": "skipping meals", + "screenings": "positive tests", + "relationships": "separation", + "otherBehavior": "Lorem ipsum dolor sit amet." + }, + "evidence": { + "crisisCenter": true, + "counseling": true, + "family": true, + "faculty": true, + "police": true, + "medical": true, + "clergy": true, + "peers": true, + "journal": true, + "none": true, + "other": true, + "otherDetails": "photographic evidence" + }, + "traumaTreatment": false, + "treatmentProviders": { + "privateCare": true, + "vetCenter": false, + "communityCare": false, + "vamc": true, + "cboc": false, + "mtf": true, + "none": false + }, + "treatmentProvidersDetails": [ + { + "facilityInfo": "Walter Reed, Bethesda, MD", + "treatmentMonth": "02", + "treatmentYear": "2014" + }, + { + "facilityInfo": "Cedarwood Behavioral Health Center, 4321 Oak Ridge Rd, Maplewood, MN", + "treatmentYear": "2024" + }, + { + "facilityInfo": "Silver Oak Recovery Center, 745 Greenfield Avenue, Clearwater, FL", + "noDates": true + }, + { + "facilityInfo": "Silver Oak Recovery Center, 745 Greenfield Avenue, Clearwater, FL", + "noDates": true + } + ], + "optionIndicator": { + "yes": false, + "no": false, + "revoke": false, + "notEnrolled": true + }, + "signatureDate": "2016-01-31" +} diff --git a/spec/fixtures/pdf_fill/21-0781V2/overflow.pdf b/spec/fixtures/pdf_fill/21-0781V2/overflow.pdf new file mode 100644 index 00000000000..51fbdd7f49c Binary files /dev/null and b/spec/fixtures/pdf_fill/21-0781V2/overflow.pdf differ diff --git a/spec/fixtures/pdf_fill/21-0781V2/overflow_extras.pdf b/spec/fixtures/pdf_fill/21-0781V2/overflow_extras.pdf new file mode 100644 index 00000000000..86dda293774 Binary files /dev/null and b/spec/fixtures/pdf_fill/21-0781V2/overflow_extras.pdf differ diff --git a/spec/fixtures/pdf_fill/21-0781V2/simple.json b/spec/fixtures/pdf_fill/21-0781V2/simple.json new file mode 100644 index 00000000000..0319b0094a4 --- /dev/null +++ b/spec/fixtures/pdf_fill/21-0781V2/simple.json @@ -0,0 +1,16 @@ +{ + "vaFileNumber": "12345678", + "veteranSocialSecurityNumber": "123456789", + "veteranFullName": { + "first": "Testy", + "middle": "Tester", + "last": "Testerson" + }, + "veteranDateOfBirth": "1981-11-05", + "veteranServiceNumber": "987654321", + "email": "testy.testerson@gmail.com", + "veteranPhone": "2123456789", + "veteranSecondaryPhone": "3133456789", + "signatureDate": "2016-01-31" +} + \ No newline at end of file diff --git a/spec/fixtures/pdf_fill/21-0781V2/simple.pdf b/spec/fixtures/pdf_fill/21-0781V2/simple.pdf new file mode 100644 index 00000000000..aab7f4ac3f0 Binary files /dev/null and b/spec/fixtures/pdf_fill/21-0781V2/simple.pdf differ diff --git a/spec/lib/claim_documents/monitor_spec.rb b/spec/lib/claim_documents/monitor_spec.rb new file mode 100644 index 00000000000..74a75f6beda --- /dev/null +++ b/spec/lib/claim_documents/monitor_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'claim_documents/monitor' + +RSpec.describe ClaimDocuments::Monitor do + let(:service) { OpenStruct.new(uuid: 'uuid') } + let(:monitor) { described_class.new } + let(:document_stats_key) { described_class::DOCUMENT_STATS_KEY } + let(:form_id) { 'ABC123' } + let(:attachment_id) { '12345' } + let(:current_user) { create(:user) } + let(:error) { StandardError.new('An error occurred') } + + describe '#track_document_upload_attempt' do + it 'logs an upload attempt' do + expect(StatsD).to receive(:increment).with("#{document_stats_key}.attempt", tags: ["form_id:#{form_id}"]) + expect(Rails.logger).to receive(:info).with( + "Creating PersistentAttachment FormID=#{form_id}", + hash_including(user_account_uuid: current_user.user_account_uuid, statsd: "#{document_stats_key}.attempt") + ) + + monitor.track_document_upload_attempt(form_id, current_user) + end + end + + describe '#track_document_upload_success' do + it 'logs a successful upload' do + expect(StatsD).to receive(:increment).with("#{document_stats_key}.success", tags: ["form_id:#{form_id}"]) + expect(Rails.logger).to receive(:info).with( + "Success creating PersistentAttachment FormID=#{form_id} AttachmentID=#{attachment_id}", + hash_including(user_account_uuid: current_user.user_account_uuid, statsd: "#{document_stats_key}.success") + ) + + monitor.track_document_upload_success(form_id, attachment_id, current_user) + end + end + + describe '#track_document_upload_failed' do + it 'logs a failed upload' do + expect(StatsD).to receive(:increment).with("#{document_stats_key}.failure", tags: ["form_id:#{form_id}"]) + expect(Rails.logger).to receive(:error).with( + "Error creating PersistentAttachment FormID=#{form_id} AttachmentID=#{attachment_id} #{error}", + hash_including(user_account_uuid: current_user.user_account_uuid, statsd: "#{document_stats_key}.failure") + ) + + monitor.track_document_upload_failed(form_id, attachment_id, current_user, error) + end + end +end diff --git a/spec/lib/evss/disability_compensation_form/form0781_spec.rb b/spec/lib/evss/disability_compensation_form/form0781_spec.rb index 75f5dcec368..d5dba6b5286 100644 --- a/spec/lib/evss/disability_compensation_form/form0781_spec.rb +++ b/spec/lib/evss/disability_compensation_form/form0781_spec.rb @@ -4,11 +4,15 @@ require 'evss/disability_compensation_form/form0781' describe EVSS::DisabilityCompensationForm::Form0781 do - subject { described_class.new(user, form_content) } + let(:subject_v1) { described_class.new(user, form_content_v1) } + let(:subject_v2) { described_class.new(user, form_content_v2) } - let(:form_content) do + let(:form_content_v1) do JSON.parse(File.read('spec/support/disability_compensation_form/all_claims_with_0781_fe_submission.json')) end + let(:form_content_v2) do + JSON.parse(File.read('spec/support/disability_compensation_form/all_claims_with_0781v2_fe_submission.json')) + end let(:user) { build(:disabilities_compensation_user) } before do @@ -16,19 +20,39 @@ end describe '#translate' do - context 'when 781 data is present in the 526 form' do - let(:expected_output) { JSON.parse(File.read('spec/support/disability_compensation_form/form_0781.json')) } + context 'when using form v1' do + context 'when 0781 data is present in the 526 form' do + let(:expected_output) { JSON.parse(File.read('spec/support/disability_compensation_form/form_0781.json')) } + + it 'returns correctly formatted json to send to async job' do + expect(subject_v1.translate).to eq expected_output + end + end - it 'returns correctly formatted json to send to async job' do - expect(subject.translate).to eq expected_output + context 'when 0781 is not present in the 526 form' do + let(:form_content_v1) { { 'form526' => {} } } + + it 'returns a nil value' do + expect(subject_v1.translate).to eq nil + end end end - context 'when 781 is not present in the 526 form' do - let(:form_content) { { 'form526' => {} } } + context 'when using form v2' do + context 'when 0781 data is present in the 526 form' do + let(:expected_output) { JSON.parse(File.read('spec/support/disability_compensation_form/form_0781v2.json')) } - it 'returns a nil value' do - expect(subject.translate).to eq nil + it 'returns correctly formatted json to send to async job' do + expect(subject_v2.translate).to eq expected_output + end + end + + context 'when 0781 is not present in the 526 form' do + let(:form_content_v2) { { 'form526' => {} } } + + it 'returns a nil value' do + expect(subject_v2.translate).to eq nil + end end end end @@ -38,7 +62,7 @@ let(:incidents) { 'this is a test' } it 'creates the form correctly' do - expect(subject.send(:create_form, incidents)).to eq( + expect(subject_v1.send(:create_form, incidents)).to eq( 'additionalIncidentText' => nil, 'email' => 'test@email.com', 'incidents' => 'this is a test', @@ -82,7 +106,7 @@ end it 'splits the incidents on personalAssualt' do - expect(subject.send(:split_incidents, incidents)).to eq [ + expect(subject_v1.send(:split_incidents, incidents)).to eq [ [ { 'personalAssault' => true, 'test' => 'foo1' }, { 'personalAssault' => true, 'test' => 'foo2' } @@ -99,7 +123,7 @@ let(:incidents) { [] } it 'returns a nil value' do - expect(subject.send(:split_incidents, incidents)).to eq nil + expect(subject_v1.send(:split_incidents, incidents)).to eq nil end end end @@ -116,7 +140,7 @@ end it 'joins it into one string' do - expect(subject.send(:join_location, location)).to eq 'Portland, OR, USA, Apt. 1' + expect(subject_v1.send(:join_location, location)).to eq 'Portland, OR, USA, Apt. 1' end end @@ -130,7 +154,7 @@ end it 'joins it into one string' do - expect(subject.send(:join_location, location)).to eq 'Portland, USA' + expect(subject_v1.send(:join_location, location)).to eq 'Portland, USA' end end @@ -138,19 +162,33 @@ let(:location) { {} } it 'joins it into one string' do - expect(subject.send(:join_location, location)).to eq '' + expect(subject_v1.send(:join_location, location)).to eq '' end end end describe '#full_name' do - context 'when the user has a full name' do - it 'returns a hash of their name' do - expect(subject.send(:full_name)).to eq( - 'first' => 'Beyonce', - 'middle' => nil, - 'last' => 'Knowles' - ) + context 'when using form v1' do + context 'when the user has a full name' do + it 'returns a hash of their name' do + expect(subject_v1.send(:full_name)).to eq( + 'first' => 'Beyonce', + 'middle' => nil, + 'last' => 'Knowles' + ) + end + end + end + + context 'when using form v2' do + context 'when the user has a full name' do + it 'returns a hash of their name' do + expect(subject_v2.send(:full_name)).to eq( + 'first' => 'Beyonce', + 'middle' => nil, + 'last' => 'Knowles' + ) + end end end end diff --git a/spec/lib/pdf_fill/filler_spec.rb b/spec/lib/pdf_fill/filler_spec.rb index 8f0208afd1b..901bab2cf51 100644 --- a/spec/lib/pdf_fill/filler_spec.rb +++ b/spec/lib/pdf_fill/filler_spec.rb @@ -75,7 +75,7 @@ end describe '#fill_ancillary_form', run_at: '2017-07-25 00:00:00 -0400' do - %w[21-4142 21-0781a 21-0781 21-8940 28-8832 28-1900 21-674 21-0538 26-1880 5655].each do |form_id| + %w[21-4142 21-0781a 21-0781 21-0781V2 21-8940 28-8832 28-1900 21-674 21-0538 26-1880 5655].each do |form_id| context "form #{form_id}" do %w[simple kitchen_sink overflow].each do |type| context "with #{type} test data" do diff --git a/spec/lib/saml/ssoe_user_spec.rb b/spec/lib/saml/ssoe_user_spec.rb index a375342d6a9..11bc71fb51b 100644 --- a/spec/lib/saml/ssoe_user_spec.rb +++ b/spec/lib/saml/ssoe_user_spec.rb @@ -127,7 +127,7 @@ verified_at: nil, sec_id: nil, mhv_icn: nil, - mhv_correlation_id: nil, + mhv_credential_uuid: nil, mhv_account_type: nil, edipi: nil, loa: { current: 1, highest: 1 }, @@ -160,7 +160,7 @@ gender: 'M', ssn: '123123123', mhv_icn: '1200049153V217987', - mhv_correlation_id: '123456', + mhv_credential_uuid: '123456', mhv_account_type: nil, edipi: nil, uuid: 'aa478abc-e494-4af1-9f87-d002f8fe1cda', @@ -200,7 +200,7 @@ gender: nil, ssn: nil, mhv_icn: nil, - mhv_correlation_id: nil, + mhv_credential_uuid: nil, mhv_account_type: nil, edipi: nil, uuid: '54e78de6140d473f87960f211be49c08', @@ -235,7 +235,7 @@ gender: nil, ssn: nil, mhv_icn: nil, - mhv_correlation_id: nil, + mhv_credential_uuid: nil, mhv_account_type: nil, edipi: nil, uuid: '54e78de6140d473f87960f211be49c08', @@ -271,7 +271,7 @@ gender: 'M', ssn: '666271152', mhv_icn: '1008830476V316605', - mhv_correlation_id: nil, + mhv_credential_uuid: nil, mhv_account_type: nil, edipi: nil, uuid: '54e78de6140d473f87960f211be49c08', @@ -311,7 +311,7 @@ gender: nil, ssn: nil, mhv_icn: nil, - mhv_correlation_id: nil, + mhv_credential_uuid: nil, mhv_account_type: 'Advanced', uuid: '881571066e5741439652bc80759dd88c', email: 'alexmac_0@example.com', @@ -352,7 +352,7 @@ gender: 'F', ssn: '230595111', mhv_icn: '1013183292V131165', - mhv_correlation_id: '15001594', + mhv_credential_uuid: '15001594', mhv_account_type: 'Advanced', uuid: '881571066e5741439652bc80759dd88c', email: 'alexmac_0@example.com', @@ -388,7 +388,7 @@ gender: nil, ssn: nil, mhv_icn: nil, - mhv_correlation_id: nil, + mhv_credential_uuid: nil, mhv_account_type: 'Basic', uuid: '72782a87a807407f83e8a052d804d7f7', email: 'pv+mhvtestb@example.com', @@ -427,7 +427,7 @@ gender: 'M', ssn: '666811850', mhv_icn: '1012853550V207686', - mhv_correlation_id: '12345748', + mhv_credential_uuid: '12345748', mhv_account_type: 'Premium', uuid: '0e1bb5723d7c4f0686f46ca4505642ad', email: 'k+tristanmhv@example.com', @@ -467,7 +467,7 @@ gender: 'M', ssn: '666811850', mhv_icn: '1012853550V207686', - mhv_correlation_id: '12345748', + mhv_credential_uuid: '12345748', mhv_account_type: 'Premium', uuid: nil, email: 'k+tristanmhv@example.com', @@ -497,7 +497,7 @@ it 'resolves mhv id' do expect(subject.to_hash).to include( - mhv_correlation_id: '999888' + mhv_credential_uuid: '999888' ) end @@ -516,7 +516,7 @@ it 'resolves mhv id' do expect(subject.to_hash).to include( - mhv_correlation_id: mhv_ien + mhv_credential_uuid: mhv_ien ) end @@ -535,7 +535,7 @@ it 'resolves mhv id' do expect(subject.to_hash).to include( - mhv_correlation_id: mhv_ien + mhv_credential_uuid: mhv_ien ) end @@ -544,7 +544,7 @@ end end - context 'with mismatching identifiers' do + context 'with multiple mhv id values identifiers' do let(:mhv_uuid) { '999888' } let(:mhv_ien) { '888777' } let(:mhv_icn) { '123456789V98765431' } @@ -557,29 +557,26 @@ let(:expected_error) { SAML::UserAttributeError::ERRORS[:multiple_mhv_ids] } let(:expected_error_data) { { mismatched_ids: [mhv_ien, mhv_uuid], icn: mhv_icn } } let(:expected_error_message) { expected_error[:message] } - let(:expected_log) { "[SAML::UserAttributes::SSOe] #{expected_error_message}, #{expected_error_data}" } + let(:expected_log) { "[SAML::UserAttributes::SSOe] #{expected_error_message}" } it 'resolves mhv id from credential provider' do expect(subject.to_hash).to include( - mhv_correlation_id: mhv_uuid + mhv_credential_uuid: mhv_uuid ) end context 'normal validation flow' do - it 'does not validate and throws an error' do - expect(Rails.logger).to receive(:warn).with(expected_log) - expect { subject.validate! }.to raise_error { |error| - expect(error).to be_a(SAML::UserAttributeError) - expect(error.message).to eq(expected_error_message) - } + it 'logs a warning and doesnt raise an error' do + expect(Rails.logger).to receive(:warn).with(expected_log, expected_error_data) + expect { subject.validate! }.not_to raise_error end end context 'MHV outbound-redirect flow' do let(:client_id) { 'mhv' } - it 'does not validate and logs a Sentry warning' do - expect(Rails.logger).to receive(:warn).with(expected_log) + it 'logs a warning and doesnt raise an error' do + expect(Rails.logger).to receive(:warn).with(expected_log, expected_error_data) expect { subject.validate! }.not_to raise_error end end @@ -595,11 +592,11 @@ end let(:expected_error_data) { { mismatched_ids: [va_eauth_icn, va_eauth_mhvicn], icn: va_eauth_icn } } let(:expected_error_message) { SAML::UserAttributeError::ERRORS[:mhv_icn_mismatch][:message] } - let(:expected_log) { "[SAML::UserAttributes::SSOe] #{expected_error_message}, #{expected_error_data}" } + let(:expected_log) { "[SAML::UserAttributes::SSOe] #{expected_error_message}" } context 'normal validation flow' do it 'does not validate' do - expect(Rails.logger).to receive(:warn).with(expected_log) + expect(Rails.logger).to receive(:warn).with(expected_log, expected_error_data) expect { subject.validate! }.to raise_error { |error| expect(error).to be_a(SAML::UserAttributeError) expect(error.message).to eq(expected_error_message) @@ -611,7 +608,7 @@ let(:client_id) { 'myvahealth' } it 'does not validate and logs a Sentry warning' do - expect(Rails.logger).to receive(:warn).with(expected_log) + expect(Rails.logger).to receive(:warn).with(expected_log, expected_error_data) expect { subject.validate! }.not_to raise_error end end @@ -634,7 +631,7 @@ it 'de-duplicates values' do expect(subject.to_hash).to include( - mhv_correlation_id: '888777' + mhv_credential_uuid: '888777' ) end @@ -649,7 +646,7 @@ it 'de-duplicates values' do expect(subject.to_hash).to include( - mhv_correlation_id: '888777' + mhv_credential_uuid: '888777' ) end @@ -664,7 +661,7 @@ it 'de-duplicates values' do expect(subject.to_hash).to include( - mhv_correlation_id: nil + mhv_credential_uuid: nil ) end @@ -680,7 +677,7 @@ it 'de-duplicates values' do expect(subject.to_hash).to include( - mhv_correlation_id: '888777' + mhv_credential_uuid: '888777' ) end @@ -696,15 +693,11 @@ let(:expected_error) { SAML::UserAttributeError::ERRORS[:multiple_mhv_ids] } let(:expected_error_data) { { mismatched_ids: [first_ien, second_ien], icn: mhv_icn } } let(:expected_error_message) { expected_error[:message] } - let(:expected_log) { "[SAML::UserAttributes::SSOe] #{expected_error_message}, #{expected_error_data}" } + let(:expected_log) { "[SAML::UserAttributes::SSOe] #{expected_error_message}" } - it 'does not validate' do - expect(Rails.logger).to receive(:warn).with(expected_log) - expect { subject.validate! } - .to raise_error { |error| - expect(error).to be_a(SAML::UserAttributeError) - expect(error.message).to eq(expected_error_message) - } + it 'logs a warning but does not raise an error' do + expect(Rails.logger).to receive(:warn).with(expected_log, expected_error_data) + expect { subject.validate! }.not_to raise_error end end @@ -715,15 +708,11 @@ let(:expected_error) { SAML::UserAttributeError::ERRORS[:multiple_mhv_ids] } let(:expected_error_data) { { mismatched_ids: [first_ien, second_ien], icn: mhv_icn } } let(:expected_error_message) { expected_error[:message] } - let(:expected_log) { "[SAML::UserAttributes::SSOe] #{expected_error_message}, #{expected_error_data}" } + let(:expected_log) { "[SAML::UserAttributes::SSOe] #{expected_error_message}" } - it 'does not validate' do - expect(Rails.logger).to receive(:warn).with(expected_log) - expect { subject.validate! } - .to raise_error { |error| - expect(error).to be_a(SAML::UserAttributeError) - expect(error.message).to eq(expected_error_message) - } + it 'logs a warning but does not raise an error' do + expect(Rails.logger).to receive(:warn).with(expected_log, expected_error_data) + expect { subject.validate! }.not_to raise_error end end end @@ -776,11 +765,11 @@ let(:expected_error) { SAML::UserAttributeError::ERRORS[:multiple_corp_ids] } let(:expected_error_data) { { mismatched_ids: [first_corp_id, second_corp_id], icn: mhv_icn } } let(:expected_error_message) { expected_error[:message] } - let(:expected_log) { "[SAML::UserAttributes::SSOe] #{expected_error_message}, #{expected_error_data}" } + let(:expected_log) { "[SAML::UserAttributes::SSOe] #{expected_error_message}" } context 'regular auth flow' do it 'does not validate and prevents login' do - expect(Rails.logger).to receive(:warn).with(expected_log) + expect(Rails.logger).to receive(:warn).with(expected_log, expected_error_data) expect { subject.validate! } .to raise_error { |error| expect(error).to be_a(SAML::UserAttributeError) @@ -793,7 +782,7 @@ let(:client_id) { 'mhv' } it 'logs a Sentry warning and allows login' do - expect(Rails.logger).to receive(:warn).with(expected_log) + expect(Rails.logger).to receive(:warn).with(expected_log, expected_error_data) expect { subject.validate! }.not_to raise_error end end @@ -802,7 +791,7 @@ let(:client_id) { 'test application' } it 'does not validate and prevents login' do - expect(Rails.logger).to receive(:warn).with(expected_log) + expect(Rails.logger).to receive(:warn).with(expected_log, expected_error_data) expect { subject.validate! } .to raise_error { |error| expect(error).to be_a(SAML::UserAttributeError) @@ -828,10 +817,10 @@ let(:expected_error) { SAML::UserAttributeError::ERRORS[:multiple_edipis] } let(:expected_error_data) { { mismatched_ids: [first_edipi, second_edipi], icn: mhv_icn } } let(:expected_error_message) { expected_error[:message] } - let(:expected_log) { "[SAML::UserAttributes::SSOe] #{expected_error_message}, #{expected_error_data}" } + let(:expected_log) { "[SAML::UserAttributes::SSOe] #{expected_error_message}" } it 'does not validate' do - expect(Rails.logger).to receive(:warn).with(expected_log) + expect(Rails.logger).to receive(:warn).with(expected_log, expected_error_data) expect { subject.validate! } .to raise_error { |error| expect(error).to be_a(SAML::UserAttributeError) @@ -888,7 +877,7 @@ gender: nil, ssn: nil, mhv_icn: nil, - mhv_correlation_id: nil, + mhv_credential_uuid: nil, mhv_account_type: nil, uuid: '0e1bb5723d7c4f0686f46ca4505642ad', email: 'kam+tristanmhv@adhocteam.us', @@ -928,7 +917,7 @@ gender: 'M', ssn: '666016789', mhv_icn: '1013173963V366678', - mhv_correlation_id: nil, + mhv_credential_uuid: nil, mhv_account_type: nil, uuid: '363761e8857642f7b77ef7d99200e711', email: 'iam.tester@example.com', @@ -968,7 +957,7 @@ gender: 'M', ssn: '796123607', mhv_icn: '1012740600V714187', - mhv_correlation_id: '14384899', + mhv_credential_uuid: '14384899', mhv_account_type: nil, uuid: '1655c16aa0784dbe973814c95bd69177', email: 'Test0206@gmail.com', @@ -1007,7 +996,7 @@ gender: 'M', ssn: '796123607', mhv_icn: '1012740600V714187', - mhv_correlation_id: '14384899', + mhv_credential_uuid: '14384899', mhv_account_type: nil, uuid: '1655c16aa0784dbe973814c95bd69177', email: 'Test0206@gmail.com', @@ -1063,7 +1052,7 @@ gender: 'F', ssn: '101174874', mhv_icn: '1012779219V964737', - mhv_correlation_id: nil, + mhv_credential_uuid: nil, mhv_account_type: nil, uuid: nil, email: nil, @@ -1116,7 +1105,7 @@ gender: 'M', ssn: '666872589', mhv_icn: '1013062086V794840', - mhv_correlation_id: '15093546', + mhv_credential_uuid: '15093546', mhv_account_type: nil, uuid: '53f065475a794e14a32d707bfd9b215f', email: nil, @@ -1154,7 +1143,7 @@ gender: 'M', ssn: '666271152', mhv_icn: '1012827134V054550', - mhv_correlation_id: '10894456', + mhv_credential_uuid: '10894456', mhv_account_type: nil, uuid: '54e78de6140d473f87960f211be49c08', email: 'vets.gov.user+262@gmail.com', diff --git a/spec/lib/sign_in/idme/service_spec.rb b/spec/lib/sign_in/idme/service_spec.rb index 0a06df3991a..a39e317f517 100644 --- a/spec/lib/sign_in/idme/service_spec.rb +++ b/spec/lib/sign_in/idme/service_spec.rb @@ -456,7 +456,7 @@ credential_aal_highest: 2, credential_ial_highest: 'classic_loa3', email:, - mhv_uuid: mhv_correlation_id, + mhv_uuid: mhv_credential_uuid, mhv_icn:, mhv_assurance:, level_of_assurance: 3, @@ -467,12 +467,12 @@ } ) end - let(:mhv_correlation_id) { 'some-mhv-correlation-id' } + let(:mhv_credential_uuid) { 'some-mhv-credential-uuid' } let(:mhv_icn) { 'some-mhv-icn' } let(:mhv_assurance) { 'some-mhv-assurance' } let(:expected_attributes) do expected_standard_attributes.merge({ mhv_icn:, - mhv_correlation_id:, + mhv_credential_uuid:, mhv_assurance: }) end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 868c67c8794..0022258933d 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -495,34 +495,60 @@ end end - context 'MHV ids' do - let(:user) { build(:user, :loa3, mhv_correlation_id: nil, participant_id:) } - let(:participant_id) { 'some_mpi_participant_id' } - - context 'when mhv ids are nil' do - let(:participant_id) { nil } + describe '#mhv_correlation_id' do + let(:user) { build(:user, :loa3, mpi_profile:) } + let(:mhv_user_account) { build(:mhv_user_account, user_profile_id: mhv_account_id) } + let(:mpi_profile) { build(:mpi_profile, active_mhv_ids:) } + let(:mhv_account_id) { 'some-id' } + let(:active_mhv_ids) { [mhv_account_id] } + + context 'when mhv_user_account is present' do + before do + allow(user).to receive(:mhv_user_account).and_return(mhv_user_account) + end - it 'has a mhv correlation id of nil' do - expect(user.mhv_correlation_id).to be_nil + it 'returns the user_profile_id from the mhv_user_account' do + expect(user.mhv_correlation_id).to eq(mhv_account_id) end end - context 'when there are mhv ids' do - it 'fetches mhv correlation id from MPI' do - expect(user.mhv_correlation_id).to eq(user.send(:mpi_profile).mhv_ids.first) - expect(user.mhv_correlation_id).to eq(user.send(:mpi_profile).active_mhv_ids.first) + context 'when mhv_user_account is not present' do + before do + allow(user).to receive(:mhv_user_account).and_return(nil) end - it 'fetches mhv_ids from MPI' do - expect(user.mhv_ids).to be(user.send(:mpi_profile).mhv_ids) + context 'when the user has one active_mhv_ids' do + it 'returns the active_mhv_id' do + expect(user.mhv_correlation_id).to eq(active_mhv_ids.first) + end end - it 'fetches active_mhv_ids from MPI' do - expect(user.active_mhv_ids).to be(user.send(:mpi_profile).active_mhv_ids) + context 'when the user has multiple active_mhv_ids' do + let(:active_mhv_ids) { %w[some-id another-id] } + + it 'returns nil' do + expect(user.mhv_correlation_id).to be_nil + end end end end + describe '#mhv_ids' do + let(:user) { build(:user, :loa3) } + + it 'fetches mhv_ids from MPI' do + expect(user.mhv_ids).to be(user.send(:mpi_profile).mhv_ids) + end + end + + describe '#active_mhv_ids' do + let(:user) { build(:user, :loa3) } + + it 'fetches active_mhv_ids from MPI' do + expect(user.active_mhv_ids).to be(user.send(:mpi_profile).active_mhv_ids) + end + end + describe '#participant_id' do let(:user) { build(:user, :loa3, participant_id: mpi_participant_id) } let(:mpi_participant_id) { 'some_mpi_participant_id' } @@ -1079,18 +1105,18 @@ described_class.new( build(:user, :loa3, idme_uuid:, logingov_uuid:, - edipi:, mhv_correlation_id:, authn_context:) + edipi:, mhv_credential_uuid:, authn_context:) ) end let(:user_verifier_object) do OpenStruct.new({ idme_uuid:, logingov_uuid:, sign_in: user.identity_sign_in, - edipi:, mhv_correlation_id: }) + edipi:, mhv_credential_uuid: }) end let(:authn_context) { LOA::IDME_LOA1_VETS } let(:logingov_uuid) { 'some-logingov-uuid' } let(:idme_uuid) { 'some-idme-uuid' } let(:edipi) { 'some-edipi' } - let(:mhv_correlation_id) { 'some-mhv-correlation-id' } + let(:mhv_credential_uuid) { 'some-mhv-credential-uuid' } let!(:user_verification) { Login::UserVerifier.new(user_verifier_object).perform } let!(:user_account) { user_verification&.user_account } @@ -1102,14 +1128,14 @@ context 'when user is logged in with mhv' do let(:authn_context) { 'myhealthevet' } - context 'and there is an mhv_correlation_id' do - it 'returns user verification with a matching mhv_correlation_id' do - expect(user.user_verification.mhv_uuid).to eq(mhv_correlation_id) + context 'and there is an mhv_credential_uuid' do + it 'returns user verification with a matching mhv_credential_uuid' do + expect(user.user_verification.mhv_uuid).to eq(mhv_credential_uuid) end end - context 'and there is not an mhv_correlation_id' do - let(:mhv_correlation_id) { nil } + context 'and there is not an mhv_credential_uuid' do + let(:mhv_credential_uuid) { nil } context 'and user has an idme_uuid' do let(:idme_uuid) { 'some-idme-uuid' } @@ -1305,8 +1331,8 @@ let(:expected_log_message) { '[User] mhv_user_account error' } let(:expected_log_payload) { { error_message: /#{expected_error_message}/, icn: user.icn } } - it 'logs and re-raises the error' do - expect { user.mhv_user_account }.to raise_error(MHV::UserAccount::Errors::UserAccountError) + it 'logs and returns nil' do + expect(user.mhv_user_account).to be_nil expect(Rails.logger).to have_received(:info).with(expected_log_message, expected_log_payload) end end diff --git a/spec/requests/swagger_spec.rb b/spec/requests/swagger_spec.rb index 4b579630800..bc21779cef4 100644 --- a/spec/requests/swagger_spec.rb +++ b/spec/requests/swagger_spec.rb @@ -46,7 +46,7 @@ let(:headers) { { '_headers' => { 'Cookie' => sign_in(mhv_user, nil, true) } } } before do - create(:mhv_user_verification, mhv_uuid: mhv_user.mhv_correlation_id) + create(:mhv_user_verification, mhv_uuid: mhv_user.mhv_credential_uuid) end describe 'backend statuses' do @@ -296,25 +296,27 @@ end it 'supports adding a claim document' do - expect(subject).to validate( - :post, - '/v0/claim_attachments', - 200, - '_data' => { - 'form_id' => '21P-530EZ', - file: fixture_file_upload('spec/fixtures/files/doctors-note.pdf') - } - ) + VCR.use_cassette('uploads/validate_document') do + expect(subject).to validate( + :post, + '/v0/claim_attachments', + 200, + '_data' => { + 'form_id' => '21P-530EZ', + file: fixture_file_upload('spec/fixtures/files/doctors-note.pdf') + } + ) - expect(subject).to validate( - :post, - '/v0/claim_attachments', - 422, - '_data' => { - 'form_id' => '21P-530EZ', - file: fixture_file_upload('spec/fixtures/files/empty_file.txt') - } - ) + expect(subject).to validate( + :post, + '/v0/claim_attachments', + 422, + '_data' => { + 'form_id' => '21P-530EZ', + file: fixture_file_upload('spec/fixtures/files/empty_file.txt') + } + ) + end end it 'supports checking stem_claim_status' do diff --git a/spec/requests/v0/claim_documents_spec.rb b/spec/requests/v0/claim_documents_spec.rb index 32446d2818d..ec1a0148505 100644 --- a/spec/requests/v0/claim_documents_spec.rb +++ b/spec/requests/v0/claim_documents_spec.rb @@ -16,69 +16,80 @@ end it 'uploads a file' do - params = { file:, form_id: '21P-527EZ' } - expect do - post('/v0/claim_documents', params:) - end.to change(PersistentAttachment, :count).by(1) - expect(response).to have_http_status(:ok) - resp = JSON.parse(response.body) - expect(resp['data']['attributes'].keys.sort).to eq(%w[confirmation_code name size]) - expect(PersistentAttachment.last).to be_a(PersistentAttachments::PensionBurial) + VCR.use_cassette('uploads/validate_document') do + params = { file:, form_id: '21P-527EZ' } + expect do + post('/v0/claim_documents', params:) + end.to change(PersistentAttachment, :count).by(1) + expect(response).to have_http_status(:ok) + resp = JSON.parse(response.body) + expect(resp['data']['attributes'].keys.sort).to eq(%w[confirmation_code name size]) + expect(PersistentAttachment.last).to be_a(PersistentAttachments::PensionBurial) + end end it 'uploads a file to the alternate route' do - params = { file:, form_id: '21P-527EZ' } - expect do - post('/v0/claim_attachments', params:) - end.to change(PersistentAttachment, :count).by(1) - expect(response).to have_http_status(:ok) - resp = JSON.parse(response.body) - expect(resp['data']['attributes'].keys.sort).to eq(%w[confirmation_code name size]) - expect(PersistentAttachment.last).to be_a(PersistentAttachments::PensionBurial) + VCR.use_cassette('uploads/validate_document') do + params = { file:, form_id: '21P-527EZ' } + expect do + post('/v0/claim_attachments', params:) + end.to change(PersistentAttachment, :count).by(1) + expect(response).to have_http_status(:ok) + resp = JSON.parse(response.body) + expect(resp['data']['attributes'].keys.sort).to eq(%w[confirmation_code name size]) + expect(PersistentAttachment.last).to be_a(PersistentAttachments::PensionBurial) + end end it 'logs a successful upload' do - expect(Rails.logger).to receive(:info).with('Creating PersistentAttachment FormID=21P-527EZ') - expect(Rails.logger).to receive(:info).with( - /^Success creating PersistentAttachment FormID=21P-527EZ AttachmentID=\d+/ - ) - expect(Rails.logger).not_to receive(:error).with( - 'Error creating PersistentAttachment FormID=21P-527EZ AttachmentID= Common::Exceptions::ValidationErrors' - ) + VCR.use_cassette('uploads/validate_document') do + expect(Rails.logger).to receive(:info).with('Creating PersistentAttachment FormID=21P-527EZ', instance_of(Hash)) + expect(Rails.logger).to receive(:info).with( + /^Success creating PersistentAttachment FormID=21P-527EZ AttachmentID=\d+/, instance_of(Hash) + ) + expect(Rails.logger).not_to receive(:error).with( + 'Error creating PersistentAttachment FormID=21P-527EZ AttachmentID= Common::Exceptions::ValidationErrors' + ) - params = { file:, form_id: '21P-527EZ' } - expect do - post('/v0/claim_documents', params:) - end.to change(PersistentAttachment, :count).by(1) + params = { file:, form_id: '21P-527EZ' } + expect do + post('/v0/claim_documents', params:) + end.to change(PersistentAttachment, :count).by(1) + end end end context 'with an invalid file' do - let(:file) do - fixture_file_upload('empty_file.txt', 'text/plain') - end + let(:file) { fixture_file_upload('empty-file.jpg') } it 'does not upload the file' do - params = { file:, form_id: '21P-527EZ' } - expect do - post('/v0/claim_attachments', params:) - end.not_to change(PersistentAttachment, :count) - expect(response).to have_http_status(:unprocessable_entity) - resp = JSON.parse(response.body) - expect(resp['errors'][0]['title']).to eq('File size must not be less than 1.0 KB') + VCR.use_cassette('uploads/validate_document') do + params = { file:, form_id: '21P-527EZ' } + expect do + post('/v0/claim_attachments', params:) + end.not_to change(PersistentAttachment, :count) + expect(response).to have_http_status(:unprocessable_entity) + resp = JSON.parse(response.body) + expect(resp['errors'][0]['detail']).to eq('File size must not be less than 1.0 KB') + end end it 'logs the error' do - expect(Rails.logger).to receive(:info).with('Creating PersistentAttachment FormID=21P-527EZ') - expect(Rails.logger).not_to receive(:info).with( - /^Success creating PersistentAttachment FormID=21P-527EZ AttachmentID=\d+/ - ) - expect(Rails.logger).to receive(:error).with( - 'Error creating PersistentAttachment FormID=21P-527EZ AttachmentID= Common::Exceptions::ValidationErrors' - ) + VCR.use_cassette('uploads/validate_document') do + expect(Rails.logger).to receive(:info).with('Creating PersistentAttachment FormID=21P-527EZ', + hash_including(user_account_uuid: nil, + statsd: 'api.claim_documents.attempt')) + expect(Rails.logger).not_to receive(:info).with( + /^Success creating PersistentAttachment FormID=21P-527EZ AttachmentID=\d+/ + ) + expect(Rails.logger).to receive(:error).with( + 'Error creating PersistentAttachment FormID=21P-527EZ AttachmentID= Common::Exceptions::UnprocessableEntity', + instance_of(Hash) + ) - params = { file:, form_id: '21P-527EZ' } - post('/v0/claim_attachments', params:) + params = { file:, form_id: '21P-527EZ' } + post('/v0/claim_attachments', params:) + end end end @@ -88,9 +99,11 @@ end it 'does not raise an error when password is correct' do - params = { file:, form_id: '26-1880', password: 'test' } - post('/v0/claim_attachments', params:) - expect(response).to have_http_status(:ok) + VCR.use_cassette('uploads/validate_document') do + params = { file:, form_id: '26-1880', password: 'test' } + post('/v0/claim_attachments', params:) + expect(response).to have_http_status(:ok) + end end it 'raises an error when password is incorrect' do diff --git a/spec/requests/v0/test_account_user_emails_spec.rb b/spec/requests/v0/test_account_user_emails_spec.rb new file mode 100644 index 00000000000..240fc2e9002 --- /dev/null +++ b/spec/requests/v0/test_account_user_emails_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'V0::TestAccountUserEmails', type: :request do + describe 'POST #create' do + subject { post '/v0/test_account_user_email', params: } + + let(:email) { 'some-email' } + let(:email_redis_key) { 'some-email-redis-key' } + let(:params) { { email: email } } + let(:rendered_error) { { 'errors' => 'invalid params' } } + + before do + allow(SecureRandom).to receive(:uuid).and_return(email_redis_key) + allow(Rails.logger).to receive(:info) + end + + shared_context 'bad_request' do + it 'responds with bad request status' do + subject + + assert_response :bad_request + end + + it 'responds with error' do + subject + + expect(JSON.parse(response.body)).to eq(rendered_error) + end + end + + context 'when params does not include email' do + let(:params) { { some_params: 'some-params' } } + + it_behaves_like 'bad_request' + end + + context 'when params include email' do + let(:params) { { email: email } } + + context 'and email param is empty' do + let(:email) { '' } + + it_behaves_like 'bad_request' + end + + context 'and email param is not empty' do + let(:email) { 'some-email' } + let(:expected_log_message) { "[V0][TestAccountUserEmailsController] create, key:#{email_redis_key}" } + + it 'responds with created status' do + subject + + assert_response :created + end + + it 'responds with test_account_user_email_uuid' do + subject + + expect(JSON.parse(response.body)['test_account_user_email_uuid']).to eq(email_redis_key) + end + + it 'makes a create log to rails logger' do + subject + + expect(Rails.logger).to have_received(:info).with(expected_log_message) + end + end + end + end +end diff --git a/spec/requests/v0/user_spec.rb b/spec/requests/v0/user_spec.rb index 6564c741618..51045023dbb 100644 --- a/spec/requests/v0/user_spec.rb +++ b/spec/requests/v0/user_spec.rb @@ -11,7 +11,7 @@ let(:mhv_user) { build(:user, :mhv) } let(:v0_user_request_headers) { {} } let(:edipi) { '1005127153' } - let!(:mhv_user_verification) { create(:mhv_user_verification, mhv_uuid: mhv_user.mhv_correlation_id) } + let!(:mhv_user_verification) { create(:mhv_user_verification, mhv_uuid: mhv_user.mhv_credential_uuid) } before do allow(SM::Client).to receive(:new).and_return(authenticated_client) @@ -134,7 +134,7 @@ end context 'with missing MHV accounts' do - let(:mhv_user) { build(:user, :mhv, mhv_ids: nil, active_mhv_ids: nil, mhv_correlation_id: nil) } + let(:mhv_user) { build(:user, :mhv, mhv_ids: nil, active_mhv_ids: nil, mhv_credential_uuid: nil) } let!(:mhv_user_verification) { create(:mhv_user_verification, backing_idme_uuid: mhv_user.idme_uuid) } before do diff --git a/spec/services/login/after_login_actions_spec.rb b/spec/services/login/after_login_actions_spec.rb index c12262c9d94..bf18310db1e 100644 --- a/spec/services/login/after_login_actions_spec.rb +++ b/spec/services/login/after_login_actions_spec.rb @@ -203,7 +203,7 @@ end context 'MHV correlation id validation' do - let(:expected_identity_value) { loa3_user.identity.mhv_correlation_id } + let(:expected_identity_value) { loa3_user.identity.mhv_credential_uuid } let(:expected_mpi_value) { loa3_user.mpi_mhv_correlation_id } let(:validation_id) { 'MHV Correlation ID' } diff --git a/spec/services/login/user_verifier_spec.rb b/spec/services/login/user_verifier_spec.rb index 1e2711522e2..21cabc7c470 100644 --- a/spec/services/login/user_verifier_spec.rb +++ b/spec/services/login/user_verifier_spec.rb @@ -11,7 +11,7 @@ { edipi: edipi_identifier, sign_in: { service_name: login_value, auth_broker: }, - mhv_correlation_id: mhv_correlation_id_identifier, + mhv_credential_uuid: mhv_credential_uuid_identifier, idme_uuid: idme_uuid_identifier, logingov_uuid: logingov_uuid_identifier, icn:, @@ -22,7 +22,7 @@ end let(:auth_broker) { 'some-auth-broker' } let(:edipi_identifier) { 'some-edipi' } - let(:mhv_correlation_id_identifier) { 'some-correlation-id' } + let(:mhv_credential_uuid_identifier) { 'some-credential=uuid' } let(:idme_uuid_identifier) { 'some-idme-uuid' } let(:logingov_uuid_identifier) { 'some-logingov-uuid' } let(:locked) { false } @@ -42,7 +42,7 @@ shared_examples 'user_verification with nil credential identifier' do let(:authn_identifier) { nil } let(:edipi_identifier) { authn_identifier } - let(:mhv_correlation_id_identifier) { authn_identifier } + let(:mhv_credential_uuid_identifier) { authn_identifier } let(:idme_uuid_identifier) { authn_identifier } let(:logingov_uuid_identifier) { authn_identifier } let(:expected_log) { "[Login::UserVerifier] Nil identifier for type=#{authn_identifier_type}" } @@ -391,7 +391,7 @@ context 'when user credential is mhv' do let(:login_value) { SignIn::Constants::Auth::MHV } - let(:authn_identifier) { user_identity.mhv_correlation_id } + let(:authn_identifier) { user_identity.mhv_credential_uuid } let(:authn_identifier_type) { :mhv_uuid } let(:backing_idme_uuid) { idme_uuid_identifier } let(:linked_user_verification_type) { :mhv_user_verification } @@ -410,7 +410,7 @@ context 'when credential identifier is nil' do let(:authn_identifier) { nil } let(:edipi_identifier) { authn_identifier } - let(:mhv_correlation_id_identifier) { authn_identifier } + let(:mhv_credential_uuid_identifier) { authn_identifier } let(:idme_uuid_identifier) { authn_identifier } let(:logingov_uuid_identifier) { authn_identifier } let(:expected_log) { "[Login::UserVerifier] Nil identifier for type=#{authn_identifier_type}" } diff --git a/spec/services/sign_in/attribute_validator_spec.rb b/spec/services/sign_in/attribute_validator_spec.rb index f1ce7697a83..f1cc08463b1 100644 --- a/spec/services/sign_in/attribute_validator_spec.rb +++ b/spec/services/sign_in/attribute_validator_spec.rb @@ -32,13 +32,13 @@ service_name:, auto_uplevel:, mhv_icn:, - mhv_correlation_id:, + mhv_credential_uuid:, edipi: } end let(:logingov_uuid) { nil } let(:idme_uuid) { nil } - let(:mhv_correlation_id) { nil } + let(:mhv_credential_uuid) { nil } let(:edipi) { nil } let(:current_ial) { SignIn::Constants::Auth::IAL_TWO } let(:ssn) { nil } @@ -131,15 +131,6 @@ it_behaves_like 'error response' end - context 'when mpi record for user has multiple mhv ids' do - let(:mhv_iens) { %w[some-mhv-ien some-other-mhv-ien] } - let(:expected_error) { SignIn::Errors::MPIMalformedAccountError } - let(:expected_error_message) { 'User attributes contain multiple distinct MHV_ID values' } - let(:expected_error_code) { SignIn::Constants::ErrorCode::MULTIPLE_MHV_IEN } - - it_behaves_like 'error response' - end - context 'when mpi record for user has multiple participant ids' do let(:participant_ids) { %w[some-participant-id some-other-participant-id] } let(:expected_error) { SignIn::Errors::MPIMalformedAccountError } @@ -148,6 +139,29 @@ it_behaves_like 'error response' end + + context 'when mpi record for user has multiple mhv ids' do + let(:mhv_iens) { %w[some-mhv-ien some-other-mhv-ien] } + let(:expected_error_message) { 'User attributes contain multiple distinct MHV_ID values' } + let(:expected_error_log) { 'attribute validator error' } + let(:expected_error_log_payload) do + { errors: expected_error_message, + credential_uuid: csp_id, + mhv_icn:, + type: service_name }.compact + end + let(:auto_uplevel) { true } + + it 'logs the error' do + subject + expect(Rails.logger).to have_received(:info).with(a_string_including(expected_error_log), + expected_error_log_payload) + end + + it 'does not raise an error' do + expect { subject }.not_to raise_error + end + end end shared_examples 'mpi versus credential mismatch' do @@ -368,9 +382,9 @@ let(:idme_uuid) { 'some-idme-uuid' } let(:csp_id) { idme_uuid } let(:address) { nil } - let(:mhv_correlation_id) { 'some-mhv-correlation-id' } + let(:mhv_credential_uuid) { 'some-mhv-correlation-id' } let(:email) { 'some-email' } - let(:identifier) { mhv_correlation_id } + let(:identifier) { mhv_credential_uuid } let(:identifier_type) { MPI::Constants::MHV_UUID } context 'and credential is missing mhv icn' do @@ -381,7 +395,7 @@ end context 'and credential is missing mhv correlation id' do - let(:mhv_correlation_id) { nil } + let(:mhv_credential_uuid) { nil } let(:attribute) { 'mhv_uuid' } it_behaves_like 'missing credential attribute' diff --git a/spec/services/users/profile_spec.rb b/spec/services/users/profile_spec.rb index 97c35b999f5..2f42bea610f 100644 --- a/spec/services/users/profile_spec.rb +++ b/spec/services/users/profile_spec.rb @@ -114,7 +114,7 @@ context 'mhv user' do let(:user) { create(:user, :mhv) } - let!(:user_verification) { create(:mhv_user_verification, mhv_uuid: user.mhv_correlation_id) } + let!(:user_verification) { create(:mhv_user_verification, mhv_uuid: user.mhv_credential_uuid) } it 'includes sign_in' do expect(profile[:sign_in]).to eq(service_name: SAML::User::MHV_ORIGINAL_CSID, diff --git a/spec/sidekiq/evss/disability_compensation_form/submit_form0781_spec.rb b/spec/sidekiq/evss/disability_compensation_form/submit_form0781_spec.rb index 5c7977fef01..6b499b0a7f5 100644 --- a/spec/sidekiq/evss/disability_compensation_form/submit_form0781_spec.rb +++ b/spec/sidekiq/evss/disability_compensation_form/submit_form0781_spec.rb @@ -27,6 +27,9 @@ let(:form0781) do File.read 'spec/support/disability_compensation_form/submissions/with_0781.json' end + let(:form0781v2) do + File.read 'spec/support/disability_compensation_form/submissions/with_0781v2.json' + end VCR.configure do |c| c.default_cassette_options = { @@ -49,50 +52,101 @@ end describe '.perform_async' do - let(:submission) do - Form526Submission.create(user_uuid: user.uuid, - auth_headers_json: auth_headers.to_json, - saved_claim_id: saved_claim.id, - form_json: form0781, - submitted_claim_id: evss_claim_id) - end + context 'when a submission has both 0781 and 0781a' do + let(:submission) do + Form526Submission.create(user_uuid: user.uuid, + auth_headers_json: auth_headers.to_json, + saved_claim_id: saved_claim.id, + form_json: form0781, + submitted_claim_id: evss_claim_id) + end + + context 'with a successful submission job' do + it 'queues a job for submit' do + expect do + subject.perform_async(submission.id) + end.to change(subject.jobs, :size).by(1) + end + + it 'submits successfully' do + VCR.use_cassette('evss/disability_compensation_form/submit_0781') do + subject.perform_async(submission.id) + jid = subject.jobs.last['jid'] + described_class.drain + expect(jid).not_to be_empty + end + end + end + + context 'with a submission timeout' do + before do + allow_any_instance_of(Faraday::Connection).to receive(:post).and_raise(Faraday::TimeoutError) + end - context 'with a successful submission job' do - it 'queues a job for submit' do - expect do + it 'raises a gateway timeout error' do subject.perform_async(submission.id) - end.to change(subject.jobs, :size).by(1) + expect { described_class.drain }.to raise_error(StandardError) + end end - it 'submits successfully' do - VCR.use_cassette('evss/disability_compensation_form/submit_0781') do + context 'with an unexpected error' do + before do + allow_any_instance_of(Faraday::Connection).to receive(:post).and_raise(StandardError.new('foo')) + end + + it 'raises a standard error' do subject.perform_async(submission.id) - jid = subject.jobs.last['jid'] - described_class.drain - expect(jid).not_to be_empty + expect { described_class.drain }.to raise_error(StandardError) end end end - context 'with a submission timeout' do - before do - allow_any_instance_of(Faraday::Connection).to receive(:post).and_raise(Faraday::TimeoutError) + context 'when a submission has 0781v2' do + let(:submission) do + Form526Submission.create(user_uuid: user.uuid, + auth_headers_json: auth_headers.to_json, + saved_claim_id: saved_claim.id, + form_json: form0781v2, + submitted_claim_id: evss_claim_id) end - it 'raises a gateway timeout error' do - subject.perform_async(submission.id) - expect { described_class.drain }.to raise_error(StandardError) + context 'with a successful submission job' do + it 'queues a job for submit' do + expect do + subject.perform_async(submission.id) + end.to change(subject.jobs, :size).by(1) + end + + it 'submits successfully' do + VCR.use_cassette('evss/disability_compensation_form/submit_0781') do + subject.perform_async(submission.id) + jid = subject.jobs.last['jid'] + described_class.drain + expect(jid).not_to be_empty + end + end end - end - context 'with an unexpected error' do - before do - allow_any_instance_of(Faraday::Connection).to receive(:post).and_raise(StandardError.new('foo')) + context 'with a submission timeout' do + before do + allow_any_instance_of(Faraday::Connection).to receive(:post).and_raise(Faraday::TimeoutError) + end + + it 'raises a gateway timeout error' do + subject.perform_async(submission.id) + expect { described_class.drain }.to raise_error(StandardError) + end end - it 'raises a standard error' do - subject.perform_async(submission.id) - expect { described_class.drain }.to raise_error(StandardError) + context 'with an unexpected error' do + before do + allow_any_instance_of(Faraday::Connection).to receive(:post).and_raise(StandardError.new('foo')) + end + + it 'raises a standard error' do + subject.perform_async(submission.id) + expect { described_class.drain }.to raise_error(StandardError) + end end end end @@ -239,221 +293,475 @@ allow(File).to receive(:delete).and_return(nil) end - let(:path_to_0781_fixture) { 'spec/fixtures/pdf_fill/21-0781/simple.pdf' } - let(:parsed_0781_form) { JSON.parse(submission.form_to_json(Form526Submission::FORM_0781))['form0781'] } - let(:form0781_only) do - original = JSON.parse(form0781) - original['form0781'].delete('form0781a') - original.to_json - end - - let(:path_to_0781a_fixture) { 'spec/fixtures/pdf_fill/21-0781a/kitchen_sink.pdf' } - let(:parsed_0781a_form) { JSON.parse(submission.form_to_json(Form526Submission::FORM_0781))['form0781a'] } - let(:form0781a_only) do - original = JSON.parse(form0781) - original['form0781'].delete('form0781') - original.to_json - end - - let(:submission) do - Form526Submission.create(user_uuid: user.uuid, - auth_headers_json: auth_headers.to_json, - saved_claim_id: saved_claim.id, - form_json: form0781, # contains 0781 and 0781a - submitted_claim_id: evss_claim_id) - end - - let(:perform_upload) do - subject.perform_async(submission.id) - described_class.drain - end + context 'when a submission includes either 0781 and/or 0781a' do + let(:path_to_0781_fixture) { 'spec/fixtures/pdf_fill/21-0781/simple.pdf' } + let(:parsed_0781_form) { JSON.parse(submission.form_to_json(Form526Submission::FORM_0781))['form0781'] } + let(:form0781_only) do + original = JSON.parse(form0781) + original['form0781'].delete('form0781a') + original.to_json + end - context 'when the disability_compensation_upload_0781_to_lighthouse flipper is enabled' do - let(:faraday_response) { instance_double(Faraday::Response) } - let(:lighthouse_request_id) { Faker::Number.number(digits: 8) } - let(:lighthouse_0781_document) do - LighthouseDocument.new( - claim_id: submission.submitted_claim_id, - participant_id: submission.auth_headers['va_eauth_pid'], - document_type: 'L228' - ) + let(:path_to_0781a_fixture) { 'spec/fixtures/pdf_fill/21-0781a/kitchen_sink.pdf' } + let(:parsed_0781a_form) { JSON.parse(submission.form_to_json(Form526Submission::FORM_0781))['form0781a'] } + let(:form0781a_only) do + original = JSON.parse(form0781) + original['form0781'].delete('form0781') + original.to_json end - let(:lighthouse_0781a_document) do - LighthouseDocument.new( - claim_id: submission.submitted_claim_id, - participant_id: submission.auth_headers['va_eauth_pid'], - document_type: 'L229' - ) + + let(:submission) do + Form526Submission.create(user_uuid: user.uuid, + auth_headers_json: auth_headers.to_json, + saved_claim_id: saved_claim.id, + form_json: form0781, # contains 0781 and 0781a + submitted_claim_id: evss_claim_id) end - let(:expected_statsd_metrics_prefix) do - 'worker.evss.submit_form0781.lighthouse_supplemental_document_upload_provider' + + let(:perform_upload) do + subject.perform_async(submission.id) + described_class.drain end - before do - allow(Flipper).to receive(:enabled?).with('disability_compensation_upload_0781_to_lighthouse', - instance_of(User)).and_return(true) + context 'when the disability_compensation_upload_0781_to_lighthouse flipper is enabled' do + let(:faraday_response) { instance_double(Faraday::Response) } + let(:lighthouse_request_id) { Faker::Number.number(digits: 8) } + let(:lighthouse_0781_document) do + LighthouseDocument.new( + claim_id: submission.submitted_claim_id, + participant_id: submission.auth_headers['va_eauth_pid'], + document_type: 'L228' + ) + end + let(:lighthouse_0781a_document) do + LighthouseDocument.new( + claim_id: submission.submitted_claim_id, + participant_id: submission.auth_headers['va_eauth_pid'], + document_type: 'L229' + ) + end + let(:expected_statsd_metrics_prefix) do + 'worker.evss.submit_form0781.lighthouse_supplemental_document_upload_provider' + end + + before do + allow(Flipper).to receive(:enabled?).with('disability_compensation_upload_0781_to_lighthouse', + instance_of(User)).and_return(true) - allow(BenefitsDocuments::Form526::UploadSupplementalDocumentService).to receive(:call) - .and_return(faraday_response) + allow(BenefitsDocuments::Form526::UploadSupplementalDocumentService).to receive(:call) + .and_return(faraday_response) - allow(faraday_response).to receive(:body).and_return( - { - 'data' => { - 'success' => true, - 'requestId' => lighthouse_request_id + allow(faraday_response).to receive(:body).and_return( + { + 'data' => { + 'success' => true, + 'requestId' => lighthouse_request_id + } } - } - ) - end + ) + end - context 'when a submission has both 0781 and 0781a' do - context 'when the request is successful' do - it 'uploads both documents to Lighthouse' do - # 0781 - allow_any_instance_of(described_class) - .to receive(:generate_stamp_pdf) - .with(parsed_0781_form, submission.submitted_claim_id, '21-0781') - .and_return(path_to_0781_fixture) + context 'when a submission has both 0781 and 0781a' do + context 'when the request is successful' do + it 'uploads both documents to Lighthouse' do + # 0781 + allow_any_instance_of(described_class) + .to receive(:generate_stamp_pdf) + .with(parsed_0781_form, submission.submitted_claim_id, '21-0781') + .and_return(path_to_0781_fixture) + + allow_any_instance_of(LighthouseSupplementalDocumentUploadProvider) + .to receive(:generate_upload_document) + .and_return(lighthouse_0781_document) + + expect(BenefitsDocuments::Form526::UploadSupplementalDocumentService) + .to receive(:call) + .with(File.read(path_to_0781_fixture), lighthouse_0781a_document) + + # 0781a + allow_any_instance_of(described_class) + .to receive(:generate_stamp_pdf) + .with(parsed_0781a_form, submission.submitted_claim_id, '21-0781a') + .and_return(path_to_0781a_fixture) + + allow_any_instance_of(LighthouseSupplementalDocumentUploadProvider) + .to receive(:generate_upload_document) + .and_return(lighthouse_0781a_document) + + expect(BenefitsDocuments::Form526::UploadSupplementalDocumentService) + .to receive(:call) + .with(File.read(path_to_0781a_fixture), lighthouse_0781a_document) + + perform_upload + end - allow_any_instance_of(LighthouseSupplementalDocumentUploadProvider) - .to receive(:generate_upload_document) - .and_return(lighthouse_0781_document) + it 'logs the upload attempt with the correct job prefix' do + expect(StatsD).to receive(:increment).with( + "#{expected_statsd_metrics_prefix}.upload_attempt" + ).twice # For 0781 and 0781a + perform_upload + end - expect(BenefitsDocuments::Form526::UploadSupplementalDocumentService) - .to receive(:call) - .with(File.read(path_to_0781_fixture), lighthouse_0781a_document) + it 'increments the correct StatsD success metric' do + expect(StatsD).to receive(:increment).with( + "#{expected_statsd_metrics_prefix}.upload_success" + ).twice # For 0781 and 0781a - # 0781a - allow_any_instance_of(described_class) - .to receive(:generate_stamp_pdf) - .with(parsed_0781a_form, submission.submitted_claim_id, '21-0781a') - .and_return(path_to_0781a_fixture) + perform_upload + end - allow_any_instance_of(LighthouseSupplementalDocumentUploadProvider) - .to receive(:generate_upload_document) - .and_return(lighthouse_0781a_document) + it 'creates a pending Lighthouse526DocumentUpload record so we can poll Lighthouse later' do + upload_attributes = { + aasm_state: 'pending', + form526_submission_id: submission.id, + lighthouse_document_request_id: lighthouse_request_id + } - expect(BenefitsDocuments::Form526::UploadSupplementalDocumentService) - .to receive(:call) - .with(File.read(path_to_0781a_fixture), lighthouse_0781a_document) + expect(Lighthouse526DocumentUpload.where(**upload_attributes).count).to eq(0) - perform_upload + perform_upload + expect(Lighthouse526DocumentUpload.where(**upload_attributes) + .where(document_type: 'Form 0781').count).to eq(1) + expect(Lighthouse526DocumentUpload.where(**upload_attributes) + .where(document_type: 'Form 0781a').count).to eq(1) + end end + end - it 'logs the upload attempt with the correct job prefix' do - expect(StatsD).to receive(:increment).with( - "#{expected_statsd_metrics_prefix}.upload_attempt" - ).twice # For 0781 and 0781a - perform_upload + context 'when a submission has 0781 only' do + before do + submission.update(form_json: form0781_only) end - it 'increments the correct StatsD success metric' do - expect(StatsD).to receive(:increment).with( - "#{expected_statsd_metrics_prefix}.upload_success" - ).twice # For 0781 and 0781a + context 'when the request is successful' do + it 'uploads to Lighthouse' do + allow_any_instance_of(described_class) + .to receive(:generate_stamp_pdf) + .with(parsed_0781_form, submission.submitted_claim_id, '21-0781') + .and_return(path_to_0781_fixture) - perform_upload + allow_any_instance_of(LighthouseSupplementalDocumentUploadProvider) + .to receive(:generate_upload_document) + .and_return(lighthouse_0781_document) + + expect(BenefitsDocuments::Form526::UploadSupplementalDocumentService) + .to receive(:call) + .with(File.read(path_to_0781_fixture), lighthouse_0781_document) + + perform_upload + end end - it 'creates a pending Lighthouse526DocumentUpload record so we can poll Lighthouse later' do - upload_attributes = { - aasm_state: 'pending', - form526_submission_id: submission.id, - lighthouse_document_request_id: lighthouse_request_id - } + context 'when Lighthouse returns an error response' do + let(:exception_errors) { [{ detail: 'Something Broke' }] } - expect(Lighthouse526DocumentUpload.where(**upload_attributes).count).to eq(0) + before do + # Skip additional logging that occurs in Lighthouse::ServiceException handling + allow(Rails.logger).to receive(:error) - perform_upload - expect(Lighthouse526DocumentUpload.where(**upload_attributes) - .where(document_type: 'Form 0781').count).to eq(1) - expect(Lighthouse526DocumentUpload.where(**upload_attributes) - .where(document_type: 'Form 0781a').count).to eq(1) + allow(BenefitsDocuments::Form526::UploadSupplementalDocumentService).to receive(:call) + .and_raise(Common::Exceptions::BadRequest.new(errors: exception_errors)) + end + + it 'logs the Lighthouse error response and re-raises the exception' do + expect(Rails.logger).to receive(:error).with( + 'LighthouseSupplementalDocumentUploadProvider upload failed', + { + class: 'LighthouseSupplementalDocumentUploadProvider', + submission_id: submission.id, + submitted_claim_id: submission.submitted_claim_id, + user_uuid: submission.user_uuid, + va_document_type_code: 'L228', + primary_form: 'Form526', + error_info: exception_errors + } + ) + + expect { perform_upload }.to raise_error(Common::Exceptions::BadRequest) + end + + it 'increments the correct status failure metric' do + expect(StatsD).to receive(:increment).with( + "#{expected_statsd_metrics_prefix}.upload_failure" + ) + + expect { perform_upload }.to raise_error(Common::Exceptions::BadRequest) + end + end + end + + context 'when a submission has 0781a only' do + before do + submission.update(form_json: form0781a_only) + end + + context 'when a request is successful' do + it 'uploads to Lighthouse' do + allow_any_instance_of(described_class) + .to receive(:generate_stamp_pdf) + .with(parsed_0781a_form, submission.submitted_claim_id, '21-0781a') + .and_return(path_to_0781a_fixture) + + allow_any_instance_of(LighthouseSupplementalDocumentUploadProvider) + .to receive(:generate_upload_document) + .and_return(lighthouse_0781a_document) + + expect(BenefitsDocuments::Form526::UploadSupplementalDocumentService) + .to receive(:call) + .with(File.read(path_to_0781a_fixture), lighthouse_0781a_document) + + perform_upload + end + end + + context 'when Lighthouse returns an error response' do + let(:exception_errors) { [{ detail: 'Something Broke' }] } + + before do + # Skip additional logging that occurs in Lighthouse::ServiceException handling + allow(Rails.logger).to receive(:error) + + allow(BenefitsDocuments::Form526::UploadSupplementalDocumentService).to receive(:call) + .and_raise(Common::Exceptions::BadRequest.new(errors: exception_errors)) + end + + it 'logs the Lighthouse error response and re-raises the exception' do + expect(Rails.logger).to receive(:error).with( + 'LighthouseSupplementalDocumentUploadProvider upload failed', + { + class: 'LighthouseSupplementalDocumentUploadProvider', + submission_id: submission.id, + submitted_claim_id: submission.submitted_claim_id, + user_uuid: submission.user_uuid, + va_document_type_code: 'L229', + primary_form: 'Form526', + error_info: exception_errors + } + ) + + expect { perform_upload }.to raise_error(Common::Exceptions::BadRequest) + end + + it 'increments the correct status failure metric' do + expect(StatsD).to receive(:increment).with( + "#{expected_statsd_metrics_prefix}.upload_failure" + ) + + expect { perform_upload }.to raise_error(Common::Exceptions::BadRequest) + end end end end - context 'when a submission has 0781 only' do + context 'when the disability_compensation_upload_0781_to_lighthouse flipper is disabled' do + let(:evss_claim_0781_document) do + EVSSClaimDocument.new( + evss_claim_id: submission.submitted_claim_id, + document_type: 'L228' + ) + end + let(:evss_claim_0781a_document) do + EVSSClaimDocument.new( + evss_claim_id: submission.submitted_claim_id, + document_type: 'L229' + ) + end + let(:client_stub) { instance_double(EVSS::DocumentsService) } + let(:expected_statsd_metrics_prefix) do + 'worker.evss.submit_form0781.evss_supplemental_document_upload_provider' + end + before do - submission.update(form_json: form0781_only) + allow(Flipper).to receive(:enabled?).with('disability_compensation_upload_0781_to_lighthouse', + instance_of(User)).and_return(false) + + allow(EVSS::DocumentsService).to receive(:new) { client_stub } + allow(client_stub).to receive(:upload) + # 0781 + allow_any_instance_of(described_class) + .to receive(:generate_stamp_pdf) + .with(parsed_0781_form, submission.submitted_claim_id, '21-0781') + .and_return(path_to_0781_fixture) + allow_any_instance_of(EVSSSupplementalDocumentUploadProvider) + .to receive(:generate_upload_document) + .with('simple.pdf') + .and_return(evss_claim_0781_document) + + # 0781a + allow_any_instance_of(described_class) + .to receive(:generate_stamp_pdf) + .with(parsed_0781a_form, submission.submitted_claim_id, '21-0781a') + .and_return(path_to_0781a_fixture) + allow_any_instance_of(EVSSSupplementalDocumentUploadProvider) + .to receive(:generate_upload_document) + .with('kitchen_sink.pdf') + .and_return(evss_claim_0781a_document) end - context 'when the request is successful' do - it 'uploads to Lighthouse' do - allow_any_instance_of(described_class) - .to receive(:generate_stamp_pdf) - .with(parsed_0781_form, submission.submitted_claim_id, '21-0781') - .and_return(path_to_0781_fixture) + context 'when a submission has both 0781 and 0781a' do + context 'when the request is successful' do + it 'uploads both documents to EVSS' do + expect(client_stub).to receive(:upload).with(File.read(path_to_0781_fixture), + evss_claim_0781_document) - allow_any_instance_of(LighthouseSupplementalDocumentUploadProvider) - .to receive(:generate_upload_document) - .and_return(lighthouse_0781_document) + expect(client_stub).to receive(:upload).with(File.read(path_to_0781a_fixture), + evss_claim_0781a_document) - expect(BenefitsDocuments::Form526::UploadSupplementalDocumentService) - .to receive(:call) - .with(File.read(path_to_0781_fixture), lighthouse_0781_document) + perform_upload + end - perform_upload + it 'logs the upload attempt with the correct job prefix' do + allow(client_stub).to receive(:upload) + expect(StatsD).to receive(:increment).with( + "#{expected_statsd_metrics_prefix}.upload_attempt" + ).twice # For 0781 and 0781a + + perform_upload + end + + it 'increments the correct StatsD success metric' do + allow(client_stub).to receive(:upload) + expect(StatsD).to receive(:increment).with( + "#{expected_statsd_metrics_prefix}.upload_success" + ).twice # For 0781 and 0781a + + perform_upload + end end - end - context 'when Lighthouse returns an error response' do - let(:exception_errors) { [{ detail: 'Something Broke' }] } + context 'when an upload raises an EVSS response error' do + it 'logs an upload error and re-raises the error' do + allow(client_stub).to receive(:upload).and_raise(EVSS::ErrorMiddleware::EVSSError) + expect_any_instance_of(EVSSSupplementalDocumentUploadProvider).to receive(:log_upload_failure) + + expect do + subject.perform_async(submission.id) + described_class.drain + end.to raise_error(EVSS::ErrorMiddleware::EVSSError) + end + end + end + context 'when a submission has only a 0781 form' do before do - # Skip additional logging that occurs in Lighthouse::ServiceException handling - allow(Rails.logger).to receive(:error) + submission.update(form_json: form0781_only) + end - allow(BenefitsDocuments::Form526::UploadSupplementalDocumentService).to receive(:call) - .and_raise(Common::Exceptions::BadRequest.new(errors: exception_errors)) + context 'when the request is successful' do + it 'uploads to EVSS' do + submission.update(form_json: form0781_only) + expect(client_stub).to receive(:upload).with(File.read(path_to_0781_fixture), + evss_claim_0781_document) + + perform_upload + end end - it 'logs the Lighthouse error response and re-raises the exception' do - expect(Rails.logger).to receive(:error).with( - 'LighthouseSupplementalDocumentUploadProvider upload failed', - { - class: 'LighthouseSupplementalDocumentUploadProvider', - submission_id: submission.id, - submitted_claim_id: submission.submitted_claim_id, - user_uuid: submission.user_uuid, - va_document_type_code: 'L228', - primary_form: 'Form526', - error_info: exception_errors - } - ) + context 'when an upload raises an EVSS response error' do + it 'logs an upload error and re-raises the error' do + allow(client_stub).to receive(:upload).and_raise(EVSS::ErrorMiddleware::EVSSError) + expect_any_instance_of(EVSSSupplementalDocumentUploadProvider).to receive(:log_upload_failure) - expect { perform_upload }.to raise_error(Common::Exceptions::BadRequest) + expect do + subject.perform_async(submission.id) + described_class.drain + end.to raise_error(EVSS::ErrorMiddleware::EVSSError) + end end + end - it 'increments the correct status failure metric' do - expect(StatsD).to receive(:increment).with( - "#{expected_statsd_metrics_prefix}.upload_failure" - ) + context 'when a submission has only a 0781a form' do + context 'when the request is successful' do + it 'uploads the 0781a document to EVSS' do + submission.update(form_json: form0781a_only) + expect(client_stub).to receive(:upload).with(File.read(path_to_0781a_fixture), + evss_claim_0781a_document) - expect { perform_upload }.to raise_error(Common::Exceptions::BadRequest) + perform_upload + end + end + + context 'when an upload raises an EVSS response error' do + it 'logs an upload error and re-raises the error' do + allow(client_stub).to receive(:upload).and_raise(EVSS::ErrorMiddleware::EVSSError) + expect_any_instance_of(EVSSSupplementalDocumentUploadProvider).to receive(:log_upload_failure) + + expect do + subject.perform_async(submission.id) + described_class.drain + end.to raise_error(EVSS::ErrorMiddleware::EVSSError) + end end end end + end + + context 'when a submission includes 0781v2' do + let(:path_to_0781v2_fixture) { 'spec/fixtures/pdf_fill/21-0781V2/kitchen_sink.pdf' } + let(:parsed_0781v2_form) { JSON.parse(submission.form_to_json(Form526Submission::FORM_0781))['form0781v2'] } + let(:form0781v2_only) do + original = JSON.parse(form0781) + original.to_json + end + + let(:submission) do + Form526Submission.create(user_uuid: user.uuid, + auth_headers_json: auth_headers.to_json, + saved_claim_id: saved_claim.id, + form_json: form0781v2, + submitted_claim_id: evss_claim_id) + end + + let(:perform_upload) do + subject.perform_async(submission.id) + described_class.drain + end + + context 'when the disability_compensation_upload_0781_to_lighthouse flipper is enabled' do + let(:faraday_response) { instance_double(Faraday::Response) } + let(:lighthouse_request_id) { Faker::Number.number(digits: 8) } + let(:lighthouse_0781v2_document) do + LighthouseDocument.new( + claim_id: submission.submitted_claim_id, + participant_id: submission.auth_headers['va_eauth_pid'], + document_type: 'L228' + ) + end + let(:expected_statsd_metrics_prefix) do + 'worker.evss.submit_form0781.lighthouse_supplemental_document_upload_provider' + end - context 'when a submission has 0781a only' do before do - submission.update(form_json: form0781a_only) + allow(Flipper).to receive(:enabled?).with('disability_compensation_upload_0781_to_lighthouse', + instance_of(User)).and_return(true) + + allow(BenefitsDocuments::Form526::UploadSupplementalDocumentService).to receive(:call) + .and_return(faraday_response) + + allow(faraday_response).to receive(:body).and_return( + { + 'data' => { + 'success' => true, + 'requestId' => lighthouse_request_id + } + } + ) end context 'when a request is successful' do it 'uploads to Lighthouse' do allow_any_instance_of(described_class) .to receive(:generate_stamp_pdf) - .with(parsed_0781a_form, submission.submitted_claim_id, '21-0781a') - .and_return(path_to_0781a_fixture) + .with(parsed_0781v2_form, submission.submitted_claim_id, '21-0781V2') + .and_return(path_to_0781v2_fixture) allow_any_instance_of(LighthouseSupplementalDocumentUploadProvider) .to receive(:generate_upload_document) - .and_return(lighthouse_0781a_document) + .and_return(lighthouse_0781v2_document) expect(BenefitsDocuments::Form526::UploadSupplementalDocumentService) .to receive(:call) - .with(File.read(path_to_0781a_fixture), lighthouse_0781a_document) + .with(File.read(path_to_0781v2_fixture), lighthouse_0781v2_document) perform_upload end @@ -478,7 +786,7 @@ submission_id: submission.id, submitted_claim_id: submission.submitted_claim_id, user_uuid: submission.user_uuid, - va_document_type_code: 'L229', + va_document_type_code: 'L228', primary_form: 'Form526', error_info: exception_errors } @@ -496,129 +804,42 @@ end end end - end - - context 'when the disability_compensation_upload_0781_to_lighthouse flipper is disabled' do - let(:evss_claim_0781_document) do - EVSSClaimDocument.new( - evss_claim_id: submission.submitted_claim_id, - document_type: 'L228' - ) - end - let(:evss_claim_0781a_document) do - EVSSClaimDocument.new( - evss_claim_id: submission.submitted_claim_id, - document_type: 'L229' - ) - end - let(:client_stub) { instance_double(EVSS::DocumentsService) } - let(:expected_statsd_metrics_prefix) do - 'worker.evss.submit_form0781.evss_supplemental_document_upload_provider' - end - - before do - allow(Flipper).to receive(:enabled?).with('disability_compensation_upload_0781_to_lighthouse', - instance_of(User)).and_return(false) - - allow(EVSS::DocumentsService).to receive(:new) { client_stub } - allow(client_stub).to receive(:upload) - # 0781 - allow_any_instance_of(described_class) - .to receive(:generate_stamp_pdf) - .with(parsed_0781_form, submission.submitted_claim_id, '21-0781') - .and_return(path_to_0781_fixture) - allow_any_instance_of(EVSSSupplementalDocumentUploadProvider) - .to receive(:generate_upload_document) - .with('simple.pdf') - .and_return(evss_claim_0781_document) - - # 0781a - allow_any_instance_of(described_class) - .to receive(:generate_stamp_pdf) - .with(parsed_0781a_form, submission.submitted_claim_id, '21-0781a') - .and_return(path_to_0781a_fixture) - allow_any_instance_of(EVSSSupplementalDocumentUploadProvider) - .to receive(:generate_upload_document) - .with('kitchen_sink.pdf') - .and_return(evss_claim_0781a_document) - end - - context 'when a submission has both 0781 and 0781a' do - context 'when the request is successful' do - it 'uploads both documents to EVSS' do - expect(client_stub).to receive(:upload).with(File.read(path_to_0781_fixture), evss_claim_0781_document) - expect(client_stub).to receive(:upload).with(File.read(path_to_0781a_fixture), - evss_claim_0781a_document) - - perform_upload - end - - it 'logs the upload attempt with the correct job prefix' do - allow(client_stub).to receive(:upload) - expect(StatsD).to receive(:increment).with( - "#{expected_statsd_metrics_prefix}.upload_attempt" - ).twice # For 0781 and 0781a - - perform_upload - end - - it 'increments the correct StatsD success metric' do - allow(client_stub).to receive(:upload) - expect(StatsD).to receive(:increment).with( - "#{expected_statsd_metrics_prefix}.upload_success" - ).twice # For 0781 and 0781a - - perform_upload - end + context 'when the disability_compensation_upload_0781_to_lighthouse flipper is disabled' do + let(:evss_claim_0781v2_document) do + EVSSClaimDocument.new( + evss_claim_id: submission.submitted_claim_id, + document_type: 'L228' + ) end - - context 'when an upload raises an EVSS response error' do - it 'logs an upload error and re-raises the error' do - allow(client_stub).to receive(:upload).and_raise(EVSS::ErrorMiddleware::EVSSError) - expect_any_instance_of(EVSSSupplementalDocumentUploadProvider).to receive(:log_upload_failure) - - expect do - subject.perform_async(submission.id) - described_class.drain - end.to raise_error(EVSS::ErrorMiddleware::EVSSError) - end + let(:client_stub) { instance_double(EVSS::DocumentsService) } + let(:expected_statsd_metrics_prefix) do + 'worker.evss.submit_form0781.evss_supplemental_document_upload_provider' end - end - context 'when a submission has only a 0781 form' do before do - submission.update(form_json: form0781_only) + allow(Flipper).to receive(:enabled?).with('disability_compensation_upload_0781_to_lighthouse', + instance_of(User)).and_return(false) + + allow(EVSS::DocumentsService).to receive(:new) { client_stub } + allow(client_stub).to receive(:upload) + allow_any_instance_of(described_class) + .to receive(:generate_stamp_pdf) + .with(parsed_0781v2_form, submission.submitted_claim_id, '21-0781V2') + .and_return(path_to_0781v2_fixture) + allow_any_instance_of(EVSSSupplementalDocumentUploadProvider) + .to receive(:generate_upload_document) + .with('kitchen_sink.pdf') + .and_return(evss_claim_0781v2_document) + + submission.update(form_json: form0781v2) end context 'when the request is successful' do it 'uploads to EVSS' do - submission.update(form_json: form0781_only) - expect(client_stub).to receive(:upload).with(File.read(path_to_0781_fixture), evss_claim_0781_document) - - perform_upload - end - end - - context 'when an upload raises an EVSS response error' do - it 'logs an upload error and re-raises the error' do - allow(client_stub).to receive(:upload).and_raise(EVSS::ErrorMiddleware::EVSSError) - expect_any_instance_of(EVSSSupplementalDocumentUploadProvider).to receive(:log_upload_failure) - - expect do - subject.perform_async(submission.id) - described_class.drain - end.to raise_error(EVSS::ErrorMiddleware::EVSSError) - end - end - end - - context 'when a submission has only a 0781a form' do - context 'when the request is successful' do - it 'uploads the 0781a document to EVSS' do - submission.update(form_json: form0781a_only) - expect(client_stub).to receive(:upload).with(File.read(path_to_0781a_fixture), - evss_claim_0781a_document) + submission.update(form_json: form0781v2) + expect(client_stub).to receive(:upload).with(File.read(path_to_0781v2_fixture), + evss_claim_0781v2_document) perform_upload end diff --git a/spec/sidekiq/form1010cg/submission_job_spec.rb b/spec/sidekiq/form1010cg/submission_job_spec.rb index d006a91fa15..73be20c0822 100644 --- a/spec/sidekiq/form1010cg/submission_job_spec.rb +++ b/spec/sidekiq/form1010cg/submission_job_spec.rb @@ -100,7 +100,7 @@ "#{statsd_key_prefix}failed_no_retries_left", tags: ["claim_id:#{claim.id}"] ) - expect(StatsD).to receive(:increment).with('silent_failure_avoided_no_confirmation', tags: zsf_tags) + expect(StatsD).to receive(:increment).with('silent_failure', tags: zsf_tags) expect(VANotify::EmailJob).not_to receive(:perform_async) end end @@ -118,7 +118,7 @@ "#{statsd_key_prefix}failed_no_retries_left", tags: ["claim_id:#{claim.id}"] ) - expect(StatsD).to receive(:increment).with('silent_failure_avoided_no_confirmation', tags: zsf_tags) + expect(StatsD).to receive(:increment).with('silent_failure', tags: zsf_tags) expect(VANotify::EmailJob).not_to receive(:perform_async) end end @@ -137,7 +137,14 @@ { 'salutation' => "Dear #{claim.parsed_form.dig('veteran', 'fullName', 'first')}," }, - api_key + api_key, + { + callback_metadata: { + notification_type: 'error', + form_number: claim.form_id, + statsd_tags: zsf_tags + } + } ] end @@ -229,9 +236,6 @@ "#{statsd_key_prefix}record_parse_error", tags: ["claim_id:#{claim.id}"] ) - expect(StatsD).to receive(:increment).with( - 'silent_failure_avoided_no_confirmation', tags: zsf_tags - ) job.perform(claim.id) end @@ -246,7 +250,7 @@ expect do job.perform(claim.id) end.to trigger_statsd_increment('api.form1010cg.async.record_parse_error', tags: ["claim_id:#{claim.id}"]) - .and trigger_statsd_increment('silent_failure_avoided_no_confirmation', tags: zsf_tags) + .and trigger_statsd_increment('silent_failure', tags: zsf_tags) expect(SavedClaim.exists?(id: claim.id)).to eq(true) expect(VANotify::EmailJob).not_to receive(:perform_async) @@ -267,7 +271,7 @@ expect do job.perform(claim.id) end.to trigger_statsd_increment('api.form1010cg.async.record_parse_error', tags: ["claim_id:#{claim.id}"]) - .and trigger_statsd_increment('silent_failure_avoided_no_confirmation', tags: zsf_tags) + .and trigger_statsd_increment('silent_failure', tags: zsf_tags) expect(SavedClaim.exists?(id: claim.id)).to eq(true) expect(VANotify::EmailJob).not_to receive(:perform_async) diff --git a/spec/sidekiq/form526_submission_failure_email_job_spec.rb b/spec/sidekiq/form526_submission_failure_email_job_spec.rb index e387975fad0..fc8dd90cddc 100644 --- a/spec/sidekiq/form526_submission_failure_email_job_spec.rb +++ b/spec/sidekiq/form526_submission_failure_email_job_spec.rb @@ -18,7 +18,7 @@ end describe '#perform' do - context 'when a user has additional form and files with their submission' do + context 'when a user has additional forms and files with their submission' do let!(:form526_submission) { create(:form526_submission, :with_uploads_and_ancillary_forms) } let(:expected_params) do @@ -70,7 +70,7 @@ end end - context 'when a user has no additional additional forms their submission' do + context 'when a user has no additional forms with their submission' do let!(:form526_submission) { create(:form526_submission, :with_uploads) } let(:expected_params) do { @@ -102,34 +102,67 @@ end end - context 'when a user has no additional additional user-uploaded files their submission' do - let(:expected_params) do - { - email_address: 'test@email.com', - template_id: 'form526_submission_failure_notification_template_id', - personalisation: { - first_name: form526_submission.get_first_name, - date_submitted: form526_submission.format_creation_time_for_mailers, - date_of_failure: failure_timestamp, - files_submitted: 'None', - forms_submitted: [ - 'VA Form 21-4142', - 'VA Form 21-0781', - 'VA Form 21-0781a', - 'VA Form 21-8940' - ] + context 'when a user has no additional user-uploaded files with their submission' do + context 'when using v1 of form 0781' do + let(:expected_params) do + { + email_address: 'test@email.com', + template_id: 'form526_submission_failure_notification_template_id', + personalisation: { + first_name: form526_submission.get_first_name, + date_submitted: form526_submission.format_creation_time_for_mailers, + date_of_failure: failure_timestamp, + files_submitted: 'None', + forms_submitted: [ + 'VA Form 21-4142', + 'VA Form 21-0781', + 'VA Form 21-0781a', + 'VA Form 21-8940' + ] + } } - } + end + + let!(:form526_submission) { create(:form526_submission, :with_everything) } + + it 'replaces the files list variable with a placeholder' do + Timecop.freeze(timestamp) do + expect(email_service).to receive(:send_email).with(expected_params) + + subject.perform_async(form526_submission.id, timestamp.to_s) + subject.drain + end + end end - let!(:form526_submission) { create(:form526_submission, :with_everything) } + context 'when using v2 of form 0781' do + let(:expected_params) do + { + email_address: 'test@email.com', + template_id: 'form526_submission_failure_notification_template_id', + personalisation: { + first_name: form526_submission.get_first_name, + date_submitted: form526_submission.format_creation_time_for_mailers, + date_of_failure: failure_timestamp, + files_submitted: 'None', + forms_submitted: [ + 'VA Form 21-4142', + 'VA Form 21-0781', + 'VA Form 21-8940' + ] + } + } + end - it 'replaces the files list variable with a placeholder' do - Timecop.freeze(timestamp) do - expect(email_service).to receive(:send_email).with(expected_params) + let!(:form526_submission) { create(:form526_submission, :with_0781v2) } - subject.perform_async(form526_submission.id, timestamp.to_s) - subject.drain + it 'replaces the files list variable with a placeholder' do + Timecop.freeze(timestamp) do + expect(email_service).to receive(:send_email).with(expected_params) + + subject.perform_async(form526_submission.id, timestamp.to_s) + subject.drain + end end end end diff --git a/spec/support/disability_compensation_form/all_claims_with_0781v2_fe_submission.json b/spec/support/disability_compensation_form/all_claims_with_0781v2_fe_submission.json new file mode 100644 index 00000000000..4763bb4ad4b --- /dev/null +++ b/spec/support/disability_compensation_form/all_claims_with_0781v2_fe_submission.json @@ -0,0 +1,251 @@ +{ + "form526": { + "mailingAddress": { + "country": "USA", + "city": "Portland", + "state": "OR", + "addressLine1": "1234 Couch Street", + "addressLine2": "Apt. 22", + "zipCode": "12345-6789" + }, + "forwardingAddress": { + "country": "USA", + "city": "Portland", + "state": "OR", + "addressLine1": "5678 Couch Street", + "addressLine2": "Apt. 2", + "zipCode": "23451-6789", + "effectiveDate": { + "from": "2018-02-01" + } + }, + "alternateNames": [ + { + "first": "JKack", + "last": "Bauer", + "middle": "Clint" + } + ], + "bankAccountNumber": "123123123123", + "bankAccountType": "Checking", + "bankName": "SomeBank", + "bankRoutingNumber": "123123123", + "confinements": [ + { + "from": "1987-02-01", + "to": "1989-01-01" + }, + { + "from": "1990-03-06", + "to": "1999-01-01" + } + ], + "homelessHousingSituation": "other", + "homelessOrAtRisk": "homeless", + "homelessnessContact": { + "name": "Jane Doe", + "phoneNumber": "1231231231" + }, + "isVAEmployee": false, + "militaryRetiredPayBranch": "Air Force", + "needToLeaveHousing": true, + "ratedDisabilities": [ + { + "diagnosticCode": 9999, + "disabilityActionType": "NEW", + "name": "PTSD (post traumatic stress disorder)", + "ratedDisabilityId": "1100583" + } + ], + "newDisabilities": [ + { + "cause": "SECONDARY", + "causedByDisability": "PTSD (post traumatic stress disorder)", + "causedByDisabilityDescription": "Lengthy description", + "condition": "PTSD personal trauma" + } + ], + "otherHomelessHousing": "other living situation", + "phoneAndEmail": { + "primaryPhone": "2024561111", + "emailAddress": "test@email.com" + }, + "privacyAgreementAccepted": true, + "separationPayBranch": "Air Force", + "separationPayDate": "2000", + "serviceInformation": { + "reservesNationalGuardService": { + "obligationTermOfServiceDateRange": { + "from": "2000-01-04", + "to": "2004-01-04" + }, + "title10Activation": { + "anticipatedSeparationDate": "2020-01-01", + "title10ActivationDate": "1999-03-04" + }, + "unitName": "Seal Team Six", + "unitPhone": "1231231231" + }, + "servicePeriods": [ + { + "dateRange": { + "from": "1980-02-05", + "to": "1990-01-02" + }, + "serviceBranch": "Air Force" + }, + { + "dateRange": { + "from": "1990-04-05", + "to": "1999-01-01" + }, + "serviceBranch": "Air Force Reserve" + } + ] + }, + "standardClaim": false, + "vaTreatmentFacilities": [ + { + "treatmentCenterName": "Private Facility 2", + "treatmentCenterAddress": { + "country": "USA" + }, + "treatmentDateRange": { + "from": "2018-03-02", + "to": "2018-03-03" + }, + "treatedDisabilityNames": [ + "PTSD (post traumatic stress disorder)" + ] + }, + { + "treatmentCenterName": "Huntsville VA Facility", + "treatmentCenterAddress": { + "country": "USA" + }, + "treatmentDateRange": { + "from": "2015-04-03", + "to": "2018-03-04" + }, + "treatedDisabilityNames": [ + "PTSD personal trauma" + ] + } + ], + "waiveRetirementPay": false, + "waiveTrainingPay": true, + "hasTrainingPay": true, + "syncModern0781Flow": true, + "form0781": { + "eventsDetails": [ + { + "details": "Lorem ipsum dolor sit amet.", + "location": "Stationed on U.S.S. XYZ", + "timing": "2000" + }, + { + "details": "Lorem ipsum dolor sit amet.", + "location": "Back alley in Big Town, USA", + "timing": "2001" + }, + { + "details": "Lorem ipsum dolor sit amet.", + "location": "Greenwich Township, N Jersey", + "timing": "2002" + }, + { + "location": "Springfield, Hampden County", + "timing": "2003" + }, + { + "details": "Lorem ipsum dolor sit amet.", + "location": "Assigned to U.S. Army unit, Vietnam" + }, + { + "details": "Lorem ipsum dolor sit amet.", + "location": "Onboard U.S.S. Intrepid, South China Sea", + "timing": "2004" + } + ], + "reports": { + "yes": true, + "restricted": true, + "police": true, + "other": true + }, + "reportsDetails": { + "police": { + "agency": "SVI", + "city": "Dallas", + "state": "Texas", + "township": "", + "country": "USA" + }, + "other": "incident report" + }, + "behaviors": { + "reassignment": true, + "performance": true, + "consultations": true, + "episodes": true, + "selfMedication": true, + "substances": true, + "appetite": true, + "screenings": true, + "relationships": true, + "otherBehavior": "Withdrawal" + }, + "behaviorsDetails": { + "reassignment": "Lorem ipsum dolor sit amet.", + "performance": "Lorem ipsum dolor sit amet.", + "episodes": "Lorem ipsum dolor sit amet.", + "selfMedication": "Lorem ipsum dolor sit amet.", + "appetite": "Lorem ipsum dolor sit amet.", + "screenings": "Lorem ipsum dolor sit amet.", + "relationships": "Lorem ipsum dolor sit amet.", + "otherBehavior": "Lorem ipsum dolor sit amet." + }, + "evidence": { + "counseling": true, + "family": true, + "police": true, + "medical": true, + "peers": true, + "journal": true, + "other": true, + "otherDetails": "Lorem ipsum dolor sit amet." + }, + "traumaTreatment": true, + "treatmentProviders": { + "privateCare": true, + "communityCare": false, + "vamc": true, + "mtf": true + }, + "treatmentProvidersDetails": [ + { + "facilityInfo": "Walter Reed, Bethesda, MD", + "treatmentMonth": "02", + "treatmentYear": "2014" + }, + { + "facilityInfo": "Cedarwood Behavioral Health Center", + "treatmentYear": "2024" + }, + { + "facilityInfo": "Silver Oak Recovery Center", + "noDates": true + }, + { + "facilityInfo": "Silver Oak Recovery Center", + "noDates": true + } + ], + "optionIndicator": { + "notEnrolled": true + }, + "additionalInformation": "Lorem ipsum dolor sit amet." + } + } + } + \ No newline at end of file diff --git a/spec/support/disability_compensation_form/form_0781v2.json b/spec/support/disability_compensation_form/form_0781v2.json new file mode 100644 index 00000000000..bc8682da34c --- /dev/null +++ b/spec/support/disability_compensation_form/form_0781v2.json @@ -0,0 +1,125 @@ +{ + "form0781v2": { + "vaFileNumber": "796068949", + "veteranSocialSecurityNumber": "796068949", + "veteranFullName": { + "first": "Beyonce", + "middle": null, + "last": "Knowles" + }, + "veteranDateOfBirth": "1809-02-12", + "email": "test@email.com", + "veteranPhone": "2024561111", + "veteranSecondaryPhone": "", + "veteranServiceNumber": "", + "eventsDetails": [ + { + "details": "Lorem ipsum dolor sit amet.", + "location": "Stationed on U.S.S. XYZ", + "timing": "2000" + }, + { + "details": "Lorem ipsum dolor sit amet.", + "location": "Back alley in Big Town, USA", + "timing": "2001" + }, + { + "details": "Lorem ipsum dolor sit amet.", + "location": "Greenwich Township, N Jersey", + "timing": "2002" + }, + { + "location": "Springfield, Hampden County", + "timing": "2003" + }, + { + "details": "Lorem ipsum dolor sit amet.", + "location": "Assigned to U.S. Army unit, Vietnam" + }, + { + "details": "Lorem ipsum dolor sit amet.", + "location": "Onboard U.S.S. Intrepid, South China Sea", + "timing": "2004" + } + ], + "reports": { + "yes": true, + "restricted": true, + "police": true, + "other": true + }, + "reportsDetails": { + "police": { + "agency": "SVI", + "city": "Dallas", + "state": "Texas", + "township": "", + "country": "USA" + }, + "other": "incident report" + }, + "behaviors": { + "reassignment": true, + "performance": true, + "consultations": true, + "episodes": true, + "selfMedication": true, + "substances": true, + "appetite": true, + "screenings": true, + "relationships": true, + "otherBehavior": "Withdrawal" + }, + "behaviorsDetails": { + "reassignment": "Lorem ipsum dolor sit amet.", + "performance": "Lorem ipsum dolor sit amet.", + "episodes": "Lorem ipsum dolor sit amet.", + "selfMedication": "Lorem ipsum dolor sit amet.", + "appetite": "Lorem ipsum dolor sit amet.", + "screenings": "Lorem ipsum dolor sit amet.", + "relationships": "Lorem ipsum dolor sit amet.", + "otherBehavior": "Lorem ipsum dolor sit amet." + }, + "evidence": { + "counseling": true, + "family": true, + "police": true, + "medical": true, + "peers": true, + "journal": true, + "other": true, + "otherDetails": "Lorem ipsum dolor sit amet." + }, + "traumaTreatment": true, + "treatmentProviders": { + "privateCare": true, + "communityCare": false, + "vamc": true, + "mtf": true + }, + "treatmentProvidersDetails": [ + { + "facilityInfo": "Walter Reed, Bethesda, MD", + "treatmentMonth": "02", + "treatmentYear": "2014" + }, + { + "facilityInfo": "Cedarwood Behavioral Health Center", + "treatmentYear": "2024" + }, + { + "facilityInfo": "Silver Oak Recovery Center", + "noDates": true + }, + { + "facilityInfo": "Silver Oak Recovery Center", + "noDates": true + } + ], + "optionIndicator": { + "notEnrolled": true + }, + "additionalInformation": "Lorem ipsum dolor sit amet." + } +} + \ No newline at end of file diff --git a/spec/support/disability_compensation_form/submissions/with_0781v2.json b/spec/support/disability_compensation_form/submissions/with_0781v2.json new file mode 100644 index 00000000000..ee8be4c0bf4 --- /dev/null +++ b/spec/support/disability_compensation_form/submissions/with_0781v2.json @@ -0,0 +1,592 @@ +{ + "form526": { + "form526": { + "veteran": { + "emailAddress": "test@email.com", + "currentMailingAddress": { + "country": "USA", + "addressLine1": "1234 Couch Street", + "addressLine2": "Apt. 22", + "type": "DOMESTIC", + "city": "Portland", + "state": "OR", + "zipFirstFive": "12345", + "zipLastFour": "6789" + }, + "daytimePhone": { + "areaCode": "603", + "phoneNumber": "5892689" + }, + "eveningPhone": { + "areaCode": "603", + "phoneNumber": "5892600" + }, + "cellPhone": { + "areaCode": "603", + "phoneNumber": "5892602" + }, + "changeOfAddress": { + "country": "USA", + "addressLine1": "5678 Couch Street", + "addressLine2": "Apt. 2", + "beginningDate": "2018-02-01", + "endingDate": "2018-06-30", + "type": "DOMESTIC", + "city": "Portland", + "state": "OR", + "zipFirstFive": "23451", + "zipLastFour": "6789", + "addressChangeType": "TEMPORARY" + }, + "homelessness": { + "pointOfContact": { + "pointOfContactName": "Jane Doe", + "primaryPhone": { + "areaCode": "123", + "phoneNumber": "1231231" + } + }, + "currentlyHomeless": { + "homelessSituationType": "FLEEING_CURRENT_RESIDENCE", + "otherLivingSituation": "other living situation" + } + }, + "currentlyVAEmployee": false + }, + "claimantCertification": true, + "standardClaim": false, + "autoCestPDFGenerationDisabled": false, + "applicationExpirationDate": "2015-08-28T19:53:45+00:00", + "directDeposit": { + "accountType": "CHECKING", + "accountNumber": "123123123123", + "routingNumber": "123123123", + "bankName": "SomeBank" + }, + "servicePay": { + "waiveVABenefitsToRetainTrainingPay": true, + "waiveVABenefitsToRetainRetiredPay": false, + "militaryRetiredPay": { + "receiving": true, + "payment": { + "serviceBranch": "Air Force" + } + }, + "separationPay": { + "received": true, + "payment": { + "serviceBranch": "Air Force" + }, + "receivedDate": { + "year": "2000" + } + } + }, + "serviceInformation": { + "servicePeriods": [ + { + "serviceBranch": "Air Force", + "activeDutyBeginDate": "1980-02-05", + "activeDutyEndDate": "1990-01-02" + }, + { + "serviceBranch": "Air Force Reserves", + "activeDutyBeginDate": "1990-04-05", + "activeDutyEndDate": "1999-01-01" + } + ], + "confinements": [ + { + "confinementBeginDate": "1987-02-01", + "confinementEndDate": "1989-01-01" + }, + { + "confinementBeginDate": "1990-03-06", + "confinementEndDate": "1999-01-01" + } + ], + "reservesNationalGuardService": { + "title10Activation": { + "anticipatedSeparationDate": "2020-01-01", + "title10ActivationDate": "1999-03-04" + }, + "obligationTermOfServiceFromDate": "2000-01-04", + "obligationTermOfServiceToDate": "2004-01-04", + "unitName": "Seal Team Six", + "unitPhone": { + "areaCode": "123", + "phoneNumber": "1231231" + }, + "receivingInactiveDutyTrainingPay": true + }, + "alternateNames": [ + { + "firstName": "JKack", + "middleName": "Clint", + "lastName": "Bauer" + } + ] + }, + "treatments": [ + { + "startDate": "2018-03-02", + "endDate": "2018-03-03", + "treatedDisabilityNames": [ + "PTSD (post traumatic stress disorder)" + ], + "center": { + "name": "Private Facility 2", + "country": "USA" + } + }, + { + "startDate": "2015-04-03", + "endDate": "2018-03-04", + "treatedDisabilityNames": [ + "PTSD personal trauma" + ], + "center": { + "name": "Huntsville VA Facility", + "country": "USA" + } + } + ], + "disabilities": [ + { + "diagnosticCode": 9999, + "disabilityActionType": "NEW", + "name": "PTSD (post traumatic stress disorder)", + "ratedDisabilityId": "1100583", + "secondaryDisabilities": [ + { + "name": "PTSD personal trauma", + "disabilityActionType": "SECONDARY", + "serviceRelevance": "Caused by a service-connected disability\nLengthy description" + } + ] + } + ] + } + }, + "form526_uploads": null, + "form4142": { + "privacyAgreementAccepted": true, + "limitedConsent": "Limit to medical records please. Exclude any psyciatric records.", + "providerFacility": [ + { + "providerFacilityName": "provider 1", + "treatmentDateRange": [ + { + "from": "1980-1-1", + "to": "1985-1-1" + }, + { + "from": "1986-1-1", + "to": "1987-1-1" + } + ], + "providerFacilityAddress": { + "street": "123 Main Street", + "street2": "1B", + "city": "Baltimore", + "state": "MD", + "country": "USA", + "postalCode": "21200-1111" + } + }, + { + "providerFacilityName": "provider 2", + "treatmentDateRange": [ + { + "from": "1980-2-1", + "to": "1985-2-1" + }, + { + "from": "1986-2-1", + "to": "1987-2-1" + } + ], + "providerFacilityAddress": { + "street": "456 Main Street", + "street2": "1B", + "city": "Baltimore", + "state": "MD", + "country": "USA", + "postalCode": "21200-1111" + } + }, + { + "providerFacilityName": "provider 3", + "treatmentDateRange": [ + { + "from": "1980-3-1", + "to": "1985-3-1" + }, + { + "from": "1986-3-1", + "to": "1987-3-1" + } + ], + "providerFacilityAddress": { + "street": "789 Main Street", + "street2": "1B", + "city": "Baltimore", + "state": "MD", + "country": "USA", + "postalCode": "21200-1111" + } + }, + { + "providerFacilityName": "provider 4", + "treatmentDateRange": [ + { + "from": "1980-4-1", + "to": "1985-4-1" + }, + { + "from": "1986-4-1", + "to": "1987-4-1" + } + ], + "providerFacilityAddress": { + "street": "101 Main Street", + "street2": "1B", + "city": "Baltimore", + "state": "MD", + "country": "USA", + "postalCode": "21200-1111" + } + }, + { + "providerFacilityName": "provider 5", + "treatmentDateRange": [ + { + "from": "1980-5-1", + "to": "1985-5-1" + }, + { + "from": "1986-5-1", + "to": "1987-5-1" + } + ], + "providerFacilityAddress": { + "street": "102 Main Street", + "street2": "1B", + "city": "Baltimore", + "state": "MD", + "country": "USA", + "postalCode": "21200-1111" + } + } + ], + "vaFileNumber": "796068949", + "veteranSocialSecurityNumber": "796068949", + "veteranFullName": { + "first": "Beyonce", + "middle": null, + "last": "Knowles" + }, + "veteranDateOfBirth": "1809-02-12", + "veteranAddress": { + "city": "Portland", + "country": "USA", + "postalCode": "12345-6789", + "street": "1234 Couch Street", + "street2": "Apt. 22", + "state": "OR" + }, + "email": "test@email.com", + "veteranPhone": "2024561111", + "veteranServiceNumber": "" + }, + "form0781": { + "form0781v2": { + "vaFileNumber": "796068949", + "veteranSocialSecurityNumber": "796068949", + "veteranFullName": { + "first": "Beyonce", + "middle": null, + "last": "Knowles" + }, + "veteranDateOfBirth": "1809-02-12", + "email": "test@email.com", + "veteranPhone": "2024561111", + "veteranSecondaryPhone": "", + "veteranServiceNumber": "", + "eventTypes": { + "combat": true, + "nonMst": true + }, + "eventsDetails": [ + { + "details": "Lorem ipsum dolor sit amet.", + "location": "Stationed on U.S.S. XYZ", + "timing": "2000" + }, + { + "details": "Lorem ipsum dolor sit amet.", + "location": "Back alley in Big Town, USA", + "timing": "2001" + }, + { + "details": "Lorem ipsum dolor sit amet.", + "location": "Greenwich Township, N Jersey", + "timing": "2002" + }, + { + "location": "Springfield, Hampden County", + "timing": "2003" + }, + { + "details": "Lorem ipsum dolor sit amet.", + "location": "Assigned to U.S. Army unit, Vietnam" + }, + { + "details": "Lorem ipsum dolor sit amet.", + "location": "Onboard U.S.S. Intrepid, South China Sea", + "timing": "2004" + } + ], + "reports": { + "yes": true, + "restricted": true, + "police": true, + "other": true + }, + "reportsDetails": { + "police": { + "agency": "SVI", + "city": "Dallas", + "state": "Texas", + "township": "", + "country": "USA" + }, + "other": "incident report" + }, + "behaviors": { + "reassignment": true, + "performance": true, + "consultations": true, + "episodes": true, + "selfMedication": true, + "substances": true, + "appetite": true, + "screenings": true, + "relationships": true, + "otherBehavior": "Withdrawal" + }, + "behaviorsDetails": { + "reassignment": "Lorem ipsum dolor sit amet.", + "performance": "Lorem ipsum dolor sit amet.", + "episodes": "Lorem ipsum dolor sit amet.", + "selfMedication": "Lorem ipsum dolor sit amet.", + "appetite": "Lorem ipsum dolor sit amet.", + "screenings": "Lorem ipsum dolor sit amet.", + "relationships": "Lorem ipsum dolor sit amet.", + "otherBehavior": "Lorem ipsum dolor sit amet." + }, + "evidence": { + "counseling": true, + "family": true, + "police": true, + "medical": true, + "peers": true, + "journal": true, + "other": true, + "otherDetails": "Lorem ipsum dolor sit amet." + }, + "traumaTreatment": true, + "treatmentProviders": { + "privateCare": true, + "communityCare": false, + "vamc": true, + "mtf": true + }, + "treatmentProvidersDetails": [ + { + "facilityInfo": "Walter Reed, Bethesda, MD", + "treatmentMonth": "02", + "treatmentYear": "2014" + }, + { + "facilityInfo": "Cedarwood Behavioral Health Center", + "treatmentYear": "2024" + }, + { + "facilityInfo": "Silver Oak Recovery Center", + "noDates": true + }, + { + "facilityInfo": "Silver Oak Recovery Center", + "noDates": true + } + ], + "optionIndicator": { + "notEnrolled": true + }, + "additionalInformation": "Lorem ipsum dolor sit amet." + } + }, + "form8940": { + "vaFileNumber": "796068949", + "veteranSocialSecurityNumber": "796068949", + "veteranFullName": { + "first": "Beyonce", + "middle": null, + "last": "Knowles" + }, + "veteranDateOfBirth": "1809-02-12", + "veteranAddress": { + "city": "Quaint Town", + "country": "USA", + "postalCode": "85918-1212", + "street": "1234 Classy Street", + "street2": "Apartment 567", + "state": "OR" + }, + "email": "test@email.com", + "veteranPhone": "2024561111", + "unemployability": { + "disabilityPreventingEmployment": "Unemployability Disability", + "underDoctorHopitalCarePast12M": false, + "doctorProvidedCare": [ + { + "name": "Doctor Care A", + "address": { + "street": "123 Main Street", + "street2": "1B", + "city": "Baltimore", + "state": "MD", + "country": "USA", + "postalCode": "21200-1111" + }, + "dates": { + "from": "1994-01-01", + "to": "1995-01-01" + } + } + ], + "hospitalProvidedCare": [ + { + "name": "Hospital Care A", + "address": { + "street": "123 Main Street", + "street2": "2B", + "city": "Baltimore", + "state": "MD", + "country": "USA", + "postalCode": "21200-1111" + }, + "dates": { + "from": "1996-01-01", + "to": "1997-01-01" + } + } + ], + "disabilityAffectedEmploymentFullTimeDate": "1998-01-01", + "lastWorkedFullTimeDate": "1997-01-01", + "becameTooDisabledToWorkDate": "1993-01-01", + "mostEarningsInAYear": "100,000", + "yearOfMostEarnings": "1992", + "occupationDuringMostEarnings": "Test", + "previousEmployers": [ + { + "name": "Employer A", + "employerAddress": { + "street": "123 Main Street", + "street2": "1B", + "city": "Baltimore", + "state": "MD", + "country": "USA", + "postalCode": "21200-1111" + }, + "typeOfWork": "work", + "hoursPerWeek": "40", + "dates": { + "from": "1991-01-01", + "to": "1992-01-01" + }, + "timeLostFromIllness": "2 months", + "mostEarningsInAMonth": "4000" + }, + { + "name": "Employer B", + "employerAddress": { + "street": "123 Main Street", + "street2": "1C", + "city": "Baltimore", + "state": "MD", + "country": "USA", + "postalCode": "21200-1111" + }, + "typeOfWork": "work", + "hoursPerWeek": "40", + "dates": { + "from": "1992-01-01", + "to": "1993-01-01" + }, + "timeLostFromIllness": "1 months", + "mostEarningsInAMonth": "2000" + } + ], + "disabilityPreventMilitaryDuties": true, + "past12MonthsEarnedIncome": "0", + "currentMonthlyEarnedIncome": "0", + "leftLastJobDueToDisability": true, + "receiveExpectDisabilityRetirement": false, + "receiveExpectWorkersCompensation": false, + "attemptedToObtainEmploymentSinceUnemployability": false, + "appliedEmployers": [ + { + "name": "Employer C", + "address": { + "street": "123 Main Street", + "street2": "1D", + "city": "Baltimore", + "state": "MD", + "country": "USA", + "postalCode": "21200-1111" + }, + "workType": "work", + "date": "1997-01-01" + }, + { + "name": "Employer D", + "address": { + "street": "123 Main Street", + "street2": "1E", + "city": "Baltimore", + "state": "MD", + "country": "USA", + "postalCode": "21200-1111" + }, + "workType": "work", + "date": "1997-01-01" + } + ], + "education": "college4", + "receivedOtherEducationTrainingPreUnemployability": true, + "otherEducationTrainingPreUnemployability": [ + { + "name": "Other Education Pre", + "dates": { + "from": "1992-01-01", + "to": "1993-01-01" + } + } + ], + "receivedOtherEducationTrainingPostUnemployability": true, + "otherEducationTrainingPostUnemployability": [ + { + "name": "Other Education Post", + "dates": { + "from": "1998-01-01", + "to": "1999-01-01" + } + } + ], + "remarks": "Lorem ipsum dolor sit amet" + } + } +} + \ No newline at end of file diff --git a/spec/support/vcr_cassettes/uploads/validate_document.yml b/spec/support/vcr_cassettes/uploads/validate_document.yml new file mode 100644 index 00000000000..cf4ca401aa8 --- /dev/null +++ b/spec/support/vcr_cassettes/uploads/validate_document.yml @@ -0,0 +1,49 @@ +--- +http_interactions: +- request: + method: post + uri: "/services/vba_documents/v1/uploads/validate_document" + body: + encoding: ASCII-8BIT + string: !binary |- + JVBERi0xLjMKJeLjz9MKMSAwIG9iagoKPDwKL1N1YnR5cGUgL0Zvcm0KL1R5cGUgL1hPYmplY3QKL01hdHJpeCBbMSAwIDAgMSAwIDBdCi9Gb3JtVHlwZSAxCi9SZXNvdXJjZXMgCjw8Ci9Qcm9jU2V0IFsvUERGIC9UZXh0IC9JbWFnZUIgL0ltYWdlQyAvSW1hZ2VJXQovRm9udCAKPDwKL0YxLjAgMiAwIFIKPj4KPj4KL0xlbmd0aCAxMTUKL0JCb3ggWzAgMCA2MTIgNzkyXQo+PgpzdHJlYW0KcQoKQlQKNS4wIDUuMCBUZAovRjEuMCAxMCBUZgpbPDU2PiA4MCA8NDEyZTQ3NGY+IDUwIDw1NjIwMzIzMDMyMzQyZDMxMzIyZDMwMzMyMDMwMzUzYTM1MzEyMDUwNGQyMDU1NTQ0Mz5dIFRKCkVUCgpRCgplbmRzdHJlYW0gCmVuZG9iagoKMiAwIG9iagoKPDwKL1N1YnR5cGUgL1R5cGUxCi9CYXNlRm9udCAvSGVsdmV0aWNhCi9UeXBlIC9Gb250Ci9FbmNvZGluZyAvV2luQW5zaUVuY29kaW5nCj4+CmVuZG9iagoKMyAwIG9iagoKPDwKL0ZpbHRlciAvRmxhdGVEZWNvZGUKL0xlbmd0aCAxMAo+PgpzdHJlYW0KeJwr5AIAAO4AfAplbmRzdHJlYW0gCmVuZG9iagoKNCAwIG9iagoKPDwKL0ZpbHRlciAvRmxhdGVEZWNvZGUKL0xlbmd0aCAzNgo+PgpzdHJlYW0KeJxTCOQq5CpUMFQwAEIImZyroB+RaaDgkq8QyBXIBQB5OAcjCmVuZHN0cmVhbSAKZW5kb2JqCgo1IDAgb2JqCgo8PAovQ29udGVudHMgWzMgMCBSIDYgMCBSIDQgMCBSXQovVHlwZSAvUGFnZQovUmVzb3VyY2VzIAo8PAovQ29sb3JTcGFjZSAKPDwKL0NzMSA3IDAgUgo+PgovUHJvY1NldCBbL1BERiAvVGV4dCAvSW1hZ2VCIC9JbWFnZUMgL0ltYWdlSV0KL0ZvbnQgCjw8Ci9UVDEgOCAwIFIKPj4KL1hPYmplY3QgCjw8Ci9YaTAgMSAwIFIKPj4KPj4KL1BhcmVudCA5IDAgUgovTWVkaWFCb3ggWzAgMCA2MTIgNzkyXQo+PgplbmRvYmoKCjkg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CjAwMDAwMDAzNTcgMDAwMDAgbiAKMDAwMDAwMDQ1NiAwMDAwMCBuIAowMDAwMDAwNTQwIDAwMDAwIG4gCjAwMDAwMDA2NTAgMDAwMDAgbiAKMDAwMDAwMDk4MSAwMDAwMCBuIAowMDAwMDAxMzkxIDAwMDAwIG4gCjAwMDAwMDE0MjYgMDAwMDAgbiAKMDAwMDAwMDg5OCAwMDAwMCBuIAowMDAwMDAxMjkxIDAwMDAwIG4gCjAwMDAwMDE4MTEgMDAwMDAgbiAKMDAwMDAwMzA4NCAwMDAwMCBuIAowMDAwMDAzMDMyIDAwMDAwIG4gCjAwMDAwMDMzMzYgMDAwMDAgbiAKMDAwMDAxMDIyMSAwMDAwMCBuIAowMDAwMDEwMjU0IDAwMDAwIG4gCjAwMDAwMTAzMDggMDAwMDAgbiAKMDAwMDAxMDM1MSAwMDAwMCBuIAowMDAwMDEwMzcxIDAwMDAwIG4gCjAwMDAwMTAzOTkgMDAwMDAgbiAKMDAwMDAxMDQ0MiAwMDAwMCBuIAowMDAwMDEwNDYyIDAwMDAwIG4gCjAwMDAwMTA0ODQgMDAwMDAgbiAKdHJhaWxlcgoKPDwKL0luZm8gMjMgMCBSCi9JRCBbPDU0NDVhZTJmNzM3ZGRiMjA1MmVhZDIzMDZmNzk4NmYyPiA8NTQ0NWFlMmY3MzdkZGIyMDUyZWFkMjMwNmY3OTg2ZjI+XQovUm9vdCAxMyAwIFIKL1NpemUgMjQKPj4Kc3RhcnR4cmVmCjEwNzUxCiUlRU9GCg== + headers: + Accept: + - application/json + Content-Type: + - application/pdf + User-Agent: + - Vets.gov Agent + Apikey: + - fake_api_key + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 03 Dec 2024 17:51:46 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '52' + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + Cache-Control: + - '' + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Frame-Options: + - SAMEORIGIN + body: + encoding: UTF-8 + string: |- + { + "message":"Invalid authentication credentials" + } + recorded_at: Tue, 03 Dec 2024 17:51:46 GMT +recorded_with: VCR 6.3.1