practical 0.1.0 → 3.0.0.pre.alpha1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +4 -4
- data/app/components/practical/views/flash_messages_component.rb +0 -1
- data/app/components/practical/views/form/fallback_errors_section_component.rb +5 -3
- data/app/components/practical/views/form/option_label_component.rb +0 -1
- data/app/components/practical/views/navigation/breadcrumb_item_component.rb +0 -1
- data/app/components/practical/views/navigation/breadcrumbs_component.rb +2 -1
- data/app/{controllers/concerns/practical/auth/passkeys → concerns/practical/auth/passkeys/controllers}/emergency_registrations.rb +2 -2
- data/app/{controllers/concerns/practical/auth/passkeys → concerns/practical/auth/passkeys/controllers}/web_authn_debug_context.rb +1 -1
- data/app/concerns/practical/memberships/controllers/membership_invitations/register_with_passkey.rb +92 -0
- data/app/lib/practical/forms/datatables/base.rb +80 -0
- data/app/lib/practical/loaders/base.rb +44 -0
- data/app/lib/practical/relation_builders/base.rb +35 -0
- data/app/lib/practical/test/shared/attachment/models/attachment/base.rb +123 -0
- data/app/lib/practical/test/shared/attachment/models/attachment/for_organization.rb +39 -0
- data/app/lib/practical/test/shared/attachment/models/organization/has_attachments.rb +12 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/base.rb +9 -6
- data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/cross_pollination.rb +49 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/passkey_management/base.rb +508 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/reauthentication/base.rb +27 -9
- data/app/lib/practical/test/shared/auth/passkeys/controllers/reauthentication/cross_pollination.rb +19 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_destroy.rb +26 -8
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_signup.rb +3 -2
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/update.rb +55 -19
- data/app/lib/practical/test/shared/auth/passkeys/controllers/sessions/cross_pollination.rb +29 -0
- data/app/lib/practical/test/shared/auth/passkeys/forms/emergency_registration.rb +0 -1
- data/app/lib/practical/test/shared/auth/passkeys/models/{passkey.rb → passkey/base.rb} +1 -1
- data/app/lib/practical/test/shared/auth/passkeys/models/passkey/emergency_registration.rb +23 -0
- data/app/lib/practical/test/shared/auth/passkeys/models/{resource_with_passkeys.rb → resource_with_passkeys/base.rb} +1 -1
- data/app/lib/practical/test/shared/auth/passkeys/models/resource_with_passkeys/emergency_registration.rb +41 -0
- data/app/lib/practical/test/shared/memberships/controllers/membership_invitations/base.rb +165 -0
- data/app/lib/practical/test/shared/memberships/controllers/membership_invitations/register_with_passkey.rb +417 -0
- data/app/lib/practical/test/shared/memberships/controllers/organization/membership.rb +400 -0
- data/app/lib/practical/test/shared/memberships/controllers/organization/membership_invitation.rb +148 -0
- data/app/lib/practical/test/shared/memberships/controllers/user/membership.rb +119 -0
- data/app/lib/practical/test/shared/memberships/controllers/user/membership_invitation.rb +57 -0
- data/app/lib/practical/test/shared/memberships/forms/create_new_user_with_membership_invitation.rb +197 -0
- data/app/lib/practical/test/shared/memberships/forms/organization/membership.rb +162 -0
- data/app/lib/practical/test/shared/memberships/forms/organization/new_membership_invitation.rb +195 -0
- data/app/lib/practical/test/shared/memberships/forms/user/membership.rb +87 -0
- data/app/lib/practical/test/shared/memberships/models/membership/base.rb +45 -0
- data/app/lib/practical/test/shared/memberships/models/membership_invitation/base.rb +85 -0
- data/app/lib/practical/test/shared/memberships/models/membership_invitation/sending.rb +76 -0
- data/app/lib/practical/test/shared/memberships/models/membership_invitation/use_for_and_notify.rb +55 -0
- data/app/lib/practical/test/shared/memberships/models/organization/base.rb +25 -0
- data/app/lib/practical/test/shared/memberships/models/user/base.rb +23 -0
- data/app/lib/practical/test/shared/memberships/policies/organization/base_resource.rb +29 -0
- data/app/lib/practical/test/shared/memberships/policies/organization/membership.rb +103 -0
- data/app/lib/practical/test/shared/memberships/policies/organization/membership_invitation.rb +94 -0
- data/app/lib/practical/test/shared/memberships/policies/organization/resource/inherits.rb +10 -0
- data/app/lib/practical/test/shared/memberships/policies/organization.rb +70 -0
- data/app/lib/practical/test/shared/memberships/policies/user/membership.rb +78 -0
- data/app/lib/practical/test/shared/memberships/policies/user/membership_invitation.rb +31 -0
- data/app/lib/practical/test/shared/models/normalized_email.rb +0 -1
- data/app/lib/practical/test/shared/policies/user/base.rb +14 -0
- data/app/lib/practical/views/error_handling.rb +2 -0
- data/app/lib/practical/views/error_response.rb +27 -0
- data/app/lib/practical/views/form_builders/base.rb +5 -4
- data/app/lib/practical/views/form_builders/collection_option.rb +5 -0
- data/app/lib/practical/views/icon_set.rb +12 -6
- data/config/locales/auth.en.yml +18 -0
- data/config/locales/memberships.en.yml +129 -0
- data/db/seeds/memberships/default.rb +68 -0
- data/db/seeds/moderators/default.rb +36 -0
- data/db/seeds/setup.rb +16 -0
- data/db/seeds/test/cases/membership_invitations.rb +31 -0
- data/db/seeds/users/default.rb +17 -15
- data/lib/generators/practical/test/shared_test/shared_test_generator.rb +2 -0
- data/lib/practical/framework/engine.rb +8 -0
- data/lib/practical/helpers/honeybadger_helper.rb +11 -0
- data/lib/practical/helpers/selector_helper.rb +8 -0
- data/lib/practical/version.rb +1 -1
- data/lib/practical/views/element_helper.rb +2 -0
- data/lib/practical/views/theme_helper.rb +13 -0
- data/lib/practical.rb +4 -1
- data/lib/tasks/practical/utility.rake +20 -0
- metadata +54 -11
- data/lib/tasks/practical/framework_tasks.rake +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: df17b4a0aff5baa56ac16602fe85779f7693b00331bff301258f084dbd4c15ef
|
4
|
+
data.tar.gz: a9ba95edf2300f43c70c72e028282a1045d831f76128cc705c1aaf7ab287e6dc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 38706fd22c3881826b066a9dd4d1032395d19ce2fe062b0ea64dca1cb974b4de0944bd7ddfc563277495dbdc35cbb0ca4ae83c264ed884515b09dec74c0f9bac
|
7
|
+
data.tar.gz: bbab74d5607eb57b2eeff685935cd20421170e2ce99c4eee30bf593e2581b4fc97d9c4c86d1e82698d0c1a2b76fd3a6b2a8156a2d4c30d103afcc344c41327cd
|
data/README.md
CHANGED
@@ -1,18 +1,18 @@
|
|
1
1
|
# `practical`: The Practical Framework
|
2
2
|
|
3
|
-
_This gem
|
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 `
|
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
|
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
|
15
|
+
$ gem install practical
|
16
16
|
|
17
17
|
## Usage
|
18
18
|
|
@@ -1,16 +1,18 @@
|
|
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: ["error-section", "fallback-error-section", "wa-callout", "wa-danger"],
|
15
|
+
id: id
|
14
16
|
}, @options)
|
15
17
|
end
|
16
18
|
|
@@ -34,7 +34,8 @@ class Practical::Views::Navigation::BreadcrumbsComponent < Practical::Views::Bas
|
|
34
34
|
end
|
35
35
|
|
36
36
|
def build_crumb(crumb:)
|
37
|
-
|
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
|
data/app/concerns/practical/memberships/controllers/membership_invitations/register_with_passkey.rb
ADDED
@@ -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,
|
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
|
-
|
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,
|
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,
|
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
|
-
|
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
|
-
|
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")
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Test::Shared::Auth::Passkeys::Controllers::EmergencyRegistration::CrossPollination
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
test "show: raises ActiveSupport::MessageVerifier::InvalidSignature if a different resource's Emergency Registration token is given" do
|
8
|
+
assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
|
9
|
+
show_emergency_registration_action(token: valid_emergency_registration_token_for_other_resource)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
test "new_create_challenge: raises ActiveSupport::MessageVerifier::InvalidSignature if a different resource's Emergency Registration token is given" do
|
14
|
+
assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
|
15
|
+
get_new_challenge_action(token: valid_emergency_registration_token_for_other_resource)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
test "use: raises ActiveSupport::MessageVerifier::InvalidSignature if a different resource's Emergency Registration token is given" do
|
20
|
+
emergency_passkey_registration = valid_emergency_registration
|
21
|
+
assert_nil emergency_passkey_registration.used_at
|
22
|
+
|
23
|
+
token = emergency_passkey_registration.generate_token_for(:emergency_registration)
|
24
|
+
|
25
|
+
get_new_challenge_action(token: token)
|
26
|
+
assert_response :ok
|
27
|
+
|
28
|
+
assert_passkey_registration_challenge(
|
29
|
+
data: response.parsed_body,
|
30
|
+
stored_challenge: expected_stored_challenge,
|
31
|
+
relying_party_data: expected_relying_party_data,
|
32
|
+
user_data: expected_user_data_for_challenge,
|
33
|
+
credentials_to_exclude: expected_credentials_to_exclude
|
34
|
+
)
|
35
|
+
|
36
|
+
challenge = expected_stored_challenge
|
37
|
+
client = webauthn_client
|
38
|
+
|
39
|
+
raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
|
40
|
+
label = Faker::Computer.os
|
41
|
+
|
42
|
+
params = params_for_using_emergency_passkey_registration(label: label, raw_credential: raw_credential)
|
43
|
+
|
44
|
+
assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
|
45
|
+
use_emergency_registration_action(token: valid_emergency_registration_token_for_other_resource, params: params)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|