practical 0.1.0 → 3.0.0.pre.alpha1

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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -4
  3. data/app/components/practical/views/flash_messages_component.rb +0 -1
  4. data/app/components/practical/views/form/fallback_errors_section_component.rb +5 -3
  5. data/app/components/practical/views/form/option_label_component.rb +0 -1
  6. data/app/components/practical/views/navigation/breadcrumb_item_component.rb +0 -1
  7. data/app/components/practical/views/navigation/breadcrumbs_component.rb +2 -1
  8. data/app/{controllers/concerns/practical/auth/passkeys → concerns/practical/auth/passkeys/controllers}/emergency_registrations.rb +2 -2
  9. data/app/{controllers/concerns/practical/auth/passkeys → concerns/practical/auth/passkeys/controllers}/web_authn_debug_context.rb +1 -1
  10. data/app/concerns/practical/memberships/controllers/membership_invitations/register_with_passkey.rb +92 -0
  11. data/app/lib/practical/forms/datatables/base.rb +80 -0
  12. data/app/lib/practical/loaders/base.rb +44 -0
  13. data/app/lib/practical/relation_builders/base.rb +35 -0
  14. data/app/lib/practical/test/shared/attachment/models/attachment/base.rb +123 -0
  15. data/app/lib/practical/test/shared/attachment/models/attachment/for_organization.rb +39 -0
  16. data/app/lib/practical/test/shared/attachment/models/organization/has_attachments.rb +12 -0
  17. data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/base.rb +9 -6
  18. data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/cross_pollination.rb +49 -0
  19. data/app/lib/practical/test/shared/auth/passkeys/controllers/passkey_management/base.rb +508 -0
  20. data/app/lib/practical/test/shared/auth/passkeys/controllers/reauthentication/base.rb +27 -9
  21. data/app/lib/practical/test/shared/auth/passkeys/controllers/reauthentication/cross_pollination.rb +19 -0
  22. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_destroy.rb +26 -8
  23. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_signup.rb +3 -2
  24. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/update.rb +55 -19
  25. data/app/lib/practical/test/shared/auth/passkeys/controllers/sessions/cross_pollination.rb +29 -0
  26. data/app/lib/practical/test/shared/auth/passkeys/forms/emergency_registration.rb +0 -1
  27. data/app/lib/practical/test/shared/auth/passkeys/models/{passkey.rb → passkey/base.rb} +1 -1
  28. data/app/lib/practical/test/shared/auth/passkeys/models/passkey/emergency_registration.rb +23 -0
  29. data/app/lib/practical/test/shared/auth/passkeys/models/{resource_with_passkeys.rb → resource_with_passkeys/base.rb} +1 -1
  30. data/app/lib/practical/test/shared/auth/passkeys/models/resource_with_passkeys/emergency_registration.rb +41 -0
  31. data/app/lib/practical/test/shared/memberships/controllers/membership_invitations/base.rb +165 -0
  32. data/app/lib/practical/test/shared/memberships/controllers/membership_invitations/register_with_passkey.rb +417 -0
  33. data/app/lib/practical/test/shared/memberships/controllers/organization/membership.rb +400 -0
  34. data/app/lib/practical/test/shared/memberships/controllers/organization/membership_invitation.rb +148 -0
  35. data/app/lib/practical/test/shared/memberships/controllers/user/membership.rb +119 -0
  36. data/app/lib/practical/test/shared/memberships/controllers/user/membership_invitation.rb +57 -0
  37. data/app/lib/practical/test/shared/memberships/forms/create_new_user_with_membership_invitation.rb +197 -0
  38. data/app/lib/practical/test/shared/memberships/forms/organization/membership.rb +162 -0
  39. data/app/lib/practical/test/shared/memberships/forms/organization/new_membership_invitation.rb +195 -0
  40. data/app/lib/practical/test/shared/memberships/forms/user/membership.rb +87 -0
  41. data/app/lib/practical/test/shared/memberships/models/membership/base.rb +45 -0
  42. data/app/lib/practical/test/shared/memberships/models/membership_invitation/base.rb +85 -0
  43. data/app/lib/practical/test/shared/memberships/models/membership_invitation/sending.rb +76 -0
  44. data/app/lib/practical/test/shared/memberships/models/membership_invitation/use_for_and_notify.rb +55 -0
  45. data/app/lib/practical/test/shared/memberships/models/organization/base.rb +25 -0
  46. data/app/lib/practical/test/shared/memberships/models/user/base.rb +23 -0
  47. data/app/lib/practical/test/shared/memberships/policies/organization/base_resource.rb +29 -0
  48. data/app/lib/practical/test/shared/memberships/policies/organization/membership.rb +103 -0
  49. data/app/lib/practical/test/shared/memberships/policies/organization/membership_invitation.rb +94 -0
  50. data/app/lib/practical/test/shared/memberships/policies/organization/resource/inherits.rb +10 -0
  51. data/app/lib/practical/test/shared/memberships/policies/organization.rb +70 -0
  52. data/app/lib/practical/test/shared/memberships/policies/user/membership.rb +78 -0
  53. data/app/lib/practical/test/shared/memberships/policies/user/membership_invitation.rb +31 -0
  54. data/app/lib/practical/test/shared/models/normalized_email.rb +0 -1
  55. data/app/lib/practical/test/shared/policies/user/base.rb +14 -0
  56. data/app/lib/practical/views/error_handling.rb +2 -0
  57. data/app/lib/practical/views/error_response.rb +27 -0
  58. data/app/lib/practical/views/form_builders/base.rb +5 -4
  59. data/app/lib/practical/views/form_builders/collection_option.rb +5 -0
  60. data/app/lib/practical/views/icon_set.rb +12 -6
  61. data/config/locales/auth.en.yml +18 -0
  62. data/config/locales/memberships.en.yml +129 -0
  63. data/db/seeds/memberships/default.rb +68 -0
  64. data/db/seeds/moderators/default.rb +36 -0
  65. data/db/seeds/setup.rb +16 -0
  66. data/db/seeds/test/cases/membership_invitations.rb +31 -0
  67. data/db/seeds/users/default.rb +17 -15
  68. data/lib/generators/practical/test/shared_test/shared_test_generator.rb +2 -0
  69. data/lib/practical/framework/engine.rb +8 -0
  70. data/lib/practical/helpers/honeybadger_helper.rb +11 -0
  71. data/lib/practical/helpers/selector_helper.rb +8 -0
  72. data/lib/practical/version.rb +1 -1
  73. data/lib/practical/views/element_helper.rb +2 -0
  74. data/lib/practical/views/theme_helper.rb +13 -0
  75. data/lib/practical.rb +4 -1
  76. data/lib/tasks/practical/utility.rake +20 -0
  77. metadata +54 -11
  78. data/lib/tasks/practical/framework_tasks.rake +0 -6
