bootic_client 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +115 -0
- data/Rakefile +6 -0
- data/bootic_client.gemspec +33 -0
- data/lib/bootic_client/client.rb +73 -0
- data/lib/bootic_client/entity.rb +125 -0
- data/lib/bootic_client/errors.rb +9 -0
- data/lib/bootic_client/relation.rb +60 -0
- data/lib/bootic_client/stores/memcache.rb +33 -0
- data/lib/bootic_client/strategies/authorized.rb +38 -0
- data/lib/bootic_client/strategies/client_credentials.rb +19 -0
- data/lib/bootic_client/strategies/strategy.rb +59 -0
- data/lib/bootic_client/version.rb +3 -0
- data/lib/bootic_client.rb +47 -0
- data/spec/authorized_strategy_spec.rb +117 -0
- data/spec/bootic_client_spec.rb +8 -0
- data/spec/client_credentials_strategy_spec.rb +96 -0
- data/spec/client_spec.rb +139 -0
- data/spec/entity_spec.rb +171 -0
- data/spec/memcache_storage_spec.rb +55 -0
- data/spec/relation_spec.rb +55 -0
- data/spec/spec_helper.rb +8 -0
- metadata +232 -0
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require "bootic_client/version"
|
3
|
+
require "bootic_client/entity"
|
4
|
+
require "bootic_client/relation"
|
5
|
+
require "bootic_client/client"
|
6
|
+
|
7
|
+
module BooticClient
|
8
|
+
|
9
|
+
AUTH_HOST = 'https://auth.bootic.net'.freeze
|
10
|
+
API_ROOT = 'https://api.bootic.net/v1'.freeze
|
11
|
+
|
12
|
+
class << self
|
13
|
+
|
14
|
+
attr_accessor :client_secret, :client_id, :logging, :cache_store
|
15
|
+
attr_writer :auth_host, :api_root, :logger
|
16
|
+
|
17
|
+
def strategies
|
18
|
+
@strategies ||= {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def client(strategy_name, client_opts = {}, &on_new_token)
|
22
|
+
opts = client_opts.dup
|
23
|
+
opts[:logging] = logging
|
24
|
+
opts[:logger] = logger if logging
|
25
|
+
opts[:cache_store] = cache_store if cache_store
|
26
|
+
require "bootic_client/strategies/#{strategy_name}"
|
27
|
+
strategies.fetch(strategy_name.to_sym).new self, opts, &on_new_token
|
28
|
+
end
|
29
|
+
|
30
|
+
def auth_host
|
31
|
+
@auth_host || AUTH_HOST
|
32
|
+
end
|
33
|
+
|
34
|
+
def api_root
|
35
|
+
@api_root || API_ROOT
|
36
|
+
end
|
37
|
+
|
38
|
+
def logger
|
39
|
+
@logger || ::Logger.new(STDOUT)
|
40
|
+
end
|
41
|
+
|
42
|
+
def configure(&block)
|
43
|
+
yield self
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'jwt'
|
3
|
+
|
4
|
+
describe 'BooticClient::Strategies::Authorized' do
|
5
|
+
require 'webmock/rspec'
|
6
|
+
|
7
|
+
let(:client_id) {'aaa'}
|
8
|
+
let(:client_secret) {'bbb'}
|
9
|
+
|
10
|
+
def jwt_assertion(expired_token, now)
|
11
|
+
JWT.encode({
|
12
|
+
iss: client_id,
|
13
|
+
aud: 'api',
|
14
|
+
prn: expired_token,
|
15
|
+
exp: now.utc.to_i + 5
|
16
|
+
}, client_secret, 'HS256')
|
17
|
+
end
|
18
|
+
|
19
|
+
def stub_api_root(access_token, status, body)
|
20
|
+
stub_request(:get, "https://api.bootic.net/v1").
|
21
|
+
with(headers: {'Accept'=>'*/*', 'Authorization' => "Bearer #{access_token}"}).
|
22
|
+
to_return(status: status, :body => JSON.dump(body))
|
23
|
+
end
|
24
|
+
|
25
|
+
def stub_auth(expired_token, status, body)
|
26
|
+
now = Time.now
|
27
|
+
Time.stub(:now).and_return now
|
28
|
+
|
29
|
+
stub_request(:post, "https://auth.bootic.net/oauth/token").
|
30
|
+
with(body: {
|
31
|
+
"assertion" => jwt_assertion(expired_token, now),
|
32
|
+
"assertion_type" => "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
33
|
+
"client_id" => "",
|
34
|
+
"client_secret" => "",
|
35
|
+
"grant_type" => "assertion",
|
36
|
+
"scope"=>""
|
37
|
+
},
|
38
|
+
headers: {
|
39
|
+
'Content-Type'=>'application/x-www-form-urlencoded'
|
40
|
+
}).
|
41
|
+
to_return(status: status, body: JSON.dump(body), headers: {'Content-Type' => 'application/json'})
|
42
|
+
end
|
43
|
+
|
44
|
+
let(:store){ Hash.new }
|
45
|
+
let(:root_data) {
|
46
|
+
{
|
47
|
+
'_links' => {
|
48
|
+
'shops' => {'href' => 'https://api.bootic.net/v1/products'}
|
49
|
+
},
|
50
|
+
'message' => "Hello!"
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
describe 'with missing client credentials' do
|
55
|
+
it 'raises error' do
|
56
|
+
expect{
|
57
|
+
BooticClient.client(:authorized)
|
58
|
+
}.to raise_error
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe 'with no access_token' do
|
63
|
+
it 'raises error' do
|
64
|
+
expect{
|
65
|
+
BooticClient.client(:authorized)
|
66
|
+
}.to raise_error
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe 'with valid client credentials and access_token' do
|
71
|
+
|
72
|
+
let(:client) do
|
73
|
+
BooticClient.client(:authorized, access_token: 'abc') do |new_token|
|
74
|
+
store[:access_token] = new_token
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
before do
|
79
|
+
BooticClient.configure do |c|
|
80
|
+
c.client_id = client_id
|
81
|
+
c.client_secret = client_secret
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'with valid token' do
|
86
|
+
before do
|
87
|
+
@root_request = stub_api_root('abc', 200, message: 'Hello!')
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'does not request new token to auth service' do
|
91
|
+
root = client.root
|
92
|
+
expect(@root_request).to have_been_requested
|
93
|
+
expect(root.message).to eql('Hello!')
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context 'with expired token' do
|
98
|
+
before do
|
99
|
+
@failed_root_request = stub_api_root('abc', 401, message: 'Unauthorized')
|
100
|
+
@auth_request = stub_auth('abc', 200, access_token: 'foobar')
|
101
|
+
@successful_root_request = stub_api_root('foobar', 200, root_data)
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'attempts unauthorised API request, refreshes token from auth an tries again' do
|
105
|
+
root = client.root
|
106
|
+
expect(@failed_root_request).to have_been_requested
|
107
|
+
expect(@auth_request).to have_been_requested
|
108
|
+
expect(root.message).to eql('Hello!')
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'yields new token to optional block' do
|
112
|
+
client.root
|
113
|
+
expect(store[:access_token]).to eql('foobar')
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'BooticClient::Strategies::ClientCredentials' do
|
4
|
+
require 'webmock/rspec'
|
5
|
+
|
6
|
+
let(:store){ Hash.new }
|
7
|
+
let(:root_data) {
|
8
|
+
{
|
9
|
+
'_links' => {
|
10
|
+
'shops' => {'href' => 'https://api.bootic.net/v1/products'}
|
11
|
+
},
|
12
|
+
'message' => "Hello!"
|
13
|
+
}
|
14
|
+
}
|
15
|
+
|
16
|
+
describe 'with missing client credentials' do
|
17
|
+
it 'raises error' do
|
18
|
+
expect{
|
19
|
+
BooticClient.client(:client_credentials, scope: 'admin')
|
20
|
+
}.to raise_error
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe 'with valid client credentials' do
|
25
|
+
|
26
|
+
def stub_auth(status, body)
|
27
|
+
stub_request(:post, "https://aaa:bbb@auth.bootic.net/oauth/token").
|
28
|
+
with(body: {"grant_type"=>"client_credentials", 'scope' => 'admin'}).
|
29
|
+
to_return(status: status, body: JSON.dump(body), headers: {'Content-Type' => 'application/json'})
|
30
|
+
end
|
31
|
+
|
32
|
+
def stub_api_root(access_token, status, body)
|
33
|
+
stub_request(:get, "https://api.bootic.net/v1").
|
34
|
+
with(headers: {'Accept'=>'*/*', 'Authorization' => "Bearer #{access_token}"}).
|
35
|
+
to_return(status: status, :body => JSON.dump(body))
|
36
|
+
end
|
37
|
+
|
38
|
+
before do
|
39
|
+
BooticClient.configure do |c|
|
40
|
+
c.client_id = 'aaa'
|
41
|
+
c.client_secret = 'bbb'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'with no previous access token' do
|
46
|
+
let(:client) do
|
47
|
+
BooticClient.client(:client_credentials, scope: 'admin') do |new_token|
|
48
|
+
store[:access_token] = new_token
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
before do
|
53
|
+
@auth_request = stub_auth(200, access_token: 'foobar')
|
54
|
+
stub_api_root 'foobar', 200, root_data
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'requests access token via client_credentials flow' do
|
58
|
+
root = client.root
|
59
|
+
expect(@auth_request).to have_been_requested
|
60
|
+
expect(root.message).to eql('Hello!')
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'yields new token to optional block' do
|
64
|
+
client.root
|
65
|
+
expect(store[:access_token]).to eql('foobar')
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'with an expired access token' do
|
70
|
+
let(:client) do
|
71
|
+
BooticClient.client(:client_credentials, scope: 'admin', access_token: 'abc') do |new_token|
|
72
|
+
store[:access_token] = new_token
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
before do
|
77
|
+
@failed_root_request = stub_api_root('abc', 401, message: 'Unauthorized')
|
78
|
+
@auth_request = stub_auth(200, access_token: 'foobar')
|
79
|
+
@successful_root_request = stub_api_root('foobar', 200, root_data)
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'attempts unauthorised API request, gets token from auth an tries again' do
|
83
|
+
root = client.root
|
84
|
+
expect(@failed_root_request).to have_been_requested
|
85
|
+
expect(@auth_request).to have_been_requested
|
86
|
+
expect(root.message).to eql('Hello!')
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'yields new token to optional block' do
|
90
|
+
client.root
|
91
|
+
expect(store[:access_token]).to eql('foobar')
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
describe BooticClient::Client do
|
5
|
+
require 'webmock/rspec'
|
6
|
+
|
7
|
+
describe 'valid response' do
|
8
|
+
let(:root_url) { 'https://api.bootic.net/v1' }
|
9
|
+
let(:client) { BooticClient::Client.new(root_url, access_token: 'xxx') }
|
10
|
+
let(:response_headers) {
|
11
|
+
{'Content-Type' => 'application/json', 'Last-Modified' => 'Sat, 07 Jun 2014 12:10:33 GMT'}
|
12
|
+
}
|
13
|
+
let(:root_data) {
|
14
|
+
{
|
15
|
+
'_links' => {
|
16
|
+
'shops' => {'href' => 'https://api.bootic.net/v1/products'}
|
17
|
+
},
|
18
|
+
'message' => "Hello!"
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
describe '#get' do
|
23
|
+
|
24
|
+
context 'fresh' do
|
25
|
+
before do
|
26
|
+
stub_request(:get, root_url)
|
27
|
+
.to_return(status: 200, body: JSON.dump(root_data), headers: response_headers)
|
28
|
+
end
|
29
|
+
|
30
|
+
let!(:response) { client.get(root_url) }
|
31
|
+
|
32
|
+
it 'returns parsed Faraday response' do
|
33
|
+
expect(response).to be_kind_of(Faraday::Response)
|
34
|
+
expect(response.status).to eql(200)
|
35
|
+
response.body.tap do |b|
|
36
|
+
expect(b['_links']['shops']).to eql({'href' => 'https://api.bootic.net/v1/products'})
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'and then cached' do
|
41
|
+
before do
|
42
|
+
@cached_request = stub_request(:get, root_url)
|
43
|
+
.with(headers: {'If-Modified-Since' => 'Sat, 07 Jun 2014 12:10:33 GMT'})
|
44
|
+
.to_return(status: 304, body: '', headers: response_headers)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'returns cached response' do
|
48
|
+
r = client.get(root_url)
|
49
|
+
expect(@cached_request).to have_been_requested
|
50
|
+
|
51
|
+
expect(r.status).to eql(200)
|
52
|
+
r.body.tap do |b|
|
53
|
+
expect(b['_links']['shops']).to eql({'href' => 'https://api.bootic.net/v1/products'})
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'errors' do
|
60
|
+
describe 'no access token' do
|
61
|
+
it 'raises error' do
|
62
|
+
expect{
|
63
|
+
BooticClient::Client.new(root_url).get(root_url)
|
64
|
+
}.to raise_error(BooticClient::NoAccessTokenError)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe '500 Server error' do
|
69
|
+
before do
|
70
|
+
stub_request(:get, root_url)
|
71
|
+
.to_return(status: 500, body: JSON.dump(message: 'Server error'), headers: response_headers)
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'raises exception' do
|
75
|
+
expect{
|
76
|
+
client.get(root_url)
|
77
|
+
}.to raise_error(BooticClient::ServerError)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe '404 Not Found' do
|
82
|
+
before do
|
83
|
+
stub_request(:get, root_url)
|
84
|
+
.to_return(status: 404, body: JSON.dump(message: 'not Found'), headers: response_headers)
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'raises exception' do
|
88
|
+
expect{
|
89
|
+
client.get(root_url)
|
90
|
+
}.to raise_error(BooticClient::NotFoundError)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe '401 Unauthorized' do
|
95
|
+
before do
|
96
|
+
stub_request(:get, root_url)
|
97
|
+
.to_return(status: 401, body: JSON.dump(message: 'Unauthorised'), headers: response_headers)
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'raises exception' do
|
101
|
+
expect{
|
102
|
+
client.get(root_url)
|
103
|
+
}.to raise_error(BooticClient::UnauthorizedError)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe '403 Access Forbidden' do
|
108
|
+
before do
|
109
|
+
stub_request(:get, root_url)
|
110
|
+
.to_return(status: 403, body: JSON.dump(message: 'Access Forbidden'), headers: response_headers)
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'raises exception' do
|
114
|
+
expect{
|
115
|
+
client.get(root_url)
|
116
|
+
}.to raise_error(BooticClient::AccessForbiddenError)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
describe '#get_and_wrap' do
|
124
|
+
before do
|
125
|
+
stub_request(:get, root_url)
|
126
|
+
.with(query: {foo: 'bar'})
|
127
|
+
.to_return(status: 200, body: JSON.dump(root_data), headers: response_headers)
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'wraps JSON response in entity' do
|
131
|
+
wrapper = double('Wrapper Class')
|
132
|
+
entity = double('Entity')
|
133
|
+
expect(wrapper).to receive(:new).with(root_data, client).and_return entity
|
134
|
+
expect(client.get_and_wrap(root_url, wrapper, foo: 'bar')).to eql(entity)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
data/spec/entity_spec.rb
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe BooticClient::Entity do
|
4
|
+
let(:client) { double(:client) }
|
5
|
+
let(:list_payload) do
|
6
|
+
{
|
7
|
+
'total_items' => 10,
|
8
|
+
'per_page' => 2,
|
9
|
+
'page' => 1,
|
10
|
+
'an_object' => {
|
11
|
+
'name' => 'Foobar',
|
12
|
+
'age' => 22
|
13
|
+
},
|
14
|
+
'_links' => {
|
15
|
+
'self' => {'href' => '/foo'},
|
16
|
+
'next' => { 'href' => '/foo?page=2'},
|
17
|
+
'btc:products' => {'href' => '/all/products'},
|
18
|
+
'btc:search' => {'href' => '/search{?q}', 'templated' => true},
|
19
|
+
'curies' => [
|
20
|
+
{
|
21
|
+
'name' => "btc",
|
22
|
+
'href' => "https://developers.bootic.net/rels/{rel}",
|
23
|
+
'templated' => true
|
24
|
+
}
|
25
|
+
]
|
26
|
+
},
|
27
|
+
"_embedded" => {
|
28
|
+
'items' => [
|
29
|
+
{
|
30
|
+
'title' => 'iPhone 4',
|
31
|
+
'price' => 12345,
|
32
|
+
'_links' => {
|
33
|
+
'self' => {href: '/products/iphone4'},
|
34
|
+
'btc:delete_product' => {'href' => '/products/12345'}
|
35
|
+
},
|
36
|
+
'_embedded' => {
|
37
|
+
'shop' => {
|
38
|
+
'name' => 'Acme'
|
39
|
+
}
|
40
|
+
}
|
41
|
+
},
|
42
|
+
|
43
|
+
{
|
44
|
+
'title' => 'iPhone 5',
|
45
|
+
'price' => 12342,
|
46
|
+
'_links' => {
|
47
|
+
'self' => {href: '/products/iphone5'}
|
48
|
+
},
|
49
|
+
'_embedded' => {
|
50
|
+
'shop' => {
|
51
|
+
'name' => 'Apple'
|
52
|
+
}
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
] # / items
|
57
|
+
}
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'parsing JSON HAL' do
|
62
|
+
let(:entity) { BooticClient::Entity.new(list_payload, client) }
|
63
|
+
|
64
|
+
it 'knows about plain properties' do
|
65
|
+
expect(entity.total_items).to eql(10)
|
66
|
+
expect(entity.per_page).to eql(2)
|
67
|
+
expect(entity.page).to eql(1)
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'wraps object properties as entities' do
|
71
|
+
expect(entity.an_object.name).to eql('Foobar')
|
72
|
+
expect(entity.an_object.age).to eql(22)
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'has a #properties object' do
|
76
|
+
expect(entity.properties[:total_items]).to eql(10)
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'responds to #has?' do
|
80
|
+
expect(entity.has?(:total_items)).to eql(true)
|
81
|
+
expect(entity.has?(:items)).to eql(true)
|
82
|
+
expect(entity.has?(:foobar)).to eql(false)
|
83
|
+
end
|
84
|
+
|
85
|
+
describe 'embedded entities' do
|
86
|
+
|
87
|
+
it 'has a #entities object' do
|
88
|
+
expect(entity.entities[:items]).to be_a(Array)
|
89
|
+
expect(entity.entities[:items].first.entities[:shop]).to be_kind_of(BooticClient::Entity)
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'are exposed like normal attributes' do
|
93
|
+
expect(entity.items).to be_kind_of(Array)
|
94
|
+
entity.items.first.tap do |product|
|
95
|
+
expect(product).to be_kind_of(BooticClient::Entity)
|
96
|
+
expect(product.title).to eql('iPhone 4')
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'recursively builds embedded entities' do
|
101
|
+
product = entity.items.first
|
102
|
+
product.shop.tap do |shop|
|
103
|
+
expect(shop).to be_kind_of(BooticClient::Entity)
|
104
|
+
expect(shop.name).to eql('Acme')
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end #/ embedded entities
|
108
|
+
|
109
|
+
describe 'link relations' do
|
110
|
+
it 'responds to #has? for link relations' do
|
111
|
+
expect(entity.has?(:next)).to eql(true)
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'builds relation objects' do
|
115
|
+
expect(entity.rels[:next]).to be_kind_of(BooticClient::Relation)
|
116
|
+
expect(entity.rels[:next].href).to eql('/foo?page=2')
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'understands namespaced cURIes' do
|
120
|
+
expect(entity.rels[:products]).to be_kind_of(BooticClient::Relation)
|
121
|
+
expect(entity.rels[:products].href).to eql('/all/products')
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'adds docs if cURIes available' do
|
125
|
+
expect(entity.rels[:products].docs).to eql('https://developers.bootic.net/rels/products')
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'adds docs if cURIes available even in nested entities' do
|
129
|
+
prod = entity.items.first
|
130
|
+
expect(prod.rels[:delete_product].docs).to eql('https://developers.bootic.net/rels/delete_product')
|
131
|
+
end
|
132
|
+
|
133
|
+
context 'eagerly fetching rels' do
|
134
|
+
let(:next_page) { BooticClient::Entity.new({'page' => 2}, client) }
|
135
|
+
|
136
|
+
it 'exposes link target resources as normal properties' do
|
137
|
+
expect(client).to receive(:get_and_wrap).with('/foo?page=2', BooticClient::Entity, {}).and_return next_page
|
138
|
+
entity.next.tap do |next_entity|
|
139
|
+
expect(next_entity).to be_kind_of(BooticClient::Entity)
|
140
|
+
expect(next_entity.page).to eql(2)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'takes optional URI parameters' do
|
145
|
+
expect(client).to receive(:get_and_wrap).with('/search?q=foo', BooticClient::Entity).and_return next_page
|
146
|
+
entity.search(q: 'foo').tap do |next_entity|
|
147
|
+
expect(next_entity).to be_kind_of(BooticClient::Entity)
|
148
|
+
expect(next_entity.page).to eql(2)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
describe 'iterating' do
|
156
|
+
it 'iterates items if it is a list' do
|
157
|
+
prods = []
|
158
|
+
entity.each{|pr| prods << pr}
|
159
|
+
expect(prods).to match_array(entity.items)
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'iterates itself if not a list' do
|
163
|
+
ent = BooticClient::Entity.new({'foo' => 'bar'}, client)
|
164
|
+
ents = []
|
165
|
+
ent.each{|e| ents << e}
|
166
|
+
expect(ents).to match_array([ent])
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'bootic_client/stores/memcache'
|
3
|
+
|
4
|
+
describe BooticClient::Stores::Memcache do
|
5
|
+
let(:dalli) { double('Dalli') }
|
6
|
+
let(:store) { BooticClient::Stores::Memcache.new(['localhost:1112'], foo: 'bar') }
|
7
|
+
|
8
|
+
before do
|
9
|
+
Dalli::Client.stub(:new).with(['localhost:1112'], foo: 'bar').and_return dalli
|
10
|
+
end
|
11
|
+
|
12
|
+
shared_examples_for 'dalli :get' do |method_name|
|
13
|
+
it 'delegates to Dalli client #get' do
|
14
|
+
expect(dalli).to receive(:get).with('foo').and_return 'bar'
|
15
|
+
expect(store.send(method_name, 'foo')).to eql('bar')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
shared_examples_for 'dalli :set' do |method_name|
|
20
|
+
it 'delegates to Dalli client #set' do
|
21
|
+
expect(dalli).to receive(:set).with('foo', 'bar', 123).and_return 'bar'
|
22
|
+
expect(store.send(method_name, 'foo', 'bar', 123)).to eql('bar')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#initialize' do
|
27
|
+
it 'creates a Dalli instance' do
|
28
|
+
expect(Dalli::Client).to receive(:new).with(['localhost:1112'], foo: 'bar').and_return dalli
|
29
|
+
expect(store.client).to eql(dalli)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#read' do
|
34
|
+
it_behaves_like 'dalli :get', :read
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '#get' do
|
38
|
+
it_behaves_like 'dalli :get', :get
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '#write' do
|
42
|
+
it_behaves_like 'dalli :set', :write
|
43
|
+
end
|
44
|
+
|
45
|
+
describe '#set' do
|
46
|
+
it_behaves_like 'dalli :set', :set
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '#stats' do
|
50
|
+
it 'delegates to Dalli client #stats' do
|
51
|
+
expect(dalli).to receive(:stats).and_return 'foobar'
|
52
|
+
expect(store.stats).to eql('foobar')
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe BooticClient::Relation do
|
4
|
+
let(:client) { double(:client) }
|
5
|
+
let(:relation) { BooticClient::Relation.new({'href' => '/foo/bars', 'type' => 'application/json', 'title' => 'A relation', 'name' => 'self'}, client) }
|
6
|
+
|
7
|
+
describe 'attributes' do
|
8
|
+
it 'has readers for known relation attributes' do
|
9
|
+
expect(relation.href).to eql('/foo/bars')
|
10
|
+
expect(relation.type).to eql('application/json')
|
11
|
+
expect(relation.title).to eql('A relation')
|
12
|
+
expect(relation.name).to eql('self')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '#get' do
|
17
|
+
let(:entity) { BooticClient::Entity.new({'title' => 'Foobar'}, client) }
|
18
|
+
|
19
|
+
it 'fetches data and returns entity' do
|
20
|
+
client.stub(:get_and_wrap).with('/foo/bars', BooticClient::Entity, {}).and_return entity
|
21
|
+
expect(relation.get).to eql(entity)
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'without URI templates' do
|
25
|
+
let(:relation) { BooticClient::Relation.new({'href' => '/foos/bar', 'type' => 'application/json', 'title' => 'A relation'}, client) }
|
26
|
+
|
27
|
+
it 'is not templated' do
|
28
|
+
expect(relation.templated?).to eql(false)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'passes query string to client' do
|
32
|
+
expect(client).to receive(:get_and_wrap).with('/foos/bar', BooticClient::Entity, id: 2, q: 'test', page: 2).and_return entity
|
33
|
+
expect(relation.get(id: 2, q: 'test', page: 2)).to eql(entity)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'with URI templates' do
|
38
|
+
let(:relation) { BooticClient::Relation.new({'href' => '/foos/{id}{?q,page}', 'type' => 'application/json', 'title' => 'A relation', 'templated' => true}, client) }
|
39
|
+
|
40
|
+
it 'is templated' do
|
41
|
+
expect(relation.templated?).to eql(true)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'works with defaults' do
|
45
|
+
expect(client).to receive(:get_and_wrap).with('/foos/', BooticClient::Entity).and_return entity
|
46
|
+
expect(relation.get).to eql(entity)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'interpolates tokens' do
|
50
|
+
expect(client).to receive(:get_and_wrap).with('/foos/2?q=test&page=2', BooticClient::Entity).and_return entity
|
51
|
+
expect(relation.get(id: 2, q: 'test', page: 2)).to eql(entity)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|