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,400 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Memberships::Controllers::Organization::Membership
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "index: lists all non-archived-by_user membership types and membership invitations" do
8
+ user = users.organization_1_owner
9
+ organization = organizations.organization_1
10
+ sign_in(user)
11
+
12
+ self_archived_user = users.archived_organization_1_manager
13
+ self_archived_user.memberships.update!(state: :archived_by_user)
14
+
15
+ active_membership_user = users.organization_1_manager
16
+ pending_reacceptance_user = users.works_at_org_1_and_2
17
+ archived_by_organization_user = users.retired_staff
18
+
19
+ membership_invitation = organization.membership_invitations.create!(email: Faker::Internet.email, membership_type: :staff, sender: user)
20
+
21
+ pending_reacceptance_user.memberships.find_by!(organization: organization).update!(state: :pending_reacceptance)
22
+
23
+ assert_index_policies_applied(organization: organization) do
24
+ get organization_memberships_url(organization)
25
+ end
26
+
27
+ assert_response :ok
28
+ assert_dom 'td', text: active_membership_user.name
29
+ assert_dom 'td', text: pending_reacceptance_user.name
30
+ assert_dom 'td', text: archived_by_organization_user.name
31
+ assert_dom 'td', text: membership_invitation.email
32
+ assert_dom 'td', text: self_archived_user.name, count: 0
33
+ end
34
+
35
+ test "create: creates a new invitation" do
36
+ user = users.organization_3_owner
37
+ organization = organizations.organization_3
38
+ sign_in(user)
39
+
40
+ email = Faker::Internet.email
41
+
42
+ params = {
43
+ organization_new_membership_invitation_form: {
44
+ email: email,
45
+ membership_type: :staff
46
+ }
47
+ }
48
+
49
+ assert_difference "MembershipInvitation.count", +1 do
50
+ assert_no_difference "Membership.count" do
51
+ assert_create_policies_applied(organization: organization) do
52
+ post organization_memberships_url(organization), params: params, as: :json
53
+ end
54
+ end
55
+ end
56
+
57
+ assert_json_redirected_to(organization_memberships_url(organization))
58
+ message = I18n.t('organization_memberships.invitation_sent_message', email: email)
59
+ assert_flash_message(type: :success, message: message, icon_name: 'envelope-dot')
60
+ end
61
+
62
+ test "create: resends a pending invitation" do
63
+ user = users.organization_3_owner
64
+ organization = organizations.organization_3
65
+ sign_in(user)
66
+
67
+ email = users.invited_user_1.email
68
+ previous_invitation = organization.membership_invitations.find_by!(email: email)
69
+ previous_invitation.update!(last_sent_at: 1.hour.ago)
70
+
71
+ params = {
72
+ organization_new_membership_invitation_form: {
73
+ email: email,
74
+ membership_type: :organization_manager
75
+ }
76
+ }
77
+
78
+ assert_no_difference "MembershipInvitation.count" do
79
+ assert_create_policies_applied(organization: organization) do
80
+ post organization_memberships_url(organization), params: params, as: :json
81
+ end
82
+ end
83
+
84
+ assert_json_redirected_to(organization_memberships_url(organization))
85
+ message = I18n.t('organization_memberships.invitation_sent_message', email: email)
86
+ assert_flash_message(type: :success, message: message, icon_name: 'envelope-dot')
87
+ end
88
+
89
+ test "create: marks a archived_by_organization invitation as pending_reacceptance" do
90
+ user = users.organization_2_owner
91
+ organization = organizations.organization_2
92
+ sign_in(user)
93
+
94
+ email = users.organization_1_staff.email
95
+ previous_membership = organization.memberships.includes(:user).find_by!(user: {email: email})
96
+ assert_equal true, previous_membership.pending_reacceptance?
97
+
98
+ params = {
99
+ organization_new_membership_invitation_form: {
100
+ email: email,
101
+ membership_type: :organization_manager
102
+ }
103
+ }
104
+
105
+ assert_no_difference "MembershipInvitation.count" do
106
+ assert_create_policies_applied(organization: organization) do
107
+ post organization_memberships_url(organization), params: params, as: :json
108
+ end
109
+ end
110
+
111
+ assert_json_redirected_to(organization_memberships_url(organization))
112
+ assert_flash_message(type: :notice, message: I18n.t('organization_memberships.awaiting_reacceptance_message'), icon_name: 'circle-info')
113
+ end
114
+
115
+ test "create: does nothing & returns a flash message if given an email for an active membership" do
116
+ user = users.organization_1_manager
117
+ organization = organizations.organization_1
118
+ sign_in(user)
119
+
120
+ email = users.works_at_org_1_and_2.email
121
+ previous_membership = organization.memberships.includes(:user).find_by!(user: {email: email})
122
+ assert_equal true, previous_membership.active?
123
+
124
+ params = {
125
+ organization_new_membership_invitation_form: {
126
+ email: email,
127
+ membership_type: :organization_manager
128
+ }
129
+ }
130
+
131
+ assert_no_difference "MembershipInvitation.count" do
132
+ assert_create_policies_applied(organization: organization) do
133
+ post organization_memberships_url(organization), params: params, as: :json
134
+ end
135
+ end
136
+
137
+ assert_json_redirected_to(organization_memberships_url(organization))
138
+ assert_flash_message(type: :success, message: I18n.t('organization_memberships.already_member_message'), icon_name: 'circle-check')
139
+ end
140
+
141
+ test "create: renders errors as JSON if given an email for a membership archived_by_user" do
142
+ user = users.organization_1_manager
143
+ organization = organizations.organization_1
144
+ sign_in(user)
145
+
146
+ email = users.works_at_org_1_and_2.email
147
+ previous_membership = organization.memberships.includes(:user).find_by!(user: {email: email})
148
+ previous_membership.update!(state: :archived_by_user)
149
+
150
+ params = {
151
+ organization_new_membership_invitation_form: {
152
+ email: email,
153
+ membership_type: :organization_manager
154
+ }
155
+ }
156
+
157
+ assert_no_difference "MembershipInvitation.count" do
158
+ assert_create_policies_applied(organization: organization) do
159
+ post organization_memberships_url(organization), params: params, as: :json
160
+ end
161
+ end
162
+
163
+ assert_response :bad_request
164
+
165
+ assert_error_json_contains(
166
+ container_id: "organization_new_membership_invitation_form_base_errors",
167
+ element_id: "organization_new_membership_invitation_form_base",
168
+ message: I18n.t('activemodel.errors.models.organization/new_membership_invitation_form.attributes.base.cannot_be_resent'),
169
+ type: "cannot_be_resent"
170
+ )
171
+ end
172
+
173
+ test "create: renders errors as JSON if given an email for a non-visible invitation" do
174
+ user = users.organization_3_owner
175
+ organization = organizations.organization_3
176
+ sign_in(user)
177
+
178
+ email = users.invited_user_1.email
179
+ previous_invitation = organization.membership_invitations.find_by!(email: email)
180
+ previous_invitation.update!(visible: false)
181
+
182
+ params = {
183
+ organization_new_membership_invitation_form: {
184
+ email: email,
185
+ membership_type: :organization_manager
186
+ }
187
+ }
188
+
189
+ assert_no_difference "MembershipInvitation.count" do
190
+ assert_create_policies_applied(organization: organization) do
191
+ post organization_memberships_url(organization), params: params, as: :json
192
+ end
193
+ end
194
+
195
+ assert_response :bad_request
196
+
197
+ assert_error_json_contains(
198
+ container_id: "organization_new_membership_invitation_form_base_errors",
199
+ element_id: "organization_new_membership_invitation_form_base",
200
+ message: I18n.t('activemodel.errors.models.organization/new_membership_invitation_form.attributes.base.cannot_be_resent'),
201
+ type: "cannot_be_resent"
202
+ )
203
+ end
204
+
205
+ test "create: renders errors as JSON if given an email for an invitation that was just sent out" do
206
+ user = users.organization_3_owner
207
+ organization = organizations.organization_3
208
+ sign_in(user)
209
+
210
+ email = users.invited_user_1.email
211
+ previous_invitation = organization.membership_invitations.find_by!(email: email)
212
+ previous_invitation.update!(last_sent_at: Time.now.utc)
213
+
214
+ params = {
215
+ organization_new_membership_invitation_form: {
216
+ email: email,
217
+ membership_type: :organization_manager
218
+ }
219
+ }
220
+
221
+ assert_no_difference "MembershipInvitation.count" do
222
+ assert_create_policies_applied(organization: organization) do
223
+ post organization_memberships_url(organization), params: params, as: :json
224
+ end
225
+ end
226
+
227
+ assert_response :bad_request
228
+
229
+ assert_error_json_contains(
230
+ container_id: "organization_new_membership_invitation_form_base_errors",
231
+ element_id: "organization_new_membership_invitation_form_base",
232
+ message: I18n.t('activemodel.errors.models.organization/new_membership_invitation_form.attributes.base.cannot_be_resent'),
233
+ type: "cannot_be_resent"
234
+ )
235
+ end
236
+
237
+ test "create: renders errors as JSON" do
238
+ user = users.organization_1_manager
239
+ organization = organizations.organization_1
240
+
241
+ sign_in(user)
242
+
243
+ params = {
244
+ organization_new_membership_invitation_form: {
245
+ email: "",
246
+ membership_type: :staff
247
+ }
248
+ }
249
+
250
+ assert_no_difference "MembershipInvitation.count" do
251
+ assert_create_policies_applied(organization: organization) do
252
+ post organization_memberships_url(organization), params: params, as: :json
253
+ end
254
+ end
255
+ assert_response :bad_request
256
+
257
+ assert_error_json_contains(
258
+ container_id: "organization_new_membership_invitation_form_email_errors",
259
+ element_id: "organization_new_membership_invitation_form_email",
260
+ message: "can't be blank",
261
+ type: "blank"
262
+ )
263
+ end
264
+
265
+ test "create: renders errors as HTML" do
266
+ user = users.organization_1_manager
267
+ organization = organizations.organization_1
268
+
269
+ sign_in(user)
270
+
271
+ params = {
272
+ organization_new_membership_invitation_form: {
273
+ email: "",
274
+ membership_type: :staff
275
+ }
276
+ }
277
+
278
+ assert_no_difference "MembershipInvitation.count" do
279
+ assert_create_policies_applied(organization: organization) do
280
+ post organization_memberships_url(organization), params: params
281
+ end
282
+ end
283
+ assert_response :bad_request
284
+
285
+ assert_error_dom(
286
+ container_id: "organization_new_membership_invitation_form_email_errors",
287
+ message: "can't be blank"
288
+ )
289
+ end
290
+
291
+ test "edit: renders the form" do
292
+ user = users.organization_1_owner
293
+ organization = organizations.organization_1
294
+ sign_in(user)
295
+
296
+ membership = users.organization_1_manager.memberships.find_by!(organization: organization)
297
+
298
+ assert_edit_policies_applied(organization: organization, membership: membership) do
299
+ get edit_organization_membership_url(organization, membership)
300
+ end
301
+
302
+ assert_response :ok
303
+
304
+ css_select "form[action='#{organization_membership_url(organization, membership)}']"
305
+ end
306
+
307
+ test "update: can archive an active membership" do
308
+ user = users.organization_1_owner
309
+ organization = organizations.organization_1
310
+ sign_in(user)
311
+
312
+ membership = users.organization_1_manager.memberships.find_by!(organization: organization)
313
+
314
+ assert_update_policies_applied(organization: organization, membership: membership) do
315
+ patch organization_membership_url(organization, membership), params: {organization_membership_form: {
316
+ state: :archived_by_organization
317
+ }}
318
+ end
319
+
320
+ assert_flash_message(type: :success, message: I18n.t('organization_memberships.updated_message'), icon_name: 'circle-check')
321
+ assert_redirected_to edit_organization_membership_url(organization, membership)
322
+ assert_equal true, membership.reload.archived_by_organization?
323
+ end
324
+
325
+ test "update: can archive a pending_reacceptance membership" do
326
+ user = users.organization_2_owner
327
+ organization = organizations.organization_2
328
+ sign_in(user)
329
+
330
+ membership = users.organization_1_staff.memberships.find_by!(organization: organization)
331
+
332
+ assert_update_policies_applied(organization: organization, membership: membership) do
333
+ patch organization_membership_url(organization, membership), params: {organization_membership_form: {
334
+ state: :archived_by_organization
335
+ }}
336
+ end
337
+
338
+ assert_redirected_to edit_organization_membership_url(organization, membership)
339
+ assert_equal true, membership.reload.archived_by_organization?
340
+ end
341
+
342
+ test "update: can unarchive an archived_by_organization membership" do
343
+ user = users.organization_1_owner
344
+ organization = organizations.organization_1
345
+ sign_in(user)
346
+
347
+ membership = users.archived_organization_1_manager.memberships.find_by!(organization: organization)
348
+
349
+ assert_update_policies_applied(organization: organization, membership: membership) do
350
+ patch organization_membership_url(organization, membership), params: {organization_membership_form: {
351
+ state: :pending_reacceptance
352
+ }}
353
+ end
354
+
355
+ assert_flash_message(type: :success, message: I18n.t('organization_memberships.updated_message'), icon_name: 'circle-check')
356
+ assert_redirected_to edit_organization_membership_url(organization, membership)
357
+ assert_equal true, membership.reload.pending_reacceptance?
358
+ end
359
+
360
+ test "update: update the membership_type" do
361
+ user = users.organization_1_owner
362
+ organization = organizations.organization_1
363
+ sign_in(user)
364
+
365
+ membership = users.organization_1_manager.memberships.find_by!(organization: organization)
366
+
367
+ assert_update_policies_applied(organization: organization, membership: membership) do
368
+ patch organization_membership_url(organization, membership), params: {organization_membership_form: {
369
+ membership_type: :staff
370
+ }}
371
+ end
372
+
373
+ assert_redirected_to edit_organization_membership_url(organization, membership)
374
+ assert_equal true, membership.reload.staff?
375
+ end
376
+
377
+ test "update: returns an error if trying to archive the only organization_manager" do
378
+ user = users.organization_2_owner
379
+ organization = organizations.organization_2
380
+ sign_in(user)
381
+
382
+ membership = user.memberships.find_by!(organization: organization)
383
+
384
+ assert_update_policies_applied(organization: organization, membership: membership) do
385
+ patch organization_membership_url(organization, membership), params: {organization_membership_form: {
386
+ state: :archived_by_organization
387
+ }}
388
+ end
389
+
390
+ assert_response :bad_request
391
+
392
+ assert_error_dom(
393
+ container_id: "generic_errors_organization_membership_form",
394
+ message: /This member cannot be archived/
395
+ )
396
+
397
+ assert_equal true, membership.reload.active?
398
+ end
399
+ end
400
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Memberships::Controllers::Organization::MembershipInvitation
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "resend: resends a pending invitation and returns a flash message" do
8
+ user = users.organization_3_owner
9
+ organization = organizations.organization_3
10
+ sign_in(user)
11
+
12
+ membership_invitation = organization.membership_invitations.find_by!(email: users.invited_user_1.email)
13
+ time = Time.now.utc
14
+
15
+ Timecop.freeze(time) do
16
+ assert_policies_applied(organization: organization, membership_invitation: membership_invitation) do
17
+ assert_no_difference "MembershipInvitation.count" do
18
+ patch resend_organization_membership_invitation_url(organization, membership_invitation)
19
+ end
20
+ end
21
+ end
22
+
23
+ assert_redirected_to organization_memberships_url(organization)
24
+ message = I18n.t('organization_memberships.invitation_sent_message', email: membership_invitation.email)
25
+ assert_flash_message(type: :success, message: message, icon_name: 'envelope-dot')
26
+
27
+ assert_enqueued_email_with(MembershipInvitationMailer, :invitation, args: [{membership_invitation: membership_invitation}])
28
+ assert_equal time.to_fs(:db), membership_invitation.reload.last_sent_at.to_fs(:db)
29
+ end
30
+
31
+ test "resend: does nothing if the invitation cannot be resent yet" do
32
+ user = users.organization_3_owner
33
+ organization = organizations.organization_3
34
+ sign_in(user)
35
+
36
+ membership_invitation = organization.membership_invitations.find_by!(email: users.invited_user_1.email)
37
+ membership_invitation.update!(last_sent_at: Time.now.utc)
38
+ old_last_sent_at = membership_invitation.last_sent_at
39
+
40
+ assert_policies_applied(organization: organization, membership_invitation: membership_invitation) do
41
+ assert_no_difference "MembershipInvitation.count" do
42
+ patch resend_organization_membership_invitation_url(organization, membership_invitation)
43
+ end
44
+ end
45
+
46
+ assert_redirected_to organization_memberships_url(organization)
47
+ message = I18n.t("organization_memberships.cannot_be_resent_message")
48
+ assert_flash_message(type: :alert, message: message, icon_name: 'triangle-exclamation')
49
+
50
+ assert_no_enqueued_emails
51
+ assert_equal old_last_sent_at.to_fs(:db), membership_invitation.reload.last_sent_at.to_fs(:db)
52
+ end
53
+
54
+ test "resend: returns 404 if the invitation is already tied to a user" do
55
+ user = users.organization_3_owner
56
+ organization = organizations.organization_3
57
+ sign_in(user)
58
+
59
+ membership_invitation = users.invited_user_2.membership_invitations.find_by!(organization: organization)
60
+ old_last_sent_at = membership_invitation.last_sent_at
61
+
62
+ assert_policies_applied_on_404(organization: organization) do
63
+ assert_no_difference "MembershipInvitation.count" do
64
+ patch resend_organization_membership_invitation_url(organization, membership_invitation)
65
+ end
66
+ end
67
+
68
+ assert_response :not_found
69
+ assert_no_enqueued_emails
70
+ end
71
+
72
+ test "resend: does nothing if the invitation is not visible" do
73
+ user = users.organization_3_owner
74
+ organization = organizations.organization_3
75
+ sign_in(user)
76
+
77
+ membership_invitation = organization.membership_invitations.find_by!(email: users.invited_user_1.email)
78
+ membership_invitation.update!(visible: false)
79
+
80
+ assert_policies_applied_on_404(organization: organization) do
81
+ assert_no_difference "MembershipInvitation.count" do
82
+ patch resend_organization_membership_invitation_url(organization, membership_invitation)
83
+ end
84
+ end
85
+
86
+ assert_response :not_found
87
+
88
+ assert_no_enqueued_emails
89
+ end
90
+
91
+
92
+ test "destroy: destroys a pending invitation and returns a flash message" do
93
+ user = users.organization_3_owner
94
+ organization = organizations.organization_3
95
+ sign_in(user)
96
+
97
+ membership_invitation = organization.membership_invitations.find_by!(email: users.invited_user_1.email)
98
+
99
+ assert_policies_applied(organization: organization, membership_invitation: membership_invitation) do
100
+ assert_difference "MembershipInvitation.count", -1 do
101
+ delete organization_membership_invitation_url(organization, membership_invitation)
102
+ end
103
+ end
104
+
105
+ assert_redirected_to organization_memberships_url(organization)
106
+ message = I18n.t('organization_memberships.invitation_revoked_message', email: membership_invitation.email)
107
+ assert_flash_message(type: :alert, message: message, icon_name: 'link-slash')
108
+
109
+ assert_no_enqueued_emails
110
+ end
111
+
112
+ test "destroy: returns 404 if the invitation is already tied to a user" do
113
+ user = users.organization_3_owner
114
+ organization = organizations.organization_3
115
+ sign_in(user)
116
+
117
+ membership_invitation = users.invited_user_2.membership_invitations.find_by!(organization: organization)
118
+ old_last_sent_at = membership_invitation.last_sent_at
119
+
120
+ assert_policies_applied_on_404(organization: organization) do
121
+ assert_no_difference "MembershipInvitation.count" do
122
+ delete organization_membership_invitation_url(organization, membership_invitation)
123
+ end
124
+ end
125
+
126
+ assert_response :not_found
127
+ assert_no_enqueued_emails
128
+ end
129
+
130
+ test "destroy: returns 404 if the invitation is not visible" do
131
+ user = users.organization_3_owner
132
+ organization = organizations.organization_3
133
+ sign_in(user)
134
+
135
+ membership_invitation = organization.membership_invitations.find_by!(email: users.invited_user_1.email)
136
+ membership_invitation.update!(visible: false)
137
+
138
+ assert_policies_applied_on_404(organization: organization) do
139
+ assert_no_difference "MembershipInvitation.count" do
140
+ delete organization_membership_invitation_url(organization, membership_invitation)
141
+ end
142
+ end
143
+
144
+ assert_response :not_found
145
+ assert_no_enqueued_emails
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Memberships::Controllers::User::Membership
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "index: lists all non-archived-by_organization membership types and membership invitations" do
8
+ extra_organization = Organization.create!(name: Faker::Company.name)
9
+ invitation_organization = Organization.create(name: Faker::Company.name)
10
+ invitation_organization.memberships.create!(user: users.user_1, membership_type: :organization_manager, state: :active)
11
+
12
+ user = users.works_at_org_1_and_2
13
+ sign_in(user)
14
+
15
+ self_archived_membership = user.memberships.find_by(organization: organizations.organization_1)
16
+ self_archived_membership.update!(state: :archived_by_user)
17
+
18
+ active_membership = user.memberships.find_by!(organization: organizations.organization_2)
19
+ pending_reacceptance_membership = user.memberships.create!(organization: organizations.organization_3, membership_type: :staff, state: :pending_reacceptance)
20
+ archived_by_organization_membership = user.memberships.create!(organization: extra_organization, membership_type: :staff, state: :archived_by_organization)
21
+
22
+ membership_invitation = invitation_organization.membership_invitations.create!(email: user.email, membership_type: :staff, sender: users.user_1)
23
+
24
+ assert_index_policies_applied(user: user) do
25
+ get user_memberships_url
26
+ end
27
+
28
+ assert_response :ok
29
+ assert_dom 'td', text: active_membership.organization.name
30
+ assert_dom 'td', text: pending_reacceptance_membership.organization.name
31
+ assert_dom 'td', text: self_archived_membership.organization.name
32
+ assert_dom 'td', text: membership_invitation.organization.name
33
+ assert_dom 'td', text: archived_by_organization_membership.organization.name, count: 0
34
+ end
35
+
36
+ test "update: can archive an active membership" do
37
+ user = users.works_at_org_1_and_2
38
+ sign_in(user)
39
+
40
+ membership = user.memberships.find_by!(organization: organizations.organization_1)
41
+
42
+ assert_update_policies_applied(user: user, membership: membership) do
43
+ patch user_membership_url(membership), params: {user_membership_form: {
44
+ state: :archived_by_user
45
+ }}
46
+ end
47
+
48
+ assert_flash_message(type: :notice, message: I18n.t('user_memberships.archived_message'), icon_name: 'circle-info')
49
+ assert_redirected_to user_memberships_url
50
+ assert_equal true, membership.reload.archived_by_user?
51
+ end
52
+
53
+ test "update: can archive a pending_reacceptance membership" do
54
+ user = users.organization_1_staff
55
+ sign_in(user)
56
+
57
+ membership = user.memberships.find_by!(organization: organizations.organization_2)
58
+
59
+ assert_update_policies_applied(user: user, membership: membership) do
60
+ patch user_membership_url(membership), params: {user_membership_form: {
61
+ state: :archived_by_user
62
+ }}
63
+ end
64
+
65
+ assert_flash_message(type: :notice, message: I18n.t('user_memberships.archived_message'), icon_name: 'circle-info')
66
+ assert_redirected_to user_memberships_url
67
+ assert_equal true, membership.reload.archived_by_user?
68
+ end
69
+
70
+ test "update: cannot unarchive an archived_by_organization membership" do
71
+ user = users.retired_staff
72
+ sign_in(user)
73
+
74
+ membership = user.memberships.find_by!(organization: organizations.organization_1)
75
+
76
+ assert_not_found_policies_applied(user: user) do
77
+ patch user_membership_url(membership), params: {user_membership_form: {
78
+ state: :pending_reacceptance
79
+ }}
80
+ end
81
+
82
+ assert_response :not_found
83
+ end
84
+
85
+ test "update: cannot update the membership_type" do
86
+ user = users.organization_1_owner
87
+ sign_in(user)
88
+
89
+ membership = user.memberships.find_by!(organization: organizations.organization_1)
90
+
91
+ assert_update_policies_applied(user: user, membership: membership) do
92
+ patch user_membership_url(membership), params: {user_membership_form: {
93
+ membership_type: :staff
94
+ }}
95
+ end
96
+
97
+ assert_redirected_to user_memberships_url
98
+ assert_equal true, membership.reload.organization_manager?
99
+ end
100
+
101
+ test "update: returns an error if trying to leave the only organization_manager" do
102
+ user = users.organization_2_owner
103
+ sign_in(user)
104
+
105
+ membership = user.memberships.find_by!(organization: organizations.organization_2)
106
+
107
+ assert_update_policies_applied(user: user, membership: membership) do
108
+ patch user_membership_url(membership), params: {user_membership_form: {
109
+ state: :archived_by_organization
110
+ }}
111
+ end
112
+
113
+ assert_flash_message(type: :alert, message: I18n.t('activemodel.errors.models.user/membership_form.attributes.state.inclusion'), icon_name: 'triangle-exclamation')
114
+ assert_redirected_to user_memberships_url
115
+
116
+ assert_equal true, membership.reload.active?
117
+ end
118
+ end
119
+ end