ably 0.1.0

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