practical 0.1.0 → 3.0.0.pre.alpha2

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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -4
  3. data/app/components/practical/views/flash_messages_component.rb +0 -1
  4. data/app/components/practical/views/form/error_list_component.rb +1 -1
  5. data/app/components/practical/views/form/error_list_item_component.rb +2 -2
  6. data/app/components/practical/views/form/error_list_item_template_component.rb +1 -1
  7. data/app/components/practical/views/form/fallback_errors_section_component.rb +6 -3
  8. data/app/components/practical/views/form/option_label_component.rb +0 -1
  9. data/app/components/practical/views/navigation/breadcrumb_item_component.rb +0 -1
  10. data/app/components/practical/views/navigation/breadcrumbs_component.rb +2 -1
  11. data/app/{controllers/concerns/practical/auth/passkeys → concerns/practical/auth/passkeys/controllers}/emergency_registrations.rb +2 -2
  12. data/app/{controllers/concerns/practical/auth/passkeys → concerns/practical/auth/passkeys/controllers}/web_authn_debug_context.rb +1 -1
  13. data/app/concerns/practical/memberships/controllers/membership_invitations/register_with_passkey.rb +92 -0
  14. data/app/lib/practical/forms/datatables/base.rb +80 -0
  15. data/app/lib/practical/loaders/base.rb +44 -0
  16. data/app/lib/practical/relation_builders/base.rb +35 -0
  17. data/app/lib/practical/test/shared/attachment/models/attachment/base.rb +123 -0
  18. data/app/lib/practical/test/shared/attachment/models/attachment/for_organization.rb +39 -0
  19. data/app/lib/practical/test/shared/attachment/models/organization/has_attachments.rb +12 -0
  20. data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/base.rb +9 -6
  21. data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/cross_pollination.rb +49 -0
  22. data/app/lib/practical/test/shared/auth/passkeys/controllers/passkey_management/base.rb +508 -0
  23. data/app/lib/practical/test/shared/auth/passkeys/controllers/reauthentication/base.rb +27 -9
  24. data/app/lib/practical/test/shared/auth/passkeys/controllers/reauthentication/cross_pollination.rb +19 -0
  25. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_destroy.rb +26 -8
  26. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_signup.rb +3 -2
  27. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/update.rb +55 -19
  28. data/app/lib/practical/test/shared/auth/passkeys/controllers/sessions/cross_pollination.rb +29 -0
  29. data/app/lib/practical/test/shared/auth/passkeys/forms/emergency_registration.rb +0 -1
  30. data/app/lib/practical/test/shared/auth/passkeys/models/{passkey.rb → passkey/base.rb} +1 -1
  31. data/app/lib/practical/test/shared/auth/passkeys/models/passkey/emergency_registration.rb +23 -0
  32. data/app/lib/practical/test/shared/auth/passkeys/models/{resource_with_passkeys.rb → resource_with_passkeys/base.rb} +1 -1
  33. data/app/lib/practical/test/shared/auth/passkeys/models/resource_with_passkeys/emergency_registration.rb +41 -0
  34. data/app/lib/practical/test/shared/memberships/controllers/membership_invitations/base.rb +165 -0
  35. data/app/lib/practical/test/shared/memberships/controllers/membership_invitations/register_with_passkey.rb +417 -0
  36. data/app/lib/practical/test/shared/memberships/controllers/organization/membership.rb +400 -0
  37. data/app/lib/practical/test/shared/memberships/controllers/organization/membership_invitation.rb +148 -0
  38. data/app/lib/practical/test/shared/memberships/controllers/user/membership.rb +119 -0
  39. data/app/lib/practical/test/shared/memberships/controllers/user/membership_invitation.rb +57 -0
  40. data/app/lib/practical/test/shared/memberships/forms/create_new_user_with_membership_invitation.rb +197 -0
  41. data/app/lib/practical/test/shared/memberships/forms/organization/membership.rb +162 -0
  42. data/app/lib/practical/test/shared/memberships/forms/organization/new_membership_invitation.rb +195 -0
  43. data/app/lib/practical/test/shared/memberships/forms/user/membership.rb +87 -0
  44. data/app/lib/practical/test/shared/memberships/models/membership/base.rb +45 -0
  45. data/app/lib/practical/test/shared/memberships/models/membership_invitation/base.rb +85 -0
  46. data/app/lib/practical/test/shared/memberships/models/membership_invitation/sending.rb +76 -0
  47. data/app/lib/practical/test/shared/memberships/models/membership_invitation/use_for_and_notify.rb +55 -0
  48. data/app/lib/practical/test/shared/memberships/models/organization/base.rb +25 -0
  49. data/app/lib/practical/test/shared/memberships/models/user/base.rb +23 -0
  50. data/app/lib/practical/test/shared/memberships/policies/organization/base_resource.rb +29 -0
  51. data/app/lib/practical/test/shared/memberships/policies/organization/membership.rb +103 -0
  52. data/app/lib/practical/test/shared/memberships/policies/organization/membership_invitation.rb +94 -0
  53. data/app/lib/practical/test/shared/memberships/policies/organization/resource/inherits.rb +10 -0
  54. data/app/lib/practical/test/shared/memberships/policies/organization.rb +70 -0
  55. data/app/lib/practical/test/shared/memberships/policies/user/membership.rb +78 -0
  56. data/app/lib/practical/test/shared/memberships/policies/user/membership_invitation.rb +31 -0
  57. data/app/lib/practical/test/shared/models/normalized_email.rb +0 -1
  58. data/app/lib/practical/test/shared/policies/user/base.rb +14 -0
  59. data/app/lib/practical/views/error_handling.rb +2 -0
  60. data/app/lib/practical/views/error_response.rb +27 -0
  61. data/app/lib/practical/views/form_builders/base.rb +5 -4
  62. data/app/lib/practical/views/form_builders/collection_option.rb +5 -0
  63. data/app/lib/practical/views/icon_set.rb +12 -6
  64. data/config/locales/auth.en.yml +18 -0
  65. data/config/locales/memberships.en.yml +129 -0
  66. data/db/seeds/memberships/default.rb +68 -0
  67. data/db/seeds/moderators/default.rb +36 -0
  68. data/db/seeds/setup.rb +16 -0
  69. data/db/seeds/test/cases/membership_invitations.rb +31 -0
  70. data/db/seeds/users/default.rb +17 -15
  71. data/lib/generators/practical/test/shared_test/shared_test_generator.rb +2 -0
  72. data/lib/practical/framework/engine.rb +9 -1
  73. data/lib/practical/helpers/honeybadger_helper.rb +11 -0
  74. data/lib/practical/helpers/selector_helper.rb +8 -0
  75. data/lib/practical/version.rb +1 -1
  76. data/lib/practical/views/element_helper.rb +2 -0
  77. data/lib/practical/views/theme_helper.rb +13 -0
  78. data/lib/practical.rb +4 -1
  79. data/lib/tasks/practical/utility.rake +20 -0
  80. metadata +54 -11
  81. data/lib/tasks/practical/framework_tasks.rake +0 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da7aaa72d992ff3aaf4a43ed5c374c6ff1652843075713e08fa95ab1244d88bf
