juggernaut_rails 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ # You should list any juggernaut hosts here.
2
+ # You need only specify the secret key if you're using that type of authentication (see juggernaut.yml)
3
+ #
4
+ # Name: Mapping:
5
+ # :port internal push server's port
6
+ # :host internal push server's host/ip
7
+ # :public_host public push server's host/ip (accessible from external clients)
8
+ # :public_port public push server's port
9
+ # :secret_key (optional) shared secret (should map to the key specified in the push server's config)
10
+ # :environment (optional) limit host to a particular RAILS_ENV
11
+
12
+ :hosts:
13
+ - :port: 5001
14
+ :host: 127.0.0.1
15
+ :public_host: 127.0.0.1
16
+ :public_port: 5001
17
+ # :secret_key: your_secret_key
18
+ # :environment: :development
File without changes
@@ -0,0 +1,5 @@
1
+ /* SWFObject v2.0 <http://code.google.com/p/swfobject/>
2
+ Copyright (c) 2007 Geoff Stearns, Michael Williams, and Bobby van der Sluis
3
+ This software is released under the MIT License <http://www.opensource.org/licenses/mit-license.php>
4
+ */
5
+ var swfobject=function(){var Z="undefined",P="object",B="Shockwave Flash",h="ShockwaveFlash.ShockwaveFlash",W="application/x-shockwave-flash",K="SWFObjectExprInst",G=window,g=document,N=navigator,f=[],H=[],Q=null,L=null,T=null,S=false,C=false;var a=function(){var l=typeof g.getElementById!=Z&&typeof g.getElementsByTagName!=Z&&typeof g.createElement!=Z&&typeof g.appendChild!=Z&&typeof g.replaceChild!=Z&&typeof g.removeChild!=Z&&typeof g.cloneNode!=Z,t=[0,0,0],n=null;if(typeof N.plugins!=Z&&typeof N.plugins[B]==P){n=N.plugins[B].description;if(n){n=n.replace(/^.*\s+(\S+\s+\S+$)/,"$1");t[0]=parseInt(n.replace(/^(.*)\..*$/,"$1"),10);t[1]=parseInt(n.replace(/^.*\.(.*)\s.*$/,"$1"),10);t[2]=/r/.test(n)?parseInt(n.replace(/^.*r(.*)$/,"$1"),10):0}}else{if(typeof G.ActiveXObject!=Z){var o=null,s=false;try{o=new ActiveXObject(h+".7")}catch(k){try{o=new ActiveXObject(h+".6");t=[6,0,21];o.AllowScriptAccess="always"}catch(k){if(t[0]==6){s=true}}if(!s){try{o=new ActiveXObject(h)}catch(k){}}}if(!s&&o){try{n=o.GetVariable("$version");if(n){n=n.split(" ")[1].split(",");t=[parseInt(n[0],10),parseInt(n[1],10),parseInt(n[2],10)]}}catch(k){}}}}var v=N.userAgent.toLowerCase(),j=N.platform.toLowerCase(),r=/webkit/.test(v)?parseFloat(v.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,i=false,q=j?/win/.test(j):/win/.test(v),m=j?/mac/.test(j):/mac/.test(v);/*@cc_on i=true;@if(@_win32)q=true;@elif(@_mac)m=true;@end@*/return{w3cdom:l,pv:t,webkit:r,ie:i,win:q,mac:m}}();var e=function(){if(!a.w3cdom){return }J(I);if(a.ie&&a.win){try{g.write("<script id=__ie_ondomload defer=true src=//:><\/script>");var i=c("__ie_ondomload");if(i){i.onreadystatechange=function(){if(this.readyState=="complete"){this.parentNode.removeChild(this);V()}}}}catch(j){}}if(a.webkit&&typeof g.readyState!=Z){Q=setInterval(function(){if(/loaded|complete/.test(g.readyState)){V()}},10)}if(typeof g.addEventListener!=Z){g.addEventListener("DOMContentLoaded",V,null)}M(V)}();function V(){if(S){return }if(a.ie&&a.win){var m=Y("span");try{var l=g.getElementsByTagName("body")[0].appendChild(m);l.parentNode.removeChild(l)}catch(n){return }}S=true;if(Q){clearInterval(Q);Q=null}var j=f.length;for(var k=0;k<j;k++){f[k]()}}function J(i){if(S){i()}else{f[f.length]=i}}function M(j){if(typeof G.addEventListener!=Z){G.addEventListener("load",j,false)}else{if(typeof g.addEventListener!=Z){g.addEventListener("load",j,false)}else{if(typeof G.attachEvent!=Z){G.attachEvent("onload",j)}else{if(typeof G.onload=="function"){var i=G.onload;G.onload=function(){i();j()}}else{G.onload=j}}}}}function I(){var l=H.length;for(var j=0;j<l;j++){var m=H[j].id;if(a.pv[0]>0){var k=c(m);if(k){H[j].width=k.getAttribute("width")?k.getAttribute("width"):"0";H[j].height=k.getAttribute("height")?k.getAttribute("height"):"0";if(O(H[j].swfVersion)){if(a.webkit&&a.webkit<312){U(k)}X(m,true)}else{if(H[j].expressInstall&&!C&&O("6.0.65")&&(a.win||a.mac)){D(H[j])}else{d(k)}}}}else{X(m,true)}}}function U(m){var k=m.getElementsByTagName(P)[0];if(k){var p=Y("embed"),r=k.attributes;if(r){var o=r.length;for(var n=0;n<o;n++){if(r[n].nodeName.toLowerCase()=="data"){p.setAttribute("src",r[n].nodeValue)}else{p.setAttribute(r[n].nodeName,r[n].nodeValue)}}}var q=k.childNodes;if(q){var s=q.length;for(var l=0;l<s;l++){if(q[l].nodeType==1&&q[l].nodeName.toLowerCase()=="param"){p.setAttribute(q[l].getAttribute("name"),q[l].getAttribute("value"))}}}m.parentNode.replaceChild(p,m)}}function F(i){if(a.ie&&a.win&&O("8.0.0")){G.attachEvent("onunload",function(){var k=c(i);if(k){for(var j in k){if(typeof k[j]=="function"){k[j]=function(){}}}k.parentNode.removeChild(k)}})}}function D(j){C=true;var o=c(j.id);if(o){if(j.altContentId){var l=c(j.altContentId);if(l){L=l;T=j.altContentId}}else{L=b(o)}if(!(/%$/.test(j.width))&&parseInt(j.width,10)<310){j.width="310"}if(!(/%$/.test(j.height))&&parseInt(j.height,10)<137){j.height="137"}g.title=g.title.slice(0,47)+" - Flash Player Installation";var n=a.ie&&a.win?"ActiveX":"PlugIn",k=g.title,m="MMredirectURL="+G.location+"&MMplayerType="+n+"&MMdoctitle="+k,p=j.id;if(a.ie&&a.win&&o.readyState!=4){var i=Y("div");p+="SWFObjectNew";i.setAttribute("id",p);o.parentNode.insertBefore(i,o);o.style.display="none";G.attachEvent("onload",function(){o.parentNode.removeChild(o)})}R({data:j.expressInstall,id:K,width:j.width,height:j.height},{flashvars:m},p)}}function d(j){if(a.ie&&a.win&&j.readyState!=4){var i=Y("div");j.parentNode.insertBefore(i,j);i.parentNode.replaceChild(b(j),i);j.style.display="none";G.attachEvent("onload",function(){j.parentNode.removeChild(j)})}else{j.parentNode.replaceChild(b(j),j)}}function b(n){var m=Y("div");if(a.win&&a.ie){m.innerHTML=n.innerHTML}else{var k=n.getElementsByTagName(P)[0];if(k){var o=k.childNodes;if(o){var j=o.length;for(var l=0;l<j;l++){if(!(o[l].nodeType==1&&o[l].nodeName.toLowerCase()=="param")&&!(o[l].nodeType==8)){m.appendChild(o[l].cloneNode(true))}}}}}return m}function R(AE,AC,q){var p,t=c(q);if(typeof AE.id==Z){AE.id=q}if(a.ie&&a.win){var AD="";for(var z in AE){if(AE[z]!=Object.prototype[z]){if(z=="data"){AC.movie=AE[z]}else{if(z.toLowerCase()=="styleclass"){AD+=' class="'+AE[z]+'"'}else{if(z!="classid"){AD+=" "+z+'="'+AE[z]+'"'}}}}}var AB="";for(var y in AC){if(AC[y]!=Object.prototype[y]){AB+='<param name="'+y+'" value="'+AC[y]+'" />'}}t.outerHTML='<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"'+AD+">"+AB+"</object>";F(AE.id);p=c(AE.id)}else{if(a.webkit&&a.webkit<312){var AA=Y("embed");AA.setAttribute("type",W);for(var x in AE){if(AE[x]!=Object.prototype[x]){if(x=="data"){AA.setAttribute("src",AE[x])}else{if(x.toLowerCase()=="styleclass"){AA.setAttribute("class",AE[x])}else{if(x!="classid"){AA.setAttribute(x,AE[x])}}}}}for(var w in AC){if(AC[w]!=Object.prototype[w]){if(w!="movie"){AA.setAttribute(w,AC[w])}}}t.parentNode.replaceChild(AA,t);p=AA}else{var s=Y(P);s.setAttribute("type",W);for(var v in AE){if(AE[v]!=Object.prototype[v]){if(v.toLowerCase()=="styleclass"){s.setAttribute("class",AE[v])}else{if(v!="classid"){s.setAttribute(v,AE[v])}}}}for(var u in AC){if(AC[u]!=Object.prototype[u]&&u!="movie"){E(s,u,AC[u])}}t.parentNode.replaceChild(s,t);p=s}}return p}function E(k,i,j){var l=Y("param");l.setAttribute("name",i);l.setAttribute("value",j);k.appendChild(l)}function c(i){return g.getElementById(i)}function Y(i){return g.createElement(i)}function O(k){var j=a.pv,i=k.split(".");i[0]=parseInt(i[0],10);i[1]=parseInt(i[1],10);i[2]=parseInt(i[2],10);return(j[0]>i[0]||(j[0]==i[0]&&j[1]>i[1])||(j[0]==i[0]&&j[1]==i[1]&&j[2]>=i[2]))?true:false}function A(m,j){if(a.ie&&a.mac){return }var l=g.getElementsByTagName("head")[0],k=Y("style");k.setAttribute("type","text/css");k.setAttribute("media","screen");if(!(a.ie&&a.win)&&typeof g.createTextNode!=Z){k.appendChild(g.createTextNode(m+" {"+j+"}"))}l.appendChild(k);if(a.ie&&a.win&&typeof g.styleSheets!=Z&&g.styleSheets.length>0){var i=g.styleSheets[g.styleSheets.length-1];if(typeof i.addRule==P){i.addRule(m,j)}}}function X(k,i){var j=i?"visible":"hidden";if(S){c(k).style.visibility=j}else{A("#"+k,"visibility:"+j)}}return{registerObject:function(l,i,k){if(!a.w3cdom||!l||!i){return }var j={};j.id=l;j.swfVersion=i;j.expressInstall=k?k:false;H[H.length]=j;X(l,false)},getObjectById:function(l){var i=null;if(a.w3cdom&&S){var j=c(l);if(j){var k=j.getElementsByTagName(P)[0];if(!k||(k&&typeof j.SetVariable!=Z)){i=j}else{if(typeof k.SetVariable!=Z){i=k}}}}return i},embedSWF:function(n,u,r,t,j,m,k,p,s){if(!a.w3cdom||!n||!u||!r||!t||!j){return }r+="";t+="";if(O(j)){X(u,false);var q=(typeof s==P)?s:{};q.data=n;q.width=r;q.height=t;var o=(typeof p==P)?p:{};if(typeof k==P){for(var l in k){if(k[l]!=Object.prototype[l]){if(typeof o.flashvars!=Z){o.flashvars+="&"+l+"="+k[l]}else{o.flashvars=l+"="+k[l]}}}}J(function(){R(q,o,u);if(q.id==u){X(u,true)}})}else{if(m&&!C&&O("6.0.65")&&(a.win||a.mac)){X(u,false);J(function(){var i={};i.id=i.altContentId=u;i.width=r;i.height=t;i.expressInstall=m;D(i)})}}},getFlashPlayerVersion:function(){return{major:a.pv[0],minor:a.pv[1],release:a.pv[2]}},hasFlashPlayerVersion:O,createSWF:function(k,j,i){if(a.w3cdom&&S){return R(k,j,i)}else{return undefined}},createCSS:function(j,i){if(a.w3cdom){A(j,i)}},addDomLoadEvent:J,addLoadEvent:M,getQueryParamValue:function(m){var l=g.location.search||g.location.hash;if(m==null){return l}if(l){var k=l.substring(1).split("&");for(var j=0;j<k.length;j++){if(k[j].substring(0,k[j].indexOf("="))==m){return k[j].substring((k[j].indexOf("=")+1))}}}return""},expressInstallCallback:function(){if(C&&L){var i=c(K);if(i){i.parentNode.replaceChild(L,i);if(T){X(T,true);if(a.ie&&a.win){L.style.display="block"}}L=null;T=null;C=false}}}}}();
data/rails/init.rb ADDED
@@ -0,0 +1,21 @@
1
+ require File.join(File.dirname(__FILE__), '../lib/juggernaut')
2
+ require File.join(File.dirname(__FILE__), '../lib/juggernaut/rails')
3
+ require File.join(File.dirname(__FILE__), '../lib/juggernaut/rails/helpers')
4
+
5
+ Juggernaut.send(:include, Juggernaut::Rails::ConvenienceMethods)
6
+ ActionView::Base.send(:include, Juggernaut::Rails::Helpers)
7
+
8
+ ActionView::Helpers::AssetTagHelper.register_javascript_expansion :juggernaut => ['juggernaut/swfobject', 'juggernaut/juggernaut']
9
+ ActionView::Helpers::AssetTagHelper.register_javascript_expansion :juggernaut_jquery => ['juggernaut/json', 'juggernaut/juggernaut', 'juggernaut/jquerynaut', 'juggernaut/swfobject']
10
+
11
+ ActionController::Base.class_eval do
12
+ alias_method :render_without_juggernaut, :render
13
+ include Juggernaut::Rails::RenderExtension
14
+ alias_method :render, :render_with_juggernaut
15
+ end
16
+
17
+ ActionView::Base.class_eval do
18
+ alias_method :render_without_juggernaut, :render
19
+ include Juggernaut::Rails::RenderExtension
20
+ alias_method :render, :render_with_juggernaut
21
+ end
@@ -0,0 +1,176 @@
1
+
2
+ $:.unshift "../lib"
3
+ require "juggernaut"
4
+ require "test/unit"
5
+ require "shoulda"
6
+ require "mocha"
7
+
8
+ class TestClient < Test::Unit::TestCase
9
+
10
+ CONFIG = File.join(File.dirname(__FILE__), "files", "juggernaut.yml")
11
+
12
+ class DummySubscriber; end
13
+
14
+ EXAMPLE_URL = "http://localhost:5000/callbacks/example"
15
+ SECURE_URL = "https://localhost:5000/callbacks/example"
16
+
17
+ context "Client" do
18
+
19
+ setup do
20
+ Juggernaut.options = {
21
+ :logout_connection_url => "http://localhost:5000/callbacks/logout_connection",
22
+ :logout_url => "http://localhost:5000/callbacks/logout",
23
+ :subscription_url => "http://localhost:5000/callbacks/subscription"
24
+ }
25
+ @s1 = DummySubscriber.new
26
+ @request = {
27
+ :client_id => "jonny",
28
+ :session_id => rand(1_000_000).to_s(16)
29
+ }
30
+ @client = Juggernaut::Client.find_or_create(@s1, @request)
31
+ end
32
+
33
+ teardown do
34
+ Juggernaut::Client.reset!
35
+ end
36
+
37
+ should "have correct JSON representation" do
38
+ assert_nothing_raised do
39
+ json = {
40
+ "client_id" => "jonny",
41
+ "num_connections" => 1,
42
+ "session_id" => @request[:session_id]
43
+ }
44
+ assert_equal json, JSON.parse(@client.to_json)
45
+ end
46
+ end
47
+
48
+ should "return the client based on subscriber's signature" do
49
+ @s1.stubs(:signature).returns("012345")
50
+ assert_equal @client, Juggernaut::Client.find_by_signature("012345")
51
+ end
52
+
53
+ should "return the client based on client ID and channel list" do
54
+ @client.stubs(:has_channels?).with(%w(a couple of channels)).returns(true)
55
+ assert_equal @client, Juggernaut::Client.find_by_id_and_channels("jonny", %w(a couple of channels))
56
+ assert_nil Juggernaut::Client.find_by_id_and_channels("peter", %w(a couple of channels))
57
+ @client.stubs(:has_channels?).with(%w(something else)).returns(false)
58
+ assert_nil Juggernaut::Client.find_by_id_and_channels("jonny", %w(something else))
59
+ end
60
+
61
+ should "automatically be registered, and can unregister" do
62
+ assert @client.send(:registered?)
63
+ assert_equal @client, @client.send(:unregister)
64
+ assert_equal false, @client.send(:registered?)
65
+ end
66
+
67
+ should "be alive if at least one subscriber is alive" do
68
+ @s1.stubs(:alive?).returns(true)
69
+ @s2 = DummySubscriber.new
70
+ @client.add_new_connection(@s2)
71
+ @s2.stubs(:alive?).returns(false)
72
+ assert @client.alive?
73
+ end
74
+
75
+ should "not be alive if no subscriber is alive" do
76
+ @s1.stubs(:alive?).returns(false)
77
+ @s2 = DummySubscriber.new
78
+ @client.add_new_connection(@s2)
79
+ @s2.stubs(:alive?).returns(false)
80
+ assert_equal false, @client.alive?
81
+ end
82
+
83
+ should "not give up if within the timeout period" do
84
+ Juggernaut.options[:timeout] = 10
85
+ @s1.stubs(:alive?).returns(false)
86
+ @client.send(:reset_logout_timeout!)
87
+ assert_equal false, @client.give_up?
88
+ end
89
+
90
+ should "not give up if at least one subscriber is alive" do
91
+ Juggernaut.options[:timeout] = 0
92
+ @s1.stubs(:alive?).returns(true)
93
+ @client.send(:reset_logout_timeout!)
94
+ assert_equal false, @client.give_up?
95
+ end
96
+
97
+ should "send logouts after timeout" do
98
+ Juggernaut.options[:timeout] = 0
99
+ @s1.stubs(:alive?).returns(false)
100
+ @client.send(:reset_logout_timeout!)
101
+ @client.expects(:logout_request).once
102
+ Juggernaut::Client.send_logouts_after_timeout
103
+ end
104
+
105
+ %w(subscription logout_connection).each do |type|
106
+
107
+ context "#{type} request" do
108
+
109
+ should "post to the correct URL" do
110
+ @client.expects(:post_request).with(Juggernaut.options[:"#{type}_url"], %w(master), :timeout => 5).returns(true)
111
+ assert_equal true, @client.send("#{type}_request", %w(master))
112
+ end
113
+
114
+ should "not raise exceptions if posting raises an exception" do
115
+ @client.expects(:post_request).with(Juggernaut.options[:"#{type}_url"], %w(master), :timeout => 5).returns(false)
116
+ assert_nothing_raised {
117
+ assert_equal false, @client.send("#{type}_request", %w(master))
118
+ }
119
+ end
120
+
121
+ end
122
+
123
+ end
124
+
125
+ context "post to URL" do
126
+
127
+ should "return true when successful" do
128
+ Net::HTTP.any_instance.
129
+ expects(:post).
130
+ with("/callbacks/example", "client_id=jonny&session_id=#{@request[:session_id]}&channels[]=master&channels[]=slave", {"User-Agent" => "Ruby/#{RUBY_VERSION}"}).
131
+ returns([Net::HTTPOK.new('1.1', '200', 'OK'), ''])
132
+ assert_equal true, @client.send(:post_request, EXAMPLE_URL, %w(master slave))
133
+ end
134
+
135
+ should "return false on an internal server error" do
136
+ Net::HTTP.any_instance.expects(:post).returns([Net::HTTPInternalServerError.new('1.1', '500', 'Internal Server Error'), ''])
137
+ assert_equal false, @client.send(:post_request, EXAMPLE_URL, %w(master slave))
138
+ end
139
+
140
+ should "return false when a runtime error is caught" do
141
+ Net::HTTP.any_instance.expects(:post).raises(RuntimeError)
142
+ assert_equal false, @client.send(:post_request, EXAMPLE_URL, %w(master slave))
143
+ end
144
+
145
+ should "return false when callback times out" do
146
+ Net::HTTP.any_instance.expects(:post).raises(Timeout::Error)
147
+ assert_equal false, @client.send(:post_request, EXAMPLE_URL, %w(master slave))
148
+ end
149
+
150
+ context "using a secure URL" do
151
+
152
+ should "return true when successful" do
153
+ Net::HTTP.any_instance.expects(:post).returns([Net::HTTPOK.new('1.1', '200', 'OK'), ''])
154
+ Net::HTTP.any_instance.expects(:use_ssl=).with(true).returns(true)
155
+ assert_equal true, @client.send(:post_request, SECURE_URL, %w(master slave))
156
+ end
157
+
158
+ end
159
+
160
+ end
161
+
162
+ context "channel list" do
163
+
164
+ should "be the unique list of all channels in the subscribers" do
165
+ @s1.stubs(:channels).returns(%w(master slave1))
166
+ @s2 = DummySubscriber.new
167
+ @s2.stubs(:channels).returns(%w(master slave2))
168
+ @client.add_new_connection(@s2)
169
+ assert_same_elements %w(master slave1 slave2), @client.channels
170
+ end
171
+
172
+ end
173
+
174
+ end
175
+
176
+ end
@@ -0,0 +1,34 @@
1
+
2
+ $:.unshift "../lib"
3
+ require "juggernaut"
4
+ require "test/unit"
5
+ require "shoulda"
6
+ require "mocha"
7
+
8
+ class TestJuggernaut < Test::Unit::TestCase
9
+
10
+ context "Juggernaut" do
11
+
12
+ setup do
13
+ Juggernaut.options = { }
14
+ end
15
+
16
+ should "set options correctly" do
17
+ options = {
18
+ :host => "0.0.0.0",
19
+ :port => 5001,
20
+ :debug => false
21
+ }
22
+ Juggernaut.options = options
23
+ assert_equal options, Juggernaut.options
24
+ end
25
+
26
+ should "have a debug logger by default" do
27
+ log = Juggernaut.logger
28
+ assert_not_nil log
29
+ assert_equal Logger::DEBUG, log.level
30
+ end
31
+
32
+ end
33
+
34
+ end
@@ -0,0 +1,26 @@
1
+
2
+ $:.unshift "../lib"
3
+ require "juggernaut"
4
+ require "test/unit"
5
+ require "shoulda"
6
+ require "mocha"
7
+
8
+ class TestMessage < Test::Unit::TestCase
9
+
10
+ context "Message" do
11
+
12
+ setup do
13
+ @msg = Juggernaut::Message.new(1, "A pre-determined message body", "a81fef13919")
14
+ assert_not_nil @msg
15
+ end
16
+
17
+ should "generate valid JSON" do
18
+ obj = {"signature" => "a81fef13919", "body" => "A pre-determined message body", "id" => "1"}
19
+ assert_nothing_raised do
20
+ assert_equal obj, JSON.parse(@msg.to_s)
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ end
@@ -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