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,15 @@
1
+ module Faye
2
+
3
+ class Transport::Local < Transport
4
+ def self.usable?(endpoint)
5
+ endpoint.is_a?(Server)
6
+ end
7
+
8
+ def request(message, timeout)
9
+ @endpoint.process(message, true) { |responses| receive(responses) }
10
+ end
11
+ end
12
+
13
+ Transport.register 'in-process', Transport::Local
14
+
15
+ end
@@ -1,4 +1,3 @@
1
- require 'em-http'
2
1
  require 'json'
3
2
  require 'uri'
4
3
 
@@ -6,21 +5,51 @@ module Faye
6
5
  class Transport
7
6
 
8
7
  include Logging
8
+ include Timeouts
9
9
 
10
10
  def initialize(client, endpoint)
11
11
  debug('Created new ? transport for ?', connection_type, endpoint)
12
- @client = client
13
- @endpoint = endpoint
12
+ @client = client
13
+ @endpoint = endpoint
14
+ @outbox = []
15
+ end
16
+
17
+ def batching?
18
+ true
14
19
  end
15
20
 
16
21
  def connection_type
17
22
  self.class.connection_type
18
23
  end
19
24
 
20
- def send(messages, timeout)
21
- messages = [messages].flatten
22
- debug('Client ? sending message to ?: ?', @client.client_id, @endpoint, messages)
23
- request(messages, timeout)
25
+ def send(message, timeout)
26
+ debug('Client ? sending message to ?: ?', @client.client_id, @endpoint, message)
27
+
28
+ return request([message], timeout) unless batching?
29
+
30
+ @outbox << message
31
+ @timeout = timeout
32
+
33
+ return flush if message['channel'] == Channel::HANDSHAKE
34
+
35
+ if message['channel'] == Channel::CONNECT
36
+ @connection_message = message
37
+ end
38
+
39
+ add_timeout(:publish, Engine::MAX_DELAY) { flush }
40
+ end
41
+
42
+ def flush
43
+ remove_timeout(:publish)
44
+
45
+ if @outbox.size > 1 and @connection_message
46
+ @connection_message['advice'] = {'timeout' => 0}
47
+ end
48
+
49
+ request(@outbox, @timeout)
50
+
51
+ @connection_message = nil
52
+ @outbox = []
24
53
  end
25
54
 
26
55
  def receive(responses)
@@ -64,49 +93,7 @@ module Faye
64
93
  @transports.map { |t| t.first }
65
94
  end
66
95
  end
67
- end
68
-
69
- class HttpTransport < Transport
70
- def self.usable?(endpoint)
71
- endpoint.is_a?(String)
72
- end
73
-
74
- def request(message, timeout)
75
- retry_block = retry_block(message, timeout)
76
-
77
- content = JSON.unparse(message)
78
- params = {
79
- :head => {
80
- 'Content-Type' => 'application/json',
81
- 'host' => URI.parse(@endpoint).host,
82
- 'Content-Length' => content.length
83
- },
84
- :body => content,
85
- :timeout => -1
86
- }
87
- request = EventMachine::HttpRequest.new(@endpoint).post(params)
88
- request.callback do
89
- begin
90
- receive(JSON.parse(request.response))
91
- rescue
92
- retry_block.call
93
- end
94
- end
95
- request.errback { retry_block.call }
96
- end
97
- end
98
- Transport.register 'long-polling', HttpTransport
99
-
100
- class LocalTransport < Transport
101
- def self.usable?(endpoint)
102
- endpoint.is_a?(Server)
103
- end
104
96
 
105
- def request(message, timeout)
106
- @endpoint.process(message, true) { |responses| receive(responses) }
107
- end
108
97
  end
109
- Transport.register 'in-process', LocalTransport
110
-
111
98
  end
112
99
 
