|
4 | 4 | let!(:org) { create(:casa_org) } |
5 | 5 | let!(:user) { create(:user, phone_number: "+12222222222", casa_org: org) } |
6 | 6 |
|
7 | | - let!(:twillio_service_double) { instance_double(TwilioService) } |
| 7 | + let!(:twilio_service_double) { instance_double(TwilioService) } |
8 | 8 | let!(:short_url_service_double) { instance_double(ShortUrlService) } |
9 | 9 |
|
10 | 10 | before do |
11 | 11 | allow(TwilioService).to( |
12 | 12 | receive(:new).with( |
13 | 13 | org |
14 | | - ).and_return(twillio_service_double) |
| 14 | + ).and_return(twilio_service_double) |
15 | 15 | ) |
16 | 16 |
|
17 | | - allow(twillio_service_double).to receive(:send_sms) |
| 17 | + allow(twilio_service_double).to receive(:send_sms) |
18 | 18 |
|
19 | 19 | allow(ShortUrlService).to receive(:new).and_return(short_url_service_double) |
20 | 20 |
|
|
37 | 37 |
|
38 | 38 | it "sends a password reset SMS to existing user" do |
39 | 39 | request |
40 | | - expect(twillio_service_double).to have_received(:send_sms).once.with( |
| 40 | + expect(twilio_service_double).to have_received(:send_sms).once.with( |
41 | 41 | {From: org.twilio_phone_number, Body: a_string_matching("reset_url"), To: user.phone_number} |
42 | 42 | ) |
43 | 43 | end |
44 | 44 |
|
45 | 45 | it "sends a password reset email to existing user" do |
46 | | - expect_any_instance_of(User).to receive(:send_reset_password_instructions).once |
47 | | - request |
| 46 | + expect { request }.to change { ActionMailer::Base.deliveries.count }.by(1) |
| 47 | + expect(ActionMailer::Base.deliveries.last.to).to include(user.email) |
48 | 48 | end |
49 | 49 |
|
50 | 50 | it { is_expected.to redirect_to(user_session_url) } |
|
60 | 60 | let(:params) { {user: {email: user.email, phone_number: ""}} } |
61 | 61 |
|
62 | 62 | it "sends a password reset email to existing user" do |
63 | | - expect_any_instance_of(User).to receive(:send_reset_password_instructions).once |
64 | | - request |
| 63 | + expect { request }.to change { ActionMailer::Base.deliveries.count }.by(1) |
| 64 | + expect(ActionMailer::Base.deliveries.last.to).to include(user.email) |
65 | 65 | end |
66 | 66 |
|
67 | 67 | it "does not send sms with reset password" do |
68 | 68 | request |
69 | | - expect(twillio_service_double).not_to have_received(:send_sms) |
| 69 | + expect(twilio_service_double).not_to have_received(:send_sms) |
70 | 70 | end |
71 | 71 | end |
72 | 72 |
|
|
75 | 75 |
|
76 | 76 | it "sends a password reset SMS to existing user" do |
77 | 77 | request |
78 | | - expect(twillio_service_double).to have_received(:send_sms).once.with( |
| 78 | + expect(twilio_service_double).to have_received(:send_sms).once.with( |
79 | 79 | {From: org.twilio_phone_number, Body: a_string_matching("reset_url"), To: user.phone_number} |
80 | 80 | ) |
81 | 81 | end |
82 | 82 |
|
83 | 83 | it "does not send email with reset password" do |
84 | | - expect_any_instance_of(User).not_to receive(:send_reset_password_instructions) |
85 | | - request |
| 84 | + expect { request }.not_to change { ActionMailer::Base.deliveries.count } |
86 | 85 | end |
87 | 86 | end |
88 | 87 | end |
|
96 | 95 | end |
97 | 96 | end |
98 | 97 |
|
99 | | - context "with wrong parameters" do |
| 98 | + context "with wrong parameters (non-existent user)" do |
100 | 99 | let(:params) { {user: {phone_number: "13333333333"}} } |
101 | 100 |
|
102 | | - it "sets errors correctly" do |
| 101 | + it "does not reveal if user exists (security)" do |
103 | 102 | request |
104 | 103 | expect(flash[:notice]).to( |
105 | 104 | eq("If the account exists you will receive an email or SMS with instructions on how to reset your password in a few minutes.") |
106 | 105 | ) |
107 | 106 | end |
108 | 107 | end |
109 | 108 |
|
| 109 | + context "with invalid phone number format" do |
| 110 | + let(:params) { {user: {email: "", phone_number: "1234"}} } |
| 111 | + |
| 112 | + it "shows phone number validation error" do |
| 113 | + request |
| 114 | + expect(request.parsed_body.to_html).to include("phone_number") |
| 115 | + end |
| 116 | + end |
| 117 | + |
| 118 | + context "with non-numeric phone number" do |
| 119 | + let(:params) { {user: {email: "", phone_number: "abc"}} } |
| 120 | + |
| 121 | + it "shows phone number validation error" do |
| 122 | + request |
| 123 | + expect(request.parsed_body.to_html).to include("phone_number") |
| 124 | + end |
| 125 | + end |
| 126 | + |
110 | 127 | context "when twilio is disabled" do |
111 | 128 | let(:params) { {user: {email: user.email, phone_number: user.phone_number}} } |
112 | 129 |
|
|
115 | 132 | end |
116 | 133 |
|
117 | 134 | it "does not send an sms, only an email" do |
118 | | - expect_any_instance_of(User).to receive(:send_reset_password_instructions).once |
| 135 | + expect { request }.to change { ActionMailer::Base.deliveries.count }.by(1) |
| 136 | + expect(flash[:notice]).to( |
| 137 | + eq("If the account exists you will receive an email or SMS with instructions on how to reset your password in a few minutes.") |
| 138 | + ) |
| 139 | + end |
| 140 | + end |
| 141 | + |
| 142 | + context "when email sending times out with Net::ReadTimeout" do |
| 143 | + let(:params) { {user: {email: user.email, phone_number: user.phone_number}} } |
| 144 | + |
| 145 | + before do |
| 146 | + allow(user).to receive(:send_reset_password_instructions).and_raise(Net::ReadTimeout) |
| 147 | + allow(User).to receive(:find_by).and_return(user) |
| 148 | + end |
| 149 | + |
| 150 | + it "handles the timeout gracefully and still shows success message" do |
| 151 | + expect(Rails.logger).to receive(:error).with(/Password reset email failed to send/) |
119 | 152 | request |
120 | 153 | expect(flash[:notice]).to( |
121 | 154 | eq("If the account exists you will receive an email or SMS with instructions on how to reset your password in a few minutes.") |
122 | 155 | ) |
123 | 156 | end |
| 157 | + |
| 158 | + it "does not crash the request" do |
| 159 | + expect { request }.not_to raise_error |
| 160 | + expect(response).to redirect_to(user_session_url) |
| 161 | + end |
| 162 | + |
| 163 | + it "notifies Bugsnag of the error" do |
| 164 | + expect(Bugsnag).to receive(:notify).with(instance_of(Net::ReadTimeout)) |
| 165 | + request |
| 166 | + end |
| 167 | + |
| 168 | + it "generates a fallback token for SMS to use" do |
| 169 | + expect(user).to receive(:generate_password_reset_token).and_call_original |
| 170 | + request |
| 171 | + end |
| 172 | + |
| 173 | + it "still sends SMS with the fallback token" do |
| 174 | + request |
| 175 | + expect(twilio_service_double).to have_received(:send_sms).once |
| 176 | + end |
124 | 177 | end |
125 | 178 |
|
126 | | - context "when email sending times out" do |
| 179 | + context "when email sending times out with Net::OpenTimeout" do |
127 | 180 | let(:params) { {user: {email: user.email, phone_number: ""}} } |
128 | 181 |
|
129 | 182 | before do |
130 | | - allow_any_instance_of(User).to receive(:send_reset_password_instructions).and_raise(Net::ReadTimeout) |
| 183 | + allow(user).to receive(:send_reset_password_instructions).and_raise(Net::OpenTimeout) |
| 184 | + allow(User).to receive(:find_by).and_return(user) |
131 | 185 | end |
132 | 186 |
|
133 | 187 | it "handles the timeout gracefully and still shows success message" do |
|
142 | 196 | expect { request }.not_to raise_error |
143 | 197 | expect(response).to redirect_to(user_session_url) |
144 | 198 | end |
| 199 | + |
| 200 | + it "notifies Bugsnag of the error" do |
| 201 | + expect(Bugsnag).to receive(:notify).with(instance_of(Net::OpenTimeout)) |
| 202 | + request |
| 203 | + end |
| 204 | + end |
| 205 | + |
| 206 | + context "when SMS sending fails" do |
| 207 | + let(:params) { {user: {email: "", phone_number: user.phone_number}} } |
| 208 | + |
| 209 | + before do |
| 210 | + allow(twilio_service_double).to receive(:send_sms).and_raise(Twilio::REST::TwilioError.new("Service unavailable")) |
| 211 | + end |
| 212 | + |
| 213 | + it "raises the error (no rescue in controller)" do |
| 214 | + expect { request }.to raise_error(Twilio::REST::TwilioError) |
| 215 | + end |
145 | 216 | end |
146 | 217 | end |
147 | 218 |
|
|
162 | 233 | } |
163 | 234 | end |
164 | 235 |
|
165 | | - subject(:submit_reset) { put user_password_path, params: params } |
| 236 | + subject(:request) { put user_password_path, params: params } |
166 | 237 |
|
167 | | - it "successfully resets the password" do |
168 | | - submit_reset |
169 | | - expect(response).to redirect_to(new_user_session_path) |
170 | | - expect(flash[:notice]).to eq("Your password has been changed successfully.") |
| 238 | + context "with valid token and password" do |
| 239 | + it "successfully resets the password" do |
| 240 | + request |
| 241 | + expect(response).to redirect_to(new_user_session_path) |
| 242 | + expect(flash[:notice]).to eq("Your password has been changed successfully.") |
| 243 | + end |
| 244 | + |
| 245 | + it "allows user to sign in with new password" do |
| 246 | + request |
| 247 | + user.reload |
| 248 | + expect(user.valid_password?("newpassword123!")).to be true |
| 249 | + end |
| 250 | + end |
| 251 | + |
| 252 | + context "with password mismatch" do |
| 253 | + let(:params) do |
| 254 | + { |
| 255 | + user: { |
| 256 | + reset_password_token: token, |
| 257 | + password: "newpassword123!", |
| 258 | + password_confirmation: "differentpassword123!" |
| 259 | + } |
| 260 | + } |
| 261 | + end |
| 262 | + |
| 263 | + it "does not reset the password" do |
| 264 | + old_password_digest = user.encrypted_password |
| 265 | + request |
| 266 | + user.reload |
| 267 | + expect(user.encrypted_password).to eq(old_password_digest) |
| 268 | + end |
| 269 | + end |
| 270 | + |
| 271 | + context "with password too short" do |
| 272 | + let(:params) do |
| 273 | + { |
| 274 | + user: { |
| 275 | + reset_password_token: token, |
| 276 | + password: "abc", |
| 277 | + password_confirmation: "abc" |
| 278 | + } |
| 279 | + } |
| 280 | + end |
| 281 | + |
| 282 | + it "does not reset the password" do |
| 283 | + old_password_digest = user.encrypted_password |
| 284 | + request |
| 285 | + user.reload |
| 286 | + expect(user.encrypted_password).to eq(old_password_digest) |
| 287 | + end |
171 | 288 | end |
172 | 289 | end |
173 | 290 | end |
0 commit comments