activity_notification 2.3.2 → 2.4.0

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build.yml +9 -36
  3. data/CHANGELOG.md +26 -1
  4. data/Gemfile +1 -1
  5. data/README.md +9 -1
  6. data/activity_notification.gemspec +5 -5
  7. data/ai-curated-specs/issues/172/design.md +220 -0
  8. data/ai-curated-specs/issues/172/tasks.md +326 -0
  9. data/ai-curated-specs/issues/188/design.md +227 -0
  10. data/ai-curated-specs/issues/188/requirements.md +78 -0
  11. data/ai-curated-specs/issues/188/tasks.md +203 -0
  12. data/ai-curated-specs/issues/188/upstream-contributions.md +592 -0
  13. data/ai-curated-specs/issues/50/design.md +235 -0
  14. data/ai-curated-specs/issues/50/requirements.md +49 -0
  15. data/ai-curated-specs/issues/50/tasks.md +232 -0
  16. data/app/controllers/activity_notification/notifications_api_controller.rb +22 -0
  17. data/app/controllers/activity_notification/notifications_controller.rb +27 -1
  18. data/app/mailers/activity_notification/mailer.rb +2 -2
  19. data/app/views/activity_notification/notifications/default/_index.html.erb +6 -1
  20. data/app/views/activity_notification/notifications/default/destroy_all.js.erb +6 -0
  21. data/docs/Setup.md +43 -6
  22. data/gemfiles/Gemfile.rails-7.0 +2 -0
  23. data/gemfiles/Gemfile.rails-7.2 +0 -2
  24. data/gemfiles/Gemfile.rails-8.0 +24 -0
  25. data/lib/activity_notification/apis/notification_api.rb +51 -2
  26. data/lib/activity_notification/controllers/concerns/swagger/notifications_api.rb +59 -0
  27. data/lib/activity_notification/helpers/view_helpers.rb +28 -0
  28. data/lib/activity_notification/mailers/helpers.rb +14 -7
  29. data/lib/activity_notification/models/concerns/target.rb +16 -0
  30. data/lib/activity_notification/models.rb +1 -1
  31. data/lib/activity_notification/notification_resilience.rb +115 -0
  32. data/lib/activity_notification/orm/dynamoid/extension.rb +4 -87
  33. data/lib/activity_notification/orm/dynamoid/notification.rb +19 -2
  34. data/lib/activity_notification/orm/dynamoid.rb +42 -6
  35. data/lib/activity_notification/rails/routes.rb +3 -2
  36. data/lib/activity_notification/version.rb +1 -1
  37. data/lib/activity_notification.rb +1 -0
  38. data/lib/generators/templates/controllers/notifications_api_controller.rb +5 -0
  39. data/lib/generators/templates/controllers/notifications_api_with_devise_controller.rb +5 -0
  40. data/lib/generators/templates/controllers/notifications_controller.rb +5 -0
  41. data/lib/generators/templates/controllers/notifications_with_devise_controller.rb +5 -0
  42. data/spec/concerns/apis/notification_api_spec.rb +161 -5
  43. data/spec/concerns/models/target_spec.rb +7 -0
  44. data/spec/controllers/controller_spec_utility.rb +1 -1
  45. data/spec/controllers/notifications_api_controller_shared_examples.rb +113 -0
  46. data/spec/controllers/notifications_controller_shared_examples.rb +150 -0
  47. data/spec/helpers/view_helpers_spec.rb +14 -0
  48. data/spec/jobs/notification_resilience_job_spec.rb +167 -0
  49. data/spec/mailers/notification_resilience_spec.rb +263 -0
  50. data/spec/models/notification_spec.rb +1 -1
  51. data/spec/models/subscription_spec.rb +1 -1
  52. data/spec/rails_app/app/helpers/devise_helper.rb +2 -0
  53. data/spec/rails_app/config/application.rb +1 -0
  54. data/spec/rails_app/config/initializers/zeitwerk.rb +10 -0
  55. metadata +67 -53
