propel_authentication 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +290 -0
- data/Rakefile +12 -0
- data/lib/generators/propel_auth/install_generator.rb +486 -0
- data/lib/generators/propel_auth/pack_generator.rb +277 -0
- data/lib/generators/propel_auth/templates/agency.rb +7 -0
- data/lib/generators/propel_auth/templates/agent.rb +7 -0
- data/lib/generators/propel_auth/templates/auth/base_passwords_controller.rb.tt +99 -0
- data/lib/generators/propel_auth/templates/auth/base_tokens_controller.rb.tt +90 -0
- data/lib/generators/propel_auth/templates/auth/passwords_controller.rb.tt +126 -0
- data/lib/generators/propel_auth/templates/auth_mailer.rb +180 -0
- data/lib/generators/propel_auth/templates/authenticatable.rb +38 -0
- data/lib/generators/propel_auth/templates/concerns/confirmable.rb +145 -0
- data/lib/generators/propel_auth/templates/concerns/lockable.rb +123 -0
- data/lib/generators/propel_auth/templates/concerns/propel_authentication.rb +44 -0
- data/lib/generators/propel_auth/templates/concerns/rack_session_disable.rb +19 -0
- data/lib/generators/propel_auth/templates/concerns/recoverable.rb +124 -0
- data/lib/generators/propel_auth/templates/config/environments/development_email.rb +43 -0
- data/lib/generators/propel_auth/templates/db/migrate/create_agencies.rb +20 -0
- data/lib/generators/propel_auth/templates/db/migrate/create_agents.rb +11 -0
- data/lib/generators/propel_auth/templates/db/migrate/create_invitations.rb +28 -0
- data/lib/generators/propel_auth/templates/db/migrate/create_organizations.rb +18 -0
- data/lib/generators/propel_auth/templates/db/migrate/create_users.rb +43 -0
- data/lib/generators/propel_auth/templates/db/seeds.rb +29 -0
- data/lib/generators/propel_auth/templates/invitation.rb +133 -0
- data/lib/generators/propel_auth/templates/lib/propel_auth.rb +84 -0
- data/lib/generators/propel_auth/templates/organization.rb +7 -0
- data/lib/generators/propel_auth/templates/propel_auth.rb +132 -0
- data/lib/generators/propel_auth/templates/services/auth_notification_service.rb +89 -0
- data/lib/generators/propel_auth/templates/test/concerns/confirmable_test.rb.tt +247 -0
- data/lib/generators/propel_auth/templates/test/concerns/lockable_test.rb.tt +282 -0
- data/lib/generators/propel_auth/templates/test/concerns/propel_authentication_test.rb.tt +75 -0
- data/lib/generators/propel_auth/templates/test/concerns/recoverable_test.rb.tt +327 -0
- data/lib/generators/propel_auth/templates/test/controllers/auth/lockable_integration_test.rb.tt +196 -0
- data/lib/generators/propel_auth/templates/test/controllers/auth/password_reset_integration_test.rb.tt +471 -0
- data/lib/generators/propel_auth/templates/test/controllers/auth/tokens_controller_test.rb.tt +265 -0
- data/lib/generators/propel_auth/templates/test/mailers/auth_mailer_test.rb.tt +216 -0
- data/lib/generators/propel_auth/templates/test/mailers/previews/auth_mailer_preview.rb +161 -0
- data/lib/generators/propel_auth/templates/tokens_controller.rb.tt +96 -0
- data/lib/generators/propel_auth/templates/user.rb +21 -0
- data/lib/generators/propel_auth/templates/user_test.rb.tt +81 -0
- data/lib/generators/propel_auth/templates/views/auth_mailer/account_unlock.html.erb +213 -0
- data/lib/generators/propel_auth/templates/views/auth_mailer/account_unlock.text.erb +56 -0
- data/lib/generators/propel_auth/templates/views/auth_mailer/email_confirmation.html.erb +213 -0
- data/lib/generators/propel_auth/templates/views/auth_mailer/email_confirmation.text.erb +32 -0
- data/lib/generators/propel_auth/templates/views/auth_mailer/password_reset.html.erb +166 -0
- data/lib/generators/propel_auth/templates/views/auth_mailer/password_reset.text.erb +32 -0
- data/lib/generators/propel_auth/templates/views/auth_mailer/user_invitation.html.erb +194 -0
- data/lib/generators/propel_auth/templates/views/auth_mailer/user_invitation.text.erb +51 -0
- data/lib/generators/propel_auth/test/dummy/Dockerfile +72 -0
- data/lib/generators/propel_auth/test/dummy/Gemfile +63 -0
- data/lib/generators/propel_auth/test/dummy/Gemfile.lock +394 -0
- data/lib/generators/propel_auth/test/dummy/README.md +24 -0
- data/lib/generators/propel_auth/test/dummy/Rakefile +6 -0
- data/lib/generators/propel_auth/test/dummy/app/assets/stylesheets/application.css +10 -0
- data/lib/generators/propel_auth/test/dummy/app/controllers/application_controller.rb +4 -0
- data/lib/generators/propel_auth/test/dummy/app/helpers/application_helper.rb +2 -0
- data/lib/generators/propel_auth/test/dummy/app/jobs/application_job.rb +7 -0
- data/lib/generators/propel_auth/test/dummy/app/mailers/application_mailer.rb +4 -0
- data/lib/generators/propel_auth/test/dummy/app/models/application_record.rb +3 -0
- data/lib/generators/propel_auth/test/dummy/app/views/layouts/application.html.erb +27 -0
- data/lib/generators/propel_auth/test/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/lib/generators/propel_auth/test/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/lib/generators/propel_auth/test/dummy/app/views/pwa/manifest.json.erb +22 -0
- data/lib/generators/propel_auth/test/dummy/app/views/pwa/service-worker.js +26 -0
- data/lib/generators/propel_auth/test/dummy/bin/brakeman +7 -0
- data/lib/generators/propel_auth/test/dummy/bin/dev +2 -0
- data/lib/generators/propel_auth/test/dummy/bin/docker-entrypoint +14 -0
- data/lib/generators/propel_auth/test/dummy/bin/rails +4 -0
- data/lib/generators/propel_auth/test/dummy/bin/rake +4 -0
- data/lib/generators/propel_auth/test/dummy/bin/rubocop +8 -0
- data/lib/generators/propel_auth/test/dummy/bin/setup +34 -0
- data/lib/generators/propel_auth/test/dummy/bin/thrust +5 -0
- data/lib/generators/propel_auth/test/dummy/config/application.rb +42 -0
- data/lib/generators/propel_auth/test/dummy/config/boot.rb +4 -0
- data/lib/generators/propel_auth/test/dummy/config/cable.yml +10 -0
- data/lib/generators/propel_auth/test/dummy/config/credentials.yml.enc +1 -0
- data/lib/generators/propel_auth/test/dummy/config/database.yml +41 -0
- data/lib/generators/propel_auth/test/dummy/config/environment.rb +5 -0
- data/lib/generators/propel_auth/test/dummy/config/environments/development.rb +72 -0
- data/lib/generators/propel_auth/test/dummy/config/environments/production.rb +89 -0
- data/lib/generators/propel_auth/test/dummy/config/environments/test.rb +53 -0
- data/lib/generators/propel_auth/test/dummy/config/initializers/assets.rb +10 -0
- data/lib/generators/propel_auth/test/dummy/config/initializers/content_security_policy.rb +25 -0
- data/lib/generators/propel_auth/test/dummy/config/initializers/filter_parameter_logging.rb +8 -0
- data/lib/generators/propel_auth/test/dummy/config/initializers/inflections.rb +16 -0
- data/lib/generators/propel_auth/test/dummy/config/locales/en.yml +31 -0
- data/lib/generators/propel_auth/test/dummy/config/master.key +1 -0
- data/lib/generators/propel_auth/test/dummy/config/puma.rb +41 -0
- data/lib/generators/propel_auth/test/dummy/config/routes.rb +2 -0
- data/lib/generators/propel_auth/test/dummy/config/storage.yml +34 -0
- data/lib/generators/propel_auth/test/dummy/config.ru +6 -0
- data/lib/generators/propel_auth/test/dummy/db/schema.rb +14 -0
- data/lib/generators/propel_auth/test/generators/authentication/controllers/tokens_controller_test.rb +230 -0
- data/lib/generators/propel_auth/test/generators/authentication/install_generator_test.rb +490 -0
- data/lib/generators/propel_auth/test/generators/authentication/uninstall_generator_test.rb +408 -0
- data/lib/generators/propel_auth/test/integration/generator_integration_test.rb +158 -0
- data/lib/generators/propel_auth/test/integration/multi_version_generator_test.rb +125 -0
- data/lib/generators/propel_auth/unpack_generator.rb +345 -0
- data/lib/propel_auth.rb +3 -0
- metadata +195 -0
@@ -0,0 +1,471 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch::IntegrationTest
|
4
|
+
def setup
|
5
|
+
@organization = Organization.create!(name: "Test Organization")
|
6
|
+
@user = User.create!(
|
7
|
+
email_address: "password-reset@example.com",
|
8
|
+
username: "passwordresetuser",
|
9
|
+
password: "originalpassword123",
|
10
|
+
organization: @organization,
|
11
|
+
first_name: "Password",
|
12
|
+
last_name: "User"
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
def teardown
|
17
|
+
User.destroy_all
|
18
|
+
Organization.destroy_all
|
19
|
+
end
|
20
|
+
|
21
|
+
# POST <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/reset - Request Password Reset Tests
|
22
|
+
test "should initiate password reset with valid email_address" do
|
23
|
+
# BEHAVIOR: Verify password reset request works with valid email_address
|
24
|
+
|
25
|
+
post <%= reset_path_helper %>, params: { email_address: @user.email_address }
|
26
|
+
|
27
|
+
# VERIFY API RESPONSE: Should return success status
|
28
|
+
assert_response :ok, "Valid email_address should initiate password reset"
|
29
|
+
|
30
|
+
response_json = JSON.parse(response.body)
|
31
|
+
assert_match(/reset.*sent/i, response_json['message'], "Should confirm reset email sent")
|
32
|
+
assert_not response_json.key?('token'), "Should not expose reset token in response"
|
33
|
+
|
34
|
+
# VERIFY EMAIL DELIVERY: Email should be sent (will test this when we add mailer)
|
35
|
+
# For now, verify the request was processed successfully
|
36
|
+
end
|
37
|
+
|
38
|
+
test "should reject password reset with invalid email_address format" do
|
39
|
+
# VALIDATION: Verify email_address format validation
|
40
|
+
|
41
|
+
post <%= reset_path_helper %>, params: { email_address: "invalid-email-format" }
|
42
|
+
|
43
|
+
# VERIFY VALIDATION ERROR: Should return validation error
|
44
|
+
assert_response :unprocessable_entity, "Invalid email_address format should be rejected"
|
45
|
+
|
46
|
+
response_json = JSON.parse(response.body)
|
47
|
+
assert_match(/email.*invalid/i, response_json['error'], "Should indicate email_address format error")
|
48
|
+
end
|
49
|
+
|
50
|
+
test "should handle password reset for nonexistent email_address safely" do
|
51
|
+
# SECURITY: Verify nonexistent email_addresses don't reveal user information
|
52
|
+
|
53
|
+
post <%= reset_path_helper %>, params: { email_address: "nonexistent@example.com" }
|
54
|
+
|
55
|
+
# VERIFY SECURITY RESPONSE: Should return same response as valid email_address (prevent enumeration)
|
56
|
+
assert_response :ok, "Nonexistent email_address should return same response as valid email_address"
|
57
|
+
|
58
|
+
response_json = JSON.parse(response.body)
|
59
|
+
assert_match(/reset.*sent/i, response_json['message'], "Should return same success message")
|
60
|
+
|
61
|
+
# VERIFY NO EMAIL SENT: No actual email should be sent for nonexistent user
|
62
|
+
end
|
63
|
+
|
64
|
+
test "should require email_address parameter for password reset request" do
|
65
|
+
# VALIDATION: Verify email_address parameter is required
|
66
|
+
|
67
|
+
post <%= reset_path_helper %>, params: {}
|
68
|
+
|
69
|
+
# VERIFY PARAMETER VALIDATION: Should return parameter error
|
70
|
+
assert_response :unprocessable_entity, "Missing email_address should return validation error"
|
71
|
+
|
72
|
+
response_json = JSON.parse(response.body)
|
73
|
+
assert_match(/email.*required/i, response_json['error'], "Should indicate email_address is required")
|
74
|
+
end
|
75
|
+
|
76
|
+
test "should rate limit password reset requests" do
|
77
|
+
# SECURITY: Verify rate limiting prevents abuse
|
78
|
+
|
79
|
+
# EXECUTE MULTIPLE REQUESTS: Send multiple reset requests rapidly
|
80
|
+
5.times do
|
81
|
+
post <%= reset_path_helper %>, params: { email_address: @user.email_address }
|
82
|
+
end
|
83
|
+
|
84
|
+
# Make one more request that should be rate limited
|
85
|
+
post <%= reset_path_helper %>, params: { email_address: @user.email_address }
|
86
|
+
|
87
|
+
# VERIFY RATE LIMITING: Should eventually rate limit (implementation dependent)
|
88
|
+
# For now, verify the endpoint is accessible (rate limiting will be added later)
|
89
|
+
assert_response :ok, "Rate limiting test - endpoint should be accessible"
|
90
|
+
end
|
91
|
+
|
92
|
+
# PUT <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/reset - Confirm Password Reset Tests
|
93
|
+
test "should reset password with valid token and new password" do
|
94
|
+
# BEHAVIOR: Verify password reset confirmation works end-to-end
|
95
|
+
|
96
|
+
# SETUP: Generate valid reset token
|
97
|
+
reset_token = @user.generate_password_reset_token
|
98
|
+
new_password = "newpassword456"
|
99
|
+
|
100
|
+
# EXECUTE RESET: Confirm password reset with token
|
101
|
+
put <%= reset_path_helper %>, params: {
|
102
|
+
token: reset_token,
|
103
|
+
password: new_password,
|
104
|
+
password_confirmation: new_password
|
105
|
+
}
|
106
|
+
|
107
|
+
# VERIFY API RESPONSE: Should return success
|
108
|
+
assert_response :ok, "Valid token should allow password reset"
|
109
|
+
|
110
|
+
response_json = JSON.parse(response.body)
|
111
|
+
assert_match(/password.*reset.*successfully/i, response_json['message'], "Should confirm password reset")
|
112
|
+
assert response_json.key?('user'), "Should return user data"
|
113
|
+
assert_equal @user.id, response_json['user']['id'], "Should return correct user"
|
114
|
+
|
115
|
+
# VERIFY PASSWORD CHANGE: User should authenticate with new password
|
116
|
+
@user.reload
|
117
|
+
assert @user.authenticate(new_password), "Should authenticate with new password"
|
118
|
+
assert_not @user.authenticate("originalpassword123"), "Should not authenticate with old password"
|
119
|
+
end
|
120
|
+
|
121
|
+
test "should reject password reset with invalid token" do
|
122
|
+
# SECURITY: Verify invalid tokens are rejected
|
123
|
+
|
124
|
+
invalid_token = "invalid.jwt.token"
|
125
|
+
new_password = "attemptedpassword"
|
126
|
+
|
127
|
+
# EXECUTE WITH INVALID TOKEN: Try to reset with invalid token
|
128
|
+
put <%= reset_path_helper %>, params: {
|
129
|
+
token: invalid_token,
|
130
|
+
password: new_password,
|
131
|
+
password_confirmation: new_password
|
132
|
+
}
|
133
|
+
|
134
|
+
# VERIFY REJECTION: Should return unauthorized
|
135
|
+
assert_response :unauthorized, "Invalid token should be rejected"
|
136
|
+
|
137
|
+
response_json = JSON.parse(response.body)
|
138
|
+
assert_match(/invalid.*expired.*token/i, response_json['error'], "Should indicate token error")
|
139
|
+
|
140
|
+
# VERIFY NO CHANGE: Password should remain unchanged
|
141
|
+
@user.reload
|
142
|
+
assert @user.authenticate("originalpassword123"), "Password should remain unchanged"
|
143
|
+
end
|
144
|
+
|
145
|
+
test "should reject password reset with expired token" do
|
146
|
+
# SECURITY: Verify expired tokens are rejected
|
147
|
+
|
148
|
+
# CREATE EXPIRED TOKEN: Generate token that's already expired
|
149
|
+
expired_payload = {
|
150
|
+
user_id: @user.id,
|
151
|
+
email_address: @user.email_address,
|
152
|
+
type: 'password_reset',
|
153
|
+
iat: Time.now.to_i,
|
154
|
+
exp: 1.second.ago.to_i, # Already expired
|
155
|
+
password_hash: @user.password_digest[0..10]
|
156
|
+
}
|
157
|
+
expired_token = JWT.encode(expired_payload, PropelAuth.configuration.jwt_secret, 'HS256')
|
158
|
+
|
159
|
+
# EXECUTE WITH EXPIRED TOKEN: Try to reset with expired token
|
160
|
+
put <%= reset_path_helper %>, params: {
|
161
|
+
token: expired_token,
|
162
|
+
password: "newpassword",
|
163
|
+
password_confirmation: "newpassword"
|
164
|
+
}
|
165
|
+
|
166
|
+
# VERIFY EXPIRATION: Should return unauthorized
|
167
|
+
assert_response :unauthorized, "Expired token should be rejected"
|
168
|
+
|
169
|
+
response_json = JSON.parse(response.body)
|
170
|
+
assert_match(/invalid.*expired.*token/i, response_json['error'], "Should indicate token expiration")
|
171
|
+
end
|
172
|
+
|
173
|
+
test "should validate password confirmation matches" do
|
174
|
+
# VALIDATION: Verify password confirmation is enforced
|
175
|
+
|
176
|
+
reset_token = @user.generate_password_reset_token
|
177
|
+
|
178
|
+
# EXECUTE WITH MISMATCHED PASSWORDS: Password and confirmation don't match
|
179
|
+
put <%= reset_path_helper %>, params: {
|
180
|
+
token: reset_token,
|
181
|
+
password: "newpassword123",
|
182
|
+
password_confirmation: "differentpassword456"
|
183
|
+
}
|
184
|
+
|
185
|
+
# VERIFY VALIDATION: Should return validation error
|
186
|
+
assert_response :unprocessable_entity, "Mismatched passwords should be rejected"
|
187
|
+
|
188
|
+
response_json = JSON.parse(response.body)
|
189
|
+
assert_match(/password.*confirmation.*match/i, response_json['error'], "Should indicate password mismatch")
|
190
|
+
|
191
|
+
# VERIFY NO CHANGE: Password should remain unchanged
|
192
|
+
@user.reload
|
193
|
+
assert @user.authenticate("originalpassword123"), "Password should remain unchanged"
|
194
|
+
end
|
195
|
+
|
196
|
+
test "should enforce password requirements during reset" do
|
197
|
+
# VALIDATION: Verify password requirements are enforced
|
198
|
+
|
199
|
+
reset_token = @user.generate_password_reset_token
|
200
|
+
weak_password = "123" # Too short
|
201
|
+
|
202
|
+
# EXECUTE WITH WEAK PASSWORD: Try to set weak password
|
203
|
+
put <%= reset_path_helper %>, params: {
|
204
|
+
token: reset_token,
|
205
|
+
password: weak_password,
|
206
|
+
password_confirmation: weak_password
|
207
|
+
}
|
208
|
+
|
209
|
+
# VERIFY VALIDATION: Should return validation error
|
210
|
+
assert_response :unprocessable_entity, "Weak password should be rejected"
|
211
|
+
|
212
|
+
response_json = JSON.parse(response.body)
|
213
|
+
assert_match(/password.*too short/i, response_json['error'], "Should indicate password strength issue")
|
214
|
+
|
215
|
+
# VERIFY NO CHANGE: Password should remain unchanged
|
216
|
+
@user.reload
|
217
|
+
assert @user.authenticate("originalpassword123"), "Password should remain unchanged"
|
218
|
+
end
|
219
|
+
|
220
|
+
test "should require all parameters for password reset confirmation" do
|
221
|
+
# VALIDATION: Verify all required parameters are present
|
222
|
+
|
223
|
+
reset_token = @user.generate_password_reset_token
|
224
|
+
|
225
|
+
# TEST MISSING TOKEN
|
226
|
+
put <%= reset_path_helper %>, params: {
|
227
|
+
password: "newpassword123",
|
228
|
+
password_confirmation: "newpassword123"
|
229
|
+
}
|
230
|
+
|
231
|
+
assert_response :unprocessable_entity, "Missing token should be rejected"
|
232
|
+
|
233
|
+
# TEST MISSING PASSWORD
|
234
|
+
put <%= reset_path_helper %>, params: {
|
235
|
+
token: reset_token,
|
236
|
+
password_confirmation: "newpassword123"
|
237
|
+
}
|
238
|
+
|
239
|
+
assert_response :unprocessable_entity, "Missing password should be rejected"
|
240
|
+
|
241
|
+
# TEST MISSING CONFIRMATION
|
242
|
+
put <%= reset_path_helper %>, params: {
|
243
|
+
token: reset_token,
|
244
|
+
password: "newpassword123"
|
245
|
+
}
|
246
|
+
|
247
|
+
assert_response :unprocessable_entity, "Missing password confirmation should be rejected"
|
248
|
+
end
|
249
|
+
|
250
|
+
test "should clear failed login attempts on successful password reset" do
|
251
|
+
# INTEGRATION: Verify password reset clears lockable state
|
252
|
+
|
253
|
+
# SETUP: Build up failed login attempts
|
254
|
+
@user.update!(failed_login_attempts: 8)
|
255
|
+
assert_equal 8, @user.failed_login_attempts, "Should have failed attempts"
|
256
|
+
|
257
|
+
# EXECUTE RESET: Reset password successfully
|
258
|
+
reset_token = @user.generate_password_reset_token
|
259
|
+
put <%= reset_path_helper %>, params: {
|
260
|
+
token: reset_token,
|
261
|
+
password: "newpassword123",
|
262
|
+
password_confirmation: "newpassword123"
|
263
|
+
}
|
264
|
+
|
265
|
+
# VERIFY SUCCESS: Should reset password
|
266
|
+
assert_response :ok, "Password reset should succeed"
|
267
|
+
|
268
|
+
# VERIFY FAILED ATTEMPTS CLEARED: Should clear lockable state
|
269
|
+
@user.reload
|
270
|
+
assert_equal 0, @user.failed_login_attempts, "Failed attempts should be cleared"
|
271
|
+
end
|
272
|
+
|
273
|
+
test "should not affect locked account status during password reset" do
|
274
|
+
# INTEGRATION: Verify password reset doesn't unlock account automatically
|
275
|
+
|
276
|
+
# SETUP: Lock the account
|
277
|
+
@user.lock_account!
|
278
|
+
assert @user.locked?, "Account should be locked"
|
279
|
+
|
280
|
+
# EXECUTE RESET: Reset password while locked
|
281
|
+
reset_token = @user.generate_password_reset_token
|
282
|
+
put <%= reset_path_helper %>, params: {
|
283
|
+
token: reset_token,
|
284
|
+
password: "newpassword123",
|
285
|
+
password_confirmation: "newpassword123"
|
286
|
+
}
|
287
|
+
|
288
|
+
# VERIFY PASSWORD RESET: Should succeed
|
289
|
+
assert_response :ok, "Password reset should succeed even for locked account"
|
290
|
+
|
291
|
+
# VERIFY LOCK STATUS: Account should remain locked (require explicit unlock)
|
292
|
+
@user.reload
|
293
|
+
assert @user.locked?, "Account should remain locked after password reset"
|
294
|
+
assert @user.authenticate("newpassword123"), "Should authenticate with new password"
|
295
|
+
end
|
296
|
+
|
297
|
+
# GET <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/reset/verify - Token Verification Tests
|
298
|
+
test "should verify valid password reset token" do
|
299
|
+
# BEHAVIOR: Verify token verification endpoint works
|
300
|
+
|
301
|
+
reset_token = @user.generate_password_reset_token
|
302
|
+
|
303
|
+
# EXECUTE VERIFICATION: Verify token is valid
|
304
|
+
get <%= reset_path_helper %>, params: { token: reset_token }
|
305
|
+
|
306
|
+
# VERIFY API RESPONSE: Should confirm token validity
|
307
|
+
assert_response :ok, "Valid token should pass verification"
|
308
|
+
|
309
|
+
response_json = JSON.parse(response.body)
|
310
|
+
assert response_json['valid'], "Should indicate token is valid"
|
311
|
+
assert response_json.key?('user'), "Should return user data for valid token"
|
312
|
+
assert_equal @user.email_address, response_json['user']['email_address'], "Should return correct user"
|
313
|
+
assert_not response_json['user'].key?('password_digest'), "Should not expose password"
|
314
|
+
end
|
315
|
+
|
316
|
+
test "should reject invalid token during verification" do
|
317
|
+
# SECURITY: Verify invalid tokens are rejected during verification
|
318
|
+
|
319
|
+
invalid_token = "invalid.jwt.token"
|
320
|
+
|
321
|
+
# EXECUTE VERIFICATION: Verify invalid token
|
322
|
+
get <%= reset_path_helper %>, params: { token: invalid_token }
|
323
|
+
|
324
|
+
# VERIFY REJECTION: Should indicate token is invalid
|
325
|
+
assert_response :unauthorized, "Invalid token should be rejected"
|
326
|
+
|
327
|
+
response_json = JSON.parse(response.body)
|
328
|
+
assert_not response_json['valid'], "Should indicate token is invalid"
|
329
|
+
assert_match(/invalid.*expired.*token/i, response_json['error'], "Should indicate token error")
|
330
|
+
end
|
331
|
+
|
332
|
+
test "should reject expired token during verification" do
|
333
|
+
# SECURITY: Verify expired tokens are rejected during verification
|
334
|
+
|
335
|
+
expired_payload = {
|
336
|
+
user_id: @user.id,
|
337
|
+
email_address: @user.email_address,
|
338
|
+
type: 'password_reset',
|
339
|
+
iat: Time.now.to_i,
|
340
|
+
exp: 1.second.ago.to_i, # Already expired
|
341
|
+
password_hash: @user.password_digest[0..10]
|
342
|
+
}
|
343
|
+
expired_token = JWT.encode(expired_payload, PropelAuth.configuration.jwt_secret, 'HS256')
|
344
|
+
|
345
|
+
# EXECUTE VERIFICATION: Verify expired token
|
346
|
+
get <%= reset_path_helper %>, params: { token: expired_token }
|
347
|
+
|
348
|
+
# VERIFY EXPIRATION: Should indicate token is expired
|
349
|
+
assert_response :unauthorized, "Expired token should be rejected"
|
350
|
+
|
351
|
+
response_json = JSON.parse(response.body)
|
352
|
+
assert_not response_json['valid'], "Should indicate token is invalid"
|
353
|
+
end
|
354
|
+
|
355
|
+
test "should require token parameter for verification" do
|
356
|
+
# VALIDATION: Verify token parameter is required for verification
|
357
|
+
|
358
|
+
get <%= reset_path_helper %>, params: {}
|
359
|
+
|
360
|
+
# VERIFY PARAMETER VALIDATION: Should return parameter error
|
361
|
+
assert_response :unprocessable_entity, "Missing token should return validation error"
|
362
|
+
|
363
|
+
response_json = JSON.parse(response.body)
|
364
|
+
assert_match(/token.*required/i, response_json['error'], "Should indicate token is required")
|
365
|
+
end
|
366
|
+
|
367
|
+
# Full Workflow Integration Tests
|
368
|
+
test "should complete full password reset workflow" do
|
369
|
+
# INTEGRATION: Test complete password reset workflow from start to finish
|
370
|
+
|
371
|
+
# STEP 1: REQUEST RESET - Send password reset request
|
372
|
+
post <%= reset_path_helper %>, params: { email_address: @user.email_address }
|
373
|
+
assert_response :ok, "Reset request should succeed"
|
374
|
+
|
375
|
+
# STEP 2: VERIFY TOKEN - Check that we can verify tokens (simulate email link click)
|
376
|
+
reset_token = @user.generate_password_reset_token # Simulate token from email
|
377
|
+
get <%= reset_path_helper %>, params: { token: reset_token }
|
378
|
+
assert_response :ok, "Token verification should succeed"
|
379
|
+
|
380
|
+
verification_json = JSON.parse(response.body)
|
381
|
+
assert verification_json['valid'], "Token should be valid"
|
382
|
+
|
383
|
+
# STEP 3: RESET PASSWORD - Complete password reset
|
384
|
+
new_password = "completeworkflow123"
|
385
|
+
put <%= reset_path_helper %>, params: {
|
386
|
+
token: reset_token,
|
387
|
+
password: new_password,
|
388
|
+
password_confirmation: new_password
|
389
|
+
}
|
390
|
+
assert_response :ok, "Password reset should succeed"
|
391
|
+
|
392
|
+
# STEP 4: VERIFY NEW PASSWORD - Test login with new password
|
393
|
+
post <%= login_path_helper %>, params: {
|
394
|
+
user: { email_address: @user.email_address, password: new_password }
|
395
|
+
}
|
396
|
+
assert_response :ok, "Should login with new password"
|
397
|
+
|
398
|
+
login_json = JSON.parse(response.body)
|
399
|
+
assert login_json['token'].present?, "Should receive JWT token"
|
400
|
+
|
401
|
+
# STEP 5: VERIFY OLD PASSWORD INVALID - Test old password doesn't work
|
402
|
+
post <%= login_path_helper %>, params: {
|
403
|
+
user: { email_address: @user.email_address, password: "originalpassword123" }
|
404
|
+
}
|
405
|
+
assert_response :unauthorized, "Should not login with old password"
|
406
|
+
end
|
407
|
+
|
408
|
+
test "should prevent token reuse after successful password reset" do
|
409
|
+
# SECURITY: Verify tokens can't be reused after successful reset
|
410
|
+
|
411
|
+
reset_token = @user.generate_password_reset_token
|
412
|
+
|
413
|
+
# FIRST RESET: Use token successfully
|
414
|
+
put <%= reset_path_helper %>, params: {
|
415
|
+
token: reset_token,
|
416
|
+
password: "firstnewpassword123",
|
417
|
+
password_confirmation: "firstnewpassword123"
|
418
|
+
}
|
419
|
+
assert_response :ok, "First password reset should succeed"
|
420
|
+
|
421
|
+
# ATTEMPTED REUSE: Try to use same token again
|
422
|
+
put <%= reset_path_helper %>, params: {
|
423
|
+
token: reset_token,
|
424
|
+
password: "secondnewpassword456",
|
425
|
+
password_confirmation: "secondnewpassword456"
|
426
|
+
}
|
427
|
+
|
428
|
+
# VERIFY PREVENTION: Second attempt should fail
|
429
|
+
assert_response :unauthorized, "Token reuse should be prevented"
|
430
|
+
|
431
|
+
# VERIFY PASSWORD UNCHANGED: Password should remain as first reset
|
432
|
+
@user.reload
|
433
|
+
assert @user.authenticate("firstnewpassword123"), "Password should remain from first reset"
|
434
|
+
assert_not @user.authenticate("secondnewpassword456"), "Second password should not be set"
|
435
|
+
end
|
436
|
+
|
437
|
+
test "should handle concurrent password reset attempts safely" do
|
438
|
+
# CONCURRENCY: Verify system handles concurrent reset attempts
|
439
|
+
|
440
|
+
# GENERATE MULTIPLE TOKENS: Simulate multiple reset requests
|
441
|
+
token1 = @user.generate_password_reset_token
|
442
|
+
sleep(0.1) # Small delay to ensure different timestamps
|
443
|
+
token2 = @user.generate_password_reset_token
|
444
|
+
|
445
|
+
# VERIFY BOTH VALID: Both tokens should be valid initially
|
446
|
+
assert @user.valid_password_reset_token?(token1), "First token should be valid"
|
447
|
+
assert @user.valid_password_reset_token?(token2), "Second token should be valid"
|
448
|
+
|
449
|
+
# USE FIRST TOKEN: Reset password with first token
|
450
|
+
put <%= reset_path_helper %>, params: {
|
451
|
+
token: token1,
|
452
|
+
password: "concurrentpassword1",
|
453
|
+
password_confirmation: "concurrentpassword1"
|
454
|
+
}
|
455
|
+
assert_response :ok, "First reset should succeed"
|
456
|
+
|
457
|
+
# TRY SECOND TOKEN: Attempt reset with second token (should fail due to password change)
|
458
|
+
put <%= reset_path_helper %>, params: {
|
459
|
+
token: token2,
|
460
|
+
password: "concurrentpassword2",
|
461
|
+
password_confirmation: "concurrentpassword2"
|
462
|
+
}
|
463
|
+
|
464
|
+
# VERIFY CONCURRENCY HANDLING: Second token should be invalid due to password change
|
465
|
+
assert_response :unauthorized, "Second token should be invalid after password change"
|
466
|
+
|
467
|
+
# VERIFY FINAL STATE: Password should be from first successful reset
|
468
|
+
@user.reload
|
469
|
+
assert @user.authenticate("concurrentpassword1"), "Password should be from first reset"
|
470
|
+
end
|
471
|
+
end
|