sidekiq-undertaker 1.0.0.rc01

Sign up to get free protection for your applications and to get access to all the features.
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