separate date alert creation for individual users

pull/11880/head
ulferts 2 years ago
parent a149e9a787
commit 85bdcaf9c3
No known key found for this signature in database
GPG Key ID: A205708DE1284017
  1. 36
      app/workers/notifications/create_date_alerts_notifications_job.rb
  2. 16
      app/workers/notifications/create_date_alerts_notifications_job/alertable_work_packages.rb
  3. 41
      app/workers/notifications/create_date_alerts_notifications_job/service.rb
  4. 63
      app/workers/notifications/schedule_date_alerts_notifications_job.rb
  5. 65
      app/workers/notifications/schedule_date_alerts_notifications_job/service.rb
  6. 18
      config/initializers/cronjobs.rb
  7. 37
      db/migrate/20230105073117_remove_renamed_date_alert_job.rb
  8. 4
      spec/features/notifications/notification_center/notification_center_date_alerts_spec.rb
  9. 191
      spec/workers/notifications/create_date_alerts_notifications_job_spec.rb
  10. 198
      spec/workers/notifications/schedule_date_alerts_notifications_job_spec.rb

@ -23,41 +23,17 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
# See COPYRIGHT and LICENSE files for more details.
#++
module Notifications
# Creates date alerts for users whose local time is 1:00 am.
class CreateDateAlertsNotificationsJob < Cron::CronJob
# runs every quarter of an hour, so 00:00, 00:15,..., 15:30, 15:45, 16:00, ...
self.cron_expression = '*/15 * * * *'
def perform
class CreateDateAlertsNotificationsJob < ApplicationJob
def perform(user)
return unless EnterpriseToken.allows_to?(:date_alerts)
service = Service.new(times_from_scheduled_to_execution)
service.call
end
# Returns times from scheduled execution time to current time in 15 minutes
# steps.
#
# As scheduled execution time can be different from current time by more
# than 15 minutes when workers are busy, all times at 15 minutes interval
# between scheduled time and current time need to be considered to match
# with 1:00am in a time zone.
def times_from_scheduled_to_execution
time = scheduled_time
times = []
begin
times << time
time += 15.minutes
end while time < Time.current
times
end
def scheduled_time
self.class.delayed_job.run_at.then { |t| t.change(min: t.min / 15 * 15) }
Service
.new(user)
.call
end
end
end

@ -23,7 +23,7 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
# See COPYRIGHT and LICENSE files for more details.
#++
class Notifications::CreateDateAlertsNotificationsJob::AlertableWorkPackages
@ -34,19 +34,21 @@ class Notifications::CreateDateAlertsNotificationsJob::AlertableWorkPackages
end
def alertable_for_start
find_alertables
.filter_map { |row| row["id"] if row["start_alert"] }
.then { |ids| WorkPackage.where(id: ids) }
alertable_for("start_alert")
end
def alertable_for_due
find_alertables
.filter_map { |row| row["id"] if row["due_alert"] }
.then { |ids| WorkPackage.where(id: ids) }
alertable_for("due_alert")
end
private
def alertable_for(alert)
find_alertables
.filter_map { |row| row["id"] if row[alert] }
.then { |ids| WorkPackage.where(id: ids) }
end
def find_alertables
@find_alertables ||= ActiveRecord::Base.connection.execute(query).to_a
end

@ -26,36 +26,23 @@
# See COPYRIGHT and LICENSE files for more details.
#++
# Creates date alerts notifications for users whose local time is 1am for the
# given run_times.
class Notifications::CreateDateAlertsNotificationsJob::Service
attr_reader :run_times
# @param run_times [Array<DateTime>] the times for which the service is run.
# Must be multiple of 15 minutes (xx:00, xx:15, xx:30, or xx:45).
def initialize(run_times)
@run_times = run_times
def initialize(user)
@user = user
end
def call
return unless EnterpriseToken.allows_to?(:date_alerts)
time_zones = time_zones_covering_1am_local_time
return if time_zones.empty?
# warning: there may be a subtle bug here: if many run_times are given, time
# zones will have different time shifting. This should be ok: as the period
# covered is small this should not have any impact. If the period is more
# than 23h, then the day will change.
Time.use_zone(time_zones.first) do
User.with_time_zone(time_zones).find_each do |user|
send_date_alert_notifications(user)
end
Time.use_zone(user.time_zone) do
send_date_alert_notifications(user)
end
end
private
attr_accessor :user
def send_date_alert_notifications(user)
alertables = Notifications::CreateDateAlertsNotificationsJob::AlertableWorkPackages.new(user)
create_date_alert_notifications(user, alertables.alertable_for_start, :date_alert_start_date)
@ -86,20 +73,4 @@ class Notifications::CreateDateAlertsNotificationsJob::Service
reason:
)
end
def time_zones_covering_1am_local_time
UserPreferences::UpdateContract
.assignable_time_zones
.select { |time_zone| executing_at_1am_for_timezone?(time_zone) }
.map { |time_zone| time_zone.tzinfo.canonical_zone.name }
end
def executing_at_1am_for_timezone?(time_zone)
run_times.any? { |time| is_1am?(time, time_zone) }
end
def is_1am?(time, time_zone)
local_time = time.in_time_zone(time_zone)
local_time.strftime('%H:%M') == '01:00'
end
end

