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,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Memberships::Controllers::User::MembershipInvitation
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "destroy: destroys a pending invitation and returns a flash message" do
8
+ resource = resource_instance
9
+ sign_in_as_resource(resource: resource)
10
+
11
+ membership_invitation = membership_invitation_class.find_by!(email: resource.email)
12
+
13
+ assert_policies_applied(resource: resource, membership_invitation: membership_invitation) do
14
+ assert_difference "#{membership_invitation_class}.count", -1 do
15
+ delete_invitation_action(membership_invitation: membership_invitation)
16
+ end
17
+ end
18
+
19
+ assert_redirected_to user_memberships_url
20
+ message = I18n.t('user_memberships.invitation_hidden_message', organization_name: membership_invitation.organization.name)
21
+ assert_flash_message(type: :alert, message: message, icon_name: 'solid-warehouse-slash')
22
+ end
23
+
24
+ test "destroy: returns 404 if the invitation is already tied to a user" do
25
+ resource = other_resource_instance
26
+ sign_in_as_resource(resource: resource)
27
+
28
+ membership_invitation = membership_invitation_class.find_by!(email: resource.email)
29
+
30
+ assert_policies_applied_on_404(resource: resource) do
31
+ assert_no_difference "membership_invitation_class.count" do
32
+ delete_invitation_action(membership_invitation: membership_invitation)
33
+ end
34
+ end
35
+
36
+ assert_response :not_found
37
+ assert_no_enqueued_emails
38
+ end
39
+
40
+ test "destroy: returns 404 if the invitation is not visible" do
41
+ resource = resource_instance
42
+ sign_in_as_resource(resource: resource)
43
+
44
+ membership_invitation = membership_invitation_class.find_by!(email: resource.email)
45
+ membership_invitation.update!(visible: false)
46
+
47
+ assert_policies_applied_on_404(resource: resource) do
48
+ assert_no_difference "membership_invitation_class.count" do
49
+ delete_invitation_action(membership_invitation: membership_invitation)
50
+ end
51
+ end
52
+
53
+ assert_response :not_found
54
+ assert_no_enqueued_emails
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Memberships::Forms::CreateNewUserWithMembershipInvitation
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ def dummy_webauthn_credential
8
+ OpenStruct.new(public_key: SecureRandom.hex, raw_id: SecureRandom.bytes(10), sign_count: 0)
9
+ end
10
+
11
+ test "save: merges errors from the attempted new user and re-raises" do
12
+ membership_invitation = organizations.organization_3.membership_invitations.first
13
+ form = form_class.new(
14
+ user: user_class.new(webauthn_id: SecureRandom.hex),
15
+ membership_invitation: membership_invitation,
16
+ email: membership_invitation.email,
17
+ name: " ",
18
+ passkey_label: Faker::Computer.os,
19
+ webauthn_credential: dummy_webauthn_credential
20
+ )
21
+
22
+ assert_no_difference "#{user_class}.count" do
23
+ assert_no_difference "#{passkey_class}.count" do
24
+ assert_raises ActiveRecord::RecordInvalid do
25
+ form.save!
26
+ end
27
+ end
28
+ end
29
+
30
+ assert_equal true, form.errors.of_kind?(:name, :blank)
31
+ end
32
+
33
+ test "save: merges errors when trying to create a user with an existing email" do
34
+ membership_invitation = organizations.organization_3.membership_invitations.first
35
+ form = form_class.new(
36
+ user: user_class.new(webauthn_id: SecureRandom.hex),
37
+ membership_invitation: membership_invitation,
38
+ email: users.organization_1_manager.email,
39
+ name: Faker::Name.name,
40
+ passkey_label: Faker::Computer.os,
41
+ webauthn_credential: dummy_webauthn_credential
42
+ )
43
+
44
+ assert_no_difference "#{user_class}.count" do
45
+ assert_no_difference "#{passkey_class}.count" do
46
+ assert_raises ActiveRecord::RecordInvalid do
47
+ form.save!
48
+ end
49
+ end
50
+ end
51
+
52
+ assert_equal true, form.errors.of_kind?(:email, :taken)
53
+ end
54
+
55
+ test "save: merges errors from the attempted new passkey and re-raises" do
56
+ membership_invitation = organizations.organization_3.membership_invitations.first
57
+
58
+ credential = dummy_webauthn_credential
59
+ credential.public_key = " "
60
+
61
+ form = form_class.new(
62
+ user: user_class.new(webauthn_id: SecureRandom.hex),
63
+ membership_invitation: membership_invitation,
64
+ email: membership_invitation.email,
65
+ name: Faker::Name.name,
66
+ passkey_label: " ",
67
+ webauthn_credential: credential
68
+ )
69
+
70
+ assert_no_difference "#{user_class}.count" do
71
+ assert_no_difference "#{passkey_class}.count" do
72
+ assert_raises ActiveRecord::RecordInvalid do
73
+ form.save!
74
+ end
75
+ end
76
+ end
77
+
78
+ assert_equal true, form.errors.of_kind?(:passkey_label, :blank)
79
+ assert_equal true, form.errors.of_kind?(:public_key, :blank)
80
+ end
81
+ test "save: merges errors when trying to create a passkey that already exists" do
82
+ membership_invitation = organizations.organization_3.membership_invitations.first
83
+
84
+ credential = dummy_webauthn_credential
85
+ credential.public_key = passkey_class.first.public_key
86
+
87
+ form = form_class.new(
88
+ user: user_class.new(webauthn_id: SecureRandom.hex),
89
+ membership_invitation: membership_invitation,
90
+ email: membership_invitation.email,
91
+ name: Faker::Name.name,
92
+ passkey_label: Faker::Computer.os,
93
+ webauthn_credential: credential
94
+ )
95
+
96
+ assert_no_difference "#{user_class}.count" do
97
+ assert_no_difference "#{passkey_class}.count" do
98
+ assert_raises ActiveRecord::RecordInvalid do
99
+ form.save!
100
+ end
101
+ end
102
+ end
103
+
104
+ assert_equal true, form.errors.of_kind?(:public_key, :taken)
105
+ end
106
+
107
+ test "save: raises unexpected errors" do
108
+ membership_invitation = organizations.organization_3.membership_invitations.first
109
+
110
+ Spy.on(membership_invitation, :use_for_and_notify!).and_raise(ArgumentError)
111
+
112
+ form = form_class.new(
113
+ user: user_class.new(webauthn_id: SecureRandom.hex),
114
+ membership_invitation: membership_invitation,
115
+ email: membership_invitation.email,
116
+ name: Faker::Name.name,
117
+ passkey_label: Faker::Computer.os,
118
+ webauthn_credential: dummy_webauthn_credential
119
+ )
120
+
121
+ assert_no_difference "#{user_class}.count" do
122
+ assert_no_difference "#{passkey_class}.count" do
123
+ assert_raises ArgumentError do
124
+ form.save!
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ test "save: raises an AlreadyUsedError if the membership_invitation has a user" do
131
+ membership_invitation = users.invited_user_2.membership_invitations.first
132
+
133
+ form = form_class.new(
134
+ user: user_class.new(webauthn_id: SecureRandom.hex),
135
+ membership_invitation: membership_invitation,
136
+ email: Faker::Internet.email,
137
+ name: Faker::Name.name,
138
+ passkey_label: Faker::Computer.os,
139
+ webauthn_credential: dummy_webauthn_credential
140
+ )
141
+
142
+ assert_no_difference "#{user_class}.count" do
143
+ assert_no_difference "#{passkey_class}.count" do
144
+ assert_raises MembershipInvitation::AlreadyUsedError do
145
+ form.save!
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ test """save:
152
+ - creates the user with the given email & name
153
+ - creates the passkey for the user
154
+ - enqueues the email that the membership_invitation has been accepted
155
+ """ do
156
+ membership_invitation = organizations.organization_3.membership_invitations.first
157
+
158
+ credential = dummy_webauthn_credential
159
+
160
+ form = form_class.new(
161
+ user: user_class.new(webauthn_id: SecureRandom.hex),
162
+ membership_invitation: membership_invitation,
163
+ email: membership_invitation.email,
164
+ name: Faker::Name.name,
165
+ passkey_label: Faker::Computer.os,
166
+ webauthn_credential: credential
167
+ )
168
+
169
+ time = Time.now.utc
170
+
171
+ Timecop.freeze(time) do
172
+ assert_difference "#{user_class}.count", +1 do
173
+ assert_difference "#{passkey_class}.count", +1 do
174
+ form.save!
175
+ end
176
+ end
177
+ end
178
+
179
+ membership_invitation.reload
180
+
181
+ new_user = form.user
182
+
183
+ assert_equal true, new_user.persisted?
184
+ assert_equal form.email, new_user.email
185
+ assert_equal form.name, new_user.name
186
+
187
+ assert_equal new_user, membership_invitation.user
188
+
189
+ new_passkey = form.user.passkeys.first
190
+
191
+ assert_equal true, new_passkey.persisted?
192
+ assert_equal form.passkey_label, new_passkey.label
193
+ assert_equal credential.public_key, new_passkey.public_key
194
+ assert_equal time.to_formatted_s(:db), new_passkey.created_at.to_formatted_s(:db)
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Memberships::Forms::Organization::Membership
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "raises a validation error if trying to archive the only organization_manager" do
8
+ user = users.organization_2_owner
9
+ organization = organizations.organization_2
10
+ membership = user.memberships.find_by!(organization: organization)
11
+
12
+ form = form_class.new(current_user: user,
13
+ current_organization: organization,
14
+ membership: membership,
15
+ state: :archived_by_organization
16
+ )
17
+
18
+ assert_raises ActiveModel::ValidationError do
19
+ form.save!
20
+ end
21
+
22
+ assert_equal true, form.errors.of_kind?(:state, :cannot_be_archived)
23
+ assert_equal true, membership.reload.active?
24
+ end
25
+
26
+ test "can archive an organization_manager" do
27
+ user = users.organization_1_owner
28
+ organization = organizations.organization_1
29
+ membership = users.organization_1_manager.memberships.find_by!(organization: organization)
30
+
31
+ form = form_class.new(current_user: user,
32
+ current_organization: organization,
33
+ membership: membership,
34
+ state: :archived_by_organization
35
+ )
36
+
37
+ form.save!
38
+
39
+ assert_equal true, membership.reload.archived_by_organization?
40
+ end
41
+
42
+ test "can change an archived_by_organization membership to pending_reacceptance" do
43
+ user = users.organization_1_owner
44
+ organization = organizations.organization_1
45
+ membership = users.retired_staff.memberships.find_by!(organization: organization)
46
+
47
+ form = form_class.new(current_user: user,
48
+ current_organization: organization,
49
+ membership: membership,
50
+ state: :pending_reacceptance
51
+ )
52
+
53
+ form.save!
54
+
55
+ assert_equal true, membership.reload.pending_reacceptance?
56
+ end
57
+
58
+ test "can change a pending_reacceptance membership to archived_by_organization" do
59
+ user = users.organization_2_owner
60
+ organization = organizations.organization_2
61
+ membership = users.organization_1_staff.memberships.find_by!(organization: organization)
62
+
63
+ form = form_class.new(current_user: user,
64
+ current_organization: organization,
65
+ membership: membership,
66
+ state: :archived_by_organization
67
+ )
68
+
69
+ form.save!
70
+
71
+ assert_equal true, membership.reload.archived_by_organization?
72
+ end
73
+
74
+ test "cannot change the membership_type for the only organization_manager" do
75
+ user = users.organization_2_owner
76
+ organization = organizations.organization_2
77
+ membership = user.memberships.find_by!(organization: organization)
78
+
79
+ form = form_class.new(current_user: user,
80
+ current_organization: organization,
81
+ membership: membership,
82
+ membership_type: :staff
83
+ )
84
+
85
+ assert_raises ActiveModel::ValidationError do
86
+ form.save!
87
+ end
88
+
89
+ assert_equal true, form.errors.of_kind?(:membership_type, :cannot_be_downgraded)
90
+ assert_equal true, membership.reload.active?
91
+ assert_equal true, membership.reload.organization_manager?
92
+ end
93
+
94
+ test "can change the membership_type for an organization_manager" do
95
+ user = users.organization_1_owner
96
+ organization = organizations.organization_1
97
+ membership = users.organization_1_manager.memberships.find_by!(organization: organization)
98
+
99
+ form = form_class.new(current_user: user,
100
+ current_organization: organization,
101
+ membership: membership,
102
+ membership_type: :staff
103
+ )
104
+
105
+ form.save!
106
+
107
+ assert_equal true, membership.reload.active?
108
+ assert_equal true, membership.reload.staff?
109
+ end
110
+
111
+ test "can change the membership_type for a staff member" do
112
+ user = users.organization_1_owner
113
+ organization = organizations.organization_1
114
+ membership = users.works_at_org_1_and_2.memberships.find_by!(organization: organization)
115
+
116
+ form = form_class.new(current_user: user,
117
+ current_organization: organization,
118
+ membership: membership,
119
+ membership_type: :organization_manager
120
+ )
121
+
122
+ form.save!
123
+
124
+ assert_equal true, membership.reload.active?
125
+ assert_equal true, membership.reload.organization_manager?
126
+ end
127
+
128
+ test "can change the membership_type for an archived membership" do
129
+ user = users.organization_1_owner
130
+ organization = organizations.organization_1
131
+ membership = users.retired_staff.memberships.find_by!(organization: organization)
132
+
133
+ form = form_class.new(current_user: user,
134
+ current_organization: organization,
135
+ membership: membership,
136
+ membership_type: :organization_manager
137
+ )
138
+
139
+ form.save!
140
+
141
+ assert_equal true, membership.reload.archived_by_organization?
142
+ assert_equal true, membership.reload.organization_manager?
143
+ end
144
+
145
+ test "can change the membership_type for a pending_reacceptance membership" do
146
+ user = users.organization_2_owner
147
+ organization = organizations.organization_2
148
+ membership = users.organization_1_staff.memberships.find_by!(organization: organization)
149
+
150
+ form = form_class.new(current_user: user,
151
+ current_organization: organization,
152
+ membership: membership,
153
+ membership_type: :organization_manager
154
+ )
155
+
156
+ form.save!
157
+
158
+ assert_equal true, membership.reload.pending_reacceptance?
159
+ assert_equal true, membership.reload.organization_manager?
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Memberships::Forms::Organization::NewMembershipInvitation
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "save!: raises a validation error if the email is blank" do
8
+ form = form_class.new(email: " ")
9
+ assert_raises ActiveModel::ValidationError do
10
+ form.save!
11
+ end
12
+
13
+ assert_equal true, form.errors.of_kind?(:email, :blank)
14
+ end
15
+
16
+ test "save!: raises a validation error if the current_organization is missing" do
17
+ form = form_class.new(current_organization: nil)
18
+ assert_raises ActiveModel::ValidationError do
19
+ form.save!
20
+ end
21
+
22
+ assert_equal true, form.errors.of_kind?(:current_organization, :blank)
23
+ end
24
+
25
+ test "save!: raises validation errors from the underlying MembershipInvitation object" do
26
+ form = form_class.new(email: Faker::Internet.email,
27
+ current_organization: organizations.organization_1,
28
+ sender: users.organization_2_owner,
29
+ membership_type: :organization_manager
30
+ )
31
+
32
+ assert_raises ActiveRecord::RecordInvalid do
33
+ form.save!
34
+ end
35
+
36
+ assert_equal true, form.errors.of_kind?(:sender, :cannot_manage_organization)
37
+ end
38
+
39
+ test "save!: creates a new MembershipInvitation and enqueues the email" do
40
+ email = Faker::Internet.email
41
+ form = form_class.new(email: email,
42
+ current_organization: organizations.organization_1,
43
+ sender: users.organization_1_manager,
44
+ membership_type: :staff
45
+ )
46
+
47
+ time = Time.now.utc
48
+
49
+ assert_difference "#{membership_invitation_class}.count", +1 do
50
+ assert_no_difference "#{membership_class}.count" do
51
+ Timecop.freeze(time) do
52
+ form.save!
53
+ end
54
+ end
55
+ end
56
+
57
+ assert_equal email, form.invitation.email
58
+ assert_equal "staff", form.invitation.membership_type
59
+ assert_equal organizations.organization_1, form.invitation.organization
60
+ assert_equal users.organization_1_manager, form.invitation.sender
61
+
62
+ assert_enqueued_email_with(MembershipInvitationMailer, :invitation, args: [{membership_invitation: form.invitation}])
63
+ assert_equal time.to_fs(:db), form.invitation.last_sent_at.to_fs(:db)
64
+ end
65
+
66
+ test "save!: re-sends a MembershipInvitation where we can re-send the email" do
67
+ email = Faker::Internet.email
68
+
69
+ existing_invitation = organizations.organization_1.membership_invitations.create!(email: email, sender: users.organization_1_owner, membership_type: :organization_manager)
70
+
71
+ form = form_class.new(email: email,
72
+ current_organization: organizations.organization_1,
73
+ sender: users.organization_1_manager,
74
+ membership_type: :staff
75
+ )
76
+
77
+ time = Time.now.utc
78
+
79
+ assert_no_difference "#{membership_invitation_class}.count" do
80
+ assert_no_difference "#{membership_class}.count" do
81
+ Timecop.freeze(time) do
82
+ form.save!
83
+ end
84
+ end
85
+ end
86
+
87
+ assert_equal email, form.invitation.email
88
+ assert_equal "organization_manager", form.invitation.membership_type
89
+ assert_equal organizations.organization_1, form.invitation.organization
90
+ assert_equal users.organization_1_owner, form.invitation.sender
91
+
92
+ assert_enqueued_email_with(MembershipInvitationMailer, :invitation, args: [{membership_invitation: existing_invitation}])
93
+ assert_equal time.to_fs(:db), form.invitation.last_sent_at.to_fs(:db)
94
+ end
95
+
96
+ test "save!: returns an error if the MembershipInvitation cannot be re-sent" do
97
+ user = users.organization_1_staff
98
+ email = user.email
99
+ existing_invitation = organizations.organization_1.membership_invitations.create!(email: email, sender: users.organization_1_manager, membership_type: :staff, user: user)
100
+
101
+ form = form_class.new(email: email,
102
+ current_organization: organizations.organization_1,
103
+ sender: users.organization_1_manager,
104
+ membership_type: :staff
105
+ )
106
+
107
+ assert_no_difference "#{membership_invitation_class}.count" do
108
+ assert_no_difference "#{membership_class}.count" do
109
+ assert_raises ActiveModel::ValidationError do
110
+ form.save!
111
+ end
112
+ end
113
+ end
114
+
115
+ assert_equal true, form.errors.of_kind?(:base, :cannot_be_resent)
116
+ assert_no_enqueued_emails
117
+ end
118
+
119
+ test "save!: returns an error if the Membership is archived_by_user" do
120
+ user = users.organization_1_staff
121
+ email = user.email
122
+ organization = organizations.organization_2
123
+
124
+ existing_membership = user.memberships.find_by!(organization: organization)
125
+ existing_membership.update!(state: :archived_by_user)
126
+
127
+ form = form_class.new(email: email,
128
+ current_organization: organizations.organization_2,
129
+ sender: users.organization_2_owner,
130
+ membership_type: :staff
131
+ )
132
+
133
+ assert_no_difference "#{membership_invitation_class}.count" do
134
+ assert_no_difference "#{membership_class}.count" do
135
+ assert_raises ActiveModel::ValidationError do
136
+ form.save!
137
+ end
138
+ end
139
+ end
140
+
141
+ assert_equal true, form.errors.of_kind?(:base, :cannot_be_resent)
142
+ assert_no_enqueued_emails
143
+ end
144
+
145
+ test "save!: transitions an archived_by_organization Membership to pending_reacceptance and stores the underlying membership" do
146
+ user = users.retired_staff
147
+ email = user.email
148
+ organization = organizations.organization_1
149
+
150
+ existing_membership = user.memberships.find_by!(organization: organization)
151
+
152
+ form = form_class.new(email: email,
153
+ current_organization: organizations.organization_1,
154
+ sender: users.organization_1_manager,
155
+ membership_type: :staff
156
+ )
157
+
158
+ assert_no_difference "#{membership_invitation_class}.count" do
159
+ assert_no_difference "#{membership_class}.count" do
160
+ form.save!
161
+ end
162
+ end
163
+
164
+ assert_equal existing_membership, form.existing_membership
165
+ assert_nil form.invitation
166
+ assert_equal "pending_reacceptance", form.existing_membership.state
167
+ assert_no_enqueued_emails
168
+ end
169
+
170
+ test "save!: does nothing to an already active Membership and stores the underlying membership" do
171
+ user = users.works_at_org_1_and_2
172
+ email = user.email
173
+ organization = organizations.organization_1
174
+
175
+ existing_membership = user.memberships.find_by!(organization: organization)
176
+
177
+ form = form_class.new(email: email,
178
+ current_organization: organizations.organization_1,
179
+ sender: users.organization_1_manager,
180
+ membership_type: :staff
181
+ )
182
+
183
+ assert_no_difference "#{membership_invitation_class}.count" do
184
+ assert_no_difference "#{membership_class}.count" do
185
+ form.save!
186
+ end
187
+ end
188
+
189
+ assert_equal existing_membership, form.existing_membership
190
+ assert_nil form.invitation
191
+ assert_equal "active", form.existing_membership.state
192
+ assert_no_enqueued_emails
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Memberships::Forms::User::Membership
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "raises a validation error if trying to leave as the only organization_manager" do
8
+ user = users.organization_2_owner
9
+ organization = organizations.organization_2
10
+ membership = user.memberships.find_by!(organization: organization)
11
+
12
+ form = form_class.new(current_user: user,
13
+ membership: membership,
14
+ state: :archived_by_user
15
+ )
16
+
17
+ assert_raises ActiveModel::ValidationError do
18
+ form.save!
19
+ end
20
+
21
+ assert_equal true, form.errors.of_kind?(:state, :cannot_be_archived)
22
+ assert_equal true, membership.reload.active?
23
+ end
24
+
25
+ test "can archive an organization_manager" do
26
+ user = users.organization_1_manager
27
+ organization = organizations.organization_1
28
+ membership = user.memberships.find_by!(organization: organization)
29
+
30
+ form = form_class.new(current_user: user,
31
+ membership: membership,
32
+ state: :archived_by_user
33
+ )
34
+
35
+ form.save!
36
+
37
+ assert_equal true, membership.reload.archived_by_user?
38
+ end
39
+
40
+ test "cannot change an archived_by_user membership" do
41
+ user = users.retired_staff
42
+ organization = organizations.organization_1
43
+ membership = user.memberships.find_by!(organization: organization)
44
+ membership.update!(state: :archived_by_user)
45
+
46
+ form = form_class.new(current_user: user,
47
+ membership: membership,
48
+ state: :pending_reacceptance
49
+ )
50
+
51
+ assert_raises ActiveModel::ValidationError do
52
+ form.save!
53
+ end
54
+
55
+ assert_equal true, form.errors.of_kind?(:state, :inclusion)
56
+ assert_equal true, membership.reload.archived_by_user?
57
+ end
58
+
59
+ test "can change a pending_reacceptance membership to archived_by_user" do
60
+ user = users.organization_1_staff
61
+ organization = organizations.organization_2
62
+ membership = user.memberships.find_by!(organization: organization)
63
+
64
+ form = form_class.new(current_user: user,
65
+ membership: membership,
66
+ state: :archived_by_user
67
+ )
68
+
69
+ form.save!
70
+
71
+ assert_equal true, membership.reload.archived_by_user?
72
+ end
73
+
74
+ test "cannot change the membership_type" do
75
+ user = users.organization_2_owner
76
+ organization = organizations.organization_2
77
+ membership = user.memberships.find_by!(organization: organization)
78
+
79
+ assert_raises ActiveModel::UnknownAttributeError do
80
+ form = form_class.new(current_user: user,
81
+ membership: membership,
82
+ membership_type: :staff
83
+ )
84
+ end
85
+ end
86
+ end
87
+ end