ably 0.6.2 → 0.7.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.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.ruby-version.old +1 -0
  4. data/.travis.yml +0 -2
  5. data/Rakefile +22 -4
  6. data/SPEC.md +1676 -0
  7. data/ably.gemspec +1 -1
  8. data/lib/ably.rb +0 -8
  9. data/lib/ably/auth.rb +54 -46
  10. data/lib/ably/exceptions.rb +19 -5
  11. data/lib/ably/logger.rb +1 -1
  12. data/lib/ably/models/error_info.rb +1 -1
  13. data/lib/ably/models/idiomatic_ruby_wrapper.rb +11 -9
  14. data/lib/ably/models/message.rb +15 -12
  15. data/lib/ably/models/message_encoders/base.rb +6 -5
  16. data/lib/ably/models/message_encoders/base64.rb +1 -0
  17. data/lib/ably/models/message_encoders/cipher.rb +6 -3
  18. data/lib/ably/models/message_encoders/json.rb +1 -0
  19. data/lib/ably/models/message_encoders/utf8.rb +2 -9
  20. data/lib/ably/models/nil_logger.rb +20 -0
  21. data/lib/ably/models/paginated_resource.rb +5 -2
  22. data/lib/ably/models/presence_message.rb +21 -12
  23. data/lib/ably/models/protocol_message.rb +22 -6
  24. data/lib/ably/modules/ably.rb +11 -0
  25. data/lib/ably/modules/async_wrapper.rb +2 -0
  26. data/lib/ably/modules/conversions.rb +23 -3
  27. data/lib/ably/modules/encodeable.rb +2 -1
  28. data/lib/ably/modules/enum.rb +2 -0
  29. data/lib/ably/modules/event_emitter.rb +7 -1
  30. data/lib/ably/modules/event_machine_helpers.rb +2 -0
  31. data/lib/ably/modules/http_helpers.rb +2 -0
  32. data/lib/ably/modules/model_common.rb +12 -2
  33. data/lib/ably/modules/state_emitter.rb +76 -0
  34. data/lib/ably/modules/state_machine.rb +53 -0
  35. data/lib/ably/modules/statesman_monkey_patch.rb +33 -0
  36. data/lib/ably/modules/uses_state_machine.rb +74 -0
  37. data/lib/ably/realtime.rb +4 -2
  38. data/lib/ably/realtime/channel.rb +51 -58
  39. data/lib/ably/realtime/channel/channel_manager.rb +91 -0
  40. data/lib/ably/realtime/channel/channel_state_machine.rb +68 -0
  41. data/lib/ably/realtime/client.rb +70 -26
  42. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +31 -13
  43. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
  44. data/lib/ably/realtime/connection.rb +135 -92
  45. data/lib/ably/realtime/connection/connection_manager.rb +216 -33
  46. data/lib/ably/realtime/connection/connection_state_machine.rb +30 -73
  47. data/lib/ably/realtime/models/nil_channel.rb +10 -1
  48. data/lib/ably/realtime/presence.rb +336 -92
  49. data/lib/ably/rest.rb +2 -2
  50. data/lib/ably/rest/channel.rb +13 -4
  51. data/lib/ably/rest/client.rb +138 -38
  52. data/lib/ably/rest/middleware/logger.rb +24 -3
  53. data/lib/ably/rest/presence.rb +12 -7
  54. data/lib/ably/version.rb +1 -1
  55. data/spec/acceptance/realtime/channel_history_spec.rb +101 -85
  56. data/spec/acceptance/realtime/channel_spec.rb +461 -120
  57. data/spec/acceptance/realtime/client_spec.rb +119 -0
  58. data/spec/acceptance/realtime/connection_failures_spec.rb +499 -0
  59. data/spec/acceptance/realtime/connection_spec.rb +571 -97
  60. data/spec/acceptance/realtime/message_spec.rb +347 -333
  61. data/spec/acceptance/realtime/presence_history_spec.rb +35 -40
  62. data/spec/acceptance/realtime/presence_spec.rb +769 -239
  63. data/spec/acceptance/realtime/stats_spec.rb +14 -22
  64. data/spec/acceptance/realtime/time_spec.rb +16 -20
  65. data/spec/acceptance/rest/auth_spec.rb +425 -364
  66. data/spec/acceptance/rest/base_spec.rb +108 -176
  67. data/spec/acceptance/rest/channel_spec.rb +89 -89
  68. data/spec/acceptance/rest/channels_spec.rb +30 -32
  69. data/spec/acceptance/rest/client_spec.rb +273 -0
  70. data/spec/acceptance/rest/encoders_spec.rb +185 -0
  71. data/spec/acceptance/rest/message_spec.rb +186 -163
  72. data/spec/acceptance/rest/presence_spec.rb +150 -111
  73. data/spec/acceptance/rest/stats_spec.rb +45 -40
  74. data/spec/acceptance/rest/time_spec.rb +8 -10
  75. data/spec/rspec_config.rb +10 -1
  76. data/spec/shared/client_initializer_behaviour.rb +212 -0
  77. data/spec/{support/model_helper.rb → shared/model_behaviour.rb} +6 -6
  78. data/spec/{support/protocol_msgbus_helper.rb → shared/protocol_msgbus_behaviour.rb} +1 -1
  79. data/spec/spec_helper.rb +9 -0
  80. data/spec/support/api_helper.rb +11 -0
  81. data/spec/support/event_machine_helper.rb +101 -3
  82. data/spec/support/markdown_spec_formatter.rb +90 -0
  83. data/spec/support/private_api_formatter.rb +36 -0
  84. data/spec/support/protocol_helper.rb +32 -0
  85. data/spec/support/random_helper.rb +15 -0
  86. data/spec/support/test_app.rb +4 -0
  87. data/spec/unit/auth_spec.rb +68 -0
  88. data/spec/unit/logger_spec.rb +77 -66
  89. data/spec/unit/models/error_info_spec.rb +1 -1
  90. data/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +2 -3
  91. data/spec/unit/models/message_encoders/base64_spec.rb +2 -2
  92. data/spec/unit/models/message_encoders/cipher_spec.rb +2 -2
  93. data/spec/unit/models/message_encoders/utf8_spec.rb +2 -46
  94. data/spec/unit/models/message_spec.rb +160 -15
  95. data/spec/unit/models/paginated_resource_spec.rb +29 -27
  96. data/spec/unit/models/presence_message_spec.rb +163 -20
  97. data/spec/unit/models/protocol_message_spec.rb +43 -8
  98. data/spec/unit/modules/async_wrapper_spec.rb +2 -3
  99. data/spec/unit/modules/conversions_spec.rb +1 -1
  100. data/spec/unit/modules/enum_spec.rb +2 -3
  101. data/spec/unit/modules/event_emitter_spec.rb +62 -5
  102. data/spec/unit/modules/state_emitter_spec.rb +283 -0
  103. data/spec/unit/realtime/channel_spec.rb +107 -2
  104. data/spec/unit/realtime/channels_spec.rb +1 -0
  105. data/spec/unit/realtime/client_spec.rb +8 -48
  106. data/spec/unit/realtime/connection_spec.rb +3 -3
  107. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +2 -2
  108. data/spec/unit/realtime/presence_spec.rb +13 -4
  109. data/spec/unit/realtime/realtime_spec.rb +0 -11
  110. data/spec/unit/realtime/websocket_transport_spec.rb +2 -2
  111. data/spec/unit/rest/channel_spec.rb +109 -0
  112. data/spec/unit/rest/channels_spec.rb +4 -3
  113. data/spec/unit/rest/client_spec.rb +30 -125
  114. data/spec/unit/rest/rest_spec.rb +10 -0
  115. data/spec/unit/util/crypto_spec.rb +10 -5
  116. data/spec/unit/util/pub_sub_spec.rb +5 -5
  117. metadata +44 -12
  118. data/spec/integration/modules/state_emitter_spec.rb +0 -80
  119. data/spec/integration/rest/auth.rb +0 -9
