X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/7c12fc4d989dd2b4c47a174280a4f9526ecb0798..9be8a468534acaf324e9c18b831677f0ae067e60:/services/api/test/integration/websocket_test.rb diff --git a/services/api/test/integration/websocket_test.rb b/services/api/test/integration/websocket_test.rb index 666175d5a5..a9993b2fc3 100644 --- a/services/api/test/integration/websocket_test.rb +++ b/services/api/test/integration/websocket_test.rb @@ -1,9 +1,8 @@ require 'test_helper' -require 'websocket_runner' require 'oj' require 'database_cleaner' -DatabaseCleaner.strategy = :truncation +DatabaseCleaner.strategy = :deletion class WebsocketTest < ActionDispatch::IntegrationTest self.use_transactional_fixtures = false @@ -16,35 +15,92 @@ class WebsocketTest < ActionDispatch::IntegrationTest DatabaseCleaner.clean end - def ws_helper (token = nil, timeout = true) + def self.startup + s = TCPServer.new('0.0.0.0', 0) + @@port = s.addr[1] + s.close + @@pidfile = "tmp/pids/passenger.#{@@port}.pid" + DatabaseCleaner.start + Dir.chdir(Rails.root) do |apidir| + # Only passenger seems to be able to run the websockets server + # successfully. + _system('passenger', 'start', '-d', + "-p#{@@port}", + "--log-file", "/dev/stderr", + "--pid-file", @@pidfile) + timeout = Time.now.tv_sec + 10 + begin + sleep 0.2 + begin + server_pid = IO.read(@@pidfile).to_i + good_pid = (server_pid > 0) and (Process.kill(0, pid) rescue false) + rescue Errno::ENOENT + good_pid = false + end + end while (not good_pid) and (Time.now.tv_sec < timeout) + if not good_pid + raise RuntimeError, "could not find API server Rails pid" + end + STDERR.puts "Started websocket server on port #{@@port} with pid #{server_pid}" + end + end + + def self.shutdown + Dir.chdir(Rails.root) do + _system('passenger', 'stop', "-p#{@@port}", + "--pid-file", @@pidfile) + end + # DatabaseCleaner leaves the database empty. Prefer to leave it full. + dc = DatabaseController.new + dc.define_singleton_method :render do |*args| end + dc.reset + end + + def self._system(*cmd) + Bundler.with_clean_env do + env = { + 'ARVADOS_WEBSOCKETS' => 'ws-only', + 'RAILS_ENV' => 'test', + } + if not system(env, *cmd) + raise RuntimeError, "Command exited #{$?}: #{cmd.inspect}" + end + end + end + + def ws_helper(token: nil, timeout: 8) opened = false close_status = nil too_long = false - EM.run { + EM.run do if token - ws = Faye::WebSocket::Client.new("ws://localhost:3002/websocket?api_token=#{api_client_authorizations(token).api_token}") + ws = Faye::WebSocket::Client.new("ws://localhost:#{@@port}/websocket?api_token=#{api_client_authorizations(token).api_token}") else - ws = Faye::WebSocket::Client.new("ws://localhost:3002/websocket") + ws = Faye::WebSocket::Client.new("ws://localhost:#{@@port}/websocket") end ws.on :open do |event| opened = true if timeout - EM::Timer.new 3 do - too_long = true + EM::Timer.new(timeout) do + too_long = true if close_status.nil? EM.stop_event_loop end end end + ws.on :error do |event| + STDERR.puts "websocket client error: #{event.inspect}" + end + ws.on :close do |event| close_status = [:close, event.code, event.reason] EM.stop_event_loop end yield ws - } + end assert opened, "Should have opened web socket" assert (not too_long), "Test took too long" @@ -56,7 +112,7 @@ class WebsocketTest < ActionDispatch::IntegrationTest ws_helper do |ws| ws.on :message do |event| - d = Oj.load event.data + d = Oj.strict_load event.data status = d["status"] ws.close end @@ -65,17 +121,16 @@ class WebsocketTest < ActionDispatch::IntegrationTest assert_equal 401, status end - test "connect, subscribe and get response" do status = nil - ws_helper :admin do |ws| + ws_helper(token: :active) do |ws| ws.on :open do |event| ws.send ({method: 'subscribe'}.to_json) end ws.on :message do |event| - d = Oj.load event.data + d = Oj.strict_load event.data status = d["status"] ws.close end @@ -84,20 +139,20 @@ class WebsocketTest < ActionDispatch::IntegrationTest assert_equal 200, status end - test "connect, subscribe, get event" do + def subscribe_test state = 1 spec = nil ev_uuid = nil - authorize_with :admin + authorize_with :active - ws_helper :admin do |ws| + ws_helper(token: :active) do |ws| ws.on :open do |event| ws.send ({method: 'subscribe'}.to_json) end ws.on :message do |event| - d = Oj.load event.data + d = Oj.strict_load event.data case state when 1 assert_equal 200, d["status"] @@ -115,6 +170,10 @@ class WebsocketTest < ActionDispatch::IntegrationTest assert_equal spec.uuid, ev_uuid end + test "connect, subscribe, get event" do + subscribe_test() + end + test "connect, subscribe, get two events" do state = 1 spec = nil @@ -122,15 +181,15 @@ class WebsocketTest < ActionDispatch::IntegrationTest spec_ev_uuid = nil human_ev_uuid = nil - authorize_with :admin + authorize_with :active - ws_helper :admin do |ws| + ws_helper(token: :active) do |ws| ws.on :open do |event| ws.send ({method: 'subscribe'}.to_json) end ws.on :message do |event| - d = Oj.load event.data + d = Oj.strict_load event.data case state when 1 assert_equal 200, d["status"] @@ -142,7 +201,10 @@ class WebsocketTest < ActionDispatch::IntegrationTest state = 3 when 3 human_ev_uuid = d["object_uuid"] + state = 4 ws.close + when 4 + assert false, "Should not get any more events" end end @@ -159,15 +221,15 @@ class WebsocketTest < ActionDispatch::IntegrationTest human = nil human_ev_uuid = nil - authorize_with :admin + authorize_with :active - ws_helper :admin do |ws| + ws_helper(token: :active) do |ws| ws.on :open do |event| ws.send ({method: 'subscribe', filters: [['object_uuid', 'is_a', 'arvados#human']]}.to_json) end ws.on :message do |event| - d = Oj.load event.data + d = Oj.strict_load event.data case state when 1 assert_equal 200, d["status"] @@ -176,7 +238,10 @@ class WebsocketTest < ActionDispatch::IntegrationTest state = 2 when 2 human_ev_uuid = d["object_uuid"] + state = 3 ws.close + when 3 + assert false, "Should not get any more events" end end @@ -194,16 +259,16 @@ class WebsocketTest < ActionDispatch::IntegrationTest spec_ev_uuid = nil human_ev_uuid = nil - authorize_with :admin + authorize_with :active - ws_helper :admin do |ws| + ws_helper(token: :active) do |ws| ws.on :open do |event| ws.send ({method: 'subscribe', filters: [['object_uuid', 'is_a', 'arvados#human']]}.to_json) ws.send ({method: 'subscribe', filters: [['object_uuid', 'is_a', 'arvados#specimen']]}.to_json) end ws.on :message do |event| - d = Oj.load event.data + d = Oj.strict_load event.data case state when 1 assert_equal 200, d["status"] @@ -211,6 +276,7 @@ class WebsocketTest < ActionDispatch::IntegrationTest when 2 assert_equal 200, d["status"] spec = Specimen.create + Trait.create # not part of filters, should not be received human = Human.create state = 3 when 3 @@ -218,7 +284,10 @@ class WebsocketTest < ActionDispatch::IntegrationTest state = 4 when 4 human_ev_uuid = d["object_uuid"] + state = 5 ws.close + when 5 + assert false, "Should not get any more events" end end @@ -230,73 +299,113 @@ class WebsocketTest < ActionDispatch::IntegrationTest assert_equal human.uuid, human_ev_uuid end + + test "connect, subscribe, compound filter" do + state = 1 + t1 = nil + + authorize_with :active + + ws_helper(token: :active) do |ws| + ws.on :open do |event| + ws.send ({method: 'subscribe', filters: [['object_uuid', 'is_a', 'arvados#trait'], ['event_type', '=', 'update']]}.to_json) + end + + ws.on :message do |event| + d = Oj.strict_load event.data + case state + when 1 + assert_equal 200, d["status"] + t1 = Trait.create("name" => "foo") + t1.name = "bar" + t1.save! + state = 2 + when 2 + assert_equal 'update', d['event_type'] + state = 3 + ws.close + when 3 + assert false, "Should not get any more events" + end + end + + end + + assert_equal 3, state + assert_not_nil t1 + end + test "connect, subscribe, ask events starting at seq num" do state = 1 - human = nil - human_ev_uuid = nil - authorize_with :admin + authorize_with :active - lastid = logs(:log3).id + lastid = logs(:admin_changes_specimen).id l1 = nil l2 = nil - ws_helper :admin do |ws| + ws_helper(token: :active) do |ws| ws.on :open do |event| ws.send ({method: 'subscribe', last_log_id: lastid}.to_json) end ws.on :message do |event| - d = Oj.load event.data + d = Oj.strict_load event.data case state when 1 assert_equal 200, d["status"] state = 2 when 2 l1 = d["object_uuid"] + assert_not_nil l1, "Unexpected message: #{d}" state = 3 when 3 l2 = d["object_uuid"] + assert_not_nil l2, "Unexpected message: #{d}" + state = 4 ws.close + when 4 + assert false, "Should not get any more events" end end - end - assert_equal l1, logs(:log4).object_uuid - assert_equal l2, logs(:log5).object_uuid + expect_next_logs = Log.where('id > ?', lastid).order('id asc') + assert_equal expect_next_logs[0].object_uuid, l1 + assert_equal expect_next_logs[1].object_uuid, l2 end - test "connect, subscribe, get event, unsubscribe" do + slow_test "connect, subscribe, get event, unsubscribe" do state = 1 spec = nil spec_ev_uuid = nil - filter_id = nil - authorize_with :admin + authorize_with :active - ws_helper :admin, false do |ws| + ws_helper(token: :active, timeout: false) do |ws| ws.on :open do |event| ws.send ({method: 'subscribe'}.to_json) EM::Timer.new 3 do + # Set a time limit on the test because after unsubscribing the server + # still has to process the next event (and then hopefully correctly + # decides not to send it because we unsubscribed.) ws.close end end ws.on :message do |event| - d = Oj.load event.data + d = Oj.strict_load event.data case state when 1 assert_equal 200, d["status"] - filter_id = d["filter_id"] spec = Specimen.create state = 2 when 2 spec_ev_uuid = d["object_uuid"] - ws.send ({method: 'unsubscribe', filter_id: filter_id}.to_json) + ws.send ({method: 'unsubscribe'}.to_json) EM::Timer.new 1 do - Human.create + Specimen.create end state = 3 @@ -314,70 +423,71 @@ class WebsocketTest < ActionDispatch::IntegrationTest assert_equal spec.uuid, spec_ev_uuid end - - test "connect, subscribe, get event, try to unsubscribe with bogus filter_id" do + slow_test "connect, subscribe, get event, unsubscribe with filter" do state = 1 spec = nil spec_ev_uuid = nil - human = nil - human_ev_uuid = nil - authorize_with :admin + authorize_with :active - ws_helper :admin do |ws| + ws_helper(token: :active, timeout: false) do |ws| ws.on :open do |event| - ws.send ({method: 'subscribe'}.to_json) + ws.send ({method: 'subscribe', filters: [['object_uuid', 'is_a', 'arvados#human']]}.to_json) + EM::Timer.new 6 do + # Set a time limit on the test because after unsubscribing the server + # still has to process the next event (and then hopefully correctly + # decides not to send it because we unsubscribed.) + ws.close + end end ws.on :message do |event| - d = Oj.load event.data + d = Oj.strict_load event.data case state when 1 assert_equal 200, d["status"] - spec = Specimen.create + spec = Human.create state = 2 when 2 spec_ev_uuid = d["object_uuid"] - ws.send ({method: 'unsubscribe', filter_id: 100000}.to_json) + ws.send ({method: 'unsubscribe', filters: [['object_uuid', 'is_a', 'arvados#human']]}.to_json) EM::Timer.new 1 do - human = Human.create + Human.create end state = 3 when 3 - assert_equal 404, d["status"] + assert_equal 200, d["status"] state = 4 when 4 - human_ev_uuid = d["object_uuid"] - ws.close + assert false, "Should not get any more events" end end end assert_not_nil spec - assert_not_nil human assert_equal spec.uuid, spec_ev_uuid - assert_equal human.uuid, human_ev_uuid end - test "connect, subscribe, get event, try to unsubscribe with missing filter_id" do + + slow_test "connect, subscribe, get event, try to unsubscribe with bogus filter" do state = 1 spec = nil spec_ev_uuid = nil human = nil human_ev_uuid = nil - authorize_with :admin + authorize_with :active - ws_helper :admin do |ws| + ws_helper(token: :active) do |ws| ws.on :open do |event| ws.send ({method: 'subscribe'}.to_json) end ws.on :message do |event| - d = Oj.load event.data + d = Oj.strict_load event.data case state when 1 assert_equal 200, d["status"] @@ -385,7 +495,7 @@ class WebsocketTest < ActionDispatch::IntegrationTest state = 2 when 2 spec_ev_uuid = d["object_uuid"] - ws.send ({method: 'unsubscribe'}.to_json) + ws.send ({method: 'unsubscribe', filters: [['foo', 'bar', 'baz']]}.to_json) EM::Timer.new 1 do human = Human.create @@ -393,11 +503,14 @@ class WebsocketTest < ActionDispatch::IntegrationTest state = 3 when 3 - assert_equal 400, d["status"] + assert_equal 404, d["status"] state = 4 when 4 human_ev_uuid = d["object_uuid"] + state = 5 ws.close + when 5 + assert false, "Should not get any more events" end end @@ -409,11 +522,10 @@ class WebsocketTest < ActionDispatch::IntegrationTest assert_equal human.uuid, human_ev_uuid end + slow_test "connected, not subscribed, no event" do + authorize_with :active - test "connected, not subscribed, no event" do - authorize_with :admin - - ws_helper :admin, false do |ws| + ws_helper(token: :active, timeout: false) do |ws| ws.on :open do |event| EM::Timer.new 1 do Specimen.create @@ -430,12 +542,12 @@ class WebsocketTest < ActionDispatch::IntegrationTest end end - test "connected, not authorized to see event" do + slow_test "connected, not authorized to see event" do state = 1 authorize_with :admin - ws_helper :active, false do |ws| + ws_helper(token: :active, timeout: false) do |ws| ws.on :open do |event| ws.send ({method: 'subscribe'}.to_json) @@ -445,7 +557,7 @@ class WebsocketTest < ActionDispatch::IntegrationTest end ws.on :message do |event| - d = Oj.load event.data + d = Oj.strict_load event.data case state when 1 assert_equal 200, d["status"] @@ -463,13 +575,13 @@ class WebsocketTest < ActionDispatch::IntegrationTest test "connect, try bogus method" do status = nil - ws_helper :admin do |ws| + ws_helper(token: :active) do |ws| ws.on :open do |event| ws.send ({method: 'frobnabble'}.to_json) end ws.on :message do |event| - d = Oj.load event.data + d = Oj.strict_load event.data status = d["status"] ws.close end @@ -481,13 +593,13 @@ class WebsocketTest < ActionDispatch::IntegrationTest test "connect, missing method" do status = nil - ws_helper :admin do |ws| + ws_helper(token: :active) do |ws| ws.on :open do |event| ws.send ({fizzbuzz: 'frobnabble'}.to_json) end ws.on :message do |event| - d = Oj.load event.data + d = Oj.strict_load event.data status = d["status"] ws.close end @@ -499,13 +611,13 @@ class WebsocketTest < ActionDispatch::IntegrationTest test "connect, send malformed request" do status = nil - ws_helper :admin do |ws| + ws_helper(token: :active) do |ws| ws.on :open do |event| ws.send '' end ws.on :message do |event| - d = Oj.load event.data + d = Oj.strict_load event.data status = d["status"] ws.close end @@ -518,9 +630,9 @@ class WebsocketTest < ActionDispatch::IntegrationTest test "connect, try subscribe too many filters" do state = 1 - authorize_with :admin + authorize_with :active - ws_helper :admin do |ws| + ws_helper(token: :active) do |ws| ws.on :open do |event| (1..17).each do |i| ws.send ({method: 'subscribe', filters: [['object_uuid', '=', i]]}.to_json) @@ -528,12 +640,12 @@ class WebsocketTest < ActionDispatch::IntegrationTest end ws.on :message do |event| - d = Oj.load event.data + d = Oj.strict_load event.data case state - when (1..16) + when (1..Rails.configuration.websocket_max_filters) assert_equal 200, d["status"] state += 1 - when 17 + when (Rails.configuration.websocket_max_filters+1) assert_equal 403, d["status"] ws.close end @@ -541,8 +653,90 @@ class WebsocketTest < ActionDispatch::IntegrationTest end - assert_equal 17, state + assert_equal Rails.configuration.websocket_max_filters+1, state + + end + + slow_test "connect, subscribe, lots of events" do + state = 1 + event_count = 0 + log_start = Log.order(:id).last.id + + authorize_with :active + + ws_helper(token: :active, timeout: false) do |ws| + EM::Timer.new 45 do + # Needs a longer timeout than the default + ws.close + end + + ws.on :open do |event| + ws.send ({method: 'subscribe'}.to_json) + end + + ws.on :message do |event| + d = Oj.strict_load event.data + case state + when 1 + assert_equal 200, d["status"] + ActiveRecord::Base.transaction do + (1..202).each do + Specimen.create + end + end + state = 2 + when 2 + event_count += 1 + assert_equal d['id'], event_count+log_start + if event_count == 202 + ws.close + end + end + end + + end + + assert_equal 202, event_count + end + + + test "connect, subscribe with invalid filter" do + state = 1 + + authorize_with :active + + ws_helper(token: :active) do |ws| + ws.on :open do |event| + # test that #6451 is fixed (invalid filter crashes websockets) + ws.send ({method: 'subscribe', filters: [['object_blarg', 'is_a', 'arvados#human']]}.to_json) + end + + ws.on :message do |event| + d = Oj.strict_load event.data + case state + when 1 + assert_equal 200, d["status"] + Specimen.create + Human.create + state = 2 + when 2 + assert_equal 500, d["status"] + state = 3 + ws.close + when 3 + assert false, "Should not get any more events" + end + end + + end + + assert_equal 3, state + + # Try connecting again, ensure that websockets server is still running and + # didn't crash per #6451 + subscribe_test() end + end