routemaster-client 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ .bundle/
2
+ tmp
data/.gitmodules ADDED
@@ -0,0 +1,3 @@
1
+ [submodule "rebund"]
2
+ path = rebund
3
+ url = https://github.com/mezis/rebund.git
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ -I.
2
+ --color
3
+ --format progress
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 1.9.3-p545
data/.travis.yml ADDED
@@ -0,0 +1,16 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.0
6
+ - 2.1.1
7
+ script:
8
+ - bundle exec rspec
9
+ install:
10
+ - "./rebund/run download"
11
+ - bundle install --path vendor/bundle
12
+ after_script:
13
+ - "./rebund/run upload"
14
+ env:
15
+ global:
16
+ secure: "kMxJcT8xyQ+QyCeKW5lDP44IU99Y8iN1AWZo9Z1BKN6HigGa8Ua8O/aD8AJjrTfRRSjnM402utmW6mk78RYVxJha3+69jJYr25I9EeyYhV2cuRu+5Ictxfcb9R9VhS0b6LDncz9h55xDS1UA35rvj+EOO4/M9moccw7T2qzPa2U="
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source ENV.fetch('GEM_SOURCE', 'https://rubygems.org')
2
+
3
+ # Specify your gem's dependencies in routemaster_client.gemspec
4
+ gemspec
5
+
6
+ # just here to avoid a safety warning
7
+ gem 'psych'
data/Gemfile.lock ADDED
@@ -0,0 +1,93 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ routemaster-client (0.0.1)
5
+ faraday
6
+ net-http-persistent
7
+ sinatra
8
+
9
+ GEM
10
+ remote: http://eu.yarp.io/
11
+ specs:
12
+ addressable (2.3.6)
13
+ celluloid (0.15.2)
14
+ timers (~> 1.1.0)
15
+ coderay (1.1.0)
16
+ crack (0.4.2)
17
+ safe_yaml (~> 1.0.0)
18
+ diff-lcs (1.2.5)
19
+ faraday (0.9.0)
20
+ multipart-post (>= 1.2, < 3)
21
+ ffi (1.9.3)
22
+ formatador (0.2.5)
23
+ guard (2.6.1)
24
+ formatador (>= 0.2.4)
25
+ listen (~> 2.7)
26
+ lumberjack (~> 1.0)
27
+ pry (>= 0.9.12)
28
+ thor (>= 0.18.1)
29
+ guard-rspec (4.2.10)
30
+ guard (~> 2.1)
31
+ rspec (>= 2.14, < 4.0)
32
+ listen (2.7.8)
33
+ celluloid (>= 0.15.2)
34
+ rb-fsevent (>= 0.9.3)
35
+ rb-inotify (>= 0.9)
36
+ lumberjack (1.0.6)
37
+ method_source (0.8.2)
38
+ multipart-post (2.0.0)
39
+ net-http-persistent (2.9.4)
40
+ pry (0.9.12.6)
41
+ coderay (~> 1.0)
42
+ method_source (~> 0.8)
43
+ slop (~> 3.4)
44
+ pry-nav (0.2.3)
45
+ pry (~> 0.9.10)
46
+ psych (2.0.5)
47
+ rack (1.5.2)
48
+ rack-protection (1.5.3)
49
+ rack
50
+ rack-test (0.6.2)
51
+ rack (>= 1.0)
52
+ rake (10.3.2)
53
+ rb-fsevent (0.9.4)
54
+ rb-inotify (0.9.5)
55
+ ffi (>= 0.5.0)
56
+ rspec (3.0.0)
57
+ rspec-core (~> 3.0.0)
58
+ rspec-expectations (~> 3.0.0)
59
+ rspec-mocks (~> 3.0.0)
60
+ rspec-core (3.0.1)
61
+ rspec-support (~> 3.0.0)
62
+ rspec-expectations (3.0.1)
63
+ diff-lcs (>= 1.2.0, < 2.0)
64
+ rspec-support (~> 3.0.0)
65
+ rspec-mocks (3.0.1)
66
+ rspec-support (~> 3.0.0)
67
+ rspec-support (3.0.0)
68
+ safe_yaml (1.0.3)
69
+ sinatra (1.4.5)
70
+ rack (~> 1.4)
71
+ rack-protection (~> 1.4)
72
+ tilt (~> 1.3, >= 1.3.4)
73
+ slop (3.5.0)
74
+ thor (0.19.1)
75
+ tilt (1.4.1)
76
+ timers (1.1.0)
77
+ webmock (1.18.0)
78
+ addressable (>= 2.3.6)
79
+ crack (>= 0.3.2)
80
+
81
+ PLATFORMS
82
+ ruby
83
+
84
+ DEPENDENCIES
85
+ bundler (~> 1.5)
86
+ guard-rspec
87
+ pry-nav
88
+ psych
89
+ rack-test
90
+ rake
91
+ routemaster-client!
92
+ rspec
93
+ webmock
data/Guardfile ADDED
@@ -0,0 +1,10 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :rspec, cmd: 'bundle exec rspec' do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^routemaster/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
9
+ end
10
+
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 HouseTrip Ltd.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # routemaster_client
2
+
3
+ A Ruby API for the [Routemaster](https://github.com/HouseTrip/routemaster) event
4
+ bus.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'routemaster-client'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install routemaster-client
19
+
20
+ ## Usage
21
+
22
+ **Configure** your client:
23
+
24
+ ```ruby
25
+ require 'routemaster/client'
26
+ client = RoutemasterClient.new(url: 'https://bus.example.com', uuid: 'john-doe')
27
+ ```
28
+
29
+ You can also specify a timeout value in seconds if you like with the ```timeout``` option.
30
+
31
+ ```ruby
32
+ RoutemasterClient.new(url: 'https://bus.example.com', uuid: 'john-doe', timeout: 2)
33
+ ```
34
+
35
+
36
+ **Push** an event about an entity in the topic `widgets` with a callback URL:
37
+
38
+ ```ruby
39
+ client.created('widgets', 'https://app.example.com/widgets/1')
40
+ client.updated('widgets', 'https://app.example.com/widgets/2')
41
+ client.noop('widgets', 'https://app.example.com/widgets/3')
42
+ ```
43
+
44
+ There are methods for the four canonical event types: `created`, `updated`,
45
+ `deleted`, and `noop`.
46
+
47
+ `noop` is typically used when a subscriber is first connected (or reset), and
48
+ the publisher floods with `noop`s for all existing entities so subscribers can
49
+ refresh their view of the domain.
50
+
51
+
52
+ **Register** to be notified about `widgets` and `kitten` at most 60 seconds after
53
+ events, in batches of at most 500 events, to a given callback URL:
54
+
55
+ ```ruby
56
+ client.subscribe(
57
+ topics: ['widgets', 'kitten'],
58
+ callback: 'https://app.example.com/events',
59
+ uuid: 'john-doe',
60
+ timeout: 60_000,
61
+ max: 500)
62
+ ```
63
+
64
+
65
+ **Receive** events at path `/events` using a Rack middleware:
66
+
67
+ ```ruby
68
+ require 'routemaster/receiver'
69
+
70
+ class Handler
71
+ def on_events(batch)
72
+ batch.each do |event|
73
+ puts event['url']
74
+ end
75
+ end
76
+ end
77
+
78
+ use Routemaster::Receiver, {
79
+ path: '/events',
80
+ uuid: 'demo',
81
+ handler: Handler.new
82
+ }
83
+ ```
84
+
85
+
86
+ **Monitor** the status of topics and subscriptions:
87
+
88
+ ```ruby
89
+ client.monitor_topics
90
+ #=> [ { name: 'widgets', publisher: 'john-doe', events: 12589 }, ...]
91
+
92
+ client.monitor_subscriptions
93
+ #=> [ {
94
+ # subscriber: 'bob',
95
+ # callback: 'https://app.example.com/events',
96
+ # topics: ['widgets', 'kitten'],
97
+ # events: { sent: 21_450, queued: 498, oldest: 59_603 }
98
+ # } ... ]
99
+ ```
100
+
101
+ ## Contributing
102
+
103
+ 1. Fork it ( http://github.com/<my-github-username>/routemaster_client/fork )
104
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
105
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
106
+ 4. Push to the branch (`git push origin my-new-feature`)
107
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,13 @@
1
+ # Adapted from
2
+ # activesupport/lib/active_support/core_ext/kernel/reporting.rb, line 44
3
+ class IO
4
+ def silence_stream(&block)
5
+ old_stream = dup
6
+ self.reopen('/dev/null')
7
+ self.sync = true
8
+ yield
9
+ ensure
10
+ reopen(old_stream)
11
+ old_stream.close
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ # In development environments, engineers will typically use self-signed
2
+ # certificates as it it not realistic to have valid cert chains for localhost
3
+ # or fake domains.
4
+ #
5
+ # This code disables SSL cert verification, and silences net-http-persistent's
6
+ # unnecessary warning.
7
+ #
8
+ # Note that this does _not_ apply in production or staging.
9
+ #
10
+ # http://docs.seattlerb.org/net-http-persistent/History_txt.html#documentation
11
+ # http://www.rubyinside.com/how-to-cure-nethttps-risky-default-https-behavior-4010.html
12
+ if ENV.fetch('RACK_ENV', 'development') !~ /production|staging/
13
+ require 'openssl'
14
+ require 'core_ext/silence_stream'
15
+
16
+ I_KNOW_THAT_OPENSSL_VERIFY_PEER_EQUALS_VERIFY_NONE_IS_WRONG = nil
17
+
18
+ $stderr.silence_stream do
19
+ OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ module Routemaster
2
+ class Client
3
+ VERSION = '0.0.1'
4
+ end
5
+ end
@@ -0,0 +1,105 @@
1
+ require 'routemaster/client/version'
2
+ require 'routemaster/client/openssl'
3
+ require 'uri'
4
+ require 'faraday'
5
+ require 'json'
6
+
7
+ module Routemaster
8
+ class Client
9
+ def initialize(options = {})
10
+ @_url = _assert_valid_url(options[:url])
11
+ @_uuid = options[:uuid]
12
+ @_timeout = options.fetch(:timeout, 1)
13
+
14
+ _assert (options[:uuid] =~ /^[a-z0-9_-]{1,64}$/), 'uuid should be alpha'
15
+ _assert_valid_timeout(@_timeout)
16
+
17
+ _conn.get('/pulse').tap do |response|
18
+ raise 'cannot connect to bus' unless response.success?
19
+ end
20
+ end
21
+
22
+ def created(topic, callback)
23
+ _send_event('create', topic, callback)
24
+ end
25
+
26
+ def updated(topic, callback)
27
+ _send_event('update', topic, callback)
28
+ end
29
+
30
+ def deleted(topic, callback)
31
+ _send_event('delete', topic, callback)
32
+ end
33
+
34
+ def noop(topic, callback)
35
+ _send_event('noop', topic, callback)
36
+ end
37
+
38
+ def subscribe(options = {})
39
+ if (options.keys - [:topics, :callback, :timeout, :max, :uuid]).any?
40
+ raise ArgumentError.new('bad options')
41
+ end
42
+ _assert options[:topics].kind_of?(Enumerable), 'topics required'
43
+ _assert options[:callback], 'callback required'
44
+ _assert_valid_timeout options[:timeout] if options[:timeout]
45
+ _assert_valid_max_events options[:max] if options[:max]
46
+
47
+ options[:topics].each { |t| _assert_valid_topic(t) }
48
+ _assert_valid_url(options[:callback])
49
+
50
+ data = options.to_json
51
+ response = _conn.post('/subscription') do |r|
52
+ r.headers['Content-Type'] = 'application/json'
53
+ r.body = data
54
+ end
55
+ # $stderr.puts response.status
56
+ unless response.success?
57
+ raise 'subscription rejected'
58
+ end
59
+ end
60
+
61
+
62
+ private
63
+
64
+ def _assert_valid_timeout(timeout)
65
+ _assert (0..3_600_000).include?(timeout), 'bad timeout'
66
+ end
67
+
68
+ def _assert_valid_max_events(max)
69
+ _assert (0..10_000).include?(max), 'bad max # events'
70
+ end
71
+
72
+ def _assert_valid_url(url)
73
+ uri = URI.parse(url)
74
+ _assert (uri.scheme == 'https'), 'HTTPS required'
75
+ return url
76
+ end
77
+
78
+ def _assert_valid_topic(topic)
79
+ _assert (topic =~ /^[a-z_]{1,32}$/), 'bad topic name'
80
+ end
81
+
82
+ def _send_event(event, topic, callback)
83
+ _assert_valid_url(callback)
84
+ _assert_valid_topic(topic)
85
+ data = { type: event, url: callback }.to_json
86
+ response = _conn.post("/topics/#{topic}") do |r|
87
+ r.headers['Content-Type'] = 'application/json'
88
+ r.body = data
89
+ end
90
+ fail "event rejected (#{response.status})" unless response.success?
91
+ end
92
+
93
+ def _assert(condition, message)
94
+ condition or raise ArgumentError.new(message)
95
+ end
96
+
97
+ def _conn
98
+ @_conn ||= Faraday.new(@_url) do |f|
99
+ f.use Faraday::Request::BasicAuthentication, @_uuid, 'x'
100
+ f.adapter :net_http_persistent
101
+ f.options.timeout = @_timeout
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,49 @@
1
+ require 'sinatra'
2
+ require 'rack/auth/basic'
3
+ require 'base64'
4
+ require 'json'
5
+
6
+ module Routemaster
7
+ class Receiver
8
+ def initialize(app, options = {})
9
+ @app = app
10
+ @path = options[:path]
11
+ @uuid = options[:uuid]
12
+ @handler = options[:handler]
13
+ end
14
+
15
+ def call(env)
16
+ catch :forward do
17
+ throw :forward unless _intercept_endpoint?(env)
18
+ return [401, {}, []] unless _has_auth?(env)
19
+ return [403, {}, []] unless _valid_auth?(env)
20
+ return [400, {}, []] unless payload = _extract_payload(env)
21
+
22
+ @handler.on_events(payload)
23
+ return [204, {}, []]
24
+ end
25
+ @app.call(env)
26
+ end
27
+
28
+ private
29
+
30
+ def _intercept_endpoint?(env)
31
+ env['PATH_INFO'] == @path && env['REQUEST_METHOD'] == 'POST'
32
+ end
33
+
34
+ def _has_auth?(env)
35
+ env.has_key?('HTTP_AUTHORIZATION')
36
+ end
37
+
38
+ def _valid_auth?(env)
39
+ Base64.
40
+ decode64(env['HTTP_AUTHORIZATION'].gsub(/^Basic /, '')).
41
+ split(':').first == @uuid
42
+ end
43
+
44
+ def _extract_payload(env)
45
+ return unless env['CONTENT_TYPE'] == 'application/json'
46
+ JSON.parse(env['rack.input'].read)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('..', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'routemaster/client/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'routemaster-client'
8
+ spec.version = Routemaster::Client::VERSION
9
+ spec.authors = ['Julien Letessier']
10
+ spec.email = ['julien.letessier@gmail.com']
11
+ spec.summary = %q{Client API for the Routemaster event bus}
12
+ spec.homepage = 'http://github.com/HouseTrip/routemaster_client'
13
+ spec.license = 'MIT'
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.test_files = spec.files.grep(%r{^spec/})
17
+ spec.require_paths = %w(.)
18
+
19
+ spec.add_development_dependency 'bundler', '~> 1.5'
20
+ spec.add_development_dependency 'rake'
21
+ spec.add_development_dependency 'rspec'
22
+ spec.add_development_dependency 'guard-rspec'
23
+ spec.add_development_dependency 'webmock'
24
+ spec.add_development_dependency 'pry-nav'
25
+ spec.add_development_dependency 'rack-test'
26
+
27
+ spec.add_runtime_dependency 'faraday'
28
+ spec.add_runtime_dependency 'net-http-persistent'
29
+ spec.add_runtime_dependency 'sinatra'
30
+ end
@@ -0,0 +1,73 @@
1
+ require 'spec_helper'
2
+ require 'spec/support/rack_test'
3
+ require 'routemaster/receiver'
4
+
5
+ describe Routemaster::Receiver do
6
+ let(:handler) { double 'handler', on_events: nil }
7
+ let(:app) { described_class.new(fake_app, options) }
8
+
9
+
10
+ def perform
11
+ post '/events', payload, 'CONTENT_TYPE' => 'application/json'
12
+ end
13
+
14
+ let(:options) do
15
+ {
16
+ path: '/events',
17
+ uuid: 'demo',
18
+ handler: handler
19
+ }
20
+ end
21
+
22
+ class FakeApp
23
+ def call(env)
24
+ [501, {}, 'fake app']
25
+ end
26
+ end
27
+
28
+ let(:fake_app) { FakeApp.new }
29
+
30
+ let(:payload) do
31
+ [{
32
+ topic: 'widgets', event: 'created', url: 'https://example.com/widgets/1', t: 1234
33
+ }, {
34
+ topic: 'widgets', event: 'created', url: 'https://example.com/widgets/2', t: 1234
35
+ }, {
36
+ topic: 'widgets', event: 'created', url: 'https://example.com/widgets/3', t: 1234
37
+ }].to_json
38
+ end
39
+
40
+
41
+ it 'passes with valid HTTP Basic' do
42
+ authorize 'demo', 'x'
43
+ perform
44
+ expect(last_response.status).to eq(204)
45
+ end
46
+
47
+ it 'fails without authentication' do
48
+ perform
49
+ expect(last_response.status).to eq(401)
50
+ end
51
+
52
+ it 'delegates to the next middleware for unknown paths' do
53
+ post '/foobar'
54
+ expect(last_response.status).to eq(501)
55
+ end
56
+
57
+ it 'delegates to the next middlex for non-POST' do
58
+ get '/events'
59
+ expect(last_response.status).to eq(501)
60
+ end
61
+
62
+ it 'calls the handler when receiving an avent' do
63
+ authorize 'demo', 'x'
64
+ expect(handler).to receive(:on_events).exactly(:once)
65
+ perform
66
+ end
67
+
68
+ it 'calls the handler multiple times' do
69
+ authorize 'demo', 'x'
70
+ expect(handler).to receive(:on_events).exactly(3).times
71
+ 3.times { perform }
72
+ end
73
+ end
@@ -0,0 +1,183 @@
1
+ require 'spec_helper'
2
+ require 'routemaster/client'
3
+ require 'webmock/rspec'
4
+
5
+ describe Routemaster::Client do
6
+ let(:options) do
7
+ { url: 'https://bus.example.com', uuid: 'john_doe' }
8
+ end
9
+
10
+ subject { described_class.new(options) }
11
+
12
+ before do
13
+ @stub_pulse = stub_request(:get, %r{^https://#{options[:uuid]}:x@bus.example.com/pulse$}).with(status: 200)
14
+ end
15
+
16
+ describe '#initialize' do
17
+ it 'passes with valid arguments' do
18
+ expect { subject }.not_to raise_error
19
+ end
20
+
21
+ it 'fails with a non-SSL URL' do
22
+ options[:url].sub!(/https/, 'http')
23
+ expect { subject }.to raise_error(ArgumentError)
24
+ end
25
+
26
+ it 'fails with a bad URL' do
27
+ options[:url].replace('foobar')
28
+ expect { subject }.to raise_error(ArgumentError)
29
+ end
30
+
31
+ it 'fails with a bad client id' do
32
+ options[:uuid].replace('123 $%')
33
+ expect { subject }.to raise_error(ArgumentError)
34
+ end
35
+
36
+ it 'fails it it cannot connect' do
37
+ stub_request(:any, %r{^https://#{options[:uuid]}:x@bus.example.com}).to_raise(Faraday::ConnectionFailed)
38
+ expect { subject }.to raise_error
39
+ end
40
+
41
+ it 'fails if it does not get a successful heartbeat from the app' do
42
+ @stub_pulse.to_return(status: 500)
43
+ expect { subject }.to raise_error
44
+ end
45
+
46
+ it 'fails if the timeout value is not an integer' do
47
+ options[:timeout] = 'timeout'
48
+ expect { subject }.to raise_error
49
+ end
50
+ end
51
+
52
+ shared_examples 'an event sender' do
53
+ let(:callback) { 'https://app.example.com/widgets/123' }
54
+ let(:topic) { 'widgets' }
55
+ let(:perform) { subject.send(event, topic, callback) }
56
+
57
+ before do
58
+ @stub = stub_request(
59
+ :post, "https://#{options[:uuid]}:x@bus.example.com/topics/widgets"
60
+ ).with(status: 200)
61
+ end
62
+
63
+ it 'sends the event' do
64
+ perform
65
+ expect(@stub).to have_been_requested
66
+ end
67
+
68
+ it 'sends a JSON payload' do
69
+ @stub.with do |req|
70
+ expect(req.headers['Content-Type']).to eq('application/json')
71
+ end
72
+ perform
73
+ end
74
+
75
+ it 'fails with a bad callback URL' do
76
+ callback.replace 'http.foo.bar'
77
+ expect { perform }.to raise_error
78
+ end
79
+
80
+ it 'fails with a non-SSL URL' do
81
+ callback.replace 'http://example.com'
82
+ expect { perform }.to raise_error
83
+ end
84
+
85
+ it 'fails with a bad topic name' do
86
+ topic.replace 'foo123$bar'
87
+ expect { perform }.to raise_error
88
+ end
89
+
90
+ it 'fails when an non-success HTTP status is returned' do
91
+ @stub.to_return(status: 500)
92
+ expect { perform }.to raise_error(RuntimeError)
93
+ end
94
+
95
+ it 'fails when the timeout is reached' do
96
+ @stub.to_timeout
97
+ expect { perform }.to raise_error(Faraday::TimeoutError)
98
+ end
99
+ end
100
+
101
+ describe '#created' do
102
+ let(:event) { 'created' }
103
+ it_behaves_like 'an event sender'
104
+ end
105
+
106
+ describe '#updated' do
107
+ let(:event) { 'updated' }
108
+ it_behaves_like 'an event sender'
109
+ end
110
+
111
+ describe '#deleted' do
112
+ let(:event) { 'deleted' }
113
+ it_behaves_like 'an event sender'
114
+ end
115
+
116
+ describe '#noop' do
117
+ let(:event) { 'noop' }
118
+ it_behaves_like 'an event sender'
119
+ end
120
+
121
+ describe '#subscribe' do
122
+ let(:perform) { subject.subscribe(subscribe_options) }
123
+ let(:subscribe_options) {{
124
+ topics: %w(widgets kitten),
125
+ callback: 'https://app.example.com/events',
126
+ timeout: 60_000,
127
+ max: 500
128
+ }}
129
+
130
+ before do
131
+ @stub = stub_request(
132
+ :post, %r{^https://#{options[:uuid]}:x@bus.example.com/subscription$}
133
+ ).with { |r|
134
+ r.headers['Content-Type'] == 'application/json' &&
135
+ JSON.parse(r.body).all? { |k,v| subscribe_options[k.to_sym] == v }
136
+ }
137
+ end
138
+
139
+ it 'passes with correct arguments' do
140
+ expect { perform }.not_to raise_error
141
+ expect(@stub).to have_been_requested
142
+ end
143
+
144
+ it 'fails with a bad callback' do
145
+ subscribe_options[:callback] = 'http://example.com'
146
+ expect { perform }.to raise_error(ArgumentError)
147
+ end
148
+
149
+ it 'fails with a bad timeout' do
150
+ subscribe_options[:timeout] = -5
151
+ expect { perform }.to raise_error(ArgumentError)
152
+ end
153
+
154
+ it 'fails with a bad max number of events' do
155
+ subscribe_options[:max] = 1_000_000
156
+ expect { perform }.to raise_error(ArgumentError)
157
+ end
158
+
159
+ it 'fails with a bad topic list' do
160
+ subscribe_options[:topics] = ['widgets', 'foo123$%bar']
161
+ expect { perform }.to raise_error(ArgumentError)
162
+ end
163
+
164
+ it 'fails on HTTP error' do
165
+ @stub.to_return(status: 500)
166
+ expect { perform }.to raise_error(RuntimeError)
167
+ end
168
+
169
+ it 'accepts a uuid' do
170
+ subscribe_options[:uuid] = 'hello'
171
+ expect { perform }.not_to raise_error
172
+ end
173
+ end
174
+
175
+ describe '#monitor_topics' do
176
+ it 'passes'
177
+ end
178
+
179
+ describe '#monitor_subscriptions' do
180
+ it 'passes'
181
+ end
182
+ end
183
+
@@ -0,0 +1,16 @@
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
+ RSpec.configure do |config|
8
+ config.run_all_when_everything_filtered = true
9
+ config.raise_errors_for_deprecations!
10
+
11
+ # Run specs in random order to surface order dependencies. If you find an
12
+ # order dependency and want to debug it, you can fix the order by providing
13
+ # the seed, which is printed after each run.
14
+ # --seed 1234
15
+ config.order = 'random'
16
+ end
@@ -0,0 +1,9 @@
1
+ ENV['RACK_ENV'] = 'test'
2
+
3
+ require 'rspec'
4
+ require 'rack/test'
5
+
6
+ RSpec.configure do |conf|
7
+ conf.include Rack::Test::Methods
8
+ end
9
+
metadata ADDED
@@ -0,0 +1,237 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: routemaster-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Julien Letessier
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-07-16 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - "~>"
20
+ - !ruby/object:Gem::Version
21
+ version: '1.5'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.5'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: guard-rspec
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: webmock
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: pry-nav
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rack-test
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: faraday
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :runtime
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ - !ruby/object:Gem::Dependency
143
+ name: net-http-persistent
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ type: :runtime
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ - !ruby/object:Gem::Dependency
159
+ name: sinatra
160
+ requirement: !ruby/object:Gem::Requirement
161
+ none: false
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ type: :runtime
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ none: false
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ description:
175
+ email:
176
+ - julien.letessier@gmail.com
177
+ executables: []
178
+ extensions: []
179
+ extra_rdoc_files: []
180
+ files:
181
+ - ".gitignore"
182
+ - ".gitmodules"
183
+ - ".rspec"
184
+ - ".ruby-version"
185
+ - ".travis.yml"
186
+ - Gemfile
187
+ - Gemfile.lock
188
+ - Guardfile
189
+ - LICENSE.txt
190
+ - README.md
191
+ - Rakefile
192
+ - core_ext/silence_stream.rb
193
+ - routemaster-client.gemspec
194
+ - routemaster/client.rb
195
+ - routemaster/client/openssl.rb
196
+ - routemaster/client/version.rb
197
+ - routemaster/receiver.rb
198
+ - spec/receiver_spec.rb
199
+ - spec/routemaster/client_spec.rb
200
+ - spec/spec_helper.rb
201
+ - spec/support/rack_test.rb
202
+ homepage: http://github.com/HouseTrip/routemaster_client
203
+ licenses:
204
+ - MIT
205
+ post_install_message:
206
+ rdoc_options: []
207
+ require_paths:
208
+ - "."
209
+ required_ruby_version: !ruby/object:Gem::Requirement
210
+ none: false
211
+ requirements:
212
+ - - ">="
213
+ - !ruby/object:Gem::Version
214
+ version: '0'
215
+ segments:
216
+ - 0
217
+ hash: 4434098965730026885
218
+ required_rubygems_version: !ruby/object:Gem::Requirement
219
+ none: false
220
+ requirements:
221
+ - - ">="
222
+ - !ruby/object:Gem::Version
223
+ version: '0'
224
+ segments:
225
+ - 0
226
+ hash: 4434098965730026885
227
+ requirements: []
228
+ rubyforge_project:
229
+ rubygems_version: 1.8.23.2
230
+ signing_key:
231
+ specification_version: 3
232
+ summary: Client API for the Routemaster event bus
233
+ test_files:
234
+ - spec/receiver_spec.rb
235
+ - spec/routemaster/client_spec.rb
236
+ - spec/spec_helper.rb
237
+ - spec/support/rack_test.rb