propel_authentication 0.1.4 → 0.2.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -2
  3. data/README.md +6 -6
  4. data/lib/generators/propel_authentication/install_generator.rb +135 -153
  5. data/lib/generators/propel_authentication/templates/application_mailer.rb +6 -0
  6. data/lib/generators/propel_authentication/templates/auth/passwords_controller.rb.tt +84 -78
  7. data/lib/generators/propel_authentication/templates/auth/signup_controller.rb.tt +242 -0
  8. data/lib/generators/propel_authentication/templates/{tokens_controller.rb.tt → auth/tokens_controller.rb.tt} +39 -22
  9. data/lib/generators/propel_authentication/templates/auth_mailer.rb +3 -1
  10. data/lib/generators/propel_authentication/templates/authenticatable.rb +8 -2
  11. data/lib/generators/propel_authentication/templates/concerns/confirmable.rb +1 -1
  12. data/lib/generators/propel_authentication/templates/concerns/lockable.rb +4 -2
  13. data/lib/generators/propel_authentication/templates/concerns/{propel_authentication.rb → propel_authentication_concern.rb} +33 -3
  14. data/lib/generators/propel_authentication/templates/concerns/recoverable.rb +16 -6
  15. data/lib/generators/propel_authentication/templates/core/configuration_methods.rb +104 -64
  16. data/lib/generators/propel_authentication/templates/db/seeds.rb +50 -4
  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_authentication/templates/{invitation.rb → models/invitation.rb.tt} +6 -0
  21. data/lib/generators/propel_authentication/templates/models/organization.rb.tt +12 -0
  22. data/lib/generators/propel_authentication/templates/{user.rb → models/user.rb.tt} +5 -0
  23. data/lib/generators/propel_authentication/templates/propel_authentication.rb.tt +94 -9
  24. data/lib/generators/propel_authentication/templates/routes/auth_routes.rb.tt +55 -0
  25. data/lib/generators/propel_authentication/templates/services/auth_notification_service.rb +3 -3
  26. data/lib/generators/propel_authentication/templates/test/concerns/confirmable_test.rb.tt +34 -10
  27. data/lib/generators/propel_authentication/templates/test/concerns/propel_authentication_test.rb.tt +1 -1
  28. data/lib/generators/propel_authentication/templates/test/concerns/recoverable_test.rb.tt +4 -4
  29. data/lib/generators/propel_authentication/templates/test/controllers/auth/lockable_integration_test.rb.tt +18 -15
  30. data/lib/generators/propel_authentication/templates/test/controllers/auth/password_reset_integration_test.rb.tt +38 -40
  31. data/lib/generators/propel_authentication/templates/test/controllers/auth/signup_controller_test.rb.tt +201 -0
  32. data/lib/generators/propel_authentication/templates/test/controllers/auth/tokens_controller_test.rb.tt +33 -25
  33. data/lib/generators/propel_authentication/templates/test/mailers/auth_mailer_test.rb.tt +51 -36
  34. data/lib/generators/propel_authentication/templates/views/auth_mailer/email_confirmation.html.erb +2 -2
  35. data/lib/generators/propel_authentication/templates/views/auth_mailer/email_confirmation.text.erb +1 -1
  36. data/lib/generators/propel_authentication/test/generators/authentication/install_generator_test.rb +4 -4
  37. data/lib/generators/propel_authentication/test/generators/authentication/uninstall_generator_test.rb +1 -1
  38. data/lib/generators/propel_authentication/test/integration/generator_integration_test.rb +1 -1
  39. data/lib/generators/propel_authentication/test/integration/multi_version_generator_test.rb +13 -12
  40. data/lib/generators/propel_authentication/unpack_generator.rb +19 -15
  41. data/lib/propel_authentication.rb +1 -1
  42. metadata +14 -11
  43. data/lib/generators/propel_authentication/templates/agency.rb +0 -7
  44. data/lib/generators/propel_authentication/templates/agent.rb +0 -7
  45. data/lib/generators/propel_authentication/templates/auth/base_passwords_controller.rb.tt +0 -99
  46. data/lib/generators/propel_authentication/templates/auth/base_tokens_controller.rb.tt +0 -90
  47. data/lib/generators/propel_authentication/templates/organization.rb +0 -7
@@ -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
@@ -1,6 +1,6 @@
1
1
  require 'test_helper'
