juggernaut 0.5.7 → 0.5.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,26 @@
1
+
2
+ $:.unshift "../lib"
3
+ require "juggernaut"
4
+ require "test/unit"
5
+ require "shoulda"
6
+ require "mocha"
7
+ require "tempfile"
8
+
9
+ class TestRunner < Test::Unit::TestCase
10
+
11
+ CONFIG = File.join(File.dirname(__FILE__), "files", "juggernaut.yml")
12
+
13
+ context "Runner" do
14
+
15
+ should "always be true" do
16
+ assert true
17
+ end
18
+
19
+ # should "run" do
20
+ # EM.run { EM.stop }
21
+ # Juggernaut::Runner.run(["-c", CONFIG])
22
+ # end
23
+
24
+ end
25
+
26
+ end
@@ -0,0 +1,573 @@
1
+
2
+ $:.unshift "../lib"
3
+ require "juggernaut"
4
+ require "test/unit"
5
+ require "shoulda"
6
+ require "mocha"
7
+
8
+ class TestServer < Test::Unit::TestCase
9
+
10
+ CONFIG = File.join(File.dirname(__FILE__), "files", "juggernaut.yml")
11
+
12
+ DEFAULT_OPTIONS = {
13
+ :host => "0.0.0.0",
14
+ :port => 5001,
15
+ :debug => false,
16
+ :cleanup_timer => 2,
17
+ :timeout => 10,
18
+ :store_messages => false
19
+ }
20
+
21
+ OPTIONS = DEFAULT_OPTIONS.merge(YAML::load(ERB.new(IO.read(CONFIG)).result))
22
+
23
+ class DirectClient
24
+ attr_reader :channels
25
+ def broadcast_to_channels(channels, body)
26
+ self.transmit :command => :broadcast, :type => :to_channels, :channels => channels, :body => body
27
+ self
28
+ end
29
+ def broadcast_to_clients(clients, body)
30
+ self.transmit :command => :broadcast, :type => :to_clients, :client_ids => clients, :body => body
31
+ end
32
+ def close
33
+ @socket.close if @socket
34
+ end
35
+ def initialize(options)
36
+ @options = options
37
+ @socket = nil
38
+ @client_id = options[:client_id]
39
+ @session_id = options[:session_id] || rand(1_000_000).to_s(16)
40
+ @channels = [ ]
41
+ @socket = TCPSocket.new(@options[:host], @options[:port])
42
+ end
43
+ def inspect
44
+ {:channels => @channels, :client_id => @client_id, :session_id => @session_id}.inspect
45
+ end
46
+ def request_crossdomain_file
47
+ @socket.print "<policy-file-request/>\0"
48
+ self
49
+ end
50
+ def query_remove_channels_from_all_clients(channels)
51
+ self.transmit :command => :query, :type => :remove_channels_from_all_clients, :channels => channels
52
+ self
53
+ end
54
+ def query_remove_channels_from_client(channels, clients)
55
+ self.transmit :command => :query, :type => :remove_channels_from_client, :client_ids => clients, :channels => channels
56
+ self
57
+ end
58
+ def query_show_channels_for_client(client_id)
59
+ self.transmit :command => :query, :type => :show_channels_for_client, :client_id => client_id
60
+ self
61
+ end
62
+ def query_show_client(client_id)
63
+ self.transmit :command => :query, :type => :show_client, :client_id => client_id
64
+ self
65
+ end
66
+ def query_show_clients(client_ids = [])
67
+ self.transmit :command => :query, :type => :show_clients, :client_ids => client_ids
68
+ self
69
+ end
70
+ def query_show_clients_for_channels(channels)
71
+ self.transmit :command => :query, :type => :show_clients_for_channels, :channels => channels
72
+ self
73
+ end
74
+ def receive(as_json = true)
75
+ return nil unless @socket
76
+ begin
77
+ # response = @socket.read.to_s
78
+ # response = @socket.readline("\0").to_s
79
+ response = ""
80
+ begin
81
+ response << @socket.read_nonblock(1024)
82
+ rescue Errno::EAGAIN
83
+ end
84
+ response.chomp!("\0")
85
+ Juggernaut.logger.info "DirectClient read: " + response.inspect
86
+ as_json ? JSON.parse(response) : response
87
+ rescue => e
88
+ Juggernaut.logger.error "DirectClient #{e.class}: #{e.message}"
89
+ raise
90
+ end
91
+ end
92
+ def subscribe(channels)
93
+ channels.each do |channel|
94
+ @channels << channel.to_s unless @channels.include?(channel.to_s)
95
+ end
96
+ self.transmit :command => :subscribe, :channels => channels
97
+ self
98
+ end
99
+ def send_raw(raw, wait_response = false)
100
+ @socket.print(raw + "\0")
101
+ @socket.flush
102
+ if wait_response
103
+ self.receive
104
+ else
105
+ nil
106
+ end
107
+ end
108
+ def transmit(hash, wait_response = false)
109
+ hash[:client_id] ||= @client_id
110
+ hash[:session_id] ||= @session_id
111
+ self.send_raw(hash.to_json, wait_response)
112
+ end
113
+ end
114
+
115
+ # Assert that the DirectClient has an awaiting message with +body+.
116
+ def assert_body(body, subscriber)
117
+ assert_response subscriber do |result|
118
+ assert_respond_to result, :[]
119
+ assert_equal body, result["body"]
120
+ end
121
+ end
122
+
123
+ # Assert that the DirectClient has no awaiting message.
124
+ def assert_no_body(subscriber)
125
+ assert_response subscriber do |result|
126
+ assert_equal false, result
127
+ end
128
+ end
129
+
130
+ def assert_no_response(subscriber)
131
+ assert_not_nil subscriber
132
+ assert_raise(EOFError) { subscriber.receive }
133
+ ensure
134
+ subscriber.close
135
+ end
136
+
137
+ def assert_raw_response(subscriber, response = nil)
138
+ assert_not_nil subscriber
139
+ result = nil
140
+ assert_nothing_raised { result = subscriber.receive(false) }
141
+ assert_not_nil result
142
+ if block_given?
143
+ yield result
144
+ else
145
+ assert_equal response, result
146
+ end
147
+ ensure
148
+ subscriber.close
149
+ end
150
+
151
+ def assert_response(subscriber, response = nil)
152
+ assert_not_nil subscriber
153
+ result = nil
154
+ assert_nothing_raised { result = subscriber.receive }
155
+ assert_not_nil result
156
+ if block_given?
157
+ yield result
158
+ else
159
+ assert_equal response, result
160
+ end
161
+ ensure
162
+ subscriber.close
163
+ end
164
+
165
+ def assert_server_disconnected(subscriber)
166
+ assert_not_nil subscriber
167
+ assert_raise(Errno::ECONNRESET, EOFError) { subscriber.receive }
168
+ end
169
+
170
+ # Convenience method to create a new DirectClient instance with overridable options.
171
+ # If a block is passed, control is yielded, passing the new client in. This method
172
+ # returns the value returned from that block, or the new client if no block was given.
173
+ def new_client(options = { })
174
+ c = DirectClient.new(OPTIONS.merge(options))
175
+ if block_given?
176
+ yield(c)
177
+ else
178
+ c
179
+ end
180
+ end
181
+
182
+ # Shortcut to run tests that require setting up, starting, then shutting down EventMachine.
183
+ # So ugly, but EventMachine doesn't have test examples on code that require back-and-forth
184
+ # communication over a long-running connection.
185
+ def with_server(options = { }, &block)
186
+ # We should not have any clients before we start
187
+ Juggernaut::Client.reset!
188
+
189
+ # Save the current options. This is an obvious hack.
190
+ old_options, Juggernaut.options = Juggernaut.options, OPTIONS.merge(options)
191
+ Juggernaut.logger.level = Logger::DEBUG
192
+
193
+ # Initialize an array to keep track of connections made to the server in this instance.
194
+ @connections = [ ]
195
+
196
+ EM.run do
197
+ # Start the server, and save each connection made so we can refer to it later.
198
+ EM.start_server(Juggernaut.options[:host], Juggernaut.options[:port], Juggernaut::Server) { |c| @connections << c }
199
+
200
+ # Guard against never-ending tests by shutting off at 2 seconds.
201
+ EM.add_timer(2) do
202
+ Juggernaut::Client.send_logouts_to_all_clients
203
+ EM.stop
204
+ end
205
+
206
+ # Deferred: evaluate the block and then run the shutdown proc. By using instance_eval,
207
+ # our block gets access to assert_* methods and the +@connections+ variable above.
208
+ EM.defer proc {
209
+ instance_eval(&block)
210
+ }, proc {
211
+ # There's probably a better way of doing this, but without this line, different
212
+ # clients may create a race condition in tests, causing some of them to sometimes
213
+ # fail. This isn't foolproof either, should any client take more than 200 ms.
214
+ EM.add_timer(0.2) do
215
+ Juggernaut::Client.send_logouts_to_all_clients
216
+ EM.stop
217
+ end
218
+ }
219
+ end
220
+ ensure
221
+ # Restore old options.
222
+ Juggernaut.options = old_options if old_options
223
+ end
224
+
225
+ context "Server" do
226
+
227
+ should "accept a connection" do
228
+ with_server do
229
+ self.new_client do |c|
230
+ c.transmit :command => :subscribe, :channels => [ ]
231
+ end
232
+ assert_equal 1, @connections.select { |c| c.alive? }.size
233
+ assert_equal true, @connections.first.alive?
234
+ end
235
+ assert_equal false, @connections.first.alive?
236
+ end
237
+
238
+ should "register channels correctly" do
239
+ with_server do
240
+ self.new_client { |c| c.transmit :command => :subscribe, :channels => ["master", "slave"] }
241
+ end
242
+ assert @connections.first.has_channel?("master")
243
+ assert_equal false, @connections.first.has_channel?("non_existant")
244
+ assert @connections.first.has_channels?(["non_existant", "master", "slave"])
245
+ assert_equal false, @connections.first.has_channels?(["non_existant", "invalid"])
246
+ end
247
+
248
+ context "channel-wide broadcast" do
249
+
250
+ body = "This is a channel-wide broadcast test!"
251
+
252
+ should "be received by client in the same channel" do
253
+ subscriber = nil
254
+ with_server do
255
+ subscriber = self.new_client(:client_id => "broadcast_channel") { |c| c.subscribe %w(master) }
256
+ self.new_client { |c| c.broadcast_to_channels %w(master), body }
257
+ end
258
+ assert_not_nil subscriber
259
+ result = subscriber.receive
260
+ subscriber.close
261
+ assert_respond_to result, :[]
262
+ assert_equal body, result["body"]
263
+ end
264
+
265
+ should "not be received by client not in a channel" do
266
+ subscriber = nil
267
+ with_server do
268
+ subscriber = self.new_client(:client_id => "broadcast_channel") { |c| c.subscribe %w() }
269
+ self.new_client { |c| c.broadcast_to_channels %w(master), body }
270
+ end
271
+ assert_no_response subscriber
272
+ end
273
+
274
+ should "not be received by client in a different channel" do
275
+ subscriber = nil
276
+ with_server do
277
+ subscriber = self.new_client(:client_id => "broadcast_test") { |c| c.subscribe %w(slave) }
278
+ self.new_client { |c| c.broadcast_to_channels %w(broadcast_channel), body }
279
+ end
280
+ assert_no_response subscriber
281
+ end
282
+
283
+ end
284
+
285
+ # For some reason, these refuse to pass:
286
+ context "broadcast with no specific channel" do
287
+
288
+ body = "This is a broadcast test!"
289
+
290
+ should "be received by client not in any channels" do
291
+ subscriber = nil
292
+ with_server do
293
+ subscriber = self.new_client(:client_id => "broadcast_all") { |c| c.subscribe %w() }
294
+ self.new_client { |c| c.broadcast_to_channels %w(), body }
295
+ end
296
+ assert_body body, subscriber
297
+ end
298
+
299
+ should "be received by client in a channel" do
300
+ subscriber = nil
301
+ with_server do
302
+ subscriber = self.new_client(:client_id => "broadcast_all") { |c| c.subscribe %w(master) }
303
+ self.new_client { |c| c.broadcast_to_channels %w(), body }
304
+ end
305
+ assert_body body, subscriber
306
+ end
307
+
308
+ end
309
+
310
+ context "broadcast to a client" do
311
+
312
+ body = "This is a client-specific broadcast test!"
313
+
314
+ should "be received by the target client" do
315
+ subscriber = nil
316
+ with_server do
317
+ subscriber = self.new_client(:client_id => "broadcast_client") { |c| c.subscribe %w() }
318
+ self.new_client { |c| c.broadcast_to_clients %w(broadcast_client), body }
319
+ end
320
+ assert_body body, subscriber
321
+ end
322
+
323
+ should "not be received by other clients" do
324
+ subscriber = nil
325
+ with_server do
326
+ subscriber = self.new_client(:client_id => "broadcast_faker") { |c| c.subscribe %w() }
327
+ self.new_client { |c| c.broadcast_to_clients %w(broadcast_client), body }
328
+ end
329
+ assert_no_response subscriber
330
+ end
331
+
332
+ should "be saved until the client reconnects" do
333
+ subscriber = nil
334
+ with_server :store_messages => true do
335
+ self.new_client(:client_id => "broadcast_client") { |c| c.subscribe %w() }.close
336
+ self.new_client { |c| c.broadcast_to_clients %w(broadcast_client), body }
337
+ subscriber = self.new_client(:client_id => "broadcast_client") { |c| c.subscribe %w() }
338
+ end
339
+ assert_body body, subscriber
340
+ end
341
+
342
+ should "only be sent to new client connection" do
343
+ old_subscriber = nil
344
+ new_subscriber = nil
345
+
346
+ with_server :store_messages => true, :timeout => 30 do
347
+ old_subscriber = self.new_client(:client_id => "broadcast_client", :session_id => "1") { |c| c.subscribe %w() }
348
+ self.new_client { |c| c.broadcast_to_clients %w(broadcast_client), body }
349
+ @connections.first.client.expects(:send_message_to_connection).times(2)
350
+ new_subscriber = self.new_client(:client_id => "broadcast_client", :session_id => "2") { |c| c.subscribe %w() }
351
+ end
352
+ end
353
+
354
+ end
355
+
356
+ context "querying client list" do
357
+
358
+ should "return all clients" do
359
+ subscriber = nil
360
+ with_server do
361
+ self.new_client(:client_id => "alex") { |c| c.subscribe %w() }
362
+ self.new_client(:client_id => "bob") { |c| c.subscribe %w() }
363
+ subscriber = self.new_client(:client_id => "cindy") { |c| c.subscribe %w(); c.query_show_clients }
364
+ end
365
+ assert_not_nil subscriber
366
+ result = subscriber.receive
367
+ assert_not_nil result
368
+ assert_equal 3, result.size
369
+ assert_same_elements %w(alex bob cindy), result.collect { |r| r["client_id"] }
370
+ end
371
+
372
+ should "not include disconnected clients" do
373
+ subscriber = nil
374
+ with_server(:timeout => 0) do
375
+ self.new_client(:client_id => "sandra") { |c| c.subscribe %w() }
376
+ self.new_client(:client_id => "tom") { |c| c.subscribe %w() }.close
377
+ subscriber = self.new_client(:client_id => "vivian") { |c| c.subscribe %w(); c.query_show_clients }
378
+ end
379
+ assert_not_nil subscriber
380
+ result = subscriber.receive
381
+ assert_not_nil result
382
+ assert_equal 2, result.size
383
+ assert_same_elements %w(sandra vivian), result.collect { |r| r["client_id"] }
384
+ end
385
+
386
+ should "only return requested clients" do
387
+ subscriber = nil
388
+ with_server do
389
+ self.new_client(:client_id => "dixie") { |c| c.subscribe %w() }
390
+ self.new_client(:client_id => "eamon") { |c| c.subscribe %w() }
391
+ self.new_client(:client_id => "fanny") { |c| c.subscribe %w() }
392
+ subscriber = self.new_client(:client_id => "zelda") { |c| c.subscribe %w(); c.query_show_clients %w(dixie fanny) }
393
+ end
394
+ assert_not_nil subscriber
395
+ result = subscriber.receive
396
+ assert_not_nil result
397
+ assert_equal 2, result.size
398
+ assert_same_elements %w(dixie fanny), result.collect { |r| r["client_id"] }
399
+ end
400
+
401
+ should "never return non-existant clients even when requested" do
402
+ subscriber = nil
403
+ with_server do
404
+ self.new_client(:client_id => "dixie") { |c| c.subscribe %w() }
405
+ self.new_client(:client_id => "eamon") { |c| c.subscribe %w() }
406
+ self.new_client(:client_id => "fanny") { |c| c.subscribe %w() }
407
+ subscriber = self.new_client(:client_id => "zelda") { |c| c.subscribe %w(); c.query_show_clients %w(ginny homer) }
408
+ end
409
+ assert_not_nil subscriber
410
+ result = subscriber.receive
411
+ assert_not_nil result
412
+ assert_equal 0, result.size
413
+ end
414
+
415
+ should "return correct number of active connections" do
416
+ subscriber = nil
417
+ with_server do
418
+ 5.times { self.new_client(:client_id => "homer") { |c| c.subscribe %w() } }
419
+ subscriber = self.new_client(:client_id => "zelda") { |c| c.subscribe %w(); c.query_show_clients %w(homer) }
420
+ end
421
+ assert_not_nil subscriber
422
+ result = subscriber.receive
423
+ assert_not_nil result
424
+ assert_equal 1, result.size
425
+ assert_equal 5, result.first["num_connections"]
426
+ end
427
+
428
+ should "be equivalent when querying one client" do
429
+ s1, s2 = nil
430
+ with_server do
431
+ 5.times { self.new_client(:client_id => "homer") { |c| c.subscribe %w() } }
432
+ s1 = self.new_client(:client_id => "zelda") { |c| c.subscribe %w(); c.query_show_client "homer" }
433
+ s2 = self.new_client(:client_id => "zelda") { |c| c.subscribe %w(); c.query_show_clients %w(homer) }
434
+ end
435
+ assert_not_nil s1
436
+ assert_not_nil s2
437
+ r1 = s1.receive
438
+ assert_not_nil r1
439
+ r2 = s2.receive
440
+ assert_not_nil r2
441
+ assert_equal 1, r2.size
442
+ assert_equal r1, r2.first
443
+ end
444
+
445
+ should "only return clients in specific channels" do
446
+ subscriber = nil
447
+ with_server do
448
+ self.new_client(:client_id => "alexa") { |c| c.subscribe %w(master slave zoo) }
449
+ self.new_client(:client_id => "bobby") { |c| c.subscribe %w(master slave) }
450
+ self.new_client(:client_id => "cindy") { |c| c.subscribe %w(master zoo) }
451
+ self.new_client(:client_id => "dixon") { |c| c.subscribe %w(slave zoo) }
452
+ self.new_client(:client_id => "eamon") { |c| c.subscribe %w(slave) }
453
+ self.new_client(:client_id => "flack") { |c| c.subscribe %w(decoy slave) }
454
+ subscriber = self.new_client(:client_id => "geoff") { |c| c.subscribe %w(zoo); c.query_show_clients_for_channels %w(master zoo) }
455
+ end
456
+ assert_response subscriber do |result|
457
+ assert_equal 5, result.size
458
+ assert_same_elements %w(alexa bobby cindy dixon geoff), result.collect { |r| r["client_id"] }
459
+ end
460
+ end
461
+
462
+ end
463
+
464
+ context "upon processing an invalid command" do
465
+
466
+ should "disconnect immediately" do
467
+ subscriber = nil
468
+ with_server do
469
+ subscriber = self.new_client(:client_id => "pinocchio") { |c| c.transmit :command => :some_undefined_command; c.subscribe %w(); c }
470
+ end
471
+ assert_server_disconnected subscriber
472
+ end
473
+
474
+ end
475
+
476
+ %w(broadcast subscribe query).each do |type|
477
+
478
+ context "upon receiving malformed #{type}" do
479
+
480
+ should "disconnect immediately" do
481
+ subscriber = nil
482
+ with_server do
483
+ subscriber = self.new_client(:client_id => "pinocchio") { |c| c.transmit :command => type, :type => :unknown; c.subscribe %w(); c }
484
+ end
485
+ assert_server_disconnected subscriber
486
+ end
487
+
488
+ end
489
+
490
+ end
491
+
492
+ context "upon receiving invalid JSON" do
493
+
494
+ should "disconnect immediately" do
495
+ subscriber = nil
496
+ with_server do
497
+ subscriber = self.new_client(:client_id => "pinocchio") { |c| c.send_raw "invalid json..."; c }
498
+ end
499
+ assert_server_disconnected subscriber
500
+ end
501
+
502
+ end
503
+
504
+ context "crossdomain file request" do
505
+
506
+ should "return contents of crossdomain file" do
507
+ subscriber = nil
508
+ with_server do
509
+ subscriber = self.new_client(:client_id => "pinocchio") { |c| c.request_crossdomain_file }
510
+ end
511
+ assert_raw_response subscriber, <<-EOF
512
+ <cross-domain-policy>
513
+ <allow-access-from domain="*" to-ports="#{OPTIONS[:port]}" />
514
+ </cross-domain-policy>
515
+ EOF
516
+ end
517
+
518
+ end
519
+
520
+ context "querying channel list" do
521
+
522
+ should "return channel list" do
523
+ subscribe = nil
524
+ with_server do
525
+ self.new_client(:client_id => "homer") { |c| c.subscribe %w(groupie master slave1 slave2) }
526
+ self.new_client(:client_id => "marge") { |c| c.subscribe %w(master slave1 slave2) }
527
+ subscribe = self.new_client(:client_id => "pinocchio") { |c|
528
+ c.subscribe %w(master slave1)
529
+ c.query_show_channels_for_client "marge"
530
+ }
531
+ end
532
+ assert_response subscribe do |result|
533
+ assert_equal 3, result.size
534
+ assert_same_elements %w(master slave1 slave2), result
535
+ end
536
+ end
537
+
538
+ end
539
+
540
+ context "remove channel request" do
541
+
542
+ should "work on all clients when requested" do
543
+ with_server do
544
+ self.new_client(:client_id => "homer") { |c| c.subscribe %w(groupie master slave1 slave2) }
545
+ self.new_client(:client_id => "marge") { |c| c.subscribe %w(master slave1 slave2) }
546
+ self.new_client(:client_id => "pinocchio") { |c|
547
+ c.subscribe %w(master slave1 slave2)
548
+ c.query_remove_channels_from_all_clients %w(slave1 slave2)
549
+ }
550
+ end
551
+ @connections.each do |connection|
552
+ assert_does_not_contain connection.channels, /slave/
553
+ end
554
+ end
555
+
556
+ should "work on specific clients when requested" do
557
+ with_server do
558
+ self.new_client(:client_id => "homer") { |c| c.subscribe %w(groupie master slave1 slave2) }
559
+ self.new_client(:client_id => "marge") { |c| c.subscribe %w(master slave1 slave2) }
560
+ self.new_client(:client_id => "pinocchio") { |c|
561
+ c.subscribe %w(master slave1 slave2)
562
+ c.query_remove_channels_from_client %w(slave1 slave2), %w(homer)
563
+ }
564
+ end
565
+ assert_does_not_contain @connections.find { |c| c.instance_eval("@request[:client_id]") == "homer" }.channels, /slave/
566
+ assert_contains @connections.find { |c| c.instance_eval("@request[:client_id]") == "marge" }.channels, /slave/
567
+ end
568
+
569
+ end
570
+
571
+ end
572
+
573
+ end