Merge branch 'master' into 9998-unsigned_manifest
[arvados.git] / services / api / test / integration / websocket_test.rb
index 9179acd6803fa2fcb0384a507103b9bf390a5d15..a9993b2fc318b4abe6ca0669f64a614250608733 100644 (file)
@@ -1,5 +1,4 @@
 require 'test_helper'
-require 'websocket_runner'
 require 'oj'
 require 'database_cleaner'
 
@@ -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 4 do
+          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"]
@@ -162,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"]
@@ -200,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"]
@@ -245,15 +304,15 @@ class WebsocketTest < ActionDispatch::IntegrationTest
     state = 1
     t1 = 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#trait'], ['event_type', '=', 'update']]}.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"]
@@ -278,22 +337,20 @@ class WebsocketTest < ActionDispatch::IntegrationTest
 
   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(: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"]
@@ -318,15 +375,14 @@ class WebsocketTest < ActionDispatch::IntegrationTest
     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
@@ -338,7 +394,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"]
@@ -367,17 +423,17 @@ class WebsocketTest < ActionDispatch::IntegrationTest
     assert_equal spec.uuid, spec_ev_uuid
   end
 
-  test "connect, subscribe, get event, unsubscribe with filter" do
+  slow_test "connect, subscribe, get event, unsubscribe with filter" do
     state = 1
     spec = nil
     spec_ev_uuid = 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', filters: [['object_uuid', 'is_a', 'arvados#human']]}.to_json)
-        EM::Timer.new 3 do
+        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.)
@@ -386,7 +442,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"]
@@ -416,22 +472,22 @@ class WebsocketTest < ActionDispatch::IntegrationTest
   end
 
 
-  test "connect, subscribe, get event, try to unsubscribe with bogus filter" 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"]
@@ -466,12 +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
@@ -488,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)
 
@@ -503,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"]
@@ -521,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
@@ -539,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
@@ -557,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 '<XML4EVER></XML4EVER>'
       end
 
       ws.on :message do |event|
-        d = Oj.load event.data
+        d = Oj.strict_load event.data
         status = d["status"]
         ws.close
       end
@@ -576,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)
@@ -586,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..EventBus::MAX_FILTERS)
+        when (1..Rails.configuration.websocket_max_filters)
           assert_equal 200, d["status"]
           state += 1
-        when (EventBus::MAX_FILTERS+1)
+        when (Rails.configuration.websocket_max_filters+1)
           assert_equal 403, d["status"]
           ws.close
         end
@@ -599,18 +653,18 @@ class WebsocketTest < ActionDispatch::IntegrationTest
 
     end
 
-    assert_equal 17, state
+    assert_equal Rails.configuration.websocket_max_filters+1, state
 
   end
 
-  test "connect, subscribe, lots of events" do
+  slow_test "connect, subscribe, lots of events" do
     state = 1
     event_count = 0
     log_start = Log.order(:id).last.id
 
-    authorize_with :admin
+    authorize_with :active
 
-    ws_helper :admin, false do |ws|
+    ws_helper(token: :active, timeout: false) do |ws|
       EM::Timer.new 45 do
         # Needs a longer timeout than the default
         ws.close
@@ -621,13 +675,13 @@ 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"]
           ActiveRecord::Base.transaction do
             (1..202).each do
-              spec = Specimen.create
+              Specimen.create
             end
           end
           state = 2
@@ -646,4 +700,43 @@ class WebsocketTest < ActionDispatch::IntegrationTest
   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