Refactoring: Migrate job_test to RSpec
This commit is contained in:
parent
cdf4eaf92a
commit
a32f5bb01c
3 changed files with 766 additions and 1204 deletions
|
@ -6,45 +6,129 @@ FactoryBot.define do
|
||||||
active { true }
|
active { true }
|
||||||
created_by_id { 1 }
|
created_by_id { 1 }
|
||||||
updated_by_id { 1 }
|
updated_by_id { 1 }
|
||||||
|
|
||||||
timeplan do
|
timeplan do
|
||||||
{ 'days' => { 'Mon' => true,
|
{ days: { Mon: true,
|
||||||
'Tue' => false,
|
Tue: false,
|
||||||
'Wed' => false,
|
Wed: false,
|
||||||
'Thu' => false,
|
Thu: false,
|
||||||
'Fri' => false,
|
Fri: false,
|
||||||
'Sat' => false,
|
Sat: false,
|
||||||
'Sun' => false },
|
Sun: false },
|
||||||
'hours' =>
|
hours: { 0 => true,
|
||||||
{ '0' => true,
|
1 => false,
|
||||||
'1' => false,
|
2 => false,
|
||||||
'2' => false,
|
3 => false,
|
||||||
'3' => false,
|
4 => false,
|
||||||
'4' => false,
|
5 => false,
|
||||||
'5' => false,
|
6 => false,
|
||||||
'6' => false,
|
7 => false,
|
||||||
'7' => false,
|
8 => false,
|
||||||
'8' => false,
|
9 => false,
|
||||||
'9' => false,
|
10 => false,
|
||||||
'10' => false,
|
11 => false,
|
||||||
'11' => false,
|
12 => false,
|
||||||
'12' => false,
|
13 => false,
|
||||||
'13' => false,
|
14 => false,
|
||||||
'14' => false,
|
15 => false,
|
||||||
'15' => false,
|
16 => false,
|
||||||
'16' => false,
|
17 => false,
|
||||||
'17' => false,
|
18 => false,
|
||||||
'18' => false,
|
19 => false,
|
||||||
'19' => false,
|
20 => false,
|
||||||
'20' => false,
|
21 => false,
|
||||||
'21' => false,
|
22 => false,
|
||||||
'22' => false,
|
23 => false },
|
||||||
'23' => false },
|
minutes: { 0 => true,
|
||||||
'minutes' => { '0' => true,
|
10 => false,
|
||||||
'10' => false,
|
20 => false,
|
||||||
'20' => false,
|
30 => false,
|
||||||
'30' => false,
|
40 => false,
|
||||||
'40' => false,
|
50 => false } }
|
||||||
'50' => false } }
|
end
|
||||||
|
|
||||||
|
trait :always_on do
|
||||||
|
timeplan do
|
||||||
|
{ days: { Mon: true,
|
||||||
|
Tue: true,
|
||||||
|
Wed: true,
|
||||||
|
Thu: true,
|
||||||
|
Fri: true,
|
||||||
|
Sat: true,
|
||||||
|
Sun: true },
|
||||||
|
hours: { 0 => true,
|
||||||
|
1 => true,
|
||||||
|
2 => true,
|
||||||
|
3 => true,
|
||||||
|
4 => true,
|
||||||
|
5 => true,
|
||||||
|
6 => true,
|
||||||
|
7 => true,
|
||||||
|
8 => true,
|
||||||
|
9 => true,
|
||||||
|
10 => true,
|
||||||
|
11 => true,
|
||||||
|
12 => true,
|
||||||
|
13 => true,
|
||||||
|
14 => true,
|
||||||
|
15 => true,
|
||||||
|
16 => true,
|
||||||
|
17 => true,
|
||||||
|
18 => true,
|
||||||
|
19 => true,
|
||||||
|
20 => true,
|
||||||
|
21 => true,
|
||||||
|
22 => true,
|
||||||
|
23 => true },
|
||||||
|
minutes: { 0 => true,
|
||||||
|
10 => true,
|
||||||
|
20 => true,
|
||||||
|
30 => true,
|
||||||
|
40 => true,
|
||||||
|
50 => true } }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
trait :never_on do
|
||||||
|
timeplan do
|
||||||
|
{ days: { Mon: false,
|
||||||
|
Tue: false,
|
||||||
|
Wed: false,
|
||||||
|
Thu: false,
|
||||||
|
Fri: false,
|
||||||
|
Sat: false,
|
||||||
|
Sun: false },
|
||||||
|
hours: { 0 => false,
|
||||||
|
1 => false,
|
||||||
|
2 => false,
|
||||||
|
3 => false,
|
||||||
|
4 => false,
|
||||||
|
5 => false,
|
||||||
|
6 => false,
|
||||||
|
7 => false,
|
||||||
|
8 => false,
|
||||||
|
9 => false,
|
||||||
|
10 => false,
|
||||||
|
11 => false,
|
||||||
|
12 => false,
|
||||||
|
13 => false,
|
||||||
|
14 => false,
|
||||||
|
15 => false,
|
||||||
|
16 => false,
|
||||||
|
17 => false,
|
||||||
|
18 => false,
|
||||||
|
19 => false,
|
||||||
|
20 => false,
|
||||||
|
21 => false,
|
||||||
|
22 => false,
|
||||||
|
23 => false },
|
||||||
|
minutes: { 0 => false,
|
||||||
|
10 => false,
|
||||||
|
20 => false,
|
||||||
|
30 => false,
|
||||||
|
40 => false,
|
||||||
|
50 => false } }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,4 +3,648 @@ require 'models/application_model_examples'
|
||||||
|
|
||||||
RSpec.describe Job, type: :model do
|
RSpec.describe Job, type: :model do
|
||||||
it_behaves_like 'ApplicationModel', can_assets: { selectors: %i[condition perform] }
|
it_behaves_like 'ApplicationModel', can_assets: { selectors: %i[condition perform] }
|
||||||
|
|
||||||
|
subject(:job) { create(:job) }
|
||||||
|
|
||||||
|
describe 'Class methods:' do
|
||||||
|
describe '.run' do
|
||||||
|
let!(:executable_jobs) { jobs.select(&:executable?).select(&:in_timeplan?) }
|
||||||
|
let!(:nonexecutable_jobs) { jobs - executable_jobs }
|
||||||
|
|
||||||
|
let!(:jobs) do
|
||||||
|
[
|
||||||
|
# executable
|
||||||
|
create(:job, :always_on, updated_at: 2.minutes.ago),
|
||||||
|
|
||||||
|
# not executable (updated too recently)
|
||||||
|
create(:job),
|
||||||
|
|
||||||
|
# not executable (inactive)
|
||||||
|
create(:job, updated_at: 2.minutes.ago, active: false),
|
||||||
|
|
||||||
|
# not executable (started too recently)
|
||||||
|
create(:job, :always_on, updated_at: 2.minutes.ago, last_run_at: 5.minutes.ago),
|
||||||
|
|
||||||
|
# executable
|
||||||
|
create(:job, :always_on, updated_at: 2.minutes.ago, last_run_at: 15.minutes.ago),
|
||||||
|
|
||||||
|
# not executable (still running, started too recently)
|
||||||
|
create(:job, :always_on, updated_at: 2.minutes.ago, running: true, last_run_at: 23.hours.ago),
|
||||||
|
|
||||||
|
# executable
|
||||||
|
create(:job, :always_on, updated_at: 2.minutes.ago, running: true, last_run_at: 25.hours.ago),
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'runs all executable jobs (and no others)' do
|
||||||
|
expect { Job.run }
|
||||||
|
.to change { executable_jobs.map(&:reload).map(&:last_run_at).any?(&:nil?) }.to(false)
|
||||||
|
.and not_change { nonexecutable_jobs.map(&:reload).map(&:last_run_at).all?(&:nil?) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Instance methods:' do
|
||||||
|
describe '#run' do
|
||||||
|
subject(:job) { create(:job, condition: condition, perform: perform) }
|
||||||
|
|
||||||
|
let(:condition) do
|
||||||
|
{
|
||||||
|
'ticket.state_id' => {
|
||||||
|
'operator' => 'is',
|
||||||
|
'value' => Ticket::State.where(name: %w[new open]).pluck(:id).map(&:to_s)
|
||||||
|
},
|
||||||
|
'ticket.created_at' => {
|
||||||
|
'operator' => 'before (relative)',
|
||||||
|
'value' => '2',
|
||||||
|
'range' => 'day'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:perform) do
|
||||||
|
{ 'ticket.state_id' => { 'value' => Ticket::State.find_by(name: 'closed').id.to_s } }
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:matching_ticket) do
|
||||||
|
create(:ticket, state: Ticket::State.lookup(name: 'new'), created_at: 3.days.ago)
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:nonmatching_ticket) do
|
||||||
|
create(:ticket, state: Ticket::State.lookup(name: 'new'), created_at: 1.day.ago)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when job is not #executable?' do
|
||||||
|
before { allow(job).to receive(:executable?).and_return(false) }
|
||||||
|
|
||||||
|
it 'does not perform changes on matching tickets' do
|
||||||
|
expect { job.run }.not_to change { matching_ticket.reload.state }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not update #last_run_at' do
|
||||||
|
expect { job.run }.to not_change { job.reload.last_run_at }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'but "force" flag is given' do
|
||||||
|
it 'performs changes on matching tickets' do
|
||||||
|
expect { job.run(true) }
|
||||||
|
.to change { matching_ticket.reload.state }
|
||||||
|
.and not_change { nonmatching_ticket.reload.state }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates #last_run_at' do
|
||||||
|
expect { job.run(true) }
|
||||||
|
.to change { job.reload.last_run_at }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when job is #executable?' do
|
||||||
|
before { allow(job).to receive(:executable?).and_return(true) }
|
||||||
|
|
||||||
|
context 'and due (#in_timeplan?)' do
|
||||||
|
before { allow(job).to receive(:in_timeplan?).and_return(true) }
|
||||||
|
|
||||||
|
it 'updates #last_run_at' do
|
||||||
|
expect { job.run }.to change { job.reload.last_run_at }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'performs changes on matching tickets' do
|
||||||
|
expect { job.run }
|
||||||
|
.to change { matching_ticket.reload.state }
|
||||||
|
.and not_change { nonmatching_ticket.reload.state }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and already marked #running' do
|
||||||
|
before { job.update(running: true) }
|
||||||
|
|
||||||
|
it 'resets #running to false' do
|
||||||
|
expect { job.run }.to change { job.reload.running }.to(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'but condition doesn’t match any tickets' do
|
||||||
|
before { job.update(condition: invalid_condition) }
|
||||||
|
|
||||||
|
let(:invalid_condition) do
|
||||||
|
{ 'ticket.state_id' => { 'operator' => 'is', 'value' => '9999' } }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'performs no changes' do
|
||||||
|
expect { job.run }
|
||||||
|
.not_to change { matching_ticket.reload.state }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'use case: deleting tickets based on tag' do
|
||||||
|
let(:condition) { { 'ticket.tags' => { 'operator' => 'contains one', 'value' => 'spam' } } }
|
||||||
|
let(:perform) { { 'ticket.action' => { 'value' => 'delete' } } }
|
||||||
|
let!(:matching_ticket) { create(:ticket).tap { |t| t.tag_add('spam', 1) } }
|
||||||
|
let!(:nonmatching_ticket) { create(:ticket) }
|
||||||
|
|
||||||
|
it 'deletes tickets matching the specified tags' do
|
||||||
|
job.run
|
||||||
|
|
||||||
|
expect { matching_ticket.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
expect { nonmatching_ticket.reload }.not_to raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'use case: deleting tickets based on group' do
|
||||||
|
let(:condition) { { 'ticket.group_id' => { 'operator' => 'is', 'value' => matching_ticket.group.id } } }
|
||||||
|
let(:perform) { { 'ticket.action' => { 'value' => 'delete' } } }
|
||||||
|
let!(:matching_ticket) { create(:ticket) }
|
||||||
|
let!(:nonmatching_ticket) { create(:ticket) }
|
||||||
|
|
||||||
|
it 'deletes tickets matching the specified groups' do
|
||||||
|
job.run
|
||||||
|
|
||||||
|
expect { matching_ticket.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
expect { nonmatching_ticket.reload }.not_to raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and not due yet' do
|
||||||
|
before { allow(job).to receive(:in_timeplan?).and_return(false) }
|
||||||
|
|
||||||
|
it 'does not perform changes on matching tickets' do
|
||||||
|
expect { job.run }.not_to change { matching_ticket.reload.state }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not update #last_run_at' do
|
||||||
|
expect { job.run }.to not_change { job.reload.last_run_at }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates #next_run_at' do
|
||||||
|
travel_to(Time.current.last_week) # force new value for #next_run_at
|
||||||
|
|
||||||
|
expect { job.run }.to change { job.reload.next_run_at }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'but "force" flag is given' do
|
||||||
|
it 'performs changes on matching tickets' do
|
||||||
|
expect { job.run(true) }
|
||||||
|
.to change { matching_ticket.reload.state }
|
||||||
|
.and not_change { nonmatching_ticket.reload.state }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates #last_run_at' do
|
||||||
|
expect { job.run(true) }.to change { job.reload.last_run_at }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates #next_run_at' do
|
||||||
|
travel_to(Time.current.last_week) # force new value for #next_run_at
|
||||||
|
|
||||||
|
expect { job.run }.to change { job.reload.next_run_at }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#executable?' do
|
||||||
|
context 'for an inactive Job' do
|
||||||
|
subject(:job) { create(:job, active: false) }
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(job.executable?).to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for an active job' do
|
||||||
|
context 'updated in the last minute' do
|
||||||
|
subject(:job) do
|
||||||
|
create(:job, active: true,
|
||||||
|
updated_at: 59.seconds.ago)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(job.executable?).to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'updated over a minute ago' do
|
||||||
|
context 'that has never run before' do
|
||||||
|
subject(:job) do
|
||||||
|
create(:job, active: true,
|
||||||
|
updated_at: 60.seconds.ago)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
expect(job.executable?).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'that was started in the last 10 minutes' do
|
||||||
|
subject(:job) do
|
||||||
|
create(:job, active: true,
|
||||||
|
updated_at: 60.seconds.ago,
|
||||||
|
last_run_at: 9.minutes.ago)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(job.executable?).to be(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
context '(or, given an argument, up to 10 minutes before that)' do
|
||||||
|
subject(:job) do
|
||||||
|
create(:job, active: true,
|
||||||
|
updated_at: 60.seconds.ago,
|
||||||
|
last_run_at: 9.minutes.before(Time.current.yesterday))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(job.executable?(Time.current.yesterday)).to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'that was started over 10 minutes ago' do
|
||||||
|
subject(:job) do
|
||||||
|
create(:job, active: true,
|
||||||
|
updated_at: 60.seconds.ago,
|
||||||
|
last_run_at: 10.minutes.ago)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
expect(job.executable?).to be(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
context '(or, given an argument, over 10 minutes before that)' do
|
||||||
|
subject(:job) do
|
||||||
|
create(:job, active: true,
|
||||||
|
updated_at: 60.seconds.ago,
|
||||||
|
last_run_at: 10.minutes.before(Time.current.yesterday))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
expect(job.executable?(Time.current.yesterday)).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'but is still running, up to 24 hours later' do
|
||||||
|
subject(:job) do
|
||||||
|
create(:job, active: true,
|
||||||
|
updated_at: 60.seconds.ago,
|
||||||
|
running: true,
|
||||||
|
last_run_at: 23.hours.ago)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(job.executable?).to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'but is still running, over 24 hours later' do
|
||||||
|
subject(:job) do
|
||||||
|
create(:job, active: true,
|
||||||
|
updated_at: 60.seconds.ago,
|
||||||
|
running: true,
|
||||||
|
last_run_at: 24.hours.ago)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
expect(job.executable?).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#in_timeplan?' do
|
||||||
|
subject(:job) { create(:job, :never_on) }
|
||||||
|
|
||||||
|
context 'when the current day, hour, and minute all match true values in #timeplan' do
|
||||||
|
context 'for Symbol/Integer keys' do
|
||||||
|
before do
|
||||||
|
job.update(
|
||||||
|
timeplan: {
|
||||||
|
days: job.timeplan[:days]
|
||||||
|
.transform_keys(&:to_sym)
|
||||||
|
.merge(Time.current.strftime('%a').to_sym => true),
|
||||||
|
hours: job.timeplan[:hours]
|
||||||
|
.transform_keys(&:to_i)
|
||||||
|
.merge(Time.current.hour => true),
|
||||||
|
minutes: job.timeplan[:minutes]
|
||||||
|
.transform_keys(&:to_i)
|
||||||
|
.merge(Time.current.min.floor(-1) => true),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
expect(job.in_timeplan?).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for String keys' do
|
||||||
|
before do
|
||||||
|
job.update(
|
||||||
|
timeplan: {
|
||||||
|
days: job.timeplan[:days]
|
||||||
|
.transform_keys(&:to_s)
|
||||||
|
.merge(Time.current.strftime('%a') => true),
|
||||||
|
hours: job.timeplan[:hours]
|
||||||
|
.transform_keys(&:to_s)
|
||||||
|
.merge(Time.current.hour.to_s => true),
|
||||||
|
minutes: job.timeplan[:minutes]
|
||||||
|
.transform_keys(&:to_s)
|
||||||
|
.merge(Time.current.min.floor(-1).to_s => true),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
expect(job.in_timeplan?).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the current day does not match a true value in #timeplan' do
|
||||||
|
context 'for Symbol/Integer keys' do
|
||||||
|
before do
|
||||||
|
job.update(
|
||||||
|
timeplan: {
|
||||||
|
days: job.timeplan[:days]
|
||||||
|
.transform_keys(&:to_sym)
|
||||||
|
.transform_values { true }
|
||||||
|
.merge(Time.current.strftime('%a').to_sym => false),
|
||||||
|
hours: job.timeplan[:hours]
|
||||||
|
.transform_keys(&:to_i)
|
||||||
|
.merge(Time.current.hour => true),
|
||||||
|
minutes: job.timeplan[:minutes]
|
||||||
|
.transform_keys(&:to_i)
|
||||||
|
.merge(Time.current.min.floor(-1) => true),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(job.in_timeplan?).to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for String keys' do
|
||||||
|
before do
|
||||||
|
job.update(
|
||||||
|
timeplan: {
|
||||||
|
days: job.timeplan[:days]
|
||||||
|
.transform_keys(&:to_s)
|
||||||
|
.transform_values { true }
|
||||||
|
.merge(Time.current.strftime('%a') => false),
|
||||||
|
hours: job.timeplan[:hours]
|
||||||
|
.transform_keys(&:to_s)
|
||||||
|
.merge(Time.current.hour.to_s => true),
|
||||||
|
minutes: job.timeplan[:minutes]
|
||||||
|
.transform_keys(&:to_s)
|
||||||
|
.merge(Time.current.min.floor(-1).to_s => true),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(job.in_timeplan?).to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the current hour does not match a true value in #timeplan' do
|
||||||
|
context 'for Symbol/Integer keys' do
|
||||||
|
before do
|
||||||
|
job.update(
|
||||||
|
timeplan: {
|
||||||
|
days: job.timeplan[:days]
|
||||||
|
.transform_keys(&:to_sym)
|
||||||
|
.merge(Time.current.strftime('%a').to_sym => true),
|
||||||
|
hours: job.timeplan[:hours]
|
||||||
|
.transform_keys(&:to_i)
|
||||||
|
.transform_values { true }
|
||||||
|
.merge(Time.current.hour => false),
|
||||||
|
minutes: job.timeplan[:minutes]
|
||||||
|
.transform_keys(&:to_i)
|
||||||
|
.merge(Time.current.min.floor(-1) => true),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(job.in_timeplan?).to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for String keys' do
|
||||||
|
before do
|
||||||
|
job.update(
|
||||||
|
timeplan: {
|
||||||
|
days: job.timeplan[:days]
|
||||||
|
.transform_keys(&:to_s)
|
||||||
|
.merge(Time.current.strftime('%a') => true),
|
||||||
|
hours: job.timeplan[:hours]
|
||||||
|
.transform_keys(&:to_s)
|
||||||
|
.transform_values { true }
|
||||||
|
.merge(Time.current.hour.to_s => false),
|
||||||
|
minutes: job.timeplan[:minutes]
|
||||||
|
.transform_keys(&:to_s)
|
||||||
|
.merge(Time.current.min.floor(-1).to_s => true),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(job.in_timeplan?).to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the current minute does not match a true value in #timeplan' do
|
||||||
|
context 'for Symbol/Integer keys' do
|
||||||
|
before do
|
||||||
|
job.update(
|
||||||
|
timeplan: {
|
||||||
|
days: job.timeplan[:days]
|
||||||
|
.transform_keys(&:to_sym)
|
||||||
|
.merge(Time.current.strftime('%a').to_sym => true),
|
||||||
|
hours: job.timeplan[:hours]
|
||||||
|
.transform_keys(&:to_i)
|
||||||
|
.merge(Time.current.hour => true),
|
||||||
|
minutes: job.timeplan[:minutes]
|
||||||
|
.transform_keys(&:to_i)
|
||||||
|
.transform_values { true }
|
||||||
|
.merge(Time.current.min.floor(-1) => false),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(job.in_timeplan?).to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for String keys' do
|
||||||
|
before do
|
||||||
|
job.update(
|
||||||
|
timeplan: {
|
||||||
|
days: job.timeplan[:days]
|
||||||
|
.transform_keys(&:to_s)
|
||||||
|
.merge(Time.current.strftime('%a') => true),
|
||||||
|
hours: job.timeplan[:hours]
|
||||||
|
.transform_keys(&:to_s)
|
||||||
|
.merge(Time.current.hour.to_s => true),
|
||||||
|
minutes: job.timeplan[:minutes]
|
||||||
|
.transform_keys(&:to_s)
|
||||||
|
.transform_values { true }
|
||||||
|
.merge(Time.current.min.floor(-1).to_s => false),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(job.in_timeplan?).to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Attributes:' do
|
||||||
|
describe '#next_run_at' do
|
||||||
|
subject(:job) { build(:job) }
|
||||||
|
|
||||||
|
it 'is set automatically on save (cannot be set manually)' do
|
||||||
|
job.next_run_at = 1.day.from_now
|
||||||
|
|
||||||
|
expect { job.save }.to change { job.next_run_at }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for an inactive Job' do
|
||||||
|
subject(:job) { build(:job, active: false) }
|
||||||
|
|
||||||
|
it 'is nil' do
|
||||||
|
expect { job.save }
|
||||||
|
.not_to change { job.next_run_at }.from(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for a never-on Job (all #timeplan values are false)' do
|
||||||
|
subject(:job) { build(:job, :never_on) }
|
||||||
|
|
||||||
|
it 'is nil' do
|
||||||
|
expect { job.save }
|
||||||
|
.not_to change { job.next_run_at }.from(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when #timeplan contains at least one true value for :day, :hour, and :minute' do
|
||||||
|
subject(:job) { build(:job, :never_on) }
|
||||||
|
let(:base_time) { Time.current.beginning_of_week }
|
||||||
|
|
||||||
|
# Tuesday & Thursday @ 12:00a, 12:30a, 6:00p, and 6:30p
|
||||||
|
before do
|
||||||
|
job.assign_attributes(
|
||||||
|
timeplan: {
|
||||||
|
days: job.timeplan[:days].merge(Tue: true, Thu: true),
|
||||||
|
hours: job.timeplan[:hours].merge(0 => true, 18 => true),
|
||||||
|
minutes: job.timeplan[:minutes].merge(0 => true, 30 => true),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:valid_timeslots) do
|
||||||
|
[
|
||||||
|
base_time + 1.day, # Tue 12:00a
|
||||||
|
base_time + 1.day + 30.minutes, # Tue 12:30a
|
||||||
|
base_time + 1.day + 18.hours, # Tue 6:00p
|
||||||
|
base_time + 1.day + 18.hours + 30.minutes, # Tue 6:30p
|
||||||
|
base_time + 3.days, # Thu 12:00a
|
||||||
|
base_time + 3.days + 30.minutes, # Thu 12:30a
|
||||||
|
base_time + 3.days + 18.hours, # Thu 6:00p
|
||||||
|
base_time + 3.days + 18.hours + 30.minutes, # Thu 6:30p
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for a Job that has never been run before' do
|
||||||
|
context 'when record is saved at the start of the week' do
|
||||||
|
before { travel_to(base_time) }
|
||||||
|
|
||||||
|
it 'is set to the first valid timeslot of the week' do
|
||||||
|
expect { job.save }
|
||||||
|
.to change { job.next_run_at.to_i } # comparing times is hard;
|
||||||
|
.to(valid_timeslots.first.to_i) # integers are less precise
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when record is saved between two valid timeslots' do
|
||||||
|
before { travel_to(valid_timeslots.third - 1.second) }
|
||||||
|
|
||||||
|
it 'is set to the latter timeslot' do
|
||||||
|
expect { job.save }
|
||||||
|
.to change { job.next_run_at.to_i } # comparing times is hard;
|
||||||
|
.to(valid_timeslots.third.to_i) # integers are less precise
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when record is saved during a valid timeslot' do
|
||||||
|
before { travel_to(valid_timeslots.fifth + 9.minutes + 59.seconds) }
|
||||||
|
|
||||||
|
it 'is set to that timeslot' do
|
||||||
|
expect { job.save }
|
||||||
|
.to change { job.next_run_at.to_i } # comparing times is hard;
|
||||||
|
.to(valid_timeslots.fifth.to_i) # integers are less precise
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for a Job that been run before' do
|
||||||
|
context 'when record is saved in the same timeslot as #last_run_at' do
|
||||||
|
before do
|
||||||
|
job.assign_attributes(last_run_at: valid_timeslots.fourth + 5.minutes)
|
||||||
|
travel_to(valid_timeslots.fourth + 7.minutes)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is set to the next valid timeslot' do
|
||||||
|
expect { job.save }
|
||||||
|
.to change { job.next_run_at.to_i } # comparing times is hard;
|
||||||
|
.to(valid_timeslots.fifth.to_i) # integers are less precise
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
describe 'Validations:' do
|
||||||
|
describe '"article.note" key' do
|
||||||
|
let(:perform) do
|
||||||
|
{ 'article.note' => { 'subject' => 'foo', 'internal' => 'true', 'body' => '' } }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'fails if an empty "body" is given' do
|
||||||
|
expect { create(:job, perform: perform) }.to raise_error(Exceptions::UnprocessableEntity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '"notification.email" key' do
|
||||||
|
let(:perform) do
|
||||||
|
{ 'notification.email' => { 'body' => 'foo', 'recipient' => '', 'subject' => 'bar' } }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'fails if an empty "recipient" is given' do
|
||||||
|
expect { create(:job, perform: perform) }.to raise_error(Exceptions::UnprocessableEntity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '"notification.sms" key' do
|
||||||
|
let(:perform) do
|
||||||
|
{ 'notification.sms' => { 'body' => 'foo', 'recipient' => '' } }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'fails if an empty "recipient" is given' do
|
||||||
|
expect { create(:job, perform: perform) }.to raise_error(Exceptions::UnprocessableEntity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue