practical 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +37 -0
  3. data/Rakefile +10 -0
  4. data/app/components/practical/views/base_component.rb +6 -0
  5. data/app/components/practical/views/button_component.rb +27 -0
  6. data/app/components/practical/views/datatable/filter_applied.rb +25 -0
  7. data/app/components/practical/views/datatable/filter_section_component.html.erb +9 -0
  8. data/app/components/practical/views/datatable/filter_section_component.rb +19 -0
  9. data/app/components/practical/views/datatable/sort_link_component.rb +48 -0
  10. data/app/components/practical/views/datatable.rb +36 -0
  11. data/app/components/practical/views/flash_messages_component.rb +65 -0
  12. data/app/components/practical/views/form/error_list_component.rb +15 -0
  13. data/app/components/practical/views/form/error_list_item_component.rb +20 -0
  14. data/app/components/practical/views/form/error_list_item_template_component.rb +9 -0
  15. data/app/components/practical/views/form/fallback_errors_section_component.html.erb +7 -0
  16. data/app/components/practical/views/form/fallback_errors_section_component.rb +21 -0
  17. data/app/components/practical/views/form/field_errors_component.rb +28 -0
  18. data/app/components/practical/views/form/field_title_component.rb +23 -0
  19. data/app/components/practical/views/form/fieldset_title_component.rb +20 -0
  20. data/app/components/practical/views/form/input_component.html.erb +7 -0
  21. data/app/components/practical/views/form/input_component.rb +22 -0
  22. data/app/components/practical/views/form/option_label_component.rb +21 -0
  23. data/app/components/practical/views/form/practical_editor_component.rb +26 -0
  24. data/app/components/practical/views/form/required_radio_collection_wrapper_component.rb +23 -0
  25. data/app/components/practical/views/form_wrapper.rb +21 -0
  26. data/app/components/practical/views/icon_component.rb +36 -0
  27. data/app/components/practical/views/icon_for_file_extension_component.rb +53 -0
  28. data/app/components/practical/views/modal_dialog_component.html.erb +10 -0
  29. data/app/components/practical/views/modal_dialog_component.rb +16 -0
  30. data/app/components/practical/views/navigation/breadcrumb_item_component.rb +20 -0
  31. data/app/components/practical/views/navigation/breadcrumbs_component.html.erb +31 -0
  32. data/app/components/practical/views/navigation/breadcrumbs_component.rb +41 -0
  33. data/app/components/practical/views/navigation/navigation_link_component.rb +39 -0
  34. data/app/components/practical/views/navigation/pagination/goto_form_component.html.erb +31 -0
  35. data/app/components/practical/views/navigation/pagination/goto_form_component.rb +34 -0
  36. data/app/components/practical/views/navigation/pagination_component.html.erb +11 -0
  37. data/app/components/practical/views/navigation/pagination_component.rb +98 -0
  38. data/app/components/practical/views/open_dialog_button_component.rb +16 -0
  39. data/app/components/practical/views/page_component.html.erb +53 -0
  40. data/app/components/practical/views/page_component.rb +12 -0
  41. data/app/components/practical/views/relative_time_component.rb +13 -0
  42. data/app/components/practical/views/tiptap_document_component.rb +311 -0
  43. data/app/components/practical/views/toast_component.html.erb +26 -0
  44. data/app/components/practical/views/toast_component.rb +19 -0
  45. data/app/controllers/concerns/practical/auth/passkeys/emergency_registrations.rb +57 -0
  46. data/app/controllers/concerns/practical/auth/passkeys/web_authn_debug_context.rb +13 -0
  47. data/app/controllers/concerns/practical/views/flash_helpers.rb +37 -0
  48. data/app/controllers/concerns/practical/views/json_redirection.rb +7 -0
  49. data/app/lib/practical/defaults/shrine.rb +48 -0
  50. data/app/lib/practical/test/helpers/administrator/test_helpers.rb +7 -0
  51. data/app/lib/practical/test/helpers/extra_assertions.rb +7 -0
  52. data/app/lib/practical/test/helpers/flash_assertions.rb +8 -0
  53. data/app/lib/practical/test/helpers/integration/assertions.rb +23 -0
  54. data/app/lib/practical/test/helpers/passkey/system/base.rb +52 -0
  55. data/app/lib/practical/test/helpers/passkey/system/rack_test.rb +45 -0
  56. data/app/lib/practical/test/helpers/passkey/system/selenium.rb +107 -0
  57. data/app/lib/practical/test/helpers/passkey/test_helper.rb +128 -0
  58. data/app/lib/practical/test/helpers/postmark.rb +11 -0
  59. data/app/lib/practical/test/helpers/relation_builder_assertions.rb +18 -0
  60. data/app/lib/practical/test/helpers/setup/debug.rb +8 -0
  61. data/app/lib/practical/test/helpers/setup/faker_seed_pinning.rb +8 -0
  62. data/app/lib/practical/test/helpers/setup/simplecov.rb +17 -0
  63. data/app/lib/practical/test/helpers/shrine/test_data.rb +101 -0
  64. data/app/lib/practical/test/helpers/spy_assertions.rb +7 -0
  65. data/app/lib/practical/test/helpers/system/assertions.rb +33 -0
  66. data/app/lib/practical/test/helpers/system/capybara_prep.rb +10 -0
  67. data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/base.rb +372 -0
  68. data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/self_service.rb +66 -0
  69. data/app/lib/practical/test/shared/auth/passkeys/controllers/reauthentication/base.rb +119 -0
  70. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/no_self_destroy.rb +13 -0
  71. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/no_self_signup.rb +22 -0
  72. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_destroy.rb +134 -0
  73. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_signup.rb +221 -0
  74. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/update.rb +220 -0
  75. data/app/lib/practical/test/shared/auth/passkeys/controllers/sessions/base.rb +108 -0
  76. data/app/lib/practical/test/shared/auth/passkeys/forms/emergency_registration.rb +82 -0
  77. data/app/lib/practical/test/shared/auth/passkeys/models/emergency_registration/base.rb +89 -0
  78. data/app/lib/practical/test/shared/auth/passkeys/models/emergency_registration/use_for_and_notify.rb +48 -0
  79. data/app/lib/practical/test/shared/auth/passkeys/models/passkey.rb +101 -0
  80. data/app/lib/practical/test/shared/auth/passkeys/models/resource_with_passkeys.rb +57 -0
  81. data/app/lib/practical/test/shared/auth/passkeys/policies/passkey.rb +18 -0
  82. data/app/lib/practical/test/shared/auth/passkeys/services/send_emergency_registration.rb +41 -0
  83. data/app/lib/practical/test/shared/models/normalized_email.rb +23 -0
  84. data/app/lib/practical/test/shared/models/user.rb +27 -0
  85. data/app/lib/practical/test/shared/models/utility/ip_address.rb +42 -0
  86. data/app/lib/practical/test/shared/models/utility/user_agent.rb +43 -0
  87. data/app/lib/practical/views/button/styling.rb +23 -0
  88. data/app/lib/practical/views/error_handling.rb +33 -0
  89. data/app/lib/practical/views/form_builders/base.rb +152 -0
  90. data/app/lib/practical/views/icon_set.rb +156 -0
  91. data/app/lib/practical/views/web_awesome/style_utility/appearance_variant.rb +19 -0
  92. data/app/lib/practical/views/web_awesome/style_utility/base.rb +21 -0
  93. data/app/lib/practical/views/web_awesome/style_utility/color_variant.rb +17 -0
  94. data/app/lib/practical/views/web_awesome/style_utility/size.rb +31 -0
  95. data/config/locales/auth.en.yml +38 -0
  96. data/config/locales/devise.passkeys.en.yml +18 -0
  97. data/config/locales/practical_framework.en.yml +9 -0
  98. data/config/routes.rb +4 -0
  99. data/db/seeds/setup.rb +13 -0
  100. data/db/seeds/users/default.rb +34 -0
  101. data/lib/generators/practical/test/helper/USAGE +8 -0
  102. data/lib/generators/practical/test/helper/helper_generator.rb +9 -0
  103. data/lib/generators/practical/test/helper/templates/helper.rb.tt +4 -0
  104. data/lib/generators/practical/test/shared_test/USAGE +9 -0
  105. data/lib/generators/practical/test/shared_test/shared_test_generator.rb +7 -0
  106. data/lib/generators/practical/test/shared_test/templates/shared_test.rb.tt +9 -0
  107. data/lib/generators/practical/views/component/USAGE +9 -0
  108. data/lib/generators/practical/views/component/component_generator.rb +20 -0
  109. data/lib/practical/framework/engine.rb +35 -0
  110. data/lib/practical/helpers/form_with_helper.rb +10 -0
  111. data/lib/practical/helpers/icon_helper.rb +18 -0
  112. data/lib/practical/helpers/text_helper.rb +20 -0
  113. data/lib/practical/helpers/translation_helper.rb +25 -0
  114. data/lib/practical/version.rb +5 -0
  115. data/lib/practical/views/element_helper.rb +48 -0
  116. data/lib/practical.rb +21 -0
  117. data/lib/tasks/practical/coverage.rake +19 -0
  118. data/lib/tasks/practical/framework_tasks.rake +6 -0
  119. metadata +303 -0
