ably 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3b4423dc2ad92073b07e69cb72a3acbbe2d0973a
4
- data.tar.gz: 76c5bb17b624875ec6fea9300c3778b216635266
3
+ metadata.gz: d9ccb54c36b77720433fde71a6580424b91cbfc0
4
+ data.tar.gz: fb242c4a0ce0c6ddd96c217d9904ee5926fcf465
5
5
  SHA512:
6
- metadata.gz: e107b6875dbfc0bd05ba14c48b37bb0d6fea3d514666353495ff12c822bcc62f09322a3442eed5239ccf1ac1df466e48cb59387970be373260bfb9c40f28dce2
7
- data.tar.gz: aa9f928e0f48b74cb158d8f0b7b8d21dec755427b02bff93d8f73fd6233be0ec4f8a5926acfd7ee0de3d6e6c3aff364f005122b561d7db4ee18aa6c169934703
6
+ metadata.gz: 9b681c12026a2fd84f3e5b71dfd901513a90f4c9cdb04ed9bef887543ce8d29a9ce1ef5e18e281090e7eae3940732146743948a8d17efa0fd0d09da0b03a6bc9
7
+ data.tar.gz: 116e3a2d88b423cd4af365656af2269467cc21601567897c3dd26efc0425c5d73e578a5d924021aa957661da1c417af527cc49dc8ef83ed4eeb82990e5dd6e7a
data/README.md CHANGED
@@ -1,9 +1,11 @@
1
- # Ably
1
+ # [Ably](https://ably.io)
2
2
 
