diff --git a/app/controllers/state_file/archived_intakes/archived_intake_controller.rb b/app/controllers/state_file/archived_intakes/archived_intake_controller.rb new file mode 100644 index 0000000000..da3f6cc9fd --- /dev/null +++ b/app/controllers/state_file/archived_intakes/archived_intake_controller.rb @@ -0,0 +1,16 @@ +module StateFile + module ArchivedIntakes + class ArchivedIntakeController < ApplicationController + def current_request + StateFileArchivedIntakeRequest.find_by(ip_address: ip_for_irs, email_address: session[:email_address]) + end + + def create_state_file_access_log(event_type) + StateFileArchivedIntakeAccessLog.create!( + event_type: event_type, + state_file_archived_intake_request: current_request + ) + end + end + end +end \ No newline at end of file diff --git a/app/controllers/state_file/archived_intakes/email_address_controller.rb b/app/controllers/state_file/archived_intakes/email_address_controller.rb new file mode 100644 index 0000000000..4f69c7f025 --- /dev/null +++ b/app/controllers/state_file/archived_intakes/email_address_controller.rb @@ -0,0 +1,38 @@ +module StateFile + module ArchivedIntakes + class EmailAddressController < ArchivedIntakeController + before_action :check_feature_flag + def edit + @form = EmailAddressForm.new + end + + def update + @form = EmailAddressForm.new(email_address_form_params) + + if @form.save + archived_intake = StateFileArchivedIntake.find_by(email_address: @form.email_address) + session[:email_address] = @form.email_address + StateFileArchivedIntakeRequest.find_or_create_by(email_address: @form.email_address, ip_address: ip_for_irs, state_file_archived_intakes_id: archived_intake&.id ) + create_state_file_access_log(0) + + redirect_to state_file_archived_intakes_edit_verification_code_path + else + render :edit + end + end + + private + + def email_address_form_params + params.require(:state_file_archived_intakes_email_address_form).permit(:email_address) + end + + def check_feature_flag + unless Flipper.enabled?(:get_your_pdf) + redirect_to root_path + end + end + + end + end +end diff --git a/app/controllers/state_file/archived_intakes/verification_code_controller.rb b/app/controllers/state_file/archived_intakes/verification_code_controller.rb new file mode 100644 index 0000000000..0886525f9c --- /dev/null +++ b/app/controllers/state_file/archived_intakes/verification_code_controller.rb @@ -0,0 +1,50 @@ +module StateFile + module ArchivedIntakes + class VerificationCodeController < ArchivedIntakeController + before_action :check_feature_flag + def edit + if current_request.access_locked? + redirect_to root_path + return + end + @form = VerificationCodeForm.new(email_address: current_request.email_address) + @email_address = current_request.email_address + ArchivedIntakeEmailVerificationCodeJob.perform_later( + email_address: @email_address, + locale: I18n.locale + ) + end + + def update + @form = VerificationCodeForm.new(verification_code_form_params, email_address: current_request.email_address) + + if @form.valid? + create_state_file_access_log(1) + current_request.reset_failed_attempts! + redirect_to root_path + else + create_state_file_access_log(2) + current_request.increment_failed_attempts + if current_request.access_locked? + create_state_file_access_log(6) + redirect_to root_path + return + end + render :edit + end + end + + private + + def verification_code_form_params + params.require(:state_file_archived_intakes_verification_code_form).permit(:verification_code) + end + + def check_feature_flag + unless Flipper.enabled?(:get_your_pdf) + redirect_to root_path + end + end + end + end +end diff --git a/app/forms/state_file/archived_intakes/email_address_form.rb b/app/forms/state_file/archived_intakes/email_address_form.rb new file mode 100644 index 0000000000..013c211f6b --- /dev/null +++ b/app/forms/state_file/archived_intakes/email_address_form.rb @@ -0,0 +1,21 @@ +module StateFile + module ArchivedIntakes + class EmailAddressForm < Form + attr_accessor :email_address + + validates :email_address, 'valid_email_2/email': true + validates :email_address, presence: true + + def initialize(attributes = {}) + super + assign_attributes(attributes) + end + + def save + run_callbacks :save do + valid? + end + end + end + end +end diff --git a/app/forms/state_file/archived_intakes/verification_code_form.rb b/app/forms/state_file/archived_intakes/verification_code_form.rb new file mode 100644 index 0000000000..78d1f2f5bb --- /dev/null +++ b/app/forms/state_file/archived_intakes/verification_code_form.rb @@ -0,0 +1,30 @@ +module StateFile + module ArchivedIntakes + class VerificationCodeForm < Form + attr_accessor :verification_code, :email_address + + validates :verification_code, presence: true + def initialize(attributes = {}, email_address: nil) + super(attributes) + @email_address = email_address + assign_attributes(attributes) + end + + def valid? + hashed_verification_code = VerificationCodeService.hash_verification_code_with_contact_info(@email_address, verification_code) + + valid_code = EmailAccessToken.lookup(hashed_verification_code).exists? + + errors.add(:verification_code, I18n.t("state_file.archived_intakes.verification_code.edit.error_message")) unless valid_code + + valid_code.present? + end + + def save + run_callbacks :save do + valid? + end + end + end + end +end diff --git a/app/jobs/archived_intake_email_verification_code_job.rb b/app/jobs/archived_intake_email_verification_code_job.rb new file mode 100644 index 0000000000..541e46dcb4 --- /dev/null +++ b/app/jobs/archived_intake_email_verification_code_job.rb @@ -0,0 +1,12 @@ +class ArchivedIntakeEmailVerificationCodeJob < ApplicationJob + retry_on Mailgun::CommunicationError + + def priority + PRIORITY_HIGH - 1 # Subtracting one to push to the top of the queue + end + + # def perform(email_address: nil, locale:) + def perform(email_address:, locale:) + ArchivedIntakeEmailVerificationCodeService.request_code(email_address: email_address, locale: locale) + end +end diff --git a/app/models/state_file_archived_intake.rb b/app/models/state_file_archived_intake.rb index e5c464a926..e4ea6a41db 100644 --- a/app/models/state_file_archived_intake.rb +++ b/app/models/state_file_archived_intake.rb @@ -17,5 +17,5 @@ # class StateFileArchivedIntake < ApplicationRecord has_one_attached :submission_pdf - has_many :access_logs, class_name: 'StateFileArchivedIntakeAccessLog' + has_many :intake_requests, class_name: 'StateFileArchivedIntakeRequest' end diff --git a/app/models/state_file_archived_intake_access_log.rb b/app/models/state_file_archived_intake_access_log.rb index 4968350060..e8b6a045f4 100644 --- a/app/models/state_file_archived_intake_access_log.rb +++ b/app/models/state_file_archived_intake_access_log.rb @@ -2,24 +2,19 @@ # # Table name: state_file_archived_intake_access_logs # -# id :bigint not null, primary key -# details :jsonb -# event_type :integer -# ip_address :string -# created_at :datetime not null -# updated_at :datetime not null -# state_file_archived_intakes_id :bigint -# -# Indexes -# -# idx_on_state_file_archived_intakes_id_e878049c06 (state_file_archived_intakes_id) +# id :bigint not null, primary key +# details :jsonb +# event_type :integer +# created_at :datetime not null +# updated_at :datetime not null +# state_file_archived_intake_request_id :bigint # # Foreign Keys # -# fk_rails_... (state_file_archived_intakes_id => state_file_archived_intakes.id) +# fk_rails_... (state_file_archived_intake_request_id => state_file_archived_intake_requests.id) # class StateFileArchivedIntakeAccessLog < ApplicationRecord - belongs_to :state_file_archived_intake + belongs_to :state_file_archived_intake_request enum event_type: { issued_email_challenge: 0, correct_email_code: 1, diff --git a/app/models/state_file_archived_intake_request.rb b/app/models/state_file_archived_intake_request.rb new file mode 100644 index 0000000000..cd1666add2 --- /dev/null +++ b/app/models/state_file_archived_intake_request.rb @@ -0,0 +1,38 @@ +# == Schema Information +# +# Table name: state_file_archived_intake_requests +# +# id :bigint not null, primary key +# details :jsonb +# email_address :string +# failed_attempts :integer default(0), not null +# ip_address :string +# locked_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# state_file_archived_intakes_id :bigint +# +# Indexes +# +# idx_on_state_file_archived_intakes_id_31501c23f8 (state_file_archived_intakes_id) +# +# Foreign Keys +# +# fk_rails_... (state_file_archived_intakes_id => state_file_archived_intakes.id) +# +class StateFileArchivedIntakeRequest < ApplicationRecord + # Include default devise modules. Others available are: + devise :lockable, unlock_in: 60.minutes, unlock_strategy: :time + has_many :access_logs, class_name: 'StateFileArchivedIntakeAccessLog' + + def self.maximum_attempts + 2 + end + + def increment_failed_attempts + super + if attempts_exceeded? && !access_locked? + lock_access! + end + end +end diff --git a/app/models/state_file_base_intake.rb b/app/models/state_file_base_intake.rb index fc4df000ac..24b533ac7e 100644 --- a/app/models/state_file_base_intake.rb +++ b/app/models/state_file_base_intake.rb @@ -135,8 +135,8 @@ def synchronize_df_w2s_to_database box_14_values[deduction[:other_description]] = deduction[:other_amount] end state_file_w2.assign_attributes( - box14_ui_wf_swf: box_14_values['UIWFSWF'], - box14_ui_hc_wd: box_14_values['UIHCWD'], + box14_ui_wf_swf: box_14_values['UI/WF/SWF'], + box14_ui_hc_wd: box_14_values['UI/HC/WD'], box14_fli: box_14_values['FLI'], box14_stpickup: box_14_values['STPICKUP'], employer_ein: direct_file_w2.EmployerEIN, diff --git a/app/services/archived_intake_email_verification_code_service.rb b/app/services/archived_intake_email_verification_code_service.rb new file mode 100644 index 0000000000..55391c2c4f --- /dev/null +++ b/app/services/archived_intake_email_verification_code_service.rb @@ -0,0 +1,19 @@ +class ArchivedIntakeEmailVerificationCodeService + def initialize(email_address: , locale: :en) + @email_address = email_address + @locale = locale + end + + def request_code + verification_code, = EmailAccessToken.generate!(email_address: @email_address) + VerificationCodeMailer.archived_intake_verification_code( + to: @email_address, + verification_code: verification_code, + locale: @locale + ).deliver_now + end + + def self.request_code(**args) + new(**args).request_code + end +end \ No newline at end of file diff --git a/app/views/state_file/archived_intakes/email_address/edit.html.erb b/app/views/state_file/archived_intakes/email_address/edit.html.erb new file mode 100644 index 0000000000..f221bc5556 --- /dev/null +++ b/app/views/state_file/archived_intakes/email_address/edit.html.erb @@ -0,0 +1,21 @@ +<% title = t(".enter_email") %> +<% content_for :page_title, title %> +
-outer"> +
+
+

