diff --git a/config/locales/en.yml b/config/locales/en.yml
index fc485e70d9..e8f78a15d7 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -417,6 +417,10 @@ en:
presence: "Enter an email address"
format: "Enter an email address in the correct format, like name@example.com"
length: "Email address must be 256 characters or less"
+ email_verification:
+ errors:
+ one_time_password:
+ invalid: An error occured while validating the passcode, please try generating a new one
mobile_number:
errors:
invalid: "Enter a mobile number, like 07700 900 982 or +44 7700 900 982"
@@ -535,6 +539,17 @@ en:
errors:
blank: Select an additional payment
inclusion: Select a valid additional payment
+ reminders:
+ personal_details:
+ errors:
+ full_name:
+ blank: Enter full name
+ length: Full name must be 100 characters or less
+ email_address:
+ blank: Enter an email address
+ invalid: Enter an email address in the correct format, like name@example.com
+ length: Email address must be 256 characters or less
+ unauthorised: Only authorised email addresses can be used when using a team-only API key
check_your_answers:
part_one:
primary_heading: Check your answers
diff --git a/config/routes.rb b/config/routes.rb
index 5cae21cc67..c0ae7ecca6 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -59,8 +59,8 @@ def matches?(request)
end
scope constraints: {journey: "additional-payments"} do
- get "reminders/personal-details", as: :new_reminder, to: "journeys/additional_payments_for_teaching/reminders#new"
- post "reminders/personal-details", as: :reminders, to: "journeys/additional_payments_for_teaching/reminders#create"
+ get "reminder", as: :new_reminder, to: "journeys/additional_payments_for_teaching/reminders#new"
+ post "reminders/:slug", constraints: {slug: %r{#{Journeys::AdditionalPaymentsForTeaching::SlugSequence::REMINDER_SLUGS.join("|")}}}, defaults: {slug: "personal-details"}, as: :reminders, to: "journeys/additional_payments_for_teaching/reminders#create"
resources :reminders, only: [:show, :update], param: :slug, constraints: {slug: %r{#{Journeys::AdditionalPaymentsForTeaching::SlugSequence::REMINDER_SLUGS.join("|")}}}, controller: "journeys/additional_payments_for_teaching/reminders"
end
diff --git a/spec/features/reminders_spec.rb b/spec/features/reminders_spec.rb
index f6e9cdfbd9..16be3353d9 100644
--- a/spec/features/reminders_spec.rb
+++ b/spec/features/reminders_spec.rb
@@ -62,7 +62,7 @@
fill_in "Full name", with: "David Tau"
fill_in "Email address", with: "david.tau1988@hotmail.co.uk"
click_on "Continue"
- fill_in "reminder_one_time_password", with: get_otp_from_email
+ fill_in "form_one_time_password", with: get_otp_from_email
click_on "Confirm"
reminder = Reminder.order(:created_at).last
@@ -140,7 +140,7 @@
expect(page).to have_text("Personal details")
click_on "Continue"
- fill_in "reminder_one_time_password", with: get_otp_from_email
+ fill_in "form_one_time_password", with: get_otp_from_email
click_on "Confirm"
reminder = Reminder.order(:created_at).last
diff --git a/spec/features/trainee_teacher_subjourney_for_lup_schools_spec.rb b/spec/features/trainee_teacher_subjourney_for_lup_schools_spec.rb
index dae15820e1..2d603472ed 100644
--- a/spec/features/trainee_teacher_subjourney_for_lup_schools_spec.rb
+++ b/spec/features/trainee_teacher_subjourney_for_lup_schools_spec.rb
@@ -41,7 +41,7 @@
fill_in "Full name", with: "David Tau"
fill_in "Email address", with: "david.tau1988@hotmail.co.uk"
click_on "Continue"
- fill_in "reminder_one_time_password", with: get_otp_from_email
+ fill_in "form_one_time_password", with: get_otp_from_email
click_on "Confirm"
reminder = Reminder.order(:created_at).last
@@ -76,7 +76,7 @@
fill_in "Full name", with: "David Tau"
fill_in "Email address", with: "david.tau1988@hotmail.co.uk"
click_on "Continue"
- fill_in "reminder_one_time_password", with: get_otp_from_email
+ fill_in "form_one_time_password", with: get_otp_from_email
click_on "Confirm"
reminder = Reminder.order(:created_at).last
diff --git a/spec/forms/email_verification_form_spec.rb b/spec/forms/email_verification_form_spec.rb
index 57de0f4851..3d5c58f25a 100644
--- a/spec/forms/email_verification_form_spec.rb
+++ b/spec/forms/email_verification_form_spec.rb
@@ -74,6 +74,12 @@
it { is_expected.not_to be_valid }
end
+ context "when the code generation timestamp is missing" do
+ let(:one_time_password) { OneTimePassword::Generator.new.code }
+ let(:sent_one_time_password_at) { nil }
+ it { is_expected.not_to be_valid }
+ end
+
context "when correct code" do
let(:one_time_password) { OneTimePassword::Generator.new.code }
let(:sent_one_time_password_at) { Time.now }
diff --git a/spec/forms/form_spec.rb b/spec/forms/form_spec.rb
index 601a8fdd74..f564a7d126 100644
--- a/spec/forms/form_spec.rb
+++ b/spec/forms/form_spec.rb
@@ -139,12 +139,7 @@ def initialize(claim, journey_session)
end
describe "#persisted?" do
- before do
- allow(claim).to receive(:persisted?)
- form.persisted?
- end
-
- it { expect(claim).to have_received(:persisted?) }
+ it { expect(form.persisted?).to eq(true) }
end
describe "#update!" do
diff --git a/spec/forms/journeys/additional_payments_for_teaching/reminders/email_verification_form_spec.rb b/spec/forms/journeys/additional_payments_for_teaching/reminders/email_verification_form_spec.rb
new file mode 100644
index 0000000000..78bc8ce8d3
--- /dev/null
+++ b/spec/forms/journeys/additional_payments_for_teaching/reminders/email_verification_form_spec.rb
@@ -0,0 +1,44 @@
+require "rails_helper"
+
+RSpec.describe Journeys::AdditionalPaymentsForTeaching::Reminders::EmailVerificationForm do
+ subject(:form) { described_class.new(claim: form_data_object, journey:, journey_session:, params:) }
+
+ let(:journey) { Journeys::AdditionalPaymentsForTeaching }
+ let(:journey_session) { build(:journeys_session, journey: journey::ROUTING_NAME) }
+ let(:form_data_object) { Reminder.new }
+ let(:slug) { "email-verification" }
+ let(:params) { ActionController::Parameters.new({slug:, form: form_params}) }
+ let(:form_params) { {one_time_password: "123456"} }
+
+ it { is_expected.to be_a(EmailVerificationForm) }
+
+ describe ".model_name" do
+ it { expect(form.model_name).to eq(ActiveModel::Name.new(Form)) }
+ end
+
+ describe "#save" do
+ subject(:save) { form.save }
+
+ before do
+ allow(form).to receive(:update!).and_return(true)
+ end
+
+ context "valid params" do
+ let(:form_params) { {"one_time_password" => OneTimePassword::Generator.new.code, "sent_one_time_password_at" => Time.now} }
+
+ it "saves the attributes" do
+ expect(save).to eq(true)
+ expect(form).to have_received(:update!).with(email_verified: true)
+ end
+ end
+
+ context "invalid params" do
+ let(:form_params) { {"one_time_password" => OneTimePassword::Generator.new.code, "sent_one_time_password_at" => ""} }
+
+ it "does not save the attributes" do
+ expect(save).to eq(false)
+ expect(form).not_to have_received(:update!)
+ end
+ end
+ end
+end
diff --git a/spec/forms/journeys/additional_payments_for_teaching/reminders/personal_details_form_spec.rb b/spec/forms/journeys/additional_payments_for_teaching/reminders/personal_details_form_spec.rb
new file mode 100644
index 0000000000..57a14906cd
--- /dev/null
+++ b/spec/forms/journeys/additional_payments_for_teaching/reminders/personal_details_form_spec.rb
@@ -0,0 +1,55 @@
+require "rails_helper"
+
+RSpec.describe Journeys::AdditionalPaymentsForTeaching::Reminders::PersonalDetailsForm, type: :model do
+ subject(:form) { described_class.new(claim: form_data_object, journey:, journey_session:, params:) }
+
+ let(:journey) { Journeys::AdditionalPaymentsForTeaching }
+ let(:journey_session) { build(:journeys_session, journey: journey::ROUTING_NAME) }
+ let(:form_data_object) { Reminder.new }
+ let(:slug) { "personal-details" }
+ let(:params) { ActionController::Parameters.new({slug:, form: form_params}) }
+ let(:form_params) { {full_name: "John Doe"} }
+
+ it { is_expected.to be_a(Form) }
+
+ describe "validations" do
+ it { is_expected.to validate_presence_of(:full_name).with_message(form.i18n_errors_path(:"full_name.blank")) }
+ it { is_expected.to validate_length_of(:full_name).is_at_most(100).with_message(form.i18n_errors_path(:"full_name.length")) }
+
+ it { is_expected.to validate_presence_of(:email_address).with_message(form.i18n_errors_path(:"email_address.blank")) }
+ it { is_expected.to validate_length_of(:email_address).is_at_most(256).with_message(form.i18n_errors_path(:"email_address.length")) }
+
+ it { is_expected.to allow_value("valid@email.com").for(:email_address) }
+ it { is_expected.not_to allow_value("in valid@email.com").for(:email_address) }
+ end
+
+ describe ".model_name" do
+ it { expect(form.model_name).to eq(ActiveModel::Name.new(Form)) }
+ end
+
+ describe "#save" do
+ subject(:save) { form.save }
+
+ before do
+ allow(form).to receive(:update!).and_return(true)
+ end
+
+ context "valid params" do
+ let(:form_params) { {"full_name" => "John Doe", "email_address" => "john.doe@email.com"} }
+
+ it "saves the attributes" do
+ expect(save).to eq(true)
+ expect(form).to have_received(:update!).with(form_params)
+ end
+ end
+
+ context "invalid params" do
+ let(:form_params) { {"full_name" => "John Doe", "email_address" => ""} }
+
+ it "does not save the attributes" do
+ expect(save).to eq(false)
+ expect(form).not_to have_received(:update!)
+ end
+ end
+ end
+end
diff --git a/spec/models/reminder_spec.rb b/spec/models/reminder_spec.rb
index 5ac5563744..b89e1314c0 100644
--- a/spec/models/reminder_spec.rb
+++ b/spec/models/reminder_spec.rb
@@ -1,36 +1,6 @@
require "rails_helper"
RSpec.describe Reminder, type: :model do
- context "that has a email address" do
- it "validates that the value is in the correct format" do
- expect(build(:reminder, email_address: "notan email@address.com")).not_to be_valid
- expect(build(:reminder, email_address: "david.tau.2020.gb@example.com")).to be_valid
- expect(build(:reminder, email_address: "name@example")).not_to be_valid
- end
-
- it "checks that the email address in not longer than 256 characters" do
- expect(build(:reminder, email_address: "#{"e" * 256}@example.com")).not_to be_valid
- end
- end
-
- context "that has a full name" do
- it "validates the length of name is 100 characters or less" do
- expect(build(:reminder, full_name: "Name " * 50)).not_to be_valid
- expect(build(:reminder, full_name: "John")).to be_valid
- end
- end
-
- context "when saving in the 'personal-details' validation context" do
- it "validates the presence of full_name" do
- expect(build(:reminder, full_name: nil)).not_to be_valid(:"personal-details")
- expect(build(:reminder, full_name: "Miss Sveta Bond-Areemev")).to be_valid(:"personal-details")
- end
-
- it "validates the presence of email_address" do
- expect(build(:reminder, email_address: nil)).not_to be_valid(:"personal-details")
- end
- end
-
describe ".to_be_sent" do
let(:count) { [*1..5].sample }
let(:email_sent_at) { nil }
diff --git a/spec/requests/form_submittable_spec.rb b/spec/requests/form_submittable_spec.rb
new file mode 100644
index 0000000000..6609ff5770
--- /dev/null
+++ b/spec/requests/form_submittable_spec.rb
@@ -0,0 +1,186 @@
+require "rails_helper"
+
+class TestDummyController < BasePublicController
+ include PartOfClaimJourney
+ include FormSubmittable
+
+ # Overriding template for current slug to bypass view search
+ def render_template_for_current_slug
+ render plain: "Rendered template for current slug: #{current_slug}"
+ end
+
+ skip_before_action :send_unstarted_claimants_to_the_start
+
+ def slugs
+ %w[first-slug second-slug]
+ end
+
+ def next_slug
+ slugs[current_slug_index + 1]
+ end
+
+ def current_slug
+ slugs[current_slug_index]
+ end
+
+ def current_slug_index
+ slugs.index(params[:slug]) || 0
+ end
+end
+
+class TestDummyForm < Form
+ def save
+ end
+end
+
+RSpec.describe FormSubmittable, type: :request do
+ before do
+ Rails.application.routes.draw do
+ scope path: ":journey", constraints: {journey: "additional-payments"} do
+ get "/claim", to: "test_dummy#new"
+ get "/:slug", as: :test_dummy, to: "test_dummy#show"
+ post "/:slug", to: "test_dummy#create", as: :test_dummies
+ patch "/:slug", to: "test_dummy#update"
+ end
+ end
+ end
+
+ after { Rails.application.reload_routes! }
+
+ before { create(:journey_configuration, :additional_payments) }
+
+ shared_context :define_filter do |filter_name|
+ before { define_filter(filter_name) }
+ after { remove_filter(filter_name) }
+
+ def define_filter(filter_name)
+ TestDummyController.class_eval do
+ define_method(filter_name) do
+ render plain: "Triggered: `#{filter_name}` filter"
+ end
+ end
+ end
+
+ def remove_filter(filter_name)
+ TestDummyController.class_eval do
+ remove_method(filter_name) if method_defined?(filter_name)
+ end
+ end
+ end
+
+ describe "GET #new" do
+ it "redirects to the first slug" do
+ get "/additional-payments/claim"
+ expect(response).to redirect_to("/additional-payments/first-slug")
+ end
+ end
+
+ describe "GET #show" do
+ context "when the `{current_slug}_before_show` filter is defined" do
+ include_context :define_filter, :first_slug_before_show
+
+ it "executes the filter" do
+ get "/additional-payments/first-slug"
+ expect(response.body).to include("Triggered: `first_slug_before_show` filter")
+ end
+ end
+
+ context "when the `{current_slug}_before_show` filter is not defined" do
+ it "renders the template for the current slug" do
+ get "/additional-payments/first-slug"
+ expect(response.body).to include("Rendered template for current slug: first-slug")
+ end
+ end
+ end
+
+ shared_examples :form_submission do
+ def submit(slug)
+ send(method, slug, params: {})
+ end
+
+ context "when a form object is not present for the current slug" do
+ context "when the `{current_slug}_before_update` filter is not defined" do
+ it "redirects to the next slug" do
+ submit "/additional-payments/first-slug"
+ expect(response).to redirect_to("/additional-payments/second-slug")
+ end
+ end
+
+ context "when the `{current_slug}_before_update` filter is defined" do
+ include_context :define_filter, :first_slug_before_update
+
+ it "executes the filter" do
+ submit "/additional-payments/first-slug"
+ expect(response.body).to include("Triggered: `first_slug_before_update` filter")
+ end
+ end
+ end
+
+ context "when a form object is present for the current slug" do
+ before do
+ stub_const("Journeys::AdditionalPaymentsForTeaching::FORMS",
+ {"test_dummy" => {"first-slug" => TestDummyForm, "second-slug" => TestDummyForm}})
+ end
+
+ context "when the form save succeeds" do
+ before do
+ allow_any_instance_of(TestDummyForm).to receive(:save).and_return(true)
+ end
+
+ context "when the `{current_slug}_after_form_save_success` filter is defined" do
+ include_context :define_filter, :first_slug_after_form_save_success
+
+ it "executes the filter" do
+ submit "/additional-payments/first-slug"
+ expect(response.body).to include("Triggered: `first_slug_after_form_save_success` filter")
+ end
+ end
+
+ context "when the `{current_slug}_after_form_save_success` filter is not defined" do
+ it "redirects to the next slug" do
+ submit "/additional-payments/first-slug"
+ expect(response).to redirect_to("/additional-payments/second-slug")
+ end
+ end
+
+ context "when it's the end of the sequence" do
+ it { expect { submit "/additional-payments/second-slug" }.to raise_error(NoMethodError, /End of sequence/) }
+ end
+ end
+
+ context "when the form save fails" do
+ before do
+ allow_any_instance_of(TestDummyForm).to receive(:save).and_return(false)
+ end
+
+ context "when the `{current_slug}_after_form_save_failure` filter is defined" do
+ include_context :define_filter, :first_slug_after_form_save_failure
+
+ it "executes the filter" do
+ submit "/additional-payments/first-slug"
+ expect(response.body).to include("Triggered: `first_slug_after_form_save_failure` filter")
+ end
+ end
+
+ context "when the `{current_slug}_after_form_save_failure` filter is not defined" do
+ it "renders to template for the current slug" do
+ submit "/additional-payments/second-slug"
+ expect(response.body).to include("Rendered template for current slug: second-slug")
+ end
+ end
+ end
+ end
+ end
+
+ describe "POST #create" do
+ let(:method) { :post }
+
+ it_behaves_like :form_submission
+ end
+
+ describe "PATCH #update" do
+ let(:method) { :patch }
+
+ it_behaves_like :form_submission
+ end
+end
diff --git a/spec/requests/reminders_spec.rb b/spec/requests/reminders_spec.rb
index 11f2114ed7..60977342a4 100644
--- a/spec/requests/reminders_spec.rb
+++ b/spec/requests/reminders_spec.rb
@@ -4,10 +4,14 @@
before { create(:journey_configuration, :additional_payments) }
describe "#create" do
+ before do
+ allow_any_instance_of(BasePublicController).to receive(:current_claim).and_return(current_claim)
+ end
+ let(:current_claim) { create(:claim, policy: Policies::LevellingUpPremiumPayments) }
let(:submit_form) { post reminders_path("additional-payments", params: form_params) }
context "with full name and valid email address" do
- let(:form_params) { {reminder: {full_name: "Joe Bloggs", email_address: "joe.bloggs@example.com"}} }
+ let(:form_params) { {form: {full_name: "Joe Bloggs", email_address: "joe.bloggs@example.com"}} }
it "redirects to /email-verfication slug" do
submit_form
@@ -16,7 +20,7 @@
end
context "with empty form" do
- let(:form_params) { {reminder: {full_name: "", email_address: ""}} }
+ let(:form_params) { {form: {full_name: "", email_address: ""}} }
before { submit_form }
@@ -30,7 +34,7 @@
end
context "invalid email address" do
- let(:form_params) { {reminder: {full_name: "Joe Bloggs", email_address: "joe.bloggs.example.com"}} }
+ let(:form_params) { {form: {full_name: "Joe Bloggs", email_address: "joe.bloggs.example.com"}} }
it "renders errors containing invalid email address" do
submit_form
@@ -39,7 +43,7 @@
end
context "Notify returns an error about email address is required" do
- let(:form_params) { {reminder: {full_name: "Joe Bloggs", email_address: "joe.bloggs@example.com"}} }
+ let(:form_params) { {form: {full_name: "Joe Bloggs", email_address: "joe.bloggs@example.com"}} }
let(:mailer) { double("notify") }
let(:notifications_error_response) { double("response", code: 400, body: "ValidationError: email_address is a required property") }
@@ -56,7 +60,7 @@
end
context "Notify returns an error about team only API key" do
- let(:form_params) { {reminder: {full_name: "Joe Bloggs", email_address: "joe.bloggs@example.com"}} }
+ let(:form_params) { {form: {full_name: "Joe Bloggs", email_address: "joe.bloggs@example.com"}} }
let(:mailer) { double("notify") }
let(:notifications_error_response) { double("response", code: 400, body: "BadRequestError: Can’t send to this recipient using a team-only API key") }
@@ -73,7 +77,7 @@
end
context "Notify returns an unknown error" do
- let(:form_params) { {reminder: {full_name: "Joe Bloggs", email_address: "joe.bloggs@example.com"}} }
+ let(:form_params) { {form: {full_name: "Joe Bloggs", email_address: "joe.bloggs@example.com"}} }
let(:mailer) { double("notify") }
let(:notifications_error_response) { double("response", code: 400, body: "Something unexpected") }