faye 0.5.5 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of faye might be problematic. Click here for more details.

Files changed (62) hide show
  1. data/History.txt +14 -0
  2. data/README.rdoc +98 -0
  3. data/Rakefile +17 -15
  4. data/lib/faye-browser-min.js +1 -1
  5. data/lib/faye.rb +14 -5
  6. data/lib/faye/adapters/rack_adapter.rb +12 -5
  7. data/lib/faye/engines/base.rb +62 -0
  8. data/lib/faye/engines/connection.rb +63 -0
  9. data/lib/faye/engines/memory.rb +89 -0
  10. data/lib/faye/engines/redis.rb +141 -0
  11. data/lib/faye/error.rb +16 -4
  12. data/lib/faye/mixins/publisher.rb +6 -0
  13. data/lib/faye/protocol/channel.rb +34 -86
  14. data/lib/faye/protocol/client.rb +36 -52
  15. data/lib/faye/protocol/extensible.rb +3 -0
  16. data/lib/faye/protocol/server.rb +119 -169
  17. data/lib/faye/transport/http.rb +45 -0
  18. data/lib/faye/transport/local.rb +15 -0
  19. data/lib/faye/{network → transport}/transport.rb +36 -49
  20. data/spec/browser.html +35 -0
  21. data/spec/install.sh +48 -0
  22. data/spec/javascript/channel_spec.js +15 -0
  23. data/spec/javascript/client_spec.js +610 -0
  24. data/spec/javascript/engine_spec.js +319 -0
  25. data/spec/javascript/faye_spec.js +15 -0
  26. data/spec/javascript/grammar_spec.js +66 -0
  27. data/spec/javascript/node_adapter_spec.js +276 -0
  28. data/spec/javascript/server/connect_spec.js +168 -0
  29. data/spec/javascript/server/disconnect_spec.js +121 -0
  30. data/spec/javascript/server/extensions_spec.js +60 -0
  31. data/spec/javascript/server/handshake_spec.js +153 -0
  32. data/spec/javascript/server/subscribe_spec.js +245 -0
  33. data/spec/javascript/server/unsubscribe_spec.js +245 -0
  34. data/spec/javascript/server_spec.js +146 -0
  35. data/spec/javascript/transport_spec.js +130 -0
  36. data/spec/node.js +34 -0
  37. data/spec/ruby/channel_spec.rb +17 -0
  38. data/spec/ruby/client_spec.rb +615 -0
  39. data/spec/ruby/engine_spec.rb +312 -0
  40. data/spec/ruby/faye_spec.rb +14 -0
  41. data/spec/ruby/grammar_spec.rb +68 -0
  42. data/spec/ruby/rack_adapter_spec.rb +209 -0
  43. data/spec/ruby/server/connect_spec.rb +170 -0
  44. data/spec/ruby/server/disconnect_spec.rb +120 -0
  45. data/spec/ruby/server/extensions_spec.rb +69 -0
  46. data/spec/ruby/server/handshake_spec.rb +151 -0
  47. data/spec/ruby/server/subscribe_spec.rb +247 -0
  48. data/spec/ruby/server/unsubscribe_spec.rb +247 -0
  49. data/spec/ruby/server_spec.rb +138 -0
  50. data/spec/ruby/transport_spec.rb +128 -0
  51. data/spec/spec_helper.rb +5 -0
  52. data/spec/testswarm.pl +200 -0
  53. data/spec/thin_proxy.rb +36 -0
  54. metadata +119 -84
  55. data/Manifest.txt +0 -27
  56. data/README.txt +0 -98
  57. data/lib/faye/protocol/connection.rb +0 -111
  58. data/test/scenario.rb +0 -172
  59. data/test/test_channel.rb +0 -54
  60. data/test/test_clients.rb +0 -381
  61. data/test/test_grammar.rb +0 -86
  62. data/test/test_server.rb +0 -488