@@ -98,7 +98,15 @@ module ActivityNotification
98
98
  ["#{attribute}_key".to_sym, value.nil? ? nil : "#{value.class.name}#{ActivityNotification.config.composite_key_delimiter}#{value.id}"] :
99
99
  [attribute, value]
100
100
  }.to_h
101
- update_attributes(attributes_with_association)
101
+
102
+ # Use update_attributes if available, otherwise use the manual approach
103
+ if respond_to?(:update_attributes)
104
+ update_attributes(attributes_with_association)
105
+ else
106
+ # Manual update for models that don't have update_attributes
107
+ attributes_with_association.each { |attribute, value| write_attribute(attribute, value) }
108
+ save
109
+ end
102
110
  end
103
111
  end
104
112
  end
@@ -179,10 +187,12 @@ module Dynamoid # :nodoc: all
179
187
  # Selects filtered notifications or subscriptions by association type.
180
188
  # @scope class
181
189
  # @param [String] name Association name
182
- # @param [Object] type Association type
190
+ # @param [Object] type Association type (can be class name string or object instance)
183
191
  # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications or subscriptions
184
192
  def filtered_by_association_type(name, type)
185
- type.present? ? where("#{name}_key.begins_with" => "#{type}#{ActivityNotification.config.composite_key_delimiter}") : none
193
+ # Handle both string class names and object instances
194
+ type_name = type.is_a?(String) ? type : type.class.name
195
+ where("#{name}_key.begins_with" => "#{type_name}#{ActivityNotification.config.composite_key_delimiter}")
186
196
  end
187
197
 
188
198
  # Selects filtered notifications or subscriptions by association type and id.
@@ -391,14 +401,32 @@ module Dynamoid # :nodoc: all
391
401
  # @scope class
392
402
  # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications
393
403
  def group_members_only
394
- where('group_owner_id.not_null': true)
404
+ # Create a new chain to avoid state issues
405
+ new_chain = @source.where('group_owner_id.not_null': true)
406
+ # Apply existing conditions from current chain
407
+ if instance_variable_defined?(:@where_conditions) && @where_conditions
408
+ @where_conditions.instance_variable_get(:@hash_conditions).each do |condition|
409
+ # Skip conflicting group_owner_id conditions
410
+ next if condition.key?(:"group_owner_id.null") || condition.key?(:"group_owner_id.not_null")
411
+ new_chain = new_chain.where(condition)
412
+ end
413
+ end
414
+ new_chain
395
415
  end
396
416
 
397
417
  # Selects unopened notifications only.
398
418
  # @scope class
399
419
  # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications
400
420
  def unopened_only
401
- where('opened_at.null': true)
421
+ # Create a new chain to avoid state issues
422
+ new_chain = @source.where('opened_at.null': true)
423
+ # Apply existing conditions from current chain
424
+ if instance_variable_defined?(:@where_conditions) && @where_conditions
425
+ @where_conditions.instance_variable_get(:@hash_conditions).each do |condition|
426
+ new_chain = new_chain.where(condition)
427
+ end
428
+ end
429
+ new_chain
402
430
  end
403
431
 
404
432
  # Selects opened notifications only without limit.
@@ -406,7 +434,15 @@ module Dynamoid # :nodoc: all
406
434
  # @scope class
407
435
  # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications
408
436
  def opened_only!
409
- where('opened_at.not_null': true)
437
+ # Create a new chain to avoid state issues
438
+ new_chain = @source.where('opened_at.not_null': true)
439
+ # Apply existing conditions from current chain
440
+ if instance_variable_defined?(:@where_conditions) && @where_conditions
441
+ @where_conditions.instance_variable_get(:@hash_conditions).each do |condition|
442
+ new_chain = new_chain.where(condition)
443
+ end
444
+ end
445
+ new_chain
410
446
  end
411
447
 
412
448
  # Selects opened notifications only with limit.
@@ -153,7 +153,7 @@ module ActionDispatch::Routing
153
153
  if options[:with_devise].present? && options[:devise_default_routes].present?
