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.
Files changed (102) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +290 -0
  4. data/Rakefile +12 -0
  5. data/lib/generators/propel_auth/install_generator.rb +486 -0
  6. data/lib/generators/propel_auth/pack_generator.rb +277 -0
  7. data/lib/generators/propel_auth/templates/agency.rb +7 -0
  8. data/lib/generators/propel_auth/templates/agent.rb +7 -0
  9. data/lib/generators/propel_auth/templates/auth/base_passwords_controller.rb.tt +99 -0
  10. data/lib/generators/propel_auth/templates/auth/base_tokens_controller.rb.tt +90 -0
  11. data/lib/generators/propel_auth/templates/auth/passwords_controller.rb.tt +126 -0
  12. data/lib/generators/propel_auth/templates/auth_mailer.rb +180 -0
  13. data/lib/generators/propel_auth/templates/authenticatable.rb +38 -0
  14. data/lib/generators/propel_auth/templates/concerns/confirmable.rb +145 -0
  15. data/lib/generators/propel_auth/templates/concerns/lockable.rb +123 -0
  16. data/lib/generators/propel_auth/templates/concerns/propel_authentication.rb +44 -0
  17. data/lib/generators/propel_auth/templates/concerns/rack_session_disable.rb +19 -0
  18. data/lib/generators/propel_auth/templates/concerns/recoverable.rb +124 -0
  19. data/lib/generators/propel_auth/templates/config/environments/development_email.rb +43 -0
  20. data/lib/generators/propel_auth/templates/db/migrate/create_agencies.rb +20 -0
  21. data/lib/generators/propel_auth/templates/db/migrate/create_agents.rb +11 -0
  22. data/lib/generators/propel_auth/templates/db/migrate/create_invitations.rb +28 -0
  23. data/lib/generators/propel_auth/templates/db/migrate/create_organizations.rb +18 -0
  24. data/lib/generators/propel_auth/templates/db/migrate/create_users.rb +43 -0
  25. data/lib/generators/propel_auth/templates/db/seeds.rb +29 -0
  26. data/lib/generators/propel_auth/templates/invitation.rb +133 -0
  27. data/lib/generators/propel_auth/templates/lib/propel_auth.rb +84 -0
  28. data/lib/generators/propel_auth/templates/organization.rb +7 -0
  29. data/lib/generators/propel_auth/templates/propel_auth.rb +132 -0
  30. data/lib/generators/propel_auth/templates/services/auth_notification_service.rb +89 -0
  31. data/lib/generators/propel_auth/templates/test/concerns/confirmable_test.rb.tt +247 -0
  32. data/lib/generators/propel_auth/templates/test/concerns/lockable_test.rb.tt +282 -0
  33. data/lib/generators/propel_auth/templates/test/concerns/propel_authentication_test.rb.tt +75 -0
  34. data/lib/generators/propel_auth/templates/test/concerns/recoverable_test.rb.tt +327 -0
  35. data/lib/generators/propel_auth/templates/test/controllers/auth/lockable_integration_test.rb.tt +196 -0
  36. data/lib/generators/propel_auth/templates/test/controllers/auth/password_reset_integration_test.rb.tt +471 -0
  37. data/lib/generators/propel_auth/templates/test/controllers/auth/tokens_controller_test.rb.tt +265 -0
  38. data/lib/generators/propel_auth/templates/test/mailers/auth_mailer_test.rb.tt +216 -0
  39. data/lib/generators/propel_auth/templates/test/mailers/previews/auth_mailer_preview.rb +161 -0
  40. data/lib/generators/propel_auth/templates/tokens_controller.rb.tt +96 -0
  41. data/lib/generators/propel_auth/templates/user.rb +21 -0
  42. data/lib/generators/propel_auth/templates/user_test.rb.tt +81 -0
  43. data/lib/generators/propel_auth/templates/views/auth_mailer/account_unlock.html.erb +213 -0
  44. data/lib/generators/propel_auth/templates/views/auth_mailer/account_unlock.text.erb +56 -0
  45. data/lib/generators/propel_auth/templates/views/auth_mailer/email_confirmation.html.erb +213 -0
  46. data/lib/generators/propel_auth/templates/views/auth_mailer/email_confirmation.text.erb +32 -0
  47. data/lib/generators/propel_auth/templates/views/auth_mailer/password_reset.html.erb +166 -0
  48. data/lib/generators/propel_auth/templates/views/auth_mailer/password_reset.text.erb +32 -0
  49. data/lib/generators/propel_auth/templates/views/auth_mailer/user_invitation.html.erb +194 -0
  50. data/lib/generators/propel_auth/templates/views/auth_mailer/user_invitation.text.erb +51 -0
  51. data/lib/generators/propel_auth/test/dummy/Dockerfile +72 -0
  52. data/lib/generators/propel_auth/test/dummy/Gemfile +63 -0
  53. data/lib/generators/propel_auth/test/dummy/Gemfile.lock +394 -0
  54. data/lib/generators/propel_auth/test/dummy/README.md +24 -0
  55. data/lib/generators/propel_auth/test/dummy/Rakefile +6 -0
  56. data/lib/generators/propel_auth/test/dummy/app/assets/stylesheets/application.css +10 -0
  57. data/lib/generators/propel_auth/test/dummy/app/controllers/application_controller.rb +4 -0
  58. data/lib/generators/propel_auth/test/dummy/app/helpers/application_helper.rb +2 -0
  59. data/lib/generators/propel_auth/test/dummy/app/jobs/application_job.rb +7 -0
  60. data/lib/generators/propel_auth/test/dummy/app/mailers/application_mailer.rb +4 -0
  61. data/lib/generators/propel_auth/test/dummy/app/models/application_record.rb +3 -0
  62. data/lib/generators/propel_auth/test/dummy/app/views/layouts/application.html.erb +27 -0
  63. data/lib/generators/propel_auth/test/dummy/app/views/layouts/mailer.html.erb +13 -0
  64. data/lib/generators/propel_auth/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  65. data/lib/generators/propel_auth/test/dummy/app/views/pwa/manifest.json.erb +22 -0
  66. data/lib/generators/propel_auth/test/dummy/app/views/pwa/service-worker.js +26 -0
  67. data/lib/generators/propel_auth/test/dummy/bin/brakeman +7 -0
  68. data/lib/generators/propel_auth/test/dummy/bin/dev +2 -0
  69. data/lib/generators/propel_auth/test/dummy/bin/docker-entrypoint +14 -0
  70. data/lib/generators/propel_auth/test/dummy/bin/rails +4 -0
  71. data/lib/generators/propel_auth/test/dummy/bin/rake +4 -0
  72. data/lib/generators/propel_auth/test/dummy/bin/rubocop +8 -0
  73. data/lib/generators/propel_auth/test/dummy/bin/setup +34 -0
  74. data/lib/generators/propel_auth/test/dummy/bin/thrust +5 -0
  75. data/lib/generators/propel_auth/test/dummy/config/application.rb +42 -0
  76. data/lib/generators/propel_auth/test/dummy/config/boot.rb +4 -0
  77. data/lib/generators/propel_auth/test/dummy/config/cable.yml +10 -0
  78. data/lib/generators/propel_auth/test/dummy/config/credentials.yml.enc +1 -0
  79. data/lib/generators/propel_auth/test/dummy/config/database.yml +41 -0
  80. data/lib/generators/propel_auth/test/dummy/config/environment.rb +5 -0
  81. data/lib/generators/propel_auth/test/dummy/config/environments/development.rb +72 -0
  82. data/lib/generators/propel_auth/test/dummy/config/environments/production.rb +89 -0
  83. data/lib/generators/propel_auth/test/dummy/config/environments/test.rb +53 -0
  84. data/lib/generators/propel_auth/test/dummy/config/initializers/assets.rb +10 -0
  85. data/lib/generators/propel_auth/test/dummy/config/initializers/content_security_policy.rb +25 -0
  86. data/lib/generators/propel_auth/test/dummy/config/initializers/filter_parameter_logging.rb +8 -0
  87. data/lib/generators/propel_auth/test/dummy/config/initializers/inflections.rb +16 -0
  88. data/lib/generators/propel_auth/test/dummy/config/locales/en.yml +31 -0
  89. data/lib/generators/propel_auth/test/dummy/config/master.key +1 -0
  90. data/lib/generators/propel_auth/test/dummy/config/puma.rb +41 -0
  91. data/lib/generators/propel_auth/test/dummy/config/routes.rb +2 -0
  92. data/lib/generators/propel_auth/test/dummy/config/storage.yml +34 -0
  93. data/lib/generators/propel_auth/test/dummy/config.ru +6 -0
  94. data/lib/generators/propel_auth/test/dummy/db/schema.rb +14 -0
  95. data/lib/generators/propel_auth/test/generators/authentication/controllers/tokens_controller_test.rb +230 -0
  96. data/lib/generators/propel_auth/test/generators/authentication/install_generator_test.rb +490 -0
  97. data/lib/generators/propel_auth/test/generators/authentication/uninstall_generator_test.rb +408 -0
  98. data/lib/generators/propel_auth/test/integration/generator_integration_test.rb +158 -0
  99. data/lib/generators/propel_auth/test/integration/multi_version_generator_test.rb +125 -0
  100. data/lib/generators/propel_auth/unpack_generator.rb +345 -0
  101. data/lib/propel_auth.rb +3 -0
  102. metadata +195 -0
