lhs 21.3.1 → 23.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,725 +3,691 @@
3
3
  require 'rails_helper'
4
4
 
5
5
  describe LHS::Record do
6
- let(:datastore) { 'http://local.ch/v2' }
7
- before { LHC.config.placeholder('datastore', datastore) }
8
-
9
- let(:stub_campaign_request) do
10
- stub_request(:get, "#{datastore}/content-ads/51dfc5690cf271c375c5a12d")
11
- .to_return(body: {
12
- 'href' => "#{datastore}/content-ads/51dfc5690cf271c375c5a12d",
13
- 'entry' => { 'href' => "#{datastore}/local-entries/lakj35asdflkj1203va" },
14
- 'user' => { 'href' => "#{datastore}/users/lakj35asdflkj1203va" }
15
- }.to_json)
16
- end
17
-
18
- let(:stub_entry_request) do
19
- stub_request(:get, "#{datastore}/local-entries/lakj35asdflkj1203va")
20
- .to_return(body: { 'name' => 'Casa Ferlin' }.to_json)
21
- end
22
-
23
- let(:stub_user_request) do
24
- stub_request(:get, "#{datastore}/users/lakj35asdflkj1203va")
25
- .to_return(body: { 'name' => 'Mario' }.to_json)
26
- end
27
-
28
- context 'singlelevel includes' do
6
+ context 'includes all' do
29
7
  before do
30
- class LocalEntry < LHS::Record
31
- endpoint '{+datastore}/local-entries'
32
- endpoint '{+datastore}/local-entries/{id}'
8
+ class Customer < LHS::Record
9
+ endpoint 'http://datastore/customers/{id}'
33
10
  end
11
+
34
12
  class User < LHS::Record
35
- endpoint '{+datastore}/users'
36
- endpoint '{+datastore}/users/{id}'
37
- end
38
- class Favorite < LHS::Record
39
- endpoint '{+datastore}/favorites'
40
- endpoint '{+datastore}/favorites/{id}'
41
- end
42
- stub_request(:get, "#{datastore}/local-entries/1")
43
- .to_return(body: { company_name: 'local.ch' }.to_json)
44
- stub_request(:get, "#{datastore}/users/1")
45
- .to_return(body: { name: 'Mario' }.to_json)
46
- stub_request(:get, "#{datastore}/favorites/1")
47
- .to_return(body: {
48
- local_entry: { href: "#{datastore}/local-entries/1" },
49
- user: { href: "#{datastore}/users/1" }
50
- }.to_json)
13
+ configuration pagination_strategy: 'link'
14
+ endpoint 'http://datastore/users'
15
+ end
51
16
  end
52
17
 
53
- it 'includes a resource' do
54
- favorite = Favorite.includes(:local_entry).find(1)
55
- expect(favorite.local_entry.company_name).to eq 'local.ch'
18
+ let(:amount_of_contracts) { 33 }
19
+ let(:amount_of_products) { 22 }
20
+ let(:amount_of_users_1st_page) { 10 }
21
+ let(:amount_of_users_2nd_page) { 3 }
22
+ let(:amount_of_users) { amount_of_users_1st_page + amount_of_users_2nd_page }
23
+
24
+ let!(:customer_request) do
25
+ stub_request(:get, 'http://datastore/customers/1')
26
+ .to_return(
27
+ body: {
28
+ contracts: { href: 'http://datastore/customers/1/contracts' },
29
+ users: { href: 'http://datastore/users?limit=10' }
30
+ }.to_json
31
+ )
56
32
  end
57
33
 
58
- it 'duplicates a class' do
59
- expect(Favorite.object_id).not_to eq(Favorite.includes(:local_entry).object_id)
34
+ #
35
+ # Contracts
36
+ #
37
+
38
+ let!(:contracts_request) do
39
+ stub_request(:get, "http://datastore/customers/1/contracts?limit=100")
40
+ .to_return(
41
+ body: {
42
+ items: 10.times.map do
43
+ {
44
+ products: { href: 'http://datastore/products' }
45
+ }
46
+ end,
47
+ limit: 10,
48
+ offset: 0,
49
+ total: amount_of_contracts
50
+ }.to_json
51
+ )
60
52
  end
61
53
 
62
- it 'includes a list of resources' do
63
- favorite = Favorite.includes(:local_entry, :user).find(1)
64
- expect(favorite.local_entry).to be_kind_of LocalEntry
65
- expect(favorite.local_entry.company_name).to eq 'local.ch'
66
- expect(favorite.user.name).to eq 'Mario'
54
+ def additional_contracts_request(offset, amount)
55
+ stub_request(:get, "http://datastore/customers/1/contracts?limit=10&offset=#{offset}")
56
+ .to_return(
57
+ body: {
58
+ items: amount.times.map do
59
+ {
60
+ products: { href: 'http://datastore/products' }
61
+ }
62
+ end,
63
+ limit: 10,
64
+ offset: offset,
65
+ total: amount_of_contracts
66
+ }.to_json
67
+ )
67
68
  end
68
69
 
69
- it 'includes an array of resources' do
70
- favorite = Favorite.includes([:local_entry, :user]).find(1)
71
- expect(favorite.local_entry.company_name).to eq 'local.ch'
72
- expect(favorite.user.name).to eq 'Mario'
70
+ let!(:contracts_request_page_2) do
71
+ additional_contracts_request(10, 10)
73
72
  end
74
- end
75
73
 
76
- context 'multilevel includes' do
77
- before do
78
- class Feedback < LHS::Record
79
- endpoint '{+datastore}/feedbacks'
80
- endpoint '{+datastore}/feedbacks/{id}'
81
- end
82
- stub_campaign_request
83
- stub_entry_request
84
- stub_user_request
74
+ let!(:contracts_request_page_3) do
75
+ additional_contracts_request(20, 10)
85
76
  end
86
77
 
