bootic_client 0.0.31 → 0.0.33

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8906bbfeddb298199ecb675eea93c3780da4a370633bd73cb633bc10f893ae5
4
- data.tar.gz: d0de74be08f4fffb7f68e02354dead206c67ee1db0662506668fe18ee0ebc4d3
3
+ metadata.gz: 355d7cbacb5e60ff2c051cc0f48452d01d4c099f0038dc5c0334a41811e8f5d3
4
+ data.tar.gz: fffcba9efcee1e1662e3337fd7ee25c1dbbe713f1153ed305f415a8a7c96e8a6
5
5
  SHA512:
6
- metadata.gz: caa8411fdb9ab778b1e30c0c9c2e39ee0847a7d8a652658314d058d273b52c653ca85fc444aed5894d4406455965a32202c52365ac7a32ba085d54280360e4df
7
- data.tar.gz: 1f4989cf0177a73128c9861cedc41a047a986c1ec70394311168b354e312da768d2f2530364c63748e0ed431e9f9b3642321c73e34266c3150500cdf5ae8c53a
6
+ metadata.gz: d8ffddf4c0a8825b5bad13e21a35c5b978d5629bf3025783c6fa42dab246dda845ab14e98694d949907698d7d81e01f779849e19c36421b471ecd6bc4d4d0c3c
7
+ data.tar.gz: fe5c3912de3febdb6ba374472e3a0996eb63f83504f8a5af266563f5b9cc46ed16b7a006fd22e66f83447669aef722893f1b24d4efd896f91638804d4a151be7
data/README.md CHANGED
@@ -320,6 +320,40 @@ messaging_api_root = client.from_url("https://some.api.com")
320
320
  messaging_api.do_something(foo: "bar") # etc