154
154
  create_notification_routes options, resources_options
155
155
  else
156
- self.resources target, only: :none do
156
+ self.resources target, only: [] do
157
157
  create_notification_routes options, resources_options
158
158
  end
159
159
  end
@@ -348,7 +348,7 @@ module ActionDispatch::Routing
348
348
  if options[:with_devise].present? && options[:devise_default_routes].present?
349
349
  create_subscription_routes options, resources_options
350
350
  else
351
- self.resources target, only: :none do
351
+ self.resources target, only: [] do
352
352
  create_subscription_routes options, resources_options
353
353
  end
354
354
  end
@@ -414,6 +414,7 @@ module ActionDispatch::Routing
414
414
  self.resources options[:model], resources_options do
415
415
  collection do
416
416
  post :open_all unless ignore_path?(:open_all, options)
417
+ post :destroy_all unless ignore_path?(:destroy_all, options)
417
418
  end
418
419
  member do
419
420
  get :move unless ignore_path?(:move, options)
@@ -1,3 +1,3 @@
1
1
  module ActivityNotification
2
- VERSION = "2.3.2"
2
+ VERSION = "2.4.0"
3
3
  end
@@ -16,6 +16,7 @@ module ActivityNotification
16
16
  autoload :Common
17
17
  autoload :Config
18
18
  autoload :Renderable
19
+ autoload :NotificationResilience
19
20
  autoload :VERSION
20
21
  autoload :GEM_VERSION
21
22
 
@@ -9,6 +9,11 @@ class <%= @target_prefix %>NotificationsController < ActivityNotification::Notif
9
9
  # super
10
10
  # end
11
11
 
12
+ # POST /:target_type/:target_id/notifications/destroy_all
13
+ # def destroy_all
14
+ # super
15
+ # end
16
+
12
17
  # GET /:target_type/:target_id/notifications/:id
13
18
  # def show
14
19
  # super
@@ -9,6 +9,11 @@ class <%= @target_prefix %>NotificationsWithDeviseController < ActivityNotificat
9
9
  # super
10
10
  # end
11
11
 
12
+ # POST /:target_type/:target_id/notifications/destroy_all
13
+ # def destroy_all
14
+ # super
15
+ # end
16
+
12
17
  # GET /:target_type/:target_id/notifications/:id
13
18
  # def show
14
19
  # super
@@ -9,6 +9,11 @@ class <%= @target_prefix %>NotificationsController < ActivityNotification::Notif
9
9
  # super
10
10
  # end
11
11
 
12
+ # POST /:target_type/:target_id/notifications/destroy_all
13
+ # def destroy_all
14
+ # super
15
+ # end
16
+
12
17
  # GET /:target_type/:target_id/notifications/:id
13
18
  # def show
14
19
  # super
@@ -9,6 +9,11 @@ class <%= @target_prefix %>NotificationsWithDeviseController < ActivityNotificat
9
9
  # super
10
10
  # end
11
11
 
12
+ # POST /:target_type/:target_id/notifications/destroy_all
13
+ # def destroy_all
14
+ # super
15
+ # end
16
+
12
17
  # GET /:target_type/:target_id/notifications/:id
13
18
  # def show
14
19
  # super
@@ -54,7 +54,7 @@ shared_examples_for :notification_api do
54
54
  expect(ActivityNotification::Mailer.deliveries.first.to[0]).to eq(@author_user.email)
55
55
  expect(ActivityNotification::Mailer.deliveries.last.to[0]).to eq(@user_1.email)
56
56
  end
57
-
57
+
58
58
  it "sends notification email with active job queue" do
