ably 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|