321
321
  ```
322
322
 
323
+ ## Testing
324
+
325
+ # What
326
+
327
+ This library provides methods to simplify testing. For example, to stub a chain of links you can simply do:
328
+
329
+ ```rb
330
+ BooticClient.stub_chain('root.shops.first').and_return_data({
331
+ 'name' => 'Foo bar'
332
+ })
333
+
334
+ client = BooticClient.client(:authorized, access_token: 'abc')
335
+ shop = client.root.shops.first
336
+ expect(shop).to be_a BooticClient::Entity
337
+ expect(shop.name).to eq 'Foo bar'
338
+ ```
339
+
340
+ You can also stub links that requires arguments, like this:
341
+
342
+ ```rb
343
+ BooticClient.stub_chain('root.shops', foo: 1).and_return_data({
344
+ 'name' => 'Foo 1'
345
+ })
346
+ BooticClient.stub_chain('root.shops', foo: 1, bar: { hello: 123 }).and_return_data({
347
+ 'name' => 'Foo 2'
348
+ })
349
+
350
+ client = BooticClient.client(:authorized, access_token: 'abc')
351
+ expect(client.root.shops(foo: 1).name).to eq 'Foo 1'
352
+
353
+ # arg order doesn't matter
354
+ expect(client.root.shops(bar: { hello: 123 }, foo: 2).name).to eq 'Foo 2'
355
+ ```
356
+
323
357
  ## Contributing
324
358
 
325
359
  1. Fork it
@@ -108,15 +108,15 @@ module BooticClient
108
108
  yield req if block_given?
109
109
  end
110
110
 
111
- raise_if_invalid! resp
111
+ raise_if_invalid! resp, "#{verb.upcase} #{href}"
112
112
  resp
113
113
  end
114
114
 
115
- def raise_if_invalid!(resp)
116
- raise ServerError, "Server Error" if resp.status > 499
117
- raise NotFoundError, "Not Found" if resp.status == 404
118
- raise UnauthorizedError, "Unauthorized request" if resp.status == 401
119
- raise AccessForbiddenError, "Access Forbidden" if resp.status == 403
115
+ def raise_if_invalid!(resp, url = nil)
116
+ raise ServerError.new("Server Error", url) if resp.status > 499
117
+ raise NotFoundError.new("Not Found", url) if resp.status == 404
118
+ raise UnauthorizedError.new("Unauthorized Request", url) if resp.status == 401
119
+ raise AccessForbiddenError.new("Access Forbidden", url) if resp.status == 403
120
120
  end
121
121
 
122
122
  def sanitized(payload)
@@ -15,9 +15,14 @@ module BooticClient
15
15
 
16
16
  Enumerator.new do |yielder|
17
17
  loop do
18
- page.each{|item| yielder.yield item }
18
+ page.each { |item| yielder.yield(item) }
19
19
  raise StopIteration unless page.has_rel?(:next)
20
20
  page = page.next
21
+
22
+ if page.has?(:errors) # && page.errors.first.messages.first['cannot be higher'] # reached last page
23
+ yielder.yield(nil, page.errors) # yield a nil value so caller can stop gracefully
24
+ raise StopIteration
25
+ end
21
26
  end
22
27
  end
23
28
  end
@@ -1,7 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BooticClient
4
- class TransportError < StandardError; end
4
+ class TransportError < StandardError
5
+ attr_reader :url
6
+ def initialize(msg = nil, url = nil)
7
+ super(msg)
8
+ @url = url
9
+ end
10
+ end
11
+
5
12
  class ServerError < TransportError; end
6
13
  class NotFoundError < ServerError; end
7
14
  class AuthorizationError < ServerError; end
@@ -0,0 +1,118 @@
1
+ module BooticClient
2
+ module Stubbing
3
+ MissingStubError = Class.new(StandardError)
4
+
5
+ module Stubber
6
+ def from_hash(h)
7
+ self
8
+ end
9
+
10
+ def stub_chain(method_path, opts = {})
11
+ meths = method_path.split('.')
12
+ c = 0
13
+ opts = stringify_keys(opts)
14
+
15
+ meths.reduce(self) do |stub, method_name|
16
+ c += 1
17
+ a = c == meths.size ? opts : {}
18
+ stub.stub(method_name, a)
19
+ end
20
+ end
21
+
22
+ def stub(method_name, opts = {})
23
+ key = stub_key(method_name, opts)
24
+ if st = stubs[key]
25
+ st.stub(method_name, opts)
26
+ st
27
+ else
28
+ stubs[key] = Stub.new(method_name, opts)
29
+ end
30
+ end
31
+
32
+ def method_missing(method_name, *args, &block)
33
+ opts = stringify_keys(args.first)
34
+ if stub = stubs[stub_key(method_name, opts)]
35
+ stub.returns? ? stub.returns : stub
36
+ else
37
+ raise MissingStubError, "No method stubbed for '#{method_name}' with options #{opts.inspect}"
38
+ end
39
+ end
40
+
41
+ def respond_to_missing?(method_name, include_private = false)
42
+ stubs.keys.any?{|k|
43
+ k.to_s =~ /^#{method_name.to_s}/
44
+ }
45
+ end
46
+
47
+ private
48
+ def stub_key(method_name, opts)
49
+ [method_name.to_s, options_key(opts || {})].join('_')
50
+ end
51
+
52
+ def options_key(opts)
53
+ # sort keys
54
+ keys = opts.keys.sort
55
+ hash = keys.each_with_object({}) do |key, h|
56
+ value = if opts[key].is_a?(Hash)
57
+ options_key(opts[key])
58
+ else
59
+ opts[key].to_s
60
+ end
61
+
62
+ h[key] = value
63
+ end
64
+
65
+ hash.inspect
66
+ end
67
+
68
+ def stringify_keys(hash)
69
+ return hash unless hash.is_a?(Hash)
70
+
71
+ hash.each_with_object({}) do |(k, v), h|
72
+ h[k.to_s] = stringify_keys(v)
73
+ end
74
+ end
75
+ end
76
+
77
+ class StubRoot
78
+ include Stubber
79
+
80
+ def initialize
81
+ @stubs = {}
82
+ end
83
+
84
+ private
85
+ attr_reader :stubs
86
+ end
87
+
88
+ class Stub
89
+ include Stubber
90
+
91
+ def initialize(method_name = '', opts = {})
92
+ @method_name, @opts = method_name, opts
93
+ @return_data = nil
94
+ @stubs = {}
95
+ end
96
+
97
+ def and_return_data(data)
98
+ @return_data = data
99
+ self
100
+ end
101
+
102
+ def returns?
103
+ !!@return_data
104
+ end
105
+
106
+ def returns
107
+ if @return_data.is_a?(Array)
108
+ @return_data.map{|d| BooticClient::Entity.new(d, nil)}
109
+ else
110
+ BooticClient::Entity.new(@return_data || {}, nil)
111
+ end
112
+ end
113
+
114
+ private
115
+ attr_reader :stubs
116
+ end
117
+ end
118
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BooticClient
4
- VERSION = "0.0.31".freeze
4
+ VERSION = "0.0.33".freeze
5
5
  end
data/lib/bootic_client.rb CHANGED
@@ -12,6 +12,7 @@ module BooticClient
12
12
  end
13
13
 
14
14
  def client(strategy_name, client_opts = {}, &on_new_token)
15
+ return @stubber if @stubber
15
16
  opts = client_opts.dup
16
17
  opts[:logging] = configuration.logging
17
18
  opts[:logger] = configuration.logger if configuration.logging
@@ -21,6 +22,10 @@ module BooticClient
21
22
  strategies.fetch(strategy_name.to_sym).new configuration, opts, &on_new_token
22
23
  end
23
24
 
25
+ def auth_host
26
+ @auth_host || AUTH_HOST
27
+ end
28
+
24
29
  def configure(&block)
25
30
  yield configuration
26
31
  end
@@ -28,5 +33,18 @@ module BooticClient
28
33
  def configuration
29
34
  @configuration ||= Configuration.new
30
35
  end
36
+
37
+ def stub!
38
+ require "bootic_client/stubbing"
39
+ @stubber = Stubbing::StubRoot.new
40
+ end
41
+
42
+ def stub_chain(method_chain, opts = {})
43
+ @stubber.stub_chain(method_chain, opts)
44
+ end
45
+
46
+ def unstub!
47
+ @stubber = nil
48
+ end
31
49
  end
32
50
  end
data/spec/client_spec.rb CHANGED
@@ -140,6 +140,12 @@ describe BooticClient::Client do
140
140
  expect{
141
141
  client.get(root_url)
142
142
  }.to raise_error(BooticClient::ServerError)
143
+
144
+ begin
145
+ client.get(root_url)
146
+ rescue BooticClient::ServerError => e
147
+ expect(e.url).to eq("GET #{root_url}")
148
+ end
143
149
  end
144
150
  end
145
151
 
@@ -153,6 +159,12 @@ describe BooticClient::Client do
153
159
  expect{
154
160
  client.get(root_url)
155
161
  }.to raise_error(BooticClient::NotFoundError)
162
+
163
+ begin
164
+ client.get(root_url)
165
+ rescue BooticClient::NotFoundError => e
166
+ expect(e.url).to eq("GET #{root_url}")
167
+ end
156
168
  end
157
169
  end
158
170
 
@@ -166,6 +178,12 @@ describe BooticClient::Client do
166
178
  expect{
167
179
  client.get(root_url)
168
180
  }.to raise_error(BooticClient::UnauthorizedError)
181
+
182
+ begin
183
+ client.get(root_url)
184
+ rescue BooticClient::UnauthorizedError => e
185
+ expect(e.url).to eq("GET #{root_url}")
186
+ end
169
187
  end
170
188
  end
171
189
 
@@ -179,6 +197,12 @@ describe BooticClient::Client do
179
197
  expect{
180
198
  client.get(root_url)
181
199
  }.to raise_error(BooticClient::AccessForbiddenError)
200
+
201
+ begin
202
+ client.get(root_url)
203
+ rescue BooticClient::AccessForbiddenError => e
204
+ expect(e.url).to eq("GET #{root_url}")
205
+ end
182
206
  end
183
207
  end
184
208
  end
data/spec/entity_spec.rb CHANGED
@@ -232,6 +232,34 @@ describe BooticClient::Entity do
232
232
  titles = results.map(&:title)
233
233
  expect(titles).to match_array(['iPhone 4', 'iPhone 5', 'Item 3', 'Item 4'])
234
234
  end
235
+
236
+ it 'handles errors gracefully' do
237
+ error_data = {
238
+ "_embedded" => {
239
+ 'errors' => [
240
+ { field: 'foo', messages: ['Ugly error'] }
241
+ ]
242
+ }
243
+ }
244
+
245
+ error_page = BooticClient::Entity.new(error_data, client)
246
+ expect(client).to receive(:request_and_wrap).with(:get, '/foo?page=2', {}).and_return(error_page)
247
+ expect(client).to_not receive(:request_and_wrap).with(:get, '/foo?page=3', {})
248
+
249
+ valid = []
250
+ errors = nil
251
+ entity.full_set.each do |item, errors|
252
+ if item
253
+ valid.push(item)
254
+ else
255
+ expect(valid.count).to eq(2)
256
+ expect(errors.first.to_hash).to eq({ field: 'foo', messages: ['Ugly error']})
257
+ end
258
+ end
259
+
260
+ titles = valid.map(&:title)
261
+ expect(titles).to match_array(['iPhone 4', 'iPhone 5'])
262
+ end
235
263
  end
236
264
  end
237
265
 
@@ -0,0 +1,96 @@
1
+ require 'spec_helper'
2
+
3
+ describe "stubbing" do
4
+ before do
5
+ BooticClient.stub!
6
+ end
7
+
8
+ after do
9
+ BooticClient.unstub!
10
+ end
11
+
12
+ it "stubs method chains and returns entities" do
13
+ BooticClient.stub_chain('root.shops.first').and_return_data({
14
+ 'name' => 'Foo bar'
15
+ })
16
+
17
+ client = BooticClient.client(:authorized, access_token: 'abc')
18
+ shop = client.root.shops.first
19
+ expect(shop).to be_a BooticClient::Entity
20
+ expect(shop.name).to eq 'Foo bar'
21
+ end
22
+
23
+ it "stubs method chains and returns arrays of entities" do
24
+ BooticClient.stub_chain('root.shops').and_return_data([
25
+ {'name' => 'Foo bar'},
26
+ {'name' => 'Bar foo'}
27
+ ])
28
+
29
+ client = BooticClient.client(:authorized, access_token: 'abc')
30
+ shops = client.root.shops
31
+ expect(shops.first).to be_a BooticClient::Entity
32
+ expect(shops.last).to be_a BooticClient::Entity
33
+ expect(shops.first.name).to eq 'Foo bar'
34
+ end
35
+
36
+ it 'can be chained further' do
37
+ BooticClient.stub_chain('foo.bar')
38
+ client = BooticClient.client(:authorized, access_token: 'abc')
39
+
40
+ stub = client.foo.bar
41
+ stub.stub_chain('another.stubz').and_return_data({
42
+ 'id' => 123
43
+ })
44
+
45
+ expect(stub.another.stubz.id).to eq 123
46
+ end
47
+
48
+ it 'stubs depending on arguments' do
49
+ BooticClient.stub_chain('root.shops', foo: 0).and_return_data({
50
+ 'name' => 'Foo 0'
51
+ })
52
+ BooticClient.stub_chain('root.shops', foo: 1).and_return_data({
53
+ 'name' => 'Foo 1'
54
+ })
55
+ BooticClient.stub_chain('root.shops', foo: 2, bar: {yup: 'yiss'}).and_return_data({
56
+ 'name' => 'Foo 2'
57
+ })
58
+
59
+ client = BooticClient.client(:authorized, access_token: 'abc')
60
+
61
+ expect(client.root.shops(foo: 0).name).to eq 'Foo 0'
62
+ expect(client.root.shops(foo: 1).name).to eq 'Foo 1'
63
+ expect(client.root.shops(foo: 2, bar: {yup: 'yiss'}).name).to eq 'Foo 2'
64
+ # arg order shouldn't matter
65
+ expect(client.root.shops(bar: {yup: 'yiss'}, foo: 2).name).to eq 'Foo 2'
66
+
67
+ expect {
68
+ client.root.shops(foo: 2, bar: {yup: 'nope'})
69
+ }.to raise_error BooticClient::Stubbing::MissingStubError
70
+ end
71
+
72
+ it "stubs multiple chains with arguments" do
73
+ BooticClient.stub_chain('one.two', arg: 1).stub_chain('three.four').and_return_data('name' => 'example 1')
74
+ BooticClient.stub_chain('one.two', arg: 2).stub_chain('three.four').and_return_data('name' => 'example 2')
75
+
76
+ client = BooticClient.client(:authorized, access_token: 'abc')
77
+
78
+ expect(client.one.two(arg: 1).three.four.name).to eq 'example 1'
79
+ expect(client.one.two(arg: 2).three.four.name).to eq 'example 2'
80
+ end
81
+
82
+ it "treats symbol and string keys the same" do
83
+ BooticClient.stub_chain('one.two', arg: 1).and_return_data('name' => 'example 1')
84
+ client = BooticClient.client(:authorized, access_token: 'abc')
85
+
86
+ expect(client.one.two("arg" => 1).name).to eq 'example 1'
87
+ end
88
+
89
+ it "raises known exception if no stub found" do
90
+ client = BooticClient.client(:authorized, access_token: 'abc')
91
+
92
+ expect{
93
+ client.nope
94
+ }.to raise_error BooticClient::Stubbing::MissingStubError
95
+ end
96
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bootic_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.31
4
+ version: 0.0.33
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ismael Celis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-03-22 00:00:00.000000000 Z
11
+ date: 2025-11-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -181,6 +181,7 @@ files:
181
181
  - lib/bootic_client/strategies/client_credentials.rb
182
182
  - lib/bootic_client/strategies/oauth2_strategy.rb
183
183
  - lib/bootic_client/strategies/strategy.rb
184
+ - lib/bootic_client/stubbing.rb
184
185
  - lib/bootic_client/version.rb
185
186
  - lib/bootic_client/whiny_uri.rb
186
187
  - spec/authorized_strategy_spec.rb
@@ -198,6 +199,7 @@ files:
198
199
  - spec/safe_cache_serializer_spec.rb
199
200
  - spec/spec_helper.rb
200
201
  - spec/strategy_spec.rb
202
+ - spec/stubbing_spec.rb
201
203
  - spec/whiny_uri_spec.rb
202
204
  homepage: https://developers.bootic.net
203
205
  licenses:
@@ -218,7 +220,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
218
220
  - !ruby/object:Gem::Version
219
221
  version: '0'
220
222
  requirements: []
221
- rubygems_version: 3.2.32
223
+ rubygems_version: 3.4.6
222
224
  signing_key:
223
225
  specification_version: 4
224
226
  summary: Official Ruby client for the Bootic API
@@ -238,4 +240,5 @@ test_files:
238
240
  - spec/safe_cache_serializer_spec.rb
239
241
  - spec/spec_helper.rb
240
242
  - spec/strategy_spec.rb
243
+ - spec/stubbing_spec.rb
241
244
  - spec/whiny_uri_spec.rb