-
Notifications
You must be signed in to change notification settings - Fork 166
LG-449 - Cancelling account deletion should notify both factors #2320
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,8 +7,10 @@ class DeleteAccountController < ApplicationController | |
| def show; end | ||
|
|
||
| def delete | ||
| analytics.track_event(Analytics::ACCOUNT_RESET, event: :delete, token_valid: true) | ||
| email = reset_session_and_set_email | ||
| user = @account_reset_request.user | ||
| analytics.track_event(Analytics::ACCOUNT_RESET, | ||
|
||
| event: :delete, token_valid: true, user_id: user.uuid) | ||
| email = reset_session_and_set_email(user) | ||
| UserMailer.account_reset_complete(email).deliver_later | ||
| redirect_to account_reset_confirm_delete_account_url | ||
| end | ||
|
|
@@ -19,8 +21,7 @@ def check_feature_enabled | |
| redirect_to root_url unless FeatureManagement.account_reset_enabled? | ||
| end | ||
|
|
||
| def reset_session_and_set_email | ||
| user = @account_reset_request.user | ||
| def reset_session_and_set_email(user) | ||
| email = user.email | ||
| user.destroy! | ||
| sign_out | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| class SmsAccountResetCancellationNotifierJob < ApplicationJob | ||
| queue_as :sms | ||
|
|
||
| def perform(phone:) | ||
| TwilioService::Utils.new.send_sms( | ||
| to: phone, | ||
| body: I18n.t( | ||
| 'jobs.sms_account_reset_cancel_job.message', | ||
| app: APP_NAME | ||
| ) | ||
| ) | ||
| end | ||
| end | ||
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| p.lead == t('.intro', app: link_to(APP_NAME, Figaro.env.mailer_domain_name, class: 'gray')) | ||
|
|
||
| table.spacer | ||
| tbody | ||
| tr | ||
| td.s10 height="10px" | ||
| | | ||
| table.hr | ||
| tr | ||
| th | ||
| | | ||
|
|
||
| p == t('.help', | ||
| app: link_to(APP_NAME, Figaro.env.mailer_domain_name, class: 'gray'), | ||
| help_link: link_to(t('user_mailer.help_link_text'), MarketingSite.help_url), | ||
| contact_link: link_to(t('user_mailer.contact_link_text'), MarketingSite.contact_url)) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,19 @@ | ||
| require 'rails_helper' | ||
|
|
||
| describe AccountReset::CancelController do | ||
| let(:user) { create(:user, :signed_up, phone: '+1 (703) 555-0000') } | ||
| before do | ||
| TwilioService::Utils.telephony_service = FakeSms | ||
| end | ||
|
|
||
| describe '#cancel' do | ||
| it 'logs a good token to the analytics' do | ||
| user = create(:user) | ||
| AccountResetService.new(user).create_request | ||
|
|
||
| stub_analytics | ||
| expect(@analytics).to receive(:track_event). | ||
| with(Analytics::ACCOUNT_RESET, event: :cancel, token_valid: true) | ||
| with(Analytics::ACCOUNT_RESET, | ||
| event: :cancel, token_valid: true, user_id: user.uuid) | ||
|
|
||
| post :cancel, params: { token: AccountResetRequest.all[0].request_token } | ||
| end | ||
|
|
@@ -25,5 +30,37 @@ | |
| post :cancel | ||
| expect(response).to redirect_to root_url | ||
| end | ||
|
|
||
| it 'sends an SMS if there is a phone' do | ||
| AccountResetService.new(user).create_request | ||
| allow(SmsAccountResetCancellationNotifierJob).to receive(:perform_now) | ||
|
|
||
| post :cancel, params: { token: AccountResetRequest.all[0].request_token } | ||
|
|
||
| expect(SmsAccountResetCancellationNotifierJob).to have_received(:perform_now).with( | ||
| phone: user.phone | ||
| ) | ||
| end | ||
|
|
||
| it 'does not send an SMS if there is no phone' do | ||
| AccountResetService.new(user).create_request | ||
| allow(SmsAccountResetCancellationNotifierJob).to receive(:perform_now) | ||
| user.phone = nil | ||
| user.save! | ||
|
|
||
| post :cancel, params: { token: AccountResetRequest.all[0].request_token } | ||
|
|
||
| expect(SmsAccountResetCancellationNotifierJob).to_not have_received(:perform_now) | ||
| end | ||
|
|
||
| it 'sends an email' do | ||
| AccountResetService.new(user).create_request | ||
|
|
||
| @mailer = instance_double(ActionMailer::MessageDelivery, deliver_later: true) | ||
| allow(UserMailer).to receive(:account_reset_cancel).with(user.email). | ||
| and_return(@mailer) | ||
|
|
||
| post :cancel, params: { token: AccountResetRequest.all[0].request_token } | ||
| end | ||
|
||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| require 'rails_helper' | ||
|
|
||
| describe SmsAccountResetCancellationNotifierJob do | ||
| include Features::ActiveJobHelper | ||
|
|
||
| describe '.perform' do | ||
| before do | ||
| reset_job_queues | ||
| TwilioService::Utils.telephony_service = FakeSms | ||
| FakeSms.messages = [] | ||
| end | ||
|
|
||
| subject(:perform) do | ||
| SmsAccountResetCancellationNotifierJob.perform_now( | ||
| phone: '+1 (888) 555-5555' | ||
| ) | ||
| end | ||
|
|
||
| it 'sends a message to the mobile number', twilio: true do | ||
| allow(Figaro.env).to receive(:twilio_messaging_service_sid).and_return('fake_sid') | ||
|
|
||
| TwilioService::Utils.telephony_service = FakeSms | ||
|
|
||
| perform | ||
|
|
||
| messages = FakeSms.messages | ||
|
|
||
| expect(messages.size).to eq(1) | ||
|
|
||
| msg = messages.first | ||
|
|
||
| expect(msg.messaging_service_sid).to eq('fake_sid') | ||
| expect(msg.to).to eq('+1 (888) 555-5555') | ||
| expect(msg.body). | ||
| to eq(I18n.t('jobs.sms_account_reset_cancel_job.message', app: APP_NAME)) | ||
| end | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we please follow the convention where the Service/Form that the controller interacts with returns a FormResponse object? That way, you only need to make one call to analytics (as opposed to one for success, and one for failure), and it makes things consistent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In other words, the User UUID would be captured as
extra_analytics_attributesinside the Service Object. Here's an example: https://github.com/18F/identity-idp/blob/master/app/services/email_confirmation_token_validator.rbThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As would the event name and token_valid attributes. The only parameter that would go in the analytics call is
result.to_h, like we do everywhere else.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I agree with that convention, the complexity of this method does not warrant an additional object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand. The complexity of which method? What I'm proposing is instead of a generic AccountResetService class that handles all aspects of this feature, it would be broken up into smaller classes that each take care of one aspect. Here, it would be the cancellation, so you would have a
CancelAccountResetclass with asubmitorcallmethod.Also, services should not have
Servicein their name. That's redundant. The class should describe what it is doing. If you find yourself having to addServiceto the end of a class name, that's usually a code smell indicating the class doesn't have a clearly defined purpose.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another test I like to do is to try to describe what the class does in one sentence. If the sentence contains the word
and, that's usually a sign it's doing too much. For example, the currentAccountResetServicecould be described as "it handles account reset requests, and account reset cancellations, and reports fraud, and sends notifications." That's a lot of stuff for one class!There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's not to say that the one public method the class has can only do one thing. The class can make calls to other services, as long as those other calls are to private methods, and usually those methods would then call other classes to perform those actions. For example:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You misunderstood. I love breaking up the service object. In fact I used that same methodology on controllers to breakup the rails monolith and everything funneling through routes. What I was referring to was an additional response object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The complexity of the method should not determine whether or not an additional response object is necessary. It's about consistency and expectations. We expect all analytics events to have at a minimum
successanderrorsattributes so we can easily differentiate between successful and unsuccessful events. This makes queries consistent for people searching in Kibana, or for the analytics team to run queries and monitor events. If I wanted to see what percentage of cancellations had an invalid token, I would normally look forproperties.event_properties.success, but that doesn't exist in this case. So then, I would have to dig into all the properties and figure out which key to use to determine success, which in this case is a custom key calledtoken_valid. This results in a poor developer experience.