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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +103 -0
  6. data/Rakefile +4 -0
  7. data/ably.gemspec +32 -0
  8. data/lib/ably.rb +11 -0
  9. data/lib/ably/auth.rb +381 -0
  10. data/lib/ably/exceptions.rb +16 -0
  11. data/lib/ably/realtime.rb +38 -0
  12. data/lib/ably/realtime/callbacks.rb +15 -0
  13. data/lib/ably/realtime/channel.rb +51 -0
  14. data/lib/ably/realtime/client.rb +82 -0
  15. data/lib/ably/realtime/connection.rb +61 -0
  16. data/lib/ably/rest.rb +15 -0
  17. data/lib/ably/rest/channel.rb +58 -0
  18. data/lib/ably/rest/client.rb +194 -0
  19. data/lib/ably/rest/middleware/exceptions.rb +42 -0
  20. data/lib/ably/rest/middleware/external_exceptions.rb +26 -0
  21. data/lib/ably/rest/middleware/parse_json.rb +15 -0
  22. data/lib/ably/rest/paged_resource.rb +107 -0
  23. data/lib/ably/rest/presence.rb +44 -0
  24. data/lib/ably/support.rb +14 -0
  25. data/lib/ably/token.rb +55 -0
  26. data/lib/ably/version.rb +3 -0
  27. data/spec/acceptance/realtime_client_spec.rb +12 -0
  28. data/spec/acceptance/rest/auth_spec.rb +441 -0
  29. data/spec/acceptance/rest/base_spec.rb +113 -0
  30. data/spec/acceptance/rest/channel_spec.rb +68 -0
  31. data/spec/acceptance/rest/presence_spec.rb +22 -0
  32. data/spec/acceptance/rest/stats_spec.rb +57 -0
  33. data/spec/acceptance/rest/time_spec.rb +14 -0
  34. data/spec/spec_helper.rb +31 -0
  35. data/spec/support/api_helper.rb +41 -0
  36. data/spec/support/test_app.rb +77 -0
  37. data/spec/unit/auth.rb +9 -0
  38. data/spec/unit/realtime_spec.rb +9 -0
  39. data/spec/unit/rest_spec.rb +99 -0
  40. data/spec/unit/token_spec.rb +90 -0
  41. 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
@@ -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
@@ -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
@@ -0,0 +1,9 @@
1
+ require "spec_helper"
2
+
3
+ describe Ably::Realtime do
4
+ let(:options) { { api_key: 'app.key:secret' } }
5
+
6
+ specify 'constructor returns an Ably::Realtime::Client' do
7
+ expect(Ably::Realtime.new(options)).to be_instance_of(Ably::Realtime::Client)
8
+ end
9
+ end