@@ -10,14 +10,18 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Registrations::Self
10
10
  client = webauthn_client
11
11
  create_passkey_for_user_and_return_webauthn_credential(user: resource_instance)
12
12
 
13
- new_reauthentication_challenge_action
13
+ assert_reauthentication_challenge_authorized do
14
+ new_reauthentication_challenge_action
15
+ end
14
16
  assert_response :ok
15
17
  assert_reauthentication_token_challenge
16
18
 
17
19
  challenge = response.parsed_body["challenge"]
18
20
  credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
19
21
 
20
- reauthenticate_action(params: {passkey_credential: credential.to_json})
22
+ assert_reauthentication_authorized do
23
+ reauthenticate_action(params: {passkey_credential: credential.to_json})
24
+ end
21
25
  assert_response :ok
22
26
  assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
23
27
  assert_nil expected_stored_reauthentication_challenge
@@ -28,9 +32,11 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Registrations::Self
28
32
  resource_id = resource_instance.id
29
33
 
30
34
  assert_difference "#{resource_class}.count", -1 do
35
+ assert_destroy_authorized do
31
36
  destroy_registration_action(params: params)
32
37
  assert_redirected_to destroy_success_url
33
38
  end
39
+ end
34
40
 
35
41
  assert_nil resource_class.find_by(id: resource_id)
36
42
  end
@@ -41,14 +47,18 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Registrations::Self
41
47
  client = webauthn_client
42
48
  create_passkey_for_user_and_return_webauthn_credential(user: resource_instance)
43
49
 