<%= title %>

+ <%= form_with model: @form, url: state_file_archived_intakes_email_address_path, local: true, method: :patch, builder: VitaMinFormBuilder do |f| %> +
+ <%= f.cfa_input_field(:email_address, t("state_file.questions.email_address.edit.email_address_label"), help_text: 'example@email.com', classes: ["form-width--long"]) %> +
+ +
+ +
+ <% end %> +
+
+
+ diff --git a/app/views/state_file/archived_intakes/verification_code/edit.html.erb b/app/views/state_file/archived_intakes/verification_code/edit.html.erb new file mode 100644 index 0000000000..658c97964a --- /dev/null +++ b/app/views/state_file/archived_intakes/verification_code/edit.html.erb @@ -0,0 +1,23 @@ +<% title = t(".title_html", email_address: @email_address) %> +
-outer"> +
+
+
+ <% content_for :page_title, title %> + <%= form_with model: @form, url: { action: :update }, local: true, method: :patch, builder: VitaMinFormBuilder do |f| %> +

<%= title %>

+

<%= t(".subtitle_html") %>

+

<%= t(".subtitle_html_2") %>

+
+ <%= f.cfa_input_field(:verification_code, t("state_file.questions.verification_code.edit.verification_code_label"), classes: ["form-width--long"]) %> +
+
+ +
+ <% end %> +
+
+
+
\ No newline at end of file diff --git a/app/views/state_file/state_file_pages/about_page.html.erb b/app/views/state_file/state_file_pages/about_page.html.erb index 7f60c9e235..b9d54ff9d2 100644 --- a/app/views/state_file/state_file_pages/about_page.html.erb +++ b/app/views/state_file/state_file_pages/about_page.html.erb @@ -42,4 +42,13 @@ <% end %> -<% end %> \ No newline at end of file +<% end %> + +
+ <% if Flipper.enabled?(:get_your_pdf) %> + <%= t(".looking_for_return_html")%> +
+ <%= link_to t(".tax_return_link"), state_file_archived_intakes_edit_email_address_path %> +
+ <% end %> +
diff --git a/config/locales/en.yml b/config/locales/en.yml index bbc9fe8516..d054bfb7be 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2063,6 +2063,16 @@ en: 04_detail_links_html: See our Terms and Conditions and Privacy Policy. subheader: Please sign up here to receive a notification when we open in January. state_file: + archived_intakes: + email_address: + edit: + enter_email: Enter the email address linked to your account + verification_code: + edit: + error_message: Incorrect verification code. After 2 failed attempts, accounts are locked. + subtitle_html: If you didn’t receive a code, please check your spam folder. + subtitle_html_2: If you still need help resubscribing, chat with us. + title_html: We’ve sent your code to %{email_address} faq: index: title: Common questions from %{state} taxpayers @@ -3828,6 +3838,7 @@ en: We're closed for the tax season. Unfortunately you can no longer file your state return with us this year.