4
- data.tar.gz: 6c935a7c4cc3ac3538aa78c2f100b6b009b3113577b53bfba7b39efed5a3878d
3
+ metadata.gz: d5cd9e0fb40bc07050d68f122c8f99b2769ebc047de756ba4baba2df7eb5a5fd
4
+ data.tar.gz: 4f9ea83a5bd5dc2b6da7a8a7b50321b185a1368abf820ba2c7abaac9e82468ae
5
5
  SHA512:
6
- metadata.gz: 88fd0246800371d155ccba2fc6337127a82e31e5ba41fb793bef3558149307914f095cb3e4fa620d86ca34a097f44fcf9278491667dc21bcd10856f04f453f2e
7
- data.tar.gz: d7c98265992ff1ed00a0e9c6562bcebe239e749e4364345c5a364224c6835226909ae7e39802941867d6daa063e26739e02adb74556c5148927fa22ba682953c
6
+ metadata.gz: 790f33b8ef5decf8588d588f798f2e9ac65cb8b776eb0ca108a124e361bed765ba8230d8637918a3e904db2c497237064421028993b876081130efab9482f3d3
7
+ data.tar.gz: 0f07a49cd479ce6ff63d3627b1111e0908536e3b81807aec6daae24b913b342ab54d285b3c33e591d08071737a1da6007ced40112f21f62444efab9089aa4cc9
data/README.md CHANGED
@@ -1,18 +1,18 @@
1
1
  # `practical`: The Practical Framework