87
- it 'includes linked resources while fetching multiple resources from one service' do
88
- stub_request(:get, "#{datastore}/feedbacks?has_reviews=true")
89
- .to_return(status: 200, body: {
90
- items: [
91
- {
92
- 'href' => "#{datastore}/feedbacks/-Sc4_pYNpqfsudzhtivfkA",
93
- 'campaign' => { 'href' => "#{datastore}/content-ads/51dfc5690cf271c375c5a12d" }
94
- }
95
- ]
96
- }.to_json)
97
-
98
- feedbacks = Feedback.includes(campaign: :entry).where(has_reviews: true)
99
- expect(feedbacks.first.campaign.entry.name).to eq 'Casa Ferlin'
78
+ let!(:contracts_request_page_4) do
79
+ additional_contracts_request(30, 3)
100
80
  end
101
81
 
102
- it 'includes linked resources while fetching a single resource from one service' do
103
- stub_request(:get, "#{datastore}/feedbacks/123")
104
- .to_return(status: 200, body: {
105
- 'href' => "#{datastore}/feedbacks/-Sc4_pYNpqfsudzhtivfkA",
106
- 'campaign' => { 'href' => "#{datastore}/content-ads/51dfc5690cf271c375c5a12d" }
107
- }.to_json)
82
+ #
83
+ # Products
84
+ #
108
85
 
109
- feedbacks = Feedback.includes(campaign: :entry).find(123)
110
- expect(feedbacks.campaign.entry.name).to eq 'Casa Ferlin'
86
+ let!(:products_request) do
87
+ stub_request(:get, "http://datastore/products?limit=100")
88
+ .to_return(
89
+ body: {
90
+ items: 10.times.map do
91
+ { name: 'LBC' }
92
+ end,
93
+ limit: 10,
94
+ offset: 0,
95
+ total: amount_of_products
96
+ }.to_json
97
+ )
111
98
  end
112
99
 
113
- it 'includes linked resources with array while fetching a single resource from one service' do
114
- stub_request(:get, "#{datastore}/feedbacks/123")
115
- .to_return(status: 200, body: {
116
- 'href' => "#{datastore}/feedbacks/-Sc4_pYNpqfsudzhtivfkA",
117
- 'campaign' => { 'href' => "#{datastore}/content-ads/51dfc5690cf271c375c5a12d" }
118
- }.to_json)
100
+ def additional_products_request(offset, amount)
101
+ stub_request(:get, "http://datastore/products?limit=10&offset=#{offset}")
102
+ .to_return(
103
+ body: {
104
+ items: amount.times.map do
105
+ { name: 'LBC' }
106
+ end,
107
+ limit: 10,
108
+ offset: offset,
109
+ total: amount_of_products
110
+ }.to_json
111
+ )
112
+ end
119
113
 
120
- feedbacks = Feedback.includes(campaign: [:entry, :user]).find(123)
121
- expect(feedbacks.campaign.entry.name).to eq 'Casa Ferlin'
122
- expect(feedbacks.campaign.user.name).to eq 'Mario'
114
+ let!(:products_request_page_2) do
115
+ additional_products_request(10, 10)
123
116
  end
124
117
 
125
- it 'includes list of linked resources while fetching a single resource from one service' do
126
- stub_request(:get, "#{datastore}/feedbacks/123")
127
- .to_return(status: 200, body: {
128
- 'href' => "#{datastore}/feedbacks/-Sc4_pYNpqfsudzhtivfkA",
129
- 'campaign' => { 'href' => "#{datastore}/content-ads/51dfc5690cf271c375c5a12d" },
130
- 'user' => { 'href' => "#{datastore}/users/lakj35asdflkj1203va" }
131
- }.to_json)
132
-
133
- feedbacks = Feedback.includes(:user, campaign: [:entry, :user]).find(123)
134
- expect(feedbacks.campaign.entry.name).to eq 'Casa Ferlin'
135
- expect(feedbacks.campaign.user.name).to eq 'Mario'
136
- expect(feedbacks.user.name).to eq 'Mario'
118
+ let!(:products_request_page_3) do
119
+ additional_products_request(20, 2)
137
120
  end
138
121
 
139
- context 'include objects from known services' do
140
- let(:stub_feedback_request) do
141
- stub_request(:get, "#{datastore}/feedbacks")
142
- .to_return(status: 200, body: {
143
- items: [
144
- {
145
- 'href' => "#{datastore}/feedbacks/-Sc4_pYNpqfsudzhtivfkA",
146
- 'entry' => {
147
- 'href' => "#{datastore}/local-entries/lakj35asdflkj1203va"
148
- }
149
- }
150
- ]
151
- }.to_json)
152
- end
122
+ #
123
+ # Users
124
+ #
153
125
 
154
- let(:interceptor) { spy('interceptor') }
126
+ let!(:users_request) do
127
+ stub_request(:get, 'http://datastore/users?limit=10')
128
+ .to_return(
129
+ body: {
130
+ items: amount_of_users_1st_page.times.map do
131
+ { name: 'Hans Muster' }
132
+ end,
133
+ limit: 10,
134
+ next: { href: 'http://datastore/users?for_user_id=10&limit=10' }
135
+ }.to_json
136
+ )
137
+ end
155
138
 
156
- before do
157
- class Entry < LHS::Record
158
- endpoint '{+datastore}/local-entries/{id}'
159
- end
160
- LHC.config.interceptors = [interceptor]
161
- end
139
+ let!(:users_request_page_2) do
140
+ stub_request(:get, 'http://datastore/users?for_user_id=10&limit=10')
141
+ .to_return(
142
+ body: {
143
+ items: amount_of_users_2nd_page.times.map do
144
+ { name: 'Lisa Müller' }
145
+ end,
146
+ limit: 10,
147
+ next: { href: 'http://datastore/users?for_user_id=13&limit=10' }
148
+ }.to_json
149
+ )
150
+ end
162
151
 
163
- it 'uses interceptors for included links from known services' do
164
- stub_feedback_request
165
- stub_entry_request
166
- expect(Feedback.includes(:entry).where.first.entry.name).to eq 'Casa Ferlin'
167
- expect(interceptor).to have_received(:before_request).twice
168
- end
152
+ let!(:users_request_page_3) do
153
+ stub_request(:get, 'http://datastore/users?for_user_id=13&limit=10')
154
+ .to_return(
155
+ body: {
156
+ items: [],
157
+ limit: 10
158
+ }.to_json
159
+ )
169
160
  end
