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.
- checksums.yaml +7 -0
- data/README.md +37 -0
- data/Rakefile +10 -0
- data/app/components/practical/views/base_component.rb +6 -0
- data/app/components/practical/views/button_component.rb +27 -0
- data/app/components/practical/views/datatable/filter_applied.rb +25 -0
- data/app/components/practical/views/datatable/filter_section_component.html.erb +9 -0
- data/app/components/practical/views/datatable/filter_section_component.rb +19 -0
- data/app/components/practical/views/datatable/sort_link_component.rb +48 -0
- data/app/components/practical/views/datatable.rb +36 -0
- data/app/components/practical/views/flash_messages_component.rb +65 -0
- data/app/components/practical/views/form/error_list_component.rb +15 -0
- data/app/components/practical/views/form/error_list_item_component.rb +20 -0
- data/app/components/practical/views/form/error_list_item_template_component.rb +9 -0
- data/app/components/practical/views/form/fallback_errors_section_component.html.erb +7 -0
- data/app/components/practical/views/form/fallback_errors_section_component.rb +21 -0
- data/app/components/practical/views/form/field_errors_component.rb +28 -0
- data/app/components/practical/views/form/field_title_component.rb +23 -0
- data/app/components/practical/views/form/fieldset_title_component.rb +20 -0
- data/app/components/practical/views/form/input_component.html.erb +7 -0
- data/app/components/practical/views/form/input_component.rb +22 -0
- data/app/components/practical/views/form/option_label_component.rb +21 -0
- data/app/components/practical/views/form/practical_editor_component.rb +26 -0
- data/app/components/practical/views/form/required_radio_collection_wrapper_component.rb +23 -0
- data/app/components/practical/views/form_wrapper.rb +21 -0
- data/app/components/practical/views/icon_component.rb +36 -0
- data/app/components/practical/views/icon_for_file_extension_component.rb +53 -0
- data/app/components/practical/views/modal_dialog_component.html.erb +10 -0
- data/app/components/practical/views/modal_dialog_component.rb +16 -0
- data/app/components/practical/views/navigation/breadcrumb_item_component.rb +20 -0
- data/app/components/practical/views/navigation/breadcrumbs_component.html.erb +31 -0
- data/app/components/practical/views/navigation/breadcrumbs_component.rb +41 -0
- data/app/components/practical/views/navigation/navigation_link_component.rb +39 -0
- data/app/components/practical/views/navigation/pagination/goto_form_component.html.erb +31 -0
- data/app/components/practical/views/navigation/pagination/goto_form_component.rb +34 -0
- data/app/components/practical/views/navigation/pagination_component.html.erb +11 -0
- data/app/components/practical/views/navigation/pagination_component.rb +98 -0
- data/app/components/practical/views/open_dialog_button_component.rb +16 -0
- data/app/components/practical/views/page_component.html.erb +53 -0
- data/app/components/practical/views/page_component.rb +12 -0
- data/app/components/practical/views/relative_time_component.rb +13 -0
- data/app/components/practical/views/tiptap_document_component.rb +311 -0
- data/app/components/practical/views/toast_component.html.erb +26 -0
- data/app/components/practical/views/toast_component.rb +19 -0
- data/app/controllers/concerns/practical/auth/passkeys/emergency_registrations.rb +57 -0
- data/app/controllers/concerns/practical/auth/passkeys/web_authn_debug_context.rb +13 -0
- data/app/controllers/concerns/practical/views/flash_helpers.rb +37 -0
- data/app/controllers/concerns/practical/views/json_redirection.rb +7 -0
- data/app/lib/practical/defaults/shrine.rb +48 -0
- data/app/lib/practical/test/helpers/administrator/test_helpers.rb +7 -0
- data/app/lib/practical/test/helpers/extra_assertions.rb +7 -0
- data/app/lib/practical/test/helpers/flash_assertions.rb +8 -0
- data/app/lib/practical/test/helpers/integration/assertions.rb +23 -0
- data/app/lib/practical/test/helpers/passkey/system/base.rb +52 -0
- data/app/lib/practical/test/helpers/passkey/system/rack_test.rb +45 -0
- data/app/lib/practical/test/helpers/passkey/system/selenium.rb +107 -0
- data/app/lib/practical/test/helpers/passkey/test_helper.rb +128 -0
- data/app/lib/practical/test/helpers/postmark.rb +11 -0
- data/app/lib/practical/test/helpers/relation_builder_assertions.rb +18 -0
- data/app/lib/practical/test/helpers/setup/debug.rb +8 -0
- data/app/lib/practical/test/helpers/setup/faker_seed_pinning.rb +8 -0
- data/app/lib/practical/test/helpers/setup/simplecov.rb +17 -0
- data/app/lib/practical/test/helpers/shrine/test_data.rb +101 -0
- data/app/lib/practical/test/helpers/spy_assertions.rb +7 -0
- data/app/lib/practical/test/helpers/system/assertions.rb +33 -0
- data/app/lib/practical/test/helpers/system/capybara_prep.rb +10 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/base.rb +372 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/self_service.rb +66 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/reauthentication/base.rb +119 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/no_self_destroy.rb +13 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/no_self_signup.rb +22 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_destroy.rb +134 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_signup.rb +221 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/update.rb +220 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/sessions/base.rb +108 -0
- data/app/lib/practical/test/shared/auth/passkeys/forms/emergency_registration.rb +82 -0
- data/app/lib/practical/test/shared/auth/passkeys/models/emergency_registration/base.rb +89 -0
- data/app/lib/practical/test/shared/auth/passkeys/models/emergency_registration/use_for_and_notify.rb +48 -0
- data/app/lib/practical/test/shared/auth/passkeys/models/passkey.rb +101 -0
- data/app/lib/practical/test/shared/auth/passkeys/models/resource_with_passkeys.rb +57 -0
- data/app/lib/practical/test/shared/auth/passkeys/policies/passkey.rb +18 -0
- data/app/lib/practical/test/shared/auth/passkeys/services/send_emergency_registration.rb +41 -0
- data/app/lib/practical/test/shared/models/normalized_email.rb +23 -0
- data/app/lib/practical/test/shared/models/user.rb +27 -0
- data/app/lib/practical/test/shared/models/utility/ip_address.rb +42 -0
- data/app/lib/practical/test/shared/models/utility/user_agent.rb +43 -0
- data/app/lib/practical/views/button/styling.rb +23 -0
- data/app/lib/practical/views/error_handling.rb +33 -0
- data/app/lib/practical/views/form_builders/base.rb +152 -0
- data/app/lib/practical/views/icon_set.rb +156 -0
- data/app/lib/practical/views/web_awesome/style_utility/appearance_variant.rb +19 -0
- data/app/lib/practical/views/web_awesome/style_utility/base.rb +21 -0
- data/app/lib/practical/views/web_awesome/style_utility/color_variant.rb +17 -0
- data/app/lib/practical/views/web_awesome/style_utility/size.rb +31 -0
- data/config/locales/auth.en.yml +38 -0
- data/config/locales/devise.passkeys.en.yml +18 -0
- data/config/locales/practical_framework.en.yml +9 -0
- data/config/routes.rb +4 -0
- data/db/seeds/setup.rb +13 -0
- data/db/seeds/users/default.rb +34 -0
- data/lib/generators/practical/test/helper/USAGE +8 -0
- data/lib/generators/practical/test/helper/helper_generator.rb +9 -0
- data/lib/generators/practical/test/helper/templates/helper.rb.tt +4 -0
- data/lib/generators/practical/test/shared_test/USAGE +9 -0
- data/lib/generators/practical/test/shared_test/shared_test_generator.rb +7 -0
- data/lib/generators/practical/test/shared_test/templates/shared_test.rb.tt +9 -0
- data/lib/generators/practical/views/component/USAGE +9 -0
- data/lib/generators/practical/views/component/component_generator.rb +20 -0
- data/lib/practical/framework/engine.rb +35 -0
- data/lib/practical/helpers/form_with_helper.rb +10 -0
- data/lib/practical/helpers/icon_helper.rb +18 -0
- data/lib/practical/helpers/text_helper.rb +20 -0
- data/lib/practical/helpers/translation_helper.rb +25 -0
- data/lib/practical/version.rb +5 -0
- data/lib/practical/views/element_helper.rb +48 -0
- data/lib/practical.rb +21 -0
- data/lib/tasks/practical/coverage.rake +19 -0
- data/lib/tasks/practical/framework_tasks.rake +6 -0
- metadata +303 -0
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ostruct'
|
4
|
+
|
5
|
+
module Practical::Test::Shared::Auth::Passkeys::Forms::EmergencyRegistration
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
def dummy_webauthn_credential
|
9
|
+
OpenStruct.new(public_key: SecureRandom.hex, raw_id: SecureRandom.bytes(10), sign_count: 0)
|
10
|
+
end
|
11
|
+
|
12
|
+
included do
|
13
|
+
test "save: merges errors from the attempted new passkey and re-raises" do
|
14
|
+
form = form_instance_with_no_passkey_label_or_public_key
|
15
|
+
|
16
|
+
assert_no_difference "#{passkey_class}.count" do
|
17
|
+
assert_raises ActiveRecord::RecordInvalid do
|
18
|
+
form.save!
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
assert_equal true, form.errors.of_kind?(:public_key, :blank)
|
23
|
+
assert_equal true, form.errors.of_kind?(:passkey_label, :blank)
|
24
|
+
end
|
25
|
+
|
26
|
+
test "save: raises unexpected errors" do
|
27
|
+
form = valid_form_instance
|
28
|
+
Spy.on(form.emergency_registration, :use_for_and_notify!).and_raise(ArgumentError)
|
29
|
+
|
30
|
+
assert_no_difference "#{passkey_class}.count" do
|
31
|
+
assert_raises ArgumentError do
|
32
|
+
form.save!
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
test "save: raises an AlreadyUsedError if the emergency_registration has a passkey" do
|
38
|
+
form = form_instance_with_emergency_registration_that_has_passkey
|
39
|
+
|
40
|
+
assert_no_difference "#{passkey_class}.count" do
|
41
|
+
assert_raises already_used_error_class do
|
42
|
+
form.save!
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
test "save: raises an AlreadyUsedError if the emergency_registration has been used" do
|
48
|
+
form = form_instance_with_emergency_registration_with_used_at
|
49
|
+
|
50
|
+
assert_no_difference "#{passkey_class}.count" do
|
51
|
+
assert_raises already_used_error_class do
|
52
|
+
form.save!
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
test """save:
|
58
|
+
- creates the passkey for the owner
|
59
|
+
- marks the emergency_registration as used
|
60
|
+
- enqueues the email that a passkey has been created
|
61
|
+
""" do
|
62
|
+
form = valid_form_instance
|
63
|
+
|
64
|
+
time = Time.now.utc
|
65
|
+
|
66
|
+
Timecop.freeze(time) do
|
67
|
+
assert_difference "#{passkey_class}.count", +1 do
|
68
|
+
form.save!
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
emergency_registration = form.emergency_registration.reload
|
73
|
+
|
74
|
+
new_passkey = passkey_class.last
|
75
|
+
assert_equal new_passkey, emergency_registration.passkey
|
76
|
+
assert_equal time.to_formatted_s(:db), emergency_registration.used_at.to_formatted_s(:db)
|
77
|
+
|
78
|
+
|
79
|
+
assert_new_passkey_email(new_passkey: new_passkey)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Test::Shared::Auth::Passkeys::Models::EmergencyRegistration::Base
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
test "belongs_to the owner of the registration" do
|
8
|
+
reflection = model_class.reflect_on_association(owner_reflection_name)
|
9
|
+
assert_equal :belongs_to, reflection.macro
|
10
|
+
end
|
11
|
+
|
12
|
+
test "requires an owner to be valid" do
|
13
|
+
instance = model_instance
|
14
|
+
instance.send(:"#{owner_reflection_name}=", nil)
|
15
|
+
|
16
|
+
assert_equal false, instance.valid?
|
17
|
+
assert_equal true, instance.errors.of_kind?(owner_reflection_name, :blank)
|
18
|
+
|
19
|
+
instance.send(:"#{owner_reflection_name}=", owner_instance)
|
20
|
+
instance.valid?
|
21
|
+
|
22
|
+
assert_equal false, instance.errors.of_kind?(owner_reflection_name, :blank)
|
23
|
+
end
|
24
|
+
|
25
|
+
test "optionally belongs_to user_agent" do
|
26
|
+
reflection = model_class.reflect_on_association(user_agent_reflection_name)
|
27
|
+
assert_equal :belongs_to, reflection.macro
|
28
|
+
assert_equal user_agent_class_name, reflection.class_name
|
29
|
+
assert_equal true, reflection.options[:optional]
|
30
|
+
end
|
31
|
+
|
32
|
+
test "optionally belongs_to ip_address" do
|
33
|
+
reflection = model_class.reflect_on_association(ip_address_reflection_name)
|
34
|
+
assert_equal :belongs_to, reflection.macro
|
35
|
+
assert_equal ip_address_class_name, reflection.class_name
|
36
|
+
assert_equal true, reflection.options[:optional]
|
37
|
+
end
|
38
|
+
|
39
|
+
test "optionally belongs_to a passkey" do
|
40
|
+
reflection = model_class.reflect_on_association(:passkey)
|
41
|
+
assert_equal :belongs_to, reflection.macro
|
42
|
+
assert_equal true, reflection.options[:optional]
|
43
|
+
end
|
44
|
+
|
45
|
+
test "when a passkey is destroyed, the emergency_registration is destroyed as well" do
|
46
|
+
emergency_registration = model_instance_with_passkey
|
47
|
+
passkey = emergency_registration.passkey
|
48
|
+
|
49
|
+
passkey.destroy!
|
50
|
+
|
51
|
+
assert_nil model_class.find_by(id: emergency_registration.id)
|
52
|
+
end
|
53
|
+
|
54
|
+
test "available: scope only returns instances where used_at is nil" do
|
55
|
+
instance = model_instance
|
56
|
+
assert_nil instance.used_at
|
57
|
+
|
58
|
+
assert_includes model_class.available, instance
|
59
|
+
|
60
|
+
instance.update!(used_at: Time.now.utc)
|
61
|
+
|
62
|
+
assert_not_includes model_class.available, instance
|
63
|
+
end
|
64
|
+
|
65
|
+
test "generates_token_for emergency_registration that expires within the designated time" do
|
66
|
+
instance = model_instance
|
67
|
+
|
68
|
+
token = instance.generate_token_for(:emergency_registration)
|
69
|
+
|
70
|
+
assert_equal instance, model_class.find_by_token_for(:emergency_registration, token)
|
71
|
+
|
72
|
+
Timecop.freeze(Time.now.utc + expiration_timespan) do
|
73
|
+
assert_nil model_class.find_by_token_for(:emergency_registration, token)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
test "generates_token_for emergency_registration that expires when used_at is set" do
|
78
|
+
instance = model_instance
|
79
|
+
|
80
|
+
token = instance.generate_token_for(:emergency_registration)
|
81
|
+
|
82
|
+
assert_equal instance, model_class.find_by_token_for(:emergency_registration, token)
|
83
|
+
|
84
|
+
instance.update!(used_at: Time.now.utc)
|
85
|
+
|
86
|
+
assert_nil model_class.find_by_token_for(:emergency_registration, token)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/app/lib/practical/test/shared/auth/passkeys/models/emergency_registration/use_for_and_notify.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Test::Shared::Auth::Passkeys::Models::EmergencyRegistration::UseForAndNotify
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
test "use_for_and_notify!: raises AlreadyUsedError if a passkey is present" do
|
8
|
+
instance = model_instance
|
9
|
+
instance.update!(passkey: create_passkey_instance)
|
10
|
+
assert_raises already_used_error_class do
|
11
|
+
instance.use_for_and_notify!(new_passkey: create_passkey_instance)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
test "use_for_and_notify!: raises AlreadyUsed error used_at is present" do
|
16
|
+
instance = model_instance
|
17
|
+
instance.update!(used_at: Time.now)
|
18
|
+
assert_raises already_used_error_class do
|
19
|
+
instance.use_for_and_notify!(new_passkey: create_passkey_instance)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
test """use_for_and_notify!:
|
24
|
+
- links to the given passkey
|
25
|
+
- marks the used_at
|
26
|
+
- matching PasskeyMailer.passkey_added class
|
27
|
+
""" do
|
28
|
+
instance = model_instance
|
29
|
+
passkey_instance = create_passkey_instance
|
30
|
+
|
31
|
+
time = Time.now.utc
|
32
|
+
|
33
|
+
Timecop.freeze(time) do
|
34
|
+
assert_enqueued_email_with(
|
35
|
+
passkey_mailer_class,
|
36
|
+
:passkey_added,
|
37
|
+
args: [{ passkey: passkey_instance }]
|
38
|
+
) do
|
39
|
+
instance.use_for_and_notify!(new_passkey: passkey_instance)
|
40
|
+
instance.reload
|
41
|
+
|
42
|
+
assert_equal passkey_instance, instance.passkey
|
43
|
+
assert_equal time.to_formatted_s(:db), instance.used_at.to_formatted_s(:db)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Test::Shared::Auth::Passkeys::Models::Passkey
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
test "belongs_to the owner of the passkey" do
|
8
|
+
reflection = model_class.reflect_on_association(owner_reflection_name)
|
9
|
+
assert_equal :belongs_to, reflection.macro
|
10
|
+
end
|
11
|
+
|
12
|
+
test "requires an owner in order to be valid" do
|
13
|
+
instance = model_instance
|
14
|
+
instance.send(:"#{owner_reflection_name}=", nil)
|
15
|
+
|
16
|
+
assert_equal false, instance.valid?
|
17
|
+
assert_equal true, instance.errors.of_kind?(owner_reflection_name, :blank)
|
18
|
+
|
19
|
+
instance.send(:"#{owner_reflection_name}=", owner_instance)
|
20
|
+
instance.valid?
|
21
|
+
|
22
|
+
assert_equal false, instance.errors.of_kind?(owner_reflection_name, :blank)
|
23
|
+
end
|
24
|
+
|
25
|
+
test "label: is required and cannot be blank" do
|
26
|
+
instance = model_instance
|
27
|
+
instance.label = " "
|
28
|
+
|
29
|
+
assert_equal false, instance.valid?
|
30
|
+
assert_equal true, instance.errors.of_kind?(:label, :blank)
|
31
|
+
|
32
|
+
instance.label = Faker::Computer.stack
|
33
|
+
|
34
|
+
instance.valid?
|
35
|
+
assert_equal false, instance.errors.of_kind?(:label, :blank)
|
36
|
+
end
|
37
|
+
|
38
|
+
test "label: must be unique for an owner" do
|
39
|
+
instance = model_instance
|
40
|
+
|
41
|
+
assert_equal true, instance.valid?
|
42
|
+
|
43
|
+
new_instance = instance.send(:"#{owner_reflection_name}").passkeys.build(label: instance.label)
|
44
|
+
|
45
|
+
assert_equal false, new_instance.valid?
|
46
|
+
assert_equal true, new_instance.errors.of_kind?(:label, :taken)
|
47
|
+
|
48
|
+
new_instance = other_owner_instance.passkeys.build(label: instance.label)
|
49
|
+
new_instance.valid?
|
50
|
+
assert_equal false, new_instance.errors.of_kind?(:label, :taken)
|
51
|
+
end
|
52
|
+
|
53
|
+
test "external_id: is required and cannot be blank" do
|
54
|
+
instance = model_instance
|
55
|
+
instance.external_id = " "
|
56
|
+
|
57
|
+
assert_equal false, instance.valid?
|
58
|
+
assert_equal true, instance.errors.of_kind?(:external_id, :blank)
|
59
|
+
|
60
|
+
instance.external_id = SecureRandom.hex
|
61
|
+
|
62
|
+
instance.valid?
|
63
|
+
assert_equal false, instance.errors.of_kind?(:external_id, :blank)
|
64
|
+
end
|
65
|
+
|
66
|
+
test "external_id: is unique" do
|
67
|
+
instance = model_class.new(external_id: model_instance.external_id)
|
68
|
+
assert_equal false, instance.valid?
|
69
|
+
assert_equal true, instance.errors.of_kind?(:external_id, :taken)
|
70
|
+
|
71
|
+
instance.external_id = SecureRandom.hex
|
72
|
+
instance.valid?
|
73
|
+
|
74
|
+
assert_equal false, instance.errors.of_kind?(:external_id, :taken)
|
75
|
+
end
|
76
|
+
|
77
|
+
test "public_key: is required and cannot be blank" do
|
78
|
+
instance = model_instance
|
79
|
+
instance.public_key = " "
|
80
|
+
|
81
|
+
assert_equal false, instance.valid?
|
82
|
+
assert_equal true, instance.errors.of_kind?(:public_key, :blank)
|
83
|
+
|
84
|
+
instance.public_key = SecureRandom.hex
|
85
|
+
|
86
|
+
instance.valid?
|
87
|
+
assert_equal false, instance.errors.of_kind?(:public_key, :blank)
|
88
|
+
end
|
89
|
+
|
90
|
+
test "public_key: is unique" do
|
91
|
+
instance = model_class.new(public_key: model_instance.public_key)
|
92
|
+
assert_equal false, instance.valid?
|
93
|
+
assert_equal true, instance.errors.of_kind?(:public_key, :taken)
|
94
|
+
|
95
|
+
instance.public_key = SecureRandom.hex
|
96
|
+
instance.valid?
|
97
|
+
|
98
|
+
assert_equal false, instance.errors.of_kind?(:public_key, :taken)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Test::Shared::Auth::Passkeys::Models::ResourceWithPasskeys
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
test "has many passkeys" do
|
8
|
+
reflection = model_class.reflect_on_association(:passkeys)
|
9
|
+
assert_equal :has_many, reflection.macro
|
10
|
+
|
11
|
+
assert_difference "#{passkey_class}.count", +1 do
|
12
|
+
new_passkey = model_instance.passkeys.create!(
|
13
|
+
label: SecureRandom.hex,
|
14
|
+
external_id: SecureRandom.hex,
|
15
|
+
public_key: SecureRandom.hex
|
16
|
+
)
|
17
|
+
|
18
|
+
assert_instance_of passkey_class, new_passkey
|
19
|
+
|
20
|
+
assert_includes model_instance.passkeys, new_passkey
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
test "passkeys_class" do
|
25
|
+
assert_equal passkey_class, model_class.passkeys_class
|
26
|
+
end
|
27
|
+
|
28
|
+
test "find_for_passkey: finds the instance for a given passkey" do
|
29
|
+
assert_equal model_instance, model_class.find_for_passkey(passkey_instance)
|
30
|
+
end
|
31
|
+
|
32
|
+
test "after_passkey_authentication: is defined" do
|
33
|
+
assert_equal true, model_instance.respond_to?(:after_passkey_authentication)
|
34
|
+
end
|
35
|
+
|
36
|
+
test "webauthn_id: is required and cannot be blank" do
|
37
|
+
instance = model_instance
|
38
|
+
instance.webauthn_id = ""
|
39
|
+
assert_equal false, instance.valid?
|
40
|
+
assert_equal true, instance.errors.of_kind?(:webauthn_id, :blank)
|
41
|
+
|
42
|
+
instance.webauthn_id = SecureRandom.hex
|
43
|
+
assert_equal true, instance.valid?
|
44
|
+
end
|
45
|
+
|
46
|
+
test "webauthn_id: is unique" do
|
47
|
+
instance = model_class.new(webauthn_id: model_instance.webauthn_id)
|
48
|
+
assert_equal false, instance.valid?
|
49
|
+
assert_equal true, instance.errors.of_kind?(:webauthn_id, :taken)
|
50
|
+
|
51
|
+
instance.webauthn_id = SecureRandom.hex
|
52
|
+
instance.valid?
|
53
|
+
|
54
|
+
assert_equal false, instance.errors.of_kind?(:webauthn_id, :taken)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Test::Shared::Auth::Passkeys::Policies::Passkey
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
test "manage?: only true for the user's passkeys" do
|
8
|
+
passkey = resource_with_passkeys.passkeys.create!(
|
9
|
+
label: SecureRandom.hex,
|
10
|
+
external_id: SecureRandom.hex,
|
11
|
+
public_key: SecureRandom.hex
|
12
|
+
)
|
13
|
+
|
14
|
+
assert_equal true, policy_class.new(passkey, user: resource_with_passkeys).apply(:manage?)
|
15
|
+
assert_equal false, policy_class.new(passkey, user: other_resource_with_passkeys).apply(:manage?)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Test::Shared::Auth::Passkeys::Services::SendEmergencyRegistration
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
test "raises ActiveRecord::RecordNotFound if no resource with this email address" do
|
8
|
+
assert_raises ActiveRecord::RecordNotFound do
|
9
|
+
service_class.new(
|
10
|
+
email: "bad@example.com",
|
11
|
+
ip_address: Faker::Internet.ip_v4_address,
|
12
|
+
user_agent: Faker::Internet.user_agent,
|
13
|
+
).run!
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
test "does not break if no ip_address given" do
|
18
|
+
assert_successful_run(service: service_class.new(
|
19
|
+
email: valid_email,
|
20
|
+
ip_address: " ",
|
21
|
+
user_agent: Faker::Internet.user_agent,
|
22
|
+
))
|
23
|
+
end
|
24
|
+
|
25
|
+
test "does not break if no user_agent given" do
|
26
|
+
assert_successful_run(service: service_class.new(
|
27
|
+
email: valid_email,
|
28
|
+
ip_address: Faker::Internet.ip_v4_address,
|
29
|
+
user_agent: " ",
|
30
|
+
))
|
31
|
+
end
|
32
|
+
|
33
|
+
test "returns true if the email was sent" do
|
34
|
+
assert_successful_run(service: service_class.new(
|
35
|
+
email: valid_email,
|
36
|
+
ip_address: Faker::Internet.ip_v4_address,
|
37
|
+
user_agent: Faker::Internet.user_agent,
|
38
|
+
))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Test::Shared::Models::NormalizedEmail
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
test "email: is normalized to lowercase and stripped" do
|
8
|
+
instance = model_instance
|
9
|
+
original_email = instance.email
|
10
|
+
normalized_email = instance.email.strip.downcase
|
11
|
+
instance = model_instance
|
12
|
+
|
13
|
+
instance.update!(email: original_email.upcase)
|
14
|
+
assert_equal instance, model_class.find_by(email: original_email.upcase)
|
15
|
+
assert_equal normalized_email, model_class.find_by(email: original_email.upcase).email
|
16
|
+
|
17
|
+
|
18
|
+
instance.update!(email: "\n\n\t#{original_email.upcase}\n\n\t")
|
19
|
+
assert_equal instance, model_class.find_by(email: "\n\n\t#{original_email.upcase}\n\n\t")
|
20
|
+
assert_equal normalized_email, model_class.find_by(email: "\n\n\t#{original_email.upcase}\n\n\t").email
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Test::Shared::Models::User
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
test "name: required and cannot be blank" do
|
8
|
+
instance = model_instance
|
9
|
+
instance.name = ""
|
10
|
+
assert_equal false, instance.valid?
|
11
|
+
assert_equal true, instance.errors.of_kind?(:name, :blank)
|
12
|
+
|
13
|
+
instance.name = Faker::Name.name
|
14
|
+
assert_equal true, instance.valid?
|
15
|
+
end
|
16
|
+
|
17
|
+
test "email: required and cannot be blank" do
|
18
|
+
instance = model_instance
|
19
|
+
instance.email = ""
|
20
|
+
assert_equal false, instance.valid?
|
21
|
+
assert_equal true, instance.errors.of_kind?(:email, :blank)
|
22
|
+
|
23
|
+
instance.email = Faker::Internet.email
|
24
|
+
assert_equal true, instance.valid?
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Test::Shared::Models::Utility::IPAddress
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
included do
|
6
|
+
test "address: is required and cannot be blank" do
|
7
|
+
instance = new_model_instance(address: nil)
|
8
|
+
assert_equal false, instance.valid?
|
9
|
+
assert_equal true, instance.errors.of_kind?(:address, :blank)
|
10
|
+
|
11
|
+
instance.address = Faker::Internet.ip_v6_address
|
12
|
+
|
13
|
+
assert_equal true, instance.valid?
|
14
|
+
assert_equal false, instance.errors.of_kind?(:address, :blank)
|
15
|
+
end
|
16
|
+
|
17
|
+
test "address: is unique" do
|
18
|
+
old_ip = Faker::Internet.ip_v6_address
|
19
|
+
|
20
|
+
old_instance = new_model_instance(address: old_ip)
|
21
|
+
old_instance.save!
|
22
|
+
|
23
|
+
instance = new_model_instance(address: old_ip)
|
24
|
+
assert_equal false, instance.valid?
|
25
|
+
assert_equal true, instance.errors.of_kind?(:address, :taken)
|
26
|
+
|
27
|
+
instance.address = Faker::Internet.ip_v6_address
|
28
|
+
|
29
|
+
assert_equal true, instance.valid?
|
30
|
+
assert_equal false, instance.errors.of_kind?(:address, :taken)
|
31
|
+
end
|
32
|
+
|
33
|
+
test "upsert_address: upserts a given IP Address" do
|
34
|
+
ip = Faker::Internet.ip_v6_address
|
35
|
+
|
36
|
+
assert_difference "#{model_class}.count", +1 do
|
37
|
+
instance = model_class.upsert_address(address: ip)
|
38
|
+
assert_equal model_class.upsert_address(address: ip), instance
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Test::Shared::Models::Utility::UserAgent
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
test "user_agent: is required and cannot be blank" do
|
8
|
+
instance = new_model_instance(user_agent: nil)
|
9
|
+
assert_equal false, instance.valid?
|
10
|
+
assert_equal true, instance.errors.of_kind?(:user_agent, :blank)
|
11
|
+
|
12
|
+
instance.user_agent = SecureRandom.hex
|
13
|
+
|
14
|
+
assert_equal true, instance.valid?
|
15
|
+
assert_equal false, instance.errors.of_kind?(:user_agent, :blank)
|
16
|
+
end
|
17
|
+
|
18
|
+
test "user_agent: is unique" do
|
19
|
+
old_user_agent = SecureRandom.hex
|
20
|
+
|
21
|
+
old_instance = new_model_instance(user_agent: old_user_agent)
|
22
|
+
old_instance.save!
|
23
|
+
|
24
|
+
instance = new_model_instance(user_agent: old_user_agent)
|
25
|
+
assert_equal false, instance.valid?
|
26
|
+
assert_equal true, instance.errors.of_kind?(:user_agent, :taken)
|
27
|
+
|
28
|
+
instance.user_agent = SecureRandom.hex
|
29
|
+
|
30
|
+
assert_equal true, instance.valid?
|
31
|
+
assert_equal false, instance.errors.of_kind?(:user_agent, :taken)
|
32
|
+
end
|
33
|
+
|
34
|
+
test "upsert_user_agent: upserts a given IP Address" do
|
35
|
+
user_agent = SecureRandom.hex
|
36
|
+
|
37
|
+
assert_difference "#{model_class}.count", +1 do
|
38
|
+
instance = model_class.upsert_user_agent(user_agent: user_agent)
|
39
|
+
assert_equal model_class.upsert_user_agent(user_agent: user_agent), instance
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Views::Button::Styling
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def initialize_style_utilities(appearance: nil, color_variant: nil, size: nil)
|
7
|
+
if appearance.present?
|
8
|
+
self.appearance = Practical::Views::WebAwesome::StyleUtility::AppearanceVariant.new(variants: appearance)
|
9
|
+
end
|
10
|
+
|
11
|
+
if color_variant.present?
|
12
|
+
self.color_variant = Practical::Views::WebAwesome::StyleUtility::ColorVariant.new(variant: color_variant)
|
13
|
+
end
|
14
|
+
|
15
|
+
if size.present?
|
16
|
+
self.size = Practical::Views::WebAwesome::StyleUtility::Size.new(size: size)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def css_classes_from_style_utilities
|
21
|
+
helpers.class_names([appearance&.to_css, color_variant&.to_css, size&.to_css].compact)
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Practical::Views::ErrorHandling
|
2
|
+
def self.build_error_json(model:, helpers:)
|
3
|
+
return model.errors.map do |error|
|
4
|
+
error_container_id = error_container_id(model: model, error: error, helpers: helpers)
|
5
|
+
element_id = error_element_id(model: model, error: error, helpers: helpers)
|
6
|
+
|
7
|
+
{
|
8
|
+
container_id: error_container_id,
|
9
|
+
element_to_invalidate_id: element_id,
|
10
|
+
message: error.message,
|
11
|
+
type: error.type
|
12
|
+
}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.error_container_id(model:, error:, helpers:)
|
17
|
+
if error.options[:error_container_id].present?
|
18
|
+
return error.options[:error_container_id]
|
19
|
+
else
|
20
|
+
attribute_name_parts = error.attribute.to_s.split(".")
|
21
|
+
return helpers.field_id(model, *attribute_name_parts, :errors)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.error_element_id(model:, error:, helpers:)
|
26
|
+
if error.options[:element_id].present?
|
27
|
+
return error.options[:element_id]
|
28
|
+
else
|
29
|
+
attribute_name_parts = error.attribute.to_s.split(".")
|
30
|
+
return helpers.field_id(model, *attribute_name_parts)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|