sinapse 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,101 @@
1
+ require 'test_helper'
2
+
3
+ describe "Sinapse::Channels" do
4
+ include RedisTestHelper
5
+
6
+ before do
7
+ Sinapse.redis do |redis|
8
+ redis.sadd 'sinapse:channels:1', 'room:1'
9
+ redis.sadd 'sinapse:channels:1', 'room:83'
10
+ end
11
+ end
12
+
13
+ after do
14
+ Sinapse.redis { |redis| redis.del 'sinapse:channels:1' }
15
+ end
16
+
17
+ let(:user) { User.new(1) }
18
+ let(:room) { Room.new(1) }
19
+
20
+ it "key" do
21
+ assert_equal "sinapse:channels:1", User.new(1).sinapse.key
22
+ assert_equal "sinapse:channels:345", User.new(345).sinapse.key
23
+ assert_equal "sinapse:channels:345:add", User.new(345).sinapse.key(:add)
24
+ assert_equal "sinapse:channels:345:remove", User.new(345).sinapse.key(:remove)
25
+ end
26
+
27
+ describe "channel_for" do
28
+ it "accepts a string" do
29
+ assert_equal 'room:1', user.sinapse.channel_for('room:1')
30
+ assert_equal 'room:876', user.sinapse.channel_for('room:876')
31
+ end
32
+
33
+ it "accepts a record" do
34
+ assert_equal 'room:1', user.sinapse.channel_for(Room.new(1))
35
+ assert_equal 'room:4321', user.sinapse.channel_for(Room.new(4321))
36
+ end
37
+ end
38
+
39
+ it "channels" do
40
+ assert_equal ['room:1', 'room:83'], user.sinapse.channels.sort
41
+ assert_equal [], User.new(2).sinapse.channels
42
+ end
43
+
44
+ it "has_channel?" do
45
+ assert user.sinapse.has_channel?(room)
46
+ refute user.sinapse.has_channel?('room:2')
47
+ refute User.new(2).sinapse.has_channel?(room)
48
+ end
49
+
50
+ it "clear" do
51
+ user.sinapse.clear
52
+ assert_empty user.sinapse.channels
53
+ end
54
+
55
+ it "destroy" do
56
+ user.sinapse.destroy
57
+
58
+ Sinapse.redis do |redis|
59
+ assert_nil redis.get(user.sinapse.auth.key)
60
+ assert_nil redis.get(user.sinapse.auth.token_key('a1b2c3d4e5f6'))
61
+ end
62
+
63
+ assert_empty user.sinapse.channels
64
+ end
65
+
66
+ describe "add_channel" do
67
+ let(:room) { Room.new(12345) }
68
+
69
+ it "adds channel to the list" do
70
+ user.sinapse.add_channel(room)
71
+ assert user.sinapse.has_channel?(room)
72
+ end
73
+
74
+ it "publishes a message" do
75
+ EM.run do
76
+ wait_for_message('sinapse:channels:*') do |channel, message|
77
+ assert_equal user.sinapse.key(:add), channel
78
+ assert_equal room.sinapse_channel, message
79
+ end
80
+ publish_until_received { user.sinapse.add_channel(room) }
81
+ end
82
+ end
83
+ end
84
+
85
+ describe "remove_channel" do
86
+ it "removes channel from the list" do
87
+ user.sinapse.remove_channel(room)
88
+ refute user.sinapse.has_channel?(room)
89
+ end
90
+
91
+ it "publishes a message" do
92
+ EM.run do
93
+ wait_for_message('sinapse:channels:*') do |channel, message|
94
+ assert_equal user.sinapse.key(:remove), channel
95
+ assert_equal room.sinapse_channel, message
96
+ end
97
+ publish_until_received { user.sinapse.remove_channel(room) }
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,167 @@
1
+ require 'test_helper'
2
+ require 'sinapse/cross_origin_resource_sharing'
3
+
4
+ describe "Sinapse::Rack::CrossOriginResourceSharing" do
5
+ let(:app) { Minitest::Mock.new }
6
+
7
+ describe "regular request" do
8
+ it "calls app" do
9
+ env = { 'REQUEST_METHOD' => 'POST' }
10
+ app.expect(:call, [200, {}, ''], [env])
11
+ assert_equal [200, {}, ''], cors(origin: '*').call(env)
12
+ app.verify
13
+ end
14
+ end
15
+
16
+ describe "preflight check" do
17
+ let(:env) do
18
+ { 'HTTP_ACCESS_CONTROL_REQUEST_METHOD' => 'GET', 'REQUEST_METHOD' => 'OPTIONS' }
19
+ end
20
+
21
+ describe "when domain is *" do
22
+ let(:domain) { %w(test.host example.com somewhere.org)[rand(0..2)] }
23
+
24
+ it "allows any domain" do
25
+ status, headers, body = cors(origin: '*')
26
+ .call(env.merge('HTTP_ORIGIN' => "http://#{domain}"))
27
+
28
+ assert_equal 200, status
29
+ assert_equal 'text/plain', headers['Content-Type']
30
+ assert_equal "http://#{domain}", headers['Access-Control-Allow-Origin']
31
+ assert_nil headers['Access-Control-Allow-Headers']
32
+ assert_empty body
33
+ end
34
+ end
35
+
36
+ describe "when origin is a domain" do
37
+ it "allows request for HTTP" do
38
+ status, headers, _ = cors(origin: 'example.com')
39
+ .call(env.merge('HTTP_ORIGIN' => 'http://example.com'))
40
+ assert_equal 200, status
41
+ assert_equal 'http://example.com', headers['Access-Control-Allow-Origin']
42
+ end
43
+
44
+ it "allows request for HTTPS" do
45
+ status, headers, _ = cors(origin: 'example.com')
46
+ .call(env.merge('HTTP_ORIGIN' => 'https://example.com'))
47
+ assert_equal 200, status
48
+ assert_equal 'https://example.com', headers['Access-Control-Allow-Origin']
49
+ end
50
+
51
+ it "refuses another domain" do
52
+ status, headers, _ = cors(origin: 'example.com')
53
+ .call(env.merge('HTTP_ORIGIN' => 'http://test.host'))
54
+ assert_equal 400, status
55
+ assert_nil headers['Access-Control-Allow-Origin']
56
+ end
57
+ end
58
+
59
+ describe "when origin is a string" do
60
+ it "allows specific origin" do
61
+ status, headers, _ = cors(origin: 'http://example.com')
62
+ .call(env.merge('HTTP_ORIGIN' => 'http://example.com'))
63
+ assert_equal 200, status
64
+ assert_equal 'http://example.com', headers['Access-Control-Allow-Origin']
65
+ end
66
+
67
+ it "refuses another origin" do
68
+ status, headers, _ = cors(origin: 'https://example.com')
69
+ .call(env.merge('HTTP_ORIGIN' => 'http://example.com'))
70
+ assert_equal 400, status
71
+ assert_nil headers['Access-Control-Allow-Origin']
72
+ end
73
+ end
74
+
75
+ describe "when origin is a regexp" do
76
+ let(:domain) { %w(example.com test.host)[rand(0..1)] }
77
+
78
+ it "allows matching origin" do
79
+ status, headers, _ = cors(origin: %r(^https?://(example\.com|test\.host)))
80
+ .call(env.merge('HTTP_ORIGIN' => "http://#{domain}"))
81
+ assert_equal 200, status
82
+ assert_equal "http://#{domain}", headers['Access-Control-Allow-Origin']
83
+ end
84
+
85
+ it "refuses an origin that doesn't match" do
86
+ status, headers, _ = cors(origin: %r(^http?://(example\.com|test\.host)))
87
+ .call(env.merge('HTTP_ORIGIN' => 'http://somewhere.org'))
88
+ assert_equal 400, status
89
+ assert_nil headers['Access-Control-Allow-Origin']
90
+ end
91
+ end
92
+
93
+ describe "methods" do
94
+ let(:methods) { %w(GET POST DELETE) }
95
+ let(:method) { methods[rand(0..2)] }
96
+
97
+ it "accepts method" do
98
+ status, headers, _ = cors(origin: '*', methods: methods).call(env.merge(
99
+ 'HTTP_ORIGIN' => "http://test.host",
100
+ 'HTTP_ACCESS_CONTROL_REQUEST_METHOD' => method
101
+ ))
102
+ assert_equal 200, status
103
+ assert_equal "http://test.host", headers['Access-Control-Allow-Origin']
104
+ assert_equal 'GET, POST, DELETE', headers['Access-Control-Allow-Methods']
105
+ end
106
+
107
+ it "refuses method" do
108
+ status, headers, _ = cors(origin: '*', methods: methods).call(env.merge(
109
+ 'HTTP_ORIGIN' => "http://test.host",
110
+ 'HTTP_ACCESS_CONTROL_REQUEST_METHOD' => 'PATCH'
111
+ ))
112
+ assert_equal 400, status
113
+ assert_nil headers['Access-Control-Allow-Origin']
114
+ assert_nil headers['Access-Control-Allow-Methods']
115
+ end
116
+ end
117
+ end
118
+
119
+ describe "actual request" do
120
+ it "adds headers when origin header is present" do
121
+ env = {
122
+ 'HTTP_ORIGIN' => 'http://example.com',
123
+ 'REQUEST_METHOD' => 'POST'
124
+ }
125
+ app.expect(:call, [200, {}, ''], [env])
126
+ status, headers, body = cors(origin: '*', methods: %w(POST)).call(env)
127
+ app.verify
128
+
129
+ assert_equal 200, status
130
+ assert_equal '', body
131
+ assert_equal 'http://example.com', headers['Access-Control-Allow-Origin']
132
+ assert_equal 'POST', headers['Access-Control-Allow-Methods']
133
+ end
134
+
135
+ it "skips headers when origin is refused" do
136
+ env = {
137
+ 'HTTP_ORIGIN' => 'http://test.com',
138
+ 'REQUEST_METHOD' => 'POST'
139
+ }
140
+ app.expect(:call, [200, {}, ''], [env])
141
+ status, headers, _ = cors(origin: 'example.com', methods: %w(POST)).call(env)
142
+ app.verify
143
+
144
+ assert_equal 200, status
145
+ assert_nil headers['Access-Control-Allow-Origin']
146
+ assert_nil headers['Access-Control-Allow-Methods']
147
+ end
148
+
149
+ it "skips headers when method is refused" do
150
+ env = {
151
+ 'HTTP_ORIGIN' => 'http://test.host',
152
+ 'REQUEST_METHOD' => 'GET'
153
+ }
154
+ app.expect(:call, [200, {}, ''], [env])
155
+ status, headers, _ = cors(origin: 'test.host', methods: %w(POST)).call(env)
156
+ app.verify
157
+
158
+ assert_equal 200, status
159
+ assert_nil headers['Access-Control-Allow-Origin']
160
+ assert_nil headers['Access-Control-Allow-Methods']
161
+ end
162
+ end
163
+
164
+ def cors(options = {})
165
+ Sinapse::Rack::CrossOriginResourceSharing.new(app, options)
166
+ end
167
+ end
@@ -0,0 +1,32 @@
1
+ require 'test_helper'
2
+
3
+ describe "Sinapse::Publishable" do
4
+ include RedisTestHelper
5
+
6
+ let(:room) { Room.new(1) }
7
+
8
+ it "sinapse_channel" do
9
+ assert_equal 'room:1', Room.new(1).sinapse_channel
10
+ assert_equal 'room:83', Room.new(83).sinapse_channel
11
+ end
12
+
13
+ it "publish" do
14
+ EM.run do
15
+ wait_for_message('room:*') do |channel, message|
16
+ assert_equal room.sinapse_channel, channel
17
+ assert_equal 'hello room 1', message
18
+ end
19
+ publish_until_received { room.publish('hello room 1') }
20
+ end
21
+ end
22
+
23
+ it "publish with event type" do
24
+ EM.run do
25
+ wait_for_message('room:*') do |channel, message|
26
+ assert_equal room.sinapse_channel, channel
27
+ assert_equal ['hello', 'hello room 1'], message
28
+ end
29
+ publish_until_received { room.publish('hello room 1', event: 'hello') }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,189 @@
1
+ require 'test_helper'
2
+ require 'sinapse/server'
3
+
4
+ describe Sinapse::Server do
5
+ include Goliath::TestHelper
6
+ include RedisTestHelper
7
+
8
+ before do
9
+ EM.synchrony do
10
+ redis.set('sinapse:tokens:valid', '1')
11
+ redis.set('sinapse:tokens:empty', '2')
12
+ redis.sadd('sinapse:channels:1', 'user:1')
13
+ redis.sadd('sinapse:channels:1', 'room:2')
14
+ redis.sadd('sinapse:channels:1', 'room:4')
15
+ EM.stop_event_loop
16
+ end
17
+ end
18
+
19
+ after do
20
+ EM.synchrony do
21
+ redis.del('sinapse:tokens:valid')
22
+ redis.del('sinapse:tokens:empty')
23
+ redis.del('sinapse:channels:1')
24
+ EM.stop_event_loop
25
+ end
26
+ end
27
+
28
+ describe "authentication" do
29
+ it "returns an event-stream on success" do
30
+ sse_connect do |client|
31
+ assert_equal 'close', client.headers['CONNECTION']
32
+ assert_equal 'text/event-stream', client.headers['CONTENT_TYPE']
33
+ assert_equal "retry: 5000\nevent: authentication\ndata: ok\n\n", client.receive
34
+ end
35
+ end
36
+
37
+ it "won't authenticate without token" do
38
+ connect(query: { access_token: '' }) do |client|
39
+ assert_equal 400, client.response_header.status
40
+ end
41
+ end
42
+
43
+ it "won't authenticate with unknown token" do
44
+ connect(query: { access_token: 'invalid' }) do |client|
45
+ assert_equal 401, client.response_header.status
46
+ end rescue LocalJumpError
47
+ end
48
+
49
+ it "won't authenticate when user has no channels" do
50
+ connect(query: { access_token: 'empty' }) do |client|
51
+ assert_equal 401, client.response_header.status
52
+ end rescue LocalJumpError
53
+ end
54
+ end
55
+
56
+ describe "cross origin resource sharing (when requested)" do
57
+ it "returns CORS headers on auth success" do
58
+ sse_connect(head: { origin: 'http://example.com' }) do |client|
59
+ assert_equal 'text/event-stream', client.headers['CONTENT_TYPE']
60
+ assert_equal 'http://example.com', client.headers['ACCESS_CONTROL_ALLOW_ORIGIN']
61
+ refute_nil client.headers['ACCESS_CONTROL_ALLOW_METHODS']
62
+ end
63
+ end
64
+
65
+ it "skips CORS headers when configured origin doesn't match" do
66
+ stub_origin('test.host') do
67
+ sse_connect(head: { origin: "http://example.com" }) do |client|
68
+ assert_nil client.headers['ACCESS_CONTROL_ALLOW_ORIGIN']
69
+ assert_nil client.headers['ACCESS_CONTROL_ALLOW_METHODS']
70
+ end
71
+ end
72
+ end
73
+
74
+ def stub_origin(forced)
75
+ Sinapse::Server.middlewares.each do |middleware, params, _|
76
+ next unless middleware == Sinapse::Rack::CrossOriginResourceSharing
77
+ options = params.first
78
+ original = options[:origin]
79
+ options[:origin] = forced
80
+ yield
81
+ options[:origin] = original
82
+ return
83
+ end
84
+ end
85
+ end
86
+
87
+ describe "pub/sub" do
88
+ let(:channel_name) { 'user:1' }
89
+
90
+ # waits for server to be listening
91
+ def wait
92
+ sleep 0.001 until redis.publish('sinapse:channels:1:wait', nil) == 1
93
+ end
94
+
95
+ it "proxies published messages" do
96
+ sse_connect do |client|
97
+ client.receive # skips authentication message
98
+ wait
99
+
100
+ assert_equal 1, publish(channel_name, "payload message")
101
+ #assert_equal "event: #{channel_name}\ndata: payload message\n\n", client.receive
102
+ assert_equal "data: payload message\n\n", client.receive
103
+
104
+ assert_equal 1, publish(channel_name, "another message")
105
+ #assert_equal "event: #{channel_name}\ndata: another message\n\n", client.receive
106
+ assert_equal "data: another message\n\n", client.receive
107
+ end
108
+ end
109
+
110
+ it "disconnects from server on connection close" do
111
+ sse_connect do |client|
112
+ client.close
113
+ EM.synchrony { assert_equal 0, redis.publish(channel_name, "message") }
114
+ end
115
+ end
116
+
117
+ it "updates subscriptions when the list changes" do
118
+ sse_connect do |client|
119
+ client.receive
120
+
121
+ redis.srem('sinapse:channels:1', 'room:2')
122
+ redis.publish('sinapse:channels:1:remove', 'room:2')
123
+
124
+ redis.sadd('sinapse:channels:1', 'room:5')
125
+ redis.publish('sinapse:channels:1:add', 'room:5')
126
+
127
+ assert_equal 1, publish('room:4', "message for room 4")
128
+ #assert_equal "event: room:4\ndata: message for room 4\n\n", client.receive
129
+ assert_equal "data: message for room 4\n\n", client.receive
130
+
131
+ assert_equal 1, publish('room:5', "message for room 5")
132
+ #assert_equal "event: room:5\ndata: message for room 5\n\n", client.receive
133
+ assert_equal "data: message for room 5\n\n", client.receive
134
+
135
+ assert_equal 0, publish('room:2', "message for room 2")
136
+ end
137
+ end
138
+
139
+ it "sets channel name as event type" do
140
+ Sinapse::Config.stub(:channel_event, true) do
141
+ sse_connect do |client|
142
+ client.receive; wait
143
+
144
+ assert_equal 1, publish(channel_name, "payload message")
145
+ assert_equal "event: #{channel_name}\ndata: payload message\n\n", client.receive
146
+ end
147
+ end
148
+ end
149
+
150
+ it "publishes with event type" do
151
+ Sinapse::Config.stub(:channel_event, true) do
152
+ sse_connect do |client|
153
+ client.receive; wait
154
+
155
+ assert_equal 1, publish(channel_name, "payload message", "hello:world")
156
+ assert_equal "event: hello:world\ndata: payload message\n\n", client.receive
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ describe "retry" do
163
+ it "uses configured value" do
164
+ Sinapse::Config.stub(:retry, 12000) do
165
+ sse_connect(head: { origin: 'http://example.com' }) do |client|
166
+ assert_match(/retry: 12000\n/, client.receive)
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+ describe "keep alive" do
173
+ it "periodically sends a comment" do
174
+ Sinapse::Config.stub(:keep_alive, 0.001) do
175
+ sse_connect do |client|
176
+ client.receive # skips authentication message
177
+ assert_equal ":\n", client.receive
178
+ assert_equal ":\n", client.receive
179
+ assert_equal ":\n", client.receive
180
+ end
181
+ end
182
+ end
183
+ end
184
+
185
+ def publish(channel, message, event = nil)
186
+ data = MessagePack.pack(event ? [event, message] : message)
187
+ redis.publish(channel, data)
188
+ end
189
+ end