Already filed your state taxes with us? You can download a copy of your state return until December 31, 2024

header: A free state filing service for taxpayers using IRS Direct File + looking_for_return_html: "Looking for your 2023 Arizona or New York State Tax Return?" section1_html: | How does it work?
    @@ -3842,6 +3853,7 @@ en: subheader_1_html: "FileYourStateTaxes integrates with IRS Direct File to help you complete your state tax return for free." subheader_2_html: IRS Direct File is now broadly available to all eligible taxpayers through April 15, 2024. For more information on timeline and eligibility, or to start filing your federal return, go to directfile.irs.gov subheader_3_html: Already filed your federal return with IRS Direct File and need to complete your state return? Sign in here. + tax_return_link: Click here to access your tax return card_postscript: responses_saved_html: Your responses are saved. If you need a break, you can come back and log in to your account at fileyourstatetaxes.org. coming_soon: diff --git a/config/locales/es.yml b/config/locales/es.yml index b8eab4a065..ea12e07f5f 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -2015,6 +2015,16 @@ es: 04_detail_links_html: Favor de consultar nuestras Condiciones generales y Política de privacidad. subheader: Regístrese aquí para recibir una notificación cuando abramos en enero. state_file: + archived_intakes: + email_address: + edit: + enter_email: Ingresa la dirección de correo electrónico vinculada a tu cuenta + verification_code: + edit: + error_message: Después de 2 intentos fallidos, las cuentas serán bloqueadas. + subtitle_html: Si no recibiste un código, revisa tu carpeta de spam. Si aún necesitas ayuda para volver a suscribirte, chatea con nosotros. + subtitle_html_2: Si aún necesitas ayuda para resuscribirte, chatea con nosotros. + title_html: Hemos enviado tu código a %{email_address} faq: index: title: 'Preguntas frecuentes de los contribuyentes de %{state}:' @@ -3802,6 +3812,7 @@ es: Estamos cerrados por la temporada de impuestos. Desafortunadamente, no puede presentar tu declaración estatal con nosotros este año.

    ¿Ya presentó tus impuestos estatales con nosotros? Puedes descargar una copia de tu declaración estatal hasta el 31 de diciembre de 2024

    header: Una herramienta sin costo para presentar impuestos estatales para los contribuyentes utilizando IRS Direct File + looking_for_return_html: "¿Está buscando su declaración de impuestos del estado de Arizona o Nueva York de 2023?" section1_html: | ¿Cómo funciona?
      @@ -3816,6 +3827,7 @@ es: subheader_1_html: "FileYourStateTaxes se integra con IRS Direct File para ayudarte a completar tu declaración de impuestos estatales sin costo." subheader_2_html: El IRS Direct File ahora está ampliamente disponible para todos los contribuyentes elegibles hasta el 15 de abril de 2024. Para obtener más información sobre el cronograma y la elegibilidad, o para comenzar a presentar tu declaración federal, ve a directfile.irs.gov. subheader_3_html: "¿Ya presentaste tu declaración federal con IRS Direct File y necesitas completar tu declaración estatal? Inicia sesión aquí." + tax_return_link: Haga clic aquí para acceder a su declaración de impuestos. card_postscript: responses_saved_html: Tus respuestas han sido guardadas. Si necesitas hacer una pausa, puedes regresar e iniciar sesión en tu cuenta en fileyourstatetaxes.org. coming_soon: diff --git a/config/routes.rb b/config/routes.rb index 6d167bb0ce..0b7da310f9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,5 @@ Rails.application.routes.draw do + devise_for :state_file_archived_intake_requests active_state_codes = StateFile::StateInformationService.active_state_codes active_state_codes.each do |code| @@ -555,6 +556,12 @@ def scoped_navigation_routes(context, navigation) constraints(Routes::StateFileDomain.new) do scope '(:locale)', locale: /#{I18n.available_locales.join('|')}/ do namespace :state_file do + namespace :archived_intakes do + get 'email_address/edit', to: 'email_address#edit', as: 'edit_email_address' + patch 'email_address', to: 'email_address#update' + get 'verification_code/edit', to: 'verification_code#edit', as: 'edit_verification_code' + patch 'verification_code', to: 'verification_code#update' + end namespace :questions do get "show_xml", to: "confirmation#show_xml" get "explain_calculations", to: "confirmation#explain_calculations" diff --git a/db/migrate/20241227174414_create_state_file_archived_intake_access_logs.rb b/db/migrate/20241227174414_create_state_file_archived_intake_access_logs.rb index 0462ef3a36..9a0b9c385b 100644 --- a/db/migrate/20241227174414_create_state_file_archived_intake_access_logs.rb +++ b/db/migrate/20241227174414_create_state_file_archived_intake_access_logs.rb @@ -3,7 +3,6 @@ def change create_table :state_file_archived_intake_access_logs do |t| t.belongs_to 'state_file_archived_intakes', foreign_key: true t.integer 'event_type' - t.string 'ip_address' t.jsonb 'details', default: '{}' t.timestamps end diff --git a/db/migrate/20250108221853_state_file_archived_intake_requests.rb b/db/migrate/20250108221853_state_file_archived_intake_requests.rb new file mode 100644 index 0000000000..55c19af010 --- /dev/null +++ b/db/migrate/20250108221853_state_file_archived_intake_requests.rb @@ -0,0 +1,14 @@ +class StateFileArchivedIntakeRequests < ActiveRecord::Migration[7.1] + def change + create_table :state_file_archived_intake_requests do |t| + t.belongs_to :state_file_archived_intakes, foreign_key: true + t.string 'ip_address' + t.string 'email_address' + t.jsonb 'details', default: '{}' + t.timestamps + end + + remove_foreign_key :state_file_archived_intake_access_logs, :state_file_archived_intakes + safety_assured { remove_column :state_file_archived_intake_access_logs, :state_file_archived_intakes_id } + end +end diff --git a/db/migrate/20250108223733_add_devise_to_state_file_archived_intake_requests.rb b/db/migrate/20250108223733_add_devise_to_state_file_archived_intake_requests.rb new file mode 100644 index 0000000000..4f453ec1fb --- /dev/null +++ b/db/migrate/20250108223733_add_devise_to_state_file_archived_intake_requests.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class AddDeviseToStateFileArchivedIntakeRequests < ActiveRecord::Migration[7.1] + def change + add_column :state_file_archived_intake_requests, :failed_attempts, :integer, default: 0, null: false + add_column :state_file_archived_intake_requests, :locked_at, :datetime + end +end diff --git a/db/migrate/20250108230315_add_foreign_keyto_state_file_archived_intake_access_logs.rb b/db/migrate/20250108230315_add_foreign_keyto_state_file_archived_intake_access_logs.rb new file mode 100644 index 0000000000..062afa8263 --- /dev/null +++ b/db/migrate/20250108230315_add_foreign_keyto_state_file_archived_intake_access_logs.rb @@ -0,0 +1,6 @@ +class AddForeignKeytoStateFileArchivedIntakeAccessLogs < ActiveRecord::Migration[7.1] + def change + add_column :state_file_archived_intake_access_logs, :state_file_archived_intake_request_id, :bigint + add_foreign_key :state_file_archived_intake_access_logs, :state_file_archived_intake_requests, column: :state_file_archived_intake_request_id, validate: false + end +end diff --git a/db/migrate/20250108231212_validate_foreign_keyfor_state_file_archived_intake_access_logs.rb b/db/migrate/20250108231212_validate_foreign_keyfor_state_file_archived_intake_access_logs.rb new file mode 100644 index 0000000000..0f849150dc --- /dev/null +++ b/db/migrate/20250108231212_validate_foreign_keyfor_state_file_archived_intake_access_logs.rb @@ -0,0 +1,5 @@ +class ValidateForeignKeyforStateFileArchivedIntakeAccessLogs < ActiveRecord::Migration[7.1] + def change + validate_foreign_key :state_file_archived_intake_access_logs, :state_file_archived_intake_requests + end +end diff --git a/db/schema.rb b/db/schema.rb index 8f0ae6dbfa..40c98bce9e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1688,10 +1688,20 @@ t.datetime "created_at", null: false t.jsonb "details", default: "{}" t.integer "event_type" + t.bigint "state_file_archived_intake_request_id" + t.datetime "updated_at", null: false + end + + create_table "state_file_archived_intake_requests", force: :cascade do |t| + t.datetime "created_at", null: false + t.jsonb "details", default: "{}" + t.string "email_address" + t.integer "failed_attempts", default: 0, null: false t.string "ip_address" + t.datetime "locked_at" t.bigint "state_file_archived_intakes_id" t.datetime "updated_at", null: false - t.index ["state_file_archived_intakes_id"], name: "idx_on_state_file_archived_intakes_id_e878049c06" + t.index ["state_file_archived_intakes_id"], name: "idx_on_state_file_archived_intakes_id_31501c23f8" end create_table "state_file_archived_intakes", force: :cascade do |t| @@ -2829,7 +2839,8 @@ add_foreign_key "site_coordinator_roles_vita_partners", "site_coordinator_roles" add_foreign_key "site_coordinator_roles_vita_partners", "vita_partners" add_foreign_key "source_parameters", "vita_partners" - add_foreign_key "state_file_archived_intake_access_logs", "state_file_archived_intakes", column: "state_file_archived_intakes_id" + add_foreign_key "state_file_archived_intake_access_logs", "state_file_archived_intake_requests" + add_foreign_key "state_file_archived_intake_requests", "state_file_archived_intakes", column: "state_file_archived_intakes_id" add_foreign_key "state_routing_fractions", "state_routing_targets" add_foreign_key "state_routing_fractions", "vita_partners" add_foreign_key "system_notes", "clients" diff --git a/spec/controllers/state_file/archived_intake/archived_intake_controller_spec.rb b/spec/controllers/state_file/archived_intake/archived_intake_controller_spec.rb new file mode 100644 index 0000000000..299a0f3bac --- /dev/null +++ b/spec/controllers/state_file/archived_intake/archived_intake_controller_spec.rb @@ -0,0 +1,58 @@ +require 'rails_helper' + +describe StateFile::ArchivedIntakes::ArchivedIntakeController, type: :controller do + let(:ip_address) { '192.168.0.1' } + let(:email_address) { 'test@example.com' } + let(:request_instance) { instance_double(StateFileArchivedIntakeRequest) } + + before do + allow(controller).to receive(:ip_for_irs).and_return(ip_address) + allow(session).to receive(:[]).with(:email_address).and_return(email_address) + end + + describe '#current_request' do + it 'finds the StateFileArchivedIntakeRequest by IP and email address' do + expect(StateFileArchivedIntakeRequest).to receive(:find_by).with( + ip_address: ip_address, + email_address: email_address + ).and_return(request_instance) + + expect(controller.current_request).to eq(request_instance) + end + + it 'returns nil if no request is found' do + expect(StateFileArchivedIntakeRequest).to receive(:find_by).with( + ip_address: ip_address, + email_address: email_address + ).and_return(nil) + + expect(controller.current_request).to be_nil + end + end + + describe '#create_state_file_access_log' do + let(:event_type) { 'access_granted' } + let(:access_log_instance) { instance_double(StateFileArchivedIntakeAccessLog) } + + before do + allow(controller).to receive(:current_request).and_return(request_instance) + end + + it 'creates a StateFileArchivedIntakeAccessLog with the correct attributes' do + expect(StateFileArchivedIntakeAccessLog).to receive(:create!).with( + event_type: event_type, + state_file_archived_intake_request: request_instance + ).and_return(access_log_instance) + + controller.create_state_file_access_log(event_type) + end + + it 'raises an error if the log cannot be created' do + allow(StateFileArchivedIntakeAccessLog).to receive(:create!).and_raise(ActiveRecord::RecordInvalid) + + expect { + controller.create_state_file_access_log(event_type) + }.to raise_error(ActiveRecord::RecordInvalid) + end + end +end diff --git a/spec/controllers/state_file/archived_intake/email_address_controller_spec.rb b/spec/controllers/state_file/archived_intake/email_address_controller_spec.rb new file mode 100644 index 0000000000..84a0cd9d4d --- /dev/null +++ b/spec/controllers/state_file/archived_intake/email_address_controller_spec.rb @@ -0,0 +1,85 @@ +require "rails_helper" + +RSpec.describe StateFile::ArchivedIntakes::EmailAddressController, type: :controller do + before do + Flipper.enable(:get_your_pdf) + end + describe "GET #edit" do + it "renders the edit template with a new EmailAddressForm" do + get :edit + + expect(assigns(:form)).to be_a(StateFile::ArchivedIntakes::EmailAddressForm) + expect(response).to render_template(:edit) + end + end + + describe "POST #update" do + let(:valid_email_address) { "test@example.com" } + let(:invalid_email_address) { "" } + let(:ip_address) { "127.0.0.1" } + + before do + allow(controller).to receive(:ip_for_irs).and_return(ip_address) + end + + context "when the form is valid" do + context "and a archived intake exists with the email address" do + let!(:archived_intake) { create :state_file_archived_intake, email_address: valid_email_address } + it "creates an access log create a request and redirects to the verification code page" do + post :update, params: { + state_file_archived_intakes_email_address_form: { email_address: valid_email_address } + } + expect(assigns(:form)).to be_valid + + request = StateFileArchivedIntakeRequest.last + expect(request.ip_address).to eq(ip_address) + expect(request.email_address).to eq(valid_email_address) + expect(request.state_file_archived_intakes_id).to eq(archived_intake.id) + + log = StateFileArchivedIntakeAccessLog.last + expect(log.state_file_archived_intake_request_id).to eq(request.id) + expect(log.event_type).to eq("issued_email_challenge") + + expect(response).to redirect_to( + state_file_archived_intakes_edit_verification_code_path + ) + end + end + + context "and a archived does not exist with the email address" do + it "creates an access log create a request and redirects to the verification code page" do + post :update, params: { + state_file_archived_intakes_email_address_form: { email_address: valid_email_address } + } + expect(assigns(:form)).to be_valid + + request = StateFileArchivedIntakeRequest.last + expect(request.ip_address).to eq(ip_address) + expect(request.email_address).to eq(valid_email_address) + expect(request.state_file_archived_intakes_id).to eq(nil) + + log = StateFileArchivedIntakeAccessLog.last + expect(log.state_file_archived_intake_request_id).to eq(request.id) + expect(log.event_type).to eq("issued_email_challenge") + + expect(response).to redirect_to( + state_file_archived_intakes_edit_verification_code_path + ) + end + end + end + context "when the form is invalid" do + it "renders the edit template" do + post :update, params: { + state_file_archived_intakes_email_address_form: { email_address: invalid_email_address } + } + + expect(assigns(:form)).not_to be_valid + + expect(StateFileArchivedIntakeAccessLog.count).to eq(0) + + expect(response).to render_template(:edit) + end + end + end +end diff --git a/spec/controllers/state_file/archived_intake/verification_code_controller_spec.rb b/spec/controllers/state_file/archived_intake/verification_code_controller_spec.rb new file mode 100644 index 0000000000..d984941885 --- /dev/null +++ b/spec/controllers/state_file/archived_intake/verification_code_controller_spec.rb @@ -0,0 +1,83 @@ +require "rails_helper" + +RSpec.describe StateFile::ArchivedIntakes::VerificationCodeController, type: :controller do + let(:current_request) { create(:state_file_archived_intake_request, failed_attempts: 0) } + let(:email_address) { "test@example.com" } + let(:valid_verification_code) { "123456" } + let(:invalid_verification_code) { "654321" } + + before do + Flipper.enable(:get_your_pdf) + allow(controller).to receive(:current_request).and_return(current_request) + allow(current_request).to receive(:email_address).and_return(email_address) + allow(I18n).to receive(:locale).and_return(:en) + end + + describe "GET #edit" do + it "renders the edit template with a new VerificationCodeForm and queues a job" do + expect(ArchivedIntakeEmailVerificationCodeJob).to receive(:perform_later).with( + email_address: email_address, + locale: :en + ) + + get :edit + + expect(assigns(:form)).to be_a(StateFile::ArchivedIntakes::VerificationCodeForm) + expect(assigns(:email_address)).to eq(email_address) + expect(response).to render_template(:edit) + end + end + + describe "POST #update" do + context "with a valid verification code" do + before do + allow_any_instance_of(StateFile::ArchivedIntakes::VerificationCodeForm).to receive(:valid?).and_return(true) + end + + it "creates a success access log and does not increment failed_attempts" do + expect { + post :update, params: { state_file_archived_intakes_verification_code_form: { verification_code: valid_verification_code } } + }.to change(StateFileArchivedIntakeAccessLog, :count).by(1) + + log = StateFileArchivedIntakeAccessLog.last + expect(log.event_type).to eq("correct_email_code") + expect(current_request.failed_attempts).to eq(0) + expect(response).to redirect_to(root_path) + end + end + + context "with an invalid verification code" do + before do + allow_any_instance_of(StateFile::ArchivedIntakes::VerificationCodeForm).to receive(:valid?).and_return(false) + end + + it "creates a failure access log, increments failed_attempts, and re-renders edit on first failed attempt" do + expect { + post :update, params: { state_file_archived_intakes_verification_code_form: { verification_code: invalid_verification_code } } + }.to change(StateFileArchivedIntakeAccessLog, :count).by(1) + + log = StateFileArchivedIntakeAccessLog.last + expect(log.event_type).to eq("incorrect_email_code") + + expect(current_request.reload.failed_attempts).to eq(1) + expect(assigns(:form)).to be_a(StateFile::ArchivedIntakes::VerificationCodeForm) + expect(response).to render_template(:edit) + end + + it "locks the account and redirects to root path after multiple failed attempts" do + current_request.update!(failed_attempts: 1) + + expect { + post :update, params: { state_file_archived_intakes_verification_code_form: { verification_code: invalid_verification_code } } + }.to change(StateFileArchivedIntakeAccessLog, :count).by(2) + + log = StateFileArchivedIntakeAccessLog.last + expect(log.event_type).to eq("client_lockout_begin") + + expect(current_request.reload.failed_attempts).to eq(2) + expect(current_request.reload.access_locked?).to be_truthy + expect(response).to redirect_to(root_path) + end + end + end +end diff --git a/spec/factories/state_file/state_file_archived_intake_requests.rb b/spec/factories/state_file/state_file_archived_intake_requests.rb new file mode 100644 index 0000000000..bc6b96dc4c --- /dev/null +++ b/spec/factories/state_file/state_file_archived_intake_requests.rb @@ -0,0 +1,11 @@ +FactoryBot.define do + factory :state_file_archived_intake_request do + email_address { "geddy_lee@gmail.com" } + failed_attempts { 0 } + locked_at { nil } + + trait :locked do + locked_at { Time.current } + end + end +end diff --git a/spec/factories/state_file_archived_intake_access_logs.rb b/spec/factories/state_file_archived_intake_access_logs.rb index 38637da7b9..6c094cd6dd 100644 --- a/spec/factories/state_file_archived_intake_access_logs.rb +++ b/spec/factories/state_file_archived_intake_access_logs.rb @@ -2,21 +2,16 @@ # # Table name: state_file_archived_intake_access_logs # -# id :bigint not null, primary key -# details :jsonb -# event_type :integer -# ip_address :string -# created_at :datetime not null -# updated_at :datetime not null -# state_file_archived_intakes_id :bigint -# -# Indexes -# -# idx_on_state_file_archived_intakes_id_e878049c06 (state_file_archived_intakes_id) +# id :bigint not null, primary key +# details :jsonb +# event_type :integer +# created_at :datetime not null +# updated_at :datetime not null +# state_file_archived_intake_request_id :bigint # # Foreign Keys # -# fk_rails_... (state_file_archived_intakes_id => state_file_archived_intakes.id) +# fk_rails_... (state_file_archived_intake_request_id => state_file_archived_intake_requests.id) # FactoryBot.define do factory :state_file_archived_intake_access_log do diff --git a/spec/factories/state_file_archived_intakes.rb b/spec/factories/state_file_archived_intakes.rb index 0f68fb4bac..3c61fa00a5 100644 --- a/spec/factories/state_file_archived_intakes.rb +++ b/spec/factories/state_file_archived_intakes.rb @@ -17,6 +17,15 @@ # FactoryBot.define do factory :state_file_archived_intake do + email_address { "geddy_lee@example.com" } + hashed_ssn { "hashed_ssn_value" } + mailing_apartment { "Apt 1" } + mailing_city { "Test City" } + mailing_state { "CA" } + mailing_street { "123 Test Street" } + mailing_zip { "12345" } + state_code { "CA" } + tax_year { 2023 } submission_pdf { nil } end -end +end \ No newline at end of file diff --git a/spec/forms/state_file/archived_intakes/email_address_form_spec.rb b/spec/forms/state_file/archived_intakes/email_address_form_spec.rb new file mode 100644 index 0000000000..8f3b88d4d9 --- /dev/null +++ b/spec/forms/state_file/archived_intakes/email_address_form_spec.rb @@ -0,0 +1,55 @@ +require "rails_helper" + +RSpec.describe StateFile::ArchivedIntakes::EmailAddressForm do + describe "#valid?" do + context "when the email address is valid" do + it "returns true" do + form = StateFile::ArchivedIntakes::EmailAddressForm.new(email_address: "test@example.com") + + expect(form.valid?).to be true + end + end + + context "when the email address is invalid" do + it "returns false for an improperly formatted email" do + form = StateFile::ArchivedIntakes::EmailAddressForm.new(email_address: "invalid-email") + + expect(form.valid?).to be false + expect(form.errors[:email_address]).to include("Please enter a valid email address.") + end + + it "returns false when the email is blank" do + form = StateFile::ArchivedIntakes::EmailAddressForm.new(email_address: "") + + expect(form.valid?).to be false + expect(form.errors[:email_address]).to include("Can't be blank.") + end + end + end + + describe "#save" do + context "when the form is valid" do + it "returns true" do + form = StateFile::ArchivedIntakes::EmailAddressForm.new(email_address: "test@example.com") + + expect(form.save).to be true + end + end + + context "when the form is invalid" do + it "returns false" do + form = StateFile::ArchivedIntakes::EmailAddressForm.new(email_address: "") + + expect(form.save).to be false + end + end + end + + describe "#initialize" do + it "assigns attributes correctly" do + form = StateFile::ArchivedIntakes::EmailAddressForm.new(email_address: "test@example.com") + + expect(form.email_address).to eq("test@example.com") + end + end +end diff --git a/spec/forms/state_file/archived_intakes/verification_code_form_spec.rb b/spec/forms/state_file/archived_intakes/verification_code_form_spec.rb new file mode 100644 index 0000000000..3bce7d1a55 --- /dev/null +++ b/spec/forms/state_file/archived_intakes/verification_code_form_spec.rb @@ -0,0 +1,100 @@ +require "rails_helper" + +RSpec.describe StateFile::ArchivedIntakes::VerificationCodeForm do + describe "#valid?" do + context "when the verification code is present and valid" do + it "returns true" do + allow(VerificationCodeService).to receive(:hash_verification_code_with_contact_info) + .with("test@example.com", "123456") + .and_return("hashed_code") + + allow(EmailAccessToken).to receive_message_chain(:lookup, :exists?).and_return(true) + + form = StateFile::ArchivedIntakes::VerificationCodeForm.new( + { verification_code: "123456" }, + email_address: "test@example.com" + ) + + expect(form.valid?).to be true + end + end + + context "when the verification code is present but invalid" do + it "adds an error and returns false" do + allow(VerificationCodeService).to receive(:hash_verification_code_with_contact_info) + .with("test@example.com", "123456") + .and_return("hashed_code") + + allow(EmailAccessToken).to receive_message_chain(:lookup, :exists?).and_return(false) + + form = StateFile::ArchivedIntakes::VerificationCodeForm.new( + { verification_code: "123456" }, + email_address: "test@example.com" + ) + + expect(form.valid?).to be false + expect(form.errors[:verification_code]).to include("Incorrect verification code. After 2 failed attempts, accounts are locked.") + end + end + + context "when the verification code is blank" do + it "adds an error and returns false" do + form = StateFile::ArchivedIntakes::VerificationCodeForm.new( + { verification_code: "" }, + email_address: "test@example.com" + ) + + expect(form.valid?).to be false + expect(form.errors[:verification_code]).to include("Incorrect verification code. After 2 failed attempts, accounts are locked.") + end + end + end + + describe "#save" do + context "when the form is valid" do + it "returns true" do + allow(VerificationCodeService).to receive(:hash_verification_code_with_contact_info) + .with("test@example.com", "123456") + .and_return("hashed_code") + + allow(EmailAccessToken).to receive_message_chain(:lookup, :exists?).and_return(true) + + form = StateFile::ArchivedIntakes::VerificationCodeForm.new( + { verification_code: "123456" }, + email_address: "test@example.com" + ) + + expect(form.save).to be true + end + end + + context "when the form is invalid" do + it "returns false" do + allow(VerificationCodeService).to receive(:hash_verification_code_with_contact_info) + .with("test@example.com", "") + .and_return("hashed_code") + + allow(EmailAccessToken).to receive_message_chain(:lookup, :exists?).and_return(false) + + form = StateFile::ArchivedIntakes::VerificationCodeForm.new( + { verification_code: "" }, + email_address: "test@example.com" + ) + + expect(form.save).to be false + end + end + end + + describe "#initialize" do + it "assigns attributes correctly" do + form = StateFile::ArchivedIntakes::VerificationCodeForm.new( + { verification_code: "123456" }, + email_address: "test@example.com" + ) + + expect(form.verification_code).to eq("123456") + expect(form.email_address).to eq("test@example.com") + end + end +end diff --git a/spec/models/state_file_archived_intake_access_log_spec.rb b/spec/models/state_file_archived_intake_access_log_spec.rb index 0c8b0e1989..b65c56e7bd 100644 --- a/spec/models/state_file_archived_intake_access_log_spec.rb +++ b/spec/models/state_file_archived_intake_access_log_spec.rb @@ -2,21 +2,16 @@ # # Table name: state_file_archived_intake_access_logs # -# id :bigint not null, primary key -# details :jsonb -# event_type :integer -# ip_address :string -# created_at :datetime not null -# updated_at :datetime not null -# state_file_archived_intakes_id :bigint -# -# Indexes -# -# idx_on_state_file_archived_intakes_id_e878049c06 (state_file_archived_intakes_id) +# id :bigint not null, primary key +# details :jsonb +# event_type :integer +# created_at :datetime not null +# updated_at :datetime not null +# state_file_archived_intake_request_id :bigint # # Foreign Keys # -# fk_rails_... (state_file_archived_intakes_id => state_file_archived_intakes.id) +# fk_rails_... (state_file_archived_intake_request_id => state_file_archived_intake_requests.id) # require 'rails_helper'