2
2
 
3
- class TokensControllerTest < ActionDispatch::IntegrationTest
3
+ class <%= controller_namespace('tokens') %>TokensControllerTest < ActionDispatch::IntegrationTest
4
4
  # BEHAVIOR-DRIVEN TESTING: NO MOCKS - Test Real Functionality
5
5
  # These tests verify actual JWT authentication works with real database operations
6
6
  # and HTTP requests to provide 100% confidence in production functionality
@@ -46,11 +46,19 @@ class TokensControllerTest < ActionDispatch::IntegrationTest
46
46
  last_name: "User",
47
47
  status: 1
48
48
  )
49
+
50
+ # Create agencies and agent associations (required for real-time agency lookup)
51
+ @agency = Agency.create!(name: "Main Agency", organization: @organization)
52
+ @rival_agency = Agency.create!(name: "Rival Agency", organization: @rival_organization)
53
+
54
+ Agent.create!(user: @valid_user, agency: @agency, role: "member")
55
+ Agent.create!(user: @other_user, agency: @rival_agency, role: "member")
56
+ Agent.create!(user: @inactive_user, agency: @agency, role: "member")
49
57
  end
50
58
 
51
- test "POST <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/login with valid credentials returns JWT token and user data" do
59
+ test "POST <%= auth_route_prefix %>/login with valid credentials returns JWT token and user data" do
52
60
  # BEHAVIOR TEST: Verify login for a user in a different organization to ensure multi-tenancy.