@@ -0,0 +1,265 @@
1
+ require 'test_helper'
2
+
3
+ class TokensControllerTest < ActionDispatch::IntegrationTest
4
+ # BEHAVIOR-DRIVEN TESTING: NO MOCKS - Test Real Functionality
5
+ # These tests verify actual JWT authentication works with real database operations
6
+ # and HTTP requests to provide 100% confidence in production functionality
7
+
8
+ setup do
9
+ # Create real test data in the database - NO MOCKS
10
+ @organization = Organization.create!(
11
+ name: "Marvel",
12
+ website: "https://marvel.com"
13
+ )
14
+ @rival_organization = Organization.create!(
15
+ name: "DC Comics",
16
+ website: "https://dccomics.com"
17
+ )
18
+
19
+ @valid_user = User.create!(
20
+ email_address: "valid@example.com",
21
+ username: "validuser",
22
+ password: "securepassword123",
23
+ password_confirmation: "securepassword123",
24
+ organization: @organization,
25
+ first_name: "Valid",
26
+ last_name: "User"
27
+ )
28
+
29
+ @other_user = User.create!(
30
+ email_address: "other@example.com",
31
+ username: "otheruser",
32
+ password: "securepassword123",
33
+ password_confirmation: "securepassword123",
34
+ organization: @rival_organization,
35
+ first_name: "Other",
36
+ last_name: "User"
37
+ )
38
+
39
+ @inactive_user = User.create!(
40
+ email_address: "inactive@example.com",
41
+ username: "inactiveuser",
42
+ password: "password123",
43
+ password_confirmation: "password123",
44
+ organization: @organization,
45
+ first_name: "Inactive",
46
+ last_name: "User",
47
+ status: 1
48
+ )
49
+ end
50
+
51
+ test "POST <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/login with valid credentials returns JWT token and user data" do
52
+ # BEHAVIOR TEST: Verify login for a user in a different organization to ensure multi-tenancy.
53
+ post <%= login_path_helper %>, params: {
54
+ user: {
55
+ email_address: @other_user.email_address,
56
+ password: "securepassword123"
57
+ }
58
+ }
59
+
60
+ # VERIFY RESPONSE STRUCTURE: Basic validation
61
+ assert_response :ok, "Login should return OK status"
62
+ response_json = JSON.parse(response.body)
63
+ assert response_json.key?("token"), "Response should contain JWT token"
64
+ assert response_json.key?("user"), "Response should contain user data"
65
+
66
+ # VERIFY USER DATA ACCURACY: Check that the correct user and organization are returned.
67
+ user_data = response_json["user"]
68
+ assert_equal @other_user.id, user_data["id"], "Should return correct user ID"
69
+ assert_equal @other_user.email_address, user_data["email_address"], "Should return correct user email_address"
70
+ assert_equal @rival_organization.id, user_data["organization_id"], "Should return correct organization ID for the rival organization"
71
+
72
+ # VERIFY DATA ISOLATION: Ensure data from the other user/org is not returned.
73
+ assert_not_equal @valid_user.id, user_data["id"], "Should not return data for the wrong user"
74
+ assert_not_equal @organization.id, user_data["organization_id"], "Should not return data for the wrong organization"
75
+ end
76
+
77
+ test "POST <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/login with invalid credentials returns proper error" do
78
+ # BEHAVIOR TEST: Verify authentication rejection with real database validation
79
+ post <%= login_path_helper %>, params: {
80
+ user: {
81
+ email_address: @valid_user.email_address,
82
+ password: "wrongpassword"
83
+ }
84
+ }
85
+
86
+ # VERIFY ERROR RESPONSE: Check proper error handling
87
+ assert_response :unauthorized, "Invalid credentials should return unauthorized status"
88
+ response_json = JSON.parse(response.body)
89
+ assert response_json.key?("error"), "Error response should contain error message"
90
+ assert_equal "Invalid credentials", response_json["error"], "Should return proper error message"
91
+ assert_nil response_json["token"], "Should not return token on authentication failure"
92
+ assert_nil response_json["user"], "Should not return user data on authentication failure"
93
+ end
94
+
95
+ test "POST <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/login with nonexistent user returns proper error" do
96
+ # BEHAVIOR TEST: Verify handling of nonexistent users
97
+ post <%= login_path_helper %>, params: {
98
+ user: {
99
+ email_address: "nonexistent@example.com",
100
+ password: "somepassword"
101
+ }
102
+ }
103
+
104
+ # VERIFY ERROR RESPONSE: Check user not found handling
105
+ assert_response :unauthorized, "Nonexistent user should return unauthorized status"
106
+ response_json = JSON.parse(response.body)
107
+ assert_equal "Invalid credentials", response_json["error"], "Should return generic error message for security"
108
+ end
109
+
110
+ test "POST <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/login with inactive user returns proper error" do
111
+ # BEHAVIOR TEST: Verify inactive user access is properly denied
112
+ post <%= login_path_helper %>, params: {
113
+ user: {
114
+ email_address: @inactive_user.email_address,
115
+ password: "password123"
116
+ }
117
+ }
118
+
119
+ # VERIFY INACTIVE USER HANDLING: Check business logic enforcement
120
+ assert_response :unauthorized, "Inactive user should be denied access"
121
+ response_json = JSON.parse(response.body)
122
+ assert_equal "Account is inactive", response_json["error"], "Should return appropriate error for inactive account"
123
+ end
124
+
125
+ test "GET <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/me with valid JWT token returns current user" do
126
+ # BEHAVIOR TEST: Verify JWT authentication for a user from a different organization.
127
+ # First get a valid token by logging in as the user from the rival organization.
128
+ post <%= login_path_helper %>, params: {
129
+ user: {
130
+ email_address: @other_user.email_address,
131
+ password: "securepassword123"
132
+ }
133
+ }
134
+
135
+ token = JSON.parse(response.body)["token"]
136
+
137
+ # Now test the protected endpoint with the real token.
138
+ get <%= me_path_helper %>, headers: {
139
+ 'Authorization' => "Bearer #{token}"
140
+ }
141
+
142
+ # VERIFY AUTHENTICATED RESPONSE: Check that the correct user data is returned.
143
+ assert_response :ok, "Valid token should allow access to protected endpoint"
144
+ response_json = JSON.parse(response.body)
145
+ user_data = response_json["user"]
146
+
147
+ # VERIFY USER DATA ACCURACY: Check that the data belongs to the authenticated user.
148
+ assert_equal @other_user.id, user_data["id"], "Should return correct authenticated user ID"
149
+ assert_equal @other_user.email_address, user_data["email_address"], "Should return correct authenticated user email_address"
150
+ assert_equal @rival_organization.id, user_data["organization_id"], "Should return correct organization relationship"
151
+ end
152
+
153
+ test "GET <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/me with invalid JWT token returns error" do
154
+ # BEHAVIOR TEST: Verify JWT validation rejects invalid tokens
155
+ get <%= me_path_helper %>, headers: {
156
+ 'Authorization' => "Bearer invalid.jwt.token"
157
+ }
158
+
159
+ # VERIFY SECURITY VALIDATION: Check token validation properly rejects invalid tokens
160
+ assert_response :unauthorized, "Invalid token should be rejected"
161
+ response_json = JSON.parse(response.body)
162
+ assert_equal "Invalid token", response_json["error"], "Should return proper error message"
163
+ end
164
+
165
+ test "GET <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/me with expired JWT token returns error" do
166
+ # BEHAVIOR TEST: Verify expired tokens are properly rejected
167
+ # Create an expired token for testing
168
+ expired_payload = {
169
+ user_id: @valid_user.id,
170
+ exp: 1.hour.ago.to_i # Token expired 1 hour ago
171
+ }
172
+ expired_token = JWT.encode(expired_payload, Rails.application.credentials.secret_key_base, 'HS256')
173
+
174
+ get <%= me_path_helper %>, headers: {
175
+ 'Authorization' => "Bearer #{expired_token}"
176
+ }
177
+
178
+ # VERIFY EXPIRATION HANDLING: Check expired tokens are rejected
179
+ assert_response :unauthorized, "Expired token should be rejected"
180
+ response_json = JSON.parse(response.body)
181
+ assert_equal "Token expired", response_json["error"], "Should return proper expiration error"
182
+ end
183
+
184
+ test "GET <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/me without authorization header returns error" do
185
+ # BEHAVIOR TEST: Verify endpoint protection when no token provided
186
+ get <%= me_path_helper %>
187
+
188
+ # VERIFY AUTHORIZATION REQUIREMENT: Check endpoint requires authentication
189
+ assert_response :unauthorized, "Missing authorization should be rejected"
190
+ response_json = JSON.parse(response.body)
191
+ assert_equal "No token provided", response_json["error"], "Should return proper missing token error"
192
+ end
193
+
194
+ test "DELETE <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/logout clears authentication state" do
195
+ # BEHAVIOR TEST: Verify logout functionality
196
+ # First login to get a token
197
+ post <%= login_path_helper %>, params: {
198
+ user: {
199
+ email_address: @valid_user.email_address,
200
+ password: "securepassword123"
201
+ }
202
+ }
203
+
204
+ token = JSON.parse(response.body)["token"]
205
+
206
+ # Now logout with the token
207
+ delete <%= logout_path_helper %>, headers: {
208
+ 'Authorization' => "Bearer #{token}"
209
+ }
210
+
211
+ # VERIFY LOGOUT RESPONSE: Check logout confirmation
212
+ assert_response :ok, "Logout should return success status"
213
+ response_json = JSON.parse(response.body)
214
+ assert_equal "Logged out successfully", response_json["message"], "Should return logout confirmation"
215
+ end
216
+
217
+ test "POST <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/login validates required parameters" do
218
+ # BEHAVIOR TEST: Verify parameter validation
219
+ post <%= login_path_helper %>, params: {
220
+ user: {
221
+ email_address: @valid_user.email_address
222
+ # Missing password parameter
223
+ }
224
+ }
225
+
226
+ # VERIFY PARAMETER VALIDATION: Check required field enforcement
227
+ assert_response :unprocessable_entity, "Missing required parameters should return unprocessable_entity"
228
+ response_json = JSON.parse(response.body)
229
+ assert response_json["error"].present?, "Should return validation error message"
230
+ end
231
+
232
+ test "JWT token contains proper user context for multi-tenant application" do
233
+ # BEHAVIOR TEST: Verify JWT for a different organization contains correct context.
234
+ post <%= login_path_helper %>, params: {
235
+ user: {
236
+ email_address: @other_user.email_address,
237
+ password: "securepassword123"
238
+ }
239
+ }
240
+
241
+ token = JSON.parse(response.body)["token"]
242
+ decoded_token = JWT.decode(token, Rails.application.credentials.secret_key_base, true, { algorithm: 'HS256' })
243
+ payload = decoded_token[0]
244
+
245
+ # VERIFY MULTI-TENANT CONTEXT: Check that the correct organization context is in the token.
246
+ assert_equal @rival_organization.id, payload['organization_id'], "Token should contain the correct rival organization context"
247
+ assert_equal @other_user.id, payload['user_id'], "Token should contain the correct user ID"
248
+ assert payload['exp'].present?, "Token should have expiration"
249
+ assert payload['iat'].present?, "Token should have issued at time"
250
+ end
251
+
252
+ private
253
+
254
+ # Helper method to create authenticated request headers for testing
255
+ def authenticated_headers_for(user)
256
+ post <%= login_path_helper %>, params: {
257
+ user: {
258
+ email_address: user.email_address,
259
+ password: 'securepassword123'
260
+ }
261
+ }
262
+ token = JSON.parse(response.body)['token']
263
+ { 'Authorization' => "Bearer #{token}" }
264
+ end
265
+ end
@@ -0,0 +1,216 @@
1
+ require 'test_helper'
2
+
3
+ class AuthMailerTest < ActionMailer::TestCase
4
+ def setup
5
+ @organization = Organization.create!(name: "Test Organization")
6
+ @user = User.create!(
7
+ email_address: "auth-mailer-test@example.com",
8
+ username: "authmailertestuser",
9
+ password: "password123",
10
+ organization: @organization,
11
+ first_name: "Auth",
12
+ last_name: "Tester"
13
+ )
14
+ end
15
+
16
+ def teardown
17
+ User.destroy_all
18
+ Organization.destroy_all
19
+ end
20
+
21
+ # BEHAVIOR: Test password reset email generation and content
22
+ test "password reset email should contain proper content and structure" do
23
+ # SETUP: Generate reset token
24
+ reset_token = @user.generate_password_reset_token
25
+ reset_url = "http://localhost:3000/reset-password?token=#{reset_token}"
26
+
27
+ # EXECUTE: Generate password reset email
28
+ email = AuthMailer.with(
29
+ user: @user,
30
+ token: reset_token,
31
+ reset_url: reset_url
32
+ ).password_reset
33
+
34
+ # VERIFY STRUCTURE: Basic email structure validation
35
+ assert_emails 1 do
36
+ email.deliver_now
37
+ end
38
+
39
+ # VERIFY FUNCTIONALITY: Email content and behavior
40
+ assert_equal [@user.email_address], email.to, "Email should be sent to user's email address"
41
+ assert_equal ["noreply@#{Rails.application.class.module_parent.name.downcase}.com"], email.from, "Email should have proper from address"
42
+ assert_match(/Reset your password/i, email.subject, "Subject should contain password reset text")
43
+
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")
51
+
52
+ # VERIFY EDGE CASES: Token format validation
53
+ assert_match(/^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/, reset_token, "Reset token should be valid JWT format")
54
+ end
55
+
56
+ # BEHAVIOR: Test account unlock email generation and content
57
+ test "account unlock email should contain proper content and security messaging" do
58
+ # SETUP: Lock user account and generate unlock token
59
+ @user.lock_account!
60
+ unlock_token = @user.generate_unlock_token
61
+ unlock_url = "http://localhost:3000/unlock-account?token=#{unlock_token}"
62
+
63
+ # VERIFY PRECONDITION: User should be locked
64
+ assert @user.locked?, "User should be locked before sending unlock email"
65
+
66
+ # EXECUTE: Generate account unlock email
67
+ email = AuthMailer.with(
68
+ user: @user,
69
+ token: unlock_token,
70
+ unlock_url: unlock_url
71
+ ).account_unlock
72
+
73
+ # VERIFY STRUCTURE: Basic email structure validation
74
+ assert_emails 1 do
75
+ email.deliver_now
76
+ end
77
+
78
+ # VERIFY FUNCTIONALITY: Email content and behavior
79
+ assert_equal [@user.email_address], email.to, "Email should be sent to user's email address"
80
+ assert_match(/Account locked/i, email.subject, "Subject should indicate account lock status")
81
+
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")
90
+
91
+ # VERIFY EDGE CASES: Token format validation
92
+ assert_match(/^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/, unlock_token, "Unlock token should be valid JWT format")
93
+ end
94
+
95
+ # BEHAVIOR: Test user invitation email generation and content
96
+ test "user invitation email should contain proper invitation content and onboarding" do
97
+ # SETUP: Create invitation
98
+ inviter = User.create!(
99
+ email_address: "inviter@example.com",
100
+ username: "inviter",
101
+ password: "password123",
102
+ organization: @organization,
103
+ first_name: "Inviter",
104
+ last_name: "User"
105
+ )
106
+
107
+ invitation = Invitation.create!(
108
+ email_address: "invited@example.com",
109
+ first_name: "Invited",
110
+ last_name: "User",
111
+ organization: @organization,
112
+ inviter: inviter
113
+ )
114
+
115
+ invitation_token = invitation.generate_invitation_token
116
+ invitation_url = "http://localhost:3000/accept-invitation?token=#{invitation_token}"
117
+
118
+ # EXECUTE: Generate invitation email
119
+ email = AuthMailer.with(
120
+ invitation: invitation,
121
+ inviter: inviter,
122
+ token: invitation_token,
123
+ invitation_url: invitation_url
124
+ ).user_invitation
125
+
126
+ # VERIFY STRUCTURE: Basic email structure validation
127
+ assert_emails 1 do
128
+ email.deliver_now
129
+ end
130
+
131
+ # VERIFY FUNCTIONALITY: Email content and behavior
132
+ assert_equal [invitation.email_address], email.to, "Email should be sent to invitation email address"
133
+ assert_match(/invited/i, email.subject, "Subject should indicate invitation")
134
+ assert_match(@organization.name, email.subject, "Subject should contain organization name")
135
+
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")
144
+
145
+ # VERIFY EDGE CASES: Token format validation
146
+ assert_match(/^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/, invitation_token, "Invitation token should be valid JWT format")
147
+ end
148
+
149
+ # BEHAVIOR: Test email delivery failure handling
150
+ test "should handle email delivery failures gracefully" do
151
+ # SETUP: Create user with invalid email to simulate delivery failure
152
+ user_with_invalid_email = User.create!(
153
+ email_address: "invalid-email-that-will-bounce@nonexistent-domain-12345.invalid",
154
+ username: "invalidemailuser",
155
+ password: "password123",
156
+ organization: @organization,
157
+ first_name: "Invalid",
158
+ last_name: "Email"
159
+ )
160
+
161
+ reset_token = user_with_invalid_email.generate_password_reset_token
162
+
163
+ # EXECUTE: Attempt to send email to invalid address
164
+ email = AuthMailer.with(
165
+ user: user_with_invalid_email,
166
+ token: reset_token,
167
+ reset_url: "http://localhost:3000/reset?token=#{reset_token}"
168
+ ).password_reset
169
+
170
+ # VERIFY FUNCTIONALITY: Email should be generated even with invalid address
171
+ assert_not_nil email, "Email should be generated even with invalid email address"
172
+ assert_equal [user_with_invalid_email.email_address], email.to, "Email should contain the provided address"
173
+
174
+ # VERIFY BUSINESS LOGIC: System should not crash on delivery attempts
175
+ assert_nothing_raised "Email delivery should not raise exceptions in test environment" do
176
+ email.deliver_now
177
+ end
178
+ end
179
+
180
+ # BEHAVIOR: Test email template rendering with missing data
181
+ test "should handle missing user data gracefully in email templates" do
182
+ # SETUP: Create user with minimal data
183
+ minimal_user = User.create!(
184
+ email_address: "minimal@example.com",
185
+ username: "minimaluser",
186
+ password: "password123",
187
+ organization: @organization
188
+ # No first_name, last_name provided
189
+ )
190
+
191
+ reset_token = minimal_user.generate_password_reset_token
192
+
193
+ # EXECUTE: Generate email for user with minimal data
194
+ email = AuthMailer.with(
195
+ user: minimal_user,
196
+ token: reset_token,
197
+ reset_url: "http://localhost:3000/reset?token=#{reset_token}"
198
+ ).password_reset
199
+
200
+ # VERIFY FUNCTIONALITY: Email should be generated successfully
201
+ assert_not_nil email, "Email should be generated for user with minimal data"
202
+
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")
207
+ end
208
+
209
+ private
210
+
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\-_\.]+)/)
214
+ match ? match[1] : nil
215
+ end
216
+ end
@@ -0,0 +1,161 @@
1
+ # Preview all authentication emails
2
+ # Visit http://localhost:3000/rails/mailers/auth_mailer to see email previews
3
+ class AuthMailerPreview < ActionMailer::Preview
4
+
5
+ # Preview email confirmation
6
+ # http://localhost:3000/rails/mailers/auth_mailer/email_confirmation
7
+ def email_confirmation
8
+ user = sample_user
9
+ user.confirmation_token = SecureRandom.hex(32)
10
+ user.confirmation_sent_at = Time.current
11
+
12
+ AuthMailer.email_confirmation(user)
13
+ end
14
+
15
+ # Preview password reset email
16
+ # http://localhost:3000/rails/mailers/auth_mailer/password_reset
17
+ def password_reset
18
+ user = sample_user
19
+ token = JWT.encode(
20
+ {
21
+ user_id: user.id,
22
+ email_address: user.email_address,
23
+ type: 'password_reset',
24
+ exp: 15.minutes.from_now.to_i
25
+ },
26
+ Rails.application.secret_key_base
27
+ )
28
+
29
+ AuthMailer.with(
30
+ user: user,
31
+ token: token,
32
+ reset_url: "http://localhost:3000/auth/reset?token=#{token}"
33
+ ).password_reset
34
+ end
35
+
36
+ # Preview account unlock email
37
+ # http://localhost:3000/rails/mailers/auth_mailer/account_unlock
38
+ def account_unlock
39
+ user = sample_user
40
+ user.locked_at = Time.current
41
+ user.failed_login_attempts = 5
42
+
43
+ token = JWT.encode(
44
+ {
45
+ user_id: user.id,
46
+ email_address: user.email_address,
47
+ type: 'account_unlock',
48
+ exp: 1.hour.from_now.to_i
49
+ },
50
+ Rails.application.secret_key_base
51
+ )
52
+
53
+ AuthMailer.with(
54
+ user: user,
55
+ token: token,
56
+ unlock_url: "http://localhost:3000/auth/unlock?token=#{token}"
57
+ ).account_unlock
58
+ end
59
+
60
+ # Preview user invitation email
61
+ # http://localhost:3000/rails/mailers/auth_mailer/user_invitation
62
+ def user_invitation
63
+ invitation = sample_invitation
64
+ inviter = sample_user
65
+
66
+ token = JWT.encode(
67
+ {
68
+ invitation_id: invitation.id,
69
+ email_address: invitation.email_address,
70
+ type: 'invitation',
71
+ exp: 7.days.from_now.to_i
72
+ },
73
+ Rails.application.secret_key_base
74
+ )
75
+
76
+ AuthMailer.with(
77
+ invitation: invitation,
78
+ inviter: inviter,
79
+ token: token,
80
+ invitation_url: "http://localhost:3000/auth/invitation?token=#{token}"
81
+ ).user_invitation
82
+ end
83
+
84
+ # Preview welcome email
85
+ # http://localhost:3000/rails/mailers/auth_mailer/welcome
86
+ def welcome
87
+ user = sample_user
88
+ user.confirmed_at = Time.current
89
+
90
+ AuthMailer.with(user: user).welcome
91
+ end
92
+
93
+ # Preview email change confirmation
94
+ # http://localhost:3000/rails/mailers/auth_mailer/email_change_confirmation
95
+ def email_change_confirmation
96
+ user = sample_user
97
+ new_email = 'newemail@example.com'
98
+
99
+ token = JWT.encode(
100
+ {
101
+ user_id: user.id,
102
+ email_address: new_email,
103
+ type: 'email_change',
104
+ exp: 24.hours.from_now.to_i
105
+ },
106
+ Rails.application.secret_key_base
107
+ )
108
+
109
+ AuthMailer.with(
110
+ user: user,
111
+ new_email_address: new_email,
112
+ token: token,
113
+ confirmation_url: "http://localhost:3000/auth/confirm-email?token=#{token}"
114
+ ).email_change_confirmation
115
+ end
116
+
117
+ private
118
+
119
+ def sample_user
120
+ # Create a sample user for preview purposes
121
+ organization = sample_organization
122
+
123
+ User.new(
124
+ id: 1,
125
+ email_address: 'john.doe@example.com',
126
+ username: 'johndoe',
127
+ first_name: 'John',
128
+ last_name: 'Doe',
129
+ phone_number: '+1-555-123-4567',
130
+ organization: organization,
131
+ created_at: Time.current,
132
+ updated_at: Time.current
133
+ )
134
+ end
135
+
136
+ def sample_organization
137
+ # Create a sample organization for preview purposes
138
+ Organization.new(
139
+ id: 1,
140
+ name: 'Acme Corporation',
141
+ domain: 'acme.com',
142
+ created_at: Time.current,
143
+ updated_at: Time.current
144
+ )
145
+ end
146
+
147
+ def sample_invitation
148
+ # Create a sample invitation for preview purposes
149
+ organization = sample_organization
150
+
151
+ Invitation.new(
152
+ id: 1,
153
+ email_address: 'newuser@example.com',
154
+ organization: organization,
155
+ role: 'user',
156
+ status: 'pending',
157
+ created_at: Time.current,
158
+ updated_at: Time.current
159
+ )
160
+ end
161
+ end