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.
- data/History.txt +14 -0
- data/README.rdoc +98 -0
- data/Rakefile +17 -15
- data/lib/faye-browser-min.js +1 -1
- data/lib/faye.rb +14 -5
- data/lib/faye/adapters/rack_adapter.rb +12 -5
- data/lib/faye/engines/base.rb +62 -0
- data/lib/faye/engines/connection.rb +63 -0
- data/lib/faye/engines/memory.rb +89 -0
- data/lib/faye/engines/redis.rb +141 -0
- data/lib/faye/error.rb +16 -4
- data/lib/faye/mixins/publisher.rb +6 -0
- data/lib/faye/protocol/channel.rb +34 -86
- data/lib/faye/protocol/client.rb +36 -52
- data/lib/faye/protocol/extensible.rb +3 -0
- data/lib/faye/protocol/server.rb +119 -169
- data/lib/faye/transport/http.rb +45 -0
- data/lib/faye/transport/local.rb +15 -0
- data/lib/faye/{network → transport}/transport.rb +36 -49
- data/spec/browser.html +35 -0
- data/spec/install.sh +48 -0
- data/spec/javascript/channel_spec.js +15 -0
- data/spec/javascript/client_spec.js +610 -0
- data/spec/javascript/engine_spec.js +319 -0
- data/spec/javascript/faye_spec.js +15 -0
- data/spec/javascript/grammar_spec.js +66 -0
- data/spec/javascript/node_adapter_spec.js +276 -0
- data/spec/javascript/server/connect_spec.js +168 -0
- data/spec/javascript/server/disconnect_spec.js +121 -0
- data/spec/javascript/server/extensions_spec.js +60 -0
- data/spec/javascript/server/handshake_spec.js +153 -0
- data/spec/javascript/server/subscribe_spec.js +245 -0
- data/spec/javascript/server/unsubscribe_spec.js +245 -0
- data/spec/javascript/server_spec.js +146 -0
- data/spec/javascript/transport_spec.js +130 -0
- data/spec/node.js +34 -0
- data/spec/ruby/channel_spec.rb +17 -0
- data/spec/ruby/client_spec.rb +615 -0
- data/spec/ruby/engine_spec.rb +312 -0
- data/spec/ruby/faye_spec.rb +14 -0
- data/spec/ruby/grammar_spec.rb +68 -0
- data/spec/ruby/rack_adapter_spec.rb +209 -0
- data/spec/ruby/server/connect_spec.rb +170 -0
- data/spec/ruby/server/disconnect_spec.rb +120 -0
- data/spec/ruby/server/extensions_spec.rb +69 -0
- data/spec/ruby/server/handshake_spec.rb +151 -0
- data/spec/ruby/server/subscribe_spec.rb +247 -0
- data/spec/ruby/server/unsubscribe_spec.rb +247 -0
- data/spec/ruby/server_spec.rb +138 -0
- data/spec/ruby/transport_spec.rb +128 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/testswarm.pl +200 -0
- data/spec/thin_proxy.rb +36 -0
- metadata +119 -84
- data/Manifest.txt +0 -27
- data/README.txt +0 -98
- data/lib/faye/protocol/connection.rb +0 -111
- data/test/scenario.rb +0 -172
- data/test/test_channel.rb +0 -54
- data/test/test_clients.rb +0 -381
- data/test/test_grammar.rb +0 -86
- 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
|