face-faye 0.8.9

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