2
2
 
3
- _This gem is currently being open-sourced from a private repo, while also being singificantly reorganized. Its 3.0 release will be live soon!_
3
+ _This gem has been open-sourced from a private repo, while also being singificantly reorganized. The code is functional and (mostly) tested; but it is marked as alpha until it can be properly documented, and existing apps switched over to use 3.0_
4
4
 
5
5
  ## Installation
6
6
 
7
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
7
+ TODO: Replace `practical` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
8
8
 
9
9
  Install the gem and add to the application's Gemfile by executing:
10
10
 
11
- $ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
11
+ $ bundle add practical
12
12
 
13
13
  If bundler is not being used to manage dependencies, install the gem by executing:
14
14
 
15
- $ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
15
+ $ gem install practical
16
16
 
17
17
  ## Usage
18
18
 
@@ -48,7 +48,6 @@ class Practical::Views::FlashMessagesComponent < Practical::Views::BaseComponent
48
48
  icon = data[:icon]
49
49
  end
50
50
 
51
-
52
51
  component = Practical::Views::ToastComponent.new(color_variant: color_variant)
53
52
 
54
53
  render component do |component|
@@ -8,7 +8,7 @@ class Practical::Views::Form::ErrorListComponent < Practical::Views::BaseCompone
8
8
  end
9
9
 
10
10
  def call
11
- tag.ul(class: 'error-list') {
11
+ tag.ul(data: {'data-pf-error-container': true}) {
12
12
  safe_join(errors.map{|error| render Practical::Views::Form::ErrorListItemComponent.new(error: error) })
13
13
  }
14
14
  end
@@ -12,9 +12,9 @@ class Practical::Views::Form::ErrorListItemComponent < Practical::Views::BaseCom
12
12
  end
13
13
 
14
14
  def call
15
- tag.li(class: 'wa-flank', data: {"error-type": error.type}) {
15
+ tag.li(class: 'wa-flank', data: {"pf-error-type": error.type, "pf-error-visible": true}) {
16
16
  render(icon_set.error_list_icon) +
17
- tag.span(error.message)
17
+ tag.span(error.message, data: { "pf-error-message": true })
18
18
  }
19
19
  end
20
20
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  class Practical::Views::Form::ErrorListItemTemplateComponent < Practical::Views::BaseComponent
4
4
  def call
5
- tag.template(id: 'error-list-item-template') {
5
+ tag.template(id: 'pf-error-list-item-template') {
6
6
  render Practical::Views::Form::ErrorListItemComponent.new(error: ActiveModel::Error.new(nil, nil, nil))
7
7
  }
8
8
  end
@@ -1,16 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Practical::Views::Form::FallbackErrorsSectionComponent < Practical::Views::BaseComponent
4
- attr_reader :f, :blurb
5
- def initialize(f:, blurb:, options:)
4
+ attr_reader :f, :id, :blurb
5
+ def initialize(f:, id:, blurb:, options:)
6
6
  @f = f
7
+ @id = id
7
8
  @blurb = blurb
8
9
  @options = options
9
10
  end
10
11
 
11
12
  def finalized_options
12
13
  mix({
13
- class: ["error-section", "fallback-error-section", "wa-callout", "wa-danger"]
14
+ class: ["wa-callout", "wa-danger"],
15
+ data: {"pf-error-container": true, "pf-fallback-error-section": true},
16
+ id: id
14
17
  }, @options)
15
18
  end
16
19
 
@@ -9,7 +9,6 @@ class Practical::Views::Form::OptionLabelComponent < Practical::Views::BaseCompo
9
9
  self.options = options
10
10
  end
11
11
 
12
-
13
12
  def call
14
13
  tag.section(**mix({class: "wa-stack wa-size-s wa-gap-0"}, options)) {
15
14
  safe_join([
@@ -8,7 +8,6 @@ class Practical::Views::Navigation::BreadcrumbItemComponent < Practical::Views::
8
8
  self.options = options
9
9
  end
10
10
 
11
-
12
11
  def call
13
12
  tag.wa_breadcrumb_item(**mix({}, options)) {
14
13
  safe_join([
@@ -34,7 +34,8 @@ class Practical::Views::Navigation::BreadcrumbsComponent < Practical::Views::Bas
34
34
  end
35
35
 
36
36
  def build_crumb(crumb:)
37
- render(Practical::Views::Navigation::BreadcrumbItemComponent.new(options: { href: crumb.current? ? nil : crumb.url })) {
37
+ href = crumb.current? ? nil : crumb.url
38
+ render(Practical::Views::Navigation::BreadcrumbItemComponent.new(options: { href: href })) {
38
39
  crumb.name
39
40
  }
40
41
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Practical::Auth::Passkeys::EmergencyRegistrations
3
+ module Practical::Auth::Passkeys::Controllers::EmergencyRegistrations
4
4
  extend ActiveSupport::Concern
5
- include Practical::Auth::Passkeys::WebAuthnDebugContext
5
+ include Practical::Auth::Passkeys::Controllers::WebAuthnDebugContext
6
6
 
7
7
  def new_challenge
8
8
  options_for_registration = generate_registration_options(
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Practical::Auth::Passkeys::WebAuthnDebugContext
3
+ module Practical::Auth::Passkeys::Controllers::WebAuthnDebugContext
4
4
  def honeybadger_webauthn_context
5
5
  debug_credential = WebAuthn::Credential.from_create(parsed_credential, relying_party: relying_party)
6
6
  debug_client_data_json = debug_credential.response.client_data.as_json
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Memberships::Controllers::MembershipInvitations::RegisterWithPasskey
4
+ extend ActiveSupport::Concern
5
+ include Practical::Auth::Passkeys::Controllers::WebAuthnDebugContext
6
+
7
+ def new_create_challenge
8
+ options_for_registration = generate_registration_options(
9
+ relying_party: relying_party,
10
+ user_details: user_details_for_registration,
11
+ exclude: []
12
+ )
13
+
14
+ store_challenge_in_session(options_for_registration: options_for_registration)
15
+
16
+ render json: options_for_registration
17
+ end
18
+
19
+ def raw_credential
20
+ passkey_params[:passkey_credential]
21
+ end
22
+
23
+ def verify_credential_integrity
24
+ return render_credential_missing_or_could_not_be_parsed_error if parsed_credential.nil?
25
+ return render_credential_missing_or_could_not_be_parsed_error unless parsed_credential.kind_of?(Hash)
26
+ rescue JSON::JSONError, TypeError
27
+ return render_credential_missing_or_could_not_be_parsed_error
28
+ end
29
+
30
+ def verify_passkey_challenge
31
+ @webauthn_credential = verify_registration(relying_party: relying_party)
32
+ rescue ::WebAuthn::Error => e
33
+ Honeybadger.notify(e, context: honeybadger_webauthn_context)
34
+ error_key = Warden::WebAuthn::ErrorKeyFinder.webauthn_error_key(exception: e)
35
+ render_passkey_error(message: find_message(error_key))
36
+ return false
37
+ end
38
+
39
+ def honeybadger_webauthn_context
40
+ debug_credential = WebAuthn::Credential.from_create(parsed_credential, relying_party: relying_party)
41
+ debug_client_data_json = debug_credential.response.client_data.as_json
42
+
43
+ return {
44
+ debug_client_data_json: debug_client_data_json,
45
+ relying_party_json: relying_party.as_json
46
+ }
47
+ end
48
+
49
+ def request_form_params
50
+ params.require(:create_new_user_with_membership_invitation_form).permit(:email)
51
+ end
52
+
53
+ def require_email_and_passkey_label
54
+ if request_form_params[:email].blank?
55
+ respond_to_email_missing_error
56
+ return false
57
+ end
58
+
59
+ if passkey_params[:passkey_label].blank?
60
+ respond_to_passkey_label_missing_error
61
+ return false
62
+ end
63
+
64
+ true
65
+ end
66
+
67
+ def render_credential_missing_or_could_not_be_parsed_error
68
+ render_passkey_error(message: find_message(:credential_missing_or_could_not_be_parsed))
69
+ delete_registration_challenge
70
+ return false
71
+ end
72
+
73
+ def render_passkey_error(message:)
74
+ errors = Practical::Views::ErrorHandling.build_error_json(
75
+ model: temporary_form_with_passkey_credential_error(message: message),
76
+ helpers: helpers
77
+ )
78
+ render json: errors, status: :bad_request
79
+ end
80
+
81
+ def registration_user_id
82
+ session[registration_user_id_key]
83
+ end
84
+
85
+ def delete_registration_user_id!
86
+ session.delete(registration_user_id_key)
87
+ end
88
+
89
+ def store_registration_user_id
90
+ session[registration_user_id_key] = WebAuthn.generate_user_id
91
+ end
92
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Forms::Datatables::Base
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ attr_accessor :sort_key, :sort_direction, :filters, :sanitized
8
+
9
+ before_validation :normalize_sort_key_and_direction
10
+ validate :matches_schema?
11
+ end
12
+
13
+ def initialize(attributes = {})
14
+ super
15
+ self.sanitize!
16
+ if self.filters.present?
17
+ self.filters = self.class.filter_class.new(self.filters)
18
+ end
19
+ end
20
+
21
+ def sanitized?
22
+ return self.sanitized == true
23
+ end
24
+
25
+ def payload
26
+ {
27
+ sort_key: sort_key,
28
+ sort_direction: sort_direction,
29
+ filters: filters.to_h,
30
+ }
31
+ end
32
+
33
+ def merged_payload(filters: nil, sort_key: nil, sort_direction: nil)
34
+ result = payload
35
+ result[:filters] ||= {}
36
+ if filters.present?
37
+ result[:filters].merge!(filters)
38
+ end
39
+
40
+ result[:sort_key] = sort_key if sort_key.present?
41
+ result[:sort_direction] = sort_direction if sort_direction.present?
42
+
43
+ return result
44
+ end
45
+
46
+ def sanitize!
47
+ validate!
48
+ self.sanitized = true
49
+ rescue ActiveModel::ValidationError
50
+ self.attributes = self.class.default_payload
51
+ self.sanitized = true
52
+ self.errors.clear
53
+ self.validate!
54
+ end
55
+
56
+ def sort_direction_for(key:)
57
+ return nil unless key.downcase.strip == self.sort_key.downcase.strip
58
+ return sort_direction
59
+ end
60
+
61
+ def inverted_sort_direction_for(key:)
62
+ return "asc" unless key.downcase.strip == self.sort_key.downcase.strip
63
+ case sort_direction
64
+ when "asc"
65
+ "desc"
66
+ else
67
+ "asc"
68
+ end
69
+ end
70
+
71
+ def normalize_sort_key_and_direction
72
+ self.sort_key = sort_key.to_s.downcase.strip
73
+ self.sort_direction = sort_direction.to_s.downcase.strip
74
+ end
75
+
76
+ def matches_schema?
77
+ return if self.class.schema.validate?(payload)
78
+ errors.add(:base, :payload_does_not_match_schema)
79
+ end
80
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Loaders::Base
4
+ include Pagy::Backend
5
+
6
+ attr_accessor :params, :base_relation, :datatable_form, :relation_builder, :pagy_instance, :records
7
+
8
+ def initialize(params:, base_relation:)
9
+ self.params = params
10
+ self.base_relation = base_relation
11
+ end
12
+
13
+ def self.load(params:, base_relation:)
14
+ instance = self.new(params: params, base_relation: base_relation)
15
+ instance.load
16
+ return instance
17
+ end
18
+
19
+ def load
20
+ self.datatable_form = build_datatable_form
21
+ self.relation_builder = build_relation_builder
22
+ self.pagy_instance, self.records = pagy(relation_builder.applied_relation, overflow: :last_page)
23
+ end
24
+
25
+ def datatable_payload
26
+ (datatable_params[:datatable] || default_payload)
27
+ end
28
+
29
+ def datatable_params
30
+ params.permit(datatable: [:sort_key, :sort_direction, filters: {}])
31
+ end
32
+
33
+ def build_datatable_form
34
+ raise NotImplementedError
35
+ end
36
+
37
+ def build_relation_builder
38
+ raise NotImplementedError
39
+ end
40
+
41
+ def default_payload
42
+ raise NotImplementedError
43
+ end
44
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::RelationBuilders::Base
4
+ attr_accessor :payload, :relation
5
+
6
+ def initialize(payload:, relation:)
7
+ self.payload = payload
8
+ self.relation = relation
9
+ end
10
+
11
+ def applied_relation
12
+ scope = apply_filtering(scope: relation)
13
+ scope = apply_ordering(scope: scope)
14
+
15
+ return scope
16
+ end
17
+
18
+ def apply_filtering(scope:)
19
+ raise NotImplementedError
20
+ end
21
+
22
+ def apply_ordering(scope:)
23
+ scope = first_order_sorting(scope: scope)
24
+ scope = second_order_sorting(scope: scope)
25
+ return scope
26
+ end
27
+
28
+ def first_order_sorting(scope:)
29
+ raise NotImplementedError
30
+ end
31
+
32
+ def second_order_sorting(scope:)
33
+ raise NotImplementedError
34
+ end
35
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Attachment::Models::Attachment::Base
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ def assert_file_metadata(attachment:, file_size:, file_name:, mime_type:)
8
+ assert_equal file_size, attachment.attachment.metadata["size"]
9
+ assert_equal file_name, attachment.attachment.metadata["filename"]
10
+ assert_equal mime_type, attachment.attachment.metadata["mime_type"]
11
+ end
12
+
13
+ def assert_file_type_support(file:, filepath:, mime_type:)
14
+ file_size = File.size(filepath)
15
+ file_name = File.basename(filepath)
16
+
17
+ create_saved_attachment(file: file) do |attachment|
18
+ assert_file_metadata(attachment: attachment,
19
+ file_size: file_size,
20
+ file_name: file_name,
21
+ mime_type: mime_type
22
+ )
23
+ end
24
+ end
25
+
26
+ def assert_invalid_file_size_too_large(attachment:)
27
+ assert_equal false, attachment.valid?
28
+ assert_equal ["size must not be greater than 20.0 MB"], attachment.errors[:attachment]
29
+ end
30
+
31
+ def assert_invalid_incorrect_mime_type(attachment:)
32
+ assert_equal false, attachment.valid?
33
+ assert_match %r{type must be one of:}, attachment.errors[:attachment].first
34
+ end
35
+
36
+ test "saves the file under attachment_data" do
37
+ assert_file_type_support(file: image_file,
38
+ filepath: image_filepath,
39
+ mime_type: "image/jpeg"
40
+ )
41
+ end
42
+
43
+ test "allows CSV attachments" do
44
+ assert_file_type_support(file: csv_file,
45
+ filepath: csv_filepath,
46
+ mime_type: "text/csv"
47
+ )
48
+ end
49
+
50
+ test "allows PDF attachments" do
51
+ assert_file_type_support(file: pdf_file,
52
+ filepath: pdf_filepath,
53
+ mime_type: "application/pdf"
54
+ )
55
+ end
56
+
57
+ test "allows Word attachments" do
58
+ assert_file_type_support(file: word_file,
59
+ filepath: word_filepath,
60
+ mime_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
61
+ )
62
+ end
63
+
64
+ test "allows Excel attachments" do
65
+ assert_file_type_support(file: excel_file,
66
+ filepath: excel_filepath,
67
+ mime_type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
68
+ )
69
+ end
70
+
71
+ test "allows Numbers attachments" do
72
+ assert_file_type_support(file: numbers_file,
73
+ filepath: numbers_filepath,
74
+ mime_type: "application/vnd.apple.numbers"
75
+ )
76
+ end
77
+
78
+ test "allows HEIC attachments" do
79
+ assert_file_type_support(file: heic_file,
80
+ filepath: heic_filepath,
81
+ mime_type: "image/heic"
82
+ )
83
+ end
84
+
85
+ test "allows plain text attachments" do
86
+ assert_file_type_support(file: plaintext_file,
87
+ filepath: plaintext_filepath,
88
+ mime_type: "text/plain"
89
+ )
90
+ end
91
+
92
+ test "allows RTF attachments" do
93
+ assert_file_type_support(file: rtf_file,
94
+ filepath: rtf_filepath,
95
+ mime_type: "application/rtf"
96
+ )
97
+ end
98
+
99
+ test "limits attachments based on size" do
100
+ file = image_file
101
+ overage_file_size = 21*1024*1024 # 21 MB
102
+
103
+ Spy.on(file, size: overage_file_size)
104
+
105
+ attachment = build_new_attachment(file: file)
106
+
107
+ assert_invalid_file_size_too_large(attachment: attachment)
108
+ end
109
+
110
+ test "does not allow attachments that are not of the correct MIME type" do
111
+ Tempfile.open(%w(foo .jpg)) do |file|
112
+ file.write SecureRandom.hex
113
+ bad_mime_type = "application/x-sh"
114
+
115
+ Spy.on(uploader_class, mime_type: "image/heic")
116
+
117
+ attachment = build_new_attachment(file: file)
118
+
119
+ assert_invalid_incorrect_mime_type(attachment: attachment)
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Attachment::Models::Attachment::ForOrganization
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "belongs_to the organization" do
8
+ reflection = model_class.reflect_on_association(:organization)
9
+ assert_equal :belongs_to, reflection.macro
10
+ end
11
+
12
+ test "belongs_to the user_reflection_name" do
13
+ reflection = model_class.reflect_on_association(user_reflection_name)
14
+ assert_equal :belongs_to, reflection.macro
15
+ end
16
+
17
+ test "validates that the user has a membership in the organization" do
18
+ attachment = valid_new_attachment
19
+
20
+ assert_not_includes other_user.organizations, attachment.organization
21
+
22
+ attachment.user = other_user
23
+
24
+ assert_equal false, attachment.valid?
25
+ assert_equal true, attachment.errors.of_kind?(:user, :cannot_access_organization)
26
+
27
+ attachment.user = regular_user_in_organization
28
+ assert_equal true, attachment.valid?
29
+
30
+ attachment.user = admin_user_in_organization
31
+ assert_equal true, attachment.valid?
32
+
33
+ attachment.save!
34
+
35
+ attachment.user = other_user
36
+ assert_equal true, attachment.valid?
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Attachment::Models::Organization::HasAttachments
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "has_many: attachments" do
8
+ reflection = model_class.reflect_on_association(:attachments)
9
+ assert_equal :has_many, reflection.macro
10
+ end
11
+ end
12
+ end
@@ -93,7 +93,8 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::EmergencyRegistrati
93
93
  assert_json_redirected_to expected_new_session_url
94
94
  end
95
95
 
96
- credential = hydrate_response_from_raw_credential(client: client, relying_party: webauthn_relying_party, raw_credential: raw_credential).credential
96
+ credential = hydrate_response_from_raw_credential(client: client, relying_party: webauthn_relying_party,
97
+ raw_credential: raw_credential).credential
97
98
 
98
99
  new_passkey = emergency_passkey_registration.reload.passkey
99
100
  assert_equal label, new_passkey.label
@@ -103,7 +104,7 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::EmergencyRegistrati
103
104
  end
104
105
 
105
106
  test "use: does not allow overriding who the passkey is registered for" do
106
- old_owner = owner_instance
107
+ owner_instance
107
108
  emergency_passkey_registration = valid_emergency_registration
108
109
  assert_nil emergency_passkey_registration.used_at
109
110
 
@@ -126,14 +127,16 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::EmergencyRegistrati
126
127
  raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
127
128
  label = Faker::Computer.os
128
129
 
129
- params = params_that_try_to_override_owner_during_emergency_registration(label: label, raw_credential: raw_credential)
130
+ params = params_that_try_to_override_owner_during_emergency_registration(label: label,
131
+ raw_credential: raw_credential)
130
132
 
131
133
  assert_difference "#{passkey_class}.count", +1 do
132
134
  use_emergency_registration_action(token: token, params: params)
133
135
  assert_json_redirected_to expected_new_session_url
134
136
  end
135
137
 
136
- credential = hydrate_response_from_raw_credential(client: client, relying_party: webauthn_relying_party, raw_credential: raw_credential).credential
138
+ credential = hydrate_response_from_raw_credential(client: client, relying_party: webauthn_relying_party,
139
+ raw_credential: raw_credential).credential
137
140
 
138
141
  new_passkey = emergency_passkey_registration.reload.passkey
139
142
  assert_equal label, new_passkey.label
@@ -321,7 +324,7 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::EmergencyRegistrati
321
324
  challenge = expected_stored_challenge
322
325
  client = webauthn_client
323
326
 
324
- raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
327
+ create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
325
328
  label = Faker::Computer.os
326
329
 
327
330
  params = params_for_using_emergency_passkey_registration(label: label, raw_credential: nil)
@@ -355,7 +358,7 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::EmergencyRegistrati
355
358
  challenge = expected_stored_challenge
356
359
  client = webauthn_client
357
360
 
358
- raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
361
+ create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
359
362
  label = Faker::Computer.os
360
363
 
361
364
  params = params_for_using_emergency_passkey_registration(label: label, raw_credential: "blah")