170
161
 
171
- context 'includes not present in response' do
172
- before do
173
- class Parent < LHS::Record
174
- endpoint '{+datastore}/local-parents'
175
- endpoint '{+datastore}/local-parents/{id}'
176
- end
162
+ it 'includes all linked business objects no matter pagination' do
163
+ customer = nil
177
164
 
178
- class OptionalChild < LHS::Record
179
- endpoint '{+datastore}/local-children/{id}'
180
- end
165
+ expect(lambda do
166
+ customer = Customer
167
+ .includes(:users, contracts: :products)
168
+ .find(1)
169
+ end).to output(
170
+ %r{\[WARNING\] You are loading all pages from a resource paginated with links only. As this is performed sequentially, it can result in very poor performance! \(https://github.com/local-ch/lhs#pagination-strategy-link\).}
171
+ ).to_stderr
172
+
173
+ expect(customer.users.length).to eq amount_of_users
174
+ expect(customer.contracts.length).to eq amount_of_contracts
175
+ expect(customer.contracts.first.products.length).to eq amount_of_products
176
+ expect(customer_request).to have_been_requested.at_least_once
177
+ expect(contracts_request).to have_been_requested.at_least_once
178
+ expect(contracts_request_page_2).to have_been_requested.at_least_once
179
+ expect(contracts_request_page_3).to have_been_requested.at_least_once
180
+ expect(contracts_request_page_4).to have_been_requested.at_least_once
181
+ expect(products_request).to have_been_requested.at_least_once
182
+ expect(products_request_page_2).to have_been_requested.at_least_once
183
+ expect(products_request_page_3).to have_been_requested.at_least_once
184
+ expect(users_request).to have_been_requested.at_least_once
185
+ expect(users_request_page_2).to have_been_requested.at_least_once
186
+ expect(users_request_page_3).to have_been_requested.at_least_once
187
+ end
188
+
189
+ context 'links already contain pagination parameters' do
190
+ let!(:customer_request) do
191
+ stub_request(:get, 'http://datastore/customers/1')
192
+ .to_return(
193
+ body: {
194
+ contracts: { href: 'http://datastore/customers/1/contracts?limit=5&offset=0' }
195
+ }.to_json
196
+ )
181
197
  end
182
198
 
183
- it 'handles missing but included fields in single object response' do
184
- stub_request(:get, "#{datastore}/local-parents/1")
185
- .to_return(status: 200, body: {
186
- 'href' => "#{datastore}/local-parents/1",
187
- 'name' => 'RspecName'
188
- }.to_json)
189
-
190
- parent = Parent.includes(:optional_children).find(1)
191
- expect(parent).not_to be nil
192
- expect(parent.name).to eq 'RspecName'
193
- expect(parent.optional_children).to be nil
199
+ let!(:contracts_request) do
200
+ stub_request(:get, "http://datastore/customers/1/contracts?limit=100")
201
+ .to_return(
202
+ body: {
203
+ items: 10.times.map do
204
+ {
205
+ products: { href: 'http://datastore/products' }
206
+ }
207
+ end,
208
+ limit: 10,
209
+ offset: 0,
210
+ total: amount_of_contracts
211
+ }.to_json
212
+ )
194
213
  end
195
214
 
196
- it 'handles missing but included fields in collection response' do
197
- stub_request(:get, "#{datastore}/local-parents")
198
- .to_return(status: 200, body: {
199
- items: [
200
- {
201
- 'href' => "#{datastore}/local-parents/1",
202
- 'name' => 'RspecParent'
203
- }, {
204
- 'href' => "#{datastore}/local-parents/2",
205
- 'name' => 'RspecParent2',
206
- 'optional_child' => {
207
- 'href' => "#{datastore}/local-children/1"
208
- }
209
- }
210
- ]
211
- }.to_json)
215
+ it 'overwrites existing pagination paramters if they are already contained in a string' do
216
+ expect(LHC).to receive(:request)
217
+ .with(url: "http://datastore/customers/1").and_call_original
218
+
219
+ expect(LHC).to receive(:request)
220
+ .with(url: "http://datastore/customers/1/contracts",
221
+ all: true,
222
+ params: { limit: 100 }).and_call_original
223
+
224
+ expect(LHC).to receive(:request)
225
+ .with([{ url: "http://datastore/customers/1/contracts",
226
+ all: true,
227
+ params: { limit: 10, offset: 10 } },
228
+ { url: "http://datastore/customers/1/contracts",
229
+ all: true,
230
+ params: { limit: 10, offset: 20 } },
231
+ { url: "http://datastore/customers/1/contracts",
232
+ all: true,
233
+ params: { limit: 10, offset: 30 } }]).and_call_original
234
+
235
+ customer = Customer
236
+ .includes(:contracts)
237
+ .find(1)
238
+ expect(customer.contracts.length).to eq amount_of_contracts
239
+ end
240
+ end
212
241
 
213
- stub_request(:get, "#{datastore}/local-children/1")
214
- .to_return(status: 200, body: {
215
- href: "#{datastore}/local_children/1",
216
- name: 'RspecOptionalChild1'
242
+ context 'includes for an empty array' do
243
+ before do
244
+ class Contract < LHS::Record
245
+ endpoint 'http://datastore/contracts/{id}'
246
+ end
247
+ stub_request(:get, %r{http://datastore/contracts/\d})
248
+ .to_return(body: {
249
+ options: nested_resources
217
250
  }.to_json)
218
-
219
- child = Parent.includes(:optional_child).where[1].optional_child
220
- expect(child).not_to be nil
221
- expect(child.name).to eq 'RspecOptionalChild1'
222
251
  end
223
- end
224
- end
225
252
 
226
- context 'links pointing to nowhere' do
227
- it 'sets nil for links that cannot be included' do
228
- class Feedback < LHS::Record
229
- endpoint '{+datastore}/feedbacks'
230
- endpoint '{+datastore}/feedbacks/{id}'
253
+ context 'empty array' do
254
+ let(:nested_resources) { [] }
255
+
256
+ it 'includes all in case of an empty array' do
257
+ expect(
258
+ -> { Contract.includes(:product).includes(:options).find(1) }
259
+ ).not_to raise_error
260
+ expect(
261
+ -> { Contract.includes(:product).includes(:options).find(1, 2) }
262
+ ).not_to raise_error
263
+ end
231
264
  end
232
265
 
233
- stub_request(:get, "#{datastore}/feedbacks/123")
234
- .to_return(status: 200, body: {
235
- 'href' => "#{datastore}/feedbacks/-Sc4_pYNpqfsudzhtivfkA",
236
- 'campaign' => { 'href' => "#{datastore}/content-ads/51dfc5690cf271c375c5a12d" }
237
- }.to_json)
266
+ context 'weird array without hrefs' do
267
+ before do
268
+ stub_request(:get, "http://datastore/options/1?limit=100")
269
+ .to_return(body: { type: 'REACH_EXT' }.to_json)
270
+ end
238
271
 
239
- stub_request(:get, "#{datastore}/content-ads/51dfc5690cf271c375c5a12d")
240
- .to_return(status: 404)
272
+ let(:nested_resources) { [{ href: 'http://datastore/options/1' }, { type: 'E_COMMERCE' }] }
241
273
 
242
- feedback = Feedback.includes(campaign: :entry).find(123)
243
- expect(feedback.campaign._raw.keys.count).to eq 1
244
- expect(feedback.campaign.href).to be_present
274
+ it 'includes in case of an unexpect objects within array' do
275
+ expect(
276
+ -> { Contract.includes(:product).includes(:options).find(1) }
277
+ ).not_to raise_error
278
+ expect(
279
+ -> { Contract.includes(:product).includes(:options).find(1, 2) }
280
+ ).not_to raise_error
281
+ end
282
+ end
245
283
  end
246
- end
247
284
 
248
- context 'modules' do
249
- before do
250
- module Services
251
- class LocalEntry < LHS::Record
252
- endpoint '{+datastore}/local-entries'
285
+ context 'include a known/identifiable record' do
286
+ before do
287
+ class Contract < LHS::Record
288
+ endpoint 'http://datastore/contracts/{id}'
253
289
  end
254
290
 
255
- class Feedback < LHS::Record
256
- endpoint '{+datastore}/feedbacks'
291
+ class Entry < LHS::Record
292
+ endpoint '{+datastore}/entry/v1/{id}.json'
257
293
  end
294
+
295
+ LHC.config.placeholder(:datastore, 'http://datastore')
258
296
  end
259
- stub_request(:get, "http://local.ch/v2/feedbacks?id=123")
260
- .to_return(body: [].to_json)
261
- end
262
297
 
263
- it 'works with modules' do
264
- Services::Feedback.includes(campaign: :entry).find(123)
265
- end
266
- end
298
+ let!(:customer_request) do
299
+ stub_request(:get, %r{http://datastore/customers/\d+})
300
+ .to_return(
301
+ body: {
302
+ contracts: [{ href: 'http://datastore/contracts/1' }, { href: 'http://datastore/contracts/2' }]
303
+ }.to_json
304
+ )
305
+ end
267
306
 
268
- context 'arrays' do
269
- before do
270
- class Place < LHS::Record
271
- endpoint '{+datastore}/place'
272
- endpoint '{+datastore}/place/{id}'
307
+ let!(:contracts_request) do
308
+ stub_request(:get, %r{http://datastore/contracts/\d+})
309
+ .to_return(
310
+ body: {
311
+ type: 'contract',
312
+ entry: { href: 'http://datastore/entry/v1/1.json' }
313
+ }.to_json
314
+ )
273
315
  end
274
- end
275
316
 
276
- let!(:place_request) do
277
- stub_request(:get, "#{datastore}/place/1")
278
- .to_return(body: {
279
- 'relations' => [
280
- { 'href' => "#{datastore}/place/relations/2" },
281
- { 'href' => "#{datastore}/place/relations/3" }
282
- ]
283
- }.to_json)
284
- end
317
+ let!(:entry_request) do
318
+ stub_request(:get, %r{http://datastore/entry/v1/\d+.json})
319
+ .to_return(
320
+ body: {
321
+ name: 'Casa Ferlin'
322
+ }.to_json
323
+ )
324
+ end
285
325
 
286
- let!(:relation_request_1) do
287
- stub_request(:get, "#{datastore}/place/relations/2")
288
- .to_return(body: { name: 'Category' }.to_json)
326
+ it 'loads included identifiable records without raising exceptions' do
327
+ customer = Customer.includes(contracts: :entry).find(1, 2).first
328
+ expect(customer.contracts.first.href).to eq 'http://datastore/contracts/1'
329
+ expect(customer.contracts.first.type).to eq 'contract'
330
+ expect(customer.contracts.first.entry.name).to eq 'Casa Ferlin'
331
+ end
289
332
  end
290
333
 
291
- let!(:relation_request_2) do
292
- stub_request(:get, "#{datastore}/place/relations/3")
293
- .to_return(body: { name: 'ZeFrank' }.to_json)
294
- end
334
+ context 'includes all for parallel loaded ids' do
335
+ before do
336
+ class Place < LHS::Record
337
+ endpoint 'http://datastore/places/{id}'
338
+ end
339
+ end
295
340
 
296
- it 'includes items of arrays' do
297
- place = Place.includes(:relations).find(1)
298
- expect(place.relations.first.name).to eq 'Category'
299
- expect(place.relations[1].name).to eq 'ZeFrank'
300
- end
341
+ let!(:place_request_1) do
342
+ stub_request(:get, %r{http://datastore/places/1})
343
+ .to_return(
344
+ body: {
345
+ category_relations: [
346
+ { href: 'http://datastore/category_relations/1' },
347
+ { href: 'http://datastore/category_relations/2' }
348
+ ]
349
+ }.to_json
350
+ )
351
+ end
301
352
 
302
- context 'parallel with empty links' do
303
353
  let!(:place_request_2) do
304
- stub_request(:get, "#{datastore}/place/2")
305
- .to_return(body: {
306
- 'relations' => []
307
- }.to_json)
354
+ stub_request(:get, %r{http://datastore/places/2})
355
+ .to_return(
356
+ body: {
357
+ category_relations: []
358
+ }.to_json
359
+ )
308
360
  end
309
361
 
310
- it 'loads places in parallel and merges included data properly' do
311
- place = Place.includes(:relations).find(2, 1)
312
- expect(place[0].relations.empty?).to be true
313
- expect(place[1].relations[0].name).to eq 'Category'
314
- expect(place[1].relations[1].name).to eq 'ZeFrank'
362
+ let!(:place_request_3) do
363
+ stub_request(:get, %r{http://datastore/places/3})
364
+ .to_return(
365
+ body: {
366
+ category_relations: [
367
+ { href: 'http://datastore/category_relations/1' },
368
+ { href: 'http://datastore/category_relations/3' }
369
+ ]
370
+ }.to_json
371
+ )
315
372
  end
316
- end
317
- end
318
373
 
319
- context 'empty collections' do
320
- it 'skips including empty collections' do
321
- class Place < LHS::Record
322
- endpoint '{+datastore}/place'
323
- endpoint '{+datastore}/place/{id}'
374
+ let!(:category_relation_request_1) do
375
+ stub_request(:get, %r{http://datastore/category_relations/1})
376
+ .to_return(
377
+ body: {
378
+ name: "Category 1"
379
+ }.to_json
380
+ )
324
381
  end
325
382
 
326
- stub_request(:get, "#{datastore}/place/1")
327
- .to_return(body: {
328
- 'available_products' => {
329
- "url" => "#{datastore}/place/1/products",
330
- "items" => []
331
- }
332
- }.to_json)
333
-
334
- place = Place.includes(:available_products).find(1)
335
- expect(place.available_products.empty?).to eq true
336
- end
337
- end
338
-
339
- context 'extend items with arrays' do
340
- it 'extends base items with arrays' do
341
- class Place < LHS::Record
342
- endpoint '{+datastore}/place'
343
- endpoint '{+datastore}/place/{id}'
383
+ let!(:category_relation_request_2) do
384
+ stub_request(:get, %r{http://datastore/category_relations/2})
385
+ .to_return(
386
+ body: {
387
+ name: "Category 2"
388
+ }.to_json
389
+ )
344
390
  end
345
391
 
346
- stub_request(:get, "#{datastore}/place/1")
347
- .to_return(body: {
348
- 'contracts' => {
349
- 'items' => [{ 'href' => "#{datastore}/place/1/contacts/1" }]
350
- }
351
- }.to_json)
392
+ let!(:category_relation_request_3) do
393
+ stub_request(:get, %r{http://datastore/category_relations/3})
394
+ .to_return(
395
+ body: {
396
+ name: "Category 3"
397
+ }.to_json
398
+ )
399
+ end
352
400
 
353
- stub_request(:get, "#{datastore}/place/1/contacts/1")
354
- .to_return(body: {
355
- 'products' => { 'href' => "#{datastore}/place/1/contacts/1/products" }
356
- }.to_json)
401
+ let(:category_name) { 'Category Relation' }
357
402
 
358
- place = Place.includes(:contracts).find(1)
359
- expect(place.contracts.first.products.href).to eq "#{datastore}/place/1/contacts/1/products"
403
+ it 'requests places in parallel and includes category relation' do
404
+ places = Place.includes(:category_relations).find(1, 2, 3)
405
+ expect(places[0].category_relations[0].name).to eq 'Category 1'
406
+ expect(places[0].category_relations[1].name).to eq 'Category 2'
407
+ expect(places[2].category_relations[0].name).to eq 'Category 1'
408
+ expect(places[2].category_relations[1].name).to eq 'Category 3'
409
+ end
360
410
  end
361
411
  end
362
412
 
363
- context 'unexpanded response when requesting the included collection' do
413
+ context 'Linked resources' do
364
414
  before do
365
- class Customer < LHS::Record
366
- endpoint '{+datastore}/customer/{id}'
415
+ stub_request(:get, 'http://datastore/places/1/contracts?offset=0&limit=10')
416
+ .to_return(
417
+ body: {
418
+ href: "http://datastore/v2/places/1/contracts?offset=0&limit=10",
419
+ items: [{ href: "http://datastore/v2/contracts/1" }],
420
+ offset: 0,
421
+ limit: 10,
422
+ total: 10
423
+ }.to_json
424
+ )
425
+
426
+ stub_request(:get, "http://datastore/v2/contracts/1")
427
+ .to_return(
428
+ body: {
429
+ customer: { name: 'Swisscom Directories AG' }
430
+ }.to_json
431
+ )
432
+
433
+ stub_request(:get, 'http://datastore/places/1?limit=1')
434
+ .to_return(
435
+ body: { href: 'http://datastore/places/1', contracts: { href: 'http://datastore/places/1/contracts?offset=0&limit=10' } }.to_json
436
+ )
437
+
438
+ class Place < LHS::Record
439
+ endpoint 'http://datastore/places/{id}'
367
440
  end
368
- end
369
441
 
370
- let!(:customer_request) do
371
- stub_request(:get, "#{datastore}/customer/1")
372
- .to_return(body: {
373
- places: {
374
- href: "#{datastore}/places"
375
- }
376
- }.to_json)
442
+ class Contract < LHS::Record
443
+ endpoint 'http://datastore/places/{place_id}/contracts'
444
+ end
377
445
  end
378
446
 
379
- let!(:places_request) do
380
- stub_request(:get, "#{datastore}/places")
381
- .to_return(body: {
382
- items: [{ href: "#{datastore}/places/1" }]
383
- }.to_json)
447
+ it 'does not use the root record endpoints when including nested records' do
448
+ place = Place
449
+ .includes(:contracts)
450
+ .find_by(id: 1)
451
+ expect(place.contracts.first.customer.name).to eq 'Swisscom Directories AG'
384
452
  end
453
+ end
385
454
 
386
- let!(:place_request) do
387
- stub_request(:get, "#{datastore}/places/1")
388
- .to_return(body: {
389
- name: 'Casa Ferlin'
390
- }.to_json)
391
- end
455
+ context 'nested includes all' do
456
+ context 'with optional children' do
392
457
 
393
- it 'loads the collection and the single items, if not already expanded' do
394
- place = Customer.includes(:places).find(1).places.first
395
- assert_requested(place_request)
396
- expect(place.name).to eq 'Casa Ferlin'
397
- end
458
+ let(:favorites_request_stub) do
459
+ stub_request(:get, %r{http://datastore/favorites})
460
+ .to_return(
461
+ body: {
462
+ items: [{
463
+ href: "http://datastore/favorites/1",
464
+ place: {
465
+ href: "http://datastore/places/1"
466
+ }
467
+ }, {
468
+ href: "http://datastore/favorite/2",
469
+ place: {
470
+ href: "http://datastore/places/2"
471
+ }
472
+ }, {
473
+ href: "http://datastore/favorite/3",
474
+ place: {
475
+ href: "http://datastore/places/3"
476
+ }
477
+ }],
478
+ total: 3,
479
+ offset: 0,
480
+ limit: 100
481
+ }.to_json
482
+ )
483
+ end
398
484
 
399
- context 'forwarding options' do
400
- let!(:places_request) do
401
- stub_request(:get, "#{datastore}/places")
402
- .with(headers: { 'Authorization' => 'Bearer 123' })
485
+ let(:place1_request_stub) do
486
+ stub_request(:get, %r{http://datastore/places/1})
403
487
  .to_return(
404
488
  body: {
405
- items: [{ href: "#{datastore}/places/1" }]
489
+ href: "http://datastore/places/1",
490
+ name: 'Place 1',
491
+ contracts: {
492
+ href: "http://datastore/places/1/contracts"
493
+ }
406
494
  }.to_json
407
495
  )
408
496
  end
409
497
 
410
- let!(:place_request) do
411
- stub_request(:get, "#{datastore}/places/1")
412
- .with(headers: { 'Authorization' => 'Bearer 123' })
498
+ let(:place2_request_stub) do
499
+ stub_request(:get, %r{http://datastore/places/2})
413
500
  .to_return(
414
501
  body: {
415
- name: 'Casa Ferlin'
502
+ href: "http://datastore/places/2",
503
+ name: 'Place 2'
416
504
  }.to_json
417
505
  )
418
506
  end
419
507
 
420
- it 'forwards options used to expand those unexpanded items' do
421
- place = Customer
422
- .includes(:places)
423
- .references(places: { headers: { 'Authorization' => 'Bearer 123' } })
424
- .find(1)
425
- .places.first
426
- assert_requested(place_request)
427
- expect(place.name).to eq 'Casa Ferlin'
508
+ let(:place3_request_stub) do
509
+ stub_request(:get, %r{http://datastore/places/3})
510
+ .to_return(
511
+ body: {
512
+ href: "http://datastore/places/3",
513
+ name: 'Place 3',
514
+ contracts: {
515
+ href: "http://datastore/places/3/contracts"
516
+ }
517
+ }.to_json
518
+ )
428
519
  end
429
- end
430
- end
431
520
 
432
- context 'includes with options' do
433
- before do
434
- class Customer < LHS::Record
435
- endpoint '{+datastore}/customers/{id}'
436
- endpoint '{+datastore}/customers'
521
+ let(:contracts_request_for_place1_stub) do
522
+ stub_request(:get, %r{http://datastore/places/1/contracts})
523
+ .to_return(
524
+ body: {
525
+ items: [{
526
+ href: "http://datastore/places/1/contracts/1",
527
+ name: 'Contract 1'
528
+ }],
529
+ total: 1,
530
+ offset: 0,
531
+ limit: 10
532
+ }.to_json
533
+ )
437
534
  end
438
535
 
439
- class Place < LHS::Record
440
- endpoint '{+datastore}/places'
536
+ let(:contracts_request_for_place3_stub) do
537
+ stub_request(:get, %r{http://datastore/places/3/contracts})
538
+ .to_return(
539
+ body: {
540
+ items: [{
541
+ href: "http://datastore/places/3/contracts/1",
542
+ name: 'Contract 3'
543
+ }],
544
+ total: 1,
545
+ offset: 0,
546
+ limit: 10
547
+ }.to_json
548
+ )
441
549
  end
442
550
 
443
- stub_request(:get, "#{datastore}/places?forwarded_params=123")
444
- .to_return(body: {
445
- 'items' => [{ id: 1 }]
446
- }.to_json)
447
- end
551
+ before do
552
+ class Favorite < LHS::Record
553
+ endpoint 'http://datastore/favorites'
554
+ end
448
555
 
449
- it 'forwards includes options to requests made for those includes' do
450
- stub_request(:get, "#{datastore}/customers/1")
451
- .to_return(body: {
452
- 'places' => {
453
- 'href' => "#{datastore}/places"
454
- }
455
- }.to_json)
456
- customer = Customer
457
- .includes(:places)
458
- .references(places: { params: { forwarded_params: 123 } })
459
- .find(1)
460
- expect(customer.places.first.id).to eq 1
461
- end
556
+ class Place < LHS::Record
557
+ endpoint 'http://datastore/places/{id}'
558
+ end
462
559
 
463
- it 'is chain-able' do
464
- stub_request(:get, "#{datastore}/customers?name=Steve")
465
- .to_return(body: [
466
- 'places' => {
467
- 'href' => "#{datastore}/places"
468
- }
469
- ].to_json)
470
- customers = Customer
471
- .where(name: 'Steve')
472
- .references(places: { params: { forwarded_params: 123 } })
473
- .includes(:places)
474
- expect(customers.first.places.first.id).to eq 1
475
- end
476
- end
560
+ class Contract < LHS::Record
561
+ endpoint 'http://datastore/places/{place_id}/contracts'
562
+ end
477
563
 
478
- context 'more complex examples' do
479
- before do
480
- class Place < LHS::Record
481
- endpoint 'http://datastore/places/{id}'
564
+ favorites_request_stub
565
+ place1_request_stub
566
+ place2_request_stub
567
+ place3_request_stub
568
+ contracts_request_for_place1_stub
569
+ contracts_request_for_place3_stub
482
570
  end
483
- end
484
571
 
485
- it 'forwards complex references' do
486
- stub_request(:get, "http://datastore/places/123?limit=1&forwarded_params=for_place")
487
- .to_return(body: {
488
- 'contracts' => {
489
- 'href' => "http://datastore/places/123/contracts"
490
- }
491
- }.to_json)
492
- stub_request(:get, "http://datastore/places/123/contracts?forwarded_params=for_contracts")
493
- .to_return(body: {
494
- href: "http://datastore/places/123/contracts?forwarded_params=for_contracts",
495
- items: [
496
- { product: { 'href' => "http://datastore/products/llo" } }
497
- ]
498
- }.to_json)
499
- stub_request(:get, "http://datastore/products/llo?forwarded_params=for_product")
500
- .to_return(body: {
501
- 'href' => "http://datastore/products/llo",
502
- 'name' => 'Local Logo'
503
- }.to_json)
504
- place = Place
505
- .options(params: { forwarded_params: 'for_place' })
506
- .includes(contracts: :product)
507
- .references(
508
- contracts: {
509
- params: { forwarded_params: 'for_contracts' },
510
- product: { params: { forwarded_params: 'for_product' } }
511
- }
512
- )
513
- .find_by(id: '123')
514
- expect(
515
- place.contracts.first.product.name
516
- ).to eq 'Local Logo'
517
- end
572
+ it 'includes nested objects when they exist' do
573
+ favorites = Favorite.includes(:place).includes(place: :contracts).all
518
574
 
519
- it 'expands empty arrays' do
520
- stub_request(:get, "http://datastore/places/123")
521
- .to_return(body: {
522
- 'contracts' => {
523
- 'href' => "http://datastore/places/123/contracts"
524
- }
525
- }.to_json)
526
- stub_request(:get, "http://datastore/places/123/contracts")
527
- .to_return(body: {
528
- href: "http://datastore/places/123/contracts",
529
- items: []
530
- }.to_json)
531
- place = Place.includes(:contracts).find('123')
532
- expect(place.contracts.collection?).to eq true
533
- expect(
534
- place.contracts.as_json
535
- ).to eq('href' => 'http://datastore/places/123/contracts', 'items' => [])
536
- expect(place.contracts.to_a).to eq([])
537
- end
538
- end
575
+ expect(favorites.first.place.name).to eq('Place 1')
576
+ expect(favorites.first.place.contracts.first.name).to eq('Contract 1')
577
+ end
539
578
 
540
- context 'include and merge arrays when calling find in parallel' do
541
- before do
542
- class Place < LHS::Record
543
- endpoint 'http://datastore/places/{id}'
579
+ it 'does not include nested objects when they are not there' do
580
+ favorites = Favorite.includes(:place).includes(place: :contracts).all
581
+
582
+ expect(favorites[1].place.name).to eq('Place 2')
583
+ expect(favorites[1].place.contracts).to be(nil)
544
584
  end
545
- stub_request(:get, 'http://datastore/places/1')
546
- .to_return(body: {
547
- category_relations: [{ href: 'http://datastore/category/1' }, { href: 'http://datastore/category/2' }]
548
- }.to_json)
549
- stub_request(:get, 'http://datastore/places/2')
550
- .to_return(body: {
551
- category_relations: [{ href: 'http://datastore/category/2' }, { href: 'http://datastore/category/1' }]
552
- }.to_json)
553
- stub_request(:get, "http://datastore/category/1").to_return(body: { name: 'Food' }.to_json)
554
- stub_request(:get, "http://datastore/category/2").to_return(body: { name: 'Drinks' }.to_json)
555
- end
556
585
 
557
- it 'includes and merges linked resources in case of an array of links' do
558
- places = Place
559
- .includes(:category_relations)
560
- .find(1, 2)
561
- expect(places[0].category_relations[0].name).to eq 'Food'
562
- expect(places[1].category_relations[0].name).to eq 'Drinks'
563
- end
564
- end
586
+ it 'does include and merges objects after nil objects in collections' do
587
+ favorites = Favorite.includes(:place).includes(place: :contracts).all
565
588
 
566
- context 'single href with array response' do
567
- it 'extends base items with arrays' do
568
- class Sector < LHS::Record
569
- endpoint '{+datastore}/sectors'
570
- endpoint '{+datastore}/sectors/{id}'
571
- end
572
-
573
- stub_request(:get, "#{datastore}/sectors")
574
- .with(query: hash_including(key: 'my_service'))
575
- .to_return(body: [
576
- {
577
- href: "#{datastore}/sectors/1",
578
- services: {
579
- href: "#{datastore}/sectors/1/services"
580
- },
581
- keys: [
582
- {
583
- key: 'my_service',
584
- language: 'de'
585
- }
586
- ]
587
- }
588
- ].to_json)
589
-
590
- stub_request(:get, "#{datastore}/sectors/1/services")
591
- .to_return(body: [
592
- {
593
- href: "#{datastore}/services/s1",
594
- price_in_cents: 9900,
595
- key: 'my_service_service_1'
596
- },
597
- {
598
- href: "#{datastore}/services/s2",
599
- price_in_cents: 19900,
600
- key: 'my_service_service_2'
601
- }
602
- ].to_json)
603
-
604
- sector = Sector.includes(:services).find_by(key: 'my_service')
605
- expect(sector.services.length).to eq 2
606
- expect(sector.services.first.key).to eq 'my_service_service_1'
589
+ expect(favorites.last.place.name).to eq('Place 3')
590
+ expect(favorites.last.place.contracts.first.name).to eq('Contract 3')
591
+ end
607
592
  end
608
593
  end
609
594
 
610
- context 'include for POST/create' do
595
+ context 'includes collection trough single item' do
611
596
 
612
597
  before do
613
- class Record < LHS::Record
614
- endpoint 'https://records'
598
+ class Place < LHS::Record
599
+ endpoint 'https://places/{id}'
615
600
  end
616
- stub_request(:post, 'https://records/')
617
- .with(body: { color: 'blue' }.to_json)
601
+
602
+ stub_request(:get, 'https://places/1')
603
+ .to_return(
604
+ body: {
605
+ customer: { href: 'https://customers/1' }
606
+ }.to_json
607
+ )
608
+
609
+ stub_request(:get, 'https://customers/1?limit=100')
618
610
  .to_return(
619
611
  body: {
620
- color: 'blue',
621
- alternative_categories: [
622
- { href: 'https://categories/blue' }
623
- ]
612
+ addresses: { 'href': 'https://customer/1/addresses' }
624
613
  }.to_json
625
614
  )
626
- stub_request(:get, 'https://categories/blue')
615
+
616
+ stub_request(:get, 'https://customer/1/addresses?limit=100')
627
617
  .to_return(
628
618
  body: {
629
- name: 'blue'
619
+ items: [
620
+ { city: 'Zurich', no: 1 },
621
+ { city: 'Zurich', no: 2 }
622
+ ],
623
+ total: 2
630
624
  }.to_json
631
625
  )
632
626
  end
633
627
 
634
- it 'includes the resources from the post response' do
635
- records = Record.includes(:alternative_categories).create(color: 'blue')
636
- expect(records.alternative_categories.first.name).to eq 'blue'
628
+ it 'includes a collection trough a single item without exceptions' do
629
+ place = Place
630
+ .includes(customer: :addresses)
631
+ .find(1)
632
+ expect(place.customer.addresses.map(&:no)).to eq [1, 2]
637
633
  end
638
634
  end
639
635
 
640
- context 'nested within another structure' do
636
+ context 'does not fail including all linked resources' do
637
+
641
638
  before do
642
- class Place < LHS::Record
643
- endpoint 'https://places/{id}'
639
+ class CustomerOnboardingToken < LHS::Record
640
+ endpoint 'https://token/{id}'
644
641
  end
645
- stub_request(:get, "https://places/1")
646
- .to_return(body: {
647
- customer: {
648
- salesforce: {
649
- href: 'https://salesforce/customers/1'
650
- }
651
- }
652
- }.to_json)
653
- end
654
-
655
- let!(:nested_request) do
656
- stub_request(:get, "https://salesforce/customers/1")
657
- .to_return(body: {
658
- name: 'Steve'
659
- }.to_json)
660
- end
661
642
 
662
- it 'includes data that has been nested in an additional structure' do
663
- place = Place.includes(customer: :salesforce).find(1)
664
- expect(nested_request).to have_been_requested
665
- expect(place.customer.salesforce.name).to eq 'Steve'
666
- end
667
-
668
- context 'included data has a configured record endpoint option' do
669
- before do
670
- class SalesforceCustomer < LHS::Record
671
- endpoint 'https://salesforce/customers/{id}', headers: { 'Authorization': 'Bearer 123' }
672
- end
643
+ class Place < LHS::Record
644
+ endpoint 'https://places/{id}'
673
645
  end
674
646
 
675
- let!(:nested_request) do
676
- stub_request(:get, "https://salesforce/customers/1")
677
- .with(headers: { 'Authorization' => 'Bearer 123' })
678
- .to_return(body: {
679
- name: 'Steve'
680
- }.to_json)
647
+ class AvailableAsset < LHS::Record
648
+ endpoint 'https://assets'
681
649
  end
682
650
 
683
- it 'includes data that has been nested in an additional structure' do
684
- place = Place.includes(customer: :salesforce).find(1)
685
- expect(nested_request).to have_been_requested
686
- expect(place.customer.salesforce.name).to eq 'Steve'
687
- end
688
- end
689
- end
651
+ stub_request(:get, 'https://token/1')
652
+ .to_return(
653
+ body: {
654
+ places: [{ href: 'https://places/1' }]
655
+ }.to_json
656
+ )
690
657
 
691
- context 'include empty structures' do
692
- before do
693
- class Place < LHS::Record
694
- endpoint 'https://places/{id}'
695
- end
696
- stub_request(:get, "https://places/1")
697
- .to_return(body: {
698
- id: '123'
699
- }.to_json)
700
- end
658
+ stub_request(:get, 'https://places/1?limit=100')
659
+ .to_return(
660
+ body: {
661
+ available_assets: { 'href': 'https://assets?limit=10&offset=0' }
662
+ }.to_json
663
+ )
701
664
 
702
- it 'skips includes when there is nothing and also does not raise an exception' do
703
- expect(-> {
704
- Place.includes(contracts: :product).find(1)
705
- }).not_to raise_exception
706
- end
707
- end
665
+ stub_request(:get, 'https://assets?limit=10&offset=0')
666
+ .to_return(
667
+ body: {
668
+ items: 10.times.map { { asset_code: 'CATEGORIES' } },
669
+ total: 17,
670
+ offset: 0,
671
+ limit: 10
672
+ }.to_json
673
+ )
708
674
 
709
- context 'include partially empty structures' do
710
- before do
711
- class Place < LHS::Record
712
- endpoint 'https://places/{id}'
713
- end
714
- stub_request(:get, "https://places/1")
715
- .to_return(body: {
716
- id: '123',
717
- customer: {}
718
- }.to_json)
675
+ stub_request(:get, 'https://assets?limit=10&offset=10')
676
+ .to_return(
677
+ body: {
678
+ items: 7.times.map { { asset_code: 'CATEGORIES' } },
679
+ total: 17,
680
+ offset: 0,
681
+ limit: 10
682
+ }.to_json
683
+ )
719
684
  end
720
685
 
721
- it 'skips includes when there is nothing and also does not raise an exception' do
722
- expect(-> {
723
- Place.includes(customer: :salesforce).find(1)
724
- }).not_to raise_exception
686
+ it 'includes a collection trough a single item without exceptions' do
687
+ token = CustomerOnboardingToken
688
+ .includes(places: :available_assets)
689
+ .find(1)
690
+ expect(token.places.first.available_assets.length).to eq 17
725
691
  end
726
692
  end
727
693
  end