11168: Change db serialize from YAML to JSON.
[arvados.git] / services / api / test / integration / websocket_test.rb
1 require 'database_cleaner'
2 require 'oj'
3 require 'safe_json'
4 require 'test_helper'
5
6 DatabaseCleaner.strategy = :deletion
7
8 class WebsocketTest < ActionDispatch::IntegrationTest
9   self.use_transactional_fixtures = false
10
11   setup do
12     DatabaseCleaner.start
13   end
14
15   teardown do
16     DatabaseCleaner.clean
17   end
18
19   def self.startup
20     s = TCPServer.new('0.0.0.0', 0)
21     @@port = s.addr[1]
22     s.close
23     @@pidfile = "tmp/pids/passenger.#{@@port}.pid"
24     DatabaseCleaner.start
25     Dir.chdir(Rails.root) do |apidir|
26       # Only passenger seems to be able to run the websockets server
27       # successfully.
28       _system('passenger', 'start', '-d',
29               "-p#{@@port}",
30               "--log-file", "/dev/stderr",
31               "--pid-file", @@pidfile)
32       timeout = Time.now.tv_sec + 10
33       begin
34         sleep 0.2
35         begin
36           server_pid = IO.read(@@pidfile).to_i
37           good_pid = (server_pid > 0) and (Process.kill(0, pid) rescue false)
38         rescue Errno::ENOENT
39           good_pid = false
40         end
41       end while (not good_pid) and (Time.now.tv_sec < timeout)
42       if not good_pid
43         raise RuntimeError, "could not find API server Rails pid"
44       end
45       STDERR.puts "Started websocket server on port #{@@port} with pid #{server_pid}"
46     end
47   end
48
49   def self.shutdown
50     Dir.chdir(Rails.root) do
51       _system('passenger', 'stop', "-p#{@@port}",
52               "--pid-file", @@pidfile)
53     end
54     # DatabaseCleaner leaves the database empty. Prefer to leave it full.
55     dc = DatabaseController.new
56     dc.define_singleton_method :render do |*args| end
57     dc.reset
58   end
59
60   def self._system(*cmd)
61     Bundler.with_clean_env do
62       env = {
63         'ARVADOS_WEBSOCKETS' => 'ws-only',
64         'RAILS_ENV' => 'test',
65       }
66       if not system(env, *cmd)
67         raise RuntimeError, "Command exited #{$?}: #{cmd.inspect}"
68       end
69     end
70   end
71
72   def ws_helper(token: nil, timeout: 8)
73     opened = false
74     close_status = nil
75     too_long = false
76
77     EM.run do
78       if token
79         ws = Faye::WebSocket::Client.new("ws://localhost:#{@@port}/websocket?api_token=#{api_client_authorizations(token).api_token}")
80       else
81         ws = Faye::WebSocket::Client.new("ws://localhost:#{@@port}/websocket")
82       end
83
84       ws.on :open do |event|
85         opened = true
86         if timeout
87           EM::Timer.new(timeout) do
88             too_long = true if close_status.nil?
89             EM.stop_event_loop
90           end
91         end
92       end
93
94       ws.on :error do |event|
95         STDERR.puts "websocket client error: #{event.inspect}"
96       end
97
98       ws.on :close do |event|
99         close_status = [:close, event.code, event.reason]
100         EM.stop_event_loop
101       end
102
103       yield ws
104     end
105
106     assert opened, "Should have opened web socket"
107     assert (not too_long), "Test took too long"
108     assert_equal 1000, close_status[1], "Connection closed unexpectedly (check log for errors)"
109   end
110
111   test "connect with no token" do
112     status = nil
113
114     ws_helper do |ws|
115       ws.on :message do |event|
116         d = SafeJSON.load event.data
117         status = d["status"]
118         ws.close
119       end
120     end
121
122     assert_equal 401, status
123   end
124
125   test "connect, subscribe and get response" do
126     status = nil
127
128     ws_helper(token: :active) do |ws|
129       ws.on :open do |event|
130         ws.send ({method: 'subscribe'}.to_json)
131       end
132
133       ws.on :message do |event|
134         d = SafeJSON.load event.data
135         status = d["status"]
136         ws.close
137       end
138     end
139
140     assert_equal 200, status
141   end
142
143   def subscribe_test
144     state = 1
145     spec = nil
146     ev_uuid = nil
147
148     authorize_with :active
149
150     ws_helper(token: :active) do |ws|
151       ws.on :open do |event|
152         ws.send ({method: 'subscribe'}.to_json)
153       end
154
155       ws.on :message do |event|
156         d = SafeJSON.load event.data
157         case state
158         when 1
159           assert_equal 200, d["status"]
160           spec = Specimen.create
161           state = 2
162         when 2
163           ev_uuid = d["object_uuid"]
164           ws.close
165         end
166       end
167
168     end
169
170     assert_not_nil spec
171     assert_equal spec.uuid, ev_uuid
172   end
173
174   test "connect, subscribe, get event" do
175     subscribe_test()
176   end
177
178   test "connect, subscribe, get two events" do
179     state = 1
180     spec = nil
181     human = nil
182     spec_ev_uuid = nil
183     human_ev_uuid = nil
184
185     authorize_with :active
186
187     ws_helper(token: :active) do |ws|
188       ws.on :open do |event|
189         ws.send ({method: 'subscribe'}.to_json)
190       end
191
192       ws.on :message do |event|
193         d = SafeJSON.load event.data
194         case state
195         when 1
196           assert_equal 200, d["status"]
197           spec = Specimen.create
198           human = Human.create
199           state = 2
200         when 2
201           spec_ev_uuid = d["object_uuid"]
202           state = 3
203         when 3
204           human_ev_uuid = d["object_uuid"]
205           state = 4
206           ws.close
207         when 4
208           assert false, "Should not get any more events"
209         end
210       end
211
212     end
213
214     assert_not_nil spec
215     assert_not_nil human
216     assert_equal spec.uuid, spec_ev_uuid
217     assert_equal human.uuid, human_ev_uuid
218   end
219
220   test "connect, subscribe, filter events" do
221     state = 1
222     human = nil
223     human_ev_uuid = nil
224
225     authorize_with :active
226
227     ws_helper(token: :active) do |ws|
228       ws.on :open do |event|
229         ws.send ({method: 'subscribe', filters: [['object_uuid', 'is_a', 'arvados#human']]}.to_json)
230       end
231
232       ws.on :message do |event|
233         d = SafeJSON.load event.data
234         case state
235         when 1
236           assert_equal 200, d["status"]
237           Specimen.create
238           human = Human.create
239           state = 2
240         when 2
241           human_ev_uuid = d["object_uuid"]
242           state = 3
243           ws.close
244         when 3
245           assert false, "Should not get any more events"
246         end
247       end
248
249     end
250
251     assert_not_nil human
252     assert_equal human.uuid, human_ev_uuid
253   end
254
255
256   test "connect, subscribe, multiple filters" do
257     state = 1
258     spec = nil
259     human = nil
260     spec_ev_uuid = nil
261     human_ev_uuid = nil
262
263     authorize_with :active
264
265     ws_helper(token: :active) do |ws|
266       ws.on :open do |event|
267         ws.send ({method: 'subscribe', filters: [['object_uuid', 'is_a', 'arvados#human']]}.to_json)
268         ws.send ({method: 'subscribe', filters: [['object_uuid', 'is_a', 'arvados#specimen']]}.to_json)
269       end
270
271       ws.on :message do |event|
272         d = SafeJSON.load event.data
273         case state
274         when 1
275           assert_equal 200, d["status"]
276           state = 2
277         when 2
278           assert_equal 200, d["status"]
279           spec = Specimen.create
280           Trait.create # not part of filters, should not be received
281           human = Human.create
282           state = 3
283         when 3
284           spec_ev_uuid = d["object_uuid"]
285           state = 4
286         when 4
287           human_ev_uuid = d["object_uuid"]
288           state = 5
289           ws.close
290         when 5
291           assert false, "Should not get any more events"
292         end
293       end
294
295     end
296
297     assert_not_nil spec
298     assert_not_nil human
299     assert_equal spec.uuid, spec_ev_uuid
300     assert_equal human.uuid, human_ev_uuid
301   end
302
303
304   test "connect, subscribe, compound filter" do
305     state = 1
306     t1 = nil
307
308     authorize_with :active
309
310     ws_helper(token: :active) do |ws|
311       ws.on :open do |event|
312         ws.send ({method: 'subscribe', filters: [['object_uuid', 'is_a', 'arvados#trait'], ['event_type', '=', 'update']]}.to_json)
313       end
314
315       ws.on :message do |event|
316         d = SafeJSON.load event.data
317         case state
318         when 1
319           assert_equal 200, d["status"]
320           t1 = Trait.create("name" => "foo")
321           t1.name = "bar"
322           t1.save!
323           state = 2
324          when 2
325           assert_equal 'update', d['event_type']
326           state = 3
327           ws.close
328         when 3
329           assert false, "Should not get any more events"
330         end
331       end
332
333     end
334
335     assert_equal 3, state
336     assert_not_nil t1
337   end
338
339   test "connect, subscribe, ask events starting at seq num" do
340     state = 1
341
342     authorize_with :active
343
344     lastid = logs(:admin_changes_specimen).id
345     l1 = nil
346     l2 = nil
347
348     ws_helper(token: :active) do |ws|
349       ws.on :open do |event|
350         ws.send ({method: 'subscribe', last_log_id: lastid}.to_json)
351       end
352
353       ws.on :message do |event|
354         d = SafeJSON.load event.data
355         case state
356         when 1
357           assert_equal 200, d["status"]
358           state = 2
359         when 2
360           l1 = d["object_uuid"]
361           assert_not_nil l1, "Unexpected message: #{d}"
362           state = 3
363         when 3
364           l2 = d["object_uuid"]
365           assert_not_nil l2, "Unexpected message: #{d}"
366           state = 4
367           ws.close
368         when 4
369           assert false, "Should not get any more events"
370         end
371       end
372     end
373
374     expect_next_logs = Log.where('id > ?', lastid).order('id asc')
375     assert_equal expect_next_logs[0].object_uuid, l1
376     assert_equal expect_next_logs[1].object_uuid, l2
377   end
378
379   slow_test "connect, subscribe, get event, unsubscribe" do
380     state = 1
381     spec = nil
382     spec_ev_uuid = nil
383
384     authorize_with :active
385
386     ws_helper(token: :active, timeout: false) do |ws|
387       ws.on :open do |event|
388         ws.send ({method: 'subscribe'}.to_json)
389         EM::Timer.new 3 do
390           # Set a time limit on the test because after unsubscribing the server
391           # still has to process the next event (and then hopefully correctly
392           # decides not to send it because we unsubscribed.)
393           ws.close
394         end
395       end
396
397       ws.on :message do |event|
398         d = SafeJSON.load event.data
399         case state
400         when 1
401           assert_equal 200, d["status"]
402           spec = Specimen.create
403           state = 2
404         when 2
405           spec_ev_uuid = d["object_uuid"]
406           ws.send ({method: 'unsubscribe'}.to_json)
407
408           EM::Timer.new 1 do
409             Specimen.create
410           end
411
412           state = 3
413         when 3
414           assert_equal 200, d["status"]
415           state = 4
416         when 4
417           assert false, "Should not get any more events"
418         end
419       end
420
421     end
422
423     assert_not_nil spec
424     assert_equal spec.uuid, spec_ev_uuid
425   end
426
427   slow_test "connect, subscribe, get event, unsubscribe with filter" do
428     state = 1
429     spec = nil
430     spec_ev_uuid = nil
431
432     authorize_with :active
433
434     ws_helper(token: :active, timeout: false) do |ws|
435       ws.on :open do |event|
436         ws.send ({method: 'subscribe', filters: [['object_uuid', 'is_a', 'arvados#human']]}.to_json)
437         EM::Timer.new 6 do
438           # Set a time limit on the test because after unsubscribing the server
439           # still has to process the next event (and then hopefully correctly
440           # decides not to send it because we unsubscribed.)
441           ws.close
442         end
443       end
444
445       ws.on :message do |event|
446         d = SafeJSON.load event.data
447         case state
448         when 1
449           assert_equal 200, d["status"]
450           spec = Human.create
451           state = 2
452         when 2
453           spec_ev_uuid = d["object_uuid"]
454           ws.send ({method: 'unsubscribe', filters: [['object_uuid', 'is_a', 'arvados#human']]}.to_json)
455
456           EM::Timer.new 1 do
457             Human.create
458           end
459
460           state = 3
461         when 3
462           assert_equal 200, d["status"]
463           state = 4
464         when 4
465           assert false, "Should not get any more events"
466         end
467       end
468
469     end
470
471     assert_not_nil spec
472     assert_equal spec.uuid, spec_ev_uuid
473   end
474
475
476   slow_test "connect, subscribe, get event, try to unsubscribe with bogus filter" do
477     state = 1
478     spec = nil
479     spec_ev_uuid = nil
480     human = nil
481     human_ev_uuid = nil
482
483     authorize_with :active
484
485     ws_helper(token: :active) do |ws|
486       ws.on :open do |event|
487         ws.send ({method: 'subscribe'}.to_json)
488       end
489
490       ws.on :message do |event|
491         d = SafeJSON.load event.data
492         case state
493         when 1
494           assert_equal 200, d["status"]
495           spec = Specimen.create
496           state = 2
497         when 2
498           spec_ev_uuid = d["object_uuid"]
499           ws.send ({method: 'unsubscribe', filters: [['foo', 'bar', 'baz']]}.to_json)
500
501           EM::Timer.new 1 do
502             human = Human.create
503           end
504
505           state = 3
506         when 3
507           assert_equal 404, d["status"]
508           state = 4
509         when 4
510           human_ev_uuid = d["object_uuid"]
511           state = 5
512           ws.close
513         when 5
514           assert false, "Should not get any more events"
515         end
516       end
517
518     end
519
520     assert_not_nil spec
521     assert_not_nil human
522     assert_equal spec.uuid, spec_ev_uuid
523     assert_equal human.uuid, human_ev_uuid
524   end
525
526   slow_test "connected, not subscribed, no event" do
527     authorize_with :active
528
529     ws_helper(token: :active, timeout: false) do |ws|
530       ws.on :open do |event|
531         EM::Timer.new 1 do
532           Specimen.create
533         end
534
535         EM::Timer.new 3 do
536           ws.close
537         end
538       end
539
540       ws.on :message do |event|
541         assert false, "Should not get any messages, message was #{event.data}"
542       end
543     end
544   end
545
546   slow_test "connected, not authorized to see event" do
547     state = 1
548
549     authorize_with :admin
550
551     ws_helper(token: :active, timeout: false) do |ws|
552       ws.on :open do |event|
553         ws.send ({method: 'subscribe'}.to_json)
554
555         EM::Timer.new 3 do
556           ws.close
557         end
558       end
559
560       ws.on :message do |event|
561         d = SafeJSON.load event.data
562         case state
563         when 1
564           assert_equal 200, d["status"]
565           Specimen.create
566           state = 2
567         when 2
568           assert false, "Should not get any messages, message was #{event.data}"
569         end
570       end
571
572     end
573
574   end
575
576   test "connect, try bogus method" do
577     status = nil
578
579     ws_helper(token: :active) do |ws|
580       ws.on :open do |event|
581         ws.send ({method: 'frobnabble'}.to_json)
582       end
583
584       ws.on :message do |event|
585         d = SafeJSON.load event.data
586         status = d["status"]
587         ws.close
588       end
589     end
590
591     assert_equal 400, status
592   end
593
594   test "connect, missing method" do
595     status = nil
596
597     ws_helper(token: :active) do |ws|
598       ws.on :open do |event|
599         ws.send ({fizzbuzz: 'frobnabble'}.to_json)
600       end
601
602       ws.on :message do |event|
603         d = SafeJSON.load event.data
604         status = d["status"]
605         ws.close
606       end
607     end
608
609     assert_equal 400, status
610   end
611
612   test "connect, send malformed request" do
613     status = nil
614
615     ws_helper(token: :active) do |ws|
616       ws.on :open do |event|
617         ws.send '<XML4EVER></XML4EVER>'
618       end
619
620       ws.on :message do |event|
621         d = SafeJSON.load event.data
622         status = d["status"]
623         ws.close
624       end
625     end
626
627     assert_equal 400, status
628   end
629
630
631   test "connect, try subscribe too many filters" do
632     state = 1
633
634     authorize_with :active
635
636     ws_helper(token: :active) do |ws|
637       ws.on :open do |event|
638         (1..17).each do |i|
639           ws.send ({method: 'subscribe', filters: [['object_uuid', '=', i]]}.to_json)
640         end
641       end
642
643       ws.on :message do |event|
644         d = SafeJSON.load event.data
645         case state
646         when (1..Rails.configuration.websocket_max_filters)
647           assert_equal 200, d["status"]
648           state += 1
649         when (Rails.configuration.websocket_max_filters+1)
650           assert_equal 403, d["status"]
651           ws.close
652         end
653       end
654
655     end
656
657     assert_equal Rails.configuration.websocket_max_filters+1, state
658
659   end
660
661   slow_test "connect, subscribe, lots of events" do
662     state = 1
663     event_count = 0
664     log_start = Log.order(:id).last.id
665
666     authorize_with :active
667
668     ws_helper(token: :active, timeout: false) do |ws|
669       EM::Timer.new 45 do
670         # Needs a longer timeout than the default
671         ws.close
672       end
673
674       ws.on :open do |event|
675         ws.send ({method: 'subscribe'}.to_json)
676       end
677
678       ws.on :message do |event|
679         d = SafeJSON.load event.data
680         case state
681         when 1
682           assert_equal 200, d["status"]
683           ActiveRecord::Base.transaction do
684             (1..202).each do
685               Specimen.create
686             end
687           end
688           state = 2
689         when 2
690           event_count += 1
691           assert_equal d['id'], event_count+log_start
692           if event_count == 202
693             ws.close
694           end
695         end
696       end
697
698     end
699
700     assert_equal 202, event_count
701   end
702
703
704   test "connect, subscribe with invalid filter" do
705     state = 1
706
707     authorize_with :active
708
709     ws_helper(token: :active) do |ws|
710       ws.on :open do |event|
711         # test that #6451 is fixed (invalid filter crashes websockets)
712         ws.send ({method: 'subscribe', filters: [['object_blarg', 'is_a', 'arvados#human']]}.to_json)
713       end
714
715       ws.on :message do |event|
716         d = SafeJSON.load event.data
717         case state
718         when 1
719           assert_equal 200, d["status"]
720           Specimen.create
721           Human.create
722           state = 2
723         when 2
724           assert_equal 500, d["status"]
725           state = 3
726           ws.close
727         when 3
728           assert false, "Should not get any more events"
729         end
730       end
731
732     end
733
734     assert_equal 3, state
735
736     # Try connecting again, ensure that websockets server is still running and
737     # didn't crash per #6451
738     subscribe_test()
739
740   end
741
742
743 end