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,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
@@ -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