sidekiq-undertaker 1.0.0.rc01

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 (39) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +6 -0
  3. data/.gitignore +22 -0
  4. data/.rubocop.yml +5 -0
  5. data/.rubocop_todo.yml +23 -0
  6. data/.travis.yml +53 -0
  7. data/Demo_Filter.png +0 -0
  8. data/Demo_Job_Filter.png +0 -0
  9. data/Demo_Morgue_1_Job.png +0 -0
  10. data/Demo_Morgue_all.png +0 -0
  11. data/Gemfile +11 -0
  12. data/LICENSE.txt +21 -0
  13. data/README.md +95 -0
  14. data/Rakefile +34 -0
  15. data/lib/sidekiq/undertaker.rb +16 -0
  16. data/lib/sidekiq/undertaker/bucket.rb +29 -0
  17. data/lib/sidekiq/undertaker/dead_job.rb +64 -0
  18. data/lib/sidekiq/undertaker/job_distributor.rb +64 -0
  19. data/lib/sidekiq/undertaker/job_filter.rb +46 -0
  20. data/lib/sidekiq/undertaker/version.rb +7 -0
  21. data/lib/sidekiq/undertaker/web_extension.rb +37 -0
  22. data/lib/sidekiq/undertaker/web_extension/api_helpers.rb +120 -0
  23. data/sidekiq-undertaker.gemspec +55 -0
  24. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_filter/when_filter_page_is_called/behaves_like_a_page/the_displayed_page_is_correct.approved.txt +240 -0
  25. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_filter/when_job_classbucket_page_is_called/behaves_like_a_page/the_displayed_page_is_correct.approved.txt +241 -0
  26. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_filter/when_job_classbucket_page_is_polled/behaves_like_a_page/the_displayed_page_is_correct.approved.txt +241 -0
  27. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_morgue/when_job_classerrorbucket_is_called/with_all_failures_and_errors/behaves_like_a_page/the_displayed_page_is_correct.approved.txt +316 -0
  28. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_morgue/when_job_classerrorbucket_is_called/with_specific_job_class_and_a_specific_error/behaves_like_a_page/the_displayed_page_is_correct.approved.txt +274 -0
  29. data/spec/sidekiq/undertaker/bucket_spec.rb +26 -0
  30. data/spec/sidekiq/undertaker/dead_jobs_spec.rb +90 -0
  31. data/spec/sidekiq/undertaker/job_distributor_spec.rb +104 -0
  32. data/spec/sidekiq/undertaker/job_filter_spec.rb +105 -0
  33. data/spec/sidekiq/undertaker/web_extension_spec.rb +270 -0
  34. data/spec/spec_helper.rb +76 -0
  35. data/web/locales/en.yml +5 -0
  36. data/web/views/filter.erb +34 -0
  37. data/web/views/filter_job_class.erb +35 -0
  38. data/web/views/morgue.erb +75 -0
  39. metadata +378 -0
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ module Sidekiq
6
+ module Undertaker
7
+ describe Bucket do
8
+ describe ".bucket_names" do
9
+ let(:expected_bucket_names) { %w[1_hour 3_hours 1_day 3_days 1_week older] }
10
+
11
+ it { expect(described_class.bucket_names).to eq expected_bucket_names }
12
+ end
13
+
14
+ describe ".for_elapsed_time" do
15
+ let(:expectation) { %w[1_hour 3_hours 1_day 3_days 1_week older] }
16
+ let(:elapsed_times) { [1, 3601, 10801, 86401, 259201, 604801] }
17
+
18
+ it "maps elapsed_time to bucket name" do
19
+ elapsed_times.each_with_index do |elapsed_time, index|
20
+ expect(described_class.for_elapsed_time(elapsed_time)).to eq expectation[index]
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ module Sidekiq
6
+ module Undertaker
7
+ describe DeadJob do
8
+ let(:job) do
9
+ build_job(
10
+ "class" => "HardWorkTask",
11
+ "failed_at" => Time.now,
12
+ "error_class" => "NoMethodError",
13
+ "queue" => "SomeQueue"
14
+ )
15
+ end
16
+
17
+ describe "attributes" do
18
+ let(:expected_attributes) do
19
+ {
20
+ job_class: "HardWorkTask",
21
+ time_elapsed_since_failure: 9,
22
+ error_class: "NoMethodError",
23
+ bucket_name: "1_hour",
24
+ job: job
25
+ }
26
+ end
27
+
28
+ context "when all attributes are given" do
29
+ let(:args) { expected_attributes }
30
+
31
+ it { expect(described_class.new(args)).to have_attributes expected_attributes }
32
+ end
33
+
34
+ context "when some attributes are derived" do
35
+ let(:args) do
36
+ {
37
+ time_elapsed_since_failure: 9,
38
+ bucket_name: "1_hour",
39
+ job: job
40
+ }
41
+ end
42
+
43
+ it do
44
+ expect(
45
+ described_class.new(args)
46
+ ).to have_attributes expected_attributes
47
+ end
48
+ end
49
+ end
50
+
51
+ describe ".for_each" do
52
+ let(:expected_dead_job) do
53
+ DeadJob.new(
54
+ job_class: "HardWorkTask",
55
+ time_elapsed_since_failure: time_elapsed,
56
+ error_class: "NoMethodError",
57
+ bucket_name: "1_hour",
58
+ job: killed_job
59
+ )
60
+ end
61
+
62
+ let(:killed_job) do
63
+ Sidekiq::DeadSet.new.find_job(job.jid)
64
+ end
65
+
66
+ let(:time_elapsed) { Time.now.to_i - job.item["failed_at"].to_i }
67
+
68
+ before do
69
+ Timecop.freeze
70
+
71
+ kill_job(job)
72
+ end
73
+
74
+ after do
75
+ Timecop.return
76
+ end
77
+
78
+ it "returns job with appropriate metadata filled" do
79
+ cnt = 0
80
+ described_class.for_each do |dead_job|
81
+ # It returns only one dead_job
82
+ cnt += 1
83
+ expect(dead_job).to eql expected_dead_job
84
+ end
85
+ expect(cnt).to be 1
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ module Sidekiq
6
+ module Undertaker
7
+ describe JobDistributor do
8
+ let(:job1) do
9
+ instance_double(Sidekiq::Job, item: {
10
+ "class" => "A",
11
+ "failed_at" => 1,
12
+ "error_class" => "E1"
13
+ })
14
+ end
15
+ let(:job2) do
16
+ instance_double(Sidekiq::Job, item: {
17
+ "class" => "A",
18
+ "failed_at" => 1,
19
+ "error_class" => "E1"
20
+ })
21
+ end
22
+ let(:job3) do
23
+ instance_double(Sidekiq::Job, item: {
24
+ "class" => "B",
25
+ "failed_at" => 1,
26
+ "error_class" => "E1"
27
+ })
28
+ end
29
+ let(:job4) do
30
+ instance_double(Sidekiq::Job, item: {
31
+ "class" => "B",
32
+ "failed_at" => 1,
33
+ "error_class" => "E2"
34
+ })
35
+ end
36
+
37
+ let(:dead_job1) do
38
+ DeadJob.new(
39
+ job: job1, # 'A', 'E1'
40
+ time_elapsed_since_failure: 10,
41
+ bucket_name: "1_hour"
42
+ )
43
+ end
44
+
45
+ let(:dead_job2) do
46
+ DeadJob.new(
47
+ job: job2, # 'A', 'E1'
48
+ time_elapsed_since_failure: 10 + 60 * 60,
49
+ bucket_name: "3_hours"
50
+ )
51
+ end
52
+ let(:dead_job3) do
53
+ DeadJob.new(
54
+ job: job3, # 'B', 'E1'
55
+ time_elapsed_since_failure: 10 + 60 * 60,
56
+ bucket_name: "3_hours"
57
+ )
58
+ end
59
+ let(:dead_job4) do
60
+ DeadJob.new(
61
+ job: job4, # 'B', 'E2'
62
+ time_elapsed_since_failure: 10 + 60 * 60 * 24,
63
+ bucket_name: "1_day"
64
+ )
65
+ end
66
+
67
+ let(:dead_jobs) { [dead_job1, dead_job2, dead_job3, dead_job4] }
68
+
69
+ # rubocop:disable Metrics/LineLength
70
+ describe "#group_by_job_class" do
71
+ subject(:distribution) { described_class.new(dead_jobs).group_by_job_class }
72
+
73
+ let(:expected_distribution) do
74
+ [
75
+ ["all", { "1_hour" => 1, "3_hours" => 2, "1_day" => 1, "3_days" => 0, "1_week" => 0, "older" => 0, "total_dead" => 4 }],
76
+ ["B", { "1_hour" => 0, "3_hours" => 1, "1_day" => 1, "3_days" => 0, "1_week" => 0, "older" => 0, "total_dead" => 2 }],
77
+ ["A", { "1_hour" => 1, "3_hours" => 1, "1_day" => 0, "3_days" => 0, "1_week" => 0, "older" => 0, "total_dead" => 2 }]
78
+ ]
79
+ end
80
+
81
+ it "distributes the dead jobs into buckets and groups them by job_class" do
82
+ expect(distribution).to eq expected_distribution
83
+ end
84
+ end
85
+
86
+ describe "#group_by_error_class" do
87
+ subject(:distribution) { described_class.new(dead_jobs).group_by_error_class }
88
+
89
+ let(:expected_distribution) do
90
+ [
91
+ ["all", { "1_hour" => 1, "3_hours" => 2, "1_day" => 1, "3_days" => 0, "1_week" => 0, "older" => 0, "total_dead" => 4 }],
92
+ ["E1", { "1_hour" => 1, "3_hours" => 2, "1_day" => 0, "3_days" => 0, "1_week" => 0, "older" => 0, "total_dead" => 3 }],
93
+ ["E2", { "1_hour" => 0, "3_hours" => 0, "1_day" => 1, "3_days" => 0, "1_week" => 0, "older" => 0, "total_dead" => 1 }]
94
+ ]
95
+ end
96
+
97
+ it "distributes the dead jobs into buckets and groups them by error_class" do
98
+ expect(distribution).to eq expected_distribution
99
+ end
100
+ end
101
+ # rubocop:enable Metrics/LineLength
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ module Sidekiq
6
+ module Undertaker
7
+ describe JobFilter do
8
+ describe ".filter_dead_jobs" do
9
+ let(:job1) do
10
+ instance_double(Sidekiq::Job, item: {
11
+ "class" => "HardWorkTask",
12
+ "failed_at" => Time.now.to_i - 5 * 60,
13
+ "error_class" => "NoMethodError"
14
+ })
15
+ end
16
+
17
+ let(:job2) do
18
+ instance_double(Sidekiq::Job, item: {
19
+ "class" => "HardWorkTask",
20
+ "failed_at" => Time.now.to_i - 2 * 60 * 60,
21
+ "error_class" => "RandomError"
22
+ })
23
+ end
24
+
25
+ let(:job3) do
26
+ instance_double(Sidekiq::Job, item: {
27
+ "class" => "LazyWorkTask",
28
+ "failed_at" => Time.now.to_i - 2 * 60 * 60,
29
+ "error_class" => "NoMethodError"
30
+ })
31
+ end
32
+
33
+ before do
34
+ Timecop.freeze
35
+
36
+ dead_set = instance_double(Sidekiq::DeadSet)
37
+
38
+ allow(dead_set).to receive(:each)
39
+ .and_yield(job1)
40
+ .and_yield(job2)
41
+ .and_yield(job3)
42
+
43
+ allow(Sidekiq::DeadSet).to receive(:new).and_return(dead_set)
44
+ end
45
+
46
+ after { Timecop.return }
47
+
48
+ context "when the job_class filter is given" do
49
+ subject(:dead_jobs) { described_class.filter_dead_jobs("job_class" => "HardWorkTask") }
50
+
51
+ it "filters jobs based on job_class" do
52
+ dead_jobs.each do |dead_job|
53
+ expect(dead_job.job_class).to eq "HardWorkTask"
54
+ end
55
+ end
56
+
57
+ it { expect(dead_jobs.size).to eq 2 }
58
+ end
59
+
60
+ context "when the job_class and bucket_name filters are given" do
61
+ subject(:dead_jobs) do
62
+ described_class.filter_dead_jobs("job_class" => "HardWorkTask",
63
+ "bucket_name" => "3_hours")
64
+ end
65
+
66
+ it "filters based on multiple filter attributes" do
67
+ dead_jobs.each do |dead_job|
68
+ expect(dead_job.job_class).to eq "HardWorkTask"
69
+ expect(dead_job.bucket_name).to eq "3_hours"
70
+ end
71
+ end
72
+
73
+ it { expect(dead_jobs.size).to eq 1 }
74
+ end
75
+
76
+ context "when the error_class filter is given" do
77
+ subject(:dead_jobs) { described_class.filter_dead_jobs("error_class" => "NoMethodError") }
78
+
79
+ it "filters jobs based on error_class" do
80
+ dead_jobs.each do |dead_job|
81
+ expect(dead_job.error_class).to eq "NoMethodError"
82
+ end
83
+ end
84
+ it { expect(dead_jobs.size).to eq 2 }
85
+ end
86
+
87
+ context "when no filters are applied" do
88
+ subject(:dead_jobs) { described_class.filter_dead_jobs }
89
+
90
+ it { expect(dead_jobs.size).to eq 3 }
91
+ end
92
+
93
+ context "when all filters are nil" do
94
+ subject(:dead_jobs) do
95
+ described_class.filter_dead_jobs("job_class" => nil,
96
+ "bucket_name" => nil,
97
+ "error_class" => nil)
98
+ end
99
+
100
+ it { expect(dead_jobs.size).to eq 3 }
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,270 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ require "sidekiq/api"
6
+ require "sidekiq/web"
7
+ require "sinatra"
8
+ require "rack/test"
9
+
10
+ module Sidekiq
11
+ # rubocop:disable Metrics/ModuleLength
12
+ module Undertaker
13
+ describe WebExtension, type: :controller do
14
+ include Rack::Test::Methods
15
+
16
+ let(:app) { Sidekiq::Web }
17
+ let(:job_refs) { [] }
18
+
19
+ let(:jid1) { "4416aa76eb8cf03f56a49220" }
20
+ let(:jid2) { "34e79a46b1956d3a1180767b" }
21
+ let(:jid3) { "8d08674fce759ac75d1a6e75" }
22
+ let(:jid4) { "bfa4a272cdcac8bfac7b9f1a" }
23
+
24
+ let(:default_job_opts) do
25
+ {
26
+ "class" => "HardWorker",
27
+ "args" => ["asdf", 1234],
28
+ "queue" => "foo",
29
+ "error_message" => "Some fake message",
30
+ "error_class" => "RuntimeError",
31
+ "retry_count" => 0,
32
+ "failed_at" => Time.now.utc
33
+ }
34
+ end
35
+
36
+ # rubocop:disable RSpec/AnyInstance
37
+ before do
38
+ Timecop.freeze(Time.gm(2018, 12, 16, 20, 57))
39
+
40
+ job_refs.push add_dead("jid" => jid1)
41
+ job_refs.push add_dead("jid" => jid2)
42
+ job_refs.push add_dead("jid" => jid3, "error_class" => "NoMethodError")
43
+ job_refs.push add_dead("jid" => jid4, "class" => "HardWorker1", "error_class" => "NoMethodError")
44
+
45
+ allow_any_instance_of(Sidekiq::WebAction).to receive(:root_path).and_return("/sidekiq/")
46
+ end
47
+ # rubocop:enable RSpec/AnyInstance
48
+
49
+ after { Timecop.return }
50
+
51
+ def add_dead(opts = {})
52
+ opts = default_job_opts.merge!(opts)
53
+
54
+ job = build_job(opts)
55
+ killed_job = kill_job(job)
56
+
57
+ "#{killed_job.score}-#{job.jid}"
58
+ end
59
+
60
+ shared_examples "a page" do
61
+ it "the displayed page is correct" do
62
+ subject
63
+
64
+ expect(last_response.status).to eq 200
65
+ verify do
66
+ apply_custom_excludes(last_response.body)
67
+ end
68
+ end
69
+ end
70
+
71
+ describe "show filter" do
72
+ # /undertaker/filter
73
+ context "when filter page is called" do
74
+ subject { get "/undertaker/filter" }
75
+
76
+ it_behaves_like "a page"
77
+ end
78
+
79
+ # /undertaker/filter/:job_class/:bucket_name
80
+ context "when job-class/bucket page is called" do
81
+ subject { get "/undertaker/filter/HardWorker/1_hour" }
82
+
83
+ it_behaves_like "a page"
84
+ end
85
+
86
+ # /undertaker/filter/:job_class/:bucket_name?poll=true
87
+ context "when job-class/bucket page is polled" do
88
+ subject { get "/undertaker/filter/HardWorker/1_hour?poll=true" }
89
+
90
+ it_behaves_like "a page"
91
+ end
92
+ end
93
+
94
+ describe "show morgue" do
95
+ # /undertaker/morgue/:job_class/:error_class/:bucket_name
96
+ context "when job-class/error/bucket is called" do
97
+ context "with specific job-class and a specific error" do
98
+ subject { get "/undertaker/morgue/HardWorker/RuntimeError/1_hour" }
99
+
100
+ it_behaves_like "a page"
101
+ end
102
+
103
+ context "with all failures and errors" do
104
+ subject { get "/undertaker/morgue/all/all/total_dead" }
105
+
106
+ it_behaves_like "a page"
107
+ end
108
+ end
109
+ end
110
+
111
+ describe "delete" do
112
+ context "when job-class, error and bucket are given" do
113
+ subject { post "/undertaker/morgue/HardWorker/RuntimeError/1_hour/delete" }
114
+
115
+ let(:expected_redirect_url) { "http://example.org/undertaker/morgue/HardWorker/RuntimeError/1_hour" }
116
+
117
+ let(:params) { { "job_class" => "HardWorker", "error_class" => "RuntimeError", "bucket_name" => "1_hour" } }
118
+ let(:dead_jobs_set) { [dead_job1, dead_job2] }
119
+ let(:dead_job1) { Sidekiq::Undertaker::DeadJob.to_dead_job(Sidekiq::DeadSet.new.find_job(jid1)) }
120
+ let(:dead_job2) { Sidekiq::Undertaker::DeadJob.to_dead_job(Sidekiq::DeadSet.new.find_job(jid2)) }
121
+
122
+ before do
123
+ allow(Sidekiq::Undertaker::JobFilter).to receive(:filter_dead_jobs).with(params)
124
+ .and_return(dead_jobs_set)
125
+ end
126
+
127
+ it "deletes the dead jobs" do
128
+ expect(dead_job1.job).to receive(:delete).and_call_original
129
+ expect(dead_job2.job).to receive(:delete).and_call_original
130
+ subject
131
+ end
132
+
133
+ it "reduces the DeadSet" do
134
+ expect { subject }.to change { Sidekiq::DeadSet.new.size }.from(4).to(2)
135
+ end
136
+
137
+ it "redirects to /undertaker/morgue/HardWorker/RuntimeError/1_hour after the delete" do
138
+ subject
139
+ expect(last_response.status).to eq 302
140
+
141
+ # Redirect
142
+ follow_redirect!
143
+ expect(last_request.url).to eq expected_redirect_url
144
+ expect(last_response.status).to eq 200
145
+ end
146
+ end
147
+
148
+ context "when referer given" do
149
+ subject do
150
+ post("/undertaker/morgue",
151
+ "key[]=#{job_refs[0]}&delete=Delete",
152
+ "HTTP_REFERER" => "/undertaker/morgue/all/all/total_dead")
153
+ end
154
+
155
+ it "redirects back to referer after delete" do
156
+ subject
157
+ expect(last_response.status).to eq 302
158
+ expect(last_response.header["Location"]).to include "/undertaker/morgue/all/all/total_dead"
159
+ end
160
+ end
161
+
162
+ context "when /undertaker/morgue is called" do
163
+ context "when a key is given" do
164
+ subject { post "/undertaker/morgue", "key[]=#{job_refs[0]}&delete=Delete" }
165
+
166
+ it "deletes specific dead job now" do
167
+ expect { subject }.to change { Sidekiq::DeadSet.new.size }.from(4).to(3)
168
+ end
169
+ end
170
+
171
+ context "when a key is missing" do
172
+ subject { post "/undertaker/morgue" }
173
+
174
+ it "returns 400 Bad Request" do
175
+ subject
176
+ expect(last_response.status).to eq 400
177
+ end
178
+ end
179
+ end
180
+ end
181
+
182
+ describe "retry" do
183
+ context "when job class, error and bucket are given" do
184
+ subject { post "/undertaker/morgue/HardWorker/RuntimeError/1_hour/retry" }
185
+
186
+ let(:expected_redirect_url) { "http://example.org/undertaker/morgue/HardWorker/RuntimeError/1_hour" }
187
+
188
+ let(:params) { { "job_class" => "HardWorker", "error_class" => "RuntimeError", "bucket_name" => "1_hour" } }
189
+ let(:dead_jobs_set) { [dead_job1, dead_job2] }
190
+ let(:dead_job1) { Sidekiq::Undertaker::DeadJob.to_dead_job(Sidekiq::DeadSet.new.find_job(jid1)) }
191
+ let(:dead_job2) { Sidekiq::Undertaker::DeadJob.to_dead_job(Sidekiq::DeadSet.new.find_job(jid2)) }
192
+
193
+ before do
194
+ allow(Sidekiq::Undertaker::JobFilter).to receive(:filter_dead_jobs).with(params)
195
+ .and_return(dead_jobs_set)
196
+ end
197
+
198
+ it "retries the dead jobs" do
199
+ expect(dead_job1.job).to receive(:retry).and_call_original
200
+ expect(dead_job2.job).to receive(:retry).and_call_original
201
+ subject
202
+ end
203
+
204
+ it "reduces DeadSet" do
205
+ expect { subject }.to change { Sidekiq::DeadSet.new.size }.from(4).to(2)
206
+ end
207
+
208
+ it "redirects to /undertaker/morgue/HardWorker/RuntimeError/1_hour" do
209
+ subject
210
+ expect(last_response.status).to eq 302
211
+
212
+ # Redirect
213
+ follow_redirect!
214
+ expect(last_request.url).to eq expected_redirect_url
215
+ expect(last_response.status).to eq 200
216
+ end
217
+ end
218
+
219
+ context "when /undertaker/morgue is called" do
220
+ context "when a key is given" do
221
+ subject { post "/undertaker/morgue", "key[]=#{job_refs[0]}&retry=Retry+Now" }
222
+
223
+ it "reduces DeadSet" do
224
+ expect { subject }.to change { Sidekiq::DeadSet.new.size }.from(4).to(3)
225
+ end
226
+ end
227
+
228
+ context "when a key is missing" do
229
+ subject { post "/undertaker/morgue" }
230
+
231
+ it "returns 400 Bad Request" do
232
+ subject
233
+ expect(last_response.status).to eq 400
234
+ end
235
+ end
236
+ end
237
+ end
238
+
239
+ describe "specific jobs" do
240
+ let(:dead_job) { Sidekiq::DeadSet.new.find_job(jid1) }
241
+
242
+ before do
243
+ score, jid = job_refs[0].split("-")
244
+
245
+ dead_set = instance_double(Sidekiq::DeadSet)
246
+
247
+ allow(dead_set).to receive(:fetch)
248
+ .with(score.to_f, jid)
249
+ .and_return([dead_job])
250
+
251
+ allow(Sidekiq::DeadSet).to receive(:new).and_return(dead_set)
252
+ end
253
+
254
+ it "retries specific dead job now" do
255
+ expect(dead_job).to receive(:retry)
256
+ post "/undertaker/morgue", "key[]=#{job_refs[0]}&retry=Retry+Now"
257
+ end
258
+
259
+ it "redirects on specific retry post" do
260
+ post("/undertaker/morgue",
261
+ "key[]=#{job_refs[0]}&retry=Retry+Now",
262
+ "HTTP_REFERER" => "/undertaker/morgue/all/all/total_dead")
263
+ expect(last_response.status).to eq 302
264
+ expect(last_response.header["Location"]).to include("/undertaker/morgue/all/all/total_dead")
265
+ end
266
+ end
267
+ end
268
+ end
269
+ # rubocop:enable Metrics/ModuleLength
270
+ end