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.
Files changed (78) 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/fallback_errors_section_component.rb +5 -3
  5. data/app/components/practical/views/form/option_label_component.rb +0 -1
  6. data/app/components/practical/views/navigation/breadcrumb_item_component.rb +0 -1
  7. data/app/components/practical/views/navigation/breadcrumbs_component.rb +2 -1
  8. data/app/{controllers/concerns/practical/auth/passkeys → concerns/practical/auth/passkeys/controllers}/emergency_registrations.rb +2 -2
  9. data/app/{controllers/concerns/practical/auth/passkeys → concerns/practical/auth/passkeys/controllers}/web_authn_debug_context.rb +1 -1
  10. data/app/concerns/practical/memberships/controllers/membership_invitations/register_with_passkey.rb +92 -0
  11. data/app/lib/practical/forms/datatables/base.rb +80 -0
  12. data/app/lib/practical/loaders/base.rb +44 -0
  13. data/app/lib/practical/relation_builders/base.rb +35 -0
  14. data/app/lib/practical/test/shared/attachment/models/attachment/base.rb +123 -0
  15. data/app/lib/practical/test/shared/attachment/models/attachment/for_organization.rb +39 -0
  16. data/app/lib/practical/test/shared/attachment/models/organization/has_attachments.rb +12 -0
  17. data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/base.rb +9 -6
  18. data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/cross_pollination.rb +49 -0
  19. data/app/lib/practical/test/shared/auth/passkeys/controllers/passkey_management/base.rb +508 -0
  20. data/app/lib/practical/test/shared/auth/passkeys/controllers/reauthentication/base.rb +27 -9
  21. data/app/lib/practical/test/shared/auth/passkeys/controllers/reauthentication/cross_pollination.rb +19 -0
  22. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_destroy.rb +26 -8
  23. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_signup.rb +3 -2
  24. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/update.rb +55 -19
  25. data/app/lib/practical/test/shared/auth/passkeys/controllers/sessions/cross_pollination.rb +29 -0
  26. data/app/lib/practical/test/shared/auth/passkeys/forms/emergency_registration.rb +0 -1
  27. data/app/lib/practical/test/shared/auth/passkeys/models/{passkey.rb → passkey/base.rb} +1 -1
  28. data/app/lib/practical/test/shared/auth/passkeys/models/passkey/emergency_registration.rb +23 -0
  29. data/app/lib/practical/test/shared/auth/passkeys/models/{resource_with_passkeys.rb → resource_with_passkeys/base.rb} +1 -1
  30. data/app/lib/practical/test/shared/auth/passkeys/models/resource_with_passkeys/emergency_registration.rb +41 -0
  31. data/app/lib/practical/test/shared/memberships/controllers/membership_invitations/base.rb +165 -0
  32. data/app/lib/practical/test/shared/memberships/controllers/membership_invitations/register_with_passkey.rb +417 -0
  33. data/app/lib/practical/test/shared/memberships/controllers/organization/membership.rb +400 -0
  34. data/app/lib/practical/test/shared/memberships/controllers/organization/membership_invitation.rb +148 -0
  35. data/app/lib/practical/test/shared/memberships/controllers/user/membership.rb +119 -0
  36. data/app/lib/practical/test/shared/memberships/controllers/user/membership_invitation.rb +57 -0
  37. data/app/lib/practical/test/shared/memberships/forms/create_new_user_with_membership_invitation.rb +197 -0
  38. data/app/lib/practical/test/shared/memberships/forms/organization/membership.rb +162 -0
  39. data/app/lib/practical/test/shared/memberships/forms/organization/new_membership_invitation.rb +195 -0
  40. data/app/lib/practical/test/shared/memberships/forms/user/membership.rb +87 -0
  41. data/app/lib/practical/test/shared/memberships/models/membership/base.rb +45 -0
  42. data/app/lib/practical/test/shared/memberships/models/membership_invitation/base.rb +85 -0
  43. data/app/lib/practical/test/shared/memberships/models/membership_invitation/sending.rb +76 -0
  44. data/app/lib/practical/test/shared/memberships/models/membership_invitation/use_for_and_notify.rb +55 -0
  45. data/app/lib/practical/test/shared/memberships/models/organization/base.rb +25 -0
  46. data/app/lib/practical/test/shared/memberships/models/user/base.rb +23 -0
  47. data/app/lib/practical/test/shared/memberships/policies/organization/base_resource.rb +29 -0
  48. data/app/lib/practical/test/shared/memberships/policies/organization/membership.rb +103 -0
  49. data/app/lib/practical/test/shared/memberships/policies/organization/membership_invitation.rb +94 -0
  50. data/app/lib/practical/test/shared/memberships/policies/organization/resource/inherits.rb +10 -0
  51. data/app/lib/practical/test/shared/memberships/policies/organization.rb +70 -0
  52. data/app/lib/practical/test/shared/memberships/policies/user/membership.rb +78 -0
  53. data/app/lib/practical/test/shared/memberships/policies/user/membership_invitation.rb +31 -0
  54. data/app/lib/practical/test/shared/models/normalized_email.rb +0 -1
  55. data/app/lib/practical/test/shared/policies/user/base.rb +14 -0
  56. data/app/lib/practical/views/error_handling.rb +2 -0
  57. data/app/lib/practical/views/error_response.rb +27 -0
  58. data/app/lib/practical/views/form_builders/base.rb +5 -4
  59. data/app/lib/practical/views/form_builders/collection_option.rb +5 -0
  60. data/app/lib/practical/views/icon_set.rb +12 -6
  61. data/config/locales/auth.en.yml +18 -0
  62. data/config/locales/memberships.en.yml +129 -0
  63. data/db/seeds/memberships/default.rb +68 -0
  64. data/db/seeds/moderators/default.rb +36 -0
  65. data/db/seeds/setup.rb +16 -0
  66. data/db/seeds/test/cases/membership_invitations.rb +31 -0
  67. data/db/seeds/users/default.rb +17 -15
  68. data/lib/generators/practical/test/shared_test/shared_test_generator.rb +2 -0
  69. data/lib/practical/framework/engine.rb +8 -0
  70. data/lib/practical/helpers/honeybadger_helper.rb +11 -0
  71. data/lib/practical/helpers/selector_helper.rb +8 -0
  72. data/lib/practical/version.rb +1 -1
  73. data/lib/practical/views/element_helper.rb +2 -0
  74. data/lib/practical/views/theme_helper.rb +13 -0
  75. data/lib/practical.rb +4 -1
  76. data/lib/tasks/practical/utility.rake +20 -0
  77. metadata +54 -11
  78. data/lib/tasks/practical/framework_tasks.rake +0 -6
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Memberships::Models::Membership::Base
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "state: has at least the base set of cases" do
8
+ expected_states = [
9
+ :pending_reacceptance,
10
+ :active,
11
+ :archived_by_organization,
12
+ :archived_by_user,
13
+ ].map(&:to_sym).to_set
14
+
15
+ actual = model_class.states.keys.map(&:to_sym).to_set
16
+
17
+ assert_equal true, expected_states.subset?(actual), [expected_states, actual]
18
+ end
19
+
20
+ test "belongs_to the user resource" do
21
+ reflection = model_class.reflect_on_association(user_reflection_name)
22
+ assert_equal :belongs_to, reflection.macro
23
+ end
24
+
25
+ test "belongs_to the organization resource" do
26
+ reflection = model_class.reflect_on_association(organization_reflection_name)
27
+ assert_equal :belongs_to, reflection.macro
28
+ end
29
+
30
+ test "user: must be unique for an organization" do
31
+ instance = model_instance
32
+
33
+ assert_equal true, instance.valid?
34
+
35
+ new_instance = instance.send(:"#{user_reflection_name}").memberships.build(organization: organization_instance)
36
+
37
+ assert_equal false, new_instance.valid?
38
+ assert_equal true, new_instance.errors.of_kind?(:user, :taken)
39
+
40
+ new_instance = other_user_instance.memberships.build(organization: instance.organization)
41
+ new_instance.valid?
42
+ assert_equal false, new_instance.errors.of_kind?(:user, :taken)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Memberships::Models::MembershipInvitation::Base
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "belongs_to an organization" do
8
+ reflection = model_class.reflect_on_association(organization_reflection_name)
9
+ assert_equal :belongs_to, reflection.macro
10
+ end
11
+
12
+ test "optionally belongs_to a user" do
13
+ reflection = model_class.reflect_on_association(user_reflection_name)
14
+ assert_equal :belongs_to, reflection.macro
15
+ assert_equal user_class.to_s, reflection.class_name
16
+ assert_equal true, reflection.options[:optional]
17
+ end
18
+
19
+ test "optionally belongs_to a membership" do
20
+ reflection = model_class.reflect_on_association(membership_reflection_name)
21
+ assert_equal :belongs_to, reflection.macro
22
+ assert_equal membership_class.to_s, reflection.class_name
23
+ assert_equal true, reflection.options[:optional]
24
+ end
25
+
26
+ test "does not require a membership or user" do
27
+ instance = valid_new_instance
28
+ assert_equal true, instance.valid?
29
+ assert_nil valid_new_instance.user
30
+ assert_nil valid_new_instance.membership
31
+ end
32
+
33
+ test "requires an organization" do
34
+ instance = valid_new_instance
35
+ instance.organization = nil
36
+ assert_equal false, instance.valid?
37
+ assert_equal true, instance.errors.of_kind?(organization_reflection_name, :blank)
38
+ end
39
+
40
+ test "requires a non-blank email" do
41
+ instance = valid_new_instance
42
+ instance.email = nil
43
+ assert_equal false, instance.valid?
44
+ assert_equal true, instance.errors.of_kind?(:email, :blank)
45
+
46
+ instance.email = ""
47
+ assert_equal false, instance.valid?
48
+ assert_equal true, instance.errors.of_kind?(:email, :blank)
49
+
50
+ instance.email = " "
51
+ assert_equal false, instance.valid?
52
+ assert_equal true, instance.errors.of_kind?(:email, :blank)
53
+ end
54
+
55
+ test "an organization can only create 1 invitation per email" do
56
+ organization = organization_instance
57
+ email = Faker::Internet.email
58
+ instance_1 = create_new_instance(organization: organization, email: email)
59
+
60
+ duplicate_instance = valid_new_instance
61
+ duplicate_instance.email = email
62
+ duplicate_instance.organization = organization
63
+
64
+ assert_equal false, duplicate_instance.valid?
65
+ assert_equal true, duplicate_instance.errors.of_kind?(:email, :taken)
66
+ end
67
+
68
+ test "unused scope returns all unused invitations" do
69
+ assert_equal true, model_class.all.any?{|x| x.user.present?}
70
+
71
+ invitations = model_class.unused
72
+ assert_not_empty invitations
73
+ assert_equal true, invitations.all?{|x| x.user.nil? }
74
+ end
75
+
76
+ test "visible scope returns all visible invitations" do
77
+ hidden_invitation = hidden_instance
78
+
79
+ assert_equal false, hidden_invitation.visible?
80
+
81
+ assert_not_empty model_class.visible
82
+ assert_not_includes model_class.visible, hidden_invitation
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Memberships::Models::MembershipInvitation::Sending
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "belongs_to an sender" do
8
+ reflection = model_class.reflect_on_association(sender_reflection_name)
9
+ assert_equal :belongs_to, reflection.macro
10
+ end
11
+
12
+ test "generates_token_for invitation that expires when the user is set" do
13
+ instance = valid_new_instance
14
+ instance.save!
15
+
16
+ token = instance.generate_token_for(:invitation)
17
+
18
+ assert_equal instance, model_class.find_by_token_for(:invitation, token)
19
+
20
+ instance.update!(user: user_instance)
21
+
22
+ assert_nil model_class.find_by_token_for(:invitation, token)
23
+ end
24
+
25
+ test "only validates that the sender has a membership in the organization on creation" do
26
+ invitation = valid_new_instance
27
+ other_user = other_user_instance_with_admin_privledges
28
+
29
+ assert_not_includes other_user.organizations, invitation.organization
30
+
31
+ invitation.sender = other_user
32
+
33
+ assert_equal false, invitation.valid?
34
+ assert_equal true, invitation.errors.of_kind?(:sender, :cannot_manage_organization)
35
+
36
+ invitation.sender = other_user_instance_without_admin_privledges
37
+
38
+ assert_equal false, invitation.valid?
39
+ assert_equal true, invitation.errors.of_kind?(:sender, :cannot_manage_organization)
40
+
41
+ invitation.sender = user_in_organization
42
+
43
+ assert_equal true, invitation.valid?
44
+
45
+ invitation.save!
46
+
47
+ assert_equal true, invitation.valid?
48
+
49
+ invitation.sender = other_user_instance_without_admin_privledges
50
+ assert_equal true, invitation.valid?
51
+ end
52
+
53
+ test "can_be_resent?: has a 10 minute waiting period" do
54
+ membership_invitation = model_class.first
55
+
56
+ assert_nil membership_invitation.last_sent_at
57
+ assert_equal true, membership_invitation.can_be_resent?
58
+
59
+ time = Time.now.utc
60
+
61
+ membership_invitation.update!(last_sent_at: time)
62
+
63
+ Timecop.freeze(time + 2.minutes) do
64
+ assert_equal false, membership_invitation.can_be_resent?
65
+ end
66
+
67
+ Timecop.freeze(time + 5.minutes) do
68
+ assert_equal false, membership_invitation.can_be_resent?
69
+ end
70
+
71
+ Timecop.freeze(time + 10.minutes) do
72
+ assert_equal true, membership_invitation.can_be_resent?
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Memberships::Models::MembershipInvitation::UseForAndNotify
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "use_for_and_notify!: raises AlreadyUsedError if a user is present" do
8
+ invitation = instance_with_user
9
+
10
+ assert_raises model_class::AlreadyUsedError do
11
+ invitation.use_for_and_notify!(user: other_user)
12
+ end
13
+ end
14
+
15
+ test "use_for_and_notify!: raises an ActiveRecord::RecordInvalid error if a membership already exists somehow for this user + organization" do
16
+ user = invited_user_instance
17
+ invitation = model_class.find_by!(email: user.email)
18
+
19
+ invitation.organization.memberships.create!(
20
+ user: user, membership_type: :staff, state: :pending_reacceptance
21
+ )
22
+
23
+ assert_raises ActiveRecord::RecordInvalid do
24
+ invitation.use_for_and_notify!(user: user)
25
+ end
26
+ end
27
+
28
+ test """use_for_and_notify!:
29
+ - links to the given user
30
+ - creates the Membership with the stored membership_type
31
+ - matching mailer class: passkey_added
32
+ """ do
33
+ user = invited_user_instance
34
+
35
+ invitation = model_class.find_by(email: user.email)
36
+
37
+ assert_difference "Membership.count", +1 do
38
+ invitation.use_for_and_notify!(user: user)
39
+ invitation.reload
40
+
41
+ assert_equal user, invitation.user
42
+ assert_equal true, invitation.membership.active?
43
+ assert_equal user, invitation.membership.user
44
+ assert_equal invitation.organization, invitation.membership.organization
45
+ additional_membership_accept_assertions(invitation: invitation)
46
+ end
47
+
48
+ assert_enqueued_email_with(
49
+ mailer_class,
50
+ :invitation_accepted,
51
+ args: [{ membership_invitation: invitation }]
52
+ )
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Memberships::Models::Organization::Base
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "has_many membership resource, dependent: :destroy" do
8
+ reflection = model_class.reflect_on_association(membership_reflection_name)
9
+ assert_equal :has_many, reflection.macro
10
+ assert_equal :destroy, reflection.options[:dependent]
11
+ end
12
+
13
+ test "has_many membership_invitations resource, dependent: :destroy" do
14
+ reflection = model_class.reflect_on_association(membership_invitation_reflection_name)
15
+ assert_equal :has_many, reflection.macro
16
+ assert_equal :destroy, reflection.options[:dependent]
17
+ end
18
+
19
+ test "has_many users resource, through: :memberships resource" do
20
+ reflection = model_class.reflect_on_association(user_reflection_name)
21
+ assert_equal :has_many, reflection.macro
22
+ assert_equal membership_reflection_name, reflection.options[:through]
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Memberships::Models::User::Base
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "has_many membership resource" do
8
+ reflection = model_class.reflect_on_association(membership_reflection_name)
9
+ assert_equal :has_many, reflection.macro
10
+ end
11
+
12
+ test "has_many membership_invitations resource" do
13
+ reflection = model_class.reflect_on_association(membership_invitation_reflection_name)
14
+ assert_equal :has_many, reflection.macro
15
+ end
16
+
17
+ test "has_many organizations resource, through: :memberships resource" do
18
+ reflection = model_class.reflect_on_association(organization_reflection_name)
19
+ assert_equal :has_many, reflection.macro
20
+ assert_equal membership_reflection_name, reflection.options[:through]
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Memberships::Policies::Organization::BaseResource
4
+ extend ActiveSupport::Concern
5
+ included do
6
+ class TestClass
7
+ attr_accessor :organization
8
+
9
+ def initialize(organization:)
10
+ self.organization = organization
11
+ end
12
+ end
13
+
14
+ class TestClassPolicy < policy_class
15
+ def show?
16
+ true
17
+ end
18
+ end
19
+
20
+ test "pre_check? the record is part of the same_organization?" do
21
+ organization = organizations.organization_1
22
+ record = TestClass.new(organization: organization)
23
+ other_record = TestClass.new(organization: organizations.organization_2)
24
+
25
+ assert_equal true, TestClassPolicy.new(record, user: users.organization_1_owner, organization: organization).apply(:show?)
26
+ assert_equal false, TestClassPolicy.new(other_record, user: users.organization_1_owner, organization: organization).apply(:show?)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Memberships::Policies::Organization::Membership
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "manage?: only true for memberships where the user has an organization_manager membership for the same organization" do
8
+ membership = users.works_at_org_1_and_2.memberships.find_by(organization: organizations.organization_1)
9
+
10
+ assert_equal true, policy_for(membership: membership, user: users.organization_1_owner).apply(:manage?)
11
+ assert_equal true, policy_for(membership: membership, user: users.organization_1_manager).apply(:manage?)
12
+
13
+ assert_equal false, policy_for(membership: membership, user: users.works_at_org_1_and_2).apply(:manage?)
14
+ assert_equal false, policy_for(membership: membership, user: users.archived_organization_1_manager).apply(:manage?)
15
+ assert_equal false, policy_for(membership: membership, user: users.organization_2_owner).apply(:manage?)
16
+ assert_equal false, policy_for(membership: membership, user: users.organization_1_staff).apply(:manage?)
17
+ assert_equal false, policy_for(membership: membership, user: users.retired_staff).apply(:manage?)
18
+ end
19
+
20
+ test "archive?: false if the user does not have an organization_manager membership in the same organization" do
21
+ membership = users.works_at_org_1_and_2.memberships.find_by(organization: organizations.organization_1)
22
+
23
+ assert_equal true, policy_for(membership: membership, user: users.organization_1_owner).apply(:archive?)
24
+ assert_equal true, policy_for(membership: membership, user: users.organization_1_manager).apply(:archive?)
25
+
26
+ assert_equal false, policy_for(membership: membership, user: users.works_at_org_1_and_2).apply(:archive?)
27
+ assert_equal false, policy_for(membership: membership, user: users.archived_organization_1_manager).apply(:archive?)
28
+ assert_equal false, policy_for(membership: membership, user: users.organization_2_owner).apply(:archive?)
29
+ assert_equal false, policy_for(membership: membership, user: users.organization_1_staff).apply(:archive?)
30
+ assert_equal false, policy_for(membership: membership, user: users.retired_staff).apply(:archive?)
31
+ end
32
+
33
+ test "archive?: true if the membership is not an organization_manager membership" do
34
+ membership = users.works_at_org_1_and_2.memberships.find_by(organization: organizations.organization_1)
35
+
36
+ assert_equal false, membership.organization_manager?
37
+
38
+ assert_equal true, policy_for(membership: membership, user: users.organization_1_owner).apply(:archive?)
39
+ assert_equal true, policy_for(membership: membership, user: users.organization_1_manager).apply(:archive?)
40
+
41
+ assert_equal false, policy_for(membership: membership, user: users.works_at_org_1_and_2).apply(:archive?)
42
+ assert_equal false, policy_for(membership: membership, user: users.archived_organization_1_manager).apply(:archive?)
43
+ assert_equal false, policy_for(membership: membership, user: users.organization_2_owner).apply(:archive?)
44
+ assert_equal false, policy_for(membership: membership, user: users.organization_1_staff).apply(:archive?)
45
+ assert_equal false, policy_for(membership: membership, user: users.retired_staff).apply(:archive?)
46
+ end
47
+
48
+ test "archive?: only true if the organization_manager membership can be removed" do
49
+ membership = users.organization_1_manager.memberships.find_by(organization: organizations.organization_1)
50
+
51
+ assert_equal true, membership.organization_manager?
52
+ assert_equal true, organization_policy(membership.organization, user: users.organization_1_owner).apply(:remove_organization_manager?)
53
+
54
+ assert_equal true, policy_for(membership: membership, user: users.organization_1_owner).apply(:archive?)
55
+ assert_equal true, policy_for(membership: membership, user: users.organization_1_manager).apply(:archive?)
56
+
57
+ assert_equal false, policy_for(membership: membership, user: users.works_at_org_1_and_2).apply(:archive?)
58
+ assert_equal false, policy_for(membership: membership, user: users.archived_organization_1_manager).apply(:archive?)
59
+ assert_equal false, policy_for(membership: membership, user: users.organization_2_owner).apply(:archive?)
60
+ assert_equal false, policy_for(membership: membership, user: users.organization_1_staff).apply(:archive?)
61
+ assert_equal false, policy_for(membership: membership, user: users.retired_staff).apply(:archive?)
62
+
63
+ other_membership = users.organization_2_owner.memberships.find_by(organization: organizations.organization_2)
64
+ assert_equal true, membership.organization_manager?
65
+ assert_equal false, organization_policy(other_membership.organization, user: users.organization_2_owner).apply(:remove_organization_manager?)
66
+
67
+ assert_equal false, policy_for(membership: membership, user: users.organization_2_owner).apply(:archive?)
68
+ end
69
+
70
+ test "relation only returns the memberships that are part of the same organization" do
71
+ assert_equal_set(
72
+ organizations.organization_1.memberships,
73
+ policy_for(
74
+ membership: users.works_at_org_1_and_2.memberships.find_by(organization: organizations.organization_1),
75
+ user: users.organization_1_manager
76
+ ).apply_scope(all_memberships_relation, type: :active_record_relation)
77
+ )
78
+
79
+ assert_equal_set(
80
+ organizations.organization_2.memberships,
81
+ policy_for(
82
+ membership: users.works_at_org_1_and_2.memberships.find_by(organization: organizations.organization_2),
83
+ user: users.organization_2_owner
84
+ ).apply_scope(all_memberships_relation, type: :active_record_relation)
85
+ )
86
+
87
+ assert_equal_set(
88
+ organizations.organization_1.memberships,
89
+ policy_for(
90
+ membership: users.works_at_org_1_and_2.memberships.find_by(organization: organizations.organization_1),
91
+ user: users.works_at_org_1_and_2
92
+ ).apply_scope(all_memberships_relation, type: :active_record_relation)
93
+ )
94
+
95
+ assert_empty(
96
+ policy_for(
97
+ membership: users.works_at_org_1_and_2.memberships.find_by(organization: organizations.organization_1),
98
+ user: users.organization_2_owner
99
+ ).apply_scope(all_memberships_relation, type: :active_record_relation)
100
+ )
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Memberships::Policies::Organization::MembershipInvitation
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ def assert_can_manage(invitation:, user:)
8
+ assert_equal true, policy_for(invitation: invitation, user: user).apply(:manage?)
9
+ end
10
+
11
+ def assert_cannot_manage(invitation:, user:)
12
+ assert_equal false, policy_for(invitation: invitation, user: user).apply(:manage?)
13
+ end
14
+
15
+ test "manage?: only true for memberships where the user has an organization_manager membership for the same organization" do
16
+ invitation = organizations.organization_1.membership_invitations.create!(
17
+ email: Faker::Internet.email,
18
+ sender: users.organization_1_owner,
19
+ membership_type: :staff
20
+ )
21
+
22
+ assert_equal true, policy_for(invitation: invitation, user: users.organization_1_owner).apply(:manage?)
23
+ assert_equal true, policy_for(invitation: invitation, user: users.organization_1_manager).apply(:manage?)
24
+
25
+ assert_equal false, policy_for(invitation: invitation, user: users.works_at_org_1_and_2).apply(:manage?)
26
+ assert_equal false, policy_for(invitation: invitation, user: users.archived_organization_1_manager).apply(:manage?)
27
+ assert_equal false, policy_for(invitation: invitation, user: users.organization_2_owner).apply(:manage?)
28
+ assert_equal false, policy_for(invitation: invitation, user: users.organization_1_staff).apply(:manage?)
29
+ assert_equal false, policy_for(invitation: invitation, user: users.retired_staff).apply(:manage?)
30
+ end
31
+
32
+ test "archive?: false if the user does not have an organization_manager membership in the same organization" do
33
+ invitation = organizations.organization_1.membership_invitations.create!(
34
+ email: Faker::Internet.email,
35
+ sender: users.organization_1_owner,
36
+ membership_type: :staff
37
+ )
38
+
39
+ assert_equal true, policy_for(invitation: invitation, user: users.organization_1_owner).apply(:archive?)
40
+ assert_equal true, policy_for(invitation: invitation, user: users.organization_1_manager).apply(:archive?)
41
+
42
+ assert_equal false, policy_for(invitation: invitation, user: users.works_at_org_1_and_2).apply(:archive?)
43
+ assert_equal false, policy_for(invitation: invitation, user: users.archived_organization_1_manager).apply(:archive?)
44
+ assert_equal false, policy_for(invitation: invitation, user: users.organization_2_owner).apply(:archive?)
45
+ assert_equal false, policy_for(invitation: invitation, user: users.organization_1_staff).apply(:archive?)
46
+ assert_equal false, policy_for(invitation: invitation, user: users.retired_staff).apply(:archive?)
47
+ end
48
+
49
+ test "relation only returns the membership_invitations that are part of the same organization" do
50
+ organizations.organization_1.membership_invitations.create!(
51
+ email: Faker::Internet.email,
52
+ sender: users.organization_1_owner,
53
+ membership_type: :staff
54
+ )
55
+
56
+ organizations.organization_2.membership_invitations.create!(
57
+ email: Faker::Internet.email,
58
+ sender: users.organization_2_owner,
59
+ membership_type: :staff
60
+ )
61
+
62
+ assert_equal_set(
63
+ organizations.organization_1.membership_invitations,
64
+ relation_policy_for(
65
+ organization: organizations.organization_1,
66
+ user: users.organization_1_manager
67
+ ).apply_scope(all_membership_invitations_relation, type: :active_record_relation)
68
+ )
69
+
70
+ assert_equal_set(
71
+ organizations.organization_2.membership_invitations,
72
+ relation_policy_for(
73
+ organization: organizations.organization_2,
74
+ user: users.organization_2_owner
75
+ ).apply_scope(all_membership_invitations_relation, type: :active_record_relation)
76
+ )
77
+
78
+ assert_equal_set(
79
+ organizations.organization_1.membership_invitations,
80
+ relation_policy_for(
81
+ organization: organizations.organization_1,
82
+ user: users.works_at_org_1_and_2
83
+ ).apply_scope(all_membership_invitations_relation, type: :active_record_relation)
84
+ )
85
+
86
+ assert_empty(
87
+ relation_policy_for(
88
+ organization: organizations.organization_1,
89
+ user: users.organization_2_owner
90
+ ).apply_scope(all_membership_invitations_relation, type: :active_record_relation)
91
+ )
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Memberships::Policies::Organization::Resource::Inherits
4
+ extend ActiveSupport::Concern
5
+ included do
6
+ test "is a subclass of the resource_policy_class" do
7
+ assert_includes policy_class.ancestors, resource_policy_class
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Memberships::Policies::Organization
4
+ extend ActiveSupport::Concern
5
+ included do
6
+ test "default scope: returns all the organizations the user has access to" do
7
+ assert_equal_set(
8
+ [organizations.organization_1],
9
+ relation_policy(user: users.organization_1_owner)
10
+ )
11
+
12
+ assert_equal_set(
13
+ [organizations.organization_1],
14
+ relation_policy(user: users.organization_1_manager)
15
+ )
16
+
17
+ assert_equal_set(
18
+ [organizations.organization_1, organizations.organization_2],
19
+ relation_policy(user: users.works_at_org_1_and_2)
20
+ )
21
+
22
+ assert_equal_set(
23
+ [organizations.organization_2],
24
+ relation_policy(user: users.organization_2_owner)
25
+ )
26
+
27
+ assert_empty relation_policy(user: users.organization_1_staff)
28
+ assert_empty relation_policy(user: users.retired_staff)
29
+ assert_empty relation_policy(user: users.archived_organization_1_manager)
30
+ end
31
+
32
+ test "show?: only true when the user has an active membership" do
33
+ organization = organizations.organization_1
34
+
35
+ assert_equal true, policy(organization: organization, user: users.organization_1_owner).apply(:show?)
36
+ assert_equal true, policy(organization: organization, user: users.organization_1_manager).apply(:show?)
37
+ assert_equal true, policy(organization: organization, user: users.works_at_org_1_and_2).apply(:show?)
38
+
39
+ assert_equal false, policy(organization: organization, user: users.retired_staff).apply(:show?)
40
+ assert_equal false, policy(organization: organization, user: users.archived_organization_1_manager).apply(:show?)
41
+
42
+ assert_equal false, policy(organization: organization, user: users.organization_1_staff).apply(:show?)
43
+ assert_equal false, policy(organization: organization, user: users.organization_2_owner).apply(:show?)
44
+ end
45
+
46
+ test "manage?: only true when the user has an active organization_manager membership" do
47
+ organization = organizations.organization_1
48
+
49
+ assert_equal true, policy(organization: organization, user: users.organization_1_owner).apply(:manage?)
50
+ assert_equal true, policy(organization: organization, user: users.organization_1_manager).apply(:manage?)
51
+
52
+ assert_equal false, policy(organization: organization, user: users.works_at_org_1_and_2).apply(:manage?)
53
+ assert_equal false, policy(organization: organization, user: users.retired_staff).apply(:manage?)
54
+ assert_equal false, policy(organization: organization, user: users.archived_organization_1_manager).apply(:manage?)
55
+
56
+ assert_equal false, policy(organization: organization, user: users.organization_1_staff).apply(:manage?)
57
+ assert_equal false, policy(organization: organization, user: users.organization_2_owner).apply(:manage?)
58
+ end
59
+
60
+ test "remove_organization_manager?: only allowed if more than 1 organization_manager" do
61
+ organization_1 = organizations.organization_1
62
+ assert_equal true, policy_class.new(organization_1, user: users.organization_1_owner).apply(:remove_organization_manager?)
63
+ assert_equal false, policy_class.new(organizations.organization_2, user: users.organization_2_owner).apply(:remove_organization_manager?)
64
+
65
+ organization_1.memberships.where(user: users.organization_1_manager).delete_all
66
+
67
+ assert_equal false, policy_class.new(organization_1, user: users.organization_1_owner).apply(:remove_organization_manager?)
68
+ end
69
+ end
70
+ end