ably 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +103 -0
- data/Rakefile +4 -0
- data/ably.gemspec +32 -0
- data/lib/ably.rb +11 -0
- data/lib/ably/auth.rb +381 -0
- data/lib/ably/exceptions.rb +16 -0
- data/lib/ably/realtime.rb +38 -0
- data/lib/ably/realtime/callbacks.rb +15 -0
- data/lib/ably/realtime/channel.rb +51 -0
- data/lib/ably/realtime/client.rb +82 -0
- data/lib/ably/realtime/connection.rb +61 -0
- data/lib/ably/rest.rb +15 -0
- data/lib/ably/rest/channel.rb +58 -0
- data/lib/ably/rest/client.rb +194 -0
- data/lib/ably/rest/middleware/exceptions.rb +42 -0
- data/lib/ably/rest/middleware/external_exceptions.rb +26 -0
- data/lib/ably/rest/middleware/parse_json.rb +15 -0
- data/lib/ably/rest/paged_resource.rb +107 -0
- data/lib/ably/rest/presence.rb +44 -0
- data/lib/ably/support.rb +14 -0
- data/lib/ably/token.rb +55 -0
- data/lib/ably/version.rb +3 -0
- data/spec/acceptance/realtime_client_spec.rb +12 -0
- data/spec/acceptance/rest/auth_spec.rb +441 -0
- data/spec/acceptance/rest/base_spec.rb +113 -0
- data/spec/acceptance/rest/channel_spec.rb +68 -0
- data/spec/acceptance/rest/presence_spec.rb +22 -0
- data/spec/acceptance/rest/stats_spec.rb +57 -0
- data/spec/acceptance/rest/time_spec.rb +14 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/support/api_helper.rb +41 -0
- data/spec/support/test_app.rb +77 -0
- data/spec/unit/auth.rb +9 -0
- data/spec/unit/realtime_spec.rb +9 -0
- data/spec/unit/rest_spec.rb +99 -0
- data/spec/unit/token_spec.rb +90 -0
- metadata +240 -0
@@ -0,0 +1,113 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "securerandom"
|
3
|
+
|
4
|
+
describe "REST" do
|
5
|
+
let(:client) do
|
6
|
+
Ably::Rest::Client.new(api_key: api_key, environment: environment)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "invalid requests in middleware" do
|
10
|
+
it "should raise a InvalidRequest exception with a valid message" do
|
11
|
+
invalid_client = Ably::Rest::Client.new(api_key: 'appid.keyuid:keysecret')
|
12
|
+
expect { invalid_client.channel('test').publish('foo', 'choo') }.to raise_error do |error|
|
13
|
+
expect(error).to be_a(Ably::InvalidRequest)
|
14
|
+
expect(error.message).to match(/invalid credentials/)
|
15
|
+
expect(error.code).to eql(40100)
|
16
|
+
expect(error.status).to eql(401)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "server error with JSON response", webmock: true do
|
21
|
+
let(:error_response) { '{ "error": { "statusCode": 500, "code": 50000, "message": "Internal error" } }' }
|
22
|
+
|
23
|
+
before do
|
24
|
+
stub_request(:get, "#{client.endpoint}/time").to_return(:status => 500, :body => error_response, :headers => { 'Content-Type' => 'application/json' })
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should raise a ServerError exception" do
|
28
|
+
expect { client.time }.to raise_error(Ably::ServerError, /Internal error/)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "server error", webmock: true do
|
33
|
+
before do
|
34
|
+
stub_request(:get, "#{client.endpoint}/time").to_return(:status => 500)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should raise a ServerError exception" do
|
38
|
+
expect { client.time }.to raise_error(Ably::ServerError, /Unknown/)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe Ably::Rest::Client do
|
44
|
+
context '#initialize' do
|
45
|
+
context 'with an auth block' do
|
46
|
+
let(:client) { Ably::Rest::Client.new(environment: environment) { token_request } }
|
47
|
+
let(:token_request) { client.auth.create_token_request(key_id: key_id, key_secret: key_secret, client_id: client_id) }
|
48
|
+
let(:client_id) { 'unique_client_id' }
|
49
|
+
|
50
|
+
it 'calls the block to get a new token' do
|
51
|
+
expect { client.channel('channel_name').publish('event', 'message') }.to change { client.auth.current_token }
|
52
|
+
expect(client.auth.current_token.client_id).to eql(client_id)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'with an auth URL' do
|
57
|
+
let(:client) { Ably::Rest::Client.new(environment: environment, auth_url: token_request_url, auth_method: :get) }
|
58
|
+
let(:token_request_url) { 'http://get.token.request.com/' }
|
59
|
+
let(:token_request) { client.auth.create_token_request(key_id: key_id, key_secret: key_secret, client_id: client_id) }
|
60
|
+
let(:client_id) { 'unique_client_id' }
|
61
|
+
|
62
|
+
before do
|
63
|
+
allow(client.auth).to receive(:token_request_from_auth_url).with(token_request_url, :auth_method => :get).and_return(token_request)
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'sends an HTTP request to get a new token' do
|
67
|
+
expect { client.channel('channel_name').publish('event', 'message') }.to change { client.auth.current_token }
|
68
|
+
expect(client.auth.current_token.client_id).to eql(client_id)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'token expiry' do
|
74
|
+
let(:client) do
|
75
|
+
Ably::Rest::Client.new(environment: environment) do
|
76
|
+
@request_index ||= 0
|
77
|
+
@request_index += 1
|
78
|
+
send("token_request_#{@request_index}")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
let(:token_request_1) { client.auth.create_token_request(token_request_options.merge(client_id: SecureRandom.hex)) }
|
82
|
+
let(:token_request_2) { client.auth.create_token_request(token_request_options.merge(client_id: SecureRandom.hex)) }
|
83
|
+
|
84
|
+
context 'when expired' do
|
85
|
+
let(:token_request_options) { { key_id: key_id, key_secret: key_secret, ttl: Ably::Token::TOKEN_EXPIRY_BUFFER } }
|
86
|
+
|
87
|
+
it 'creates a new token automatically when the old token expires' do
|
88
|
+
expect { client.channel('channel_name').publish('event', 'message') }.to change { client.auth.current_token }
|
89
|
+
expect(client.auth.current_token.client_id).to eql(token_request_1[:client_id])
|
90
|
+
|
91
|
+
sleep 1
|
92
|
+
|
93
|
+
expect { client.channel('channel_name').publish('event', 'message') }.to change { client.auth.current_token }
|
94
|
+
expect(client.auth.current_token.client_id).to eql(token_request_2[:client_id])
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context 'token authentication with long expiry token' do
|
99
|
+
let(:token_request_options) { { key_id: key_id, key_secret: key_secret, ttl: 3600 } }
|
100
|
+
|
101
|
+
it 'creates a new token automatically when the old token expires' do
|
102
|
+
expect { client.channel('channel_name').publish('event', 'message') }.to change { client.auth.current_token }
|
103
|
+
expect(client.auth.current_token.client_id).to eql(token_request_1[:client_id])
|
104
|
+
|
105
|
+
sleep 1
|
106
|
+
|
107
|
+
expect { client.channel('channel_name').publish('event', 'message') }.to_not change { client.auth.current_token }
|
108
|
+
expect(client.auth.current_token.client_id).to eql(token_request_1[:client_id])
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "securerandom"
|
3
|
+
|
4
|
+
describe "REST" do
|
5
|
+
let(:client) do
|
6
|
+
Ably::Rest::Client.new(api_key: api_key, environment: environment)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "publishing messages" do
|
10
|
+
let(:channel) { client.channel("test") }
|
11
|
+
let(:event) { "foo" }
|
12
|
+
let(:message) { "woop!" }
|
13
|
+
|
14
|
+
it "should publish the message ok" do
|
15
|
+
expect(channel.publish(event, message)).to eql(true)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "fetching channel history" do
|
20
|
+
let(:channel) { client.channel("persisted:#{SecureRandom.hex(4)}") }
|
21
|
+
let(:expected_history) do
|
22
|
+
[
|
23
|
+
{ :name => "test1", :data => "foo" },
|
24
|
+
{ :name => "test2", :data => "bar" },
|
25
|
+
{ :name => "test3", :data => "baz" }
|
26
|
+
]
|
27
|
+
end
|
28
|
+
|
29
|
+
before(:each) do
|
30
|
+
expected_history.each do |message|
|
31
|
+
channel.publish(message[:name], message[:data]) || raise("Unable to publish message")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should return all the history for the channel" do
|
36
|
+
actual_history = channel.history
|
37
|
+
|
38
|
+
expect(actual_history.size).to eql(3)
|
39
|
+
|
40
|
+
expected_history.each do |message|
|
41
|
+
expect(actual_history).to include(message)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should return paged history" do
|
46
|
+
page_1 = channel.history(limit: 1)
|
47
|
+
page_2 = page_1.next
|
48
|
+
page_3 = page_2.next
|
49
|
+
|
50
|
+
all_items = [page_1[0], page_2[0], page_3[0]]
|
51
|
+
expect(all_items.uniq).to eql(all_items)
|
52
|
+
|
53
|
+
expect(page_1.size).to eql(1)
|
54
|
+
expect(page_1).to_not be_last
|
55
|
+
expect(page_1).to be_first
|
56
|
+
|
57
|
+
# Page 2
|
58
|
+
expect(page_2.size).to eql(1)
|
59
|
+
expect(page_2).to_not be_last
|
60
|
+
expect(page_2).to_not be_first
|
61
|
+
|
62
|
+
# Page 3
|
63
|
+
expect(page_3.size).to eql(1)
|
64
|
+
expect(page_3).to be_last
|
65
|
+
expect(page_3).to_not be_first
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "securerandom"
|
3
|
+
|
4
|
+
describe "REST" do
|
5
|
+
let(:client) do
|
6
|
+
Ably::Rest::Client.new(api_key: api_key, environment: environment)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "fetching presence" do
|
10
|
+
let(:channel) { client.channel("persisted:presence_fixtures") }
|
11
|
+
let(:presence) { channel.presence.get }
|
12
|
+
|
13
|
+
it "should return current members on the channel" do
|
14
|
+
expect(presence.size).to eql(4)
|
15
|
+
|
16
|
+
TestApp::APP_SPEC['channels'].first['presence'].each do |presence_hash|
|
17
|
+
presence_match = presence.find { |client| client['clientId'] == presence_hash['clientId'] }
|
18
|
+
expect(presence_match['clientData']).to eql(presence_hash['clientData'])
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "securerandom"
|
3
|
+
|
4
|
+
describe "REST" do
|
5
|
+
let(:client) do
|
6
|
+
Ably::Rest::Client.new(api_key: api_key, environment: environment)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "fetching application stats" do
|
10
|
+
def number_of_channels() 3 end
|
11
|
+
def number_of_messages_per_channel() 5 end
|
12
|
+
|
13
|
+
before(:context) do
|
14
|
+
@context_client = Ably::Rest::Client.new(api_key: api_key, environment: environment)
|
15
|
+
|
16
|
+
# Wait until the start of the next minute according to the service
|
17
|
+
# time because stats are created in 1 minute intervals
|
18
|
+
service_time = @context_client.time
|
19
|
+
@interval_start = (service_time.to_i / 60 + 1) * 60
|
20
|
+
sleep_time = @interval_start - Time.now.to_i
|
21
|
+
|
22
|
+
if sleep_time > 30
|
23
|
+
@interval_start -= 60 # there is enough time to generate the stats in this minute interval
|
24
|
+
elsif sleep_time > 0
|
25
|
+
sleep sleep_time
|
26
|
+
end
|
27
|
+
|
28
|
+
number_of_channels.times do |i|
|
29
|
+
channel = @context_client.channel("stats-#{i}")
|
30
|
+
|
31
|
+
number_of_messages_per_channel.times do |j|
|
32
|
+
channel.publish("event-#{j}", "data-#{j}") || raise("Unable to publish message")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
sleep(10)
|
37
|
+
end
|
38
|
+
|
39
|
+
[:minute, :hour, :day, :month].each do |interval|
|
40
|
+
context "by #{interval}" do
|
41
|
+
it "should return all the stats for the application" do
|
42
|
+
stats = @context_client.stats(start: @interval_start * 1000, by: interval.to_s, direction: 'forwards')
|
43
|
+
|
44
|
+
expect(stats.size).to eql(1)
|
45
|
+
|
46
|
+
stat = stats.first
|
47
|
+
|
48
|
+
expect(stat[:inbound][:all][:all][:count]).to eql(number_of_channels * number_of_messages_per_channel)
|
49
|
+
expect(stat[:inbound][:rest][:all][:count]).to eql(number_of_channels * number_of_messages_per_channel)
|
50
|
+
|
51
|
+
# TODO: Review number of Channels opened issue for intervals other than minute
|
52
|
+
expect(stat[:channels][:opened]).to eql(number_of_channels) if interval == :minute
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "securerandom"
|
3
|
+
|
4
|
+
describe "REST" do
|
5
|
+
let(:client) do
|
6
|
+
Ably::Rest::Client.new(api_key: api_key, environment: environment)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "fetching the service time" do
|
10
|
+
it "should return the service time as a Time object" do
|
11
|
+
expect(client.time).to be_within(2).of(Time.now)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
4
|
+
# loaded once.
|
5
|
+
#
|
6
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
7
|
+
|
8
|
+
require 'webmock/rspec'
|
9
|
+
|
10
|
+
require "ably"
|
11
|
+
|
12
|
+
require "support/api_helper"
|
13
|
+
|
14
|
+
RSpec.configure do |config|
|
15
|
+
config.run_all_when_everything_filtered = true
|
16
|
+
config.filter_run :focus
|
17
|
+
|
18
|
+
# Run specs in random order to surface order dependencies. If you find an
|
19
|
+
# order dependency and want to debug it, you can fix the order by providing
|
20
|
+
# the seed, which is printed after each run.
|
21
|
+
# --seed 1234
|
22
|
+
config.order = 'random'
|
23
|
+
|
24
|
+
config.before(:example) do
|
25
|
+
WebMock.disable!
|
26
|
+
end
|
27
|
+
|
28
|
+
config.before(:example, :webmock => true) do
|
29
|
+
WebMock.enable!
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "support/test_app"
|
2
|
+
|
3
|
+
module ApiHelper
|
4
|
+
def app_id
|
5
|
+
TestApp.instance.app_id
|
6
|
+
end
|
7
|
+
|
8
|
+
def key_id
|
9
|
+
TestApp.instance.key_id
|
10
|
+
end
|
11
|
+
|
12
|
+
def key_secret
|
13
|
+
api_key.split(':')[1]
|
14
|
+
end
|
15
|
+
|
16
|
+
def api_key
|
17
|
+
TestApp.instance.api_key
|
18
|
+
end
|
19
|
+
|
20
|
+
def environment
|
21
|
+
TestApp.instance.environment
|
22
|
+
end
|
23
|
+
|
24
|
+
def encode64(text)
|
25
|
+
Base64.encode64(text).gsub("\n", '')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
RSpec.configure do |config|
|
30
|
+
config.include ApiHelper
|
31
|
+
|
32
|
+
config.before(:suite) do
|
33
|
+
WebMock.disable!
|
34
|
+
TestApp.instance
|
35
|
+
end
|
36
|
+
|
37
|
+
config.after(:suite) do
|
38
|
+
WebMock.disable!
|
39
|
+
TestApp.instance.delete
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require "singleton"
|
2
|
+
|
3
|
+
class TestApp
|
4
|
+
APP_SPEC = {
|
5
|
+
'keys' => [
|
6
|
+
{}
|
7
|
+
],
|
8
|
+
'namespaces' => [
|
9
|
+
{ 'id' => 'persisted', 'persisted' => true }
|
10
|
+
]
|
11
|
+
# ],
|
12
|
+
# 'channels' => [
|
13
|
+
# {
|
14
|
+
# 'name' => 'persisted:presence_fixtures',
|
15
|
+
# 'presence' => [
|
16
|
+
# { 'clientId' => 'client_bool', 'clientData' => true },
|
17
|
+
# { 'clientId' => 'client_int', 'clientData' => 24 },
|
18
|
+
# { 'clientId' => 'client_string', 'clientData' => 'This is a string clientData payload' },
|
19
|
+
# { 'clientId' => 'client_json', 'clientData' => { "test" => 'This is a JSONObject clientData payload'} }
|
20
|
+
# ]
|
21
|
+
# }
|
22
|
+
# ]
|
23
|
+
}.to_json
|
24
|
+
|
25
|
+
include Singleton
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
url = "#{sandbox_client.endpoint}/apps"
|
29
|
+
|
30
|
+
headers = {
|
31
|
+
"Accept" => "application/json",
|
32
|
+
"Content-Type" => "application/json"
|
33
|
+
}
|
34
|
+
|
35
|
+
response = Faraday.post(url, APP_SPEC, headers)
|
36
|
+
|
37
|
+
@attributes = JSON.parse(response.body)
|
38
|
+
end
|
39
|
+
|
40
|
+
def app_id
|
41
|
+
@attributes["id"]
|
42
|
+
end
|
43
|
+
|
44
|
+
def key
|
45
|
+
@attributes["keys"].first
|
46
|
+
end
|
47
|
+
|
48
|
+
def key_id
|
49
|
+
"#{app_id}.#{key["id"]}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def key_value
|
53
|
+
key["value"]
|
54
|
+
end
|
55
|
+
|
56
|
+
def api_key
|
57
|
+
"#{key_id}:#{key_value}"
|
58
|
+
end
|
59
|
+
|
60
|
+
def delete
|
61
|
+
url = "#{sandbox_client.endpoint}/apps/#{app_id}"
|
62
|
+
|
63
|
+
basic_auth = Base64.encode64(api_key).chomp
|
64
|
+
headers = { "Authorization" => "Basic #{basic_auth}" }
|
65
|
+
|
66
|
+
Faraday.delete(url, nil, headers)
|
67
|
+
end
|
68
|
+
|
69
|
+
def environment
|
70
|
+
'sandbox'
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
def sandbox_client
|
75
|
+
@sandbox_client ||= Ably::Rest::Client.new(api_key: 'app.key:secret', tls: true, environment: environment)
|
76
|
+
end
|
77
|
+
end
|
data/spec/unit/auth.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Ably::Auth do
|
4
|
+
let(:client) { Ably::Rest::Client.new(key_id: 'id', key_secret: 'secret') }
|
5
|
+
|
6
|
+
it "should not allow changes to the options" do
|
7
|
+
expect { client.auth.options['key_id'] = 'new_id' }.to raise_error RuntimeError, /can't modify frozen Hash/
|
8
|
+
end
|
9
|
+
end
|