ably 0.1.0 → 0.1.1

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 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