3
3
  A Ruby client library for [ably.io](https://ably.io), the real-time messaging service.
4
4
 
5
5
  ## Installation
6
6
 
7
+ The client library is available as a [gem from RubyGems.org](https://rubygems.org/gems/ably).
8
+
7
9
  Add this line to your application's Gemfile:
8
10
 
9
11
  gem 'ably'
@@ -22,15 +24,14 @@ Or install it yourself as:
22
24
 
23
25
  Given:
24
26
 
25
- ```
27
+ ```ruby
26
28
  client = Ably::Realtime.new(api_key: "xxxxx")
27
-
28
29
  channel = client.channel("test")
29
30
  ```
30
31
 
31
32
  Subscribe to all events:
32
33
 
33
- ```
34
+ ```ruby
34
35
  channel.subscribe do |message|
35
36
  message[:name] #=> "greeting"
36
37
  message[:data] #=> "Hello World!"
@@ -39,7 +40,7 @@ end
39
40
 
40
41
  Only certain events:
41
42
 
42
- ```
43
+ ```ruby
43
44
  channel.subscribe("myEvent") do |message|
44
45
  message[:name] #=> "myEvent"
45
46
  message[:data] #=> "myData"
@@ -48,11 +49,9 @@ end
48
49
 
49
50
  ### Publishing to a channel
50
51
 
51
- ```
52
+ ```ruby
52
53
  client = Ably::Realtime.new(api_key: "xxxxx")
53
-
54
54
  channel = client.channel("test")
55
-
56
55
  channel.publish("greeting", "Hello World!")
57
56
  ```
58
57
 
@@ -60,37 +59,40 @@ channel.publish("greeting", "Hello World!")
60
59
 
61
60
  ### Publishing a message to a channel
62
61
 
63
- ```
62
+ ```ruby
64
63
  client = Ably::Rest.new(api_key: "xxxxx")
65
-
66
64
  channel = client.channel("test")
67
-
68
65
  channel.publish("myEvent", "Hello!") #=> true
69
66
  ```
70
67
 
71
68
  ### Fetching a channel's history
72
69
 
73
- ```
70
+ ```ruby
74
71
  client = Ably::Rest.new(api_key: "xxxxx")
75
-
76
72
  channel = client.channel("test")
77
-
78
73
  channel.history #=> [{:name=>"test", :data=>"payload"}]
79
74
  ```
80
75
 
81
- ### Fetching your application's stats
76
+ ### Authentication with a token
82
77
 
83
- ```
78
+ ```ruby
84
79
  client = Ably::Rest.new(api_key: "xxxxx")
80
+ client.auth.authorise # creates a token and will use token authentication moving forwards
81
+ client.auth.current_token #=> #<Ably::Token>
82
+ channel.publish("myEvent", "Hello!") #=> true, sent using token authentication
83
+ ```
85
84
 
85
+ ### Fetching your application's stats
86
+
87
+ ```ruby
88
+ client = Ably::Rest.new(api_key: "xxxxx")
86
89
  client.stats #=> [{:channels=>..., :apiRequests=>..., ...}]
87
90
  ```
88
91
 
89
92
  ### Fetching the Ably service time
90
93
 
91
- ```
94
+ ```ruby
92
95
  client = Ably::Rest.new(api_key: "xxxxx")
93
-
94
96
  client.time #=> 2013-12-12 14:23:34 +0000
95
97
  ```
96
98
 
@@ -2,6 +2,7 @@ require "ably/support"
2
2
 
3
3
  require "ably/auth"
4
4
  require "ably/exceptions"
5
+ require "ably/message"
5
6
  require "ably/rest"
6
7
  require "ably/realtime"
7
8
  require "ably/token"
@@ -37,6 +37,8 @@ module Ably
37
37
  # @param [Hash] auth_options see {Ably::Rest::Client#initialize}
38
38
  # @yield [auth_options] see {Ably::Rest::Client#initialize}
39
39
  def initialize(client, auth_options, &auth_block)
40
+ auth_options = auth_options.dup
41
+
40
42
  @client = client
41
43
  @options = auth_options
42
44
  @auth_callback = auth_block if block_given?
@@ -160,7 +162,7 @@ module Ably
160
162
 
161
163
  response = client.post("/keys/#{token_request.fetch(:id)}/requestToken", token_request, send_auth_header: false)
162
164
 
163
- Ably::Token.new(response.body[:access_token])
165
+ Ably::Token.new(response.body.fetch(:access_token))
164
166
  end
165
167
 
166
168
  # Creates and signs a token request that can then subsequently be used by any client to request a token
@@ -194,7 +196,7 @@ module Ably
194
196
  request_key_id = token_options.delete(:key_id) || key_id
195
197
  request_key_secret = token_options.delete(:key_secret) || key_secret
196
198
 
197
- raise TokenRequestError, "Key ID and Key Secret are required to generate a new token request" unless request_key_id && request_key_secret
199
+ raise Ably::Exceptions::TokenRequestError, "Key ID and Key Secret are required to generate a new token request" unless request_key_id && request_key_secret
198
200
 
199
201
  timestamp = if token_options[:query_time]
200
202
  client.time
@@ -261,11 +263,23 @@ module Ably
261
263
  end
262
264
  end
263
265
 
266
+ # True if prerequisites for creating a new token request are present
267
+ #
268
+ # One of the following criterion must be met:
269
+ # * Valid key id and secret
270
+ # * Authentication callback for new token requests
271
+ # * Authentication URL for new token requests
272
+ #
273
+ # @return [Boolean]
274
+ def token_renewable?
275
+ token_creatable_externally? || api_key_present?
276
+ end
277
+
264
278
  private
265
279
  attr_reader :auth_callback
266
280
 
267
281
  def basic_auth_header
268
- raise InsecureRequestError, "Cannot use Basic Auth over non-TLS connections" unless client.use_tls?
282
+ raise Ably::Exceptions::InsecureRequestError, "Cannot use Basic Auth over non-TLS connections" unless client.use_tls?
269
283
  "Basic #{encode64("#{api_key}")}"
270
284
  end
271
285
 
@@ -363,10 +377,6 @@ module Ably
363
377
  token_callback_present? || token_url_present?
364
378
  end
365
379
 
366
- def token_renewable?
367
- use_basic_auth? || token_creatable_externally?
368
- end
369
-
370
380
  def has_client_id?
371
381
  !!client_id
372
382
  end
@@ -1,16 +1,38 @@
1
1
  module Ably
2
- class InvalidRequest < StandardError
3
- attr_reader :status, :code
4
- def initialize(message, status: nil, code: nil)
5
- super message
6
- @status = status
7
- @code = code
2
+ module Exceptions
3
+ # An invalid request was received by Ably
4
+ #
5
+ # @!attribute [r] message
6
+ # @return [String] Error message from Ably
7
+ # @!attribute [r] status
8
+ # @return [String] HTTP status code of error
9
+ # @!attribute [r] code
10
+ # @return [String] Ably specific error code
11
+ class InvalidRequest < StandardError
12
+ attr_reader :status, :code
13
+ def initialize(message, status: nil, code: nil)
14
+ super message
15
+ @status = status
16
+ @code = code
17
+ end
8
18
  end
9
- end
10
19
 
11
- class ServerError < StandardError; end
12
- class InvalidPageError < StandardError; end
13
- class InvalidResponseBody < StandardError; end
14
- class InsecureRequestError < StandardError; end
15
- class TokenRequestError < StandardError; end
20
+ # The HTTP request has returned a 500 error
21
+ class ServerError < StandardError; end
22
+
23
+ # PagedResource cannot retrieve the page
24
+ class InvalidPageError < StandardError; end
25
+
26
+ # The expected response from the server was invalid
27
+ class InvalidResponseBody < StandardError; end
28
+
29
+ # The request cannot be performed because it is insecure
30
+ class InsecureRequestError < StandardError; end
31
+
32
+ # The token request could not be created
33
+ class TokenRequestError < StandardError; end
34
+
35
+ # The token is invalid
36
+ class InvalidToken < InvalidRequest; end
37
+ end
16
38
  end
@@ -0,0 +1,70 @@
1
+ module Ably
2
+ # A Message encapsulates an individual message sent or received in Ably
3
+ class Message
4
+ def initialize(message)
5
+ @message = message.dup.freeze
6
+ end
7
+
8
+ # Event name
9
+ #
10
+ # @return [String]
11
+ def name
12
+ @message[:name]
13
+ end
14
+
15
+ # Payload
16
+ #
17
+ # @return [Object]
18
+ def data
19
+ @message[:data]
20
+ end
21
+
22
+ # Client ID of the publisher of the message
23
+ #
24
+ # @return [String]
25
+ def client_id
26
+ @message[:client_id]
27
+ end
28
+
29
+ # Timestamp in milliseconds since epoch. This property is populated by the Ably system.
30
+ #
31
+ # @return [Integer]
32
+ def timestamp
33
+ @message[:timestamp]
34
+ end
35
+
36
+ # Timestamp as {Time}. This property is populated by the Ably system.
37
+ #
38
+ # @return [Time]
39
+ def timestamp_at
40
+ raise RuntimeError, "Timestamp is missing" unless timestamp
41
+ Time.at(timestamp / 1000.0)
42
+ end
43
+
44
+ # Unique serial number of this message within the channel
45
+ #
46
+ # @return [Integer]
47
+ def channel_serial
48
+ @message[:channel_serial]
49
+ end
50
+
51
+ # Provide a normal Hash accessor to the underlying raw message object
52
+ #
53
+ # @return [Object]
54
+ def [](key)
55
+ @message[key]
56
+ end
57
+
58
+ # Raw message object
59
+ #
60
+ # @return [Hash]
61
+ def raw_message
62
+ @message
63
+ end
64
+
65
+ def ==(other)
66
+ self.kind_of?(other.class) &&
67
+ raw_message == other.raw_message
68
+ end
69
+ end
70
+ end
@@ -1,4 +1,5 @@
1
1
  require "ably/rest/channel"
2
+ require "ably/rest/channels"
2
3
  require "ably/rest/client"
3
4
  require "ably/rest/paged_resource"
4
5
  require "ably/rest/presence"
@@ -1,25 +1,30 @@
1
1
  module Ably
2
2
  module Rest
3
+ # The Ably Realtime service organises the traffic within any application into named channels.
4
+ # Channels are the "unit" of message distribution; clients attach to channels to subscribe to messages, and every message broadcast by the service is associated with a unique channel.
3
5
  class Channel
4
- attr_reader :client, :name
6
+ attr_reader :client, :name, :options
5
7
 
6
8
  # Initialize a new Channel object
7
9
  #
8
10
  # @param client [Ably::Rest::Client]
9
11
  # @param name [String] The name of the channel
10
- def initialize(client, name)
11
- @client = client
12
- @name = name
12
+ # @param channel_options [Hash] Channel options, currently reserved for Encryption options
13
+ def initialize(client, name, channel_options = {})
14
+ @client = client
15
+ @name = name
16
+ @options = channel_options.dup.freeze
13
17
  end
14
18
 
15
19
  # Publish a message to the channel
16
20
  #
17
- # @param message [Hash] The message to publish (must contain :name and :data keys)
21
+ # @param name [String] The event name of the message to publish
22
+ # @param data [String] The message payload
18
23
  # @return [Boolean] true if the message was published, otherwise false
19
- def publish(event, message)
24
+ def publish(name, data)
20
25
  payload = {
21
- name: event,
22
- data: message
26
+ name: name,
27
+ data: data
23
28
  }
24
29
 
25
30
  response = client.post("#{base_path}/publish", payload)
@@ -29,12 +34,12 @@ module Ably
29
34
 
30
35
  # Return the message history of the channel
31
36
  #
32
- # Options:
33
- # - start: Time or millisecond since epoch
34
- # - end: Time or millisecond since epoch
35
- # - direction: :forwards or :backwards
36
- # - limit: Maximum number of messages to retrieve up to 10,000
37
- # - by: :message, :bundle or :hour. Defaults to :message
37
+ # @param [Hash] options the options for the message history request
38
+ # @option options [Integer] :start Time or millisecond since epoch
39
+ # @option options [Integer] :end Time or millisecond since epoch
40
+ # @option options [Symbol] :direction `:forwards` or `:backwards`
41
+ # @option options [Integer] :limit Maximum number of messages to retrieve up to 10,000
42
+ # @option options [Symbol] :by `:message`, `:bundle` or `:hour`. Defaults to `:message`
38
43
  #
39
44
  # @return [PagedResource] An Array of hashes representing the message history that supports paging (next, first)
40
45
  def history(options = {})
@@ -42,7 +47,7 @@ module Ably
42
47
  # TODO: Remove live param as all history should be live
43
48
  response = client.get(url, options.merge(live: true))
44
49
 
45
- PagedResource.new(response, url, client)
50
+ PagedResource.new(response, url, client, coerce_into: 'Ably::Message')
46
51
  end
47
52
 
48
53
  def presence
@@ -0,0 +1,30 @@
1
+ module Ably
2
+ module Rest
3
+ class Channels
4
+ attr_reader :client
5
+
6
+ # Initialize a new Channels object
7
+ #
8
+ # {Ably::Rest::Channels} provides simple accessor methods to access a {Ably::Rest::Channel} object
9
+ def initialize(client)
10
+ @client = client
11
+ @channels = {}
12
+ end
13
+
14
+ # Return a REST {Ably::Rest::Channel} for the given name
15
+ #
16
+ # @param name [String] The name of the channel
17
+ # @param channel_options [Hash] Channel options, currently reserved for Encryption options
18
+ #
19
+ # @return [Ably::Rest::Channel]
20
+ def get(name, channel_options = {})
21
+ @channels[name] ||= Ably::Rest::Channel.new(client, name, channel_options)
22
+ end
23
+ alias_method :[], :get
24
+
25
+ def close(channel)
26
+ @channels.delete(channel)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -24,7 +24,7 @@ module Ably
24
24
 
25
25
  DOMAIN = "rest.ably.io"
26
26
 
27
- attr_reader :tls, :environment, :auth
27
+ attr_reader :tls, :environment, :auth, :channels
28
28
  def_delegator :auth, :client_id, :auth_options
29
29
 
30
30
  # Creates a {Ably::Rest::Client Rest Client} and configures the {Ably::Auth} object for the connection.
@@ -61,6 +61,8 @@ module Ably
61
61
  # client = Ably::Rest::Client.new(api_key: 'key.id:secret', client_id: 'john')
62
62
  #
63
63
  def initialize(options, &auth_block)
64
+ options = options.dup
65
+
64
66
  if options.kind_of?(String)
65
67
  options = { api_key: options }
66
68
  end
@@ -69,16 +71,16 @@ module Ably
69
71
  @environment = options.delete(:environment) # nil is production
70
72
  @debug_http = options.delete(:debug_http)
71
73
 
72
- @auth = Auth.new(self, options, &auth_block)
74
+ @auth = Auth.new(self, options, &auth_block)
75
+ @channels = Ably::Rest::Channels.new(self)
73
76
  end
74
77
 
75
78
  # Return a REST {Ably::Rest::Channel} for the given name
76
79
  #
77
- # @param name [String] The name of the channel
78
- # @return [Ably::Rest::Channel]
79
- def channel(name)
80
- @channels ||= {}
81
- @channels[name] ||= Ably::Rest::Channel.new(self, name)
80
+ # @param [String] name see {Ably::Rest::Channels#get}
81
+ # @param [Hash] channel_options see {Ably::Rest::Channels#get}
82
+ def channel(name, channel_options = {})
83
+ channels.get(name, channel_options)
82
84
  end
83
85
 
84
86
  # Return the stats for the application
@@ -137,9 +139,26 @@ module Ably
137
139
 
138
140
  private
139
141
  def request(method, path, params = {}, options = {})
140
- connection.send(method, path, params) do |request|
141
- unless options[:send_auth_header] == false
142
- request.headers[:authorization] = auth.auth_header
142
+ reauthorise_on_authorisation_failure do
143
+ connection.send(method, path, params) do |request|
144
+ unless options[:send_auth_header] == false
145
+ request.headers[:authorization] = auth.auth_header
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ def reauthorise_on_authorisation_failure
152
+ attempts = 0
153
+ begin
154
+ yield
155
+ rescue Ably::Exceptions::InvalidRequest => e
156
+ attempts += 1
157
+ if attempts == 1 && e.code == 40140 && auth.token_renewable?
158
+ auth.authorise force: true
159
+ retry
160
+ else
161
+ raise Ably::Exceptions::InvalidToken.new(e.message, status: e.status, code: e.code)
143
162
  end
144
163
  end
145
164
  end
@@ -29,9 +29,9 @@ module Ably
29
29
  message = "Unknown server error" if message.to_s.strip == ''
30
30
 
31
31
  if env[:status] >= 500
32
- raise Ably::ServerError, message
32
+ raise Ably::Exceptions::ServerError, message
33
33
  else
34
- raise Ably::InvalidRequest.new(message, status: error_status_code, code: error_code)
34
+ raise Ably::Exceptions::InvalidRequest.new(message, status: error_status_code, code: error_code)
35
35
  end
36
36
  end
37
37
  end
@@ -13,9 +13,9 @@ module Ably
13
13
  message = "Error #{error_status_code}: #{(env[:body] || '')[0...200]}"
14
14
 
15
15
  if error_status_code >= 500
16
- raise Ably::ServerError, message
16
+ raise Ably::Exceptions::ServerError, message
17
17
  else
18
- raise Ably::InvalidRequest, message
18
+ raise Ably::Exceptions::InvalidRequest, message
19
19
  end
20
20
  end
21
21
  end
@@ -7,7 +7,7 @@ module Ably
7
7
  def parse(body)
8
8
  JSON.parse(body, symbolize_names: true)
9
9
  rescue JSON::ParserError => e
10
- raise InvalidResponseBody, "Expected JSON response. #{e.message}"
10
+ raise Ably::Exceptions::InvalidResponseBody, "Expected JSON response. #{e.message}"
11
11
  end
12
12
  end
13
13
  end
@@ -10,27 +10,37 @@ module Ably
10
10
  # @param [Faraday::Response] http_response Initial HTTP response from an Ably request to a paged resource
11
11
  # @param [String] base_url Base URL for request that generated the http_response so that subsequent paged requests can be made
12
12
  # @param [Ably::Rest::Client] client {Ably::Client} used to make the request to Ably
13
+ # @param [Hash] options Options for this paged resource
14
+ # @option options [Symbol] :coerce_into symbol representing class that should be used to represent each item in the PagedResource
13
15
  #
14
16
  # @return [Ably::Rest::PagedResource]
15
- def initialize(http_response, base_url, client)
17
+ def initialize(http_response, base_url, client, coerce_into: nil)
16
18
  @http_response = http_response
17
- @body = http_response.body
18
19
  @client = client
19
20
  @base_url = "#{base_url.gsub(%r{/[^/]*$}, '')}/"
21
+ @coerce_into = coerce_into
22
+
23
+ @body = if coerce_into
24
+ http_response.body.map do |item|
25
+ Kernel.const_get(coerce_into).new(item)
26
+ end
27
+ else
28
+ http_response.body
29
+ end
20
30
  end
21
31
 
22
32
  # Retrieve the first page of results
23
33
  #
24
34
  # @return [Ably::Rest::PagedResource]
25
35
  def first
26
- PagedResource.new(@client.get(pagination_url('first')), @base_url, @client)
36
+ PagedResource.new(@client.get(pagination_url('first')), @base_url, @client, coerce_into: @coerce_into)
27
37
  end
28
38
 
29
39
  # Retrieve the next page of results
30
40
  #
31
41
  # @return [Ably::Rest::PagedResource]
32
42
  def next
33
- PagedResource.new(@client.get(pagination_url('next')), @base_url, @client)
43
+ PagedResource.new(@client.get(pagination_url('next')), @base_url, @client, coerce_into: @coerce_into)
34
44
  end
35
45
 
36
46
  # True if this is the last page in the paged resource set
@@ -94,7 +104,7 @@ module Ably
94
104
  end
95
105
 
96
106
  def pagination_url(id)
97
- raise InvalidPageError, "Paging heading link #{id} does not exist" unless pagination_header(id)
107
+ raise Ably::Exceptions::InvalidPageError, "Paging heading link #{id} does not exist" unless pagination_header(id)
98
108
 
99
109
  if pagination_header(id).match(%r{^\./})
100
110
  "#{@base_url}#{pagination_header(id)[2..-1]}"
@@ -8,7 +8,7 @@ module Ably
8
8
  TOKEN_EXPIRY_BUFFER = 5
9
9
 
10
10
  def initialize(attributes)
11
- @attributes = attributes.freeze
11
+ @attributes = attributes.dup.freeze
12
12
  end
13
13
 
14
14
  def id
@@ -1,3 +1,3 @@
1
1
  module Ably
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
@@ -156,7 +156,7 @@ describe "REST" do
156
156
  end
157
157
 
158
158
  it 'raises ServerError' do
159
- expect { auth.request_token options }.to raise_error(Ably::ServerError)
159
+ expect { auth.request_token options }.to raise_error(Ably::Exceptions::ServerError)
160
160
  end
161
161
  end
162
162
 
@@ -167,7 +167,7 @@ describe "REST" do
167
167
  end
168
168
 
169
169
  it 'raises InvalidResponseBody' do
170
- expect { auth.request_token options }.to raise_error(Ably::InvalidResponseBody)
170
+ expect { auth.request_token options }.to raise_error(Ably::Exceptions::InvalidResponseBody)
171
171
  end
172
172
  end
173
173
  end
@@ -294,11 +294,11 @@ describe "REST" do
294
294
  let(:client) { Ably::Rest::Client.new(auth_url: 'http://example.com') }
295
295
 
296
296
  it "should raise an exception if key secret is missing" do
297
- expect { auth.create_token_request(key_id: 'id') }.to raise_error Ably::TokenRequestError
297
+ expect { auth.create_token_request(key_id: 'id') }.to raise_error Ably::Exceptions::TokenRequestError
298
298
  end
299
299
 
300
300
  it "should raise an exception if key id is missing" do
301
- expect { auth.create_token_request(key_secret: 'secret') }.to raise_error Ably::TokenRequestError
301
+ expect { auth.create_token_request(key_secret: 'secret') }.to raise_error Ably::Exceptions::TokenRequestError
302
302
  end
303
303
  end
304
304
 
@@ -353,7 +353,7 @@ describe "REST" do
353
353
 
354
354
  it "disallows publishing on unspecified capability channels" do
355
355
  expect { token_auth_client.channel("bar").publish("event", "data") }.to raise_error do |error|
356
- expect(error).to be_a(Ably::InvalidRequest)
356
+ expect(error).to be_a(Ably::Exceptions::InvalidRequest)
357
357
  expect(error.status).to eql(401)
358
358
  expect(error.code).to eql(40160)
359
359
  end
@@ -361,7 +361,7 @@ describe "REST" do
361
361
 
362
362
  it "fails if timestamp is invalid" do
363
363
  expect { auth.request_token(timestamp: Time.now.to_i - 180) }.to raise_error do |error|
364
- expect(error).to be_a(Ably::InvalidRequest)
364
+ expect(error).to be_a(Ably::Exceptions::InvalidRequest)
365
365
  expect(error.status).to eql(401)
366
366
  expect(error.code).to eql(40101)
367
367
  end
@@ -417,7 +417,7 @@ describe "REST" do
417
417
  capability_with_str_key = Ably::Token::DEFAULTS[:capability]
418
418
  capability = Hash[capability_with_str_key.keys.map(&:to_sym).zip(capability_with_str_key.values)]
419
419
  expect(token.capability).to eql(capability)
420
- expect(token.expires_at).to be_within(2).of(Time.now + Ably::Token::DEFAULTS[:ttl])
420
+ expect(token.expires_at.to_i).to be_within(2).of(Time.now.to_i + Ably::Token::DEFAULTS[:ttl])
421
421
  expect(token.client_id).to eql(client_id)
422
422
  end
423
423
  end
@@ -7,10 +7,10 @@ describe "REST" do
7
7
  end
8
8
 
9
9
  describe "invalid requests in middleware" do
10
- it "should raise a InvalidRequest exception with a valid message" do
10
+ it "should raise an InvalidRequest exception with a valid message" do
11
11
  invalid_client = Ably::Rest::Client.new(api_key: 'appid.keyuid:keysecret')
12
12
  expect { invalid_client.channel('test').publish('foo', 'choo') }.to raise_error do |error|
13
- expect(error).to be_a(Ably::InvalidRequest)
13
+ expect(error).to be_a(Ably::Exceptions::InvalidRequest)
14
14
  expect(error.message).to match(/invalid credentials/)
15
15
  expect(error.code).to eql(40100)
16
16
  expect(error.status).to eql(401)
@@ -25,7 +25,7 @@ describe "REST" do
25
25
  end
26
26
 
27
27
  it "should raise a ServerError exception" do
28
- expect { client.time }.to raise_error(Ably::ServerError, /Internal error/)
28
+ expect { client.time }.to raise_error(Ably::Exceptions::ServerError, /Internal error/)
29
29
  end
30
30
  end
31
31
 
@@ -35,7 +35,60 @@ describe "REST" do
35
35
  end
36
36
 
37
37
  it "should raise a ServerError exception" do
38
- expect { client.time }.to raise_error(Ably::ServerError, /Unknown/)
38
+ expect { client.time }.to raise_error(Ably::Exceptions::ServerError, /Unknown/)
39
+ end
40
+ end
41
+ end
42
+
43
+ describe 'authentication failure', webmock: true do
44
+ let(:token_1) { { id: SecureRandom.hex } }
45
+ let(:token_2) { { id: SecureRandom.hex } }
46
+ let(:channel) { 'channelname' }
47
+
48
+ before do
49
+ @token_requests = 0
50
+ @publish_attempts = 0
51
+
52
+ stub_request(:post, "#{client.endpoint}/keys/#{key_id}/requestToken").to_return do
53
+ @token_requests += 1
54
+ {
55
+ :body => { access_token: send("token_#{@token_requests}").merge(expires: Time.now.to_i + 3600) }.to_json,
56
+ :headers => { 'Content-Type' => 'application/json' }
57
+ }
58
+ end
59
+
60
+ stub_request(:post, "#{client.endpoint}/channels/#{channel}/publish").to_return do
61
+ @publish_attempts += 1
62
+ if [1, 3].include?(@publish_attempts)
63
+ { status: 201, :body => '[]' }
64
+ else
65
+ raise Ably::Exceptions::InvalidRequest.new('Authentication failure', status: 401, code: 40140)
66
+ end
67
+ end
68
+ end
69
+
70
+ context 'when auth#token_renewable?' do
71
+ before do
72
+ client.auth.authorise
73
+ end
74
+
75
+ it 'should automatically reissue a token' do
76
+ client.channel(channel).publish('evt', 'msg')
77
+ expect(@publish_attempts).to eql(1)
78
+
79
+ client.channel(channel).publish('evt', 'msg')
80
+ expect(@publish_attempts).to eql(3)
81
+ expect(@token_requests).to eql(2)
82
+ end
83
+ end
84
+
85
+ context 'when NOT auth#token_renewable?' do
86
+ let(:client) { Ably::Rest::Client.new(token_id: 'token ID cannot be used to create a new token', environment: environment) }
87
+ it 'should raise the exception' do
88
+ client.channel(channel).publish('evt', 'msg')
89
+ expect(@publish_attempts).to eql(1)
90
+ expect { client.channel(channel).publish('evt', 'msg') }.to raise_error Ably::Exceptions::InvalidToken
91
+ expect(@token_requests).to eql(0)
39
92
  end
40
93
  end
41
94
  end
@@ -38,7 +38,8 @@ describe "REST" do
38
38
  expect(actual_history.size).to eql(3)
39
39
 
40
40
  expected_history.each do |message|
41
- expect(actual_history).to include(message)
41
+ expect(actual_history).to include(Ably::Message.new(message))
42
+ expect(actual_history.map(&:raw_message)).to include(message)
42
43
  end
43
44
  end
44
45
 
@@ -0,0 +1,39 @@
1
+ require "spec_helper"
2
+ require "securerandom"
3
+
4
+ describe Ably::Rest::Channels do
5
+ let(:client) do
6
+ Ably::Rest::Client.new(api_key: api_key, environment: environment)
7
+ end
8
+ let(:channel_name) { SecureRandom.hex }
9
+ let(:options) { { key: 'value' } }
10
+
11
+ shared_examples "a channel" do
12
+ it "should access a channel" do
13
+ expect(channel).to be_a Ably::Rest::Channel
14
+ expect(channel.name).to eql(channel_name)
15
+ end
16
+
17
+ it "should allow options to be set on a channel" do
18
+ expect(channel_with_options.options).to eql(options)
19
+ end
20
+ end
21
+
22
+ describe "using shortcut method on client" do
23
+ let(:channel) { client.channel(channel_name) }
24
+ let(:channel_with_options) { client.channel(channel_name, options) }
25
+ it_behaves_like 'a channel'
26
+ end
27
+
28
+ describe "using documented .get method on client.channels" do
29
+ let(:channel) { client.channels.get(channel_name) }
30
+ let(:channel_with_options) { client.channels.get(channel_name, options) }
31
+ it_behaves_like 'a channel'
32
+ end
33
+
34
+ describe "using undocumented [] method on client.channels" do
35
+ let(:channel) { client.channels[channel_name] }
36
+ let(:channel_with_options) { client.channels[channel_name, options] }
37
+ it_behaves_like 'a channel'
38
+ end
39
+ end
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
  describe Ably::Auth do
4
4
  let(:client) { Ably::Rest::Client.new(key_id: 'id', key_secret: 'secret') }
5
5
 
6
- it "should not allow changes to the options" do
6
+ it "has immutable options" do
7
7
  expect { client.auth.options['key_id'] = 'new_id' }.to raise_error RuntimeError, /can't modify frozen Hash/
8
8
  end
9
9
  end
@@ -0,0 +1,73 @@
1
+ require "spec_helper"
2
+
3
+ describe Ably::Message do
4
+ context 'attributes' do
5
+ let(:unique_value) { 'unique_value' }
6
+
7
+ %w(name data client_id timestamp channel_serial).each do |attribute|
8
+ context "##{attribute}" do
9
+ subject { Ably::Message.new({ attribute.to_sym => unique_value }) }
10
+
11
+ it "retrieves attribute :#{attribute}" do
12
+ expect(subject.public_send(attribute)).to eql(unique_value)
13
+ end
14
+ end
15
+ end
16
+
17
+ context '#timestamp_at' do
18
+ subject { Ably::Message.new(timestamp: Time.now.to_i * 1000) }
19
+ it 'retrieves attribute :key' do
20
+ expect(subject.timestamp_at.to_i).to be_within(1).of(Time.now.to_i)
21
+ end
22
+ end
23
+
24
+ context '#raw_message' do
25
+ let(:attributes) { { timestamp: Time.now.to_i * 1000 } }
26
+ subject { Ably::Message.new(attributes) }
27
+
28
+ it 'provides access to #raw_message' do
29
+ expect(subject.raw_message).to eql(attributes)
30
+ end
31
+ end
32
+
33
+ context '#[]' do
34
+ subject { Ably::Message.new(unusual: 'attribute') }
35
+
36
+ it 'provides accessor method to #raw_message' do
37
+ expect(subject[:unusual]).to eql('attribute')
38
+ end
39
+ end
40
+ end
41
+
42
+ context '==' do
43
+ let(:attributes) { { client_id: 'unique' } }
44
+
45
+ it 'is true when attributes are the same' do
46
+ new_message = -> { Ably::Message.new(attributes) }
47
+ expect(new_message[]).to eq(new_message[])
48
+ end
49
+
50
+ it 'is false when attributes are not the same' do
51
+ expect(Ably::Message.new(client_id: 1)).to_not eq(Ably::Message.new(client_id: 2))
52
+ end
53
+
54
+ it 'is false when class type differs' do
55
+ expect(Ably::Message.new(client_id: 1)).to_not eq(nil)
56
+ end
57
+ end
58
+
59
+ context 'is immutable' do
60
+ let(:options) { { client_id: 'John' } }
61
+ subject { Ably::Message.new(options) }
62
+
63
+ it 'prevents changes' do
64
+ expect { subject.raw_message[:client_id] = 'Joe' }.to raise_error RuntimeError, /can't modify frozen Hash/
65
+ end
66
+
67
+ it 'dups options' do
68
+ expect(subject.raw_message[:client_id]).to eql('John')
69
+ options[:client_id] = 'Joe'
70
+ expect(subject.raw_message[:client_id]).to eql('John')
71
+ end
72
+ end
73
+ end
@@ -75,7 +75,7 @@ describe Ably::Rest do
75
75
  end
76
76
 
77
77
  it 'fails when authenticating with basic auth' do
78
- expect { client.channel('a').publish('event', 'message') }.to raise_error(Ably::InsecureRequestError)
78
+ expect { client.channel('a').publish('event', 'message') }.to raise_error(Ably::Exceptions::InsecureRequestError)
79
79
  end
80
80
  end
81
81
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ably
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lewis Marshall
@@ -168,6 +168,7 @@ files:
168
168
  - lib/ably.rb
169
169
  - lib/ably/auth.rb
170
170
  - lib/ably/exceptions.rb
171
+ - lib/ably/message.rb
171
172
  - lib/ably/realtime.rb
172
173
  - lib/ably/realtime/callbacks.rb
173
174
  - lib/ably/realtime/channel.rb
@@ -175,6 +176,7 @@ files:
175
176
  - lib/ably/realtime/connection.rb
176
177
  - lib/ably/rest.rb
177
178
  - lib/ably/rest/channel.rb
179
+ - lib/ably/rest/channels.rb
178
180
  - lib/ably/rest/client.rb
179
181
  - lib/ably/rest/middleware/exceptions.rb
180
182
  - lib/ably/rest/middleware/external_exceptions.rb
@@ -188,6 +190,7 @@ files:
188
190
  - spec/acceptance/rest/auth_spec.rb
189
191
  - spec/acceptance/rest/base_spec.rb
190
192
  - spec/acceptance/rest/channel_spec.rb
193
+ - spec/acceptance/rest/channels_spec.rb
191
194
  - spec/acceptance/rest/presence_spec.rb
192
195
  - spec/acceptance/rest/stats_spec.rb
193
196
  - spec/acceptance/rest/time_spec.rb
@@ -195,6 +198,7 @@ files:
195
198
  - spec/support/api_helper.rb
196
199
  - spec/support/test_app.rb
197
200
  - spec/unit/auth.rb
201
+ - spec/unit/message_spec.rb
198
202
  - spec/unit/realtime_spec.rb
199
203
  - spec/unit/rest_spec.rb
200
204
  - spec/unit/token_spec.rb
@@ -227,6 +231,7 @@ test_files:
227
231
  - spec/acceptance/rest/auth_spec.rb
228
232
  - spec/acceptance/rest/base_spec.rb
229
233
  - spec/acceptance/rest/channel_spec.rb
234
+ - spec/acceptance/rest/channels_spec.rb
230
235
  - spec/acceptance/rest/presence_spec.rb
231
236
  - spec/acceptance/rest/stats_spec.rb
232
237
  - spec/acceptance/rest/time_spec.rb
@@ -234,6 +239,7 @@ test_files:
234
239
  - spec/support/api_helper.rb
235
240
  - spec/support/test_app.rb
236
241
  - spec/unit/auth.rb
242
+ - spec/unit/message_spec.rb
237
243
  - spec/unit/realtime_spec.rb
238
244
  - spec/unit/rest_spec.rb
239
245
  - spec/unit/token_spec.rb