@@ -0,0 +1,372 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Auth::Passkeys::Controllers::EmergencyRegistration::Base
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "show: renders successfully when given an emergency_registration token for the resource's emergency registration class" do
8
+ show_emergency_registration_action(token: valid_emergency_registration_token)
9
+ assert_response :success
10
+ end
11
+
12
+ test "show: raises ActiveSupport::MessageVerifier::InvalidSignature if an expired token is given" do
13
+ assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
14
+ show_emergency_registration_action(token: expired_emergency_registration_token)
15
+ end
16
+ end
17
+
18
+ test "show: raises ActiveSupport::MessageVerifier::InvalidSignature if a bad token is given" do
19
+ assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
20
+ show_emergency_registration_action(token: SecureRandom.hex)
21
+ end
22
+ end
23
+
24
+ test "show: raises ActiveSupport::MessageVerifier::InvalidSignature if a raw emergency registration ID is given" do
25
+ assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
26
+ show_emergency_registration_action(token: raw_emergency_registration_id)
27
+ end
28
+ end
29
+
30
+ test "new_create_challenge: renders successfully when given a valid emergency_registration token for the resource's emergency registration class" do
31
+ emergency_passkey_registration = valid_emergency_registration
32
+ assert_nil emergency_passkey_registration.used_at
33
+
34
+ token = emergency_passkey_registration.generate_token_for(:emergency_registration)
35
+
36
+ get_new_challenge_action(token: token)
37
+ assert_response :ok
38
+
39
+ assert_passkey_registration_challenge(
40
+ data: response.parsed_body,
41
+ stored_challenge: expected_stored_challenge,
42
+ relying_party_data: expected_relying_party_data,
43
+ user_data: expected_user_data_for_challenge,
44
+ credentials_to_exclude: expected_credentials_to_exclude
45
+ )
46
+ end
47
+
48
+ test "new_create_challenge: raises ActiveSupport::MessageVerifier::InvalidSignature if an expired token is given" do
49
+ assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
50
+ get_new_challenge_action(token: expired_emergency_registration_token)
51
+ end
52
+ end
53
+
54
+ test "new_create_challenge: raises ActiveSupport::MessageVerifier::InvalidSignature if a bad token is given" do
55
+ assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
56
+ get_new_challenge_action(token: SecureRandom.hex)
57
+ end
58
+ end
59
+
60
+ test "new_create_challenge: raises ActiveSupport::MessageVerifier::InvalidSignature if a raw emergency registration ID is given" do
61
+ assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
62
+ get_new_challenge_action(token: raw_emergency_registration_id)
63
+ end
64
+ end
65
+
66
+ test "use: registers a new passkey for the owner, using the client details" do
67
+ emergency_passkey_registration = valid_emergency_registration
68
+ assert_nil emergency_passkey_registration.used_at
69
+
70
+ token = emergency_passkey_registration.generate_token_for(:emergency_registration)
71
+
72
+ get_new_challenge_action(token: token)
73
+ assert_response :ok
74
+
75
+ assert_passkey_registration_challenge(
76
+ data: response.parsed_body,
77
+ stored_challenge: expected_stored_challenge,
78
+ relying_party_data: expected_relying_party_data,
79
+ user_data: expected_user_data_for_challenge,
80
+ credentials_to_exclude: expected_credentials_to_exclude
81
+ )
82
+
83
+ challenge = expected_stored_challenge
84
+ client = webauthn_client
85
+
86
+ raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
87
+ label = Faker::Computer.os
88
+
89
+ params = params_for_using_emergency_passkey_registration(label: label, raw_credential: raw_credential)
90
+
91
+ assert_difference "#{passkey_class}.count", +1 do
92
+ use_emergency_registration_action(token: token, params: params)
93
+ assert_json_redirected_to expected_new_session_url
94
+ end
95
+
96
+ credential = hydrate_response_from_raw_credential(client: client, relying_party: webauthn_relying_party, raw_credential: raw_credential).credential
97
+
98
+ new_passkey = emergency_passkey_registration.reload.passkey
99
+ assert_equal label, new_passkey.label
100
+ assert_equal Base64.strict_encode64(credential.id), new_passkey.external_id
101
+ assert_not_nil new_passkey.public_key
102
+ assert_nil new_passkey.last_used_at
103
+ end
104
+
105
+ test "use: does not allow overriding who the passkey is registered for" do
106
+ old_owner = owner_instance
107
+ emergency_passkey_registration = valid_emergency_registration
108
+ assert_nil emergency_passkey_registration.used_at
109
+
110
+ token = emergency_passkey_registration.generate_token_for(:emergency_registration)
111
+
112
+ get_new_challenge_action(token: token)
113
+ assert_response :ok
114
+
115
+ assert_passkey_registration_challenge(
116
+ data: response.parsed_body,
117
+ stored_challenge: expected_stored_challenge,
118
+ relying_party_data: expected_relying_party_data,
119
+ user_data: expected_user_data_for_challenge,
120
+ credentials_to_exclude: expected_credentials_to_exclude
121
+ )
122
+
123
+ challenge = expected_stored_challenge
124
+ client = webauthn_client
125
+
126
+ raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
127
+ label = Faker::Computer.os
128
+
129
+ params = params_that_try_to_override_owner_during_emergency_registration(label: label, raw_credential: raw_credential)
130
+
131
+ assert_difference "#{passkey_class}.count", +1 do
132
+ use_emergency_registration_action(token: token, params: params)
133
+ assert_json_redirected_to expected_new_session_url
134
+ end
135
+
136
+ credential = hydrate_response_from_raw_credential(client: client, relying_party: webauthn_relying_party, raw_credential: raw_credential).credential
137
+
138
+ new_passkey = emergency_passkey_registration.reload.passkey
139
+ assert_equal label, new_passkey.label
140
+ assert_equal Base64.strict_encode64(credential.id), new_passkey.external_id
141
+ assert_not_nil new_passkey.public_key
142
+ assert_nil new_passkey.last_used_at
143
+
144
+ assert_old_owner_owns_passkey(passkey: new_passkey)
145
+ end
146
+
147
+ test "use: raises ActiveRecord::RecordNotFound if an expired token is given" do
148
+ emergency_passkey_registration = valid_emergency_registration
149
+ assert_nil emergency_passkey_registration.used_at
150
+
151
+ token = emergency_passkey_registration.generate_token_for(:emergency_registration)
152
+
153
+ get_new_challenge_action(token: token)
154
+ assert_response :ok
155
+
156
+ assert_passkey_registration_challenge(
157
+ data: response.parsed_body,
158
+ stored_challenge: expected_stored_challenge,
159
+ relying_party_data: expected_relying_party_data,
160
+ user_data: expected_user_data_for_challenge,
161
+ credentials_to_exclude: expected_credentials_to_exclude
162
+ )
163
+
164
+ challenge = expected_stored_challenge
165
+ client = webauthn_client
166
+
167
+ raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
168
+ label = Faker::Computer.os
169
+
170
+ params = params_for_using_emergency_passkey_registration(label: label, raw_credential: raw_credential)
171
+
172
+ assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
173
+ use_emergency_registration_action(token: expired_emergency_registration_token, params: params)
174
+ end
175
+ end
176
+
177
+ test "use: raises ActiveRecord::RecordNotFound if a bad token is given" do
178
+ emergency_passkey_registration = valid_emergency_registration
179
+ assert_nil emergency_passkey_registration.used_at
180
+
181
+ token = emergency_passkey_registration.generate_token_for(:emergency_registration)
182
+
183
+ get_new_challenge_action(token: token)
184
+ assert_response :ok
185
+
186
+ assert_passkey_registration_challenge(
187
+ data: response.parsed_body,
188
+ stored_challenge: expected_stored_challenge,
189
+ relying_party_data: expected_relying_party_data,
190
+ user_data: expected_user_data_for_challenge,
191
+ credentials_to_exclude: expected_credentials_to_exclude
192
+ )
193
+
194
+ challenge = expected_stored_challenge
195
+ client = webauthn_client
196
+
197
+ raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
198
+ label = Faker::Computer.os
199
+
200
+ params = params_for_using_emergency_passkey_registration(label: label, raw_credential: raw_credential)
201
+
202
+ assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
203
+ use_emergency_registration_action(token: SecureRandom.hex, params: params)
204
+ end
205
+ end
206
+
207
+ test "use: raises ActiveRecord::RecordNotFound if a raw emergency registration ID is given" do
208
+ emergency_passkey_registration = valid_emergency_registration
209
+ assert_nil emergency_passkey_registration.used_at
210
+
211
+ token = emergency_passkey_registration.generate_token_for(:emergency_registration)
212
+
213
+ get_new_challenge_action(token: token)
214
+ assert_response :ok
215
+
216
+ assert_passkey_registration_challenge(
217
+ data: response.parsed_body,
218
+ stored_challenge: expected_stored_challenge,
219
+ relying_party_data: expected_relying_party_data,
220
+ user_data: expected_user_data_for_challenge,
221
+ credentials_to_exclude: expected_credentials_to_exclude
222
+ )
223
+
224
+ challenge = expected_stored_challenge
225
+ client = webauthn_client
226
+
227
+ raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
228
+ label = Faker::Computer.os
229
+
230
+ params = params_for_using_emergency_passkey_registration(label: label, raw_credential: raw_credential)
231
+
232
+ assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
233
+ use_emergency_registration_action(token: raw_emergency_registration_id, params: params)
234
+ end
235
+ end
236
+
237
+ test "use: returns a unprocessable_entity with the PracticalFramework error JSON if the passkey label is missing" do
238
+ emergency_passkey_registration = valid_emergency_registration
239
+ assert_nil emergency_passkey_registration.used_at
240
+
241
+ token = emergency_passkey_registration.generate_token_for(:emergency_registration)
242
+
243
+ get_new_challenge_action(token: token)
244
+ assert_response :ok
245
+
246
+ assert_passkey_registration_challenge(
247
+ data: response.parsed_body,
248
+ stored_challenge: expected_stored_challenge,
249
+ relying_party_data: expected_relying_party_data,
250
+ user_data: expected_user_data_for_challenge,
251
+ credentials_to_exclude: expected_credentials_to_exclude
252
+ )
253
+
254
+ challenge = expected_stored_challenge
255
+ client = webauthn_client
256
+
257
+ raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
258
+
259
+ params = params_for_using_emergency_passkey_registration(label: " ", raw_credential: raw_credential)
260
+
261
+ assert_no_difference "#{passkey_class}.count" do
262
+ use_emergency_registration_action(token: token, params: params)
263
+ end
264
+
265
+ assert_response :unprocessable_entity
266
+
267
+ assert_form_error_for_label(message: "can't be blank", type: :blank)
268
+ end
269
+
270
+ test "use: returns a unprocessable_entity with the PracticalFramework error JSON if the passkey challenge fails" do
271
+ emergency_passkey_registration = valid_emergency_registration
272
+ assert_nil emergency_passkey_registration.used_at
273
+
274
+ token = emergency_passkey_registration.generate_token_for(:emergency_registration)
275
+
276
+ get_new_challenge_action(token: token)
277
+ assert_response :ok
278
+
279
+ assert_passkey_registration_challenge(
280
+ data: response.parsed_body,
281
+ stored_challenge: expected_stored_challenge,
282
+ relying_party_data: expected_relying_party_data,
283
+ user_data: expected_user_data_for_challenge,
284
+ credentials_to_exclude: expected_credentials_to_exclude
285
+ )
286
+
287
+ challenge = SecureRandom.hex
288
+ client = webauthn_client
289
+
290
+ raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
291
+ label = Faker::Computer.os
292
+
293
+ params = params_for_using_emergency_passkey_registration(label: label, raw_credential: raw_credential)
294
+
295
+ assert_no_difference "#{passkey_class}.count" do
296
+ use_emergency_registration_action(token: token, params: params)
297
+ end
298
+
299
+ assert_response :unprocessable_entity
300
+
301
+ assert_form_error_for_credential(message: I18n.translate("devise.emergency_passkey_registrations.webauthn_challenge_verification_error"))
302
+ end
303
+
304
+ test "use: returns a unprocessable_entity with the PracticalFramework error JSON if the credential was missing" do
305
+ emergency_passkey_registration = valid_emergency_registration
306
+ assert_nil emergency_passkey_registration.used_at
307
+
308
+ token = emergency_passkey_registration.generate_token_for(:emergency_registration)
309
+
310
+ get_new_challenge_action(token: token)
311
+ assert_response :ok
312
+
313
+ assert_passkey_registration_challenge(
314
+ data: response.parsed_body,
315
+ stored_challenge: expected_stored_challenge,
316
+ relying_party_data: expected_relying_party_data,
317
+ user_data: expected_user_data_for_challenge,
318
+ credentials_to_exclude: expected_credentials_to_exclude
319
+ )
320
+
321
+ challenge = expected_stored_challenge
322
+ client = webauthn_client
323
+
324
+ raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
325
+ label = Faker::Computer.os
326
+
327
+ params = params_for_using_emergency_passkey_registration(label: label, raw_credential: nil)
328
+
329
+ assert_no_difference "#{passkey_class}.count" do
330
+ use_emergency_registration_action(token: token, params: params)
331
+ end
332
+
333
+ assert_response :unprocessable_entity
334
+
335
+ assert_form_error_for_credential(message: I18n.translate("devise.emergency_passkey_registrations.credential_missing_or_could_not_be_parsed"))
336
+ end
337
+
338
+ test "use: returns a unprocessable_entity with the PracticalFramework error JSON if the credential could not be parsed" do
339
+ emergency_passkey_registration = valid_emergency_registration
340
+ assert_nil emergency_passkey_registration.used_at
341
+
342
+ token = emergency_passkey_registration.generate_token_for(:emergency_registration)
343
+
344
+ get_new_challenge_action(token: token)
345
+ assert_response :ok
346
+
347
+ assert_passkey_registration_challenge(
348
+ data: response.parsed_body,
349
+ stored_challenge: expected_stored_challenge,
350
+ relying_party_data: expected_relying_party_data,
351
+ user_data: expected_user_data_for_challenge,
352
+ credentials_to_exclude: expected_credentials_to_exclude
353
+ )
354
+
355
+ challenge = expected_stored_challenge
356
+ client = webauthn_client
357
+
358
+ raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
359
+ label = Faker::Computer.os
360
+
361
+ params = params_for_using_emergency_passkey_registration(label: label, raw_credential: "blah")
362
+
363
+ assert_no_difference "#{passkey_class}.count" do
364
+ use_emergency_registration_action(token: token, params: params)
365
+ end
366
+
367
+ assert_response :unprocessable_entity
368
+
369
+ assert_form_error_for_credential(message: I18n.translate("devise.emergency_passkey_registrations.credential_missing_or_could_not_be_parsed"))
370
+ end
371
+ end
372
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Auth::Passkeys::Controllers::EmergencyRegistration::SelfService
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "new: renders successfully" do
8
+ get_new_registration_action
9
+ assert_response :success
10
+ end
11
+
12
+ test "create: calls service to send emergency passkey registration with the user_agent, ip_address, and found owner" do
13
+ owner = owner_instance
14
+
15
+ params = {
16
+ new_emergency_passkey_registration_form: {
17
+ email: owner.email
18
+ }
19
+ }
20
+
21
+ ip_address = Faker::Internet.ip_v6_address
22
+ user_agent = Faker::Internet.user_agent
23
+
24
+ env = {"REMOTE_ADDR" => ip_address, "User-Agent" => user_agent}
25
+
26
+ service_spy = Spy.on_instance_method(send_registration_service_class, run!: true)
27
+
28
+ request_emergency_registration_action(params: params, env: env)
29
+
30
+ assert_json_redirected_to expected_new_session_url
31
+ assert_flash_message(
32
+ type: :notice,
33
+ message: I18n.translate('emergency_passkey_registrations.sent_message'),
34
+ icon_name: "envelope-dot"
35
+ )
36
+
37
+ assert_times_called(spy: service_spy, times: 1)
38
+ end
39
+
40
+ test "create: silently ignores bad email addresses" do
41
+ params = {
42
+ new_emergency_passkey_registration_form: {
43
+ email: "bad@example.com"
44
+ }
45
+ }
46
+
47
+ ip_address = Faker::Internet.ip_v6_address
48
+ user_agent = Faker::Internet.user_agent
49
+
50
+ env = {"REMOTE_ADDR" => ip_address, "User-Agent" => user_agent}
51
+
52
+ service_spy = Spy.on_instance_method(send_registration_service_class, run!: true)
53
+
54
+ request_emergency_registration_action(params: params, env: env)
55
+
56
+ assert_json_redirected_to expected_new_session_url
57
+ assert_flash_message(
58
+ type: :notice,
59
+ message: I18n.translate('emergency_passkey_registrations.sent_message'),
60
+ icon_name: "envelope-dot"
61
+ )
62
+
63
+ assert_times_called(spy: service_spy, times: 0)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Auth::Passkeys::Controllers::Reauthentication::Base
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "new_challenge: requires the resource to be authenticated" do
8
+ issue_new_challenge_action
9
+ assert_response :not_found
10
+ end
11
+
12
+ test "new_challenge: sets the session variable that stores the new challenge" do
13
+ sign_in_as_resource
14
+ issue_new_challenge_action
15
+ assert_response :ok
16
+ assert_equal get_session_challenge, response.parsed_body["challenge"]
17
+ end
18
+
19
+ test "new_challenge: overrides the session variable that stores the new challenge" do
20
+ sign_in_as_resource
21
+ issue_new_challenge_action
22
+ assert_response :ok
23
+ old_session_challenge = get_session_challenge
24
+
25
+ issue_new_challenge_action
26
+ assert_response :ok
27
+
28
+ assert_not_equal old_session_challenge, get_session_challenge
29
+ assert_not_equal old_session_challenge, response.parsed_body["challenge"]
30
+ end
31
+
32
+ test "reauthenticate: requires the resource to be authenticated" do
33
+ reauthenticate_action(params: {})
34
+ assert_response :not_found
35
+ end
36
+
37
+ test """reauthenticate:
38
+ - verifies that the given challenge matches what is in the session
39
+ - clears the challenge session variable
40
+ - returns the reauthentication_token
41
+ - stores the reauthentication_token in the session
42
+ """ do
43
+ sign_in_as_resource
44
+ issue_new_challenge_action
45
+ assert_response :ok
46
+
47
+ assert_passkey_authentication_challenge(
48
+ data: response.parsed_body,
49
+ stored_challenge: expected_stored_challenge,
50
+ credentials_to_allow: expected_credentials_to_allow
51
+ )
52
+
53
+ challenge = response.parsed_body["challenge"]
54
+ credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
55
+
56
+ reauthenticate_action(params: {passkey_credential: credential.to_json})
57
+ assert_response :ok
58
+
59
+ assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
60
+ assert_nil get_session_challenge
61
+ end
62
+
63
+ test """reauthenticate:
64
+ - raises an error if the challenge does not match what is in the session
65
+ - clears the challenge session variable
66
+ - does not have a reauthentication_token in the session
67
+ """ do
68
+ sign_in_as_resource
69
+ issue_new_challenge_action
70
+ assert_response :ok
71
+
72
+ assert_passkey_authentication_challenge(
73
+ data: response.parsed_body,
74
+ stored_challenge: expected_stored_challenge,
75
+ credentials_to_allow: expected_credentials_to_allow
76
+ )
77
+
78
+ challenge = SecureRandom.hex
79
+ credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
80
+
81
+ reauthenticate_action(params: {passkey_credential: credential.to_json})
82
+ assert_response :unauthorized
83
+
84
+ assert_equal I18n.translate("devise.failure.webauthn_challenge_verification_error"), response.parsed_body["error"]
85
+ assert_nil expected_stored_reauthentication_token
86
+ assert_nil get_session_challenge
87
+ assert_resource_not_signed_in
88
+ end
89
+
90
+ test """reauthenticate:
91
+ - raises an error if the credential is invalid
92
+ - clears the challenge session variable
93
+ - does not have a reauthentication_token in the session
94
+ """ do
95
+ sign_in_as_resource
96
+ issue_new_challenge_action
97
+ assert_response :ok
98
+
99
+ assert_passkey_authentication_challenge(
100
+ data: response.parsed_body,
101
+ stored_challenge: expected_stored_challenge,
102
+ credentials_to_allow: expected_credentials_to_allow
103
+ )
104
+
105
+ challenge = response.parsed_body["challenge"]
106
+ credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
107
+
108
+ invalidate_all_credentials
109
+
110
+ reauthenticate_action(params: {passkey_credential: credential.to_json})
111
+ assert_response :unauthorized
112
+
113
+ assert_equal I18n.translate("devise.failure.stored_credential_not_found"), response.parsed_body["error"]
114
+ assert_nil expected_stored_reauthentication_token
115
+ assert_nil get_session_challenge
116
+ assert_resource_not_signed_in
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Auth::Passkeys::Controllers::Registrations::NoSelfDestroy
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "destroy action returns 501" do
8
+ sign_in_as_resource
9
+ destroy_registration_action
10
+ assert_response :not_implemented
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Auth::Passkeys::Controllers::Registrations::NoSelfSignup
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "new registration challenge action returns 501" do
8
+ new_registration_challenge_action
9
+ assert_response :not_implemented
10
+ end
11
+
12
+ test "new registration action returns 501" do
13
+ new_registration_action
14
+ assert_response :not_implemented
15
+ end
16
+
17
+ test "create registration action returns 501" do
18
+ create_registration_action
19
+ assert_response :not_implemented
20
+ end
21
+ end
22
+ end