59
59
  expect {
60
60
  described_class.notify(:users, @comment_2)
@@ -318,7 +318,7 @@ shared_examples_for :notification_api do
318
318
 
319
319
  context "with options" do
320
320
  context "as default" do
321
- let(:created_notification) {
321
+ let(:created_notification) {
322
322
  described_class.notify_to(@user_1, @comment_2)
323
323
  @user_1.notifications.latest
324
324
  }
@@ -360,7 +360,7 @@ shared_examples_for :notification_api do
360
360
  end
361
361
 
362
362
  context "as specified default value" do
363
- let(:created_notification) {
363
+ let(:created_notification) {
364
364
  described_class.notify_to(@user_1, @comment_2)
365
365
  }
366
366
 
@@ -382,7 +382,7 @@ shared_examples_for :notification_api do
382
382
  end
383
383
 
384
384
  context "as api options" do
385
- let(:created_notification) {
385
+ let(:created_notification) {
386
386
  described_class.notify_to(
387
387
  @user_1, @comment_2,
388
388
  key: 'custom_test_key',
@@ -592,6 +592,131 @@ shared_examples_for :notification_api do
592
592
  expect(@user_1.notifications.opened_only!.count).to eq(1)
593
593
  end
594
594
  end
595
+
596
+ context 'with ids options' do
597
+ it "opens notifications with specified IDs only" do
598
+ notification_to_open = @user_1.notifications.first
599
+ described_class.open_all_of(@user_1, { ids: [notification_to_open.id] })
600
+ expect(@user_1.notifications.unopened_only.count).to eq(1)
601
+ expect(@user_1.notifications.opened_only!.count).to eq(1)
602
+ expect(@user_1.notifications.opened_only!.first).to eq(notification_to_open)
603
+ end
604
+
605
+ it "applies other filter options when ids are specified" do
606
+ notification_to_open = @user_1.notifications.first
607
+ described_class.open_all_of(@user_1, {
608
+ ids: [notification_to_open.id],
609
+ filtered_by_key: 'non_existent_key'
610
+ })
611
+ expect(@user_1.notifications.unopened_only.count).to eq(2)
612
+ expect(@user_1.notifications.opened_only!.count).to eq(0)
613
+ end
614
+
615
+ it "only opens unopened notifications even when opened notification IDs are provided" do
616
+ # First open one notification
617
+ notification_to_open = @user_1.notifications.first
618
+ notification_to_open.open!
619
+
620
+ # Try to open it again using ids parameter
621
+ described_class.open_all_of(@user_1, { ids: [notification_to_open.id] })
622
+
623
+ # Should not affect the count since it was already opened
624
+ expect(@user_1.notifications.unopened_only.count).to eq(1)
625
+ expect(@user_1.notifications.opened_only!.count).to eq(1)
626
+ end
627
+ end
628
+ end
629
+
630
+ describe ".destroy_all_of" do
631
+ before do
632
+ described_class.notify_to(@user_1, @article, group: @article, key: 'key.1')
633
+ described_class.notify_to(@user_1, @comment_2, group: @comment_2, key: 'key.2')
634
+ expect(@user_1.notifications.count).to eq(2)
635
+ expect(@user_2.notifications.count).to eq(0)
636
+ end
637
+
638
+ it "returns array of destroyed notification records" do
639
+ destroyed_notifications = described_class.destroy_all_of(@user_1)
640
+ expect(destroyed_notifications).to be_a Array
641
+ expect(destroyed_notifications.size).to eq(2)
642
+ end
643
+
644
+ it "destroys all notifications of the target" do
645
+ described_class.destroy_all_of(@user_1)
646
+ expect(@user_1.notifications.count).to eq(0)
647
+ end
648
+
649
+ it "does not destroy any notifications of the other targets" do
650
+ described_class.destroy_all_of(@user_2)
651
+ expect(@user_1.notifications.count).to eq(2)
652
+ expect(@user_2.notifications.count).to eq(0)
653
+ end
654
+
655
+ context 'with filtered_by_type options' do
656
+ it "destroys filtered notifications only" do
657
+ described_class.destroy_all_of(@user_1, { filtered_by_type: @comment_2.to_class_name })
658
+ expect(@user_1.notifications.count).to eq(1)
659
+ expect(@user_1.notifications.first.notifiable).to eq(@article)
660
+ end
661
+ end
662
+
663
+ context 'with filtered_by_group options' do
664
+ it "destroys filtered notifications only" do
665
+ described_class.destroy_all_of(@user_1, { filtered_by_group: @comment_2 })
666
+ expect(@user_1.notifications.count).to eq(1)
667
+ expect(@user_1.notifications.first.notifiable).to eq(@article)
668
+ end
669
+ end
670
+
671
+ context 'with filtered_by_group_type and :filtered_by_group_id options' do
672
+ it "destroys filtered notifications only" do
673
+ described_class.destroy_all_of(@user_1, { filtered_by_group_type: 'Comment', filtered_by_group_id: @comment_2.id.to_s })
674
+ expect(@user_1.notifications.count).to eq(1)
675
+ expect(@user_1.notifications.first.notifiable).to eq(@article)
676
+ end
677
+ end
678
+
679
+ context 'with filtered_by_key options' do
680
+ it "destroys filtered notifications only" do
681
+ described_class.destroy_all_of(@user_1, { filtered_by_key: 'key.2' })
682
+ expect(@user_1.notifications.count).to eq(1)
683
+ expect(@user_1.notifications.first.notifiable).to eq(@article)
684
+ end
685
+ end
686
+
687
+ context 'with later_than options' do
688
+ it "destroys filtered notifications only" do
689
+ described_class.destroy_all_of(@user_1, { later_than: (@user_1.notifications.earliest.created_at.in_time_zone + 0.001).iso8601(3) })
690
+ expect(@user_1.notifications.count).to eq(1)
691
+ expect(@user_1.notifications.first).to eq(@user_1.notifications.earliest)
692
+ end
693
+ end
694
+
695
+ context 'with earlier_than options' do
696
+ it "destroys filtered notifications only" do
697
+ described_class.destroy_all_of(@user_1, { earlier_than: @user_1.notifications.latest.created_at.iso8601(3) })
698
+ expect(@user_1.notifications.count).to eq(1)
699
+ expect(@user_1.notifications.first).to eq(@user_1.notifications.latest)
700
+ end
701
+ end
702
+
703
+ context 'with ids options' do
704
+ it "destroys notifications with specified IDs only" do
705
+ notification_to_destroy = @user_1.notifications.first
706
+ described_class.destroy_all_of(@user_1, { ids: [notification_to_destroy.id] })
707
+ expect(@user_1.notifications.count).to eq(1)
708
+ expect(@user_1.notifications.first).not_to eq(notification_to_destroy)
709
+ end
710
+
711
+ it "applies other filter options when ids are specified" do
712
+ notification_to_destroy = @user_1.notifications.first
713
+ described_class.destroy_all_of(@user_1, {
714
+ ids: [notification_to_destroy.id],
715
+ filtered_by_key: 'non_existent_key'
716
+ })
717
+ expect(@user_1.notifications.count).to eq(2)
718
+ end
719
+ end
595
720
  end
596
721
 
597
722
  describe ".group_member_exists?" do
@@ -817,6 +942,37 @@ shared_examples_for :notification_api do
817
942
  expect(test_instance.open!(with_members: false)).to eq(1)
818
943
  end
819
944
  end
945
+
946
+ context "when the associated notifiable record has been deleted" do
947
+ let(:notifiable_id) { test_instance.notifiable.id }
948
+
949
+ before do
950
+ notifiable_class.where(id: notifiable_id).delete_all
951
+ test_instance.reload
952
+ end
953
+
954
+ it "ensures the notifiable is gone and the notification is still persisted" do
955
+ expect(notifiable_class.exists?(notifiable_id)).to be_falsey
956
+ expect(test_instance).to be_persisted
957
+ end
958
+
959
+ if ActivityNotification.config.orm == :active_record
960
+ it "does not open the notification without skip_validation option when using ActiveRecord" do
961
+ test_instance.open!
962
+ expect(test_instance.reload.opened?).to be_falsey
963
+ end
964
+ else
965
+ it "opens the notification without skip_validation option when using Mongoid or Dynamoid" do
966
+ test_instance.open!
967
+ expect(test_instance.reload.opened?).to be_truthy
968
+ end
969
+ end
970
+
971
+ it "opens the notification when skip_validation is true" do
972
+ test_instance.open!(skip_validation: true)
973
+ expect(test_instance.reload.opened?).to be_truthy
974
+ end
975
+ end
820
976
  end
821
977
 
822
978
  describe "#unopened?" do
@@ -833,7 +989,7 @@ shared_examples_for :notification_api do
833
989
  end
834
990
  end
835
991
  end
836
-
992
+
837
993
  describe "#opened?" do
838
994
  context "when opened_at is blank" do
839
995
  it "returns false" do
@@ -935,6 +935,13 @@ shared_examples_for :target do
935
935
  end
936
936
  end
937
937
 
938
+ describe "#destroy_all_notifications" do
939
+ it "is an alias of ActivityNotification::Notification.destroy_all_of" do
940
+ expect(ActivityNotification::Notification).to receive(:destroy_all_of)
941
+ test_instance.destroy_all_notifications
942
+ end
943
+ end
944
+
938
945
 
939
946
  # Methods to be overridden
940
947
 
@@ -92,7 +92,7 @@ module ActivityNotification
92
92
  def assert_all_schema_confirm(response, status)
93
93
  expect(response).to have_http_status(status)
94
94
  assert_request_schema_confirm
95
- assert_response_schema_confirm
95
+ assert_response_schema_confirm(status)
96
96
  end
97
97
  end
98
98
  end
@@ -275,6 +275,112 @@ shared_examples_for :notifications_api_controller do
275
275
  expect(@notification_2.reload.opened?).to be_truthy
276
276
  end
277
277
  end
278
+
279
+ context 'with ids parameter' do
280
+ it "opens only specified notifications" do
281
+ post_with_compatibility :open_all, target_params.merge({ typed_target_param => test_target, ids: [@notification_1.id] }), valid_session
282
+ expect(@notification_1.reload.opened?).to be_truthy
283
+ expect(@notification_2.reload.opened?).to be_falsey
284
+ end
285
+
286
+ it "applies other filter options when ids are specified" do
287
+ post_with_compatibility :open_all, target_params.merge({
288
+ typed_target_param => test_target,
289
+ ids: [@notification_1.id],
290
+ filtered_by_key: 'non_existent_key'
291
+ }), valid_session
292
+ expect(@notification_1.reload.opened?).to be_falsey
293
+ expect(@notification_2.reload.opened?).to be_falsey
294
+ end
295
+ end
296
+ end
297
+ end
298
+
299
+ describe "POST #destroy_all" do
300
+ context "http POST request" do
301
+ before do
302
+ @notification = create(:notification, target: test_target)
303
+ expect(test_target.notifications.count).to eq(1)
304
+ post_with_compatibility :destroy_all, target_params.merge({ typed_target_param => test_target }), valid_session
305
+ end
306
+
307
+ it "returns 200 as http status code" do
308
+ expect(response.status).to eq(200)
309
+ end
310
+
311
+ it "destroys all notifications of the target" do
312
+ expect(test_target.notifications.count).to eq(0)
313
+ end
314
+
315
+ it "returns JSON response" do
316
+ expect(response_json["count"]).to eq(1)
317
+ assert_json_with_object_array(response_json["notifications"], [@notification])
318
+ end
319
+ end
320
+
321
+ context "with filter request parameters" do
322
+ before do
323
+ @target_1, @notifiable_1, @group_1, @key_1 = create(:confirmed_user), create(:article), nil, "key.1"
324
+ @target_2, @notifiable_2, @group_2, @key_2 = create(:confirmed_user), create(:comment), @notifiable_1, "key.2"
325
+ @notification_1 = create(:notification, target: test_target, notifiable: @notifiable_1, group: @group_1, key: @key_1)
326
+ @notification_2 = create(:notification, target: test_target, notifiable: @notifiable_2, group: @group_2, key: @key_2, created_at: @notification_1.created_at + 10.second)
327
+ expect(test_target.notifications.count).to eq(2)
328
+ end
329
+
330
+ context "with filtered_by_type request parameters" do
331
+ it "destroys filtered notifications only" do
332
+ post_with_compatibility :destroy_all, target_params.merge({ typed_target_param => test_target, 'filtered_by_type' => @notifiable_2.to_class_name }), valid_session
333
+ expect(test_target.notifications.count).to eq(1)
334
+ expect(test_target.notifications.first).to eq(@notification_1)
335
+ end
336
+ end
337
+
338
+ context 'with filtered_by_group_type and :filtered_by_group_id request parameters' do
339
+ it "destroys filtered notifications only" do
340
+ post_with_compatibility :destroy_all, target_params.merge({ typed_target_param => test_target, 'filtered_by_group_type' => 'Article', 'filtered_by_group_id' => @group_2.id.to_s }), valid_session
341
+ expect(test_target.notifications.count).to eq(1)
342
+ expect(test_target.notifications.first).to eq(@notification_1)
343
+ end
344
+ end
345
+
346
+ context 'with filtered_by_key request parameters' do
347
+ it "destroys filtered notifications only" do
348
+ post_with_compatibility :destroy_all, target_params.merge({ typed_target_param => test_target, 'filtered_by_key' => 'key.2' }), valid_session
349
+ expect(test_target.notifications.count).to eq(1)
350
+ expect(test_target.notifications.first).to eq(@notification_1)
351
+ end
352
+ end
353
+
354
+ context 'with later_than request parameters' do
355
+ it "destroys filtered notifications only" do
356
+ post_with_compatibility :destroy_all, target_params.merge({ typed_target_param => test_target, 'later_than' => (@notification_1.created_at.in_time_zone + 5.second).iso8601(3) }), valid_session
357
+ expect(test_target.notifications.count).to eq(1)
358
+ expect(test_target.notifications.first).to eq(@notification_1)
359
+ end
360
+ end
361
+
362
+ context 'with earlier_than request parameters' do
363
+ it "destroys filtered notifications only" do
364
+ post_with_compatibility :destroy_all, target_params.merge({ typed_target_param => test_target, 'earlier_than' => (@notification_2.created_at.in_time_zone - 5.second).iso8601(3) }), valid_session
365
+ expect(test_target.notifications.count).to eq(1)
366
+ expect(test_target.notifications.first).to eq(@notification_2)
367
+ end
368
+ end
369
+
370
+ context "with ids request parameters" do
371
+ it "destroys notifications with specified IDs only" do
372
+ post_with_compatibility :destroy_all, target_params.merge({ typed_target_param => test_target, 'ids' => [@notification_2.id.to_s] }), valid_session
373
+ expect(test_target.notifications.count).to eq(1)
374
+ expect(test_target.notifications.first).to eq(@notification_1)
375
+ end
376
+ end
377
+
378
+ context "with no filter request parameters" do
379
+ it "destroys all notifications of the target" do
380
+ post_with_compatibility :destroy_all, target_params.merge({ typed_target_param => test_target}), valid_session
381
+ expect(test_target.notifications.count).to eq(0)
382
+ end
383
+ end
278
384
  end
279
385
  end
280
386
 
@@ -466,6 +572,13 @@ shared_examples_for :notifications_api_request do
466
572
  end
467
573
  end
468
574
 
575
+ describe "POST /{target_type}/{target_id}/notifications/destroy_all", type: :request do
576
+ it "returns response as API references" do
577
+ post_with_compatibility "#{api_path}/notifications/destroy_all", headers: @headers
578
+ assert_all_schema_confirm(response, 200)
579
+ end
580
+ end
581
+
469
582
  describe "GET /{target_type}/{target_id}/notifications/{id}", type: :request do
470
583
  it "returns response as API references" do
471
584
  get_with_compatibility "#{api_path}/notifications/#{@notification.id}", headers: @headers