@@ -1,30 +1,22 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe 'Ably::Realtime::Client stats' do
4
- include RSpec::EventMachine
5
-
6
- [:msgpack, :json].each do |protocol|
7
- context "over #{protocol}" do
8
- let(:client) do
9
- Ably::Realtime::Client.new(api_key: api_key, environment: environment, protocol: protocol)
10
- end
3
+ describe Ably::Realtime::Client, '#stats', :event_machine do
4
+ vary_by_protocol do
5
+ let(:client) do
6
+ Ably::Realtime::Client.new(api_key: api_key, environment: environment, protocol: protocol)
7
+ end
11
8
 
12
- describe 'fetching stats' do
13
- it 'should return a Hash' do
14
- run_reactor do
15
- client.stats do |stats|
16
- expect(stats).to be_a(Array)
17
- stop_reactor
18
- end
19
- end
9
+ describe 'fetching stats' do
10
+ it 'should return a Hash' do
11
+ client.stats do |stats|
12
+ expect(stats).to be_a(Array)
13
+ stop_reactor
20
14
  end
15
+ end
21
16
 
22
- it 'should return a deferrable object' do
23
- run_reactor do
24
- expect(client.stats).to be_a(EventMachine::Deferrable)
25
- stop_reactor
26
- end
27
- end
17
+ it 'should return a Deferrable object' do
18
+ expect(client.stats).to be_a(EventMachine::Deferrable)
19
+ stop_reactor
28
20
  end
29
21
  end
30
22
  end
@@ -1,29 +1,25 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe 'Ably::Realtime::Client time' do
4
- include RSpec::EventMachine
5
-
6
- [:msgpack, :json].each do |protocol|
7
- context "over #{protocol}" do
8
- let(:client) do
9
- Ably::Realtime::Client.new(api_key: api_key, environment: environment, protocol: protocol)
10
- end
3
+ describe Ably::Realtime::Client, '#time', :event_machine do
4
+ vary_by_protocol do
5
+ let(:client) do
6
+ Ably::Realtime::Client.new(api_key: api_key, environment: environment, protocol: protocol)
7
+ end
11
8
 
12
- describe 'fetching the service time' do
13
- it 'should return the service time as a Time object' do
14
- run_reactor do
15
- client.time do |time|
16
- expect(time).to be_within(2).of(Time.now)
17
- stop_reactor
18
- end
9
+ describe 'fetching the service time' do
10
+ it 'should return the service time as a Time object' do
11
+ run_reactor do
12
+ client.time do |time|
13
+ expect(time).to be_within(2).of(Time.now)
14
+ stop_reactor
19
15
  end
20
16
  end
17
+ end
21
18
 
22
- it 'should return a deferrable object' do
23
- run_reactor do
24
- expect(client.time).to be_a(EventMachine::Deferrable)
25
- stop_reactor
26
- end
19
+ it 'should return a deferrable object' do
20
+ run_reactor do
21
+ expect(client.time).to be_a(EventMachine::Deferrable)
22
+ stop_reactor
27
23
  end
28
24
  end
29
25
  end
@@ -1,498 +1,559 @@
1
+ # encoding: utf-8
1
2
  require 'spec_helper'
2
- require 'securerandom'
3
3
 
4
4
  describe Ably::Auth do
5
5
  include Ably::Modules::Conversions
6
6
 
7
- [:msgpack, :json].each do |protocol|
8
- context "over #{protocol}" do
9
- let(:client) do
10
- Ably::Rest::Client.new(api_key: api_key, environment: environment, protocol: protocol)
11
- end
12
- let(:auth) { client.auth }
13
- let(:content_type) do
14
- if protocol == :msgpack
15
- 'application/x-msgpack'
16
- else
17
- 'application/json'
18
- end
19
- end
7
+ def hmac_for(token_request, secret)
8
+ text = token_request.values_at(
9
+ :id,
10
+ :ttl,
11
+ :capability,
12
+ :client_id,
13
+ :timestamp,
14
+ :nonce
15
+ ).map { |t| "#{t}\n" }.join("")
20
16
 
