# encoding: utf-8 # frozen_string_literal: true require_relative 'helper' require 'sidekiq/web' require 'rack/test' class TestWeb < Sidekiq::Test describe 'sidekiq web' do include Rack::Test::Methods def app Sidekiq::Web end def job_params(job, score) "#{score}-#{job['jid']}" end before do Sidekiq.redis {|c| c.flushdb } end class WebWorker include Sidekiq::Worker def perform(a, b) a + b end end it 'can configure via set() syntax' do app.set(:session_secret, "foo") assert_equal "foo", app.session_secret end it 'can show text with any locales' do rackenv = {'HTTP_ACCEPT_LANGUAGE' => 'ru,en'} get '/', {}, rackenv assert_match(/Панель управления/, last_response.body) rackenv = {'HTTP_ACCEPT_LANGUAGE' => 'es,en'} get '/', {}, rackenv assert_match(/Panel de Control/, last_response.body) rackenv = {'HTTP_ACCEPT_LANGUAGE' => 'en-us'} get '/', {}, rackenv assert_match(/Dashboard/, last_response.body) rackenv = {'HTTP_ACCEPT_LANGUAGE' => 'zh-cn'} get '/', {}, rackenv assert_match(/信息板/, last_response.body) rackenv = {'HTTP_ACCEPT_LANGUAGE' => 'zh-tw'} get '/', {}, rackenv assert_match(/資訊主頁/, last_response.body) rackenv = {'HTTP_ACCEPT_LANGUAGE' => 'nb'} get '/', {}, rackenv assert_match(/Oversikt/, last_response.body) end describe 'busy' do it 'can display workers' do Sidekiq.redis do |conn| conn.incr('busy') conn.sadd('processes', 'foo:1234') conn.hmset('foo:1234', 'info', Sidekiq.dump_json('hostname' => 'foo', 'started_at' => Time.now.to_f, "queues" => []), 'at', Time.now.to_f, 'busy', 4) identity = 'foo:1234:workers' hash = {:queue => 'critical', :payload => { 'class' => WebWorker.name, 'args' => [1,'abc'] }, :run_at => Time.now.to_i } conn.hmset(identity, 1001, Sidekiq.dump_json(hash)) end assert_equal ['1001'], Sidekiq::Workers.new.map { |pid, tid, data| tid } get '/busy' assert_equal 200, last_response.status assert_match(/status-active/, last_response.body) assert_match(/critical/, last_response.body) assert_match(/WebWorker/, last_response.body) end it 'can quiet a process' do identity = 'identity' signals_key = "#{identity}-signals" assert_nil Sidekiq.redis { |c| c.lpop signals_key } post '/busy', 'quiet' => '1', 'identity' => identity assert_equal 302, last_response.status assert_equal 'TSTP', Sidekiq.redis { |c| c.lpop signals_key } end it 'can stop a process' do identity = 'identity' signals_key = "#{identity}-signals" assert_nil Sidekiq.redis { |c| c.lpop signals_key } post '/busy', 'stop' => '1', 'identity' => identity assert_equal 302, last_response.status assert_equal 'TERM', Sidekiq.redis { |c| c.lpop signals_key } end end it 'can display queues' do assert Sidekiq::Client.push('queue' => :foo, 'class' => WebWorker, 'args' => [1, 3]) get '/queues' assert_equal 200, last_response.status assert_match(/foo/, last_response.body) refute_match(/HardWorker/, last_response.body) end it 'handles queue view' do get '/queues/default' assert_equal 200, last_response.status end it 'can delete a queue' do Sidekiq.redis do |conn| conn.rpush('queue:foo', '{\"args\":[]}') conn.sadd('queues', 'foo') end get '/queues/foo' assert_equal 200, last_response.status post '/queues/foo' assert_equal 302, last_response.status Sidekiq.redis do |conn| refute conn.smembers('queues').include?('foo') refute conn.exists('queue:foo') end end it 'can delete a job' do Sidekiq.redis do |conn| conn.rpush('queue:foo', "{\"args\":[]}") conn.rpush('queue:foo', "{\"foo\":\"bar\",\"args\":[]}") conn.rpush('queue:foo', "{\"foo2\":\"bar2\",\"args\":[]}") end get '/queues/foo' assert_equal 200, last_response.status post '/queues/foo/delete', key_val: "{\"foo\":\"bar\"}" assert_equal 302, last_response.status Sidekiq.redis do |conn| refute conn.lrange('queue:foo', 0, -1).include?("{\"foo\":\"bar\"}") end end it 'can display retries' do get '/retries' assert_equal 200, last_response.status assert_match(/found/, last_response.body) refute_match(/HardWorker/, last_response.body) add_retry get '/retries' assert_equal 200, last_response.status refute_match(/found/, last_response.body) assert_match(/HardWorker/, last_response.body) end it 'can display a single retry' do params = add_retry get '/retries/0-shouldntexist' assert_equal 302, last_response.status get "/retries/#{job_params(*params)}" assert_equal 200, last_response.status assert_match(/HardWorker/, last_response.body) end it 'handles missing retry' do get "/retries/0-shouldntexist" assert_equal 302, last_response.status end it 'can delete a single retry' do params = add_retry post "/retries/#{job_params(*params)}", 'delete' => 'Delete' assert_equal 302, last_response.status assert_equal 'http://example.org/retries', last_response.header['Location'] get "/retries" assert_equal 200, last_response.status refute_match(/#{params.first['args'][2]}/, last_response.body) end it 'can delete all retries' do 3.times { add_retry } post "/retries/all/delete", 'delete' => 'Delete' assert_equal 0, Sidekiq::RetrySet.new.size assert_equal 302, last_response.status assert_equal 'http://example.org/retries', last_response.header['Location'] end it 'can retry a single retry now' do params = add_retry post "/retries/#{job_params(*params)}", 'retry' => 'Retry' assert_equal 302, last_response.status assert_equal 'http://example.org/retries', last_response.header['Location'] get '/queues/default' assert_equal 200, last_response.status assert_match(/#{params.first['args'][2]}/, last_response.body) end it 'can kill a single retry now' do params = add_retry post "/retries/#{job_params(*params)}", 'kill' => 'Kill' assert_equal 302, last_response.status assert_equal 'http://example.org/retries', last_response.header['Location'] get '/morgue' assert_equal 200, last_response.status assert_match(/#{params.first['args'][2]}/, last_response.body) end it 'can display scheduled' do get '/scheduled' assert_equal 200, last_response.status assert_match(/found/, last_response.body) refute_match(/HardWorker/, last_response.body) add_scheduled get '/scheduled' assert_equal 200, last_response.status refute_match(/found/, last_response.body) assert_match(/HardWorker/, last_response.body) end it 'can display a single scheduled job' do params = add_scheduled get '/scheduled/0-shouldntexist' assert_equal 302, last_response.status get "/scheduled/#{job_params(*params)}" assert_equal 200, last_response.status assert_match(/HardWorker/, last_response.body) end it 'handles missing scheduled job' do get "/scheduled/0-shouldntexist" assert_equal 302, last_response.status end it 'can add to queue a single scheduled job' do params = add_scheduled post "/scheduled/#{job_params(*params)}", 'add_to_queue' => true assert_equal 302, last_response.status assert_equal 'http://example.org/scheduled', last_response.header['Location'] get '/queues/default' assert_equal 200, last_response.status assert_match(/#{params.first['args'][2]}/, last_response.body) end it 'can delete a single scheduled job' do params = add_scheduled post "/scheduled/#{job_params(*params)}", 'delete' => 'Delete' assert_equal 302, last_response.status assert_equal 'http://example.org/scheduled', last_response.header['Location'] get "/scheduled" assert_equal 200, last_response.status refute_match(/#{params.first['args'][2]}/, last_response.body) end it 'can delete scheduled' do params = add_scheduled Sidekiq.redis do |conn| assert_equal 1, conn.zcard('schedule') post '/scheduled', 'key' => [job_params(*params)], 'delete' => 'Delete' assert_equal 302, last_response.status assert_equal 'http://example.org/scheduled', last_response.header['Location'] assert_equal 0, conn.zcard('schedule') end end it "can move scheduled to default queue" do q = Sidekiq::Queue.new params = add_scheduled Sidekiq.redis do |conn| assert_equal 1, conn.zcard('schedule') assert_equal 0, q.size post '/scheduled', 'key' => [job_params(*params)], 'add_to_queue' => 'AddToQueue' assert_equal 302, last_response.status assert_equal 'http://example.org/scheduled', last_response.header['Location'] assert_equal 0, conn.zcard('schedule') assert_equal 1, q.size get '/queues/default' assert_equal 200, last_response.status assert_match(/#{params[0]['args'][2]}/, last_response.body) end end it 'can retry all retries' do msg = add_retry.first add_retry post "/retries/all/retry", 'retry' => 'Retry' assert_equal 302, last_response.status assert_equal 'http://example.org/retries', last_response.header['Location'] assert_equal 2, Sidekiq::Queue.new("default").size get '/queues/default' assert_equal 200, last_response.status assert_match(/#{msg['args'][2]}/, last_response.body) end it 'calls updatePage() once when polling' do get '/busy?poll=true' assert_equal 200, last_response.status assert_equal 1, last_response.body.scan('data-poll-path="/busy').count end it 'escape job args and error messages' do # on /retries page params = add_xss_retry get '/retries' assert_equal 200, last_response.status assert_match(/FailWorker/, last_response.body) assert last_response.body.include?( "fail message: <a>hello</a>" ) assert !last_response.body.include?( "fail message: hello" ) assert last_response.body.include?( "args\">"<a>hello</a>"<" ) assert !last_response.body.include?( "args\">hello<" ) # on /workers page Sidekiq.redis do |conn| pro = 'foo:1234' conn.sadd('processes', pro) conn.hmset(pro, 'info', Sidekiq.dump_json('started_at' => Time.now.to_f, 'labels' => ['frumduz'], 'queues' =>[]), 'busy', 1, 'beat', Time.now.to_f) identity = "#{pro}:workers" hash = {:queue => 'critical', :payload => { 'class' => "FailWorker", 'args' => ["hello"] }, :run_at => Time.now.to_i } conn.hmset(identity, 100001, Sidekiq.dump_json(hash)) conn.incr('busy') end get '/busy' assert_equal 200, last_response.status assert_match(/FailWorker/, last_response.body) assert_match(/frumduz/, last_response.body) assert last_response.body.include?( "<a>hello</a>" ) assert !last_response.body.include?( "hello" ) # on /queues page params = add_xss_retry # sorry, don't know how to easily make this show up on queues page otherwise. post "/retries/#{job_params(*params)}", 'retry' => 'Retry' assert_equal 302, last_response.status get '/queues/foo' assert_equal 200, last_response.status assert last_response.body.include?( "<a>hello</a>" ) assert !last_response.body.include?( "hello" ) end it 'can show user defined tab' do begin Sidekiq::Web.tabs['Custom Tab'] = '/custom' get '/' assert_match 'Custom Tab', last_response.body ensure Sidekiq::Web.tabs.delete 'Custom Tab' end end it 'can display home' do get '/' assert_equal 200, last_response.status end describe 'custom locales' do before do Sidekiq::Web.settings.locales << File.join(File.dirname(__FILE__), "fixtures") Sidekiq::Web.tabs['Custom Tab'] = '/custom' Sidekiq::WebApplication.get('/custom') do clear_caches # ugly hack since I can't figure out how to access WebHelpers outside of this context t('translated_text') end end after do Sidekiq::Web.tabs.delete 'Custom Tab' Sidekiq::Web.settings.locales.pop end it 'can show user defined tab with custom locales' do get '/custom' assert_match(/Changed text/, last_response.body) end end describe 'dashboard/stats' do it 'redirects to stats' do get '/dashboard/stats' assert_equal 302, last_response.status assert_equal 'http://example.org/stats', last_response.header['Location'] end end describe 'stats' do include Sidekiq::Util before do Sidekiq.redis do |conn| conn.set("stat:processed", 5) conn.set("stat:failed", 2) conn.sadd("queues", "default") end 2.times { add_retry } 3.times { add_scheduled } 4.times { add_worker } end it 'works' do get '/stats' @response = Sidekiq.load_json(last_response.body) assert_equal 200, last_response.status assert_includes @response.keys, "sidekiq" assert_equal 5, @response["sidekiq"]["processed"] assert_equal 2, @response["sidekiq"]["failed"] assert_equal 4, @response["sidekiq"]["busy"] assert_equal 1, @response["sidekiq"]["processes"] assert_equal 2, @response["sidekiq"]["retries"] assert_equal 3, @response["sidekiq"]["scheduled"] assert_equal 0, @response["sidekiq"]["default_latency"] assert_includes @response.keys, "redis" assert_includes @response["redis"].keys, "redis_version" assert_includes @response["redis"].keys, "uptime_in_days" assert_includes @response["redis"].keys, "connected_clients" assert_includes @response["redis"].keys, "used_memory_human" assert_includes @response["redis"].keys, "used_memory_peak_human" assert_includes @response.keys, "server_utc_time" end end describe 'bad JSON' do it 'displays without error' do s = Sidekiq::DeadSet.new (_, score) = kill_bad assert_equal 1, s.size get '/morgue' assert_equal 200, last_response.status assert_match(/#{score.to_i}/, last_response.body) assert_match("something bad", last_response.body) assert_equal 1, s.size post "/morgue/#{score}-", 'delete' => 'Delete' assert_equal 302, last_response.status assert_equal 0, s.size end end describe 'stats/queues' do include Sidekiq::Util before do Sidekiq.redis do |conn| conn.set("stat:processed", 5) conn.set("stat:failed", 2) conn.sadd("queues", "default") conn.sadd("queues", "queue2") end 2.times { add_retry } 3.times { add_scheduled } 4.times { add_worker } get '/stats/queues' @response = Sidekiq.load_json(last_response.body) end it 'reports the queue depth' do assert_equal 0, @response["default"] assert_equal 0, @response["queue2"] end end describe 'dead jobs' do it 'shows empty index' do get 'morgue' assert_equal 200, last_response.status end it 'shows index with jobs' do (_, score) = add_dead get 'morgue' assert_equal 200, last_response.status assert_match(/#{score}/, last_response.body) end it 'can delete all dead' do 3.times { add_dead } assert_equal 3, Sidekiq::DeadSet.new.size post "/morgue/all/delete", 'delete' => 'Delete' assert_equal 0, Sidekiq::DeadSet.new.size assert_equal 302, last_response.status assert_equal 'http://example.org/morgue', last_response.header['Location'] end it 'can display a dead job' do params = add_dead get "/morgue/#{job_params(*params)}" assert_equal 200, last_response.status end it 'can retry a dead job' do params = add_dead post "/morgue/#{job_params(*params)}", 'retry' => 'Retry' assert_equal 302, last_response.status assert_equal 'http://example.org/morgue', last_response.header['Location'] get '/queues/foo' assert_equal 200, last_response.status assert_match(/#{params.first['args'][2]}/, last_response.body) end it 'handles bad query input' do get '/queues/foo?page=B 'HardWorker', 'args' => ['bob', 1, Time.now.to_f], 'jid' => SecureRandom.hex(12) } Sidekiq.redis do |conn| conn.zadd('schedule', score, Sidekiq.dump_json(msg)) end [msg, score] end def add_retry msg = { 'class' => 'HardWorker', 'args' => ['bob', 1, Time.now.to_f], 'queue' => 'default', 'error_message' => 'Some fake message', 'error_class' => 'RuntimeError', 'retry_count' => 0, 'failed_at' => Time.now.to_f, 'jid' => SecureRandom.hex(12) } score = Time.now.to_f Sidekiq.redis do |conn| conn.zadd('retry', score, Sidekiq.dump_json(msg)) end [msg, score] end def add_dead msg = { 'class' => 'HardWorker', 'args' => ['bob', 1, Time.now.to_f], 'queue' => 'foo', 'error_message' => 'Some fake message', 'error_class' => 'RuntimeError', 'retry_count' => 0, 'failed_at' => Time.now.utc, 'jid' => SecureRandom.hex(12) } score = Time.now.to_f Sidekiq.redis do |conn| conn.zadd('dead', score, Sidekiq.dump_json(msg)) end [msg, score] end def kill_bad job = "{ something bad }" score = Time.now.to_f Sidekiq.redis do |conn| conn.zadd('dead', score, job) end [job, score] end def add_xss_retry(job_id=SecureRandom.hex(12)) msg = { 'class' => 'FailWorker', 'args' => ['hello'], 'queue' => 'foo', 'error_message' => 'fail message: hello', 'error_class' => 'RuntimeError', 'retry_count' => 0, 'failed_at' => Time.now.to_f, 'jid' => SecureRandom.hex(12) } score = Time.now.to_f Sidekiq.redis do |conn| conn.zadd('retry', score, Sidekiq.dump_json(msg)) end [msg, score] end def add_worker key = "#{hostname}:#{$$}" msg = "{\"queue\":\"default\",\"payload\":{\"retry\":true,\"queue\":\"default\",\"timeout\":20,\"backtrace\":5,\"class\":\"HardWorker\",\"args\":[\"bob\",10,5],\"jid\":\"2b5ad2b016f5e063a1c62872\"},\"run_at\":1361208995}" Sidekiq.redis do |conn| conn.multi do conn.sadd("processes", key) conn.hmset(key, 'info', Sidekiq.dump_json('hostname' => 'foo', 'started_at' => Time.now.to_f, "queues" => []), 'at', Time.now.to_f, 'busy', 4) conn.hmset("#{key}:workers", Time.now.to_f, msg) end end end end describe 'sidekiq web with basic auth' do include Rack::Test::Methods def app app = Sidekiq::Web.new app.use(Rack::Auth::Basic) { |user, pass| user == "a" && pass == "b" } app end it 'requires basic authentication' do get '/' assert_equal 401, last_response.status refute_nil last_response.header["WWW-Authenticate"] end it 'authenticates successfuly' do basic_authorize 'a', 'b' get '/' assert_equal 200, last_response.status end end describe 'sidekiq web with custom session' do include Rack::Test::Methods def app app = Sidekiq::Web.new app.use Rack::Session::Cookie, secret: 'v3rys3cr31', host: 'nicehost.org' app end it 'requires basic authentication' do get '/' session_options = last_request.env['rack.session'].options assert_equal 'v3rys3cr31', session_options[:secret] assert_equal 'nicehost.org', session_options[:host] end end describe 'sidekiq web sessions options' do include Rack::Test::Methods describe 'using #disable' do def app app = Sidekiq::Web.new app.disable(:sessions) app end it "doesn't create sessions" do get '/' assert_nil last_request.env['rack.session'] end end describe 'using #set with false argument' do def app app = Sidekiq::Web.new app.set(:sessions, false) app end it "doesn't create sessions" do get '/' assert_nil last_request.env['rack.session'] end end describe 'using #set with an hash' do def app app = Sidekiq::Web.new app.set(:sessions, { domain: :all }) app end it "creates sessions" do get '/' refute_nil last_request.env['rack.session'] refute_empty last_request.env['rack.session'].options assert_equal :all, last_request.env['rack.session'].options[:domain] end end describe 'using #enable' do def app app = Sidekiq::Web.new app.enable(:sessions) app end it "creates sessions" do get '/' refute_nil last_request.env['rack.session'] refute_empty last_request.env['rack.session'].options refute_nil last_request.env['rack.session'].options[:secret] end end end end