propel_authentication 0.1.3 → 0.2.0

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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +128 -0
  3. data/README.md +254 -116
  4. data/lib/generators/{propel_auth → propel_authentication}/install_generator.rb +152 -170
  5. data/lib/generators/propel_authentication/templates/application_mailer.rb +6 -0
  6. data/lib/generators/propel_authentication/templates/auth/passwords_controller.rb.tt +132 -0
  7. data/lib/generators/propel_authentication/templates/auth/signup_controller.rb.tt +242 -0
  8. data/lib/generators/{propel_auth/templates → propel_authentication/templates/auth}/tokens_controller.rb.tt +39 -22
  9. data/lib/generators/{propel_auth → propel_authentication}/templates/auth_mailer.rb +3 -1
  10. data/lib/generators/{propel_auth → propel_authentication}/templates/authenticatable.rb +10 -4
  11. data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/confirmable.rb +3 -3
  12. data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/lockable.rb +10 -8
  13. data/lib/generators/{propel_auth/templates/concerns/propel_authentication.rb → propel_authentication/templates/concerns/propel_authentication_concern.rb} +33 -3
  14. data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/recoverable.rb +21 -11
  15. data/lib/generators/propel_authentication/templates/core/configuration_methods.rb +191 -0
  16. data/lib/generators/propel_authentication/templates/db/seeds.rb +75 -0
  17. data/lib/generators/propel_authentication/templates/doc/signup_flow.md +315 -0
  18. data/lib/generators/propel_authentication/templates/models/agency.rb.tt +13 -0
  19. data/lib/generators/propel_authentication/templates/models/agent.rb.tt +13 -0
  20. data/lib/generators/{propel_auth/templates/invitation.rb → propel_authentication/templates/models/invitation.rb.tt} +8 -2
  21. data/lib/generators/propel_authentication/templates/models/organization.rb.tt +12 -0
  22. data/lib/generators/{propel_auth/templates/user.rb → propel_authentication/templates/models/user.rb.tt} +5 -0
  23. data/lib/generators/propel_authentication/templates/propel_authentication.rb.tt +218 -0
  24. data/lib/generators/propel_authentication/templates/routes/auth_routes.rb.tt +55 -0
  25. data/lib/generators/{propel_auth → propel_authentication}/templates/services/auth_notification_service.rb +3 -3
  26. data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/confirmable_test.rb.tt +34 -10
  27. data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/lockable_test.rb.tt +12 -12
  28. data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/propel_authentication_test.rb.tt +2 -2
  29. data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/recoverable_test.rb.tt +11 -11
  30. data/lib/generators/{propel_auth → propel_authentication}/templates/test/controllers/auth/lockable_integration_test.rb.tt +18 -15
  31. data/lib/generators/{propel_auth → propel_authentication}/templates/test/controllers/auth/password_reset_integration_test.rb.tt +38 -40
  32. data/lib/generators/propel_authentication/templates/test/controllers/auth/signup_controller_test.rb.tt +201 -0
  33. data/lib/generators/{propel_auth → propel_authentication}/templates/test/controllers/auth/tokens_controller_test.rb.tt +33 -25
  34. data/lib/generators/{propel_auth → propel_authentication}/templates/test/mailers/auth_mailer_test.rb.tt +51 -36
  35. data/lib/generators/{propel_auth → propel_authentication}/templates/user_test.rb.tt +1 -1
  36. data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/email_confirmation.html.erb +2 -2
  37. data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/email_confirmation.text.erb +1 -1
  38. data/lib/generators/{propel_auth → propel_authentication}/test/generators/authentication/install_generator_test.rb +4 -4
  39. data/lib/generators/{propel_auth → propel_authentication}/test/generators/authentication/uninstall_generator_test.rb +1 -1
  40. data/lib/generators/{propel_auth → propel_authentication}/test/integration/generator_integration_test.rb +1 -1
  41. data/lib/generators/{propel_auth → propel_authentication}/test/integration/multi_version_generator_test.rb +13 -12
  42. data/lib/generators/{propel_auth → propel_authentication}/unpack_generator.rb +55 -38
  43. data/lib/propel_authentication.rb +3 -0
  44. metadata +101 -98
  45. data/lib/generators/propel_auth/core/configuration_methods.rb +0 -134
  46. data/lib/generators/propel_auth/pack_generator.rb +0 -277
  47. data/lib/generators/propel_auth/templates/agency.rb +0 -7
  48. data/lib/generators/propel_auth/templates/agent.rb +0 -7
  49. data/lib/generators/propel_auth/templates/auth/base_passwords_controller.rb.tt +0 -99
  50. data/lib/generators/propel_auth/templates/auth/base_tokens_controller.rb.tt +0 -90
  51. data/lib/generators/propel_auth/templates/auth/passwords_controller.rb.tt +0 -126
  52. data/lib/generators/propel_auth/templates/db/seeds.rb +0 -29
  53. data/lib/generators/propel_auth/templates/organization.rb +0 -7
  54. data/lib/generators/propel_auth/templates/propel_auth.rb.tt +0 -141
  55. data/lib/propel_auth.rb +0 -3
  56. /data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/rack_session_disable.rb +0 -0
  57. /data/lib/generators/{propel_auth → propel_authentication}/templates/config/environments/development_email.rb +0 -0
  58. /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_agencies.rb +0 -0
  59. /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_agents.rb +0 -0
  60. /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_invitations.rb +0 -0
  61. /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_organizations.rb +0 -0
  62. /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_users.rb +0 -0
  63. /data/lib/generators/{propel_auth → propel_authentication}/templates/test/mailers/previews/auth_mailer_preview.rb +0 -0
  64. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/account_unlock.html.erb +0 -0
  65. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/account_unlock.text.erb +0 -0
  66. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/password_reset.html.erb +0 -0
  67. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/password_reset.text.erb +0 -0
  68. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/user_invitation.html.erb +0 -0
  69. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/user_invitation.text.erb +0 -0
  70. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/Dockerfile +0 -0
  71. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/Gemfile +0 -0
  72. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/README.md +0 -0
  73. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/Rakefile +0 -0
  74. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/assets/stylesheets/application.css +0 -0
  75. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/controllers/application_controller.rb +0 -0
  76. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/helpers/application_helper.rb +0 -0
  77. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/jobs/application_job.rb +0 -0
  78. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/mailers/application_mailer.rb +0 -0
  79. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/models/application_record.rb +0 -0
  80. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/layouts/application.html.erb +0 -0
  81. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/layouts/mailer.html.erb +0 -0
  82. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/layouts/mailer.text.erb +0 -0
  83. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/pwa/manifest.json.erb +0 -0
  84. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/pwa/service-worker.js +0 -0
  85. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/brakeman +0 -0
  86. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/dev +0 -0
  87. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/docker-entrypoint +0 -0
  88. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/rails +0 -0
  89. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/rake +0 -0
  90. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/rubocop +0 -0
  91. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/setup +0 -0
  92. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/thrust +0 -0
  93. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/application.rb +0 -0
  94. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/boot.rb +0 -0
  95. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/cable.yml +0 -0
  96. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/credentials.yml.enc +0 -0
  97. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/database.yml +0 -0
  98. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environment.rb +0 -0
  99. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environments/development.rb +0 -0
  100. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environments/production.rb +0 -0
  101. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environments/test.rb +0 -0
  102. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/assets.rb +0 -0
  103. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/content_security_policy.rb +0 -0
  104. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/filter_parameter_logging.rb +0 -0
  105. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/inflections.rb +0 -0
  106. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/locales/en.yml +0 -0
  107. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/master.key +0 -0
  108. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/puma.rb +0 -0
  109. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/routes.rb +0 -0
  110. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/storage.yml +0 -0
  111. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config.ru +0 -0
  112. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/db/schema.rb +0 -0
  113. /data/lib/generators/{propel_auth → propel_authentication}/test/generators/authentication/controllers/tokens_controller_test.rb +0 -0