21
- def request_body_includes(request, protocol, key, val)
22
- body = if protocol == :msgpack
23
- MessagePack.unpack(request.body)
24
- else
25
- JSON.parse(request.body)
26
- end
27
- body[key.to_s].to_s == val.to_s
17
+ encode64(
18
+ Digest::HMAC.digest(text, key_secret, Digest::SHA256)
19
+ )
20
+ end
21
+
22
+ vary_by_protocol do
23
+ let(:client) do
24
+ Ably::Rest::Client.new(api_key: api_key, environment: environment, protocol: protocol)
25
+ end
26
+ let(:auth) { client.auth }
27
+ let(:content_type) do
28
+ if protocol == :msgpack
29
+ 'application/x-msgpack'
30
+ else
31
+ 'application/json'
28
32
  end
33
+ end
29
34
 
30
- def serialize(object, protocol)
31
- if protocol == :msgpack
32
- MessagePack.pack(token_response)
33
- else
34
- JSON.dump(token_response)
35
- end
35
+ def request_body_includes(request, protocol, key, val)
36
+ body = if protocol == :msgpack
37
+ MessagePack.unpack(request.body)
38
+ else
39
+ JSON.parse(request.body)
36
40
  end
41
+ body[key.to_s].to_s == val.to_s
42
+ end
37
43
 
38
- describe "#request_token" do
39
- let(:ttl) { 30 * 60 }
40
- let(:capability) { { :foo => ['publish'] } }
44
+ def serialize(object, protocol)
45
+ if protocol == :msgpack
46
+ MessagePack.pack(token_response)
47
+ else
48
+ JSON.dump(token_response)
49
+ end
50
+ end
41
51
 
42
- it 'returns the requested token' do
43
- actual_token = auth.request_token(
44
- ttl: ttl,
45
- capability: capability
46
- )
52
+ it 'has immutable options' do
53
+ expect { auth.options['key_id'] = 'new_id' }.to raise_error RuntimeError, /can't modify frozen Hash/
54
+ end
47
55
 