@ -0,0 +1,63 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
module Notifications
# Creates date alert jobs for users whose local time is 1:00 am.
class ScheduleDateAlertsNotificationsJob < Cron::CronJob
# runs every quarter of an hour, so 00:00, 00:15,..., 15:30, 15:45, 16:00, ...
self.cron_expression = '*/15 * * * *'
def perform
return unless EnterpriseToken.allows_to?(:date_alerts)
service = Service.new(times_from_scheduled_to_execution)
service.call
end
# Returns times from scheduled execution time to current time in 15 minutes
# steps.
#
# As scheduled execution time can be different from current time by more
# than 15 minutes when workers are busy, all times at 15 minutes interval
# between scheduled time and current time need to be considered to match
# with 1:00am in a time zone.
def times_from_scheduled_to_execution
time = scheduled_time
times = []
begin
times << time
time += 15.minutes
end while time < Time.current
times
end
def scheduled_time
self.class.delayed_job.run_at.then { |t| t.change(min: t.min / 15 * 15) }
end
end
end

@ -0,0 +1,65 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
# Creates date alerts notifications for users whose local time is 1am for the
# given run_times.
class Notifications::ScheduleDateAlertsNotificationsJob::Service
attr_reader :run_times
# @param run_times [Array<DateTime>] the times for which the service is run.
# Must be multiple of 15 minutes (xx:00, xx:15, xx:30, or xx:45).
def initialize(run_times)
@run_times = run_times
end
def call
return unless EnterpriseToken.allows_to?(:date_alerts)
User.with_time_zone(time_zones_covering_1am_local_time).find_each do |user|
Notifications::CreateDateAlertsNotificationsJob.perform_later(user)
end
end
private
def time_zones_covering_1am_local_time
UserPreferences::UpdateContract
.assignable_time_zones
.select { |time_zone| executing_at_1am_for_timezone?(time_zone) }
.map { |time_zone| time_zone.tzinfo.canonical_zone.name }
end
def executing_at_1am_for_timezone?(time_zone)
run_times.any? { |time| is_1am?(time, time_zone) }
end
def is_1am?(time, time_zone)
local_time = time.in_time_zone(time_zone)
local_time.strftime('%H:%M') == '01:00'
end
end

@ -2,14 +2,14 @@
OpenProject::Application.configure do |application|
application.config.to_prepare do
::Cron::CronJob.register! ::Cron::ClearOldSessionsJob,
::Cron::ClearTmpCacheJob,
::Cron::ClearUploadedFilesJob,
::OAuth::CleanupJob,
::PaperTrailAudits::CleanupJob,
::Attachments::CleanupUncontaineredJob,
::Notifications::CreateDateAlertsNotificationsJob,
::Notifications::ScheduleReminderMailsJob,
::Ldap::SynchronizationJob
Cron::CronJob.register! Cron::ClearOldSessionsJob,
Cron::ClearTmpCacheJob,
Cron::ClearUploadedFilesJob,
OAuth::CleanupJob,
PaperTrailAudits::CleanupJob,
Attachments::CleanupUncontaineredJob,
Notifications::ScheduleDateAlertsNotificationsJob,
Notifications::ScheduleReminderMailsJob,
Ldap::SynchronizationJob
end
end

@ -0,0 +1,37 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class RemoveRenamedDateAlertJob < ActiveRecord::Migration[6.0]
def up
# The job has been renamed to Notifications::ScheduleDateAlertsNotificationsJob.
# The new job will be added on restarting the application.
Delayed::Job
.where('handler LIKE ?', "%job_class: Notifications::CreateDateAlertsNotificationsJob%")
.delete_all
end
end