44
- new_reauthentication_challenge_action
50
+ assert_reauthentication_challenge_authorized do
51
+ new_reauthentication_challenge_action
52
+ end
45
53
  assert_response :ok
46
54
  assert_reauthentication_token_challenge
47
55
 
48
56
  challenge = response.parsed_body["challenge"]
49
57
  credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
50
58
 
51
- reauthenticate_action(params: {passkey_credential: credential.to_json})
59
+ assert_reauthentication_authorized do
60
+ reauthenticate_action(params: {passkey_credential: credential.to_json})
61
+ end
52
62
  assert_response :ok
53
63
  assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
54
64
  assert_nil expected_stored_reauthentication_challenge
@@ -73,14 +83,18 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Registrations::Self
73
83
  client = webauthn_client
74
84
  create_passkey_for_user_and_return_webauthn_credential(user: resource_instance)
75
85
 
76
- new_reauthentication_challenge_action
86
+ assert_reauthentication_challenge_authorized do
87
+ new_reauthentication_challenge_action
88
+ end
77
89
  assert_response :ok
78
90
  assert_reauthentication_token_challenge
79
91
 
80
92
  challenge = response.parsed_body["challenge"]
81
93
  credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
82
94
 
83
- reauthenticate_action(params: {passkey_credential: credential.to_json})
95
+ assert_reauthentication_authorized do
96
+ reauthenticate_action(params: {passkey_credential: credential.to_json})
97
+ end
84
98
  assert_response :ok
85
99
  assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
86
100
  assert_nil expected_stored_reauthentication_challenge
@@ -105,14 +119,18 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Registrations::Self
105
119
  client = webauthn_client
106
120
  create_passkey_for_user_and_return_webauthn_credential(user: resource_instance)
107
121
 
108
- new_reauthentication_challenge_action
122
+ assert_reauthentication_challenge_authorized do
123
+ new_reauthentication_challenge_action
124
+ end
109
125
  assert_response :ok
110
126
  assert_reauthentication_token_challenge
111
127
 
112
128
  challenge = response.parsed_body["challenge"]
113
129
  credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
114
130
 
115
- reauthenticate_action(params: {passkey_credential: credential.to_json})
131
+ assert_reauthentication_authorized do
132
+ reauthenticate_action(params: {passkey_credential: credential.to_json})
133
+ end
116
134
  assert_response :ok
117
135
  assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
118
136
  assert_nil expected_stored_reauthentication_challenge
@@ -80,7 +80,8 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Registrations::Self
80
80
  end
81
81
 
82
82
  new_resource = resource_class.last
83
- credential = hydrate_response_from_raw_credential(client: client, relying_party: webauthn_relying_party, raw_credential: raw_credential).credential
83
+ credential = hydrate_response_from_raw_credential(client: client, relying_party: webauthn_relying_party,
84
+ raw_credential: raw_credential).credential
84
85
 
85
86
  new_passkey = new_resource.passkeys.last
86
87
  assert_equal passkey_label, new_passkey.label
@@ -205,7 +206,7 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Registrations::Self
205
206
 
206
207
  client = webauthn_client
207
208
  challenge = expected_stored_challenge
208
- raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
209
+ create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
209
210
 
210
211
  params = params_for_registration(email: email, passkey_label: passkey_label, raw_credential: " ")
211
212
 
@@ -3,16 +3,20 @@
3
3
  module Practical::Test::Shared::Auth::Passkeys::Controllers::Registrations::Update
4
4
  extend ActiveSupport::Concern
5
5
 
6
- included do
6
+ included do
7
7
  test "edit renders successfully for the resource" do
8
8
  sign_in_as_resource
9
- edit_registration_action
9
+ assert_edit_authorized do
10
+ edit_registration_action
11
+ end
10
12
  assert_response :ok
11
13
  end
12
14
 
13
15
  test "edit does not render a different resource" do
14
16
  sign_in_as_resource
15
- attempt_to_edit_other_resource_action
17
+ assert_edit_authorized do
18
+ attempt_to_edit_other_resource_action
19
+ end
16
20
  assert_response :ok
17
21
  assert_not_includes response.body, other_resource.email
18
22
  end
@@ -23,14 +27,18 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Registrations::Upda
23
27
  client = webauthn_client
