routemaster-drain 1.1.0 → 2.0.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.
- checksums.yaml +4 -4
- data/.env.test +0 -1
- data/.travis.yml +4 -0
- data/Appraisals +11 -0
- data/Gemfile +7 -3
- data/Gemfile.lock +12 -7
- data/README.md +60 -11
- data/gemfiles/rails_3.gemfile +24 -0
- data/gemfiles/rails_3.gemfile.lock +224 -0
- data/gemfiles/rails_4.gemfile +24 -0
- data/gemfiles/rails_4.gemfile.lock +224 -0
- data/gemfiles/rails_5.gemfile +24 -0
- data/gemfiles/rails_5.gemfile.lock +250 -0
- data/lib/routemaster/{fetcher.rb → api_client.rb} +34 -20
- data/lib/routemaster/cache.rb +5 -5
- data/lib/routemaster/config.rb +14 -0
- data/lib/routemaster/drain.rb +1 -1
- data/lib/routemaster/errors.rb +73 -0
- data/lib/routemaster/middleware/error_handling.rb +28 -0
- data/lib/routemaster/middleware/{caching.rb → response_caching.rb} +23 -11
- data/lib/routemaster/resources/rest_resource.rb +26 -0
- data/lib/routemaster/responses/hateoas_response.rb +51 -0
- data/routemaster-drain.gemspec +1 -1
- data/spec/routemaster/{fetcher_spec.rb → api_client_spec.rb} +33 -2
- data/spec/routemaster/cache_spec.rb +7 -7
- data/spec/routemaster/integration/api_client_spec.rb +76 -0
- data/spec/routemaster/integration/cache_spec.rb +10 -3
- data/spec/routemaster/middleware/authenticate_spec.rb +2 -2
- data/spec/routemaster/resources/rest_resource_spec.rb +35 -0
- data/spec/routemaster/responses/hateoas_response_spec.rb +50 -0
- metadata +25 -8
@@ -0,0 +1,73 @@
|
|
1
|
+
module Routemaster
|
2
|
+
module Errors
|
3
|
+
class BaseError < RuntimeError
|
4
|
+
attr_reader :env
|
5
|
+
|
6
|
+
def initialize(env)
|
7
|
+
@env = env
|
8
|
+
super(message)
|
9
|
+
end
|
10
|
+
|
11
|
+
def errors
|
12
|
+
body['errors']
|
13
|
+
end
|
14
|
+
|
15
|
+
def message
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
def body
|
20
|
+
@body ||= deserialized_body
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def deserialized_body
|
26
|
+
@env.body.empty? ? {} : JSON.parse(@env.body)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class UnauthorizedResourceAccessError < BaseError
|
31
|
+
def message
|
32
|
+
"Unauthorized Resource Access Error"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class InvalidResourceError < BaseError
|
37
|
+
def message
|
38
|
+
"Invalid Resource Error"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class ResourceNotFoundError < BaseError
|
43
|
+
def message
|
44
|
+
"Resource Not Found Error"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class FatalResourceError < BaseError
|
49
|
+
def message
|
50
|
+
"Fatal Resource Error. body: #{body}, url: #{env.url}, method: #{env.method}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class ConflictResourceError < BaseError
|
55
|
+
def message
|
56
|
+
"ConflictResourceError Resource Error"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class IncompatibleVersionError < BaseError
|
61
|
+
def message
|
62
|
+
headers = env.request_headers.select { |k, _| k != 'Authorization' }
|
63
|
+
"Incompatible Version Error. headers: #{headers}, url: #{env.url}, method: #{env.method}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class ResourceThrottlingError < BaseError
|
68
|
+
def message
|
69
|
+
"Resource Throttling Error"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'faraday_middleware'
|
2
|
+
require 'routemaster/errors'
|
3
|
+
|
4
|
+
module Routemaster
|
5
|
+
module Middleware
|
6
|
+
class ErrorHandling < Faraday::Response::Middleware
|
7
|
+
ERRORS_MAPPING = {
|
8
|
+
(400..400) => Errors::InvalidResourceError,
|
9
|
+
(401..401) => Errors::UnauthorizedResourceAccessError,
|
10
|
+
(403..403) => Errors::UnauthorizedResourceAccessError,
|
11
|
+
(404..404) => Errors::ResourceNotFoundError,
|
12
|
+
(409..409) => Errors::ConflictResourceError,
|
13
|
+
(412..412) => Errors::IncompatibleVersionError,
|
14
|
+
(413..413) => Errors::InvalidResourceError,
|
15
|
+
(429..429) => Errors::ResourceThrottlingError,
|
16
|
+
(407..500) => Errors::FatalResourceError
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
def on_complete(env)
|
20
|
+
ERRORS_MAPPING.each do |range, error_class|
|
21
|
+
if range.include?(env[:status])
|
22
|
+
raise error_class.new(env)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -2,9 +2,10 @@ require 'wisper'
|
|
2
2
|
|
3
3
|
module Routemaster
|
4
4
|
module Middleware
|
5
|
-
class
|
5
|
+
class ResponseCaching
|
6
6
|
KEY_TEMPLATE = 'cache:{url}'
|
7
|
-
|
7
|
+
BODY_FIELD_TEMPLATE = 'v:{version},l:{locale},body'
|
8
|
+
HEADERS_FIELD_TEMPLATE = 'v:{version},l:{locale},headers'
|
8
9
|
VERSION_REGEX = /application\/json;v=(?<version>\S*)/.freeze
|
9
10
|
|
10
11
|
def initialize(app, cache: Config.cache_redis, listener: nil)
|
@@ -27,27 +28,38 @@ module Routemaster
|
|
27
28
|
response = response_env.response
|
28
29
|
|
29
30
|
if response.success?
|
30
|
-
@cache.
|
31
|
-
|
31
|
+
@cache.multi do |multi|
|
32
|
+
multi.hset(cache_key(env), body_cache_field(env), response.body)
|
33
|
+
multi.hset(cache_key(env), headers_cache_field(env), Marshal.dump(response.headers))
|
34
|
+
multi.expire(cache_key(env), @expiry)
|
35
|
+
end
|
32
36
|
@listener._publish(:cache_miss, url(env)) if @listener
|
33
37
|
end
|
34
38
|
end
|
35
39
|
end
|
36
40
|
|
37
41
|
def fetch_from_cache(env)
|
38
|
-
|
42
|
+
body = @cache.hget(cache_key(env), body_cache_field(env))
|
43
|
+
headers = @cache.hget(cache_key(env), headers_cache_field(env))
|
39
44
|
|
40
|
-
if
|
45
|
+
if body && headers
|
41
46
|
@listener._publish(:cache_hit, url(env)) if @listener
|
42
47
|
Faraday::Response.new(status: 200,
|
43
|
-
body:
|
44
|
-
response_headers:
|
48
|
+
body: body,
|
49
|
+
response_headers: Marshal.load(headers),
|
50
|
+
request: {})
|
45
51
|
end
|
46
52
|
end
|
47
53
|
|
48
|
-
def
|
49
|
-
|
50
|
-
|
54
|
+
def body_cache_field(env)
|
55
|
+
BODY_FIELD_TEMPLATE
|
56
|
+
.gsub('{version}', version(env).to_s)
|
57
|
+
.gsub('{locale}', locale(env).to_s)
|
58
|
+
end
|
59
|
+
|
60
|
+
def headers_cache_field(env)
|
61
|
+
HEADERS_FIELD_TEMPLATE
|
62
|
+
.gsub('{version}', version(env).to_s)
|
51
63
|
.gsub('{locale}', locale(env).to_s)
|
52
64
|
end
|
53
65
|
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'routemaster/api_client'
|
2
|
+
|
3
|
+
module Routemaster
|
4
|
+
module Resources
|
5
|
+
class RestResource
|
6
|
+
attr_reader :url
|
7
|
+
|
8
|
+
def initialize(url, client: nil)
|
9
|
+
@url = url
|
10
|
+
@client = client || Routemaster::APIClient.new(response_class: Responses::HateoasResponse)
|
11
|
+
end
|
12
|
+
|
13
|
+
def create(params)
|
14
|
+
@client.post(@url, body: params)
|
15
|
+
end
|
16
|
+
|
17
|
+
def show(id=nil)
|
18
|
+
@client.get(@url.gsub('{id}', id.to_s))
|
19
|
+
end
|
20
|
+
|
21
|
+
def index
|
22
|
+
@client.get(@url)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'faraday_middleware'
|
2
|
+
require 'routemaster/api_client'
|
3
|
+
require 'routemaster/responses/hateoas_response'
|
4
|
+
require 'routemaster/resources/rest_resource'
|
5
|
+
require 'forwardable'
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
module Routemaster
|
9
|
+
module Responses
|
10
|
+
class HateoasResponse
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
attr_reader :response
|
14
|
+
def_delegators :@response, :body, :status, :headers, :success?
|
15
|
+
|
16
|
+
def initialize(response, client: nil)
|
17
|
+
@response = response
|
18
|
+
@client = client || Routemaster::APIClient.new(response_class: Routemaster::Responses::HateoasResponse)
|
19
|
+
end
|
20
|
+
|
21
|
+
def method_missing(m, *args, &block)
|
22
|
+
method_name = m.to_s
|
23
|
+
normalized_method_name = method_name == '_self' ? 'self' : method_name
|
24
|
+
|
25
|
+
if _links.keys.include?(normalized_method_name)
|
26
|
+
unless respond_to?(method_name)
|
27
|
+
resource = Resources::RestResource.new(_links[normalized_method_name]['href'], client: @client)
|
28
|
+
|
29
|
+
self.class.send(:define_method, method_name) do |*m_args|
|
30
|
+
resource
|
31
|
+
end
|
32
|
+
|
33
|
+
resource
|
34
|
+
end
|
35
|
+
else
|
36
|
+
super
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def body_without_links
|
41
|
+
body.reject { |key, _| ['_links'].include?(key) }
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def _links
|
47
|
+
@links ||= @response.body.fetch('_links', {})
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/routemaster-drain.gemspec
CHANGED
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.add_runtime_dependency 'faraday', '>= 0.9.0'
|
20
20
|
spec.add_runtime_dependency 'faraday_middleware'
|
21
21
|
spec.add_runtime_dependency 'net-http-persistent', '< 3' # 3.x is currently incompatible with faraday
|
22
|
-
spec.add_runtime_dependency 'rack', '>= 1.
|
22
|
+
spec.add_runtime_dependency 'rack', '>= 1.4.5'
|
23
23
|
spec.add_runtime_dependency 'wisper', '~> 1.6.1'
|
24
24
|
spec.add_runtime_dependency 'hashie'
|
25
25
|
spec.add_runtime_dependency 'redis-namespace'
|
@@ -2,10 +2,10 @@ require 'spec_helper'
|
|
2
2
|
require 'spec/support/uses_dotenv'
|
3
3
|
require 'spec/support/uses_redis'
|
4
4
|
require 'spec/support/uses_webmock'
|
5
|
-
require 'routemaster/
|
5
|
+
require 'routemaster/api_client'
|
6
6
|
require 'json'
|
7
7
|
|
8
|
-
describe Routemaster::
|
8
|
+
describe Routemaster::APIClient do
|
9
9
|
uses_dotenv
|
10
10
|
uses_redis
|
11
11
|
uses_webmock
|
@@ -24,6 +24,14 @@ describe Routemaster::Fetcher do
|
|
24
24
|
'content-type' => 'application/json;v=1'
|
25
25
|
}
|
26
26
|
)
|
27
|
+
|
28
|
+
@post_req = stub_request(:post, /example\.com/).to_return(
|
29
|
+
status: 200,
|
30
|
+
body: { id: 132, type: 'widget' }.to_json,
|
31
|
+
headers: {
|
32
|
+
'content-type' => 'application/json;v=1'
|
33
|
+
}
|
34
|
+
)
|
27
35
|
end
|
28
36
|
|
29
37
|
it 'GETs from the URL' do
|
@@ -31,6 +39,15 @@ describe Routemaster::Fetcher do
|
|
31
39
|
expect(@req).to have_been_requested
|
32
40
|
end
|
33
41
|
|
42
|
+
context 'POST request' do
|
43
|
+
subject { fetcher.post(url, body: {}, headers: headers) }
|
44
|
+
|
45
|
+
it 'POSTs from the URL' do
|
46
|
+
subject
|
47
|
+
expect(@post_req).to have_been_requested
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
34
51
|
it 'has :status, :headers, :body' do
|
35
52
|
expect(subject.status).to eq(200)
|
36
53
|
expect(subject.headers).to have_key('content-type')
|
@@ -56,5 +73,19 @@ describe Routemaster::Fetcher do
|
|
56
73
|
expect(req.headers).to include('X-Custom-Header')
|
57
74
|
end
|
58
75
|
end
|
76
|
+
|
77
|
+
context 'when response_class is present' do
|
78
|
+
before do
|
79
|
+
class DummyResponse
|
80
|
+
def initialize(res, client: nil); end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
let(:fetcher) { described_class.new(response_class: DummyResponse) }
|
85
|
+
|
86
|
+
it 'returns a response_class instance as a response' do
|
87
|
+
expect(subject).to be_an_instance_of(DummyResponse)
|
88
|
+
end
|
89
|
+
end
|
59
90
|
end
|
60
91
|
end
|
@@ -3,7 +3,7 @@ require 'spec/support/uses_redis'
|
|
3
3
|
require 'spec/support/uses_dotenv'
|
4
4
|
require 'spec/support/uses_webmock'
|
5
5
|
require 'routemaster/cache'
|
6
|
-
require 'routemaster/
|
6
|
+
require 'routemaster/api_client'
|
7
7
|
|
8
8
|
module Routemaster
|
9
9
|
describe Cache do
|
@@ -27,8 +27,8 @@ module Routemaster
|
|
27
27
|
context 'with no options' do
|
28
28
|
let(:options) { {} }
|
29
29
|
|
30
|
-
it 'calls get on the
|
31
|
-
expect_any_instance_of(
|
30
|
+
it 'calls get on the api client with no version and locale headers' do
|
31
|
+
expect_any_instance_of(APIClient)
|
32
32
|
.to receive(:get)
|
33
33
|
.with(url, headers: { 'Accept' => 'application/json' })
|
34
34
|
.and_call_original
|
@@ -40,8 +40,8 @@ module Routemaster
|
|
40
40
|
context 'with a specific version' do
|
41
41
|
let(:options) { { version: 2 } }
|
42
42
|
|
43
|
-
it 'calls get on the
|
44
|
-
expect_any_instance_of(
|
43
|
+
it 'calls get on the api client with version header' do
|
44
|
+
expect_any_instance_of(APIClient)
|
45
45
|
.to receive(:get)
|
46
46
|
.with(url, headers: { 'Accept' => 'application/json;v=2' })
|
47
47
|
.and_call_original
|
@@ -53,8 +53,8 @@ module Routemaster
|
|
53
53
|
context 'with a specific locale' do
|
54
54
|
let(:options) { { locale: 'fr' } }
|
55
55
|
|
56
|
-
it 'calls get on the
|
57
|
-
expect_any_instance_of(
|
56
|
+
it 'calls get on the api client with locale header' do
|
57
|
+
expect_any_instance_of(APIClient)
|
58
58
|
.to receive(:get)
|
59
59
|
.with(url, headers: { 'Accept' => 'application/json', 'Accept-Language' => 'fr' })
|
60
60
|
.and_call_original
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'spec/support/uses_redis'
|
3
|
+
require 'spec/support/uses_dotenv'
|
4
|
+
require 'routemaster/api_client'
|
5
|
+
require 'webrick'
|
6
|
+
|
7
|
+
RSpec.describe 'Api client integration specs' do
|
8
|
+
uses_dotenv
|
9
|
+
uses_redis
|
10
|
+
|
11
|
+
let!(:log) { WEBrick::Log.new '/dev/null' }
|
12
|
+
let(:service) do
|
13
|
+
WEBrick::HTTPServer.new(Port: 8000, DocumentRoot: Dir.pwd, Logger: log).tap do |server|
|
14
|
+
[400, 401, 403, 404, 409, 412, 413, 429, 500].each do |status_code|
|
15
|
+
server.mount_proc "/#{status_code}" do |req, res|
|
16
|
+
res.status = status_code
|
17
|
+
res.body = { field: 'test' }.to_json
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
before do
|
24
|
+
@pid = fork do
|
25
|
+
trap 'INT' do service.shutdown end
|
26
|
+
service.start
|
27
|
+
end
|
28
|
+
sleep(0.5) # leave sometime for the previous webrick to teardown
|
29
|
+
end
|
30
|
+
|
31
|
+
after do
|
32
|
+
Process.kill('KILL', @pid)
|
33
|
+
Process.wait(@pid)
|
34
|
+
end
|
35
|
+
|
36
|
+
subject { Routemaster::APIClient.new }
|
37
|
+
let(:host) { 'http://localhost:8000' }
|
38
|
+
|
39
|
+
describe 'error handling' do
|
40
|
+
it 'raises an ResourceNotFoundError on 404' do
|
41
|
+
expect { subject.get(host + '/404') }.to raise_error(Routemaster::Errors::ResourceNotFoundError)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'raises an InvalidResourceError on 400' do
|
45
|
+
expect { subject.get(host + '/400') }.to raise_error(Routemaster::Errors::InvalidResourceError)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'raises an UnauthorizedResourceAccessError on 401' do
|
49
|
+
expect { subject.get(host + '/401') }.to raise_error(Routemaster::Errors::UnauthorizedResourceAccessError)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'raises an UnauthorizedResourceAccessError on 403' do
|
53
|
+
expect { subject.get(host + '/403') }.to raise_error(Routemaster::Errors::UnauthorizedResourceAccessError)
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'raises an ConflictResourceError on 409' do
|
57
|
+
expect { subject.get(host + '/409') }.to raise_error(Routemaster::Errors::ConflictResourceError)
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'raises an IncompatibleVersionError on 412' do
|
61
|
+
expect { subject.get(host + '/412') }.to raise_error(Routemaster::Errors::IncompatibleVersionError)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'raises an InvalidResourceError on 413' do
|
65
|
+
expect { subject.get(host + '/413') }.to raise_error(Routemaster::Errors::InvalidResourceError)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'raises an ResourceThrottlingError on 429' do
|
69
|
+
expect { subject.get(host + '/429') }.to raise_error(Routemaster::Errors::ResourceThrottlingError)
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'raises an FatalResourceError on 500' do
|
73
|
+
expect { subject.get(host + '/500') }.to raise_error(Routemaster::Errors::FatalResourceError)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -33,7 +33,8 @@ RSpec.describe 'Requests with caching' do
|
|
33
33
|
subject { Routemaster::Cache.new }
|
34
34
|
|
35
35
|
describe 'GET request' do
|
36
|
-
let(:
|
36
|
+
let(:body_cache_keys) { ["cache:#{url}", "v:,l:,body"] }
|
37
|
+
let(:headers_cache_keys) { ["cache:#{url}", "v:,l:,headers"] }
|
37
38
|
let(:url) { 'http://localhost:8000/test' }
|
38
39
|
|
39
40
|
context 'when there is no previous cached response' do
|
@@ -44,10 +45,16 @@ RSpec.describe 'Requests with caching' do
|
|
44
45
|
|
45
46
|
it 'sets the new response onto the cache' do
|
46
47
|
expect { subject.get(url) }
|
47
|
-
.to change { Routemaster::Config.cache_redis.hget(*
|
48
|
+
.to change { Routemaster::Config.cache_redis.hget(*body_cache_keys)}
|
48
49
|
.from(nil)
|
49
50
|
.to({ field: 'test'}.to_json)
|
50
51
|
end
|
52
|
+
|
53
|
+
it 'sets the response headers onto the cache' do
|
54
|
+
expect { subject.get(url) }
|
55
|
+
.to change { Routemaster::Config.cache_redis.hget(*headers_cache_keys)}
|
56
|
+
.from(nil)
|
57
|
+
end
|
51
58
|
end
|
52
59
|
|
53
60
|
context 'when there is a previous cached response' do
|
@@ -61,7 +68,7 @@ RSpec.describe 'Requests with caching' do
|
|
61
68
|
|
62
69
|
it 'does not make an http call' do
|
63
70
|
response = subject.get(url)
|
64
|
-
expect(response.
|
71
|
+
expect(response.env.request).to be_empty
|
65
72
|
end
|
66
73
|
end
|
67
74
|
end
|
@@ -8,11 +8,11 @@ describe Routemaster::Middleware::Authenticate do
|
|
8
8
|
let(:app) { described_class.new(ErrorRackApp.new, options) }
|
9
9
|
let(:listener) { double 'listener', on_authenticate: nil }
|
10
10
|
let(:options) {{ uuid: 'demo' }}
|
11
|
-
|
11
|
+
|
12
12
|
def perform
|
13
13
|
post '/whatever'
|
14
14
|
end
|
15
|
-
|
15
|
+
|
16
16
|
before { Wisper.subscribe(listener, scope: described_class.name, prefix: true) }
|
17
17
|
after { Wisper::GlobalListeners.clear }
|
18
18
|
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'routemaster/resources/rest_resource'
|
3
|
+
|
4
|
+
module Routemaster
|
5
|
+
module Resources
|
6
|
+
RSpec.describe RestResource do
|
7
|
+
let(:url) { 'test_url' }
|
8
|
+
let(:client) { double('Client') }
|
9
|
+
let(:params) { {} }
|
10
|
+
|
11
|
+
subject { described_class.new(url, client: client) }
|
12
|
+
|
13
|
+
describe '#create' do
|
14
|
+
it 'posts to the given url' do
|
15
|
+
expect(client).to receive(:post).with(url, body: params)
|
16
|
+
subject.create(params)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#show' do
|
21
|
+
it 'gets to the given url' do
|
22
|
+
expect(client).to receive(:get).with(url)
|
23
|
+
subject.show(1)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#index' do
|
28
|
+
it 'gets to the given url' do
|
29
|
+
expect(client).to receive(:get).with(url)
|
30
|
+
subject.index
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'routemaster/responses/hateoas_response'
|
3
|
+
|
4
|
+
module Routemaster
|
5
|
+
module Responses
|
6
|
+
RSpec.describe HateoasResponse do
|
7
|
+
let(:response) { double('Response', status: status, body: body, headers: headers) }
|
8
|
+
let(:status) { 200 }
|
9
|
+
let(:body) { {}.to_json }
|
10
|
+
let(:headers) { {} }
|
11
|
+
|
12
|
+
subject { described_class.new(response) }
|
13
|
+
|
14
|
+
context 'link traversal' do
|
15
|
+
let(:body) do
|
16
|
+
{
|
17
|
+
'_links' => {
|
18
|
+
'self' => { 'href' => 'self_url' },
|
19
|
+
'resource_a' => { 'href' => 'resource_a_url' },
|
20
|
+
'resource_b' => { 'href' => 'resource_b_url' }
|
21
|
+
}
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'creates a method for every key in _links attribute' do
|
26
|
+
expect(subject.resource_a.url).to eq('resource_a_url')
|
27
|
+
expect(subject.resource_b.url).to eq('resource_b_url')
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'creates a _self method if there is a link with name self' do
|
31
|
+
expect(subject._self.url).to eq('self_url')
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'raise an exception when requested link does not exist' do
|
35
|
+
expect { subject.some_unsupported_link }.to raise_error(NoMethodError)
|
36
|
+
end
|
37
|
+
|
38
|
+
describe '#body_without_links' do
|
39
|
+
before do
|
40
|
+
body.merge!('foo' => 'bar')
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'returns the body without the _links key' do
|
44
|
+
expect(subject.body_without_links).to eq({ 'foo' => 'bar' })
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|