@ -166,13 +166,15 @@ describe "Notification center date alerts", js: true, with_settings: { journal_a
end
def run_create_date_alerts_notifications_job
create_date_alerts_service = Notifications::CreateDateAlertsNotificationsJob::Service.new([timezone_time('1:00', time_zone)])
create_date_alerts_service = Notifications::ScheduleDateAlertsNotificationsJob::Service
.new([timezone_time('1:00', time_zone)])
travel_to(timezone_time('1:04', time_zone))
create_date_alerts_service.call
end
before do
run_create_date_alerts_notifications_job
perform_enqueued_jobs
login_as user
visit notifications_center_path
end

@ -23,7 +23,7 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
# See COPYRIGHT and LICENSE files for more details.
#++
require 'spec_helper'
@ -48,43 +48,19 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: %
shared_let(:in_3_days) { today + 3.days }
shared_let(:in_7_days) { today + 7.days }
shared_let(:user_paris) do
shared_let(:user) do
create(
:user,
firstname: 'Paris',
preferences: { time_zone: timezone_paris.name }
)
end
shared_let(:user_berlin) do
create(
:user,
firstname: 'Berlin',
preferences: { time_zone: timezone_berlin.name }
)
end
shared_let(:user_kathmandu) do
create(
:user,
firstname: 'Kathmandu',
preferences: { time_zone: timezone_kathmandu.name }
)
end
shared_let(:alertable_work_packages) do
create_list(:work_package, 2, project:, author: user_paris)
end
let(:scheduled_job) do
described_class.ensure_scheduled!
described_class.delayed_job
create_list(:work_package, 2, project:, author: user)
end
before do
# We need to access the job as stored in the database to get at the run_at time persisted there
allow(ActiveJob::Base)
.to receive(:queue_adapter)
.and_return(ActiveJob::QueueAdapters::DelayedJobAdapter.new)
end
let(:job) { described_class }
define :have_a_start_date_alert_notification_for do |work_package|
match do |user|
@ -113,9 +89,9 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: %
when WorkPackage then %i[id start_date due_date assigned_to_id responsible_id]
end
formatted_pairs = object
.slice(*keys)
.map { |k, v| "#{k}: #{v.is_a?(Date) ? v.to_s : v.inspect}" }
.join(', ')
.slice(*keys)
.map { |k, v| "#{k}: #{v.is_a?(Date) ? v.to_s : v.inspect}" }
.join(', ')
"#<#{formatted_pairs}>"
end
end
@ -147,19 +123,15 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: %
when WorkPackage then %i[id start_date due_date assigned_to_id responsible_id]
end
formatted_pairs = object
.slice(*keys)
.map { |k, v| "#{k}: #{v.is_a?(Date) ? v.to_s : v.inspect}" }
.join(', ')
.slice(*keys)
.map { |k, v| "#{k}: #{v.is_a?(Date) ? v.to_s : v.inspect}" }
.join(', ')
"#<#{formatted_pairs}>"
end
end
def set_scheduled_time(run_at)
scheduled_job.update_column(:run_at, run_at)
end
def alertable_work_package(attributes = {})
assignee = attributes.slice(:responsible, :responsible_id).any? ? nil : user_paris
assignee = attributes.slice(:responsible, :responsible_id).any? ? nil : user
attributes = attributes.reverse_merge(
start_date: in_1_day,
assigned_to: assignee
@ -179,9 +151,8 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: %
end
def run_job(scheduled_at: '1:00', local_time: '1:04', timezone: timezone_paris)
set_scheduled_time(timezone_time(scheduled_at, timezone))
travel_to(timezone_time(local_time, timezone)) do
scheduled_job.reload.invoke_job
job.perform_now(user)
yield
end
@ -193,57 +164,63 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: %
closed_work_package = alertable_work_package(status: status_closed)
run_job do
expect(user_paris).to have_a_start_date_alert_notification_for(open_work_package)
expect(user_paris).not_to have_a_start_date_alert_notification_for(closed_work_package)
expect(user).to have_a_start_date_alert_notification_for(open_work_package)
expect(user).not_to have_a_start_date_alert_notification_for(closed_work_package)
end
end
it 'creates date alert notifications only for users whose local time is 1:00 am when the job is executed' do
work_package_for_paris_user = alertable_work_package(assigned_to: user_paris,
start_date: timezone_paris.today + 1.day)
work_package_for_kathmandu_user = alertable_work_package(assigned_to: user_kathmandu,
start_date: timezone_kathmandu.today + 1.day)
it 'creates date alert notifications if user is assigned to the work package' do
work_package_assigned = alertable_work_package(assigned_to: user)
run_job(timezone: timezone_paris) do
expect(user_paris).to have_a_start_date_alert_notification_for(work_package_for_paris_user)
expect(user_kathmandu).not_to have_a_start_date_alert_notification_for(work_package_for_kathmandu_user)
run_job do
expect(user).to have_a_start_date_alert_notification_for(work_package_assigned)
end
end
# change timezone to cover Kathmandu
run_job(timezone: timezone_kathmandu) do
expect(user_kathmandu).to have_a_start_date_alert_notification_for(work_package_for_kathmandu_user)
it 'creates date alert notifications if user is accountable of the work package' do
work_package_accountable = alertable_work_package(responsible: user)
run_job do
expect(user).to have_a_start_date_alert_notification_for(work_package_accountable)
end
end
it 'creates date alert notifications if user is assigned to or accountable of the work package' do
work_package_assigned = alertable_work_package(assigned_to: user_paris)
work_package_accountable = alertable_work_package(responsible: user_paris)
it 'creates date alert notifications if user is watcher of the work package' do
work_package_watched = alertable_work_package(responsible: nil)
build(:watcher, watchable: work_package_watched, user:).save(validate: false)
run_job do
expect(user_paris).to have_a_start_date_alert_notification_for(work_package_assigned)
expect(user_paris).to have_a_start_date_alert_notification_for(work_package_accountable)
expect(user).to have_a_start_date_alert_notification_for(work_package_watched)
end
end
it 'creates start and finish date alert notifications based on user notification settings' do
user_paris.notification_settings.first.update(
it 'creates start date alert notifications based on user notification settings' do
user.notification_settings.first.update(
start_date: 1,
due_date: nil
)
user_berlin.notification_settings.first.update(
work_package = alertable_work_package(assigned_to: user,
start_date: in_1_day,
due_date: in_3_days)
run_job do
expect(user).to have_a_start_date_alert_notification_for(work_package)
expect(user).not_to have_a_due_date_alert_notification_for(work_package)
end
end
it 'creates due date alert notifications based on user notification settings' do
user.notification_settings.first.update(
start_date: nil,
due_date: 3
)
work_package = alertable_work_package(assigned_to: user_paris,
responsible: user_berlin,
work_package = alertable_work_package(assigned_to: user,
start_date: in_1_day,
due_date: in_3_days)
run_job do
expect(user_paris).to have_a_start_date_alert_notification_for(work_package)
expect(user_paris).not_to have_a_due_date_alert_notification_for(work_package)
expect(user_berlin).not_to have_a_start_date_alert_notification_for(work_package)
expect(user_berlin).to have_a_due_date_alert_notification_for(work_package)
expect(user).not_to have_a_start_date_alert_notification_for(work_package)
expect(user).to have_a_due_date_alert_notification_for(work_package)
end
end
@ -252,7 +229,7 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: %
work_package = alertable_work_package
run_job do
expect(user_paris).not_to have_a_start_date_alert_notification_for(work_package)
expect(user).not_to have_a_start_date_alert_notification_for(work_package)
end
end
end
@ -260,52 +237,52 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: %
context 'when project notification settings are defined for a user' do
it 'creates date alert notifications using these settings for work packages of the project' do
# global notification settings
user_paris.notification_settings.first.update(
user.notification_settings.first.update(
start_date: 1,
due_date: nil
)
# project notifications settings
user_paris.notification_settings.create(
user.notification_settings.create(
project:,
start_date: nil,
due_date: 7
)
silent_work_package = alertable_work_package(assigned_to: user_paris,
silent_work_package = alertable_work_package(assigned_to: user,
project:,
start_date: in_1_day,
due_date: in_1_day)
noisy_work_package = alertable_work_package(assigned_to: user_paris,
noisy_work_package = alertable_work_package(assigned_to: user,
project:,
start_date: in_7_days,
due_date: in_7_days)
run_job do
expect(user_paris).not_to have_a_start_date_alert_notification_for(silent_work_package)
expect(user_paris).not_to have_a_due_date_alert_notification_for(silent_work_package)
expect(user_paris).not_to have_a_start_date_alert_notification_for(noisy_work_package)
expect(user_paris).to have_a_due_date_alert_notification_for(noisy_work_package)
expect(user).not_to have_a_start_date_alert_notification_for(silent_work_package)
expect(user).not_to have_a_due_date_alert_notification_for(silent_work_package)
expect(user).not_to have_a_start_date_alert_notification_for(noisy_work_package)
expect(user).to have_a_due_date_alert_notification_for(noisy_work_package)
end
end
end
context 'with existing date alerts' do
it 'marks them as read when new ones are created' do
work_package = alertable_work_package(assigned_to: user_paris,
work_package = alertable_work_package(assigned_to: user,
start_date: in_1_day,
due_date: in_1_day)
existing_start_notification = create(:notification,
resource: work_package,
recipient: user_paris,
recipient: user,
reason: :date_alert_start_date)
existing_due_notification = create(:notification,
resource: work_package,
recipient: user_paris,
recipient: user,
reason: :date_alert_due_date)
run_job do
expect(existing_start_notification.reload).to have_attributes(read_ian: true)
expect(existing_due_notification.reload).to have_attributes(read_ian: true)
unread_date_alert_notifications = Notification.where(recipient: user_paris,
unread_date_alert_notifications = Notification.where(recipient: user,
read_ian: false,
resource: work_package)
expect(unread_date_alert_notifications.pluck(:reason))
@ -315,27 +292,27 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: %
# rubocop:disable RSpec/ExampleLength
it 'does not mark them as read when if no new notifications are created' do
work_package_start = alertable_work_package(assigned_to: user_paris,
work_package_start = alertable_work_package(assigned_to: user,
start_date: in_1_day,
due_date: nil)
work_package_due = alertable_work_package(assigned_to: user_paris,
work_package_due = alertable_work_package(assigned_to: user,
start_date: nil,
due_date: in_1_day)
existing_wp_start_start_notif = create(:notification,
reason: :date_alert_start_date,
recipient: user_paris,
recipient: user,
resource: work_package_start)
existing_wp_start_due_notif = create(:notification,
reason: :date_alert_due_date,
recipient: user_paris,
recipient: user,
resource: work_package_start)
existing_wp_due_start_notif = create(:notification,
reason: :date_alert_start_date,
recipient: user_paris,
recipient: user,
resource: work_package_due)
existing_wp_due_due_notif = create(:notification,
reason: :date_alert_due_date,
recipient: user_paris,
recipient: user,
resource: work_package_due)
run_job do
@ -347,45 +324,5 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: %
end
# rubocop:enable RSpec/ExampleLength
end
context 'when scheduled and executed at 01:00 am local time' do
it 'creates a start date alert notification for a user in the same time zone' do
work_package = alertable_work_package
run_job(scheduled_at: '1:00', local_time: '1:00') do
expect(user_paris).to have_a_start_date_alert_notification_for(work_package)
end
end
end
context 'when scheduled and executed at 01:14 am local time' do
it 'creates a start date alert notification for a user in the same time zone' do
work_package = alertable_work_package
run_job(scheduled_at: '1:14', local_time: '1:14') do
expect(user_paris).to have_a_start_date_alert_notification_for(work_package)
end
end
end
context 'when scheduled and executed at 01:15 am local time' do
it 'does not create a start date alert notification for a user in the same time zone' do
work_package = alertable_work_package
run_job(scheduled_at: '1:15', local_time: '1:15') do
expect(user_paris).not_to have_a_start_date_alert_notification_for(work_package)
end
end
end
context 'when scheduled at 01:00 am local time and executed at 01:37 am local time' do
it 'creates a start date alert notification for a user in the same time zone' do
work_package = alertable_work_package
run_job(scheduled_at: '1:00', local_time: '1:37') do
expect(user_paris).to have_a_start_date_alert_notification_for(work_package)
end
end
end
end
end

@ -0,0 +1,198 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe Notifications::ScheduleDateAlertsNotificationsJob, type: :job, with_ee: %i[date_alerts] do
include ActiveSupport::Testing::TimeHelpers
shared_let(:project) { create(:project, name: 'main') }
# Paris and Berlin are both UTC+01:00 (CET) or UTC+02:00 (CEST)
shared_let(:timezone_paris) { ActiveSupport::TimeZone['Europe/Paris'] }
# Kathmandu is UTC+05:45 (no DST)
shared_let(:timezone_kathmandu) { ActiveSupport::TimeZone['Asia/Kathmandu'] }
shared_let(:user_paris) do
create(
:user,
firstname: 'Paris',
preferences: { time_zone: timezone_paris.name }
)
end
shared_let(:user_kathmandu) do
create(
:user,
firstname: 'Kathmandu',
preferences: { time_zone: timezone_kathmandu.name }
)
end
let(:schedule_job) do
described_class.ensure_scheduled!
described_class.delayed_job
end
before do
# We need to access the job as stored in the database to get at the run_at time persisted there
allow(ActiveJob::Base)
.to receive(:queue_adapter)
.and_return(ActiveJob::QueueAdapters::DelayedJobAdapter.new)
schedule_job
end
def set_scheduled_time(run_at)
schedule_job.update_column(:run_at, run_at)
end
# Converts "hh:mm" into { hour: h, min: m }
def time_hash(time)
%i[hour min].zip(time.split(':', 2).map(&:to_i)).to_h
end
def timezone_time(time, timezone)
timezone.now.change(time_hash(time))
end
def run_job(scheduled_at: '1:00', local_time: '1:04', timezone: timezone_paris)
set_scheduled_time(timezone_time(scheduled_at, timezone))
travel_to(timezone_time(local_time, timezone)) do
schedule_job.reload.invoke_job
yield if block_given?
end
end
def deserialized_of_job(job)
deserializer_class = Class.new do
include(ActiveJob::Arguments)
end
deserializer = deserializer_class.new
deserializer.deserialize(job.payload_object.job_data).to_h
end
def expect_job(job, klass, *arguments)
job_data = deserialized_of_job(job)
expect(job_data['job_class'])
.to eql klass
expect(job_data['arguments'])
.to match_array arguments
end
shared_examples_for 'job execution creates date alerts creation job' do
let(:timezone) { timezone_paris }
let(:scheduled_at) { '1:00' }
let(:local_time) { '1:04' }
let(:user) { user_paris }
it 'creates the job for the user' do
expect do
run_job(timezone:, scheduled_at:, local_time:) do
expect_job(Delayed::Job.last, "Notifications::CreateDateAlertsNotificationsJob", user)
end
end.to change(Delayed::Job, :count).by 1
end
end
shared_examples_for 'job execution creates no date alerts creation job' do
let(:timezone) { timezone_paris }
let(:scheduled_at) { '1:00' }
let(:local_time) { '1:04' }
it 'creates no job' do
expect do
run_job(timezone:, scheduled_at:, local_time:)
end.not_to change(Delayed::Job, :count)
end
end
describe '#perform' do
context 'for users whose local time is 1:00 am (UTC+1) when the job is executed' do
it_behaves_like 'job execution creates date alerts creation job' do
let(:timezone) { timezone_paris }
let(:scheduled_at) { '1:00' }
let(:local_time) { '1:04' }
let(:user) { user_paris }
end
end
context 'for users whose local time is 1:00 am (UTC+05:45) when the job is executed' do
it_behaves_like 'job execution creates date alerts creation job' do
let(:timezone) { timezone_kathmandu }
let(:scheduled_at) { '1:00' }
let(:local_time) { '1:04' }
let(:user) { user_kathmandu }
end
end
context 'without enterprise token', with_ee: false do
it_behaves_like 'job execution creates no date alerts creation job' do
let(:timezone) { timezone_paris }
let(:scheduled_at) { '1:00' }
let(:local_time) { '1:04' }
end
end
context 'when scheduled and executed at 01:00 am local time' do
it_behaves_like 'job execution creates date alerts creation job' do
let(:timezone) { timezone_paris }
let(:scheduled_at) { '1:00' }
let(:local_time) { '1:00' }
let(:user) { user_paris }
end
end
context 'when scheduled and executed at 01:14 am local time' do
it_behaves_like 'job execution creates date alerts creation job' do
let(:timezone) { timezone_paris }
let(:scheduled_at) { '1:14' }
let(:local_time) { '1:14' }
let(:user) { user_paris }
end
end
context 'when scheduled and executed at 01:15 am local time' do
it_behaves_like 'job execution creates no date alerts creation job' do
let(:timezone) { timezone_paris }
let(:scheduled_at) { '1:15' }
let(:local_time) { '1:15' }
end
end
context 'when scheduled at 01:00 am local time and executed at 01:37 am local time' do
it_behaves_like 'job execution creates date alerts creation job' do
let(:timezone) { timezone_paris }
let(:scheduled_at) { '1:00' }
let(:local_time) { '1:37' }
let(:user) { user_paris }
end
end
end
end
Loading…
Cancel
Save