@@ -1,14 +1,17 @@
1
1
  require 'test_helper'
2
2
 
3
- class <%= controller_namespace %>::LockableIntegrationTest < ActionDispatch::IntegrationTest
3
+ class <%= controller_namespace('tokens') %>LockableIntegrationTest < ActionDispatch::IntegrationTest
4
4
  def setup
5
5
  @organization = Organization.create!(name: "Test Organization")
6
+ @agency = Agency.create!(name: "Test Agency", organization: @organization)
6
7
  @user = User.create!(
7
8
  email_address: "lockable-integration@example.com",
8
9
  username: "lockableintegration",
9
10
  password: "correctpassword123",
10
11
  organization: @organization
11
12
  )
13
+ # Create agent association for agency access (required for real-time agency lookup)
14
+ Agent.create!(user: @user, agency: @agency, role: "member")
12
15
  end
13
16
 
14
17
  # CRITICAL: Real API integration with lockable functionality
@@ -16,7 +19,7 @@ class <%= controller_namespace %>::LockableIntegrationTest < ActionDispatch::Int
16
19
  test "should track failed attempts during actual login API calls" do
17
20
  # BEHAVIOR: Verify failed login attempts are tracked through real API
18
21
  9.times do |i|
19
- post <%= login_path_helper %>, params: {
22
+ post '<%= auth_route_prefix %>/login', params: {
20
23
  user: { email_address: @user.email_address, password: "wrongpassword" }
21
24
  }
22
25
 
@@ -32,14 +35,14 @@ class <%= controller_namespace %>::LockableIntegrationTest < ActionDispatch::Int
32
35
 
33
36
  # First 9 attempts should not lock
34
37
  9.times do
35
- post <%= login_path_helper %>, params: {
38
+ post '<%= auth_route_prefix %>/login', params: {
36
39
  user: { email_address: @user.email_address, password: "wrongpassword" }
37
40
  }
38
41
  assert_response :unauthorized
39
42
  end
40
43
 
41
44
  # 10th attempt should lock the account
42
- post <%= login_path_helper %>, params: {
45
+ post '<%= auth_route_prefix %>/login', params: {
43
46
  user: { email_address: @user.email_address, password: "wrongpassword" }
44
47
  }
45
48
 
@@ -52,14 +55,14 @@ class <%= controller_namespace %>::LockableIntegrationTest < ActionDispatch::Int
52
55
  # BEHAVIOR: Verify locked accounts return proper HTTP status and error
53
56
  @user.lock_account!
54
57
 
55
- post <%= login_path_helper %>, params: {
58
+ post '<%= auth_route_prefix %>/login', params: {
56
59
  user: { email_address: @user.email_address, password: "correctpassword123" }
57
60
  }
58
61
 
59
62
  assert_response :locked, "Should return 423 locked status"
60
63
 
61
64
  json = JSON.parse(response.body)
62
- assert_match(/locked.*too many failed attempts/i, json['error'], "Should return descriptive error message")
65
+ assert_match(/locked.*too many.*failed.*attempts/i, json['error'], "Should return descriptive error message")
63
66
  assert_nil json['token'], "Should not return authentication token"
64
67
  assert_nil json['user'], "Should not return user data"
65
68
  end
@@ -69,7 +72,7 @@ class <%= controller_namespace %>::LockableIntegrationTest < ActionDispatch::Int
69
72
  @user.lock_account!
70
73
 
71
74
  # Try with correct password
72
- post <%= login_path_helper %>, params: {
75
+ post '<%= auth_route_prefix %>/login', params: {
73
76
  user: { email_address: @user.email_address, password: "correctpassword123" }
74
77
  }
75
78
 
@@ -85,7 +88,7 @@ class <%= controller_namespace %>::LockableIntegrationTest < ActionDispatch::Int
85
88
 
86
89
  # Build up some failed attempts
87
90
  5.times do
88
- post <%= login_path_helper %>, params: {
91
+ post '<%= auth_route_prefix %>/login', params: {
89
92
  user: { email_address: @user.email_address, password: "wrongpassword" }
90
93
  }
91
94
  end
@@ -94,7 +97,7 @@ class <%= controller_namespace %>::LockableIntegrationTest < ActionDispatch::Int
94
97
  assert_equal 5, @user.failed_login_attempts, "Should have 5 failed attempts"
95
98
 
96
99
  # Successful login should reset counter
97
- post <%= login_path_helper %>, params: {
100
+ post '<%= auth_route_prefix %>/login', params: {
98
101
  user: { email_address: @user.email_address, password: "correctpassword123" }
99
102
  }
100
103
 
@@ -111,7 +114,7 @@ class <%= controller_namespace %>::LockableIntegrationTest < ActionDispatch::Int
111
114
  @user.lock_account!
112
115
  unlock_token = @user.generate_unlock_token
113
116
 
114
- post <%= unlock_path_helper %>, params: { token: unlock_token }
117
+ post '<%= auth_route_prefix %>/unlock', params: { token: unlock_token }
115
118
 
116
119
  assert_response :ok, "Valid unlock token should return 200"
117
120
 
@@ -128,7 +131,7 @@ class <%= controller_namespace %>::LockableIntegrationTest < ActionDispatch::Int
128
131
  # API INTEGRATION: Verify unlock endpoint properly validates tokens
129
132
  @user.lock_account!
130
133
 
131
- post <%= unlock_path_helper %>, params: { token: "invalid_token_12345" }
134
+ post '<%= auth_route_prefix %>/unlock', params: { token: "invalid_token_12345" }
132
135
 
133
136
  assert_response :unauthorized, "Invalid token should return 401"
134
137
 
@@ -143,7 +146,7 @@ class <%= controller_namespace %>::LockableIntegrationTest < ActionDispatch::Int
143
146
  # API VALIDATION: Verify unlock endpoint validates required parameters
144
147
  @user.lock_account!
145
148
 
146
- post <%= unlock_path_helper %>, params: {}
149
+ post '<%= auth_route_prefix %>/unlock', params: {}
147
150
 
148
151
  assert_response :unprocessable_entity, "Missing token should return 422"
149
152
 
@@ -153,7 +156,7 @@ class <%= controller_namespace %>::LockableIntegrationTest < ActionDispatch::Int
153
156
 
154
157
  test "should not increment attempts when user does not exist" do
155
158
  # SECURITY: Verify failed attempts are not tracked for nonexistent users
156
- post <%= login_path_helper %>, params: {
159
+ post '<%= auth_route_prefix %>/login', params: {
157
160
  user: { email_address: "nonexistent@example.com", password: "anypassword" }
158
161
  }
159
162
 
@@ -166,7 +169,7 @@ class <%= controller_namespace %>::LockableIntegrationTest < ActionDispatch::Int
166
169
  test "should handle graceful error responses for authentication failures" do
167
170
  # ERROR HANDLING: Verify system handles authentication failures gracefully
168
171
 
169
- post <%= login_path_helper %>, params: {
172
+ post '<%= auth_route_prefix %>/login', params: {
170
173
  user: { email_address: "nonexistent@example.com", password: "wrongpassword" }
171
174
  }
172
175
 
@@ -182,7 +185,7 @@ class <%= controller_namespace %>::LockableIntegrationTest < ActionDispatch::Int
182
185
  # BEHAVIOR: Verify login timestamp tracking
183
186
  start_time = Time.current
184
187
 
185
- post <%= login_path_helper %>, params: {
188
+ post '<%= auth_route_prefix %>/login', params: {
186
189
  user: { email_address: @user.email_address, password: "correctpassword123" }
187
190
  }
188
191
 
@@ -1,8 +1,9 @@
1
1
  require 'test_helper'
2
2
 
3
- class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch::IntegrationTest
3
+ class <%= controller_namespace('passwords') %>PasswordResetIntegrationTest < ActionDispatch::IntegrationTest
4
4
  def setup
5
5
  @organization = Organization.create!(name: "Test Organization")
6
+ @agency = Agency.create!(name: "Test Agency", organization: @organization)
6
7
  @user = User.create!(
7
8
  email_address: "password-reset@example.com",
8
9
  username: "passwordresetuser",
@@ -11,18 +12,15 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
11
12
  first_name: "Password",
12
13
  last_name: "User"
13
14
  )
15
+ # Create agent association for agency access (required for real-time agency lookup)
16
+ Agent.create!(user: @user, agency: @agency, role: "member")
14
17
  end
15
18
 
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
19
+ # POST <%= auth_route_prefix %>/reset - Request Password Reset Tests
22
20
  test "should initiate password reset with valid email_address" do
23
21
  # BEHAVIOR: Verify password reset request works with valid email_address
24
22
 
25
- post <%= reset_path_helper %>, params: { email_address: @user.email_address }
23
+ post '<%= auth_route_prefix %>/reset', params: { email_address: @user.email_address }
26
24
 
27
25
  # VERIFY API RESPONSE: Should return success status
28
26
  assert_response :ok, "Valid email_address should initiate password reset"
@@ -38,7 +36,7 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
38
36
  test "should reject password reset with invalid email_address format" do
39
37
  # VALIDATION: Verify email_address format validation
40
38
 
41
- post <%= reset_path_helper %>, params: { email_address: "invalid-email-format" }
39
+ post '<%= auth_route_prefix %>/reset', params: { email_address: "invalid-email-format" }
42
40
 
43
41
  # VERIFY VALIDATION ERROR: Should return validation error
44
42
  assert_response :unprocessable_entity, "Invalid email_address format should be rejected"
@@ -50,7 +48,7 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
50
48
  test "should handle password reset for nonexistent email_address safely" do
51
49
  # SECURITY: Verify nonexistent email_addresses don't reveal user information
52
50
 
53
- post <%= reset_path_helper %>, params: { email_address: "nonexistent@example.com" }
51
+ post '<%= auth_route_prefix %>/reset', params: { email_address: "nonexistent@example.com" }
54
52
 
55
53
  # VERIFY SECURITY RESPONSE: Should return same response as valid email_address (prevent enumeration)
56
54
  assert_response :ok, "Nonexistent email_address should return same response as valid email_address"
@@ -64,7 +62,7 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
64
62
  test "should require email_address parameter for password reset request" do
65
63
  # VALIDATION: Verify email_address parameter is required
66
64
 
67
- post <%= reset_path_helper %>, params: {}
65
+ post '<%= auth_route_prefix %>/reset', params: {}
68
66
 
69
67
  # VERIFY PARAMETER VALIDATION: Should return parameter error
70
68
  assert_response :unprocessable_entity, "Missing email_address should return validation error"
@@ -78,18 +76,18 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
78
76
 
79
77
  # EXECUTE MULTIPLE REQUESTS: Send multiple reset requests rapidly
80
78
  5.times do
81
- post <%= reset_path_helper %>, params: { email_address: @user.email_address }
79
+ post '<%= auth_route_prefix %>/reset', params: { email_address: @user.email_address }
82
80
  end
83
81
 
84
82
  # Make one more request that should be rate limited
85
- post <%= reset_path_helper %>, params: { email_address: @user.email_address }
83
+ post '<%= auth_route_prefix %>/reset', params: { email_address: @user.email_address }
86
84
 
87
85
  # VERIFY RATE LIMITING: Should eventually rate limit (implementation dependent)
88
86
  # For now, verify the endpoint is accessible (rate limiting will be added later)
89
87
  assert_response :ok, "Rate limiting test - endpoint should be accessible"
90
88
  end
91
89
 
92
- # PUT <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/reset - Confirm Password Reset Tests
90
+ # PUT <%= auth_route_prefix %>/reset - Confirm Password Reset Tests
93
91
  test "should reset password with valid token and new password" do
94
92
  # BEHAVIOR: Verify password reset confirmation works end-to-end
95
93
 
@@ -98,7 +96,7 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
98
96
  new_password = "newpassword456"
99
97
 
100
98
  # EXECUTE RESET: Confirm password reset with token
101
- put <%= reset_path_helper %>, params: {
99
+ patch '<%= auth_route_prefix %>/reset', params: {
102
100
  token: reset_token,
103
101
  password: new_password,
104
102
  password_confirmation: new_password
@@ -125,7 +123,7 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
125
123
  new_password = "attemptedpassword"
126
124
 
127
125
  # EXECUTE WITH INVALID TOKEN: Try to reset with invalid token
128
- put <%= reset_path_helper %>, params: {
126
+ patch '<%= auth_route_prefix %>/reset', params: {
129
127
  token: invalid_token,
130
128
  password: new_password,
131
129
  password_confirmation: new_password
@@ -154,10 +152,10 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
154
152
  exp: 1.second.ago.to_i, # Already expired
155
153
  password_hash: @user.password_digest[0..10]
156
154
  }
157
- expired_token = JWT.encode(expired_payload, PropelAuth.configuration.jwt_secret, 'HS256')
155
+ expired_token = JWT.encode(expired_payload, PropelAuthentication.configuration.jwt_secret, 'HS256')
158
156
 
159
157
  # EXECUTE WITH EXPIRED TOKEN: Try to reset with expired token
160
- put <%= reset_path_helper %>, params: {
158
+ patch '<%= auth_route_prefix %>/reset', params: {
161
159
  token: expired_token,
162
160
  password: "newpassword",
163
161
  password_confirmation: "newpassword"
@@ -176,7 +174,7 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
176
174
  reset_token = @user.generate_password_reset_token
177
175
 
178
176
  # EXECUTE WITH MISMATCHED PASSWORDS: Password and confirmation don't match
179
- put <%= reset_path_helper %>, params: {
177
+ patch '<%= auth_route_prefix %>/reset', params: {
180
178
  token: reset_token,
181
179
  password: "newpassword123",
182
180
  password_confirmation: "differentpassword456"
@@ -200,7 +198,7 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
200
198
  weak_password = "123" # Too short
201
199
 
202
200
  # EXECUTE WITH WEAK PASSWORD: Try to set weak password
203
- put <%= reset_path_helper %>, params: {
201
+ patch '<%= auth_route_prefix %>/reset', params: {
204
202
  token: reset_token,
205
203
  password: weak_password,
206
204
  password_confirmation: weak_password
@@ -223,7 +221,7 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
223
221
  reset_token = @user.generate_password_reset_token
224
222
 
225
223
  # TEST MISSING TOKEN
226
- put <%= reset_path_helper %>, params: {
224
+ patch '<%= auth_route_prefix %>/reset', params: {
227
225
  password: "newpassword123",
228
226
  password_confirmation: "newpassword123"
229
227
  }
@@ -231,7 +229,7 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
231
229
  assert_response :unprocessable_entity, "Missing token should be rejected"
232
230
 
233
231
  # TEST MISSING PASSWORD
234
- put <%= reset_path_helper %>, params: {
232
+ patch '<%= auth_route_prefix %>/reset', params: {
235
233
  token: reset_token,
236
234
  password_confirmation: "newpassword123"
237
235
  }
@@ -239,7 +237,7 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
239
237
  assert_response :unprocessable_entity, "Missing password should be rejected"
240
238
 
241
239
  # TEST MISSING CONFIRMATION
242
- put <%= reset_path_helper %>, params: {
240
+ patch '<%= auth_route_prefix %>/reset', params: {
243
241
  token: reset_token,
244
242
  password: "newpassword123"
245
243
  }
@@ -256,7 +254,7 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
256
254
 
257
255
  # EXECUTE RESET: Reset password successfully
258
256
  reset_token = @user.generate_password_reset_token
259
- put <%= reset_path_helper %>, params: {
257
+ patch '<%= auth_route_prefix %>/reset', params: {
260
258
  token: reset_token,
261
259
  password: "newpassword123",
262
260
  password_confirmation: "newpassword123"
@@ -279,7 +277,7 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
279
277
 
280
278
  # EXECUTE RESET: Reset password while locked
281
279
  reset_token = @user.generate_password_reset_token
282
- put <%= reset_path_helper %>, params: {
280
+ patch '<%= auth_route_prefix %>/reset', params: {
283
281
  token: reset_token,
284
282
  password: "newpassword123",
285
283
  password_confirmation: "newpassword123"
@@ -294,14 +292,14 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
294
292
  assert @user.authenticate("newpassword123"), "Should authenticate with new password"
295
293
  end
296
294
 
297
- # GET <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/reset/verify - Token Verification Tests
295
+ # GET <%= auth_route_prefix %>/reset/verify - Token Verification Tests
298
296
  test "should verify valid password reset token" do
299
297
  # BEHAVIOR: Verify token verification endpoint works
300
298
 
301
299
  reset_token = @user.generate_password_reset_token
302
300
 
303
301
  # EXECUTE VERIFICATION: Verify token is valid
304
- get <%= reset_path_helper %>, params: { token: reset_token }
302
+ get '<%= auth_route_prefix %>/reset', params: { token: reset_token }
305
303
 
306
304
  # VERIFY API RESPONSE: Should confirm token validity
307
305
  assert_response :ok, "Valid token should pass verification"
@@ -319,7 +317,7 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
319
317
  invalid_token = "invalid.jwt.token"
320
318
 
321
319
  # EXECUTE VERIFICATION: Verify invalid token
322
- get <%= reset_path_helper %>, params: { token: invalid_token }
320
+ get '<%= auth_route_prefix %>/reset', params: { token: invalid_token }
323
321
 
324
322
  # VERIFY REJECTION: Should indicate token is invalid
325
323
  assert_response :unauthorized, "Invalid token should be rejected"
@@ -340,10 +338,10 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
340
338
  exp: 1.second.ago.to_i, # Already expired
341
339
  password_hash: @user.password_digest[0..10]
342
340
  }
343
- expired_token = JWT.encode(expired_payload, PropelAuth.configuration.jwt_secret, 'HS256')
341
+ expired_token = JWT.encode(expired_payload, PropelAuthentication.configuration.jwt_secret, 'HS256')
344
342
 
345
343
  # EXECUTE VERIFICATION: Verify expired token
346
- get <%= reset_path_helper %>, params: { token: expired_token }
344
+ get '<%= auth_route_prefix %>/reset', params: { token: expired_token }
347
345
 
348
346
  # VERIFY EXPIRATION: Should indicate token is expired
349
347
  assert_response :unauthorized, "Expired token should be rejected"
@@ -355,7 +353,7 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
355
353
  test "should require token parameter for verification" do
356
354
  # VALIDATION: Verify token parameter is required for verification
357
355
 
358
- get <%= reset_path_helper %>, params: {}
356
+ get '<%= auth_route_prefix %>/reset', params: {}
359
357
 
360
358
  # VERIFY PARAMETER VALIDATION: Should return parameter error
361
359
  assert_response :unprocessable_entity, "Missing token should return validation error"
@@ -369,12 +367,12 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
369
367
  # INTEGRATION: Test complete password reset workflow from start to finish
370
368
 
371
369
  # STEP 1: REQUEST RESET - Send password reset request
372
- post <%= reset_path_helper %>, params: { email_address: @user.email_address }
370
+ post '<%= auth_route_prefix %>/reset', params: { email_address: @user.email_address }
373
371
  assert_response :ok, "Reset request should succeed"
374
372
 
375
373
  # STEP 2: VERIFY TOKEN - Check that we can verify tokens (simulate email link click)
376
374
  reset_token = @user.generate_password_reset_token # Simulate token from email
377
- get <%= reset_path_helper %>, params: { token: reset_token }
375
+ get '<%= auth_route_prefix %>/reset', params: { token: reset_token }
378
376
  assert_response :ok, "Token verification should succeed"
379
377
 
380
378
  verification_json = JSON.parse(response.body)
@@ -382,7 +380,7 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
382
380
 
383
381
  # STEP 3: RESET PASSWORD - Complete password reset
384
382
  new_password = "completeworkflow123"
385
- put <%= reset_path_helper %>, params: {
383
+ patch '<%= auth_route_prefix %>/reset', params: {
386
384
  token: reset_token,
387
385
  password: new_password,
388
386
  password_confirmation: new_password
@@ -390,7 +388,7 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
390
388
  assert_response :ok, "Password reset should succeed"
391
389
 
392
390
  # STEP 4: VERIFY NEW PASSWORD - Test login with new password
393
- post <%= login_path_helper %>, params: {
391
+ post '<%= auth_route_prefix %>/login', params: {
394
392
  user: { email_address: @user.email_address, password: new_password }
395
393
  }
396
394
  assert_response :ok, "Should login with new password"
@@ -399,7 +397,7 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
399
397
  assert login_json['token'].present?, "Should receive JWT token"
400
398
 
401
399
  # STEP 5: VERIFY OLD PASSWORD INVALID - Test old password doesn't work
402
- post <%= login_path_helper %>, params: {
400
+ post '<%= auth_route_prefix %>/login', params: {
403
401
  user: { email_address: @user.email_address, password: "originalpassword123" }
404
402
  }
405
403
  assert_response :unauthorized, "Should not login with old password"
@@ -411,7 +409,7 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
411
409
  reset_token = @user.generate_password_reset_token
412
410
 
413
411
  # FIRST RESET: Use token successfully
414
- put <%= reset_path_helper %>, params: {
412
+ patch '<%= auth_route_prefix %>/reset', params: {
415
413
  token: reset_token,
416
414
  password: "firstnewpassword123",
417
415
  password_confirmation: "firstnewpassword123"
@@ -419,7 +417,7 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
419
417
  assert_response :ok, "First password reset should succeed"
420
418
 
421
419
  # ATTEMPTED REUSE: Try to use same token again
422
- put <%= reset_path_helper %>, params: {
420
+ patch '<%= auth_route_prefix %>/reset', params: {
423
421
  token: reset_token,
424
422
  password: "secondnewpassword456",
425
423
  password_confirmation: "secondnewpassword456"
@@ -447,7 +445,7 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
447
445
  assert @user.valid_password_reset_token?(token2), "Second token should be valid"
448
446
 
449
447
  # USE FIRST TOKEN: Reset password with first token
450
- put <%= reset_path_helper %>, params: {
448
+ patch '<%= auth_route_prefix %>/reset', params: {
451
449
  token: token1,
452
450
  password: "concurrentpassword1",
453
451
  password_confirmation: "concurrentpassword1"
@@ -455,7 +453,7 @@ class <%= controller_namespace %>::PasswordResetIntegrationTest < ActionDispatch
455
453
  assert_response :ok, "First reset should succeed"
456
454
 
457
455
  # TRY SECOND TOKEN: Attempt reset with second token (should fail due to password change)
458
- put <%= reset_path_helper %>, params: {
456
+ patch '<%= auth_route_prefix %>/reset', params: {
459
457
  token: token2,
460
458
  password: "concurrentpassword2",
461
459
  password_confirmation: "concurrentpassword2"
@@ -0,0 +1,201 @@
1
+ require "test_helper"
2
+
3
+ class <%= controller_namespace('signup') %>SignupControllerTest < ActionDispatch::IntegrationTest
4
+
5
+ def setup
6
+ @valid_signup_params = {
7
+ user: {
8
+ email_address: "newuser@example.com",
9
+ username: "newuser",
10
+ password: "password123",
11
+ password_confirmation: "password123",
12
+ first_name: "New",
13
+ last_name: "User"
14
+ },
15
+ organization: {
16
+ name: "New Company Inc",
17
+ website: "https://newcompany.com",
18
+ time_zone: "UTC"
19
+ }
20
+ }
21
+
22
+ @valid_signup_params_with_agency = @valid_signup_params.merge(
23
+ agency: {
24
+ name: "Primary Agency",
25
+ description: "Main operations agency"
26
+ },
27
+ agent: {
28
+ role: "owner"
29
+ }
30
+ )
31
+ end
32
+
33
+ test "should create user and organization successfully" do
34
+ assert_difference ['User.count', 'Organization.count'], 1 do
35
+ post '<%= auth_route_prefix %>/signup',
36
+ params: @valid_signup_params_with_agency,
37
+ as: :json
38
+ end
39
+
40
+ assert_response :created
41
+ response_body = JSON.parse(response.body)
42
+
43
+ # Verify response structure
44
+ assert_includes response_body.keys, 'token'
45
+ assert_includes response_body.keys, 'user'
46
+ assert_includes response_body.keys, 'organization'
47
+ assert_includes response_body.keys, 'message'
48
+ assert_includes response_body.keys, 'next_steps'
49
+
50
+ # Verify user data
51
+ user_data = response_body['user']
52
+ assert_equal @valid_signup_params[:user][:email_address], user_data['email_address']
53
+ assert_equal @valid_signup_params[:user][:username], user_data['username']
54
+ assert_equal @valid_signup_params[:user][:first_name], user_data['first_name']
55
+
56
+ # Verify organization data
57
+ org_data = response_body['organization']
58
+ assert_equal @valid_signup_params[:organization][:name], org_data['name']
59
+ assert_equal @valid_signup_params[:organization][:website], org_data['website']
60
+
61
+ # Verify JWT token is valid
62
+ token = response_body['token']
63
+ assert_not_nil token
64
+
65
+ # Verify the token can be used for authenticated requests
66
+ get '<%= auth_route_prefix %>/me',
67
+ headers: { 'Authorization' => "Bearer #{token}" }
68
+ assert_response :success
69
+ end
70
+
71
+ test "should create user, organization, agency, and agent when agency tenancy enabled" do
72
+ skip "Agency tenancy test - enable when PropelApi.configuration.agency_tenancy = true" unless agency_tenancy_enabled?
73
+
74
+ assert_difference ['User.count', 'Organization.count', 'Agency.count', 'Agent.count'], 1 do
75
+ post '<%= auth_route_prefix %>/signup',
76
+ params: @valid_signup_params_with_agency,
77
+ as: :json
78
+ end
79
+
80
+ assert_response :created
81
+ response_body = JSON.parse(response.body)
82
+
83
+ # Verify user, agency, and agent were created
84
+ assert_includes response_body.keys, 'user'
85
+ assert_includes response_body.keys, 'agency'
86
+ assert_includes response_body.keys, 'agent'
87
+
88
+ user_data = response_body['user']
89
+ agency_data = response_body['agency']
90
+ assert_equal @valid_signup_params_with_agency[:agency][:name], agency_data['name']
91
+
92
+ agent_data = response_body['agent']
93
+ assert_equal @valid_signup_params_with_agency[:agent][:role], agent_data['role']
94
+
95
+ # Verify JWT contains minimal secure claims (agency access is now real-time lookup)
96
+ token = response_body['token']
97
+ payload = JWT.decode(token, PropelAuthentication.configuration.jwt_secret, true, { algorithm: 'HS256' })[0]
98
+ assert_includes payload.keys, 'user_id'
99
+ assert_includes payload.keys, 'organization_id'
100
+ assert_not_includes payload.keys, 'agency_ids', "agency_ids removed for security - now real-time lookup"
101
+
102
+ # Verify user has real-time agency access via agents association
103
+ created_user = User.find(user_data['id'])
104
+ assert_includes created_user.agency_ids, agency_data['id']
105
+ end
106
+
107
+ test "should reject signup with missing user data" do
108
+ invalid_params = @valid_signup_params.except(:user)
109
+
110
+ post '<%= auth_route_prefix %>/signup',
111
+ params: invalid_params,
112
+ as: :json
113
+
114
+ assert_response :unprocessable_entity
115
+ response_body = JSON.parse(response.body)
116
+ assert_includes response_body.keys, 'error'
117
+ end
118
+
119
+ test "should reject signup with missing organization data" do
120
+ invalid_params = @valid_signup_params.except(:organization)
121
+
122
+ post '<%= auth_route_prefix %>/signup',
123
+ params: invalid_params,
124
+ as: :json
125
+
126
+ assert_response :unprocessable_entity
127
+ response_body = JSON.parse(response.body)
128
+ assert_includes response_body.keys, 'error'
129
+ end
130
+
131
+ test "should reject signup with invalid email format" do
132
+ invalid_params = @valid_signup_params_with_agency.dup
133
+ invalid_params[:user][:email_address] = "invalid-email"
134
+
135
+ post '<%= auth_route_prefix %>/signup',
136
+ params: invalid_params,
137
+ as: :json
138
+
139
+ assert_response :unprocessable_entity
140
+ response_body = JSON.parse(response.body)
141
+ assert_includes response_body.keys, 'details'
142
+ assert_includes response_body['details'].keys, 'email_address'
143
+ end
144
+
145
+ test "should reject signup with password confirmation mismatch" do
146
+ invalid_params = @valid_signup_params_with_agency.dup
147
+ invalid_params[:user][:password_confirmation] = "different_password"
148
+
149
+ post '<%= auth_route_prefix %>/signup',
150
+ params: invalid_params,
151
+ as: :json
152
+
153
+ assert_response :unprocessable_entity
154
+ response_body = JSON.parse(response.body)
155
+ assert_includes response_body.keys, 'details'
156
+ end
157
+
158
+ test "should reject duplicate email address" do
159
+ # Create first user
160
+ post '<%= auth_route_prefix %>/signup',
161
+ params: @valid_signup_params_with_agency,
162
+ as: :json
163
+ assert_response :created
164
+
165
+ # Try to create second user with same email
166
+ duplicate_params = @valid_signup_params_with_agency.dup
167
+ duplicate_params[:user][:username] = "different_username"
168
+ duplicate_params[:organization][:name] = "Different Company"
169
+ duplicate_params[:agency][:name] = "Different Agency"
170
+
171
+ post '<%= auth_route_prefix %>/signup',
172
+ params: duplicate_params,
173
+ as: :json
174
+
175
+ assert_response :unprocessable_entity
176
+ response_body = JSON.parse(response.body)
177
+ assert_includes response_body.keys, 'details'
178
+ assert_includes response_body['details'].keys, 'email_address'
179
+ end
180
+
181
+ test "should require agency data when agency tenancy is enabled" do
182
+ skip "Agency tenancy test - enable when PropelApi.configuration.agency_tenancy = true" unless agency_tenancy_enabled?
183
+
184
+ # Try signup without agency data when agency tenancy is enabled
185
+ post '<%= auth_route_prefix %>/signup',
186
+ params: @valid_signup_params,
187
+ as: :json
188
+
189
+ assert_response :unprocessable_entity
190
+ response_body = JSON.parse(response.body)
191
+ assert_equal 'MISSING_AGENCY_DATA', response_body['code']
192
+ end
193
+
194
+ private
195
+
196
+ def agency_tenancy_enabled?
197
+ defined?(PropelApi) && PropelApi.configuration.agency_tenancy
198
+ rescue
199
+ false
200
+ end
201
+ end