@@ -0,0 +1,146 @@
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( [], response )
31
+ }})
32
+
33
+ it("routes single messages to appropriate handlers", function() { with(this) {
34
+ expect(server, "handshake").given(handshake, false).yielding([{}])
35
+ expect(engine, "publish").given(handshake)
36
+ server.process(handshake, false, function() {})
37
+ }})
38
+
39
+ it("routes a list of messages to appropriate handlers", function() { with(this) {
40
+ expect(server, "handshake").given(handshake, false).yielding([{}])
41
+ expect(server, "connect").given(connect, false).yielding([{}])
42
+ expect(server, "disconnect").given(disconnect, false).yielding([{}])
43
+ expect(server, "subscribe").given(subscribe, false).yielding([{}])
44
+ expect(server, "unsubscribe").given(unsubscribe, false).yielding([{}])
45
+
46
+ expect(engine, "publish").given(handshake)
47
+ expect(engine, "publish").given(connect)
48
+ expect(engine, "publish").given(disconnect)
49
+ expect(engine, "publish").given(subscribe)
50
+ expect(engine, "publish").given(unsubscribe)
51
+ expect(engine, "publish").given(publish)
52
+
53
+ server.process([handshake, connect, disconnect, subscribe, unsubscribe, publish], false, function() {})
54
+ }})
55
+
56
+ describe("publishing a message", function() { with(this) {
57
+ it("tells the engine to publish the message", function() { with(this) {
58
+ expect(engine, "publish").given(publish)
59
+ server.process(publish, false, function() {})
60
+ }})
61
+
62
+ it("returns no respons", function() { with(this) {
63
+ stub(engine, "publish")
64
+ server.process(publish, false, function(response) {
65
+ assertEqual( [], response)
66
+ })
67
+ }})
68
+
69
+ describe("with an error", function() { with(this) {
70
+ before(function() { with(this) {
71
+ publish.error = "invalid"
72
+ }})
73
+
74
+ it("does not tell the engine to publish the message", function() { with(this) {
75
+ expect(engine, "publish").exactly(0)
76
+ server.process(publish, false, function() {})
77
+ }})
78
+
79
+ it("returns no response", function() { with(this) {
80
+ stub(engine, "publish")
81
+ server.process(publish, false, function(response) {
82
+ assertEqual( [], response)
83
+ })
84
+ }})
85
+ }})
86
+
87
+ describe("to an invalid channel", function() { with(this) {
88
+ before(function() { with(this) {
89
+ publish.channel = "/invalid/*"
90
+ }})
91
+
92
+ it("does not tell the engine to publish the message", function() { with(this) {
93
+ expect(engine, "publish").exactly(0)
94
+ server.process(publish, false, function() {})
95
+ }})
96
+ }})
97
+ }})
98
+
99
+ describe("handshaking", function() { with(this) {
100
+ before(function() { with(this) {
101
+ expect(engine, "publish").given(handshake)
102
+ expect(server, "handshake").given(handshake, false).yielding([{successful: true}])
103
+ }})
104
+
105
+ it("returns the handshake response with advice", function() { with(this) {
106
+ server.process(handshake, false, function(response) {
107
+ assertEqual([
108
+ { successful: true,
109
+ advice: {reconnect: "retry", interval: 0, timeout: 60000}
110
+ }
111
+ ], response)
112
+ })
113
+ }})
114
+ }})
115
+
116
+ describe("connecting for messages", function() { with(this) {
117
+ before(function() { with(this) {
118
+ this.messages = [{channel: "/a"}, {channel: "/b"}]
119
+ expect(engine, "publish").given(connect)
120
+ expect(server, "connect").given(connect, false).yielding([messages])
121
+ }})
122
+
123
+ it("returns the new messages", function() { with(this) {
124
+ server.process(connect, false, function(response) {
125
+ assertEqual( messages, response )
126
+ })
127
+ }})
128
+ }})
129
+ }})
130
+
131
+ describe("#flushConnection", function() { with(this) {
132
+ before(function() { with(this) {
133
+ this.message = {clientId: "fakeclientid"}
134
+ }})
135
+
136
+ it("flushes the connection when given one message", function() { with(this) {
137
+ expect(engine, "flush").given("fakeclientid")
138
+ server.flushConnection(message)
139
+ }})
140
+
141
+ it("flushes the connection when given a list of messages", function() { with(this) {
142
+ expect(engine, "flush").given("fakeclientid")
143
+ server.flushConnection([message])
144
+ }})
145
+ }})
146
+ }})
@@ -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,34 @@
1
+ JSCLASS_PATH = 'vendor/js.class/build/src'
2
+ require('../' + JSCLASS_PATH + '/loader')
3
+
4
+ JS.Packages(function() { with(this) {
5
+ file('build/faye-node.js').provides('Faye')
6
+ autoload(/.*Spec/, {from: 'spec/javascript'})
7
+ }})
8
+
9
+ JS.require('Faye', 'JS.Test', 'JS.Range', function() {
10
+ JS.Test.Unit.Assertions.include({
11
+ assertYield: function(expected) {
12
+ var testcase = this
13
+ return function(actual) { testcase.assertEqual(expected, actual) }
14
+ }
15
+ })
16
+
17
+ JS.ENV.Server = {}
18
+
19
+ JS.require( 'FayeSpec',
20
+ 'GrammarSpec',
21
+ 'ChannelSpec',
22
+ 'EngineSpec',
23
+ 'ServerSpec',
24
+ 'Server.HandshakeSpec',
25
+ 'Server.ConnectSpec',
26
+ 'Server.DisconnectSpec',
27
+ 'Server.SubscribeSpec',
28
+ 'Server.UnsubscribeSpec',
29
+ 'Server.ExtensionsSpec',
30
+ 'NodeAdapterSpec',
31
+ 'ClientSpec',
32
+ 'TransportSpec',
33
+ JS.Test.method('autorun'))
34
+ })
@@ -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,615 @@
1
+ require "spec_helper"
2
+
3
+ describe Faye::Client do
4
+ let :transport do
5
+ transport = mock("transport")
6
+ transport.stub(:connection_type).and_return "fake"
7
+ transport.stub(:send)
8
+ transport
9
+ end
10
+
11
+ before { EM.stub(:add_timer) }
12
+
13
+ def stub_response(response)
14
+ transport.stub(:send) do |message, *args|
15
+ response["id"] = message["id"]
16
+ @client.receive_message(response)
17
+ end
18
+ end
19
+
20
+ def create_client
21
+ Faye::Transport.stub(:get).and_return(transport)
22
+ @client = Faye::Client.new("http://localhost/")
23
+ end
24
+
25
+ def create_connected_client
26
+ create_client
27
+ stub_response "channel" => "/meta/handshake",
28
+ "successful" => true,
29
+ "version" => "1.0",
30
+ "supportedConnectionTypes" => ["websocket"],
31
+ "clientId" => "fakeid"
32
+
33
+ @client.handshake
34
+ end
35
+
36
+ def subscribe(client, channel, callback = nil)
37
+ stub_response "channel" => "/meta/subscribe",
38
+ "successful" => true,
39
+ "clientId" => "fakeid",
40
+ "subscription" => channel
41
+
42
+ @subs_called = 0
43
+ callback ||= lambda { |m| @subs_called = 1 }
44
+ @client.subscribe(channel, &callback)
45
+ end
46
+
47
+ describe :initialize do
48
+ it "creates a transport the server must support" do
49
+ Faye::Transport.should_receive(:get).with(instance_of(Faye::Client),
50
+ ["long-polling", "callback-polling", "in-process"]).
51
+ and_return(transport)
52
+ Faye::Client.new("http://localhost/")
53
+ end
54
+
55
+ it "puts the client in the UNCONNECTED state" do
56
+ Faye::Transport.stub(:get)
57
+ client = Faye::Client.new("http://localhost/")
58
+ client.state.should == :UNCONNECTED
59
+ end
60
+ end
61
+
62
+ describe :handshake do
63
+ before { create_client }
64
+
65
+ it "sends a handshake message to the server" do
66
+ transport.should_receive(:send).with({
67
+ "channel" => "/meta/handshake",
68
+ "version" => "1.0",
69
+ "supportedConnectionTypes" => ["fake"],
70
+ "id" => instance_of(String)
71
+ }, 60)
72
+ @client.handshake
73
+ end
74
+
75
+ it "puts the client in the CONNECTING state" do
76
+ transport.stub(:send)
77
+ @client.handshake
78
+ @client.state.should == :CONNECTING
79
+ end
80
+
81
+ describe "with an outgoing extension installed" do
82
+ before do
83
+ extension = Class.new do
84
+ def outgoing(message, callback)
85
+ message["ext"] = {"auth" => "password"}
86
+ callback.call(message)
87
+ end
88
+ end
89
+ @client.add_extension(extension.new)
90
+ end
91
+
92
+ it "passes the handshake message through the extension" do
93
+ transport.should_receive(:send).with({
94
+ "channel" => "/meta/handshake",
95
+ "version" => "1.0",
96
+ "supportedConnectionTypes" => ["fake"],
97
+ "id" => instance_of(String),
98
+ "ext" => {"auth" => "password"}
99
+ }, 60)
100
+ @client.handshake
101
+ end
102
+ end
103
+
104
+ describe "on successful response" do
105
+ before do
106
+ stub_response "channel" => "/meta/handshake",
107
+ "successful" => true,
108
+ "version" => "1.0",
109
+ "supportedConnectionTypes" => ["websocket"],
110
+ "clientId" => "fakeid"
111
+ end
112
+
113
+ it "stores the clientId" do
114
+ @client.handshake
115
+ @client.client_id.should == "fakeid"
116
+ end
117
+
118
+ it "puts the client in the CONNECTED state" do
119
+ @client.handshake
120
+ @client.state.should == :CONNECTED
121
+ end
122
+
123
+ it "selects a new transport based on what the server supports" do
124
+ Faye::Transport.should_receive(:get).with(instance_of(Faye::Client), ["websocket"]).
125
+ and_return(transport)
126
+ @client.handshake
127
+ end
128
+
129
+ it "registers any pre-existing subscriptions" do
130
+ @client.should_receive(:subscribe).with([], true)
131
+ @client.handshake
132
+ end
133
+ end
134
+
135
+ describe "on unsuccessful response" do
136
+ before do
137
+ stub_response "channel" => "/meta/handshake",
138
+ "successful" => false,
139
+ "version" => "1.0",
140
+ "supportedConnectionTypes" => ["websocket"]
141
+ end
142
+
143
+ it "schedules a retry" do
144
+ EM.should_receive(:add_timer)
145
+ @client.handshake
146
+ end
147
+
148
+ it "puts the client in the UNCONNECTED state" do
149
+ EM.stub(:add_timer)
150
+ @client.handshake
151
+ @client.state.should == :UNCONNECTED
152
+ end
153
+ end
154
+
155
+ describe "with existing subscriptions after a server restart" do
156
+ before do
157
+ create_connected_client
158
+
159
+ @message = nil
160
+ subscribe @client, "/messages/foo", lambda { |m| @message = m }
161
+
162
+ @client.receive_message "advice" => {"reconnect" => "handshake"}
163
+
164
+ stub_response "channel" => "/meta/handshake",
165
+ "successful" => true,
166
+ "version" => "1.0",
167
+ "supportedConnectionTypes" => ["websocket"],
168
+ "clientId" => "reconnectid"
169
+ end
170
+
171
+ it "resends the subscriptions to the server" do
172
+ transport.should_receive(:send).with(hash_including("channel" => "/meta/handshake"), 60)
173
+ transport.should_receive(:send).with({
174
+ "channel" => "/meta/subscribe",
175
+ "clientId" => "reconnectid",
176
+ "subscription" => "/messages/foo",
177
+ "id" => instance_of(String)
178
+ }, 60)
179
+ @client.handshake
180
+ end
181
+
182
+ it "retains the listeners for the subscriptions" do
183
+ @client.handshake
184
+ @client.receive_message("channel" => "/messages/foo", "data" => "ok")
185
+ @message.should == "ok"
186
+ end
187
+ end
188
+
189
+ describe "with a connected client" do
190
+ before { create_connected_client }
191
+
192
+ it "does not send a handshake message to the server" do
193
+ transport.should_not_receive(:send).with({
194
+ "channel" => "/meta/handshake",
195
+ "version" => "1.0",
196
+ "supportedConnectionTypes" => ["fake"],
197
+ "id" => instance_of(String)
198
+ }, 60)
199
+ @client.handshake
200
+ end
201
+ end
202
+ end
203
+
204
+ describe :connect do
205
+ describe "with an unconnected client" do
206
+ before do
207
+ stub_response "channel" => "/meta/handshake",
208
+ "successful" => true,
209
+ "version" => "1.0",
210
+ "supportedConnectionTypes" => ["websocket"],
211
+ "clientId" => "handshakeid"
212
+
213
+ create_client
214
+ end
215
+
216
+ it "handshakes before connecting" do
217
+ transport.should_receive(:send).with({
218
+ "channel" => "/meta/connect",
219
+ "clientId" => "handshakeid",
220
+ "connectionType" => "fake",
221
+ "id" => instance_of(String)
222
+ }, 60)
223
+ @client.connect
224
+ end
225
+ end
226
+
227
+ describe "with a connected client" do
228
+ before { create_connected_client }
229
+
230
+ it "sends a connect message to the server" do
231
+ transport.should_receive(:send).with({
232
+ "channel" => "/meta/connect",
233
+ "clientId" => "fakeid",
234
+ "connectionType" => "fake",
235
+ "id" => instance_of(String)
236
+ }, 60)
237
+ @client.connect
238
+ end
239
+
240
+ it "only opens one connect request at a time" do
241
+ transport.should_receive(:send).with({
242
+ "channel" => "/meta/connect",
243
+ "clientId" => "fakeid",
244
+ "connectionType" => "fake",
245
+ "id" => instance_of(String)
246
+ }, 60).
247
+ exactly(1).
248
+ and_return # override stub implementation
249
+
250
+ @client.connect
251
+ @client.connect
252
+ end
253
+ end
254
+ end
255
+
256
+ describe :disconnect do
257
+ before { create_connected_client }
258
+
259
+ it "sends a disconnect message to the server" do
260
+ transport.should_receive(:send).with({
261
+ "channel" => "/meta/disconnect",
262
+ "clientId" => "fakeid",
263
+ "id" => instance_of(String)
264
+ }, 60)
265
+ @client.disconnect
266
+ end
267
+
268
+ it "puts the client in the DISCONNECTED state" do
269
+ @client.disconnect
270
+ @client.state.should == :DISCONNECTED
271
+ end
272
+ end
273
+
274
+ describe :subscribe do
275
+ before do
276
+ create_connected_client
277
+ @subscribe_message = {
278
+ "channel" => "/meta/subscribe",
279
+ "clientId" => "fakeid",
280
+ "subscription" => "/foo/*",
281
+ "id" => instance_of(String)
282
+ }
283
+ end
284
+
285
+ describe "with no prior subscriptions" do
286
+ it "sends a subscribe message to the server" do
287
+ transport.should_receive(:send).with(@subscribe_message, 60)
288
+ @client.subscribe("/foo/*")
289
+ end
290
+
291
+ # The Bayeux spec says the server should accept a list of subscriptions
292
+ # in one message but the cometD server doesn't actually support this
293
+ it "sends multiple subscribe messages if given an array" do
294
+ transport.should_receive(:send).with({
295
+ "channel" => "/meta/subscribe",
296
+ "clientId" => "fakeid",
297
+ "subscription" => "/foo",
298
+ "id" => instance_of(String)
299
+ }, 60)
300
+ transport.should_receive(:send).with({
301
+ "channel" => "/meta/subscribe",
302
+ "clientId" => "fakeid",
303
+ "subscription" => "/bar",
304
+ "id" => instance_of(String)
305
+ }, 60)
306
+ @client.subscribe(["/foo", "/bar"])
307
+ end
308
+
309
+ describe "on successful response" do
310
+ before do
311
+ stub_response "channel" => "/meta/subscribe",
312
+ "successful" => true,
313
+ "clientId" => "fakeid",
314
+ "subscription" => "/foo/*"
315
+ end
316
+
317
+ it "sets up a listener for the subscribed channel" do
318
+ @message = nil
319
+ @client.subscribe("/foo/*") { |m| @message = m }
320
+ @client.receive_message("channel" => "/foo/bar", "data" => "hi")
321
+ @message.should == "hi"
322
+ end
323
+
324
+ it "does not call the listener for non-matching channels" do
325
+ @message = nil
326
+ @client.subscribe("/foo/*") { |m| @message = m }
327
+ @client.receive_message("channel" => "/bar", "data" => "hi")
328
+ @message.should be_nil
329
+ end
330
+
331
+ it "activates the subscription" do
332
+ active = false
333
+ @client.subscribe("/foo/*").callback { active = true }
334
+ active.should be_true
335
+ end
336
+
337
+ describe "with an incoming extension installed" do
338
+ before do
339
+ extension = Class.new do
340
+ def incoming(message, callback)
341
+ message["data"]["changed"] = true if message["data"]
342
+ callback.call(message)
343
+ end
344
+ end
345
+ @client.add_extension(extension.new)
346
+ @message = nil
347
+ @client.subscribe("/foo/*") { |m| @message = m }
348
+ end
349
+
350
+ it "passes delivered messages through the extension" do
351
+ @client.receive_message("channel" => "/foo/bar", "data" => {"hello" => "there"})
352
+ @message.should == {"hello" => "there", "changed" => true}
353
+ end
354
+ end
355
+
356
+ describe "with an outgoing extension installed" do
357
+ before do
358
+ extension = Class.new do
359
+ def outgoing(message, callback)
360
+ message["data"]["changed"] = true if message["data"]
361
+ callback.call(message)
362
+ end
363
+ end
364
+ @client.add_extension(extension.new)
365
+ @message = nil
366
+ @client.subscribe("/foo/*") { |m| @message = m }
367
+ end
368
+
369
+ it "leaves messages unchanged" do
370
+ @client.receive_message("channel" => "/foo/bar", "data" => {"hello" => "there"})
371
+ @message.should == {"hello" => "there"}
372
+ end
373
+ end
374
+
375
+ describe "with an incoming extension that invalidates the response" do
376
+ before do
377
+ extension = Class.new do
378
+ def incoming(message, callback)
379
+ message["successful"] = false if message["channel"] == "/meta/subscribe"
380
+ callback.call(message)
381
+ end
382
+ end
383
+ @client.add_extension(extension.new)
384
+ end
385
+
386
+ it "does not set up a listener for the subscribed channel" do
387
+ @message = nil
388
+ @client.subscribe("/foo/*") { |m| @message = m }
389
+ @client.receive_message("channel" => "/foo/bar", "data" => "hi")
390
+ @message.should be_nil
391
+ end
392
+
393
+ it "does not activate the subscription" do
394
+ active = false
395
+ @client.subscribe("/foo/*").callback { active = true }
396
+ active.should be_false
397
+ end
398
+ end
399
+ end
400
+
401
+ describe "on unsuccessful response" do
402
+ before do
403
+ stub_response "channel" => "/meta/subscribe",
404
+ "error" => "403:/meta/foo:Forbidden channel",
405
+ "successful" => false,
406
+ "clientId" => "fakeid",
407
+ "subscription" => "/meta/foo"
408
+ end
409
+
410
+ it "does not set up a listener for the subscribed channel" do
411
+ @message = nil
412
+ @client.subscribe("/meta/foo") { |m| @message = m }
413
+ @client.receive_message("channel" => "/meta/foo", "data" => "hi")
414
+ @message.should be_nil
415
+ end
416
+
417
+ it "does not activate the subscription" do
418
+ active = false
419
+ @client.subscribe("/meta/foo").callback { active = true }
420
+ active.should be_false
421
+ end
422
+
423
+ it "reports the error through an errback" do
424
+ error = nil
425
+ @client.subscribe("/meta/foo").errback { |e| error = e }
426
+ error.code.should == 403
427
+ error.params.should == ["/meta/foo"]
428
+ error.message.should == "Forbidden channel"
429
+ end
430
+ end
431
+ end
432
+
433
+ describe "with an existing subscription" do
434
+ before do
435
+ subscribe @client, "/foo/*"
436
+ end
437
+
438
+ it "does not send another subscribe message to the server" do
439
+ transport.should_not_receive(:send).with(@subscribe_message, 60)
440
+ @client.subscribe("/foo/*")
441
+ end
442
+
443
+ it "sets up another listener on the channel" do
444
+ @client.subscribe("/foo/*") { @subs_called += 1 }
445
+ @client.receive_message("channel" => "/foo/bar", "data" => "hi")
446
+ @subs_called.should == 2
447
+ end
448
+
449
+ it "activates the subscription" do
450
+ active = false
451
+ @client.subscribe("/foo/*").callback { active = true }
452
+ active.should be_true
453
+ end
454
+ end
455
+ end
456
+
457
+ describe :unsubscribe do
458
+ before do
459
+ create_connected_client
460
+ @unsubscribe_message = {
461
+ "channel" => "/meta/unsubscribe",
462
+ "clientId" => "fakeid",
463
+ "subscription" => "/foo/*",
464
+ "id" => instance_of(String)
465
+ }
466
+ end
467
+
468
+ describe "with no subscriptions" do
469
+ it "does not send an unsubscribe message to the server" do
470
+ transport.should_not_receive(:send).with(@unsubscribe_message, 60)
471
+ @client.unsubscribe("/foo/*")
472
+ end
473
+ end
474
+
475
+ describe "with a single subscription" do
476
+ before do
477
+ @message = nil
478
+ @listener = lambda { |m| @message = m }
479
+ subscribe @client, "/foo/*", @listener
480
+ end
481
+
482
+ it "sends an unsubscribe message to the server" do
483
+ transport.should_receive(:send).with(@unsubscribe_message, 60)
484
+ @client.unsubscribe("/foo/*")
485
+ end
486
+
487
+ it "removes the listener from the channel" do
488
+ @client.receive_message("channel" => "/foo/bar", "data" => "first")
489
+ @client.unsubscribe("/foo/*", &@listener)
490
+ @client.receive_message("channel" => "/foo/bar", "data" => "second")
491
+ @message.should == "first"
492
+ end
493
+ end
494
+
495
+ describe "with multiple subscriptions to the same channel" do
496
+ before do
497
+ @messages = []
498
+ @hey = lambda { |m| @messages << ("hey " + m["text"]) }
499
+ @bye = lambda { |m| @messages << ("bye " + m["text"]) }
500
+ subscribe @client, "/foo/*", @hey
501
+ subscribe @client, "/foo/*", @bye
502
+ end
503
+
504
+ it "removes one of the listeners from the channel" do
505
+ @client.receive_message("channel" => "/foo/bar", "data" => {"text" => "you"})
506
+ @client.unsubscribe("/foo/*", &@hey)
507
+ @client.receive_message("channel" => "/foo/bar", "data" => {"text" => "you"})
508
+ @messages.should == ["hey you", "bye you", "bye you"]
509
+ end
510
+
511
+ it "does not send an unsubscribe message if one listener is removed" do
512
+ transport.should_not_receive(:send).with(@unsubscribe_message, 60)
513
+ @client.unsubscribe("/foo/*", &@bye)
514
+ end
515
+
516
+ it "sends an unsubscribe message if each listener is removed" do
517
+ transport.should_receive(:send).with(@unsubscribe_message, 60)
518
+ @client.unsubscribe("/foo/*", &@bye)
519
+ @client.unsubscribe("/foo/*", &@hey)
520
+ end
521
+
522
+ it "sends an unsubscribe message if all listeners are removed" do
523
+ transport.should_receive(:send).with(@unsubscribe_message, 60)
524
+ @client.unsubscribe("/foo/*")
525
+ end
526
+ end
527
+
528
+ describe "with multiple subscriptions to different channels" do
529
+ before do
530
+ subscribe @client, "/foo"
531
+ subscribe @client, "/bar"
532
+ end
533
+
534
+ it "sends multiple unsubscribe messages if given an array" do
535
+ transport.should_receive(:send).with({
536
+ "channel" => "/meta/unsubscribe",
537
+ "clientId" => "fakeid",
538
+ "subscription" => "/foo",
539
+ "id" => instance_of(String)
540
+ }, 60)
541
+ transport.should_receive(:send).with({
542
+ "channel" => "/meta/unsubscribe",
543
+ "clientId" => "fakeid",
544
+ "subscription" => "/bar",
545
+ "id" => instance_of(String)
546
+ }, 60)
547
+ @client.unsubscribe(["/foo", "/bar"])
548
+ end
549
+ end
550
+ end
551
+
552
+ describe :publish do
553
+ before { create_connected_client }
554
+
555
+ it "sends the message to the server with an ID" do
556
+ transport.should_receive(:send).with({
557
+ "channel" => "/messages/foo",
558
+ "clientId" => "fakeid",
559
+ "data" => {"hello" => "world"},
560
+ "id" => instance_of(String)
561
+ }, 60)
562
+ @client.publish("/messages/foo", "hello" => "world")
563
+ end
564
+
565
+ it "throws an error when publishing to an invalid channel" do
566
+ transport.should_not_receive(:send).with(hash_including("channel" => "/messages/*"), 60)
567
+ lambda { @client.publish("/messages/*", "hello" => "world") }.should raise_error
568
+ end
569
+
570
+ describe "with an outgoing extension installed" do
571
+ before do
572
+ extension = Class.new do
573
+ def outgoing(message, callback)
574
+ message["ext"] = {"auth" => "password"}
575
+ callback.call(message)
576
+ end
577
+ end
578
+ @client.add_extension(extension.new)
579
+ end
580
+
581
+ it "passes messages through the extension" do
582
+ transport.should_receive(:send).with({
583
+ "channel" => "/messages/foo",
584
+ "clientId" => "fakeid",
585
+ "data" => {"hello" => "world"},
586
+ "id" => instance_of(String),
587
+ "ext" => {"auth" => "password"}
588
+ }, 60)
589
+ @client.publish("/messages/foo", "hello" => "world")
590
+ end
591
+ end
592
+
593
+ describe "with an incoming extension installed" do
594
+ before do
595
+ extension = Class.new do
596
+ def incoming(message, callback)
597
+ message["ext"] = {"auth" => "password"}
598
+ callback.call(message)
599
+ end
600
+ end
601
+ @client.add_extension(extension.new)
602
+ end
603
+
604
+ it "leaves the message unchanged" do
605
+ transport.should_receive(:send).with({
606
+ "channel" => "/messages/foo",
607
+ "clientId" => "fakeid",
608
+ "data" => {"hello" => "world"},
609
+ "id" => instance_of(String)
610
+ }, 60)
611
+ @client.publish("/messages/foo", "hello" => "world")
612
+ end
613
+ end
614
+ end
615
+ end