24
28
  create_passkey_for_user_and_return_webauthn_credential(user: resource_instance)
25
29
 
26
- new_reauthentication_challenge_action
30
+ assert_reauthentication_challenge_authorized do
31
+ new_reauthentication_challenge_action
32
+ end
27
33
  assert_response :ok
28
34
  assert_reauthentication_token_challenge
29
35
 
30
36
  challenge = response.parsed_body["challenge"]
31
37
  credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
32
38
 
33
- reauthenticate_action(params: {passkey_credential: credential.to_json})
39
+ assert_reauthentication_authorized do
40
+ reauthenticate_action(params: {passkey_credential: credential.to_json})
41
+ end
34
42
  assert_response :ok
35
43
  assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
36
44
  assert_nil expected_stored_reauthentication_challenge
@@ -41,8 +49,10 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Registrations::Upda
41
49
  params = params_for_updating_resource(email: new_email, reauthentication_token: reauthentication_token)
42
50
 
43
51
  assert_no_difference "#{resource_class}.count" do
52
+ assert_update_authorized do
44
53
  update_registration_action(params: params)
45
- assert_json_redirected_to expected_update_success_url
54
+ assert_update_redirect
55
+ end
46
56
  end
47
57
 
48
58
  resource_instance.reload
@@ -55,14 +65,18 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Registrations::Upda
55
65
  client = webauthn_client
56
66
  create_passkey_for_user_and_return_webauthn_credential(user: resource_instance)
57
67
 
58
- new_reauthentication_challenge_action
68
+ assert_reauthentication_challenge_authorized do
69
+ new_reauthentication_challenge_action
70
+ end
59
71
  assert_response :ok
60
72
  assert_reauthentication_token_challenge
61
73
 
62
74
  challenge = response.parsed_body["challenge"]
63
75
  credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
64
76
 
65
- reauthenticate_action(params: {passkey_credential: credential.to_json})
77
+ assert_reauthentication_authorized do
78
+ reauthenticate_action(params: {passkey_credential: credential.to_json})
79
+ end
66
80
  assert_response :ok
67
81
  assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
68
82
  assert_nil expected_stored_reauthentication_challenge
@@ -89,14 +103,18 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Registrations::Upda
89
103
  client = webauthn_client
90
104
  create_passkey_for_user_and_return_webauthn_credential(user: resource_instance)
91
105
 
92
- new_reauthentication_challenge_action
106
+ assert_reauthentication_challenge_authorized do
107
+ new_reauthentication_challenge_action
108
+ end
93
109
  assert_response :ok
94
110
  assert_reauthentication_token_challenge
95
111
 
96
112
  challenge = response.parsed_body["challenge"]
97
113
  credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
98
114
 
99
- reauthenticate_action(params: {passkey_credential: credential.to_json})
115
+ assert_reauthentication_authorized do
116
+ reauthenticate_action(params: {passkey_credential: credential.to_json})
117
+ end
100
118
  assert_response :ok
101
119
  assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
102
120
  assert_nil expected_stored_reauthentication_challenge
@@ -123,14 +141,18 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Registrations::Upda
123
141
  client = webauthn_client
124
142
  create_passkey_for_user_and_return_webauthn_credential(user: resource_instance)
125
143
 
126
- new_reauthentication_challenge_action
144
+ assert_reauthentication_challenge_authorized do
145
+ new_reauthentication_challenge_action
146
+ end
127
147
  assert_response :ok
128
148
  assert_reauthentication_token_challenge
129
149
 
130
150
  challenge = response.parsed_body["challenge"]
131
151
  credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
132
152
 
133
- reauthenticate_action(params: {passkey_credential: credential.to_json})
153
+ assert_reauthentication_authorized do
154
+ reauthenticate_action(params: {passkey_credential: credential.to_json})
155
+ end
134
156
  assert_response :ok
135
157
  assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
136
158
  assert_nil expected_stored_reauthentication_challenge
@@ -142,9 +164,11 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Registrations::Upda
142
164
  params = params_for_updating_resource(email: new_email, reauthentication_token: reauthentication_token)
143
165
 
144
166
  assert_no_difference "#{resource_class}.count" do
167
+ assert_update_authorized do
145
168
  update_registration_action(params: params)
