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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +51 -2
- data/README.md +6 -6
- data/lib/generators/propel_authentication/install_generator.rb +135 -153
- data/lib/generators/propel_authentication/templates/application_mailer.rb +6 -0
- data/lib/generators/propel_authentication/templates/auth/passwords_controller.rb.tt +84 -78
- data/lib/generators/propel_authentication/templates/auth/signup_controller.rb.tt +242 -0
- data/lib/generators/propel_authentication/templates/{tokens_controller.rb.tt → auth/tokens_controller.rb.tt} +39 -22
- data/lib/generators/propel_authentication/templates/auth_mailer.rb +3 -1
- data/lib/generators/propel_authentication/templates/authenticatable.rb +8 -2
- data/lib/generators/propel_authentication/templates/concerns/confirmable.rb +1 -1
- data/lib/generators/propel_authentication/templates/concerns/lockable.rb +4 -2
- data/lib/generators/propel_authentication/templates/concerns/{propel_authentication.rb → propel_authentication_concern.rb} +33 -3
- data/lib/generators/propel_authentication/templates/concerns/recoverable.rb +16 -6
- data/lib/generators/propel_authentication/templates/core/configuration_methods.rb +104 -64
- data/lib/generators/propel_authentication/templates/db/seeds.rb +50 -4
- data/lib/generators/propel_authentication/templates/doc/signup_flow.md +315 -0
- data/lib/generators/propel_authentication/templates/models/agency.rb.tt +13 -0
- data/lib/generators/propel_authentication/templates/models/agent.rb.tt +13 -0
- data/lib/generators/propel_authentication/templates/{invitation.rb → models/invitation.rb.tt} +6 -0
- data/lib/generators/propel_authentication/templates/models/organization.rb.tt +12 -0
- data/lib/generators/propel_authentication/templates/{user.rb → models/user.rb.tt} +5 -0
- data/lib/generators/propel_authentication/templates/propel_authentication.rb.tt +94 -9
- data/lib/generators/propel_authentication/templates/routes/auth_routes.rb.tt +55 -0
- data/lib/generators/propel_authentication/templates/services/auth_notification_service.rb +3 -3
- data/lib/generators/propel_authentication/templates/test/concerns/confirmable_test.rb.tt +34 -10
- data/lib/generators/propel_authentication/templates/test/concerns/propel_authentication_test.rb.tt +1 -1
- data/lib/generators/propel_authentication/templates/test/concerns/recoverable_test.rb.tt +4 -4
- data/lib/generators/propel_authentication/templates/test/controllers/auth/lockable_integration_test.rb.tt +18 -15
- data/lib/generators/propel_authentication/templates/test/controllers/auth/password_reset_integration_test.rb.tt +38 -40
- data/lib/generators/propel_authentication/templates/test/controllers/auth/signup_controller_test.rb.tt +201 -0
- data/lib/generators/propel_authentication/templates/test/controllers/auth/tokens_controller_test.rb.tt +33 -25
- data/lib/generators/propel_authentication/templates/test/mailers/auth_mailer_test.rb.tt +51 -36
- data/lib/generators/propel_authentication/templates/views/auth_mailer/email_confirmation.html.erb +2 -2
- data/lib/generators/propel_authentication/templates/views/auth_mailer/email_confirmation.text.erb +1 -1
- data/lib/generators/propel_authentication/test/generators/authentication/install_generator_test.rb +4 -4
- data/lib/generators/propel_authentication/test/generators/authentication/uninstall_generator_test.rb +1 -1
- data/lib/generators/propel_authentication/test/integration/generator_integration_test.rb +1 -1
- data/lib/generators/propel_authentication/test/integration/multi_version_generator_test.rb +13 -12
- data/lib/generators/propel_authentication/unpack_generator.rb +19 -15
- data/lib/propel_authentication.rb +1 -1
- metadata +14 -11
- data/lib/generators/propel_authentication/templates/agency.rb +0 -7
- data/lib/generators/propel_authentication/templates/agent.rb +0 -7
- data/lib/generators/propel_authentication/templates/auth/base_passwords_controller.rb.tt +0 -99
- data/lib/generators/propel_authentication/templates/auth/base_tokens_controller.rb.tt +0 -90
- 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 <%=
|
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 <%=
|
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 <%=
|
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 <%=
|
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 <%=
|
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 <%=
|
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 <%=
|
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 <%=
|
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 <%=
|
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 <%=
|
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 <%=
|
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 <%=
|
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 <%=
|
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 <%=
|
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 <%=
|
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 <%=
|
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 <%=
|
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 <%=
|
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 <%=
|
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 <%=
|
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 <%=
|
225
|
+
test "POST <%= auth_route_prefix %>/login validates required parameters" do
|
218
226
|
# BEHAVIOR TEST: Verify parameter validation
|
219
|
-
post <%=
|
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 <%=
|
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 <%=
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
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
|
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
|
-
|
205
|
-
|
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
|
212
|
-
def
|
213
|
-
|
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
|
data/lib/generators/propel_authentication/templates/views/auth_mailer/email_confirmation.html.erb
CHANGED
@@ -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
|
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
|
179
|
+
<p class="token-code"><%= @confirmation_url %></p>
|
180
180
|
</div>
|
181
181
|
|
182
182
|
<!-- Security information -->
|
data/lib/generators/propel_authentication/templates/views/auth_mailer/email_confirmation.text.erb
CHANGED
@@ -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
|
10
|
+
<%= @confirmation_url %>
|
11
11
|
|
12
12
|
This confirmation link will expire in 24 hours for security reasons.
|
13
13
|
|
data/lib/generators/propel_authentication/test/generators/authentication/install_generator_test.rb
CHANGED
@@ -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
|
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
|
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
|
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
|
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"))
|
data/lib/generators/propel_authentication/test/generators/authentication/uninstall_generator_test.rb
CHANGED
@@ -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/
|
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
|
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
|
20
|
-
run_generator ["--
|
19
|
+
test "generator creates dynamic controller structure with proper namespace configuration" do
|
20
|
+
run_generator ["--namespace=api", "--version=v1"]
|
21
21
|
|
22
|
-
# Verify
|
23
|
-
assert_file "app/controllers/api/
|
24
|
-
assert_match(/class Api::
|
25
|
-
assert_match(/POST \/api
|
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/
|
33
|
-
assert_match(/class Api::
|
34
|
-
assert_match(/POST \/api
|
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
|
40
|
+
# Verify routes are created with proper namespacing and as: options
|
41
41
|
assert_file "config/routes.rb" do |content|
|
42
|
-
assert_match(
|
43
|
-
assert_match(/
|
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)
|