data/spec/browser.html ADDED
@@ -0,0 +1,35 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
5
+ <title>Faye test suite</title>
6
+ <script type="text/javascript" src="../vendor/js.class/build/min/loader.js"></script>
7
+ </head>
8
+ <body>
9
+ <script type="text/javascript">
10
+
11
+ JS.Packages(function() { with(this) {
12
+ file('../build/faye-browser-min.js').provides('Faye')
13
+ autoload(/.*Spec/, {from: './javascript'})
14
+ }})
15
+
16
+ JS.require('Faye', 'JS.Test', 'JS.Range', function() {
17
+ JS.Test.Unit.Assertions.include({
18
+ assertYield: function(expected) {
19
+ var testcase = this
20
+ return function(actual) { testcase.assertEqual(expected, actual) }
21
+ }
22
+ })
23
+
24
+ JS.require( 'FayeSpec',
25
+ 'GrammarSpec',
26
+ 'ChannelSpec',
27
+ 'ClientSpec',
28
+ 'TransportSpec',
29
+ JS.Test.method('autorun'))
30
+ })
31
+
32
+ </script>
33
+ </body>
34
+ </html>
35
+
data/spec/install.sh ADDED
@@ -0,0 +1,48 @@
1
+ # This script installs all the necessary software to run the Ruby and
2
+ # Node versions of Faye. Tested on Ubuntu 10.04 LTS 64-bit EC2 image:
3
+ # http://uec-images.ubuntu.com/releases/10.04/release/
4
+
5
+ FAYE_BRANCH=extract-engine
6
+ NODE_VERSION=0.4.7
7
+ REDIS_VERSION=2.2.7
8
+ RUBY_VERSION=1.9.2
9
+
10
+ sudo apt-get update
11
+ sudo apt-get install build-essential g++ git-core \
12
+ openssl libcurl4-openssl-dev libreadline-dev \
13
+ curl wget
14
+
15
+ bash < <(curl -s https://rvm.beginrescueend.com/install/rvm)
16
+ echo "source \"\$HOME/.rvm/scripts/rvm\"" | tee -a ~/.bashrc
17
+ source ~/.rvm/scripts/rvm
18
+ rvm install 1.9.2
19
+ rvm --default use 1.9.2
20
+
21
+ echo "install: --no-rdoc --no-ri
22
+ update: --no-rdoc --no-ri" | tee ~/.gemrc
23
+ gem install rake bundler
24
+
25
+ cd
26
+ git clone git://github.com/creationix/nvm.git ~/.nvm
27
+ . ~/.nvm/nvm.sh
28
+ echo ". ~/.nvm/nvm.sh" | tee -a ~/.bashrc
29
+ nvm install v$NODE_VERSION
30
+ nvm use v$NODE_VERSION
31
+ npm install redis
32
+
33
+ cd /usr/src
34
+ sudo wget http://redis.googlecode.com/files/redis-$REDIS_VERSION.tar.gz
35
+ sudo tar zxvf redis-$REDIS_VERSION.tar.gz
36
+ cd redis-$REDIS_VERSION
37
+ sudo make
38
+ sudo ln -s /usr/src/redis-$REDIS_VERSION/src/redis-server /usr/bin/redis-server
39
+ sudo ln -s /usr/src/redis-$REDIS_VERSION/src/redis-cli /usr/bin/redis-cli
40
+
41
+ cd
42
+ git clone git://github.com/jcoglan/faye.git
43
+ cd faye
44
+ git checkout $FAYE_BRANCH
45
+ git submodule update --init --recursive
46
+ bundle install
47
+ cd vendor/js.class && jake
48
+ cd ../.. && jake
@@ -0,0 +1,15 @@
1
+ JS.ENV.ChannelSpec = JS.Test.describe("Channel", function() { with(this) {
2
+ describe("expand", function() { with(this) {
3
+ it("returns all patterns that match a channel", function() { with(this) {
4
+
5
+ assertEqual( ["/**", "/foo", "/*"],
6
+ Faye.Channel.expand("/foo") )
7
+
8
+ assertEqual( ["/**", "/foo/bar", "/foo/*", "/foo/**"],
9
+ Faye.Channel.expand("/foo/bar") )
10
+
11
+ assertEqual( ["/**", "/foo/bar/qux", "/foo/bar/*", "/foo/**", "/foo/bar/**"],
12
+ Faye.Channel.expand("/foo/bar/qux") )
13
+ }})
14
+ }})
15
+ }})
@@ -0,0 +1,610 @@
1
+ JS.ENV.ClientSpec = JS.Test.describe("Client", function() { with(this) {
2
+ before(function() { with(this) {
3
+ this.transport = {connectionType: "fake", send: function() {}}
4
+ stub(Faye.Transport, "get").yields([transport])
5
+ }})
6
+
7
+ before(function() { with(this) {
8
+ stub("setTimeout")
9
+ }})
10
+
11
+ define("stubResponse", function(response) { with(this) {
12
+ stub(transport, "send", function(message) {
13
+ response.id = message.id
14
+ client.receiveMessage(response)
15
+ })
16
+ }})
17
+
18
+ define("createClient", function() { with(this) {
19
+ this.client = new Faye.Client("http://localhost/")
20
+ }})
21
+
22
+ define("createConnectedClient", function() { with(this) {
23
+ createClient()
24
+ stubResponse({channel: "/meta/handshake",
25
+ successful: true,
26
+ version: "1.0",
27
+ supportedConnectionTypes: ["websocket"],
28
+ clientId: "fakeid" })
29
+
30
+ client.handshake()
31
+ }})
32
+
33
+ define("subscribe", function(client, channel, callback) { with(this) {
34
+ stubResponse({channel: "/meta/subscribe",
35
+ successful: true,
36
+ clientId: "fakeid",
37
+ subscription: channel })
38
+
39
+ this.subsCalled = 0
40
+ callback = callback || function() { subsCalled += 1 }
41
+ client.subscribe(channel, callback)
42
+ }})
43
+
44
+ describe("initialize", function() { with(this) {
45
+ it("creates a transport the server must support", function() { with(this) {
46
+ expect(Faye.Transport, "get").given(instanceOf(Faye.Client),
47
+ ["long-polling", "callback-polling", "in-process"])
48
+ .yielding([transport])
49
+ new Faye.Client("http://localhost/")
50
+ }})
51
+
52
+ it("puts the client in the UNCONNECTED state", function() { with(this) {
53
+ stub(Faye.Transport, "get")
54
+ var client = new Faye.Client("http://localhost/")
55
+ assertEqual( "UNCONNECTED", client.getState() )
56
+ }})
57
+ }})
58
+
59
+ describe("handshake", function() { with(this) {
60
+ before(function() { this.createClient() })
61
+
62
+ it("sends a handshake message to the server", function() { with(this) {
63
+ expect(transport, "send").given({
64
+ channel: "/meta/handshake",
65
+ version: "1.0",
66
+ supportedConnectionTypes: ["fake"],
67
+ id: instanceOf("string")
68
+ }, 60)
69
+ client.handshake()
70
+ }})
71
+
72
+ it("puts the client in the CONNECTING state", function() { with(this) {
73
+ stub(transport, "send")
74
+ client.handshake()
75
+ assertEqual( "CONNECTING", client.getState() )
76
+ }})
77
+
78
+ describe("with an outgoing extension installed", function() { with(this) {
79
+ before(function() { with(this) {
80
+ var extension = {
81
+ outgoing: function(message, callback) {
82
+ message.ext = {auth: "password"}
83
+ callback(message)
84
+ }
85
+ }
86
+ client.addExtension(extension)
87
+ }})
88
+
89
+ it("passes the handshake message through the extension", function() { with(this) {
90
+ expect(transport, "send").given({
91
+ channel: "/meta/handshake",
92
+ version: "1.0",
93
+ supportedConnectionTypes: ["fake"],
94
+ id: instanceOf("string"),
95
+ ext: {auth: "password"}
96
+ }, 60)
97
+ client.handshake()
98
+ }})
99
+ }})
100
+
101
+ describe("on successful response", function() { with(this) {
102
+ before(function() { with(this) {
103
+ stubResponse({channel: "/meta/handshake",
104
+ successful: true,
105
+ version: "1.0",
106
+ supportedConnectionTypes: ["websocket"],
107
+ clientId: "fakeid" })
108
+ }})
109
+
110
+ it("stores the clientId", function() { with(this) {
111
+ client.handshake()
112
+ assertEqual( "fakeid", client.getClientId() )
113
+ }})
114
+
115
+ it("puts the client in the CONNECTED state", function() { with(this) {
116
+ client.handshake()
117
+ assertEqual( "CONNECTED", client.getState() )
118
+ }})
119
+
120
+ it("selects a new transport based on what the server supports", function() { with(this) {
121
+ expect(Faye.Transport, "get").given(instanceOf(Faye.Client), ["websocket"])
122
+ .yielding([transport])
123
+ client.handshake()
124
+ }})
125
+
126
+ it("registers any pre-existing subscriptions", function() { with(this) {
127
+ expect(client, "subscribe").given([], true)
128
+ client.handshake()
129
+ }})
130
+ }})
131
+
132
+ describe("on unsuccessful response", function() { with(this) {
133
+ before(function() { with(this) {
134
+ stubResponse({channel: "/meta/handshake",
135
+ successful: false,
136
+ version: "1.0",
137
+ supportedConnectionTypes: ["websocket"] })
138
+ }})
139
+
140
+ it("schedules a retry", function() { with(this) {
141
+ expect("setTimeout")
142
+ client.handshake()
143
+ }})
144
+
145
+ it("puts the client in the UNCONNECTED state", function() { with(this) {
146
+ stub("setTimeout")
147
+ client.handshake()
148
+ assertEqual( "UNCONNECTED", client.getState() )
149
+ }})
150
+ }})
151
+
152
+ describe("with existing subscriptions after a server restart", function() { with(this) {
153
+ before(function() { with(this) {
154
+ createConnectedClient()
155
+
156
+ this.message = null
157
+ subscribe(client, "/messages/foo", function(m) { message = m })
158
+
159
+ client.receiveMessage({advice: {reconnect: "handshake"}})
160
+
161
+ stubResponse({channel: "/meta/handshake",
162
+ successful: true,
163
+ version: "1.0",
164
+ supportedConnectionTypes: ["websocket"],
165
+ clientId: "reconnectid" })
166
+ }})
167
+
168
+ it("resends the subscriptions to the server", function() { with(this) {
169
+ expect(transport, "send").given({
170
+ channel: "/meta/subscribe",
171
+ clientId: "reconnectid",
172
+ subscription: "/messages/foo",
173
+ id: instanceOf("string")
174
+ }, 60)
175
+ client.handshake()
176
+ }})
177
+
178
+ it("retains the listeners for the subscriptions", function() { with(this) {
179
+ client.handshake()
180
+ client.receiveMessage({channel: "/messages/foo", "data": "ok"})
181
+ assertEqual( "ok", message )
182
+ }})
183
+ }})
184
+
185
+ describe("with a connected client", function() { with(this) {
186
+ before(function() { this.createConnectedClient() })
187
+
188
+ it("does not send a handshake message to the server", function() { with(this) {
189
+ expect(transport, "send").given({
190
+ channel: "/meta/handshake",
191
+ version: "1.0",
192
+ supportedConnectionTypes: ["fake"],
193
+ id: instanceOf("string")
194
+ }, 60)
195
+ .exactly(0)
196
+
197
+ client.handshake()
198
+ }})
199
+ }})
200
+ }})
201
+
202
+ describe("connect", function() { with(this) {
203
+ describe("with an unconnected client", function() { with(this) {
204
+ before(function() { with(this) {
205
+ stubResponse({channel: "/meta/handshake",
206
+ successful: true,
207
+ version: "1.0",
208
+ supportedConnectionTypes: ["websocket"],
209
+ clientId: "handshakeid" })
210
+
211
+ createClient()
212
+ }})
213
+
214
+ it("handshakes before connecting", function() { with(this) {
215
+ expect(transport, "send").given({
216
+ channel: "/meta/connect",
217
+ clientId: "handshakeid",
218
+ connectionType: "fake",
219
+ id: instanceOf("string")
220
+ }, 60)
221
+ client.connect()
222
+ }})
223
+ }})
224
+
225
+ describe("with a connected client", function() { with(this) {
226
+ before(function() { this.createConnectedClient() })
227
+
228
+ it("sends a connect message to the server", function() { with(this) {
229
+ expect(transport, "send").given({
230
+ channel: "/meta/connect",
231
+ clientId: "fakeid",
232
+ connectionType: "fake",
233
+ id: instanceOf("string")
234
+ }, 60)
235
+ client.connect()
236
+ }})
237
+
238
+ it("only opens one connect request at a time", function() { with(this) {
239
+ expect(transport, "send").given({
240
+ channel: "/meta/connect",
241
+ clientId: "fakeid",
242
+ connectionType: "fake",
243
+ id: instanceOf("string")
244
+ }, 60)
245
+ .exactly(1)
246
+
247
+ client.connect()
248
+ client.connect()
249
+ }})
250
+ }})
251
+ }})
252
+
253
+ describe("disconnect", function() { with(this) {
254
+ before(function() { this.createConnectedClient() })
255
+
256
+ it("sends a disconnect message to the server", function() { with(this) {
257
+ expect(transport, "send").given({
258
+ channel: "/meta/disconnect",
259
+ clientId: "fakeid",
260
+ id: instanceOf("string")
261
+ }, 60)
262
+ client.disconnect()
263
+ }})
264
+
265
+ it("puts the client in the DISCONNECTED state", function() { with(this) {
266
+ client.disconnect()
267
+ assertEqual( "DISCONNECTED", client.getState() )
268
+ }})
269
+ }})
270
+
271
+ describe("subscribe", function() { with(this) {
272
+ before(function() { with(this) {
273
+ createConnectedClient()
274
+ this.subscribeMessage = {
275
+ channel: "/meta/subscribe",
276
+ clientId: "fakeid",
277
+ subscription: "/foo",
278
+ id: instanceOf("string")
279
+ }
280
+ }})
281
+
282
+ describe("with no prior subscriptions", function() { with(this) {
283
+ it("sends a subscribe message to the server", function() { with(this) {
284
+ expect(transport, "send").given(subscribeMessage, 60)
285
+ client.subscribe("/foo")
286
+ }})
287
+
288
+ // The Bayeux spec says the server should accept a list of subscriptions
289
+ // in one message but the cometD server doesn't actually support this
290
+ it("sends multiple subscribe messages if given an array", function() { with(this) {
291
+ expect(transport, "send").given({
292
+ channel: "/meta/subscribe",
293
+ clientId: "fakeid",
294
+ subscription: "/foo",
295
+ id: instanceOf("string")
296
+ }, 60)
297
+ expect(transport, "send").given({
298
+ channel: "/meta/subscribe",
299
+ clientId: "fakeid",
300
+ subscription: "/bar",
301
+ id: instanceOf("string")
302
+ }, 60)
303
+ client.subscribe(["/foo", "/bar"])
304
+ }})
305
+
306
+ describe("on successful response", function() { with(this) {
307
+ before(function() { with(this) {
308
+ stubResponse({channel: "/meta/subscribe",
309
+ successful: true,
310
+ clientId: "fakeid",
311
+ subscription: "/foo/*" })
312
+ }})
313
+
314
+ it("sets up a listener for the subscribed channel", function() { with(this) {
315
+ var message
316
+ client.subscribe("/foo/*", function(m) { message = m })
317
+ client.receiveMessage({channel: "/foo/bar", data: "hi"})
318
+ assertEqual( "hi", message )
319
+ }})
320
+
321
+ it("does not call the listener for non-matching channels", function() { with(this) {
322
+ var message
323
+ client.subscribe("/foo/*", function(m) { message = m })
324
+ client.receiveMessage({channel: "/bar", data: "hi"})
325
+ assertEqual( undefined, message )
326
+ }})
327
+
328
+ it("activates the subscription", function() { with(this) {
329
+ var active = false
330
+ client.subscribe("/foo/*").callback(function() { active = true })
331
+ assert( active )
332
+ }})
333
+
334
+ describe("with an incoming extension installed", function() { with(this) {
335
+ before(function() { with(this) {
336
+ var extension = {
337
+ incoming: function(message, callback) {
338
+ if (message.data) message.data.changed = true
339
+ callback(message)
340
+ }
341
+ }
342
+ client.addExtension(extension)
343
+ this.message = null
344
+ client.subscribe("/foo/*", function(m) { message = m })
345
+ }})
346
+
347
+ it("passes delivered messages through the extension", function() { with(this) {
348
+ client.receiveMessage({channel: "/foo/bar", data: {hello: "there"}})
349
+ assertEqual( {hello: "there", changed: true}, message )
350
+ }})
351
+ }})
352
+
353
+ describe("with an outgoing extension installed", function() { with(this) {
354
+ before(function() { with(this) {
355
+ var extension = {
356
+ outgoing: function(message, callback) {
357
+ if (message.data) message.data.changed = true
358
+ callback(message)
359
+ }
360
+ }
361
+ client.addExtension(extension)
362
+ this.message = null
363
+ client.subscribe("/foo/*", function(m) { message = m })
364
+ }})
365
+
366
+ it("leaves messages unchanged", function() { with(this) {
367
+ client.receiveMessage({channel: "/foo/bar", data: {hello: "there"}})
368
+ assertEqual( {hello: "there"}, message )
369
+ }})
370
+ }})
371
+
372
+ describe("with an incoming extension that invalidates the response", function() { with(this) {
373
+ before(function() { with(this) {
374
+ var extension = {
375
+ incoming: function(message, callback) {
376
+ if (message.channel === "/meta/subscribe") message.successful = false
377
+ callback(message)
378
+ }
379
+ }
380
+ client.addExtension(extension)
381
+ }})
382
+
383
+ it("does not set up a listener for the subscribed channel", function() { with(this) {
384
+ var message
385
+ client.subscribe("/foo/*", function(m) { message = m })
386
+ client.receiveMessage({channel: "/foo/bar", data: "hi"})
387
+ assertEqual( undefined, message )
388
+ }})
389
+
390
+ it("does not activate the subscription", function() { with(this) {
391
+ var active = false
392
+ client.subscribe("/foo/*").callback(function() { active = true })
393
+ assert( !active )
394
+ }})
395
+ }})
396
+ }})
397
+
398
+ describe("on unsuccessful response", function() { with(this) {
399
+ before(function() { with(this) {
400
+ stubResponse({channel: "/meta/subscribe",
401
+ successful: false,
402
+ error: "403:/meta/foo:Forbidden channel",
403
+ clientId: "fakeid",
404
+ subscription: "/meta/foo" })
405
+ }})
406
+
407
+ it("does not set up a listener for the subscribed channel", function() { with(this) {
408
+ var message
409
+ client.subscribe("/meta/foo", function(m) { message = m })
410
+ client.receiveMessage({channel: "/meta/foo", data: "hi"})
411
+ assertEqual( undefined, message )
412
+ }})
413
+
414
+ it("does not activate the subscription", function() { with(this) {
415
+ var active = false
416
+ client.subscribe("/meta/foo").callback(function() { active = true })
417
+ assert( !active )
418
+ }})
419
+
420
+ it("reports the error through an errback", function() { with(this) {
421
+ var error = null
422
+ client.subscribe("/meta/foo").errback(function(e) { error = e })
423
+ assertEqual( objectIncluding({code: 403, params: ["/meta/foo"], message: "Forbidden channel"}), error )
424
+ }})
425
+ }})
426
+ }})
427
+
428
+ describe("with an existing subscription", function() { with(this) {
429
+ before(function() { with(this) {
430
+ subscribe(client, "/foo/*")
431
+ }})
432
+
433
+ it("does not send another subscribe message to the server", function() { with(this) {
434
+ expect(transport, "send").given(subscribeMessage, 60).exactly(0)
435
+ client.subscribe("/foo/*")
436
+ }})
437
+
438
+ it("sets up another listener on the channel", function() { with(this) {
439
+ client.subscribe("/foo/*", function() { subsCalled += 1 })
440
+ client.receiveMessage({channel: "/foo/bar", data: "hi"})
441
+ assertEqual( 2, subsCalled )
442
+ }})
443
+
444
+ it("activates the subscription", function() { with(this) {
445
+ var active = false
446
+ client.subscribe("/foo/*").callback(function() { active = true })
447
+ assert( active )
448
+ }})
449
+ }})
450
+ }})
451
+
452
+ describe("unsubscribe", function() { with(this) {
453
+ before(function() { with(this) {
454
+ createConnectedClient()
455
+ this.unsubscribeMessage = {
456
+ channel: "/meta/unsubscribe",
457
+ clientId: "fakeid",
458
+ subscription: "/foo/*",
459
+ id: instanceOf("string")
460
+ }
461
+ }})
462
+
463
+ describe("with no subscriptions", function() { with(this) {
464
+ it("does not send an unsubscribe message to the server", function() { with(this) {
465
+ expect(transport, "send").given(unsubscribeMessage, 60).exactly(0)
466
+ client.unsubscribe("/foo/*")
467
+ }})
468
+ }})
469
+
470
+ describe("with a single subscription", function() { with(this) {
471
+ before(function() { with(this) {
472
+ this.message = null
473
+ this.listener = function(m) { message = m }
474
+ subscribe(client, "/foo/*", listener)
475
+ }})
476
+
477
+ it("sends an unsubscribe message to the server", function() { with(this) {
478
+ expect(transport, "send").given(unsubscribeMessage, 60)
479
+ client.unsubscribe("/foo/*")
480
+ }})
481
+
482
+ it("removes the listener from the channel", function() { with(this) {
483
+ client.receiveMessage({channel: "/foo/bar", data: "first"})
484
+ client.unsubscribe("/foo/*", listener)
485
+ client.receiveMessage({channel: "/foo/bar", data: "second"})
486
+ assertEqual( "first", message )
487
+ }})
488
+ }})
489
+
490
+ describe("with multiple subscriptions to the same channel", function() { with(this) {
491
+ before(function() { with(this) {
492
+ this.messages = []
493
+ this.hey = function(m) { messages.push("hey " + m.text) }
494
+ this.bye = function(m) { messages.push("bye " + m.text) }
495
+ subscribe(client, "/foo/*", hey)
496
+ subscribe(client, "/foo/*", bye)
497
+ }})
498
+
499
+ it("removes one of the listeners from the channel", function() { with(this) {
500
+ client.receiveMessage({channel: "/foo/bar", data: {text: "you"}})
501
+ client.unsubscribe("/foo/*", hey)
502
+ client.receiveMessage({channel: "/foo/bar", data: {text: "you"}})
503
+ assertEqual( ["hey you", "bye you", "bye you"], messages)
504
+ }})
505
+
506
+ it("does not send an unsubscribe message if one listener is removed", function() { with(this) {
507
+ expect(transport, "send").given(unsubscribeMessage, 60).exactly(0)
508
+ client.unsubscribe("/foo/*", bye)
509
+ }})
510
+
511
+ it("sends an unsubscribe message if each listener is removed", function() { with(this) {
512
+ expect(transport, "send").given(unsubscribeMessage, 60)
513
+ client.unsubscribe("/foo/*", bye)
514
+ client.unsubscribe("/foo/*", hey)
515
+ }})
516
+
517
+ it("sends an unsubscribe message if all listeners are removed", function() { with(this) {
518
+ expect(transport, "send").given(unsubscribeMessage, 60)
519
+ client.unsubscribe("/foo/*")
520
+ }})
521
+ }})
522
+
523
+ describe("with multiple subscriptions to different channels", function() { with(this) {
524
+ before(function() { with(this) {
525
+ subscribe(client, "/foo")
526
+ subscribe(client, "/bar")
527
+ }})
528
+
529
+ it("sends multiple unsubscribe messages if given an array", function() { with(this) {
530
+ expect(transport, "send").given({
531
+ channel: "/meta/unsubscribe",
532
+ clientId: "fakeid",
533
+ subscription: "/foo",
534
+ id: instanceOf("string")
535
+ }, 60)
536
+ expect(transport, "send").given({
537
+ channel: "/meta/unsubscribe",
538
+ clientId: "fakeid",
539
+ subscription: "/bar",
540
+ id: instanceOf("string")
541
+ }, 60)
542
+ client.unsubscribe(["/foo", "/bar"])
543
+ }})
544
+ }})
545
+ }})
546
+
547
+ describe("publish", function() { with(this) {
548
+ before(function() { this.createConnectedClient() })
549
+
550
+ it("sends the message to the server with an ID", function() { with(this) {
551
+ expect(transport, "send").given({
552
+ channel: "/messages/foo",
553
+ clientId: "fakeid",
554
+ data: {hello: "world"},
555
+ id: instanceOf("string")
556
+ }, 60)
557
+ client.publish("/messages/foo", {hello: "world"})
558
+ }})
559
+
560
+ it("throws an error when publishing to an invalid channel", function() { with(this) {
561
+ expect(transport, "send").given(objectIncluding({channel: "/messages/*"}), 60).exactly(0)
562
+ assertThrows(Error, function() { client.publish("/messages/*", {hello: "world"}) })
563
+ }})
564
+
565
+ describe("with an outgoing extension installed", function() { with(this) {
566
+ before(function() { with(this) {
567
+ var extension = {
568
+ outgoing: function(message, callback) {
569
+ message.ext = {auth: "password"}
570
+ callback(message)
571
+ }
572
+ }
573
+ client.addExtension(extension)
574
+ }})
575
+
576
+ it("passes messages through the extension", function() { with(this) {
577
+ expect(transport, "send").given({
578
+ channel: "/messages/foo",
579
+ clientId: "fakeid",
580
+ data: {hello: "world"},
581
+ id: instanceOf("string"),
582
+ ext: {auth: "password"}
583
+ }, 60)
584
+ client.publish("/messages/foo", {hello: "world"})
585
+ }})
586
+ }})
587
+
588
+ describe("with an incoming extension installed", function() { with(this) {
589
+ before(function() { with(this) {
590
+ var extension = {
591
+ incoming: function(message, callback) {
592
+ message.ext = {auth: "password"}
593
+ callback(message)
594
+ }
595
+ }
596
+ client.addExtension(extension)
597
+ }})
598
+
599
+ it("leaves the message unchanged", function() { with(this) {
600
+ expect(transport, "send").given({
601
+ channel: "/messages/foo",
602
+ clientId: "fakeid",
603
+ data: {hello: "world"},
604
+ id: instanceOf("string")
605
+ }, 60)
606
+ client.publish("/messages/foo", {hello: "world"})
607
+ }})
608
+ }})
609
+ }})
610
+ }})