ctt-background-jobs 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CTT-Background-Jobs.gemspec +37 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +25 -0
- data/Rakefile +29 -0
- data/lib/background_jobs/configuration.rb +27 -0
- data/lib/background_jobs/job.rb +17 -0
- data/lib/background_jobs/job_factory.rb +9 -0
- data/lib/background_jobs/job_id_generator.rb +15 -0
- data/lib/background_jobs/job_notifier.rb +31 -0
- data/lib/background_jobs/job_queue_factory.rb +36 -0
- data/lib/background_jobs/job_registry.rb +65 -0
- data/lib/background_jobs/queue_service.rb +17 -0
- data/lib/background_jobs/strategies/direct_call/job_queue.rb +11 -0
- data/lib/background_jobs/strategies/sidekiq/job_adapter.rb +21 -0
- data/lib/background_jobs/strategies/sidekiq/job_attributes_adapter.rb +141 -0
- data/lib/background_jobs/strategies/sidekiq/job_queue.rb +42 -0
- data/lib/background_jobs/strategies/sidekiq/setup.rb +31 -0
- data/lib/version.rb +3 -0
- data/spec/fast_helper.rb +11 -0
- data/spec/integration/sidekiq_spec.rb +34 -0
- data/spec/lib/background_jobs/configuration_spec.rb +13 -0
- data/spec/lib/background_jobs/job_factory_fast_spec.rb +24 -0
- data/spec/lib/background_jobs/job_fast_spec.rb +38 -0
- data/spec/lib/background_jobs/job_id_generator_fast_spec.rb +23 -0
- data/spec/lib/background_jobs/job_notififer_spec.rb +9 -0
- data/spec/lib/background_jobs/job_queue_factory_fast_spec.rb +58 -0
- data/spec/lib/background_jobs/queue_service_fast_spec.rb +36 -0
- data/spec/lib/background_jobs/strategies/direct_call/job_queue_spec.rb +27 -0
- data/spec/lib/background_jobs/strategies/sidekiq/job_adapter_spec.rb +30 -0
- data/spec/lib/background_jobs/strategies/sidekiq/job_attributes_adapter_spec.rb +129 -0
- data/spec/lib/background_jobs/strategies/sidekiq/job_queue_spec.rb +63 -0
- data/spec/lib/background_jobs/torquebox_jobs_queue_fast_spec.rb +31 -0
- data/spec/support/job_queue.rb +7 -0
- metadata +281 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
require 'background_jobs/job_registry'
|
3
|
+
require 'background_jobs/strategies/sidekiq/job_adapter'
|
4
|
+
require 'background_jobs/strategies/sidekiq/job_attributes_adapter'
|
5
|
+
|
6
|
+
module BackgroundJobs
|
7
|
+
module Sidekiq
|
8
|
+
class JobQueue
|
9
|
+
|
10
|
+
def enqueue(job_type, job_id, attributes, options = {})
|
11
|
+
priority = options[:priority] || 1
|
12
|
+
unique = options[:unique] || false
|
13
|
+
at = (Time.now + options[:in]).to_i if options[:in]
|
14
|
+
|
15
|
+
return if unique && is_scheduled?(job_type)
|
16
|
+
|
17
|
+
queue = BackgroundJobs.job_registry.get_type(job_type)
|
18
|
+
attributes.unshift job_id
|
19
|
+
attributes.unshift job_type
|
20
|
+
|
21
|
+
args = {
|
22
|
+
'queue' => queue.to_s, # queue by priority?
|
23
|
+
'class' => JobAdapter,
|
24
|
+
'args' => JobAttributesAdapter.new(attributes).encode
|
25
|
+
}
|
26
|
+
|
27
|
+
args['at'] = at if at
|
28
|
+
|
29
|
+
::Sidekiq::Client.push(args)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def is_scheduled?(job_name)
|
35
|
+
entries = ::Sidekiq::ScheduledSet.new.entries
|
36
|
+
job_names = entries.map(&:item).map {|item| item['args'].first }
|
37
|
+
|
38
|
+
job_names.include?(job_name.to_s)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
require 'background_jobs/job_registry'
|
3
|
+
|
4
|
+
module BackgroundJobs
|
5
|
+
module Sidekiq
|
6
|
+
def self.setup(options)
|
7
|
+
::Sidekiq.configure_server do |config|
|
8
|
+
config.redis = options[:redis] if options[:redis]
|
9
|
+
end
|
10
|
+
|
11
|
+
BackgroundJobs.job_registry.register_callback = method(:register_job_callback)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.register_job_callback(job_metadata)
|
15
|
+
options = job_metadata.options
|
16
|
+
|
17
|
+
BackgroundJobs::QueueService.enqueue(
|
18
|
+
job_metadata.name, [], :in => options[:schedule], :unique => true
|
19
|
+
) if options[:schedule]
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def self.is_scheduled?(job_name)
|
25
|
+
entries = ::Sidekiq::ScheduledSet.new.entries
|
26
|
+
job_names = entries.map(&:item).map {|item| item['args'].first }
|
27
|
+
|
28
|
+
job_names.include?(job_name.to_s)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/version.rb
ADDED
data/spec/fast_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# This file is copied to spec/ when you run 'rails generate rspec:install'
|
2
|
+
ENV["RAILS_ENV"] ||= 'test'
|
3
|
+
|
4
|
+
require 'rspec'
|
5
|
+
require 'shoulda'
|
6
|
+
require 'require_relative'
|
7
|
+
require 'timecop'
|
8
|
+
|
9
|
+
RSpec.configure do |config|
|
10
|
+
config.mock_with :mocha
|
11
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'fast_helper'
|
2
|
+
require 'background_jobs/configuration'
|
3
|
+
require 'background_jobs/queue_service'
|
4
|
+
require 'background_jobs/strategies/sidekiq/setup'
|
5
|
+
|
6
|
+
describe 'Sidekiq' do
|
7
|
+
|
8
|
+
let(:sidekiq_fake_job) { Class.new }
|
9
|
+
|
10
|
+
let(:clock) { stub('clock') }
|
11
|
+
let(:clock_schedule) { stub('clock_schedule') }
|
12
|
+
|
13
|
+
before do
|
14
|
+
BackgroundJobs.configure do |config|
|
15
|
+
config.strategy = 'sidekiq'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'with schedule' do
|
20
|
+
let(:scheduled_set) { stub('scheduled_set') }
|
21
|
+
|
22
|
+
it 'should schedule the job if schedule is set' do
|
23
|
+
BackgroundJobs::QueueService.expects(:enqueue).with(:fake_job, [], :in => 60*10, :unique => true)
|
24
|
+
|
25
|
+
BackgroundJobs.register_job :fake_job, :core, sidekiq_fake_job, :schedule => 60*10
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should not schedule the job if schedule not set' do
|
30
|
+
BackgroundJobs::QueueService.expects(:enqueue).never
|
31
|
+
|
32
|
+
BackgroundJobs.register_job :fake_job, :core, sidekiq_fake_job
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'fast_helper'
|
2
|
+
require 'background_jobs/configuration'
|
3
|
+
|
4
|
+
module BackgroundJobs
|
5
|
+
describe Configuration do
|
6
|
+
describe '#strategy' do
|
7
|
+
context 'with env configuration set' do
|
8
|
+
before { BackgroundJobs.configure {|c| c.strategy = 'foo' } }
|
9
|
+
it { BackgroundJobs.config.strategy.should == 'foo' }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'fast_helper'
|
2
|
+
require 'background_jobs/job_factory'
|
3
|
+
|
4
|
+
module BackgroundJobs
|
5
|
+
describe JobFactory do
|
6
|
+
before do
|
7
|
+
BackgroundJobs.register_job :foo, :bar, Imitate.mock('Foo')
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '.build' do
|
11
|
+
context 'for existing job' do
|
12
|
+
it 'should create job instance' do
|
13
|
+
JobFactory.build(:foo).should be_instance_of Foo
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'for not-existing job' do
|
18
|
+
it 'should throw an exception' do
|
19
|
+
expect { JobFactory.build(:not_existing_job) }.to raise_error("Unknown job 'not_existing_job'!")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'fast_helper'
|
2
|
+
require 'background_jobs/job'
|
3
|
+
|
4
|
+
describe BackgroundJobs::Job do
|
5
|
+
before do
|
6
|
+
Imitate.mock 'Airbrake' do
|
7
|
+
def self.notify(param); end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
let(:job) { BackgroundJobs::Job.new }
|
12
|
+
|
13
|
+
subject { job }
|
14
|
+
|
15
|
+
describe '#perform' do
|
16
|
+
it { expect { subject.perform }.to raise_error NotImplementedError }
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#id=' do
|
20
|
+
before { job.id = 123 }
|
21
|
+
|
22
|
+
it { subject.instance_variable_get(:@id).should eq 123 }
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#execute' do
|
26
|
+
it { expect { subject.execute }.to raise_error NotImplementedError }
|
27
|
+
end
|
28
|
+
|
29
|
+
describe 'error' do
|
30
|
+
let(:exception) { stub('exception') }
|
31
|
+
|
32
|
+
before do
|
33
|
+
Airbrake.expects(:notify).with(exception)
|
34
|
+
end
|
35
|
+
|
36
|
+
it { job.error('whatever', exception) }
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'fast_helper'
|
2
|
+
require 'background_jobs/job_id_generator'
|
3
|
+
require 'imitate'
|
4
|
+
|
5
|
+
describe BackgroundJobs::JobIdGenerator do
|
6
|
+
let(:generator) do
|
7
|
+
Imitate.mock('Generator') do
|
8
|
+
include BackgroundJobs::JobIdGenerator
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '.generate' do
|
13
|
+
let(:result1) { generator.generate_job_id }
|
14
|
+
let(:result2) { generator.generate_job_id }
|
15
|
+
let(:result3) { generator.generate_job_id }
|
16
|
+
|
17
|
+
subject { result1 }
|
18
|
+
|
19
|
+
it { should_not == result2 }
|
20
|
+
it { should_not == result3 }
|
21
|
+
it(:result2) { should_not == result3 }
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'fast_helper'
|
2
|
+
require 'background_jobs/job_queue_factory'
|
3
|
+
require 'background_jobs/strategies/sidekiq/job_queue'
|
4
|
+
require 'background_jobs/strategies/direct_call/job_queue'
|
5
|
+
|
6
|
+
module BackgroundJobs
|
7
|
+
describe JobQueueFactory do
|
8
|
+
describe '.build' do
|
9
|
+
subject { JobQueueFactory.build }
|
10
|
+
|
11
|
+
context 'sidekiq' do
|
12
|
+
before { configure_strategy('sidekiq', options) }
|
13
|
+
|
14
|
+
let(:sidekiq_job_queue) { stub('sidekiq_job_queue') }
|
15
|
+
|
16
|
+
let(:options) { stub('options') }
|
17
|
+
|
18
|
+
it {
|
19
|
+
Sidekiq::JobQueue.expects(:new).returns(sidekiq_job_queue)
|
20
|
+
JobQueueFactory.build.should == sidekiq_job_queue
|
21
|
+
}
|
22
|
+
|
23
|
+
after { clear_job_queue_strategy }
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'direct_call' do
|
27
|
+
before { configure_strategy('direct_call') }
|
28
|
+
|
29
|
+
it { should be_instance_of DirectCall::JobQueue }
|
30
|
+
|
31
|
+
after { clear_job_queue_strategy }
|
32
|
+
end
|
33
|
+
|
34
|
+
context 'with no strategy configured' do
|
35
|
+
before { clear_job_queue_strategy }
|
36
|
+
|
37
|
+
it "will blow up" do
|
38
|
+
expect {
|
39
|
+
JobQueueFactory.build
|
40
|
+
}.to raise_exception UnknownBackgroundJobsQueueStrategy
|
41
|
+
end
|
42
|
+
|
43
|
+
after { configure_strategy('delayed_job') }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def clear_job_queue_strategy
|
50
|
+
configure_strategy nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def configure_strategy(name, options = {})
|
54
|
+
BackgroundJobs.configure do |config|
|
55
|
+
config.strategy = name
|
56
|
+
config.strategy_options = options
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'fast_helper'
|
2
|
+
require 'background_jobs/queue_service'
|
3
|
+
|
4
|
+
module BackgroundJobs
|
5
|
+
describe QueueService do
|
6
|
+
let(:job_type) { :fix_all_bugs_while_we_sleep }
|
7
|
+
|
8
|
+
let(:priority) { 1 }
|
9
|
+
|
10
|
+
let(:data) { [1, 2, 3, 'foo', {1 => :uno}] }
|
11
|
+
|
12
|
+
let(:job_id) { 'job_id' }
|
13
|
+
|
14
|
+
let(:expected_queue_params) { data }
|
15
|
+
|
16
|
+
let(:strategy) { stub('strategy') }
|
17
|
+
|
18
|
+
describe '.enqueue' do
|
19
|
+
let(:delay) { stub('delay') }
|
20
|
+
|
21
|
+
before do
|
22
|
+
QueueService.stubs(:generate_job_id).returns(job_id)
|
23
|
+
|
24
|
+
JobQueueFactory.expects(:build).returns(strategy)
|
25
|
+
|
26
|
+
strategy.expects(:enqueue).with(
|
27
|
+
job_type, job_id, expected_queue_params, :priority => priority, :in => delay
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
subject { QueueService.enqueue(job_type, data, :priority => priority, :in => delay) }
|
32
|
+
|
33
|
+
it { should == job_id }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'fast_helper'
|
2
|
+
require 'background_jobs/strategies/direct_call/job_queue'
|
3
|
+
|
4
|
+
module BackgroundJobs
|
5
|
+
module DirectCall
|
6
|
+
describe JobQueue.new do
|
7
|
+
let(:type) { :foo }
|
8
|
+
|
9
|
+
let(:priority) { 1 }
|
10
|
+
|
11
|
+
let(:attrs) { [10..10, :foo => :bar] }
|
12
|
+
|
13
|
+
let(:job) { stub('job') }
|
14
|
+
|
15
|
+
let(:job_id) { 123 }
|
16
|
+
|
17
|
+
describe '.enqueue' do
|
18
|
+
it 'should create a job and execute it' do
|
19
|
+
JobFactory.expects(:build).with(type, attrs).returns(job)
|
20
|
+
job.expects(:execute)
|
21
|
+
|
22
|
+
JobQueue.new.enqueue(type, job_id, attrs)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'fast_helper'
|
2
|
+
require 'background_jobs/strategies/sidekiq/job_queue'
|
3
|
+
|
4
|
+
module BackgroundJobs
|
5
|
+
module Sidekiq
|
6
|
+
describe JobAdapter.new do
|
7
|
+
let(:job) { stub('job', :execute => true) }
|
8
|
+
|
9
|
+
describe '#peform' do
|
10
|
+
subject { JobAdapter.new.perform('foo', '1', :bar, :baz, 'range:[10,1000]') }
|
11
|
+
|
12
|
+
before { JobFactory.stubs(:build).returns(job) }
|
13
|
+
|
14
|
+
it 'should call job factory with appropriate job type and attributes' do
|
15
|
+
JobFactory.expects(:build).with(:foo, [:bar, :baz, 10..1000]).returns(job)
|
16
|
+
job.expects(:id=).with(1)
|
17
|
+
|
18
|
+
subject
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should call job perform with no attributes' do
|
22
|
+
job.expects(:execute)
|
23
|
+
job.expects(:id=).with(1)
|
24
|
+
|
25
|
+
subject
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'fast_helper'
|
2
|
+
require 'background_jobs/strategies/sidekiq/job_attributes_adapter'
|
3
|
+
|
4
|
+
module BackgroundJobs
|
5
|
+
module Sidekiq
|
6
|
+
describe JobAttributesAdapter do
|
7
|
+
context 'date' do
|
8
|
+
let(:attributes) { [Date.parse('1st Jan 2011')] }
|
9
|
+
let(:attributes_encoded) { ['date:2011-01-01'] }
|
10
|
+
|
11
|
+
it 'does decode to string with "date:"" prefix' do
|
12
|
+
JobAttributesAdapter.new(attributes_encoded).decode.should eq attributes
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'does encode to string with "date:"" prefix' do
|
16
|
+
JobAttributesAdapter.new(attributes).encode.should eq attributes_encoded
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'date range' do
|
21
|
+
let(:attributes) { [Date.parse('1st Jan 2011')..Date.parse('1st Feb 2011')] }
|
22
|
+
let(:attributes_encoded) { ['range:[date:2011-01-01,date:2011-02-01]'] }
|
23
|
+
|
24
|
+
it 'does decode to string with "range:" prefix and encode its boundary dates' do
|
25
|
+
JobAttributesAdapter.new(attributes_encoded).decode.should eq attributes
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'does encode to string with "range:" prefix and encode its boundary dates' do
|
29
|
+
JobAttributesAdapter.new(attributes).encode.should eq attributes_encoded
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'integers range' do
|
34
|
+
let(:attributes) { [1..10] }
|
35
|
+
let(:attributes_encoded) { ['range:[1,10]'] }
|
36
|
+
|
37
|
+
it 'does decode to string with prefix range:' do
|
38
|
+
JobAttributesAdapter.new(attributes_encoded).decode.should eq attributes
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'does encode to string with prefix range:' do
|
42
|
+
JobAttributesAdapter.new(attributes).encode.should eq attributes_encoded
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'integer' do
|
47
|
+
let(:attributes) { [12345] }
|
48
|
+
let(:attributes_encoded) { [12345] }
|
49
|
+
|
50
|
+
it 'is left decodehout any tranformations' do
|
51
|
+
JobAttributesAdapter.new(attributes_encoded).decode.should eq attributes
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'does encode to string with prefix range:' do
|
55
|
+
JobAttributesAdapter.new(attributes).encode.should eq attributes_encoded
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'hash' do
|
60
|
+
let(:attributes) { [{:foo => 'bar'}] }
|
61
|
+
let(:attributes_encoded) { [{'foo' => 'bar'}] }
|
62
|
+
|
63
|
+
it 'is left decodehout any tranformations' do
|
64
|
+
JobAttributesAdapter.new(attributes_encoded).decode.should eq attributes
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'does encode to string with prefix range:' do
|
68
|
+
JobAttributesAdapter.new(attributes).encode.should eq attributes_encoded
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context 'integer, hash and range' do
|
73
|
+
let(:attributes) { [12345, {:foo => 'bar'}, 10..1000] }
|
74
|
+
let(:attributes_encoded) { [12345, {'foo'=>"bar"}, "range:[10,1000]"] }
|
75
|
+
|
76
|
+
it 'does decode all together appropriately to their type' do
|
77
|
+
JobAttributesAdapter.new(attributes_encoded).decode.should eq attributes
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'does encode to string with prefix range:' do
|
81
|
+
JobAttributesAdapter.new(attributes).encode.should eq attributes_encoded
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'unknown group id bug' do
|
86
|
+
let(:attributes) {
|
87
|
+
[
|
88
|
+
"624ea403ad028e03045ba34c923bce0b2a812099bcd815bb1ddf2d92d8aaa560",
|
89
|
+
{
|
90
|
+
:group_id => 627,
|
91
|
+
:date_range => [Date.parse("2016-12-31")],
|
92
|
+
:week_schedule => {
|
93
|
+
:monday => 0,
|
94
|
+
:tuesday => 1,
|
95
|
+
:wednesday => 2,
|
96
|
+
:thursday => 3,
|
97
|
+
:friday => 2,
|
98
|
+
:saturday => 0,
|
99
|
+
:sunday => 0
|
100
|
+
}
|
101
|
+
}
|
102
|
+
]
|
103
|
+
}
|
104
|
+
|
105
|
+
let(:attributes_encoded) {
|
106
|
+
[
|
107
|
+
"624ea403ad028e03045ba34c923bce0b2a812099bcd815bb1ddf2d92d8aaa560",
|
108
|
+
{
|
109
|
+
"group_id"=>627,
|
110
|
+
"date_range"=>["date:2016-12-31"],
|
111
|
+
"week_schedule"=>{"monday"=>0, "tuesday"=>1, "wednesday"=>2, "thursday"=>3, "friday"=>2, "saturday"=>0, "sunday"=>0}
|
112
|
+
}
|
113
|
+
]
|
114
|
+
}
|
115
|
+
|
116
|
+
it 'does encode well' do
|
117
|
+
JobAttributesAdapter.new(attributes).encode.should eq attributes_encoded
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'does decode well' do
|
121
|
+
decoded = JobAttributesAdapter.new(attributes_encoded).decode
|
122
|
+
|
123
|
+
decoded.should eq attributes
|
124
|
+
decoded.last[:group_id].should == 627
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|