146
169
  assert_response :unprocessable_entity
147
- assert_form_error_for_email(message: "can't be blank")
170
+ assert_form_error_for_blank_email
171
+ end
148
172
  end
149
173
 
150
174
  resource_instance.reload
@@ -157,14 +181,18 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Registrations::Upda
157
181
  client = webauthn_client
158
182
  create_passkey_for_user_and_return_webauthn_credential(user: resource_instance)
159
183
 
160
- new_reauthentication_challenge_action
184
+ assert_reauthentication_challenge_authorized do
185
+ new_reauthentication_challenge_action
186
+ end
161
187
  assert_response :ok
162
188
  assert_reauthentication_token_challenge
163
189
 
164
190
  challenge = response.parsed_body["challenge"]
165
191
  credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
166
192
 
167
- reauthenticate_action(params: {passkey_credential: credential.to_json})
193
+ assert_reauthentication_authorized do
194
+ reauthenticate_action(params: {passkey_credential: credential.to_json})
195
+ end
168
196
  assert_response :ok
169
197
  assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
170
198
  assert_nil expected_stored_reauthentication_challenge
@@ -176,9 +204,11 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Registrations::Upda
176
204
  params = params_for_updating_resource(email: new_email, reauthentication_token: reauthentication_token)
177
205
 
178
206
  assert_no_difference "#{resource_class}.count" do
207
+ assert_update_authorized do
179
208
  update_registration_action(params: params)
180
209
  assert_response :unprocessable_entity
181
- assert_form_error_for_email(message: "has already been taken")
210
+ assert_form_error_for_taken_email
211
+ end
182
212
  end
183
213
 
184
214
  resource_instance.reload
@@ -191,14 +221,18 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Registrations::Upda
191
221
  client = webauthn_client
192
222
  create_passkey_for_user_and_return_webauthn_credential(user: resource_instance)
193
223
 
194
- new_reauthentication_challenge_action
224
+ assert_reauthentication_challenge_authorized do
225
+ new_reauthentication_challenge_action
226
+ end
195
227
  assert_response :ok
196
228
  assert_reauthentication_token_challenge
197
229
 
198
230
  challenge = response.parsed_body["challenge"]
199
231
  credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
200
232
 
201
- reauthenticate_action(params: {passkey_credential: credential.to_json})
233
+ assert_reauthentication_authorized do
234
+ reauthenticate_action(params: {passkey_credential: credential.to_json})
235
+ end
202
236
  assert_response :ok
203
237
  assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
204
238
  assert_nil expected_stored_reauthentication_challenge
@@ -209,8 +243,10 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Registrations::Upda
209
243
  params = params_trying_to_update_other_resource(email: new_email, reauthentication_token: reauthentication_token)
210
244
 
211
245
  assert_no_difference "#{resource_class}.count" do
246
+ assert_update_authorized do
212
247
  update_registration_action(params: params)
213
- assert_json_redirected_to expected_update_success_url
248
+ assert_update_redirect
249
+ end
214
250
  end
215
251
 
216
252
  resource_instance.reload
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Auth::Passkeys::Controllers::Sessions::CrossPollination
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "create: does not allow another resource to log in" do
8
+ sign_in_as_other_resource
9
+
10
+ issue_new_challenge_action
11
+ assert_response :ok
12
+
13
+ assert_passkey_authentication_challenge(
14
+ data: response.parsed_body,
15
+ stored_challenge: expected_stored_challenge,
16
+ credentials_to_allow: []
17
+ )
18
+
19
+ challenge = response.parsed_body["challenge"]
20
+ credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
21
+
22
+ authenticate_action(params: {resource_key => {passkey_credential: credential.to_json}})
23
+ assert_response :unprocessable_entity
24
+
25
+ assert_nil get_session_challenge
26
+ assert_resource_not_signed_in
27
+ end
28
+ end
29
+ end
@@ -75,7 +75,6 @@ module Practical::Test::Shared::Auth::Passkeys::Forms::EmergencyRegistration
75
75
  assert_equal new_passkey, emergency_registration.passkey
76
76
  assert_equal time.to_formatted_s(:db), emergency_registration.used_at.to_formatted_s(:db)
77
77
 
78
-
79
78
  assert_new_passkey_email(new_passkey: new_passkey)
80
79
  end
81
80
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Practical::Test::Shared::Auth::Passkeys::Models::Passkey
3
+ module Practical::Test::Shared::Auth::Passkeys::Models::Passkey::Base
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Auth::Passkeys::Models::Passkey::EmergencyRegistration
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "has_one: emergency_passkey_registration, dependent: :destroy" do
8
+ reflection = model_class.reflect_on_association(emergency_passkey_registration_reflection_name)
9
+ assert_equal :has_one, reflection.macro
10
+ assert_equal :destroy, reflection.options[:dependent]
11
+ end
12
+
13
+ test "can destroy a passkey if it has an emergency_passkey_registration" do
14
+ instance = model_instance_with_emergency_registration
15
+
16
+ assert_not_nil instance.emergency_passkey_registration
17
+
18
+ assert_difference "#{model_class}.count", -1 do
19
+ instance.destroy!
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Practical::Test::Shared::Auth::Passkeys::Models::ResourceWithPasskeys
3
+ module Practical::Test::Shared::Auth::Passkeys::Models::ResourceWithPasskeys::Base
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Auth::Passkeys::Models::ResourceWithPasskeys::EmergencyRegistration
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "email: is required and cannot be blank" do
8
+ instance = model_instance
9
+ instance.email = ""
10
+ assert_equal false, instance.valid?
11
+ assert_equal true, instance.errors.of_kind?(:email, :blank)
12
+
13
+ instance.email = Faker::Internet.email
14
+ assert_equal true, instance.valid?
15
+ end
16
+
17
+ test "email: is unique" do
18
+ instance = model_class.new(email: model_instance.email)
19
+ assert_equal false, instance.valid?
20
+ assert_equal true, instance.errors.of_kind?(:email, :taken)
21
+
22
+ instance.email = Faker::Internet.email
23
+ instance.valid?
24
+
25
+ assert_equal false, instance.errors.of_kind?(:email, :taken)
26
+ end
27
+
28
+ test "has many emergency_passkey_registrations" do
29
+ reflection = model_class.reflect_on_association(:emergency_passkey_registrations)
30
+ assert_equal :has_many, reflection.macro
31
+
32
+ assert_difference "#{emergency_passkey_registration_class}.count", +1 do
33
+ new_emergency_registration = model_instance.emergency_passkey_registrations.create!
34
+
35
+ assert_instance_of emergency_passkey_registration_class, new_emergency_registration
36
+
37
+ assert_includes model_instance.emergency_passkey_registrations, new_emergency_registration
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Memberships::Controllers::MembershipInvitations::Base
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "show: renders successfully when given a visible, unused membership_invitation token" do
8
+ show_membership_invitation_action(token: visible_unused_token)
9
+ assert_response :ok
10
+ end
11
+
12
+ test "show: returns 404 if a used membership_invitation token is given" do
13
+ show_membership_invitation_action(token: used_token)
14
+ assert_response :not_found
15
+ end
16
+
17
+ test "show: returns 404 if a hidden membership_invitation token is given" do
18
+ show_membership_invitation_action(token: hidden_token)
19
+ assert_response :not_found
20
+ end
21
+
22
+ test "show: raises ActiveSupport::MessageVerifier::InvalidSignature if a bad token is given" do
23
+ assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
24
+ show_membership_invitation_action(token: bad_token)
25
+ end
26
+ end
27
+
28
+ test "show: raises ActiveSupport::MessageVerifier::InvalidSignature if a raw token is given" do
29
+ assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
30
+ show_membership_invitation_action(token: raw_token)
31
+ end
32
+ end
33
+
34
+ test "sign_out_then_show: signs out the resource, but stores the path and redirects them" do
35
+ sign_in_as_resource
36
+
37
+ sign_out_then_show_action(token: visible_unused_token)
38
+ assert_redirected_to_invitation(token: visible_unused_token)
39
+ end
40
+
41
+ test "sign_out_then_show: returns 404 if a used membership_invitation token is given" do
42
+ sign_in_as_resource
43
+
44
+ sign_out_then_show_action(token: used_token)
45
+ assert_response :not_found
46
+ end
47
+
48
+ test "sign_out_then_show: returns 404 if a hidden membership_invitation token is given" do
49
+ sign_in_as_resource
50
+
51
+ sign_out_then_show_action(token: hidden_token)
52
+ assert_response :not_found
53
+ end
54
+
55
+ test "sign_out_then_show: raises ActiveSupport::MessageVerifier::InvalidSignature if a bad token is given" do
56
+ sign_in_as_resource
57
+
58
+ assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
59
+ sign_out_then_show_action(token: bad_token)
60
+ end
61
+ end
62
+
63
+ test "sign_out_then_show: raises ActiveSupport::MessageVerifier::InvalidSignature if a raw token is given" do
64
+ sign_in_as_resource
65
+
66
+ assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
67
+ sign_out_then_show_action(token: raw_token)
68
+ end
69
+ end
70
+
71
+ test "accept_as_current_user: links the invitation to a given resource, creating the final membership" do
72
+ sign_in_as_resource
73
+
74
+ assert_difference "#{membership_class}.count", +1 do
75
+ accept_as_current_user_action(token: visible_unused_token)
76
+ end
77
+
78
+ assert_redirected_to_organization
79
+ assert_accepted_invitation_flash_message
80
+
81
+ assert_membership_accepted_for_resource(resource: resource_instance)
82
+ end
83
+
84
+ test "accept_as_current_user: links the invitation to a given resource, even if the email does not match" do
85
+ sign_in_as_resource_with_different_email
86
+
87
+ assert_difference "#{membership_class}.count", +1 do
88
+ accept_as_current_user_action(token: visible_unused_token)
89
+ end
90
+
91
+ assert_redirected_to_organization
92
+ assert_accepted_invitation_flash_message
93
+
94
+ assert_membership_accepted_for_resource(resource: resource_instance_with_different_email)
95
+ end
96
+
97
+ test "accept_as_current_user: raises an error and does not change the invitation if the resource is already a member of this organization" do
98
+ sign_in_as_resource_already_in_organization
99
+
100
+ assert_no_difference "#{membership_class}.count" do
101
+ accept_as_current_user_action(token: visible_unused_token)
102
+ end
103
+
104
+ assert_response :unprocessable_entity
105
+ assert_taken_flash_message
106
+
107
+ assert_membership_invitation_unclaimed
108
+ end
109
+
110
+ test "accept_as_current_user: returns 404 if the resource is not signed in" do
111
+ assert_no_difference "#{membership_class}.count" do
112
+ accept_as_current_user_action(token: visible_unused_token)
113
+ end
114
+
115
+ assert_response :not_found
116
+ assert_membership_invitation_unclaimed
117
+ end
118
+
119
+ test "accept_as_current_user: returns 404 if a used membership_invitation token is given" do
120
+ sign_in_as_resource
121
+
122
+ assert_no_difference "#{membership_class}.count" do
123
+ accept_as_current_user_action(token: used_token)
124
+ end
125
+
126
+ assert_response :not_found
127
+ assert_membership_invitation_unclaimed
128
+ end
129
+
130
+ test "accept_as_current_user: returns 404 if a hidden membership_invitation token is given" do
131
+ sign_in_as_resource
132
+
133
+ assert_no_difference "#{membership_class}.count" do
134
+ accept_as_current_user_action(token: hidden_token)
135
+ end
136
+
137
+ assert_response :not_found
138
+ assert_membership_invitation_unclaimed
139
+ end
140
+
141
+ test "accept_as_current_user: raises ActiveSupport::MessageVerifier::InvalidSignature if a bad token is given" do
142
+ sign_in_as_resource
143
+
144
+ assert_no_difference "#{membership_class}.count" do
145
+ assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
146
+ accept_as_current_user_action(token: bad_token)
147
+ end
148
+ end
149
+
150
+ assert_membership_invitation_unclaimed
151
+ end
152
+
153
+ test "accept_as_current_user: raises ActiveSupport::MessageVerifier::InvalidSignature if a raw token is given" do
154
+ sign_in_as_resource
155
+
156
+ assert_no_difference "#{membership_class}.count" do
157
+ assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
158
+ accept_as_current_user_action(token: raw_token)
159
+ end
160
+ end
161
+
162
+ assert_membership_invitation_unclaimed
163
+ end
164
+ end
165
+ end