_bushido-faye 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/History.txt +247 -0
  2. data/README.rdoc +92 -0
  3. data/lib/faye-browser-min.js +1 -0
  4. data/lib/faye.rb +121 -0
  5. data/lib/faye/adapters/rack_adapter.rb +210 -0
  6. data/lib/faye/engines/connection.rb +60 -0
  7. data/lib/faye/engines/memory.rb +112 -0
  8. data/lib/faye/engines/proxy.rb +111 -0
  9. data/lib/faye/error.rb +49 -0
  10. data/lib/faye/mixins/logging.rb +47 -0
  11. data/lib/faye/mixins/publisher.rb +30 -0
  12. data/lib/faye/mixins/timeouts.rb +22 -0
  13. data/lib/faye/protocol/channel.rb +124 -0
  14. data/lib/faye/protocol/client.rb +378 -0
  15. data/lib/faye/protocol/extensible.rb +43 -0
  16. data/lib/faye/protocol/grammar.rb +58 -0
  17. data/lib/faye/protocol/publication.rb +5 -0
  18. data/lib/faye/protocol/server.rb +282 -0
  19. data/lib/faye/protocol/subscription.rb +24 -0
  20. data/lib/faye/transport/http.rb +76 -0
  21. data/lib/faye/transport/local.rb +22 -0
  22. data/lib/faye/transport/transport.rb +115 -0
  23. data/lib/faye/transport/web_socket.rb +99 -0
  24. data/lib/faye/util/namespace.rb +20 -0
  25. data/spec/browser.html +45 -0
  26. data/spec/encoding_helper.rb +7 -0
  27. data/spec/install.sh +78 -0
  28. data/spec/javascript/channel_spec.js +15 -0
  29. data/spec/javascript/client_spec.js +714 -0
  30. data/spec/javascript/engine/memory_spec.js +7 -0
  31. data/spec/javascript/engine_spec.js +417 -0
  32. data/spec/javascript/faye_spec.js +15 -0
  33. data/spec/javascript/grammar_spec.js +66 -0
  34. data/spec/javascript/node_adapter_spec.js +307 -0
  35. data/spec/javascript/publisher_spec.js +27 -0
  36. data/spec/javascript/server/connect_spec.js +168 -0
  37. data/spec/javascript/server/disconnect_spec.js +121 -0
  38. data/spec/javascript/server/extensions_spec.js +60 -0
  39. data/spec/javascript/server/handshake_spec.js +145 -0
  40. data/spec/javascript/server/integration_spec.js +124 -0
  41. data/spec/javascript/server/publish_spec.js +85 -0
  42. data/spec/javascript/server/subscribe_spec.js +247 -0
  43. data/spec/javascript/server/unsubscribe_spec.js +245 -0
  44. data/spec/javascript/server_spec.js +110 -0
  45. data/spec/javascript/transport_spec.js +130 -0
  46. data/spec/node.js +55 -0
  47. data/spec/phantom.js +17 -0
  48. data/spec/ruby/channel_spec.rb +17 -0
  49. data/spec/ruby/client_spec.rb +724 -0
  50. data/spec/ruby/engine/memory_spec.rb +7 -0
  51. data/spec/ruby/engine_examples.rb +427 -0
  52. data/spec/ruby/faye_spec.rb +14 -0
  53. data/spec/ruby/grammar_spec.rb +68 -0
  54. data/spec/ruby/publisher_spec.rb +27 -0
  55. data/spec/ruby/rack_adapter_spec.rb +236 -0
  56. data/spec/ruby/server/connect_spec.rb +170 -0
  57. data/spec/ruby/server/disconnect_spec.rb +120 -0
  58. data/spec/ruby/server/extensions_spec.rb +68 -0
  59. data/spec/ruby/server/handshake_spec.rb +143 -0
  60. data/spec/ruby/server/integration_spec.rb +126 -0
  61. data/spec/ruby/server/publish_spec.rb +81 -0
  62. data/spec/ruby/server/subscribe_spec.rb +247 -0
  63. data/spec/ruby/server/unsubscribe_spec.rb +247 -0
  64. data/spec/ruby/server_spec.rb +110 -0
  65. data/spec/ruby/transport_spec.rb +134 -0
  66. data/spec/spec_helper.rb +11 -0
  67. data/spec/testswarm +29 -0
  68. data/spec/thin_proxy.rb +37 -0
  69. metadata +302 -0