48
- expect(actual_token.id).to match(/^#{app_id}\.[\w-]+$/)
49
- expect(actual_token.key_id).to match(/^#{key_id}$/)
50
- expect(actual_token.issued_at).to be_within(2).of(Time.now)
51
- expect(actual_token.expires_at).to be_within(2).of(Time.now + ttl)
52
- end
53
-
54
- %w(client_id capability nonce timestamp ttl).each do |option|
55
- context "option :#{option}", webmock: true do
56
- let(:random) { SecureRandom.random_number(1_000_000_000).to_s }
57
- let(:options) { { option.to_sym => random } }
58
-
59
- let(:token_response) { { access_token: {} } }
60
- let!(:request_token_stub) do
61
- stub_request(:post, "#{client.endpoint}/keys/#{key_id}/requestToken").
62
- with do |request|
63
- request_body_includes(request, protocol, option, random)
64
- end.to_return(
65
- :status => 201,
66
- :body => serialize(token_response, protocol),
67
- :headers => { 'Content-Type' => content_type }
68
- )
69
- end
56
+ describe '#request_token' do
57
+ let(:ttl) { 30 * 60 }
58
+ let(:capability) { { :foo => ['publish'] } }
70
59
 
71
- before { auth.request_token options }
60
+ it 'returns the requested token' do
61
+ actual_token = auth.request_token(
62
+ ttl: ttl,
63
+ capability: capability
64
+ )
72
65
 
73
- it 'overrides default' do
74
- expect(request_token_stub).to have_been_requested
75
- end
76
- end
77
- end
66
+ expect(actual_token.id).to match(/^#{app_id}\.[\w-]+$/)
67
+ expect(actual_token.key_id).to match(/^#{key_id}$/)
68
+ expect(actual_token.issued_at).to be_within(2).of(Time.now)
69
+ expect(actual_token.expires_at).to be_within(2).of(Time.now + ttl)
70
+ end
78
71
 
79
- context 'with :key_id & :key_secret options', webmock: true do
80
- let(:key_id) { SecureRandom.hex }
81
- let(:key_secret) { SecureRandom.hex }
82
- let(:nonce) { SecureRandom.hex }
83
- let(:token_options) { { key_id: key_id, key_secret: key_secret, nonce: nonce, timestamp: Time.now } }
84
- let(:token_request) { auth.create_token_request(token_options) }
85
- let(:mac) do
86
- hmac_for(token_request, key_secret)
87
- end
72
+ %w(client_id capability nonce timestamp ttl).each do |option|
73
+ context "option :#{option}", :webmock do
74
+ let(:random) { random_int_str }
75
+ let(:options) { { option.to_sym => random } }
88
76
 
89
77
  let(:token_response) { { access_token: {} } }
90
78
  let!(:request_token_stub) do
91
79
  stub_request(:post, "#{client.endpoint}/keys/#{key_id}/requestToken").
92
80
  with do |request|
93
- request_body_includes(request, protocol, 'mac', mac)
81
+ request_body_includes(request, protocol, option, random)
94
82
  end.to_return(
95
83
  :status => 201,
96
84
  :body => serialize(token_response, protocol),
97
- :headers => { 'Content-Type' => content_type })
85
+ :headers => { 'Content-Type' => content_type }
86
+ )
98
87
  end
99
88
 
100
- let!(:token) { auth.request_token(token_options) }
89
+ before { auth.request_token options }
101
90
 
102
- specify 'key_id is used in request and signing uses key_secret' do
91
+ it 'overrides default' do
103
92
  expect(request_token_stub).to have_been_requested
104
93
  end
105
94
  end
95
+ end
106
96
 
107
- context 'with :query_time option' do
108
- let(:options) { { query_time: true } }
97
+ context 'with :key_id & :key_secret options', :webmock do
98
+ let(:key_id) { random_str }
99
+ let(:key_secret) { random_str }
100
+ let(:nonce) { random_str }
101
+ let(:token_options) { { key_id: key_id, key_secret: key_secret, nonce: nonce, timestamp: Time.now } }
102
+ let(:token_request) { auth.create_token_request(token_options) }
103
+ let(:mac) do
104
+ hmac_for(token_request, key_secret)
105
+ end
109
106
 
110
- it 'queries the server for the time' do
111
- expect(client).to receive(:time).and_call_original
112
- auth.request_token(options)
113
- end
107
+ let(:token_response) { { access_token: {} } }
108
+ let!(:request_token_stub) do
109
+ stub_request(:post, "#{client.endpoint}/keys/#{key_id}/requestToken").
110
+ with do |request|
111
+ request_body_includes(request, protocol, 'mac', mac)
112
+ end.to_return(
113
+ :status => 201,
114
+ :body => serialize(token_response, protocol),
115
+ :headers => { 'Content-Type' => content_type })
114
116
  end
115
117
 
116
- context 'without :query_time option' do
117
- let(:options) { { query_time: false } }
118
+ let!(:token) { auth.request_token(token_options) }
118
119
 
119
- it 'queries the server for the time' do
120
- expect(client).to_not receive(:time)
121
- auth.request_token(options)
122
- end
120
+ specify 'key_id is used in request and signing uses key_secret' do
121
+ expect(request_token_stub).to have_been_requested
122
+ end
123
+ end
124
+
125
+ context 'with :query_time option' do
126
+ let(:options) { { query_time: true } }
127
+
128
+ it 'queries the server for the time' do
129
+ expect(client).to receive(:time).and_call_original
130
+ auth.request_token(options)
123
131
  end
132
+ end
124
133
 
125
- context 'with :auth_url option', webmock: true do
126
- let(:auth_url) { 'https://www.fictitious.com/get_token' }
127
- let(:token_request) { { id: key_id } }
128
- let(:token_response) { { access_token: { } } }
129
- let(:query_params) { nil }
130
- let(:headers) { nil }
131
- let(:auth_method) { :get }
132
- let(:options) do
133
- {
134
- auth_url: auth_url,
135
- auth_params: query_params,
136
- auth_headers: headers,
137
- auth_method: auth_method
138
- }
139
- end
134
+ context 'without :query_time option' do
135
+ let(:options) { { query_time: false } }
140
136
 
141
- let!(:auth_url_request_stub) do
142
- stub = stub_request(auth_method, auth_url)
143
- stub.with(:query => query_params) unless query_params.nil?
144
- stub.with(:headers => headers) unless headers.nil?
145
- stub.to_return(
137
+ it 'queries the server for the time' do
138
+ expect(client).to_not receive(:time)
139
+ auth.request_token(options)
140
+ end
141
+ end
142
+
143
+ context 'with :auth_url option', :webmock do
144
+ let(:auth_url) { 'https://www.fictitious.com/get_token' }
145
+ let(:token_request) { { id: key_id } }
146
+ let(:token_response) { { access_token: { } } }
147
+ let(:query_params) { nil }
148
+ let(:headers) { nil }
149
+ let(:auth_method) { :get }
150
+ let(:options) do
151
+ {
152
+ auth_url: auth_url,
153
+ auth_params: query_params,
154
+ auth_headers: headers,
155
+ auth_method: auth_method
156
+ }
157
+ end
158
+
159
+ let!(:auth_url_request_stub) do
160
+ stub = stub_request(auth_method, auth_url)
161
+ stub.with(:query => query_params) unless query_params.nil?
162
+ stub.with(:headers => headers) unless headers.nil?
163
+ stub.to_return(
164
+ :status => 201,
165
+ :body => token_request.to_json,
166
+ :headers => { 'Content-Type' => 'application/json' }
167
+ )
168
+ end
169
+
170
+ let!(:request_token_stub) do
171
+ stub_request(:post, "#{client.endpoint}/keys/#{key_id}/requestToken").
172
+ with do |request|
173
+ request_body_includes(request, protocol, 'id', key_id)
174
+ end.to_return(
146
175
  :status => 201,
147
- :body => token_request.to_json,
148
- :headers => { 'Content-Type' => 'application/json' }
176
+ :body => serialize(token_response, protocol),
177
+ :headers => { 'Content-Type' => content_type }
149
178
  )
150
- end
179
+ end
151
180
 
152
- let!(:request_token_stub) do
153
- stub_request(:post, "#{client.endpoint}/keys/#{key_id}/requestToken").
154
- with do |request|
155
- request_body_includes(request, protocol, 'id', key_id)
156
- end.to_return(
157
- :status => 201,
158
- :body => serialize(token_response, protocol),
159
- :headers => { 'Content-Type' => content_type }
160
- )
161
- end
181
+ context 'when response is valid' do
182
+ before { auth.request_token options }
162
183
 
163
- context 'valid' do
164
- before { auth.request_token options }
184
+ it 'requests a token from :auth_url using an HTTP GET request' do
185
+ expect(request_token_stub).to have_been_requested
186
+ expect(auth_url_request_stub).to have_been_requested
187
+ end
165
188
 
166
- context 'and default options' do
167
- it 'requests a token from :auth_url' do
168
- expect(request_token_stub).to have_been_requested
169
- expect(auth_url_request_stub).to have_been_requested
170
- end
171
- end
189
+ context 'with :query_params' do
190
+ let(:query_params) { { 'key' => random_str } }
172
191
 
173
- context 'with params' do
174
- let(:query_params) { { 'key' => SecureRandom.hex } }
175
- it 'requests a token from :auth_url' do
176
- expect(request_token_stub).to have_been_requested
177
- expect(auth_url_request_stub).to have_been_requested
178
- end
192
+ it 'requests a token from :auth_url with the :query_params' do
193
+ expect(request_token_stub).to have_been_requested
194
+ expect(auth_url_request_stub).to have_been_requested
179
195
  end
196
+ end
180
197
 
181
- context 'with headers' do
182
- let(:headers) { { 'key' => SecureRandom.hex } }
183
- it 'requests a token from :auth_url' do
184
- expect(request_token_stub).to have_been_requested
185
- expect(auth_url_request_stub).to have_been_requested
186
- end
198
+ context 'with :headers' do
199
+ let(:headers) { { 'key' => random_str } }
200
+ it 'requests a token from :auth_url with the HTTP headers set' do
201
+ expect(request_token_stub).to have_been_requested
202
+ expect(auth_url_request_stub).to have_been_requested
187
203
  end
204
+ end
188
205
 
189
- context 'with POST' do
190
- let(:auth_method) { :post }
191
- it 'requests a token from :auth_url' do
192
- expect(request_token_stub).to have_been_requested
193
- expect(auth_url_request_stub).to have_been_requested
194
- end
206
+ context 'with POST' do
207
+ let(:auth_method) { :post }
208
+ it 'requests a token from :auth_url using an HTTP POST instead of the default GET' do
209
+ expect(request_token_stub).to have_been_requested
210
+ expect(auth_url_request_stub).to have_been_requested
195
211
  end
196
212
  end
213
+ end
197
214
 
198
- context 'when response is invalid' do
199
- context '500' do
200
- let!(:auth_url_request_stub) do
201
- stub_request(auth_method, auth_url).to_return(:status => 500)
202
- end
215
+ context 'when response is invalid' do
216
+ context '500' do
217
+ let!(:auth_url_request_stub) do
218
+ stub_request(auth_method, auth_url).to_return(:status => 500)
219
+ end
203
220
 
204
- it 'raises ServerError' do
205
- expect { auth.request_token options }.to raise_error(Ably::Exceptions::ServerError)
206
- end
221
+ it 'raises ServerError' do
222
+ expect { auth.request_token options }.to raise_error(Ably::Exceptions::ServerError)
207
223
  end
224
+ end
208
225
 
209
- context 'XML' do
210
- let!(:auth_url_request_stub) do
211
- stub_request(auth_method, auth_url).
212
- to_return(:status => 201, :body => '<xml></xml>', :headers => { 'Content-Type' => 'application/xml' })
213
- end
226
+ context 'XML' do
227
+ let!(:auth_url_request_stub) do
228
+ stub_request(auth_method, auth_url).
229
+ to_return(:status => 201, :body => '<xml></xml>', :headers => { 'Content-Type' => 'application/xml' })
230
+ end
214
231
 
215
- it 'raises InvalidResponseBody' do
216
- expect { auth.request_token options }.to raise_error(Ably::Exceptions::InvalidResponseBody)
217
- end
232
+ it 'raises InvalidResponseBody' do
233
+ expect { auth.request_token options }.to raise_error(Ably::Exceptions::InvalidResponseBody)
218
234
  end
219
235
  end
220
236
  end
237
+ end
221
238
 
222
- context 'with auth_block' do
223
- let(:client_id) { SecureRandom.hex }
224
- let(:options) { { client_id: client_id } }
225
- let!(:token) do
226
- auth.request_token(options) do |block_options|
227
- @block_called = true
228
- @block_options = block_options
229
- auth.create_token_request(client_id: client_id)
230
- end
239
+ context 'with token_request_block' do
240
+ let(:client_id) { random_str }
241
+ let(:options) { { client_id: client_id } }
242
+ let!(:token) do
243
+ auth.request_token(options) do |block_options|
244
+ @block_called = true
245
+ @block_options = block_options
246
+ auth.create_token_request(client_id: client_id)
231
247
  end
248
+ end
232
249
 
233
- it 'calls the block' do
234
- expect(@block_called).to eql(true)
235
- expect(@block_options).to include(options)
236
- end
250
+ it 'calls the block when authenticating to obtain the request token' do
251
+ expect(@block_called).to eql(true)
252
+ expect(@block_options).to include(options)
253
+ end
237
254
 
238
- it 'uses the token request when requesting a new token' do
239
- expect(token.client_id).to eql(client_id)
240
- end
255
+ it 'uses the token request from the block when requesting a new token' do
256
+ expect(token.client_id).to eql(client_id)
241
257
  end
242
258
  end
259
+ end
243
260
 
244
- describe '#authorise' do
245
- context 'with no previous authorisation' do
246
- let(:request_options) do
247
- { auth_url: 'http://somewhere.com/' }
248
- end
261
+ context 'before #authorise has been called' do
262
+ it 'has no current_token' do
263
+ expect(auth.current_token).to be_nil
264
+ end
265
+ end
249
266
 
250
- it 'has no current_token' do
251
- expect(auth.current_token).to be_nil
252
- end
267
+ describe '#authorise' do
268
+ context 'when called for the first time since the client has been instantiated' do
269
+ let(:request_options) do
270
+ { auth_url: 'http://somewhere.com/' }
271
+ end
253
272
 
254
- it 'passes all options to request_token' do
255
- expect(auth).to receive(:request_token).with(request_options)
256
- auth.authorise request_options
257
- end
273
+ it 'passes all options to #request_token' do
274
+ expect(auth).to receive(:request_token).with(request_options)
275
+ auth.authorise request_options
276
+ end
258
277
 
259
- it 'returns a valid token' do
260
- expect(auth.authorise).to be_a(Ably::Models::Token)
261
- end
278
+ it 'returns a valid token' do
279
+ expect(auth.authorise).to be_a(Ably::Models::Token)
280
+ end
262
281
 
263
- it 'issues a new token if option :force => true' do
264
- expect { auth.authorise(force: true) }.to change { auth.current_token }
265
- end
282
+ it 'issues a new token if option :force => true' do
283
+ expect { auth.authorise(force: true) }.to change { auth.current_token }
266
284
  end
285
+ end
267
286
 
268
- context 'with previous authorisation' do
269
- before do
270
- auth.authorise
271
- expect(auth.current_token).to_not be_expired
272
- end
287
+ context 'with previous authorisation' do
288
+ before do
289
+ auth.authorise
290
+ expect(auth.current_token).to_not be_expired
291
+ end
273
292
 
274
- it 'does not request a token if token is not expired' do
275
- expect(auth).to_not receive(:request_token)
276
- auth.authorise
277
- end
293
+ it 'does not request a token if current_token has not expired' do
294
+ expect(auth).to_not receive(:request_token)
295
+ auth.authorise
296
+ end
278
297
 
279
- it 'requests a new token if token is expired' do
280
- allow(auth.current_token).to receive(:expired?).and_return(true)
281
- expect(auth).to receive(:request_token)
282
- expect { auth.authorise }.to change { auth.current_token }
283
- end
298
+ it 'requests a new token if token is expired' do
299
+ allow(auth.current_token).to receive(:expired?).and_return(true)
300
+ expect(auth).to receive(:request_token)
301
+ expect { auth.authorise }.to change { auth.current_token }
302
+ end
284
303
 
285
- it 'issues a new token if option :force => true' do
286
- expect { auth.authorise(force: true) }.to change { auth.current_token }
287
- end
304
+ it 'issues a new token if option :force => true' do
305
+ expect { auth.authorise(force: true) }.to change { auth.current_token }
288
306
  end
289
307
  end
290
308
 
291
- describe '#create_token_request' do
292
- let(:ttl) { 60 * 60 }
293
- let(:capability) { { :foo => ["publish"] } }
294
- let(:options) { Hash.new }
295
- subject { auth.create_token_request(options) }
309
+ it 'updates the persisted auth options thare are then used for subsequent authorise requests' do
310
+ expect(auth.options[:ttl]).to_not eql(26)
311
+ auth.authorise(ttl: 26)
312
+ expect(auth.options[:ttl]).to eql(26)
313
+ end
296
314
 
297
- it 'uses the key ID from the client' do
298
- expect(subject[:id]).to eql(key_id)
315
+ context 'with token_request_block' do
316
+ let(:client_id) { random_str }
317
+ let!(:token) do
318
+ auth.authorise do
319
+ @block_called ||= 0
320
+ @block_called += 1
321
+ auth.create_token_request(client_id: client_id)
322
+ end
299
323
  end
300
324
 
301
- it 'uses the default TTL' do
302
- expect(subject[:ttl]).to eql(Ably::Models::Token::DEFAULTS[:ttl])
325
+ it 'calls the block' do
326
+ expect(@block_called).to eql(1)
303
327
  end
304
328
 
305
- it 'uses the default capability' do
306
- expect(subject[:capability]).to eql(Ably::Models::Token::DEFAULTS[:capability].to_json)
329
+ it 'uses the token request returned from the block when requesting a new token' do
330
+ expect(token.client_id).to eql(client_id)
307
331
  end
308
332
 
309
- it 'has a unique nonce' do
333
+ context 'for every subsequent #request_token' do
334
+ context 'without a provided block' do
335
+ it 'calls the originally provided block' do
336
+ auth.request_token
337
+ expect(@block_called).to eql(2)
338
+ end
339
+ end
340
+
341
+ context 'with a provided block' do
342
+ it 'does not call the originally provided block and calls the new #request_token block' do
343
+ auth.request_token { @request_block_called = true; auth.create_token_request }
344
+ expect(@block_called).to eql(1)
345
+ expect(@request_block_called).to eql(true)
346
+ end
347
+ end
348
+ end
349
+ end
350
+ end
351
+
352
+ describe '#create_token_request' do
353
+ let(:ttl) { 60 * 60 }
354
+ let(:capability) { { :foo => ["publish"] } }
355
+ let(:options) { Hash.new }
356
+ subject { auth.create_token_request(options) }
357
+
358
+ it 'uses the key ID from the client' do
359
+ expect(subject[:id]).to eql(key_id)
360
+ end
361
+
362
+ it 'uses the default TTL' do
363
+ expect(subject[:ttl]).to eql(Ably::Models::Token::DEFAULTS[:ttl])
364
+ end
365
+
366
+ it 'uses the default capability' do
367
+ expect(subject[:capability]).to eql(Ably::Models::Token::DEFAULTS[:capability].to_json)
368
+ end
369
+
370
+ context 'the nonce' do
371
+ it 'is unique for every request' do
310
372
  unique_nonces = 100.times.map { auth.create_token_request[:nonce] }
311
373
  expect(unique_nonces.uniq.length).to eql(100)
312
374
  end
313
375
 
314
- it 'has a nonce of at least 16 characters' do
376
+ it 'is at least 16 characters' do
315
377
  expect(subject[:nonce].length).to be >= 16
316
378
  end
379
+ end
317
380
 
318
- %w(ttl capability nonce timestamp client_id).each do |attribute|
319
- context "with option :#{attribute}" do
320
- let(:option_value) { SecureRandom.random_number(1_000_000_000) }
321
- before do
322
- options[attribute.to_sym] = option_value
323
- end
324
- it "overrides default" do
325
- expect(subject[attribute.to_sym]).to eql(option_value)
326
- end
381
+ %w(ttl capability nonce timestamp client_id).each do |attribute|
382
+ context "with option :#{attribute}" do
383
+ let(:option_value) { random_int_str(1_000_000_000) }
384
+ before do
385
+ options[attribute.to_sym] = option_value
386
+ end
387
+ it "overrides default" do
388
+ expect(subject[attribute.to_sym].to_s).to eql(option_value.to_s)
327
389
  end
328
390
  end
391
+ end
329
392
 
330
- context 'invalid attributes' do
331
- let(:options) { { nonce: 'valid', is_not_used_by_token_request: 'invalid' } }
332
- specify 'are ignored' do
333
- expect(subject.keys).to_not include(:is_not_used_by_token_request)
334
- expect(subject.keys).to include(:nonce)
335
- expect(subject[:nonce]).to eql('valid')
336
- end
393
+ context 'with additional invalid attributes' do
394
+ let(:options) { { nonce: 'valid', is_not_used_by_token_request: 'invalid' } }
395
+ specify 'are ignored' do
396
+ expect(subject.keys).to_not include(:is_not_used_by_token_request)
397
+ expect(subject.keys).to include(:nonce)
398
+ expect(subject[:nonce]).to eql('valid')
337
399
  end
400
+ end
338
401
 
339
- context 'missing key ID and/or secret' do
340
- let(:client) { Ably::Rest::Client.new(auth_url: 'http://example.com', protocol: protocol) }
402
+ context 'when required fields are missing' do
403
+ let(:client) { Ably::Rest::Client.new(auth_url: 'http://example.com', protocol: protocol) }
341
404
 
342
- it 'should raise an exception if key secret is missing' do
343
- expect { auth.create_token_request(key_id: 'id') }.to raise_error Ably::Exceptions::TokenRequestError
344
- end
405
+ it 'should raise an exception if key secret is missing' do
406
+ expect { auth.create_token_request(key_id: 'id') }.to raise_error Ably::Exceptions::TokenRequestError
407
+ end
345
408
 
346
- it 'should raise an exception if key id is missing' do
347
- expect { auth.create_token_request(key_secret: 'secret') }.to raise_error Ably::Exceptions::TokenRequestError
348
- end
409
+ it 'should raise an exception if key id is missing' do
410
+ expect { auth.create_token_request(key_secret: 'secret') }.to raise_error Ably::Exceptions::TokenRequestError
349
411
  end
412
+ end
350
413
 
351
- context 'with :query_time option' do
352
- let(:time) { Time.now - 30 }
353
- let(:options) { { query_time: true } }
414
+ context 'with :query_time option' do
415
+ let(:time) { Time.now - 30 }
416
+ let(:options) { { query_time: true } }
354
417
 
355
- it 'queries the server for the time' do
356
- expect(client).to receive(:time).and_return(time)
357
- expect(subject[:timestamp]).to eql(time.to_i)
358
- end
418
+ it 'queries the server for the timestamp' do
419
+ expect(client).to receive(:time).and_return(time)
420
+ expect(subject[:timestamp]).to eql(time.to_i)
359
421
  end
422
+ end
360
423
 
361
- context 'with :timestamp option' do
362
- let(:token_request_time) { Time.now + 5 }
363
- let(:options) { { timestamp: token_request_time } }
424
+ context 'with :timestamp option' do
425
+ let(:token_request_time) { Time.now + 5 }
426
+ let(:options) { { timestamp: token_request_time } }
364
427
 
365
- it 'uses the provided timestamp' do
366
- expect(subject[:timestamp]).to eql(token_request_time.to_i)
367
- end
428
+ it 'uses the provided timestamp in the token request' do
429
+ expect(subject[:timestamp]).to eql(token_request_time.to_i)
368
430
  end
431
+ end
369
432
 
370
- context 'signing' do
371
- let(:options) do
372
- {
373
- id: SecureRandom.hex,
374
- ttl: SecureRandom.hex,
375
- capability: SecureRandom.hex,
376
- client_id: SecureRandom.hex,
377
- timestamp: SecureRandom.random_number(1_000_000_000),
378
- nonce: SecureRandom.hex
379
- }
380
- end
433
+ context 'signing' do
434
+ let(:options) do
435
+ {
436
+ id: random_str,
437
+ ttl: random_str,
438
+ capability: random_str,
439
+ client_id: random_str,
440
+ timestamp: random_int_str,
441
+ nonce: random_str
442
+ }
443
+ end
381
444
 
382
- it 'generates a valid HMAC' do
383
- hmac = hmac_for(options, key_secret)
384
- expect(subject[:mac]).to eql(hmac)
385
- end
445
+ it 'generates a valid HMAC' do
446
+ hmac = hmac_for(options, key_secret)
447
+ expect(subject[:mac]).to eql(hmac)
386
448
  end
387
449
  end
450
+ end
388
451
 
389
- context 'client with token authentication' do
390
- let(:capability) { { :foo => ["publish"] } }
452
+ context 'using token authentication' do
453
+ let(:capability) { { :foo => ["publish"] } }
391
454
 
392
- describe "with token_id argument" do
393
- let(:ttl) { 60 * 60 }
394
- let(:token) do
395
- auth.request_token(
396
- ttl: ttl,
397
- capability: capability
398
- )
399
- end
400
- let(:token_id) { token.id }
401
- let(:token_auth_client) do
402
- Ably::Rest::Client.new(token_id: token_id, environment: environment, protocol: protocol)
403
- end
455
+ describe 'with :token_id option' do
456
+ let(:ttl) { 60 * 60 }
457
+ let(:token) do
458
+ auth.request_token(
459
+ ttl: ttl,
460
+ capability: capability
461
+ )
462
+ end
463
+ let(:token_id) { token.id }
464
+ let(:token_auth_client) do
465
+ Ably::Rest::Client.new(token_id: token_id, environment: environment, protocol: protocol)
466
+ end
404
467
 
405
- it 'authenticates successfully' do
406
- expect(token_auth_client.channel('foo').publish('event', 'data')).to be_truthy
407
- end
468
+ it 'authenticates successfully using the provided :token_id' do
469
+ expect(token_auth_client.channel('foo').publish('event', 'data')).to be_truthy
470
+ end
408
471
 
409
- it 'disallows publishing on unspecified capability channels' do
410
- expect { token_auth_client.channel('bar').publish('event', 'data') }.to raise_error do |error|
411
- expect(error).to be_a(Ably::Exceptions::InvalidToken)
412
- expect(error.status).to eql(401)
413
- expect(error.code).to eql(40160)
414
- end
472
+ it 'disallows publishing on unspecified capability channels' do
473
+ expect { token_auth_client.channel('bar').publish('event', 'data') }.to raise_error do |error|
474
+ expect(error).to be_a(Ably::Exceptions::InvalidRequest)
475
+ expect(error.status).to eql(401)
476
+ expect(error.code).to eql(40160)
415
477
  end
478
+ end
416
479
 
417
- it 'fails if timestamp is invalid' do
418
- expect { auth.request_token(timestamp: Time.now - 180) }.to raise_error do |error|
419
- expect(error).to be_a(Ably::Exceptions::InvalidToken)
420
- expect(error.status).to eql(401)
421
- expect(error.code).to eql(40101)
422
- end
480
+ it 'fails if timestamp is invalid' do
481
+ expect { auth.request_token(timestamp: Time.now - 180) }.to raise_error do |error|
482
+ expect(error).to be_a(Ably::Exceptions::InvalidRequest)
483
+ expect(error.status).to eql(401)
484
+ expect(error.code).to eql(40101)
423
485
  end
424
486
  end
425
487
 
426
- describe 'implicit through client id' do
427
- let(:client_id) { '999' }
428
- let(:client) do
429
- Ably::Rest::Client.new(api_key: api_key, client_id: client_id, environment: environment, protocol: protocol)
488
+ it 'cannot be renewed automatically' do
489
+ expect(token_auth_client.auth).to_not be_token_renewable
490
+ end
491
+ end
492
+
493
+ context 'when implicit as a result of using :client id' do
494
+ let(:client_id) { '999' }
495
+ let(:client) do
496
+ Ably::Rest::Client.new(api_key: api_key, client_id: client_id, environment: environment, protocol: protocol)
497
+ end
498
+ let(:token_id) { 'unique-token-id' }
499
+ let(:token_response) do
500
+ {
501
+ access_token: {
502
+ id: token_id
503
+ }
504
+ }.to_json
505
+ end
506
+
507
+ context 'and requests to the Ably server are mocked', :webmock do
508
+ let!(:request_token_stub) do
509
+ stub_request(:post, "#{client.endpoint}/keys/#{key_id}/requestToken").
510
+ to_return(:status => 201, :body => token_response, :headers => { 'Content-Type' => 'application/json' })
430
511
  end
431
- let(:token_id) { 'unique-token-id' }
432
- let(:token_response) do
433
- {
434
- access_token: {
435
- id: token_id
436
- }
437
- }.to_json
512
+ let!(:publish_message_stub) do
513
+ stub_request(:post, "#{client.endpoint}/channels/foo/publish").
514
+ with(headers: { 'Authorization' => "Bearer #{encode64(token_id)}" }).
515
+ to_return(status: 201, body: '{}', headers: { 'Content-Type' => 'application/json' })
438
516
  end
439
517
 
440
- context 'stubbed', webmock: true do
441
- let!(:request_token_stub) do
442
- stub_request(:post, "#{client.endpoint}/keys/#{key_id}/requestToken").
443
- to_return(:status => 201, :body => token_response, :headers => { 'Content-Type' => 'application/json' })
444
- end
445
- let!(:publish_message_stub) do
446
- stub_request(:post, "#{client.endpoint}/channels/foo/publish").
447
- with(headers: { 'Authorization' => "Bearer #{encode64(token_id)}" }).
448
- to_return(status: 201, body: '{}', headers: { 'Content-Type' => 'application/json' })
449
- end
450
-
451
- it 'will create a token request' do
452
- client.channel('foo').publish('event', 'data')
453
- expect(request_token_stub).to have_been_requested
454
- end
518
+ it 'will send a token request to the server' do
519
+ client.channel('foo').publish('event', 'data')
520
+ expect(request_token_stub).to have_been_requested
455
521
  end
522
+ end
456
523
 
457
- context 'will create a token' do
458
- let(:token) { client.auth.current_token }
524
+ describe 'a token is created' do
525
+ let(:token) { client.auth.current_token }
459
526
 
460
- it 'before a request is made' do
461
- expect(token).to be_nil
462
- end
527
+ it 'before a request is made' do
528
+ expect(token).to be_nil
529
+ end
463
530
 
464
- it 'when a message is published' do
465
- expect(client.channel('foo').publish('event', 'data')).to be_truthy
466
- end
531
+ it 'when a message is published' do
532
+ expect(client.channel('foo').publish('event', 'data')).to be_truthy
533
+ end
467
534
 
468
- it 'with capability and TTL defaults' do
469
- client.channel('foo').publish('event', 'data')
535
+ it 'with capability and TTL defaults' do
536
+ client.channel('foo').publish('event', 'data')
470
537
 
471
- expect(token).to be_a(Ably::Models::Token)
472
- capability_with_str_key = Ably::Models::Token::DEFAULTS[:capability]
473
- capability = Hash[capability_with_str_key.keys.map(&:to_sym).zip(capability_with_str_key.values)]
474
- expect(token.capability).to eq(capability)
475
- expect(token.expires_at.to_i).to be_within(2).of(Time.now.to_i + Ably::Models::Token::DEFAULTS[:ttl])
476
- expect(token.client_id).to eq(client_id)
477
- end
538
+ expect(token).to be_a(Ably::Models::Token)
539
+ capability_with_str_key = Ably::Models::Token::DEFAULTS[:capability]
540
+ capability = Hash[capability_with_str_key.keys.map(&:to_sym).zip(capability_with_str_key.values)]
541
+ expect(token.capability).to eq(capability)
542
+ expect(token.expires_at.to_i).to be_within(2).of(Time.now.to_i + Ably::Models::Token::DEFAULTS[:ttl])
543
+ expect(token.client_id).to eq(client_id)
478
544
  end
479
545
  end
480
546
  end
481
547
  end
482
- end
483
548
 
484
- def hmac_for(token_request, secret)
485
- text = token_request.values_at(
486
- :id,
487
- :ttl,
488
- :capability,
489
- :client_id,
490
- :timestamp,
491
- :nonce
492
- ).map { |t| "#{t}\n" }.join("")
549
+ context 'when using an :api_key and basic auth' do
550
+ specify '#using_token_auth? is false' do
551
+ expect(auth).to_not be_using_token_auth
552
+ end
493
553
 
494
- encode64(
495
- Digest::HMAC.digest(text, key_secret, Digest::SHA256)
496
- )
554
+ specify '#using_basic_auth? is true' do
555
+ expect(auth).to be_using_basic_auth
556
+ end
557
+ end
497
558
  end
498
559
  end