discourse/spec/jobs/bookmark_reminder_notifications_spec.rb
Martin Brennan 793f39139a
FEATURE: Send notifications for time-based and At Desktop bookmark reminders (#9071)
* This PR implements the scheduling and notification system for bookmark reminders. Every 5 minutes a schedule runs to check any reminders that need to be sent before now, limited to **300** reminders at a time. Any leftover reminders will be sent in the next run. This is to avoid having to deal with fickle sidekiq and reminders in the far-flung future, which would necessitate having a background job anyway to clean up any missing `enqueue_at` reminders.

* If a reminder is sent its `reminder_at` time is cleared and the `reminder_last_sent_at` time is filled in. Notifications are only user-level notifications for now.

* All JavaScript and frontend code related to displaying the bookmark reminder notification is contained here. The reminder functionality is now re-enabled in the bookmark modal as well.

* This PR also implements the "Remind me next time I am at my desktop" bookmark reminder functionality. When the user is on a mobile device they are able to select this option. When they choose this option we set a key in Redis saying they have a pending at desktop reminder. The next time they change devices we check if the new device is desktop, and if it is we send reminders using a DistributedMutex. There is also a job to ensure consistency of these reminders in Redis (in case Redis drops the ball) and the at desktop reminders expire after 20 days.

* Also in this PR is a fix to delete all Bookmarks for a user via `UserDestroyer`
2020-03-12 10:16:00 +10:00

140 lines
5.5 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Jobs::BookmarkReminderNotifications do
subject { described_class.new }
let(:five_minutes_ago) { Time.zone.now - 5.minutes }
let(:bookmark1) { Fabricate(:bookmark) }
let(:bookmark2) { Fabricate(:bookmark) }
let(:bookmark3) { Fabricate(:bookmark) }
let!(:bookmarks) do
[
bookmark1,
bookmark2,
bookmark3
]
end
before do
SiteSetting.enable_bookmarks_with_reminders = true
# this is done to avoid model validations on Bookmark
bookmark1.update_column(:reminder_at, five_minutes_ago - 10.minutes)
bookmark2.update_column(:reminder_at, five_minutes_ago - 5.minutes)
bookmark3.update_column(:reminder_at, five_minutes_ago)
Discourse.redis.flushall
end
it "sends every reminder and marks the reminder_at to nil for all bookmarks, as well as last sent date" do
subject.execute
bookmark1.reload
bookmark2.reload
bookmark3.reload
expect(bookmark1.reminder_at).to eq(nil)
expect(bookmark1.reminder_last_sent_at).not_to eq(nil)
expect(bookmark2.reminder_at).to eq(nil)
expect(bookmark2.reminder_last_sent_at).not_to eq(nil)
expect(bookmark3.reminder_at).to eq(nil)
expect(bookmark3.reminder_last_sent_at).not_to eq(nil)
end
it "will not send a reminder for a bookmark in the future" do
bookmark4 = Fabricate(:bookmark, reminder_at: Time.zone.now + 1.day)
BookmarkReminderNotificationHandler.expects(:send_notification).with(bookmark1)
BookmarkReminderNotificationHandler.expects(:send_notification).with(bookmark2)
BookmarkReminderNotificationHandler.expects(:send_notification).with(bookmark3)
BookmarkReminderNotificationHandler.expects(:send_notification).with(bookmark4).never
subject.execute
expect(bookmark4.reload.reminder_at).not_to eq(nil)
end
it "increments the job run number correctly and resets to 1 when it reaches 6" do
expect(Discourse.redis.get(described_class::JOB_RUN_NUMBER_KEY)).to eq(nil)
subject.execute
expect(Discourse.redis.get(described_class::JOB_RUN_NUMBER_KEY)).to eq('1')
subject.execute
subject.execute
subject.execute
subject.execute
subject.execute
expect(Discourse.redis.get(described_class::JOB_RUN_NUMBER_KEY)).to eq('6')
subject.execute
expect(Discourse.redis.get(described_class::JOB_RUN_NUMBER_KEY)).to eq('1')
end
context "when the bookmark with reminder site setting is disabled" do
it "does nothing" do
Bookmark.expects(:where).never
SiteSetting.enable_bookmarks_with_reminders = false
subject.execute
end
end
context "when one of the bookmark reminder types is at_desktop" do
let(:bookmark1) { Fabricate(:bookmark, reminder_type: :at_desktop) }
it "is not included in the run, because it is not time-based" do
BookmarkManager.any_instance.expects(:send_reminder_notification).with(bookmark1.id).never
subject.execute
end
end
context "when the number of notifications exceed max_reminder_notifications_per_run" do
it "does not send them in the current run, but will send them in the next" do
begin
Jobs::BookmarkReminderNotifications.max_reminder_notifications_per_run = 2
subject.execute
expect(bookmark1.reload.reminder_at).to eq(nil)
expect(bookmark2.reload.reminder_at).to eq(nil)
expect(bookmark3.reload.reminder_at).not_to eq(nil)
end
end
end
context "when this is the 6th run (so every half hour) of this job we need to ensure consistency of at_desktop reminders" do
let(:set_at) { Time.zone.now }
let!(:bookmark) do
Fabricate(
:bookmark,
reminder_type: Bookmark.reminder_types[:at_desktop],
reminder_at: nil,
reminder_set_at: set_at
)
end
before do
Discourse.redis.set(Jobs::BookmarkReminderNotifications::JOB_RUN_NUMBER_KEY, 6)
bookmark.user.update(last_seen_at: Time.zone.now - 1.minute)
end
context "when an at_desktop reminder is not pending in redis for a user who should have one" do
it "puts the pending reminder into redis" do
expect(BookmarkReminderNotificationHandler.user_has_pending_at_desktop_reminders?(bookmark.user)).to eq(false)
subject.execute
expect(BookmarkReminderNotificationHandler.user_has_pending_at_desktop_reminders?(bookmark.user)).to eq(true)
end
context "if the user has not been seen in the past 24 hours" do
before do
bookmark.user.update(last_seen_at: Time.zone.now - 25.hours)
end
it "does not put the pending reminder into redis" do
subject.execute
expect(BookmarkReminderNotificationHandler.user_has_pending_at_desktop_reminders?(bookmark.user)).to eq(false)
end
end
context "if the at_desktop reminder is expired (set over PENDING_AT_DESKTOP_EXPIRY_DAYS days ago)" do
let(:set_at) { Time.zone.now - (BookmarkReminderNotificationHandler::PENDING_AT_DESKTOP_EXPIRY_DAYS + 1).days }
it "does not put the pending reminder into redis, and clears the reminder type/time" do
expect(BookmarkReminderNotificationHandler.user_has_pending_at_desktop_reminders?(bookmark.user)).to eq(false)
subject.execute
expect(BookmarkReminderNotificationHandler.user_has_pending_at_desktop_reminders?(bookmark.user)).to eq(false)
bookmark.reload
expect(bookmark.reminder_set_at).to eq(nil)
expect(bookmark.reminder_last_sent_at).to eq(nil)
expect(bookmark.reminder_type).to eq(nil)
end
end
end
end
end