@@ -0,0 +1,110 @@
1
+ JS.ENV.ServerSpec = JS.Test.describe("Server", function() { with(this) {
2
+ before(function() { with(this) {
3
+ this.engine = {}
4
+ stub(Faye.Engine, "get").returns(engine)
5
+ this.server = new Faye.Server()
6
+ }})
7
+
8
+ describe("#process", function() { with(this) {
9
+ before(function() { with(this) {
10
+ this.handshake = {channel: "/meta/handshake", data: "handshake"}
11
+ this.connect = {channel: "/meta/connect", data: "connect"}
12
+ this.disconnect = {channel: "/meta/disconnect", data: "disconnect"}
13
+ this.subscribe = {channel: "/meta/subscribe", data: "subscribe"}
14
+ this.unsubscribe = {channel: "/meta/unsubscribe", data: "unsubscribe"}
15
+ this.publish = {channel: "/some/channel", data: "publish"}
16
+
17
+ stub(engine, "interval", 0)
18
+ stub(engine, "timeout", 60)
19
+ }})
20
+
21
+ it("returns an empty response for no messages", function() { with(this) {
22
+ var response = null
23
+ server.process([], false, function(r) { response = r})
24
+ assertEqual( [], response )
25
+ }})
26
+
27
+ it("ignores invalid messages", function() { with(this) {
28
+ var response = null
29
+ server.process([{}, {channel: "invalid"}], false, function(r) { response = r})
30
+ assertEqual([
31
+ { successful: false,
32
+ error: "405::Invalid channel"
33
+ },
34
+ { channel: "invalid",
35
+ successful: false,
36
+ error: "405:invalid:Invalid channel"
37
+ }
38
+ ], response)
39
+ }})
40
+
41
+ it("routes single messages to appropriate handlers", function() { with(this) {
42
+ expect(server, "handshake").given(handshake, false).yielding([{}])
43
+ server.process(handshake, false, function() {})
44
+ }})
45
+
46
+ it("routes a list of messages to appropriate handlers", function() { with(this) {
47
+ expect(server, "handshake").given(handshake, false).yielding([{}])
48
+ expect(server, "connect").given(connect, false).yielding([{}])
49
+ expect(server, "disconnect").given(disconnect, false).yielding([{}])
50
+ expect(server, "subscribe").given(subscribe, false).yielding([{}])
51
+ expect(server, "unsubscribe").given(unsubscribe, false).yielding([{}])
52
+
53
+ expect(engine, "publish").given(handshake).exactly(0)
54
+ expect(engine, "publish").given(connect).exactly(0)
55
+ expect(engine, "publish").given(disconnect).exactly(0)
56
+ expect(engine, "publish").given(subscribe).exactly(0)
57
+ expect(engine, "publish").given(unsubscribe).exactly(0)
58
+
59
+ expect(engine, "publish").given(publish)
60
+
61
+ server.process([handshake, connect, disconnect, subscribe, unsubscribe, publish], false, function() {})
62
+ }})
63
+
64
+ describe("handshaking", function() { with(this) {
65
+ before(function() { with(this) {
66
+ expect(server, "handshake").given(handshake, false).yielding([{channel: "/meta/handshake", successful: true}])
67
+ }})
68
+
69
+ it("returns the handshake response with advice", function() { with(this) {
70
+ server.process(handshake, false, function(response) {
71
+ assertEqual([
72
+ { channel: "/meta/handshake",
73
+ successful: true,
74
+ advice: {reconnect: "retry", interval: 0, timeout: 60000}
75
+ }
76
+ ], response)
77
+ })
78
+ }})
79
+ }})
80
+
81
+ describe("connecting for messages", function() { with(this) {
82
+ before(function() { with(this) {
83
+ this.messages = [{channel: "/a"}, {channel: "/b"}]
84
+ expect(server, "connect").given(connect, false).yielding([messages])
85
+ }})
86
+
87
+ it("returns the new messages", function() { with(this) {
88
+ server.process(connect, false, function(response) {
89
+ assertEqual( messages, response )
90
+ })
91
+ }})
92
+ }})
93
+ }})
94
+
95
+ describe("#flushConnection", function() { with(this) {
96
+ before(function() { with(this) {
97
+ this.message = {clientId: "fakeclientid"}
98
+ }})
99
+
100
+ it("flushes the connection when given one message", function() { with(this) {
101
+ expect(engine, "flush").given("fakeclientid")
102
+ server.flushConnection(message)
103
+ }})
104
+
105
+ it("flushes the connection when given a list of messages", function() { with(this) {
106
+ expect(engine, "flush").given("fakeclientid")
107
+ server.flushConnection([message])
108
+ }})
109
+ }})
110
+ }})
@@ -0,0 +1,130 @@
1
+ JS.ENV.TransportSpec = JS.Test.describe("Transport", function() { with(this) {
2
+ before(function() { with(this) {
3
+ this.client = {endpoint: "http://example.com/"}
4
+
5
+ if (Faye.Transport.NodeLocal) {
6
+ this.LocalTransport = Faye.Transport.NodeLocal
7
+ this.HttpTransport = Faye.Transport.NodeHttp
8
+ this.inProcess = "in-process"
9
+ this.longPolling = "long-polling"
10
+ } else {
11
+ this.LocalTransport = Faye.Transport.WebSocket
12
+ this.HttpTransport = Faye.Transport.XHR
13
+ this.inProcess = "websocket"
14
+ this.longPolling = "long-polling"
15
+ }
16
+ }})
17
+
18
+ describe("get", function() { with(this) {
19
+ before(function() { with(this) {
20
+ stub(HttpTransport, "isUsable").yields([false])
21
+ stub(LocalTransport, "isUsable").yields([false])
22
+ }})
23
+
24
+ describe("when no transport is usable", function() { with(this) {
25
+ it("raises an exception", function() { with(this) {
26
+ assertThrows(Error, function() { Faye.Transport.get(client, [longPolling, inProcess]) })
27
+ }})
28
+ }})
29
+
30
+ describe("when a less preferred transport is usable", function() { with(this) {
31
+ before(function() { with(this) {
32
+ stub(HttpTransport, "isUsable").yields([true])
33
+ }})
34
+
35
+ it("returns a transport of the usable type", function() { with(this) {
36
+ Faye.Transport.get(client, [longPolling, inProcess], function(transport) {
37
+ assertKindOf( HttpTransport, transport )
38
+ })
39
+ }})
40
+
41
+ it("raises an exception if the usable type is not requested", function() { with(this) {
42
+ assertThrows(Error, function() { Faye.Transport.get(client, [inProcess]) })
43
+ }})
44
+
45
+ it("allows the usable type to be specifically selected", function() { with(this) {
46
+ Faye.Transport.get(client, [longPolling], function(transport) {
47
+ assertKindOf( HttpTransport, transport )
48
+ })
49
+ }})
50
+ }})
51
+
52
+ describe("when all transports are usable", function() { with(this) {
53
+ before(function() { with(this) {
54
+ stub(LocalTransport, "isUsable").yields([true])
55
+ stub(HttpTransport, "isUsable").yields([true])
56
+ }})
57
+
58
+ it("returns the most preferred type", function() { with(this) {
59
+ Faye.Transport.get(client, [longPolling, inProcess], function(transport) {
60
+ assertKindOf( LocalTransport, transport )
61
+ })
62
+ }})
63
+
64
+ it("allows types to be specifically selected", function() { with(this) {
65
+ Faye.Transport.get(client, [inProcess], function(transport) {
66
+ assertKindOf( LocalTransport, transport )
67
+ })
68
+ Faye.Transport.get(client, [longPolling], function(transport) {
69
+ assertKindOf( HttpTransport, transport )
70
+ })
71
+ }})
72
+ }})
73
+ }})
74
+
75
+ describe("send", function() { with(this) {
76
+ include(JS.Test.FakeClock)
77
+ before(function() { this.clock.stub() })
78
+ after(function() { this.clock.reset() })
79
+
80
+ describe("for batching transports", function() { with(this) {
81
+ before(function() { with(this) {
82
+ this.Transport = Faye.Class(Faye.Transport, {batching: true})
83
+ this.transport = new Transport(client)
84
+ }})
85
+
86
+ it("does not make an immediate request", function() { with(this) {
87
+ expect(transport, "request").exactly(0)
88
+ transport.send({batch: "me"}, 60)
89
+ }})
90
+
91
+ it("queues the message to be sent after a timeout", function() { with(this) {
92
+ expect(transport, "request").given([{batch: "me"}], 60)
93
+ transport.send({batch: "me"}, 60)
94
+ clock.tick(10)
95
+ }})
96
+
97
+ it("allows multiple messages to be batched together", function() { with(this) {
98
+ expect(transport, "request").given([{id: 1}, {id: 2}], 60)
99
+ transport.send({id: 1}, 60)
100
+ transport.send({id: 2}, 60)
101
+ clock.tick(10)
102
+ }})
103
+
104
+ it("adds advice to connect messages sent with others", function() { with(this) {
105
+ expect(transport, "request").given([{channel: "/meta/connect", advice: {timeout: 0}}, {}], 60)
106
+ transport.send({channel: "/meta/connect"}, 60)
107
+ transport.send({}, 60)
108
+ clock.tick(10)
109
+ }})
110
+
111
+ it("adds no advice to connect messages sent alone", function() { with(this) {
112
+ expect(transport, "request").given([{channel: "/meta/connect"}], 60)
113
+ transport.send({channel: "/meta/connect"}, 60)
114
+ clock.tick(10)
115
+ }})
116
+ }})
117
+
118
+ describe("for non-batching transports", function() { with(this) {
119
+ before(function() { with(this) {
120
+ this.Transport = Faye.Class(Faye.Transport, {batching: false})
121
+ this.transport = new Transport(client)
122
+ }})
123
+
124
+ it("makes a request immediately", function() { with(this) {
125
+ expect(transport, "request").given([{no: "batch"}], 60)
126
+ transport.send({no: "batch"}, 60)
127
+ }})
128
+ }})
129
+ }})
130
+ }})
data/spec/node.js ADDED
@@ -0,0 +1,55 @@
1
+ require('jsclass')
2
+ Faye = require('../build/faye-node')
3
+ Faye.logger = function() {}
4
+
5
+ JS.Packages(function() { with(this) {
6
+ autoload(/.*Spec/, {from: 'spec/javascript'})
7
+ }})
8
+
9
+ FakeSocket = function() {
10
+ this._fragments = []
11
+ }
12
+ FakeSocket.prototype.write = function(buffer, encoding) {
13
+ this._fragments.push([buffer, encoding])
14
+ }
15
+ FakeSocket.prototype.read = function() {
16
+ var output = []
17
+ this._fragments.forEach(function(buffer, i) {
18
+ for (var j = 0, n = buffer[0].length; j < n; j++)
19
+ output.push(buffer[0][j])
20
+ })
21
+ return output
22
+ }
23
+ FakeSocket.prototype.addListener = function() {}
24
+
25
+ JS.require('Faye', 'JS.Test', 'JS.Range', function() {
26
+ JS.Test.Unit.Assertions.include({
27
+ assertYield: function(expected) {
28
+ var testcase = this
29
+ return function(actual) { testcase.assertEqual(expected, actual) }
30
+ }
31
+ })
32
+
33
+ JS.ENV.Engine = {}
34
+ JS.ENV.Server = {}
35
+
36
+ JS.require( 'FayeSpec',
37
+ 'GrammarSpec',
38
+ 'PublisherSpec',
39
+ 'ChannelSpec',
40
+ 'EngineSpec',
41
+ 'Engine.MemorySpec',
42
+ 'ServerSpec',
43
+ 'Server.HandshakeSpec',
44
+ 'Server.ConnectSpec',
45
+ 'Server.DisconnectSpec',
46
+ 'Server.SubscribeSpec',
47
+ 'Server.UnsubscribeSpec',
48
+ 'Server.PublishSpec',
49
+ 'Server.ExtensionsSpec',
50
+ 'Server.IntegrationSpec',
51
+ 'NodeAdapterSpec',
52
+ 'ClientSpec',
53
+ 'TransportSpec',
54
+ JS.Test.method('autorun'))
55
+ })
data/spec/phantom.js ADDED
@@ -0,0 +1,17 @@
1
+ // This script should be run with PhantomJS
2
+ // http://www.phantomjs.org/
3
+
4
+ var page = new WebPage()
5
+
6
+ page.onConsoleMessage = function(message) {
7
+ try {
8
+ var result = JSON.parse(message)
9
+ if ('total' in result && 'fail' in result) {
10
+ console.log(message)
11
+ var status = (!result.fail && !result.error) ? 0 : 1
12
+ phantom.exit(status)
13
+ }
14
+ } catch (e) {}
15
+ }
16
+
17
+ page.open('spec/browser.html')
@@ -0,0 +1,17 @@
1
+ require "spec_helper"
2
+
3
+ describe Faye::Channel do
4
+ describe :expand do
5
+ it "returns all patterns that match a channel" do
6
+ Faye::Channel.expand("/foo").should == [
7
+ "/**", "/foo", "/*"]
8
+
9
+ Faye::Channel.expand("/foo/bar").should == [
10
+ "/**", "/foo/bar", "/foo/*", "/foo/**"]
11
+
12
+ Faye::Channel.expand("/foo/bar/qux").should == [
13
+ "/**", "/foo/bar/qux", "/foo/bar/*", "/foo/**", "/foo/bar/**"]
14
+ end
15
+ end
16
+ end
17
+
@@ -0,0 +1,724 @@
1
+ require "spec_helper"
2
+
3
+ describe Faye::Client do
4
+ let :transport do
5
+ transport = mock("transport", :cookies= => nil, :headers= => nil)
6
+ transport.stub(:connection_type).and_return "fake"
7
+ transport.stub(:send)
8
+ transport.extend(Faye::Publisher)
9
+ transport
10
+ end
11
+
12
+ before { EM.stub(:add_timer) }
13
+
14
+ def stub_response(response)
15
+ transport.stub(:send) do |message, *args|
16
+ response["id"] = message["id"]
17
+ @client.receive_message(response)
18
+ end
19
+ end
20
+
21
+ def create_client
22
+ Faye::Transport.stub(:get).and_yield(transport)
23
+ @client = Faye::Client.new("http://localhost/")
24
+ end
25
+
26
+ def create_connected_client
27
+ create_client
28
+ stub_response "channel" => "/meta/handshake",
29
+ "successful" => true,
30
+ "version" => "1.0",
31
+ "supportedConnectionTypes" => ["websocket"],
32
+ "clientId" => "fakeid"
33
+
34
+ @client.handshake
35
+ end
36
+
37
+ def subscribe(client, channel, callback = nil)
38
+ stub_response "channel" => "/meta/subscribe",
39
+ "successful" => true,
40
+ "clientId" => "fakeid",
41
+ "subscription" => channel
42
+
43
+ @subs_called = 0
44
+ callback ||= lambda { |m| @subs_called = 1 }
45
+ @client.subscribe(channel, &callback)
46
+ end
47
+
48
+ describe :initialize do
49
+ it "creates a transport the server must support" do
50
+ Faye::Transport.should_receive(:get).with(instance_of(Faye::Client),
51
+ ["long-polling", "callback-polling", "in-process"]).
52
+ and_return(transport)
53
+ Faye::Client.new("http://localhost/")
54
+ end
55
+
56
+ it "puts the client in the UNCONNECTED state" do
57
+ Faye::Transport.stub(:get)
58
+ client = Faye::Client.new("http://localhost/")
59
+ client.state.should == :UNCONNECTED
60
+ end
61
+ end
62
+
63
+ describe :handshake do
64
+ before { create_client }
65
+
66
+ it "sends a handshake message to the server" do
67
+ transport.should_receive(:send).with({
68
+ "channel" => "/meta/handshake",
69
+ "version" => "1.0",
70
+ "supportedConnectionTypes" => ["fake"],
71
+ "id" => instance_of(String)
72
+ }, 60)
73
+ @client.handshake
74
+ end
75
+
76
+ it "puts the client in the CONNECTING state" do
77
+ transport.stub(:send)
78
+ @client.handshake
79
+ @client.state.should == :CONNECTING
80
+ end
81
+
82
+ describe "with an outgoing extension installed" do
83
+ before do
84
+ extension = Class.new do
85
+ def outgoing(message, callback)
86
+ message["ext"] = {"auth" => "password"}
87
+ callback.call(message)
88
+ end
89
+ end
90
+ @client.add_extension(extension.new)
91
+ end
92
+
93
+ it "passes the handshake message through the extension" do
94
+ transport.should_receive(:send).with({
95
+ "channel" => "/meta/handshake",
96
+ "version" => "1.0",
97
+ "supportedConnectionTypes" => ["fake"],
98
+ "id" => instance_of(String),
99
+ "ext" => {"auth" => "password"}
100
+ }, 60)
101
+ @client.handshake
102
+ end
103
+ end
104
+
105
+ describe "on successful response" do
106
+ before do
107
+ stub_response "channel" => "/meta/handshake",
108
+ "successful" => true,
109
+ "version" => "1.0",
110
+ "supportedConnectionTypes" => ["long-polling", "websocket"],
111
+ "clientId" => "fakeid"
112
+ end
113
+
114
+ it "stores the clientId" do
115
+ @client.handshake
116
+ @client.client_id.should == "fakeid"
117
+ end
118
+
119
+ it "puts the client in the CONNECTED state" do
120
+ @client.handshake
121
+ @client.state.should == :CONNECTED
122
+ end
123
+
124
+ it "registers any pre-existing subscriptions" do
125
+ @client.should_receive(:subscribe).with([], true)
126
+ @client.handshake
127
+ end
128
+
129
+ it "selects a new transport based on what the server supports" do
130
+ Faye::Transport.should_receive(:get).with(instance_of(Faye::Client), ["long-polling", "websocket"]).
131
+ and_return(transport)
132
+ @client.handshake
133
+ end
134
+
135
+ describe "with websocket disabled" do
136
+ before { @client.disable("websocket") }
137
+
138
+ it "selects a new transport, excluding websocket" do
139
+ Faye::Transport.should_receive(:get).with(instance_of(Faye::Client), ["long-polling"]).
140
+ and_return(transport)
141
+ @client.handshake
142
+ end
143
+ end
144
+ end
145
+
146
+ describe "on unsuccessful response" do
147
+ before do
148
+ stub_response "channel" => "/meta/handshake",
149
+ "successful" => false,
150
+ "version" => "1.0",
151
+ "supportedConnectionTypes" => ["websocket"]
152
+ end
153
+
154
+ it "schedules a retry" do
155
+ EM.should_receive(:add_timer)
156
+ @client.handshake
157
+ end
158
+
159
+ it "puts the client in the UNCONNECTED state" do
160
+ EM.stub(:add_timer)
161
+ @client.handshake
162
+ @client.state.should == :UNCONNECTED
163
+ end
164
+ end
165
+
166
+ describe "with existing subscriptions after a server restart" do
167
+ before do
168
+ create_connected_client
169
+
170
+ @message = nil
171
+ subscribe @client, "/messages/foo", lambda { |m| @message = m }
172
+
173
+ @client.receive_message "advice" => {"reconnect" => "handshake"}
174
+
175
+ stub_response "channel" => "/meta/handshake",
176
+ "successful" => true,
177
+ "version" => "1.0",
178
+ "supportedConnectionTypes" => ["websocket"],
179
+ "clientId" => "reconnectid"
180
+ end
181
+
182
+ it "resends the subscriptions to the server" do
183
+ transport.should_receive(:send).with(hash_including("channel" => "/meta/handshake"), 60)
184
+ transport.should_receive(:send).with({
185
+ "channel" => "/meta/subscribe",
186
+ "clientId" => "reconnectid",
187
+ "subscription" => "/messages/foo",
188
+ "id" => instance_of(String)
189
+ }, 60)
190
+ @client.handshake
191
+ end
192
+
193
+ it "retains the listeners for the subscriptions" do
194
+ @client.handshake
195
+ @client.receive_message("channel" => "/messages/foo", "data" => "ok")
196
+ @message.should == "ok"
197
+ end
198
+ end
199
+
200
+ describe "with a connected client" do
201
+ before { create_connected_client }
202
+
203
+ it "does not send a handshake message to the server" do
204
+ transport.should_not_receive(:send).with({
205
+ "channel" => "/meta/handshake",
206
+ "version" => "1.0",
207
+ "supportedConnectionTypes" => ["fake"],
208
+ "id" => instance_of(String)
209
+ }, 60)
210
+ @client.handshake
211
+ end
212
+ end
213
+ end
214
+
215
+ describe :connect do
216
+ describe "with an unconnected client" do
217
+ before do
218
+ stub_response "channel" => "/meta/handshake",
219
+ "successful" => true,
220
+ "version" => "1.0",
221
+ "supportedConnectionTypes" => ["websocket"],
222
+ "clientId" => "handshakeid"
223
+
224
+ create_client
225
+ end
226
+
227
+ it "handshakes before connecting" do
228
+ transport.should_receive(:send).with({
229
+ "channel" => "/meta/connect",
230
+ "clientId" => "handshakeid",
231
+ "connectionType" => "fake",
232
+ "id" => instance_of(String)
233
+ }, 60)
234
+ @client.connect
235
+ end
236
+ end
237
+
238
+ describe "with a connected client" do
239
+ before { create_connected_client }
240
+
241
+ it "sends a connect message to the server" do
242
+ transport.should_receive(:send).with({
243
+ "channel" => "/meta/connect",
244
+ "clientId" => "fakeid",
245
+ "connectionType" => "fake",
246
+ "id" => instance_of(String)
247
+ }, 60)
248
+ @client.connect
249
+ end
250
+
251
+ it "only opens one connect request at a time" do
252
+ transport.should_receive(:send).with({
253
+ "channel" => "/meta/connect",
254
+ "clientId" => "fakeid",
255
+ "connectionType" => "fake",
256
+ "id" => instance_of(String)
257
+ }, 60).
258
+ exactly(1).
259
+ and_return # override stub implementation
260
+
261
+ @client.connect
262
+ @client.connect
263
+ end
264
+ end
265
+ end
266
+
267
+ describe :disconnect do
268
+ before { create_connected_client }
269
+
270
+ it "sends a disconnect message to the server" do
271
+ transport.stub(:close)
272
+ transport.should_receive(:send).with({
273
+ "channel" => "/meta/disconnect",
274
+ "clientId" => "fakeid",
275
+ "id" => instance_of(String)
276
+ }, 60)
277
+ @client.disconnect
278
+ end
279
+
280
+ it "puts the client in the DISCONNECTED state" do
281
+ transport.stub(:close)
282
+ @client.disconnect
283
+ @client.state.should == :DISCONNECTED
284
+ end
285
+
286
+ describe "on successful response" do
287
+ before do
288
+ stub_response "channel" => "/meta/disconnect",
289
+ "successful" => true,
290
+ "clientId" => "fakeid"
291
+ end
292
+
293
+ it "closes the transport" do
294
+ transport.should_receive(:close)
295
+ @client.disconnect
296
+ end
297
+ end
298
+ end
299
+
300
+ describe :subscribe do
301
+ before do
302
+ create_connected_client
303
+ @subscribe_message = {
304
+ "channel" => "/meta/subscribe",
305
+ "clientId" => "fakeid",
306
+ "subscription" => "/foo/*",
307
+ "id" => instance_of(String)
308
+ }
309
+ end
310
+
311
+ describe "with no prior subscriptions" do
312
+ it "sends a subscribe message to the server" do
313
+ transport.should_receive(:send).with(@subscribe_message, 60)
314
+ @client.subscribe("/foo/*")
315
+ end
316
+
317
+ # The Bayeux spec says the server should accept a list of subscriptions
318
+ # in one message but the cometD server doesn't actually support this
319
+ it "sends multiple subscribe messages if given an array" do
320
+ transport.should_receive(:send).with({
321
+ "channel" => "/meta/subscribe",
322
+ "clientId" => "fakeid",
323
+ "subscription" => "/foo",
324
+ "id" => instance_of(String)
325
+ }, 60)
326
+ transport.should_receive(:send).with({
327
+ "channel" => "/meta/subscribe",
328
+ "clientId" => "fakeid",
329
+ "subscription" => "/bar",
330
+ "id" => instance_of(String)
331
+ }, 60)
332
+ @client.subscribe(["/foo", "/bar"])
333
+ end
334
+
335
+ describe "on successful response" do
336
+ before do
337
+ stub_response "channel" => "/meta/subscribe",
338
+ "successful" => true,
339
+ "clientId" => "fakeid",
340
+ "subscription" => "/foo/*"
341
+ end
342
+
343
+ it "sets up a listener for the subscribed channel" do
344
+ @message = nil
345
+ @client.subscribe("/foo/*") { |m| @message = m }
346
+ @client.receive_message("channel" => "/foo/bar", "data" => "hi")
347
+ @message.should == "hi"
348
+ end
349
+
350
+ it "does not call the listener for non-matching channels" do
351
+ @message = nil
352
+ @client.subscribe("/foo/*") { |m| @message = m }
353
+ @client.receive_message("channel" => "/bar", "data" => "hi")
354
+ @message.should be_nil
355
+ end
356
+
357
+ it "activates the subscription" do
358
+ active = false
359
+ @client.subscribe("/foo/*").callback { active = true }
360
+ active.should be_true
361
+ end
362
+
363
+ describe "with an incoming extension installed" do
364
+ before do
365
+ extension = Class.new do
366
+ def incoming(message, callback)
367
+ message["data"]["changed"] = true if message["data"]
368
+ callback.call(message)
369
+ end
370
+ end
371
+ @client.add_extension(extension.new)
372
+ @message = nil
373
+ @client.subscribe("/foo/*") { |m| @message = m }
374
+ end
375
+
376
+ it "passes delivered messages through the extension" do
377
+ @client.receive_message("channel" => "/foo/bar", "data" => {"hello" => "there"})
378
+ @message.should == {"hello" => "there", "changed" => true}
379
+ end
380
+ end
381
+
382
+ describe "with an outgoing extension installed" do
383
+ before do
384
+ extension = Class.new do
385
+ def outgoing(message, callback)
386
+ message["data"]["changed"] = true if message["data"]
387
+ callback.call(message)
388
+ end
389
+ end
390
+ @client.add_extension(extension.new)
391
+ @message = nil
392
+ @client.subscribe("/foo/*") { |m| @message = m }
393
+ end
394
+
395
+ it "leaves messages unchanged" do
396
+ @client.receive_message("channel" => "/foo/bar", "data" => {"hello" => "there"})
397
+ @message.should == {"hello" => "there"}
398
+ end
399
+ end
400
+
401
+ describe "with an incoming extension that invalidates the response" do
402
+ before do
403
+ extension = Class.new do
404
+ def incoming(message, callback)
405
+ message["successful"] = false if message["channel"] == "/meta/subscribe"
406
+ callback.call(message)
407
+ end
408
+ end
409
+ @client.add_extension(extension.new)
410
+ end
411
+
412
+ it "does not set up a listener for the subscribed channel" do
413
+ @message = nil
414
+ @client.subscribe("/foo/*") { |m| @message = m }
415
+ @client.receive_message("channel" => "/foo/bar", "data" => "hi")
416
+ @message.should be_nil
417
+ end
418
+
419
+ it "does not activate the subscription" do
420
+ active = false
421
+ @client.subscribe("/foo/*").callback { active = true }
422
+ active.should be_false
423
+ end
424
+ end
425
+ end
426
+
427
+ describe "on unsuccessful response" do
428
+ before do
429
+ stub_response "channel" => "/meta/subscribe",
430
+ "error" => "403:/meta/foo:Forbidden channel",
431
+ "successful" => false,
432
+ "clientId" => "fakeid",
433
+ "subscription" => "/meta/foo"
434
+ end
435
+
436
+ it "does not set up a listener for the subscribed channel" do
437
+ @message = nil
438
+ @client.subscribe("/meta/foo") { |m| @message = m }
439
+ @client.receive_message("channel" => "/meta/foo", "data" => "hi")
440
+ @message.should be_nil
441
+ end
442
+
443
+ it "does not activate the subscription" do
444
+ active = false
445
+ @client.subscribe("/meta/foo").callback { active = true }
446
+ active.should be_false
447
+ end
448
+
449
+ it "reports the error through an errback" do
450
+ error = nil
451
+ @client.subscribe("/meta/foo").errback { |e| error = e }
452
+ error.code.should == 403
453
+ error.params.should == ["/meta/foo"]
454
+ error.message.should == "Forbidden channel"
455
+ end
456
+ end
457
+ end
458
+
459
+ describe "with an existing subscription" do
460
+ before do
461
+ subscribe @client, "/foo/*"
462
+ end
463
+
464
+ it "does not send another subscribe message to the server" do
465
+ transport.should_not_receive(:send).with(@subscribe_message, 60)
466
+ @client.subscribe("/foo/*")
467
+ end
468
+
469
+ it "sets up another listener on the channel" do
470
+ @client.subscribe("/foo/*") { @subs_called += 1 }
471
+ @client.receive_message("channel" => "/foo/bar", "data" => "hi")
472
+ @subs_called.should == 2
473
+ end
474
+
475
+ it "activates the subscription" do
476
+ active = false
477
+ @client.subscribe("/foo/*").callback { active = true }
478
+ active.should be_true
479
+ end
480
+ end
481
+ end
482
+
483
+ describe :unsubscribe do
484
+ before do
485
+ create_connected_client
486
+ @unsubscribe_message = {
487
+ "channel" => "/meta/unsubscribe",
488
+ "clientId" => "fakeid",
489
+ "subscription" => "/foo/*",
490
+ "id" => instance_of(String)
491
+ }
492
+ end
493
+
494
+ describe "with no subscriptions" do
495
+ it "does not send an unsubscribe message to the server" do
496
+ transport.should_not_receive(:send).with(@unsubscribe_message, 60)
497
+ @client.unsubscribe("/foo/*")
498
+ end
499
+ end
500
+
501
+ describe "with a single subscription" do
502
+ before do
503
+ @message = nil
504
+ @listener = lambda { |m| @message = m }
505
+ subscribe @client, "/foo/*", @listener
506
+ end
507
+
508
+ it "sends an unsubscribe message to the server" do
509
+ transport.should_receive(:send).with(@unsubscribe_message, 60)
510
+ @client.unsubscribe("/foo/*")
511
+ end
512
+
513
+ it "removes the listener from the channel" do
514
+ @client.receive_message("channel" => "/foo/bar", "data" => "first")
515
+ @client.unsubscribe("/foo/*", &@listener)
516
+ @client.receive_message("channel" => "/foo/bar", "data" => "second")
517
+ @message.should == "first"
518
+ end
519
+ end
520
+
521
+ describe "with multiple subscriptions to the same channel" do
522
+ before do
523
+ @messages = []
524
+ @hey = lambda { |m| @messages << ("hey " + m["text"]) }
525
+ @bye = lambda { |m| @messages << ("bye " + m["text"]) }
526
+ subscribe @client, "/foo/*", @hey
527
+ subscribe @client, "/foo/*", @bye
528
+ end
529
+
530
+ it "removes one of the listeners from the channel" do
531
+ @client.receive_message("channel" => "/foo/bar", "data" => {"text" => "you"})
532
+ @client.unsubscribe("/foo/*", &@hey)
533
+ @client.receive_message("channel" => "/foo/bar", "data" => {"text" => "you"})
534
+ @messages.should == ["hey you", "bye you", "bye you"]
535
+ end
536
+
537
+ it "does not send an unsubscribe message if one listener is removed" do
538
+ transport.should_not_receive(:send).with(@unsubscribe_message, 60)
539
+ @client.unsubscribe("/foo/*", &@bye)
540
+ end
541
+
542
+ it "sends an unsubscribe message if each listener is removed" do
543
+ transport.should_receive(:send).with(@unsubscribe_message, 60)
544
+ @client.unsubscribe("/foo/*", &@bye)
545
+ @client.unsubscribe("/foo/*", &@hey)
546
+ end
547
+
548
+ it "sends an unsubscribe message if all listeners are removed" do
549
+ transport.should_receive(:send).with(@unsubscribe_message, 60)
550
+ @client.unsubscribe("/foo/*")
551
+ end
552
+ end
553
+
554
+ describe "with multiple subscriptions to different channels" do
555
+ before do
556
+ subscribe @client, "/foo"
557
+ subscribe @client, "/bar"
558
+ end
559
+
560
+ it "sends multiple unsubscribe messages if given an array" do
561
+ transport.should_receive(:send).with({
562
+ "channel" => "/meta/unsubscribe",
563
+ "clientId" => "fakeid",
564
+ "subscription" => "/foo",
565
+ "id" => instance_of(String)
566
+ }, 60)
567
+ transport.should_receive(:send).with({
568
+ "channel" => "/meta/unsubscribe",
569
+ "clientId" => "fakeid",
570
+ "subscription" => "/bar",
571
+ "id" => instance_of(String)
572
+ }, 60)
573
+ @client.unsubscribe(["/foo", "/bar"])
574
+ end
575
+ end
576
+ end
577
+
578
+ describe :publish do
579
+ before { create_connected_client }
580
+
581
+ it "sends the message to the server with an ID" do
582
+ transport.should_receive(:send).with({
583
+ "channel" => "/messages/foo",
584
+ "clientId" => "fakeid",
585
+ "data" => {"hello" => "world"},
586
+ "id" => instance_of(String)
587
+ }, 60)
588
+ @client.publish("/messages/foo", "hello" => "world")
589
+ end
590
+
591
+ it "throws an error when publishing to an invalid channel" do
592
+ transport.should_not_receive(:send).with(hash_including("channel" => "/messages/*"), 60)
593
+ lambda { @client.publish("/messages/*", "hello" => "world") }.should raise_error
594
+ end
595
+
596
+ describe "on publish failure" do
597
+ before do
598
+ stub_response "channel" => "/messages/foo",
599
+ "error" => "407:/messages/foo:Failed to publish",
600
+ "successful" => false,
601
+ "clientId" => "fakeid"
602
+ end
603
+
604
+ it "should not be published" do
605
+ published = false
606
+ @client.publish("/messages/foo", "text" => "hi").callback { published = true }
607
+ published.should be_false
608
+ end
609
+
610
+ it "reports the error through an errback" do
611
+ error = nil
612
+ @client.publish("/messages/foo", "text" => "hi").errback { |e| error = e }
613
+ error.code.should == 407
614
+ error.params.should == ["/messages/foo"]
615
+ error.message.should == "Failed to publish"
616
+ end
617
+ end
618
+
619
+ describe "on receipt of the published message" do
620
+ before do
621
+ stub_response "channel" => "/messages/foo",
622
+ "data" => {"text" => "hi"},
623
+ "clientId" => "fakeid"
624
+ end
625
+
626
+ it "does not trigger the callbacks" do
627
+ published = false
628
+ publication = @client.publish("/messages/foo", "text" => "hi")
629
+ publication.callback { published = true }
630
+ publication.errback { published = true }
631
+ published.should be_false
632
+ end
633
+ end
634
+
635
+ describe "with an outgoing extension installed" do
636
+ before do
637
+ extension = Class.new do
638
+ def outgoing(message, callback)
639
+ message["ext"] = {"auth" => "password"}
640
+ callback.call(message)
641
+ end
642
+ end
643
+ @client.add_extension(extension.new)
644
+ end
645
+
646
+ it "passes messages through the extension" do
647
+ transport.should_receive(:send).with({
648
+ "channel" => "/messages/foo",
649
+ "clientId" => "fakeid",
650
+ "data" => {"hello" => "world"},
651
+ "id" => instance_of(String),
652
+ "ext" => {"auth" => "password"}
653
+ }, 60)
654
+ @client.publish("/messages/foo", "hello" => "world")
655
+ end
656
+ end
657
+
658
+ describe "with an incoming extension installed" do
659
+ before do
660
+ extension = Class.new do
661
+ def incoming(message, callback)
662
+ message["ext"] = {"auth" => "password"}
663
+ callback.call(message)
664
+ end
665
+ end
666
+ @client.add_extension(extension.new)
667
+ end
668
+
669
+ it "leaves the message unchanged" do
670
+ transport.should_receive(:send).with({
671
+ "channel" => "/messages/foo",
672
+ "clientId" => "fakeid",
673
+ "data" => {"hello" => "world"},
674
+ "id" => instance_of(String)
675
+ }, 60)
676
+ @client.publish("/messages/foo", "hello" => "world")
677
+ end
678
+ end
679
+ end
680
+
681
+ describe "network notifications" do
682
+ before { create_client }
683
+
684
+ describe "in the default state" do
685
+ it "broadcasts a down notification" do
686
+ @client.should_receive(:trigger).with("transport:down")
687
+ transport.trigger(:down)
688
+ end
689
+
690
+ it "broadcasts an up notification" do
691
+ @client.should_receive(:trigger).with("transport:up")
692
+ transport.trigger(:up)
693
+ end
694
+ end
695
+
696
+ describe "when the transport is up" do
697
+ before { transport.trigger(:up) }
698
+
699
+ it "broadcasts a down notification" do
700
+ @client.should_receive(:trigger).with("transport:down")
701
+ transport.trigger(:down)
702
+ end
703
+
704
+ it "does not broadcast an up notification" do
705
+ @client.should_not_receive(:trigger)
706
+ transport.trigger(:up)
707
+ end
708
+ end
709
+
710
+ describe "when the transport is down" do
711
+ before { transport.trigger(:down) }
712
+
713
+ it "does not broadcast a down notification" do
714
+ @client.should_not_receive(:trigger)
715
+ transport.trigger(:down)
716
+ end
717
+
718
+ it "broadcasts an up notification" do
719
+ @client.should_receive(:trigger).with("transport:up")
720
+ transport.trigger(:up)
721
+ end
722
+ end
723
+ end
724
+ end