bootic_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.
- 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
|