ably 0.1.5 → 0.1.6
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.
- checksums.yaml +4 -4
- data/README.md +11 -1
- data/ably.gemspec +4 -3
- data/lib/ably.rb +6 -2
- data/lib/ably/auth.rb +24 -16
- data/lib/ably/exceptions.rb +16 -5
- data/lib/ably/{realtime/models → models}/error_info.rb +9 -11
- data/lib/ably/models/idiomatic_ruby_wrapper.rb +57 -26
- data/lib/ably/{realtime/models → models}/message.rb +45 -38
- data/lib/ably/{realtime/models → models}/nil_channel.rb +4 -4
- data/lib/ably/{rest/models/paged_resource.rb → models/paginated_resource.rb} +21 -10
- data/lib/ably/models/presence_message.rb +126 -0
- data/lib/ably/{realtime/models → models}/protocol_message.rb +76 -38
- data/lib/ably/models/token.rb +74 -0
- data/lib/ably/modules/channels_collection.rb +49 -0
- data/lib/ably/modules/conversions.rb +2 -0
- data/lib/ably/modules/event_emitter.rb +43 -8
- data/lib/ably/modules/event_machine_helpers.rb +1 -0
- data/lib/ably/modules/http_helpers.rb +9 -2
- data/lib/ably/modules/message_pack.rb +14 -0
- data/lib/ably/modules/model_common.rb +29 -0
- data/lib/ably/modules/{state.rb → state_emitter.rb} +8 -7
- data/lib/ably/realtime.rb +37 -7
- data/lib/ably/realtime/channel.rb +154 -31
- data/lib/ably/realtime/channels.rb +47 -0
- data/lib/ably/realtime/client.rb +39 -33
- data/lib/ably/realtime/client/incoming_message_dispatcher.rb +50 -21
- data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +9 -11
- data/lib/ably/realtime/connection.rb +148 -79
- data/lib/ably/realtime/connection/connection_state_machine.rb +111 -0
- data/lib/ably/realtime/connection/websocket_transport.rb +161 -0
- data/lib/ably/realtime/presence.rb +270 -0
- data/lib/ably/rest.rb +14 -3
- data/lib/ably/rest/channel.rb +3 -3
- data/lib/ably/rest/channels.rb +26 -12
- data/lib/ably/rest/client.rb +42 -25
- data/lib/ably/rest/middleware/exceptions.rb +21 -23
- data/lib/ably/rest/middleware/external_exceptions.rb +8 -10
- data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +17 -0
- data/lib/ably/rest/middleware/parse_json.rb +9 -2
- data/lib/ably/rest/middleware/parse_message_pack.rb +6 -2
- data/lib/ably/rest/presence.rb +4 -4
- data/lib/ably/version.rb +1 -1
- data/spec/acceptance/realtime/channel_history_spec.rb +125 -0
- data/spec/acceptance/realtime/channel_spec.rb +135 -63
- data/spec/acceptance/realtime/connection_spec.rb +86 -0
- data/spec/acceptance/realtime/message_spec.rb +116 -94
- data/spec/acceptance/realtime/presence_history_spec.rb +0 -0
- data/spec/acceptance/realtime/presence_spec.rb +277 -0
- data/spec/acceptance/rest/auth_spec.rb +351 -347
- data/spec/acceptance/rest/base_spec.rb +43 -26
- data/spec/acceptance/rest/channel_spec.rb +88 -83
- data/spec/acceptance/rest/channels_spec.rb +32 -28
- data/spec/acceptance/rest/presence_spec.rb +83 -63
- data/spec/acceptance/rest/stats_spec.rb +38 -37
- data/spec/acceptance/rest/time_spec.rb +10 -6
- data/spec/integration/modules/{state_spec.rb → state_emitter_spec.rb} +16 -2
- data/spec/spec_helper.rb +14 -0
- data/spec/support/api_helper.rb +4 -0
- data/spec/support/model_helper.rb +28 -9
- data/spec/support/protocol_msgbus_helper.rb +8 -1
- data/spec/support/test_app.rb +24 -14
- data/spec/unit/{realtime → models}/error_info_spec.rb +4 -4
- data/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +46 -9
- data/spec/unit/models/message_spec.rb +229 -0
- data/spec/unit/{rest/paged_resource_spec.rb → models/paginated_resource_spec.rb} +19 -11
- data/spec/unit/models/presence_message_spec.rb +230 -0
- data/spec/unit/models/protocol_message_spec.rb +280 -0
- data/spec/unit/{token_spec.rb → models/token_spec.rb} +18 -22
- data/spec/unit/modules/conversions_spec.rb +1 -1
- data/spec/unit/modules/event_emitter_spec.rb +36 -4
- data/spec/unit/realtime/channel_spec.rb +76 -2
- data/spec/unit/realtime/channels_spec.rb +50 -0
- data/spec/unit/realtime/client_spec.rb +31 -1
- data/spec/unit/realtime/connection_spec.rb +8 -15
- data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +6 -6
- data/spec/unit/realtime/presence_spec.rb +100 -0
- data/spec/unit/rest/channels_spec.rb +48 -0
- metadata +72 -38
- data/lib/ably/realtime/models/shared.rb +0 -17
- data/lib/ably/rest/models/message.rb +0 -64
- data/lib/ably/rest/models/presence_message.rb +0 -21
- data/lib/ably/token.rb +0 -80
- data/spec/unit/realtime/message_spec.rb +0 -117
- data/spec/unit/realtime/protocol_message_spec.rb +0 -172
- data/spec/unit/rest/message_spec.rb +0 -75
File without changes
|
@@ -0,0 +1,277 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
describe 'Ably::Realtime::Presence Messages' do
|
5
|
+
include RSpec::EventMachine
|
6
|
+
|
7
|
+
[:msgpack, :json].each do |protocol|
|
8
|
+
context "over #{protocol}" do
|
9
|
+
let(:default_options) { { api_key: api_key, environment: environment, protocol: protocol } }
|
10
|
+
|
11
|
+
let(:channel_name) { "presence-#{SecureRandom.hex(2)}" }
|
12
|
+
|
13
|
+
let(:anonymous_client) { Ably::Realtime::Client.new(default_options) }
|
14
|
+
let(:client_one) { Ably::Realtime::Client.new(default_options.merge(client_id: SecureRandom.hex(4))) }
|
15
|
+
let(:client_two) { Ably::Realtime::Client.new(default_options.merge(client_id: SecureRandom.hex(4))) }
|
16
|
+
|
17
|
+
let(:channel_anonymous_client) { anonymous_client.channel(channel_name) }
|
18
|
+
let(:presence_anonymous_client) { channel_anonymous_client.presence }
|
19
|
+
let(:channel_client_one) { client_one.channel(channel_name) }
|
20
|
+
let(:channel_rest_client_one) { client_one.rest_client.channel(channel_name) }
|
21
|
+
let(:presence_client_one) { channel_client_one.presence }
|
22
|
+
let(:channel_client_two) { client_two.channel(channel_name) }
|
23
|
+
let(:presence_client_two) { channel_client_two.presence }
|
24
|
+
|
25
|
+
let(:client_data_payload) { SecureRandom.hex(8) }
|
26
|
+
|
27
|
+
specify 'an attached channel that is not presence maintains presence state' do
|
28
|
+
run_reactor do
|
29
|
+
channel_anonymous_client.attach do
|
30
|
+
presence_anonymous_client.subscribe(:enter) do |presence_message|
|
31
|
+
expect(presence_message.client_id).to eql(client_one.client_id)
|
32
|
+
members = presence_anonymous_client.get
|
33
|
+
expect(members.first.client_id).to eql(client_one.client_id)
|
34
|
+
expect(members.first.action).to eq(:enter)
|
35
|
+
|
36
|
+
presence_anonymous_client.subscribe(:leave) do |presence_message|
|
37
|
+
expect(presence_message.client_id).to eql(client_one.client_id)
|
38
|
+
members = presence_anonymous_client.get
|
39
|
+
expect(members.count).to eql(0)
|
40
|
+
|
41
|
+
stop_reactor
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
presence_client_one.enter do
|
47
|
+
presence_client_one.leave
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
it '#enter allows client_id to be set on enter for anonymous clients' do
|
53
|
+
run_reactor do
|
54
|
+
channel_anonymous_client.presence.enter client_id: "123"
|
55
|
+
|
56
|
+
channel_anonymous_client.presence.subscribe do |presence|
|
57
|
+
expect(presence.client_id).to eq("123")
|
58
|
+
stop_reactor
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'enters and then leaves' do
|
64
|
+
leave_callback_called = false
|
65
|
+
run_reactor do
|
66
|
+
presence_client_one.enter do
|
67
|
+
presence_client_one.leave do |presence|
|
68
|
+
leave_callback_called = true
|
69
|
+
end
|
70
|
+
presence_client_one.on(:left) do
|
71
|
+
EventMachine.next_tick do
|
72
|
+
expect(leave_callback_called).to eql(true)
|
73
|
+
stop_reactor
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'enters the :left state if the channel detaches' do
|
81
|
+
detached = false
|
82
|
+
run_reactor do
|
83
|
+
channel_client_one.presence.on(:left) do
|
84
|
+
expect(channel_client_one.presence.state).to eq(:left)
|
85
|
+
EventMachine.next_tick do
|
86
|
+
expect(detached).to eq(true)
|
87
|
+
stop_reactor
|
88
|
+
end
|
89
|
+
end
|
90
|
+
channel_client_one.presence.enter do |presence|
|
91
|
+
expect(presence.state).to eq(:entered)
|
92
|
+
channel_client_one.detach do
|
93
|
+
expect(channel_client_one.state).to eq(:detached)
|
94
|
+
detached = true
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
specify '#get returns the current member on the channel' do
|
101
|
+
run_reactor do
|
102
|
+
presence_client_one.enter do
|
103
|
+
members = presence_client_one.get
|
104
|
+
expect(members.count).to eq(1)
|
105
|
+
|
106
|
+
expect(client_one.client_id).to_not be_nil
|
107
|
+
|
108
|
+
this_member = members.first
|
109
|
+
expect(this_member.client_id).to eql(client_one.client_id)
|
110
|
+
|
111
|
+
stop_reactor
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
specify '#get returns no members on the channel following an enter and leave' do
|
117
|
+
run_reactor do
|
118
|
+
presence_client_one.enter do
|
119
|
+
presence_client_one.leave do
|
120
|
+
expect(presence_client_one.get).to eq([])
|
121
|
+
stop_reactor
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
specify 'verify two clients appear in members from #get' do
|
128
|
+
run_reactor do
|
129
|
+
presence_client_one.enter(client_data: client_data_payload)
|
130
|
+
presence_client_two.enter
|
131
|
+
|
132
|
+
entered_callback = Proc.new do
|
133
|
+
next unless presence_client_one.state == :entered && presence_client_two.state == :entered
|
134
|
+
|
135
|
+
EventMachine.add_timer(0.25) do
|
136
|
+
expect(presence_client_one.get.count).to eq(presence_client_two.get.count)
|
137
|
+
|
138
|
+
members = presence_client_one.get
|
139
|
+
member_client_one = members.find { |presence| presence.client_id == client_one.client_id }
|
140
|
+
member_client_two = members.find { |presence| presence.client_id == client_two.client_id }
|
141
|
+
|
142
|
+
expect(member_client_one).to be_a(Ably::Models::PresenceMessage)
|
143
|
+
expect(member_client_one.client_data).to eql(client_data_payload)
|
144
|
+
expect(member_client_two).to be_a(Ably::Models::PresenceMessage)
|
145
|
+
|
146
|
+
stop_reactor
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
presence_client_one.on :entered, &entered_callback
|
151
|
+
presence_client_two.on :entered, &entered_callback
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
specify '#subscribe and #unsubscribe to presence events' do
|
156
|
+
run_reactor do
|
157
|
+
client_two_subscribe_messages = []
|
158
|
+
|
159
|
+
subscribe_client_one_leaving_callback = Proc.new do |presence_message|
|
160
|
+
expect(presence_message.client_id).to eql(client_one.client_id)
|
161
|
+
expect(presence_message.client_data).to eql(client_data_payload)
|
162
|
+
expect(presence_message.action).to eq(:leave)
|
163
|
+
|
164
|
+
stop_reactor
|
165
|
+
end
|
166
|
+
|
167
|
+
subscribe_self_callback = Proc.new do |presence_message|
|
168
|
+
if presence_message.client_id == client_two.client_id
|
169
|
+
expect(presence_message.action).to eq(:enter)
|
170
|
+
|
171
|
+
presence_client_two.unsubscribe &subscribe_self_callback
|
172
|
+
presence_client_two.subscribe &subscribe_client_one_leaving_callback
|
173
|
+
|
174
|
+
presence_client_one.leave client_data: client_data_payload
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
presence_client_one.enter do
|
179
|
+
presence_client_two.enter
|
180
|
+
presence_client_two.subscribe &subscribe_self_callback
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
specify 'verify REST #get returns current members' do
|
186
|
+
run_reactor do
|
187
|
+
presence_client_one.enter(client_data: client_data_payload) do
|
188
|
+
members = channel_rest_client_one.presence.get
|
189
|
+
this_member = members.first
|
190
|
+
|
191
|
+
expect(this_member).to be_a(Ably::Models::PresenceMessage)
|
192
|
+
expect(this_member.client_id).to eql(client_one.client_id)
|
193
|
+
expect(this_member.client_data).to eql(client_data_payload)
|
194
|
+
|
195
|
+
stop_reactor
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
specify 'verify REST #get returns no members once left' do
|
201
|
+
run_reactor do
|
202
|
+
presence_client_one.enter(client_data: client_data_payload) do
|
203
|
+
presence_client_one.leave do
|
204
|
+
members = channel_rest_client_one.presence.get
|
205
|
+
expect(members.count).to eql(0)
|
206
|
+
stop_reactor
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
specify 'expect :left event once underlying connection is closed' do
|
213
|
+
run_reactor do
|
214
|
+
presence_client_one.on(:left) do
|
215
|
+
expect(presence_client_one.state).to eq(:left)
|
216
|
+
stop_reactor
|
217
|
+
end
|
218
|
+
presence_client_one.enter do
|
219
|
+
client_one.close
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
specify 'expect :left event with no client data to retain original client_data in Leave event' do
|
225
|
+
run_reactor do
|
226
|
+
presence_client_one.subscribe(:leave) do |message|
|
227
|
+
expect(presence_client_one.get.count).to eq(0)
|
228
|
+
expect(message.client_data).to eq(client_data_payload)
|
229
|
+
stop_reactor
|
230
|
+
end
|
231
|
+
presence_client_one.enter(client_data: client_data_payload) do
|
232
|
+
presence_client_one.leave
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
specify '#update automatically connects' do
|
238
|
+
run_reactor do
|
239
|
+
presence_client_one.update(client_data: client_data_payload) do
|
240
|
+
expect(presence_client_one.state).to eq(:entered)
|
241
|
+
stop_reactor
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
specify '#update changes the client_data' do
|
247
|
+
run_reactor do
|
248
|
+
presence_client_one.enter(client_data: 'prior') do
|
249
|
+
presence_client_one.update(client_data: client_data_payload)
|
250
|
+
end
|
251
|
+
presence_client_one.subscribe(:update) do |message|
|
252
|
+
expect(message.client_data).to eql(client_data_payload)
|
253
|
+
stop_reactor
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
it 'raises an exception if client_id is not set' do
|
259
|
+
run_reactor do
|
260
|
+
expect { channel_anonymous_client.presence.enter }.to raise_error(Ably::Exceptions::Standard, /without a client_id/)
|
261
|
+
stop_reactor
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
it '#leave raises an exception if not entered' do
|
266
|
+
run_reactor do
|
267
|
+
expect { channel_anonymous_client.presence.leave }.to raise_error(Ably::Exceptions::Standard, /Unable to leave presence channel that is not entered/)
|
268
|
+
stop_reactor
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
skip 'ensure member_id is unique an updated on ENTER'
|
273
|
+
skip 'stop a call to get when the channel has not been entered'
|
274
|
+
skip 'stop a call to get when the channel has been entered but the list is not up to date'
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
@@ -2,432 +2,436 @@ require "spec_helper"
|
|
2
2
|
require "securerandom"
|
3
3
|
|
4
4
|
describe "REST" do
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
describe "#request_token" do
|
11
|
-
let(:ttl) { 30 * 60 }
|
12
|
-
let(:capability) { { :foo => ["publish"] } }
|
13
|
-
|
14
|
-
it "returns the requested token" do
|
15
|
-
actual_token = auth.request_token(
|
16
|
-
ttl: ttl,
|
17
|
-
capability: capability
|
18
|
-
)
|
19
|
-
|
20
|
-
expect(actual_token.id).to match(/^#{app_id}\.[\w-]+$/)
|
21
|
-
expect(actual_token.key_id).to match(/^#{key_id}$/)
|
22
|
-
expect(actual_token.issued_at).to be_within(2).of(Time.now)
|
23
|
-
expect(actual_token.expires_at).to be_within(2).of(Time.now + ttl)
|
24
|
-
end
|
25
|
-
|
26
|
-
%w(client_id ttl timestamp capability nonce).each do |option|
|
27
|
-
context "option :#{option}", webmock: true do
|
28
|
-
let(:random) { SecureRandom.random_number(1_000_000_000).to_s }
|
29
|
-
let(:options) { { option.to_sym => random } }
|
30
|
-
|
31
|
-
let(:token_response) { { access_token: {} }.to_json }
|
32
|
-
let!(:request_token_stub) do
|
33
|
-
stub_request(:post, "#{client.endpoint}/keys/#{key_id}/requestToken").
|
34
|
-
with(:body => hash_including({ option => random })).
|
35
|
-
to_return(:status => 201, :body => token_response, :headers => { 'Content-Type' => 'application/json' })
|
36
|
-
end
|
37
|
-
|
38
|
-
before { auth.request_token options }
|
39
|
-
|
40
|
-
it 'overrides default' do
|
41
|
-
expect(request_token_stub).to have_been_requested
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
context 'with :key_id & :key_secret options', webmock: true do
|
47
|
-
let(:key_id) { SecureRandom.hex }
|
48
|
-
let(:key_secret) { SecureRandom.hex }
|
49
|
-
let(:nonce) { SecureRandom.hex }
|
50
|
-
let(:token_options) { { key_id: key_id, key_secret: key_secret, nonce: nonce, timestamp: Time.now } }
|
51
|
-
let(:token_request) { auth.create_token_request(token_options) }
|
52
|
-
let(:mac) do
|
53
|
-
hmac_for(token_request, key_secret)
|
54
|
-
end
|
55
|
-
|
56
|
-
let(:token_response) { { access_token: {} }.to_json }
|
57
|
-
let!(:request_token_stub) do
|
58
|
-
stub_request(:post, "#{client.endpoint}/keys/#{key_id}/requestToken").
|
59
|
-
with(:body => hash_including({ 'mac' => mac })).
|
60
|
-
to_return(:status => 201, :body => token_response, :headers => { 'Content-Type' => 'application/json' })
|
5
|
+
[:msgpack, :json].each do |protocol|
|
6
|
+
context "over #{protocol}" do
|
7
|
+
let(:client) do
|
8
|
+
Ably::Rest::Client.new(api_key: api_key, environment: environment, protocol: protocol)
|
61
9
|
end
|
10
|
+
let(:auth) { client.auth }
|
62
11
|
|
63
|
-
|
12
|
+
describe "#request_token" do
|
13
|
+
let(:ttl) { 30 * 60 }
|
14
|
+
let(:capability) { { :foo => ["publish"] } }
|
64
15
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
16
|
+
it "returns the requested token" do
|
17
|
+
actual_token = auth.request_token(
|
18
|
+
ttl: ttl,
|
19
|
+
capability: capability
|
20
|
+
)
|
69
21
|
|
70
|
-
|
71
|
-
|
22
|
+
expect(actual_token.id).to match(/^#{app_id}\.[\w-]+$/)
|
23
|
+
expect(actual_token.key_id).to match(/^#{key_id}$/)
|
24
|
+
expect(actual_token.issued_at).to be_within(2).of(Time.now)
|
25
|
+
expect(actual_token.expires_at).to be_within(2).of(Time.now + ttl)
|
26
|
+
end
|
72
27
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
end
|
28
|
+
%w(client_id ttl timestamp capability nonce).each do |option|
|
29
|
+
context "option :#{option}", webmock: true do
|
30
|
+
let(:random) { SecureRandom.random_number(1_000_000_000).to_s }
|
31
|
+
let(:options) { { option.to_sym => random } }
|
78
32
|
|
79
|
-
|
80
|
-
|
33
|
+
let(:token_response) { { access_token: {} }.to_json }
|
34
|
+
let!(:request_token_stub) do
|
35
|
+
stub_request(:post, "#{client.endpoint}/keys/#{key_id}/requestToken").
|
36
|
+
with(:body => hash_including({ option => random })).
|
37
|
+
to_return(:status => 201, :body => token_response, :headers => { 'Content-Type' => 'application/json' })
|
38
|
+
end
|
81
39
|
|
82
|
-
|
83
|
-
expect(client).to_not receive(:time)
|
84
|
-
auth.request_token(options)
|
85
|
-
end
|
86
|
-
end
|
40
|
+
before { auth.request_token options }
|
87
41
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
let(:headers) { nil }
|
94
|
-
let(:auth_method) { :get }
|
95
|
-
let(:options) do
|
96
|
-
{
|
97
|
-
auth_url: auth_url,
|
98
|
-
auth_params: query_params,
|
99
|
-
auth_headers: headers,
|
100
|
-
auth_method: auth_method
|
101
|
-
}
|
102
|
-
end
|
42
|
+
it 'overrides default' do
|
43
|
+
expect(request_token_stub).to have_been_requested
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
103
47
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
48
|
+
context 'with :key_id & :key_secret options', webmock: true do
|
49
|
+
let(:key_id) { SecureRandom.hex }
|
50
|
+
let(:key_secret) { SecureRandom.hex }
|
51
|
+
let(:nonce) { SecureRandom.hex }
|
52
|
+
let(:token_options) { { key_id: key_id, key_secret: key_secret, nonce: nonce, timestamp: Time.now } }
|
53
|
+
let(:token_request) { auth.create_token_request(token_options) }
|
54
|
+
let(:mac) do
|
55
|
+
hmac_for(token_request, key_secret)
|
56
|
+
end
|
110
57
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
58
|
+
let(:token_response) { { access_token: {} }.to_json }
|
59
|
+
let!(:request_token_stub) do
|
60
|
+
stub_request(:post, "#{client.endpoint}/keys/#{key_id}/requestToken").
|
61
|
+
with(:body => hash_including({ 'mac' => mac })).
|
62
|
+
to_return(:status => 201, :body => token_response, :headers => { 'Content-Type' => 'application/json' })
|
63
|
+
end
|
116
64
|
|
117
|
-
|
118
|
-
before { auth.request_token options }
|
65
|
+
let!(:token) { auth.request_token(token_options) }
|
119
66
|
|
120
|
-
|
121
|
-
it 'requests a token from :auth_url' do
|
67
|
+
specify 'key_id is used in request and signing uses key_secret' do
|
122
68
|
expect(request_token_stub).to have_been_requested
|
123
|
-
expect(auth_url_request_stub).to have_been_requested
|
124
69
|
end
|
125
70
|
end
|
126
71
|
|
127
|
-
context
|
128
|
-
let(:
|
129
|
-
|
130
|
-
|
131
|
-
expect(
|
72
|
+
context "with :query_time option" do
|
73
|
+
let(:options) { { query_time: true } }
|
74
|
+
|
75
|
+
it 'queries the server for the time' do
|
76
|
+
expect(client).to receive(:time).and_call_original
|
77
|
+
auth.request_token(options)
|
132
78
|
end
|
133
79
|
end
|
134
80
|
|
135
|
-
context
|
136
|
-
let(:
|
137
|
-
|
138
|
-
|
139
|
-
expect(
|
81
|
+
context "without :query_time option" do
|
82
|
+
let(:options) { { query_time: false } }
|
83
|
+
|
84
|
+
it 'queries the server for the time' do
|
85
|
+
expect(client).to_not receive(:time)
|
86
|
+
auth.request_token(options)
|
140
87
|
end
|
141
88
|
end
|
142
89
|
|
143
|
-
context 'with
|
144
|
-
let(:
|
145
|
-
|
146
|
-
|
147
|
-
|
90
|
+
context 'with :auth_url option', webmock: true do
|
91
|
+
let(:auth_url) { 'https://www.fictitious.com/get_token' }
|
92
|
+
let(:token_request) { { id: key_id }.to_json }
|
93
|
+
let(:token_response) { { access_token: { } }.to_json }
|
94
|
+
let(:query_params) { nil }
|
95
|
+
let(:headers) { nil }
|
96
|
+
let(:auth_method) { :get }
|
97
|
+
let(:options) do
|
98
|
+
{
|
99
|
+
auth_url: auth_url,
|
100
|
+
auth_params: query_params,
|
101
|
+
auth_headers: headers,
|
102
|
+
auth_method: auth_method
|
103
|
+
}
|
148
104
|
end
|
149
|
-
end
|
150
|
-
end
|
151
105
|
|
152
|
-
context 'when response is invalid' do
|
153
|
-
context '500' do
|
154
106
|
let!(:auth_url_request_stub) do
|
155
|
-
stub_request(auth_method, auth_url)
|
107
|
+
stub = stub_request(auth_method, auth_url)
|
108
|
+
stub.with(:query => hash_including(query_params)) unless query_params.nil?
|
109
|
+
stub.with(:header => hash_including(headers)) unless headers.nil?
|
110
|
+
stub.to_return(:status => 201, :body => token_request, :headers => { 'Content-Type' => 'application/json' })
|
156
111
|
end
|
157
112
|
|
158
|
-
|
159
|
-
|
113
|
+
let!(:request_token_stub) do
|
114
|
+
stub_request(:post, "#{client.endpoint}/keys/#{key_id}/requestToken").
|
115
|
+
with(:body => hash_including({ 'id' => key_id })).
|
116
|
+
to_return(:status => 201, :body => token_response, :headers => { 'Content-Type' => 'application/json' })
|
160
117
|
end
|
161
|
-
end
|
162
118
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
119
|
+
context 'valid' do
|
120
|
+
before { auth.request_token options }
|
121
|
+
|
122
|
+
context 'and default options' do
|
123
|
+
it 'requests a token from :auth_url' do
|
124
|
+
expect(request_token_stub).to have_been_requested
|
125
|
+
expect(auth_url_request_stub).to have_been_requested
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context 'with params' do
|
130
|
+
let(:query_params) { { 'key' => SecureRandom.hex } }
|
131
|
+
it 'requests a token from :auth_url' do
|
132
|
+
expect(request_token_stub).to have_been_requested
|
133
|
+
expect(auth_url_request_stub).to have_been_requested
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
context 'with headers' do
|
138
|
+
let(:headers) { { 'key' => SecureRandom.hex } }
|
139
|
+
it 'requests a token from :auth_url' do
|
140
|
+
expect(request_token_stub).to have_been_requested
|
141
|
+
expect(auth_url_request_stub).to have_been_requested
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
context 'with POST' do
|
146
|
+
let(:auth_method) { :post }
|
147
|
+
it 'requests a token from :auth_url' do
|
148
|
+
expect(request_token_stub).to have_been_requested
|
149
|
+
expect(auth_url_request_stub).to have_been_requested
|
150
|
+
end
|
151
|
+
end
|
167
152
|
end
|
168
153
|
|
169
|
-
|
170
|
-
|
154
|
+
context 'when response is invalid' do
|
155
|
+
context '500' do
|
156
|
+
let!(:auth_url_request_stub) do
|
157
|
+
stub_request(auth_method, auth_url).to_return(:status => 500)
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'raises ServerError' do
|
161
|
+
expect { auth.request_token options }.to raise_error(Ably::Exceptions::ServerError)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
context 'XML' do
|
166
|
+
let!(:auth_url_request_stub) do
|
167
|
+
stub_request(auth_method, auth_url).
|
168
|
+
to_return(:status => 201, :body => '<xml></xml>', :headers => { 'Content-Type' => 'application/xml' })
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'raises InvalidResponseBody' do
|
172
|
+
expect { auth.request_token options }.to raise_error(Ably::Exceptions::InvalidResponseBody)
|
173
|
+
end
|
174
|
+
end
|
171
175
|
end
|
172
176
|
end
|
173
|
-
end
|
174
|
-
end
|
175
177
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
178
|
+
context 'with auth_block' do
|
179
|
+
let(:client_id) { SecureRandom.hex }
|
180
|
+
let(:options) { { client_id: client_id } }
|
181
|
+
let!(:token) do
|
182
|
+
auth.request_token(options) do |block_options|
|
183
|
+
@block_called = true
|
184
|
+
@block_options = block_options
|
185
|
+
auth.create_token_request(client_id: client_id)
|
186
|
+
end
|
187
|
+
end
|
186
188
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
189
|
+
it 'calls the block' do
|
190
|
+
expect(@block_called).to eql(true)
|
191
|
+
expect(@block_options).to include(options)
|
192
|
+
end
|
191
193
|
|
192
|
-
|
193
|
-
|
194
|
+
it 'uses the token request when requesting a new token' do
|
195
|
+
expect(token.client_id).to eql(client_id)
|
196
|
+
end
|
197
|
+
end
|
194
198
|
end
|
195
|
-
end
|
196
|
-
end
|
197
199
|
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
200
|
+
describe '#authorise' do
|
201
|
+
context 'with no previous authorisation' do
|
202
|
+
let(:request_options) do
|
203
|
+
{ auth_url: 'http://somewhere.com/' }
|
204
|
+
end
|
203
205
|
|
204
|
-
|
205
|
-
|
206
|
-
|
206
|
+
it 'has no current_token' do
|
207
|
+
expect(auth.current_token).to be_nil
|
208
|
+
end
|
207
209
|
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
210
|
+
it 'passes all options to request_token' do
|
211
|
+
expect(auth).to receive(:request_token).with(request_options)
|
212
|
+
auth.authorise request_options
|
213
|
+
end
|
212
214
|
|
213
|
-
|
214
|
-
|
215
|
-
|
215
|
+
it 'returns a valid token' do
|
216
|
+
expect(auth.authorise).to be_a(Ably::Models::Token)
|
217
|
+
end
|
216
218
|
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
219
|
+
it 'issues a new token if option :force => true' do
|
220
|
+
expect { auth.authorise(force: true) }.to change { auth.current_token }
|
221
|
+
end
|
222
|
+
end
|
221
223
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
224
|
+
context 'with previous authorisation' do
|
225
|
+
before do
|
226
|
+
auth.authorise
|
227
|
+
expect(auth.current_token).to_not be_expired
|
228
|
+
end
|
227
229
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
230
|
+
it 'does not request a token if token is not expired' do
|
231
|
+
expect(auth).to_not receive(:request_token)
|
232
|
+
auth.authorise
|
233
|
+
end
|
232
234
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
235
|
+
it 'requests a new token if token is expired' do
|
236
|
+
allow(auth.current_token).to receive(:expired?).and_return(true)
|
237
|
+
expect(auth).to receive(:request_token)
|
238
|
+
expect { auth.authorise }.to change { auth.current_token }
|
239
|
+
end
|
238
240
|
|
239
|
-
|
240
|
-
|
241
|
+
it 'issues a new token if option :force => true' do
|
242
|
+
expect { auth.authorise(force: true) }.to change { auth.current_token }
|
243
|
+
end
|
244
|
+
end
|
241
245
|
end
|
242
|
-
end
|
243
|
-
end
|
244
246
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
it "uses the key ID from the client" do
|
252
|
-
expect(subject[:id]).to eql(key_id)
|
253
|
-
end
|
247
|
+
describe "#create_token_request" do
|
248
|
+
let(:ttl) { 60 * 60 }
|
249
|
+
let(:capability) { { :foo => ["publish"] } }
|
250
|
+
let(:options) { Hash.new }
|
251
|
+
subject { auth.create_token_request(options) }
|
254
252
|
|
255
|
-
|
256
|
-
|
257
|
-
|
253
|
+
it "uses the key ID from the client" do
|
254
|
+
expect(subject[:id]).to eql(key_id)
|
255
|
+
end
|
258
256
|
|
259
|
-
|
260
|
-
|
261
|
-
|
257
|
+
it "uses the default TTL" do
|
258
|
+
expect(subject[:ttl]).to eql(Ably::Models::Token::DEFAULTS[:ttl])
|
259
|
+
end
|
262
260
|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
end
|
261
|
+
it "uses the default capability" do
|
262
|
+
expect(subject[:capability]).to eql(Ably::Models::Token::DEFAULTS[:capability].to_json)
|
263
|
+
end
|
267
264
|
|
268
|
-
|
269
|
-
|
270
|
-
|
265
|
+
it "has a unique nonce" do
|
266
|
+
unique_nonces = 100.times.map { auth.create_token_request[:nonce] }
|
267
|
+
expect(unique_nonces.uniq.length).to eql(100)
|
268
|
+
end
|
271
269
|
|
272
|
-
|
273
|
-
|
274
|
-
let(:option_value) { SecureRandom.random_number(1_000_000_000) }
|
275
|
-
before do
|
276
|
-
options[attribute.to_sym] = option_value
|
270
|
+
it "has a nonce of at least 16 characters" do
|
271
|
+
expect(subject[:nonce].length).to be >= 16
|
277
272
|
end
|
278
|
-
|
279
|
-
|
273
|
+
|
274
|
+
%w(ttl capability nonce timestamp client_id).each do |attribute|
|
275
|
+
context "with option :#{attribute}" do
|
276
|
+
let(:option_value) { SecureRandom.random_number(1_000_000_000) }
|
277
|
+
before do
|
278
|
+
options[attribute.to_sym] = option_value
|
279
|
+
end
|
280
|
+
it "overrides default" do
|
281
|
+
expect(subject[attribute.to_sym]).to eql(option_value)
|
282
|
+
end
|
283
|
+
end
|
280
284
|
end
|
281
|
-
end
|
282
|
-
end
|
283
285
|
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
286
|
+
context "invalid attributes" do
|
287
|
+
let(:options) { { nonce: 'valid', is_not_used_by_token_request: 'invalid' } }
|
288
|
+
specify 'are ignored' do
|
289
|
+
expect(subject.keys).to_not include(:is_not_used_by_token_request)
|
290
|
+
expect(subject.keys).to include(:nonce)
|
291
|
+
expect(subject[:nonce]).to eql('valid')
|
292
|
+
end
|
293
|
+
end
|
292
294
|
|
293
|
-
|
294
|
-
|
295
|
+
context "missing key ID and/or secret" do
|
296
|
+
let(:client) { Ably::Rest::Client.new(auth_url: 'http://example.com', protocol: protocol) }
|
295
297
|
|
296
|
-
|
297
|
-
|
298
|
-
|
298
|
+
it "should raise an exception if key secret is missing" do
|
299
|
+
expect { auth.create_token_request(key_id: 'id') }.to raise_error Ably::Exceptions::TokenRequestError
|
300
|
+
end
|
299
301
|
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
302
|
+
it "should raise an exception if key id is missing" do
|
303
|
+
expect { auth.create_token_request(key_secret: 'secret') }.to raise_error Ably::Exceptions::TokenRequestError
|
304
|
+
end
|
305
|
+
end
|
304
306
|
|
305
|
-
|
306
|
-
|
307
|
-
|
307
|
+
context "with :query_time option" do
|
308
|
+
let(:time) { Time.now - 30 }
|
309
|
+
let(:options) { { query_time: true } }
|
308
310
|
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
311
|
+
it 'queries the server for the time' do
|
312
|
+
expect(client).to receive(:time).and_return(time)
|
313
|
+
expect(subject[:timestamp]).to eql(time.to_i)
|
314
|
+
end
|
315
|
+
end
|
314
316
|
|
315
|
-
|
316
|
-
|
317
|
-
|
317
|
+
context "with :timestamp option" do
|
318
|
+
let(:token_request_time) { Time.now + 5 }
|
319
|
+
let(:options) { { timestamp: token_request_time } }
|
318
320
|
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
321
|
+
it 'uses the provided timestamp' do
|
322
|
+
expect(subject[:timestamp]).to eql(token_request_time.to_i)
|
323
|
+
end
|
324
|
+
end
|
323
325
|
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
326
|
+
context "signing" do
|
327
|
+
let(:options) do
|
328
|
+
{
|
329
|
+
id: SecureRandom.hex,
|
330
|
+
ttl: SecureRandom.hex,
|
331
|
+
capability: SecureRandom.hex,
|
332
|
+
client_id: SecureRandom.hex,
|
333
|
+
timestamp: SecureRandom.random_number(1_000_000_000),
|
334
|
+
nonce: SecureRandom.hex
|
335
|
+
}
|
336
|
+
end
|
335
337
|
|
336
|
-
|
337
|
-
|
338
|
-
|
338
|
+
it 'generates a valid HMAC' do
|
339
|
+
hmac = hmac_for(options, key_secret)
|
340
|
+
expect(subject[:mac]).to eql(hmac)
|
341
|
+
end
|
342
|
+
end
|
339
343
|
end
|
340
|
-
end
|
341
|
-
end
|
342
344
|
|
343
|
-
|
344
|
-
|
345
|
+
context "client with token authentication" do
|
346
|
+
let(:capability) { { :foo => ["publish"] } }
|
345
347
|
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
348
|
+
describe "with token_id argument" do
|
349
|
+
let(:ttl) { 60 * 60 }
|
350
|
+
let(:token) do
|
351
|
+
auth.request_token(
|
352
|
+
ttl: ttl,
|
353
|
+
capability: capability
|
354
|
+
)
|
355
|
+
end
|
356
|
+
let(:token_id) { token.id }
|
357
|
+
let(:token_auth_client) do
|
358
|
+
Ably::Rest::Client.new(token_id: token_id, environment: environment, protocol: protocol)
|
359
|
+
end
|
358
360
|
|
359
|
-
|
360
|
-
|
361
|
-
|
361
|
+
it "authenticates successfully" do
|
362
|
+
expect(token_auth_client.channel("foo").publish("event", "data")).to be_truthy
|
363
|
+
end
|
362
364
|
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
365
|
+
it "disallows publishing on unspecified capability channels" do
|
366
|
+
expect { token_auth_client.channel("bar").publish("event", "data") }.to raise_error do |error|
|
367
|
+
expect(error).to be_a(Ably::Exceptions::InvalidToken)
|
368
|
+
expect(error.status).to eql(401)
|
369
|
+
expect(error.code).to eql(40160)
|
370
|
+
end
|
371
|
+
end
|
370
372
|
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
373
|
+
it "fails if timestamp is invalid" do
|
374
|
+
expect { auth.request_token(timestamp: Time.now - 180) }.to raise_error do |error|
|
375
|
+
expect(error).to be_a(Ably::Exceptions::InvalidToken)
|
376
|
+
expect(error.status).to eql(401)
|
377
|
+
expect(error.code).to eql(40101)
|
378
|
+
end
|
379
|
+
end
|
376
380
|
end
|
377
|
-
end
|
378
|
-
end
|
379
|
-
|
380
|
-
describe "implicit through client id" do
|
381
|
-
let(:client_id) { '999' }
|
382
|
-
let(:client) do
|
383
|
-
Ably::Rest::Client.new(api_key: api_key, client_id: client_id, environment: environment)
|
384
|
-
end
|
385
|
-
let(:token_id) { 'unique-token-id' }
|
386
|
-
let(:token_response) do
|
387
|
-
{
|
388
|
-
access_token: {
|
389
|
-
id: token_id
|
390
|
-
}
|
391
|
-
}.to_json
|
392
|
-
end
|
393
381
|
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
382
|
+
describe "implicit through client id" do
|
383
|
+
let(:client_id) { '999' }
|
384
|
+
let(:client) do
|
385
|
+
Ably::Rest::Client.new(api_key: api_key, client_id: client_id, environment: environment, protocol: protocol)
|
386
|
+
end
|
387
|
+
let(:token_id) { 'unique-token-id' }
|
388
|
+
let(:token_response) do
|
389
|
+
{
|
390
|
+
access_token: {
|
391
|
+
id: token_id
|
392
|
+
}
|
393
|
+
}.to_json
|
394
|
+
end
|
404
395
|
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
396
|
+
context 'stubbed', webmock: true do
|
397
|
+
let!(:request_token_stub) do
|
398
|
+
stub_request(:post, "#{client.endpoint}/keys/#{key_id}/requestToken").
|
399
|
+
to_return(:status => 201, :body => token_response, :headers => { 'Content-Type' => 'application/json' })
|
400
|
+
end
|
401
|
+
let!(:publish_message_stub) do
|
402
|
+
stub_request(:post, "#{client.endpoint}/channels/foo/publish").
|
403
|
+
with(headers: { 'Authorization' => "Bearer #{encode64(token_id)}" }).
|
404
|
+
to_return(status: 201, body: '{}', headers: { 'Content-Type' => 'application/json' })
|
405
|
+
end
|
406
|
+
|
407
|
+
it "will create a token request" do
|
408
|
+
client.channel("foo").publish("event", "data")
|
409
|
+
expect(request_token_stub).to have_been_requested
|
410
|
+
end
|
411
|
+
end
|
410
412
|
|
411
|
-
|
412
|
-
|
413
|
+
context "will create a token" do
|
414
|
+
let(:token) { client.auth.current_token }
|
413
415
|
|
414
|
-
|
415
|
-
|
416
|
-
|
416
|
+
it "before a request is made" do
|
417
|
+
expect(token).to be_nil
|
418
|
+
end
|
417
419
|
|
418
|
-
|
419
|
-
|
420
|
-
|
420
|
+
it "when a message is published" do
|
421
|
+
expect(client.channel("foo").publish("event", "data")).to be_truthy
|
422
|
+
end
|
421
423
|
|
422
|
-
|
423
|
-
|
424
|
+
it "with capability and TTL defaults" do
|
425
|
+
client.channel("foo").publish("event", "data")
|
424
426
|
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
427
|
+
expect(token).to be_a(Ably::Models::Token)
|
428
|
+
capability_with_str_key = Ably::Models::Token::DEFAULTS[:capability]
|
429
|
+
capability = Hash[capability_with_str_key.keys.map(&:to_sym).zip(capability_with_str_key.values)]
|
430
|
+
expect(token.capability).to eq(capability)
|
431
|
+
expect(token.expires_at.to_i).to be_within(2).of(Time.now.to_i + Ably::Models::Token::DEFAULTS[:ttl])
|
432
|
+
expect(token.client_id).to eq(client_id)
|
433
|
+
end
|
434
|
+
end
|
431
435
|
end
|
432
436
|
end
|
433
437
|
end
|