53
- post <%= login_path_helper %>, params: {
61
+ post '<%= auth_route_prefix %>/login', params: {
54
62
  user: {
55
63
  email_address: @other_user.email_address,
56
64
  password: "securepassword123"
@@ -74,9 +82,9 @@ class TokensControllerTest < ActionDispatch::IntegrationTest
74
82
  assert_not_equal @organization.id, user_data["organization_id"], "Should not return data for the wrong organization"
75
83
  end
76
84
 
77
- test "POST <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/login with invalid credentials returns proper error" do
85
+ test "POST <%= auth_route_prefix %>/login with invalid credentials returns proper error" do
78
86
  # BEHAVIOR TEST: Verify authentication rejection with real database validation
79
- post <%= login_path_helper %>, params: {
87
+ post '<%= auth_route_prefix %>/login', params: {
80
88
  user: {
81
89
  email_address: @valid_user.email_address,
82
90
  password: "wrongpassword"
@@ -92,9 +100,9 @@ class TokensControllerTest < ActionDispatch::IntegrationTest
92
100
  assert_nil response_json["user"], "Should not return user data on authentication failure"
93
101
  end
94
102
 
95
- test "POST <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/login with nonexistent user returns proper error" do
103
+ test "POST <%= auth_route_prefix %>/login with nonexistent user returns proper error" do
96
104
  # BEHAVIOR TEST: Verify handling of nonexistent users
97
- post <%= login_path_helper %>, params: {
105
+ post '<%= auth_route_prefix %>/login', params: {
98
106
  user: {
99
107
  email_address: "nonexistent@example.com",
100
108
  password: "somepassword"
@@ -107,9 +115,9 @@ class TokensControllerTest < ActionDispatch::IntegrationTest
107
115
  assert_equal "Invalid credentials", response_json["error"], "Should return generic error message for security"
108
116
  end
109
117
 
110
- test "POST <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/login with inactive user returns proper error" do
118
+ test "POST <%= auth_route_prefix %>/login with inactive user returns proper error" do
111
119
  # BEHAVIOR TEST: Verify inactive user access is properly denied
112
- post <%= login_path_helper %>, params: {
120
+ post '<%= auth_route_prefix %>/login', params: {
113
121
  user: {
114
122
  email_address: @inactive_user.email_address,
115
123
  password: "password123"
@@ -122,10 +130,10 @@ class TokensControllerTest < ActionDispatch::IntegrationTest
122
130
  assert_equal "Account is inactive", response_json["error"], "Should return appropriate error for inactive account"
123
131
  end
124
132
 
125
- test "GET <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/me with valid JWT token returns current user" do
133
+ test "GET <%= auth_route_prefix %>/me with valid JWT token returns current user" do
126
134
  # BEHAVIOR TEST: Verify JWT authentication for a user from a different organization.
127
135
  # First get a valid token by logging in as the user from the rival organization.
128
- post <%= login_path_helper %>, params: {
136
+ post '<%= auth_route_prefix %>/login', params: {
129
137
  user: {
130
138
  email_address: @other_user.email_address,
131
139
  password: "securepassword123"
@@ -135,7 +143,7 @@ class TokensControllerTest < ActionDispatch::IntegrationTest
135
143
  token = JSON.parse(response.body)["token"]
136
144
 
137
145
  # Now test the protected endpoint with the real token.
138
- get <%= me_path_helper %>, headers: {
146
+ get '<%= auth_route_prefix %>/me', headers: {
139
147
  'Authorization' => "Bearer #{token}"
140
148
  }
141
149
 
@@ -150,9 +158,9 @@ class TokensControllerTest < ActionDispatch::IntegrationTest
150
158
  assert_equal @rival_organization.id, user_data["organization_id"], "Should return correct organization relationship"
151
159
  end
152
160
 
153
- test "GET <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/me with invalid JWT token returns error" do
161
+ test "GET <%= auth_route_prefix %>/me with invalid JWT token returns error" do
154
162
  # BEHAVIOR TEST: Verify JWT validation rejects invalid tokens
155
- get <%= me_path_helper %>, headers: {
163
+ get '<%= auth_route_prefix %>/me', headers: {
156
164
  'Authorization' => "Bearer invalid.jwt.token"
157
165
  }
158
166
 
@@ -162,7 +170,7 @@ class TokensControllerTest < ActionDispatch::IntegrationTest
162
170
  assert_equal "Invalid token", response_json["error"], "Should return proper error message"
163
171
  end
164
172
 
165
- test "GET <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/me with expired JWT token returns error" do
173
+ test "GET <%= auth_route_prefix %>/me with expired JWT token returns error" do
166
174
  # BEHAVIOR TEST: Verify expired tokens are properly rejected
167
175
  # Create an expired token for testing
168
176
  expired_payload = {
@@ -171,7 +179,7 @@ class TokensControllerTest < ActionDispatch::IntegrationTest
171
179
  }
172
180
  expired_token = JWT.encode(expired_payload, Rails.application.credentials.secret_key_base, 'HS256')
173
181
 
174
- get <%= me_path_helper %>, headers: {
182
+ get '<%= auth_route_prefix %>/me', headers: {
175
183
  'Authorization' => "Bearer #{expired_token}"
176
184
  }
177
185
 
@@ -181,9 +189,9 @@ class TokensControllerTest < ActionDispatch::IntegrationTest
181
189
  assert_equal "Token expired", response_json["error"], "Should return proper expiration error"
182
190
  end
183
191
 
184
- test "GET <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/me without authorization header returns error" do
192
+ test "GET <%= auth_route_prefix %>/me without authorization header returns error" do
185
193
  # BEHAVIOR TEST: Verify endpoint protection when no token provided
186
- get <%= me_path_helper %>
194
+ get '<%= auth_route_prefix %>/me'
187
195
 
188
196
  # VERIFY AUTHORIZATION REQUIREMENT: Check endpoint requires authentication
189
197
  assert_response :unauthorized, "Missing authorization should be rejected"
@@ -191,10 +199,10 @@ class TokensControllerTest < ActionDispatch::IntegrationTest
191
199
  assert_equal "No token provided", response_json["error"], "Should return proper missing token error"
192
200
  end
193
201
 
194
- test "DELETE <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/logout clears authentication state" do
202
+ test "DELETE <%= auth_route_prefix %>/logout clears authentication state" do
195
203
  # BEHAVIOR TEST: Verify logout functionality
196
204
  # First login to get a token
197
- post <%= login_path_helper %>, params: {
205
+ post '<%= auth_route_prefix %>/login', params: {
198
206
  user: {
199
207
  email_address: @valid_user.email_address,
200
208
  password: "securepassword123"
@@ -204,7 +212,7 @@ class TokensControllerTest < ActionDispatch::IntegrationTest
204
212
  token = JSON.parse(response.body)["token"]
205
213
 
206
214
  # Now logout with the token
207
- delete <%= logout_path_helper %>, headers: {
215
+ delete '<%= auth_route_prefix %>/logout', headers: {
208
216
  'Authorization' => "Bearer #{token}"
209
217
  }
210
218
 
@@ -214,9 +222,9 @@ class TokensControllerTest < ActionDispatch::IntegrationTest
214
222
  assert_equal "Logged out successfully", response_json["message"], "Should return logout confirmation"
215
223
  end
216
224
 
217
- test "POST <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/login validates required parameters" do
225
+ test "POST <%= auth_route_prefix %>/login validates required parameters" do
218
226
  # BEHAVIOR TEST: Verify parameter validation
219
- post <%= login_path_helper %>, params: {
227
+ post '<%= auth_route_prefix %>/login', params: {
220
228
  user: {
221
229
  email_address: @valid_user.email_address
222
230
  # Missing password parameter
@@ -231,7 +239,7 @@ class TokensControllerTest < ActionDispatch::IntegrationTest
231
239
 
232
240
  test "JWT token contains proper user context for multi-tenant application" do
233
241
  # BEHAVIOR TEST: Verify JWT for a different organization contains correct context.
234
- post <%= login_path_helper %>, params: {
242
+ post '<%= auth_route_prefix %>/login', params: {
235
243
  user: {
236
244
  email_address: @other_user.email_address,
237
245
  password: "securepassword123"
@@ -253,7 +261,7 @@ class TokensControllerTest < ActionDispatch::IntegrationTest
253
261
 
254
262
  # Helper method to create authenticated request headers for testing
255
263
  def authenticated_headers_for(user)
256
- post <%= login_path_helper %>, params: {
264
+ post '<%= auth_route_prefix %>/login', params: {
257
265
  user: {
258
266
  email_address: user.email_address,
259
267
  password: 'securepassword123'
@@ -13,11 +13,6 @@ class AuthMailerTest < ActionMailer::TestCase
13
13
  )
14
14
  end
15
15
 
16
- def teardown
17
- User.destroy_all
18
- Organization.destroy_all
19
- end
20
-
21
16
  # BEHAVIOR: Test password reset email generation and content
22
17
  test "password reset email should contain proper content and structure" do
23
18
  # SETUP: Generate reset token
@@ -41,13 +36,12 @@ class AuthMailerTest < ActionMailer::TestCase
41
36
  assert_equal ["noreply@#{Rails.application.class.module_parent.name.downcase}.com"], email.from, "Email should have proper from address"
42
37
  assert_match(/Reset your password/i, email.subject, "Subject should contain password reset text")
43
38
 
44
- # VERIFY BUSINESS LOGIC: Email body content validation
45
- email_body = email.body.to_s
46
- assert_match(@user.first_name, email_body, "Email should contain user's first name for personalization")
47
- assert_match(@user.email_address, email_body, "Email should display user's email address for verification")
48
- assert_match(reset_url, email_body, "Email should contain the reset URL with token")
49
- assert_match(/15 minutes/i, email_body, "Email should specify token expiration time")
50
- assert_match(/security/i, email_body, "Email should contain security messaging")
39
+ # VERIFY BUSINESS LOGIC: Email body content validation (multipart-aware)
40
+ assert_email_contains_text(email, @user.first_name, "Email should contain user's first name for personalization")
41
+ assert_email_contains_text(email, @user.email_address, "Email should display user's email address for verification")
42
+ assert_email_contains_text(email, reset_url, "Email should contain the reset URL with token")
43
+ assert_email_contains_text(email, /15 minutes/i, "Email should specify token expiration time")
44
+ assert_email_contains_text(email, /security/i, "Email should contain security messaging")
51
45
 
52
46
  # VERIFY EDGE CASES: Token format validation
53
47
  assert_match(/^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/, reset_token, "Reset token should be valid JWT format")
@@ -79,14 +73,13 @@ class AuthMailerTest < ActionMailer::TestCase
79
73
  assert_equal [@user.email_address], email.to, "Email should be sent to user's email address"
80
74
  assert_match(/Account locked/i, email.subject, "Subject should indicate account lock status")
81
75
 
82
- # VERIFY BUSINESS LOGIC: Email body content validation
83
- email_body = email.body.to_s
84
- assert_match(@user.first_name, email_body, "Email should contain user's first name")
85
- assert_match(/multiple failed login attempts/i, email_body, "Email should explain why account was locked")
86
- assert_match(unlock_url, email_body, "Email should contain unlock URL with token")
87
- assert_match(/1 hour/i, email_body, "Email should specify unlock token expiration")
88
- assert_match(/security/i, email_body, "Email should contain security messaging")
89
- assert_match(/contact support/i, email_body, "Email should provide support contact information")
76
+ # VERIFY BUSINESS LOGIC: Email body content validation (multipart-aware)
77
+ assert_email_contains_text(email, @user.first_name, "Email should contain user's first name")
78
+ assert_email_contains_text(email, /multiple failed login attempts/i, "Email should explain why account was locked")
79
+ assert_email_contains_text(email, unlock_url, "Email should contain unlock URL with token")
80
+ assert_email_contains_text(email, /1 hour/i, "Email should specify unlock token expiration")
81
+ assert_email_contains_text(email, /security/i, "Email should contain security messaging")
82
+ assert_email_contains_text(email, /contact support/i, "Email should provide support contact information")
90
83
 
91
84
  # VERIFY EDGE CASES: Token format validation
92
85
  assert_match(/^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/, unlock_token, "Unlock token should be valid JWT format")
@@ -133,14 +126,13 @@ class AuthMailerTest < ActionMailer::TestCase
133
126
  assert_match(/invited/i, email.subject, "Subject should indicate invitation")
134
127
  assert_match(@organization.name, email.subject, "Subject should contain organization name")
135
128
 
136
- # VERIFY BUSINESS LOGIC: Email body content validation
137
- email_body = email.body.to_s
138
- assert_match(invitation.first_name, email_body, "Email should contain invitee's first name")
139
- assert_match(inviter.first_name, email_body, "Email should contain inviter's name")
140
- assert_match(@organization.name, email_body, "Email should contain organization name")
141
- assert_match(invitation_url, email_body, "Email should contain invitation acceptance URL")
142
- assert_match(/7 days/i, email_body, "Email should specify invitation expiration")
143
- assert_match(/getting started/i, email_body, "Email should contain onboarding information")
129
+ # VERIFY BUSINESS LOGIC: Email body content validation (multipart-aware)
130
+ assert_email_contains_text(email, invitation.first_name, "Email should contain invitee's first name")
131
+ assert_email_contains_text(email, inviter.first_name, "Email should contain inviter's name")
132
+ assert_email_contains_text(email, @organization.name, "Email should contain organization name")
133
+ assert_email_contains_text(email, invitation_url, "Email should contain invitation acceptance URL")
134
+ assert_email_contains_text(email, /7 days/i, "Email should specify invitation expiration")
135
+ assert_email_contains_text(email, /getting started/i, "Email should contain onboarding information")
144
136
 
145
137
  # VERIFY EDGE CASES: Token format validation
146
138
  assert_match(/^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/, invitation_token, "Invitation token should be valid JWT format")
@@ -172,7 +164,7 @@ class AuthMailerTest < ActionMailer::TestCase
172
164
  assert_equal [user_with_invalid_email.email_address], email.to, "Email should contain the provided address"
173
165
 
174
166
  # VERIFY BUSINESS LOGIC: System should not crash on delivery attempts
175
- assert_nothing_raised "Email delivery should not raise exceptions in test environment" do
167
+ assert_nothing_raised do
176
168
  email.deliver_now
177
169
  end
178
170
  end
@@ -200,17 +192,40 @@ class AuthMailerTest < ActionMailer::TestCase
200
192
  # VERIFY FUNCTIONALITY: Email should be generated successfully
201
193
  assert_not_nil email, "Email should be generated for user with minimal data"
202
194
 
203
- # VERIFY BUSINESS LOGIC: Email should handle missing first name gracefully
204
- email_body = email.body.to_s
205
- assert_match(/Hello/i, email_body, "Email should have fallback greeting")
206
- assert_match(minimal_user.email_address, email_body, "Email should display email address when name unavailable")
195
+ # VERIFY BUSINESS LOGIC: Email should handle missing first name gracefully (multipart-aware)
196
+ assert_email_contains_text(email, /Hello/i, "Email should have fallback greeting")
197
+ assert_email_contains_text(email, minimal_user.email_address, "Email should display email address when name unavailable")
207
198
  end
208
199
 
209
200
  private
210
201
 
211
- # Helper method to extract token from email body
212
- def extract_token_from_email_body(email_body)
213
- match = email_body.match(/token=([A-Za-z0-9\-_\.]+)/)
202
+ # Helper method to check content in multipart emails using Rails built-in methods
203
+ def assert_email_contains_text(email, expected_text, message = nil)
204
+ # Use Rails' built-in methods for cleaner multipart handling
205
+ email_content = [
206
+ email.text_part&.body&.to_s,
207
+ email.html_part&.body&.to_s&.gsub(/<[^>]*>/, ' ')&.gsub(/\s+/, ' ')&.strip
208
+ ].compact.join(' ')
209
+
210
+ # Fallback to body for single-part emails
211
+ email_content = email.body.to_s if email_content.empty?
212
+
213
+ if expected_text.is_a?(Regexp)
214
+ assert_match expected_text, email_content, message
215
+ else
216
+ assert_includes email_content, expected_text.to_s, message
217
+ end
218
+ end
219
+
220
+ # Helper method to extract token from email body using Rails methods
221
+ def extract_token_from_email_body(email)
222
+ email_content = [
223
+ email.text_part&.body&.to_s,
224
+ email.html_part&.body&.to_s
225
+ ].compact.join(' ')
226
+
227
+ email_content = email.body.to_s if email_content.empty?
228
+ match = email_content.match(/token=([A-Za-z0-9\-_\.]+)/)
214
229
  match ? match[1] : nil
215
230
  end
216
231
  end
@@ -164,7 +164,7 @@
164
164
  <p>Thank you for signing up! To get started, please confirm your email address by clicking the button below:</p>
165
165
 
166
166
  <div style="text-align: center;">
167
- <a href="<%= confirmation_url(@user) %>" class="confirm-button">
167
+ <a href="<%= @confirmation_url %>" class="confirm-button">
168
168
  Confirm My Email Address
169
169
  </a>
170
170
  </div>
@@ -176,7 +176,7 @@
176
176
 
177
177
  <div class="token-section">
178
178
  <p style="margin: 0;"><strong>Confirmation Link:</strong></p>
179
- <p class="token-code"><%= confirmation_url(@user) %></p>
179
+ <p class="token-code"><%= @confirmation_url %></p>
180
180
  </div>
181
181
 
182
182
  <!-- Security information -->
@@ -7,7 +7,7 @@ Welcome <%= @display_name %>!
7
7
  Thank you for signing up! To get started, please confirm your email address.
8
8
 
9
9
  CONFIRM YOUR EMAIL ADDRESS:
10
- <%= confirmation_url(@user) %>
10
+ <%= @confirmation_url %>
11
11
 
12
12
  This confirmation link will expire in 24 hours for security reasons.
13
13
 
@@ -116,7 +116,7 @@ class Propel::Authentication::InstallGeneratorTest < Rails::Generators::TestCase
116
116
  assert_match(/GET \/auth\/me/, content)
117
117
 
118
118
  # Verify proper JWT authentication middleware
119
- assert_match(/include PropelAuthentication/, content)
119
+ assert_match(/include PropelAuthenticationConcern/, content)
120
120
  assert_match(/return unless authenticate_user/, content)
121
121
 
122
122
  # Verify proper API response format
@@ -265,7 +265,7 @@ class Propel::Authentication::InstallGeneratorTest < Rails::Generators::TestCase
265
265
  assert_match(/def me/, content)
266
266
 
267
267
  # Verify JWT authentication support for testing
268
- assert_match(/include PropelAuthentication/, content)
268
+ assert_match(/include PropelAuthenticationConcern/, content)
269
269
  assert_match(/return unless authenticate_user/, content)
270
270
 
271
271
  # Verify proper error handling for testing
@@ -370,7 +370,7 @@ class Propel::Authentication::InstallGeneratorTest < Rails::Generators::TestCase
370
370
  assert_match(/status: :unauthorized/, content)
371
371
 
372
372
  # Verify JWT token handling
373
- assert_match(/include PropelAuthentication/, content)
373
+ assert_match(/include PropelAuthenticationConcern/, content)
374
374
  assert_match(/return unless authenticate_user/, content)
375
375
  end
376
376
  end
@@ -450,7 +450,7 @@ class Propel::Authentication::InstallGeneratorTest < Rails::Generators::TestCase
450
450
  # Verify the system would work for a JavaScript frontend
451
451
  controller_code = File.read(File.join(destination_root, "app/controllers/auth/tokens_controller.rb"))
452
452
  assert_match(/render json:/, controller_code)
453
- assert_match(/include PropelAuthentication/, controller_code)
453
+ assert_match(/include PropelAuthenticationConcern/, controller_code)
454
454
 
455
455
  # Verify JWT token functionality exists
456
456
  authenticatable_code = File.read(File.join(destination_root, "app/models/concerns/authenticatable.rb"))
@@ -65,7 +65,7 @@ class Propel::Authentication::UninstallGeneratorTest < Rails::Generators::TestCa
65
65
  assert_no_file "app/models/concerns/authenticatable.rb"
66
66
  assert_no_file "app/models/concerns/lockable.rb"
67
67
  assert_no_file "app/controllers/auth/tokens_controller.rb"
68
- assert_no_file "app/controllers/concerns/propel_authentication.rb"
68
+ assert_no_file "app/controllers/concerns/propel_authentication_concern.rb"
69
69
  assert_no_file "config/initializers/propel_access.rb"
70
70
  assert_no_file "config/initializers/propel_access_configuration.rb"
71
71
  assert_no_file "db/seeds.rb"
@@ -140,7 +140,7 @@ class GeneratorIntegrationTest < ActiveSupport::TestCase
140
140
  "Controller should have destroy action (logout)")
141
141
  assert_match(/def me/, controller_content,
142
142
  "Controller should have me action")
143
- assert_match(/include PropelAuthentication/, controller_content,
143
+ assert_match(/include PropelAuthenticationConcern/, controller_content,
144
144
  "Controller should include authentication concern")
145
145
 
146
146
  # Test routes were generated
@@ -16,31 +16,32 @@ class MultiVersionGeneratorTest < Rails::Generators::TestCase
16
16
  super
17
17
  end
18
18
 
19
- test "generator creates dynamic API versioning structure" do
20
- run_generator ["--configurable-versioning"]
19
+ test "generator creates dynamic controller structure with proper namespace configuration" do
20
+ run_generator ["--namespace=api", "--version=v1"]
21
21
 
22
- # Verify base controllers are created
23
- assert_file "app/controllers/api/auth/base_tokens_controller.rb" do |content|
24
- assert_match(/class Api::Auth::BaseTokensController < ApplicationController/, content)
25
- assert_match(/POST \/api\/\{version\}\/auth\/login/, content)
22
+ # Verify direct controllers are created with proper class names (no inheritance)
23
+ assert_file "app/controllers/api/v1/tokens_controller.rb" do |content|
24
+ assert_match(/class Api::V1::TokensController < ApplicationController/, content)
25
+ assert_match(/POST \/api\/v1\/login/, content)
26
26
  assert_match(/def create/, content)
27
27
  assert_match(/def show/, content)
28
28
  assert_match(/def destroy/, content)
29
29
  assert_match(/def unlock/, content)
30
30
  end
31
31
 
32
- assert_file "app/controllers/api/auth/base_passwords_controller.rb" do |content|
33
- assert_match(/class Api::Auth::BasePasswordsController < ApplicationController/, content)
34
- assert_match(/POST \/api\/\{version\}\/auth\/reset/, content)
32
+ assert_file "app/controllers/api/v1/passwords_controller.rb" do |content|
33
+ assert_match(/class Api::V1::PasswordsController < ApplicationController/, content)
34
+ assert_match(/POST \/api\/v1\/reset/, content)
35
35
  assert_match(/def create/, content)
36
36
  assert_match(/def show/, content)
37
37
  assert_match(/def update/, content)
38
38
  end
39
39
 
40
- # Verify runtime configurable routes are created
40
+ # Verify routes are created with proper namespacing and as: options
41
41
  assert_file "config/routes.rb" do |content|
42
- assert_match(/# Runtime configurable API versioning routes/, content)
43
- assert_match(/api_version = PropelAccess\.configuration\.api_version/, content)
42
+ assert_match(/namespace :api/, content)
43
+ assert_match(/namespace :v1/, content)
44
+ assert_match(/post 'login', to: 'tokens#create', as: :login/, content)
44
45
  assert_match(/namespace :api do/, content)
45
46
  assert_match(/namespace api_version do/, content)
46
47
  assert_match(/namespace :auth do/, content)