routemaster-client 0.0.1
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.
- data/.gitignore +2 -0
- data/.gitmodules +3 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +16 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +93 -0
- data/Guardfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +107 -0
- data/Rakefile +1 -0
- data/core_ext/silence_stream.rb +13 -0
- data/routemaster/client/openssl.rb +21 -0
- data/routemaster/client/version.rb +5 -0
- data/routemaster/client.rb +105 -0
- data/routemaster/receiver.rb +49 -0
- data/routemaster-client.gemspec +30 -0
- data/spec/receiver_spec.rb +73 -0
- data/spec/routemaster/client_spec.rb +183 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/rack_test.rb +9 -0
- metadata +237 -0
data/.gitignore
ADDED
data/.gitmodules
ADDED
data/.rspec
ADDED
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
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,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
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
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
|