hoodoo 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,448 @@
1
+ class RSpecTestServiceExoticStubImplementation < Hoodoo::Services::Implementation
2
+ def list( context )
3
+ context.response.set_estimated_resources( [], 88 )
4
+ context.response.set_resources( [], 99 )
5
+ end
6
+ end
7
+
8
+ class RSpecTestServiceExoticStubInterface < Hoodoo::Services::Interface
9
+ interface :Version do
10
+ endpoint :version, RSpecTestServiceExoticStubImplementation
11
+ version 2
12
+ end
13
+ end
14
+
15
+ class RSpecTestServiceExoticStub < Hoodoo::Services::Service
16
+ comprised_of RSpecTestServiceExoticStubInterface
17
+ end
18
+
19
+ shared_examples 'an AMQP-based middleware/client endpoint' do
20
+ before :each do
21
+ @old_queue = ENV[ 'AMQ_URI' ]
22
+ ENV[ 'AMQ_URI' ] = 'amqp://test:test@127.0.0.1'
23
+ @mw = Hoodoo::Services::Middleware.new( RSpecTestServiceExoticStub.new )
24
+
25
+ @cvar = false
26
+ if Hoodoo::Services::Middleware.class_variable_defined?( '@@alchemy' )
27
+ @cvar = true
28
+ @cvar_val = Hoodoo::Services::Middleware.class_variable_get( '@@alchemy' )
29
+ end
30
+
31
+ # Need to blow away the discoverer's local cache or the simulation will
32
+ # never attempt to run on queue.
33
+
34
+ discoverer = @mw.instance_variable_get( '@discoverer' )
35
+ discoverer.instance_variable_set( '@known_local_resources', {} ) # Hack for test!
36
+ end
37
+
38
+ after :each do
39
+ ENV[ 'AMQ_URI' ] = @old_queue
40
+
41
+ if Hoodoo::Services::Middleware.class_variable_defined?( '@@alchemy' )
42
+ if @cvar == true
43
+ Hoodoo::Services::Middleware.class_variable_set( '@@alchemy', @cvar_val )
44
+ else
45
+ Hoodoo::Services::Middleware.remove_class_variable( '@@alchemy' )
46
+ end
47
+ end
48
+ end
49
+
50
+ it 'knows it is on-queue' do
51
+ expect( Hoodoo::Services::Middleware.on_queue? ).to eq( true )
52
+ end
53
+
54
+ it 'returns known queue endpoint locations' do
55
+ location = @mw.instance_variable_get( '@discoverer' ).discover( :Version, 2 )
56
+ expect( location ).to be_a( Hoodoo::Services::Discovery::ForAMQP )
57
+ expect( location.routing_path ).to eq( '/2/Version' )
58
+ end
59
+
60
+ context 'calling Alchemy' do
61
+ before :each do
62
+ mw = Hoodoo::Services::Middleware.new( RSpecTestServiceExoticStub.new )
63
+ @interaction = Hoodoo::Services::Middleware::Interaction.new(
64
+ {},
65
+ mw,
66
+ Hoodoo::Services::Middleware.test_session()
67
+ )
68
+ @interaction.target_interface = OpenStruct.new
69
+ @interaction.context.request.locale = 'fr'
70
+
71
+ @mock_alchemy = OpenStruct.new
72
+ Hoodoo::Services::Middleware.class_variable_set( '@@alchemy', @mock_alchemy )
73
+ end
74
+
75
+ def run_expectations( action, full_path, mock_method, mock_query, mock_remote, mock_response )
76
+ expect_any_instance_of( Hoodoo::Services::Discovery::ByFlux ).to receive(
77
+ :discover_remote
78
+ ).and_return(
79
+ mock_remote
80
+ )
81
+
82
+ if mock_query
83
+ mock_query = Hoodoo::Utilities.stringify( mock_query )
84
+ mock_query[ 'search' ] = URI.encode_www_form( mock_query[ 'search' ] ) if ( mock_query[ 'search' ].is_a?( ::Hash ) )
85
+ mock_query[ 'filter' ] = URI.encode_www_form( mock_query[ 'filter' ] ) if ( mock_query[ 'filter' ].is_a?( ::Hash ) )
86
+ end
87
+
88
+ expect( @mock_alchemy ).to receive( :send_request_to_resource ).once do | message |
89
+ expect( message ).to eq( {
90
+ 'scheme' => 'http',
91
+ 'verb' => mock_method,
92
+
93
+ 'host' => 'localhost',
94
+ 'port' => 80,
95
+ 'path' => full_path,
96
+ 'query' => mock_query,
97
+
98
+ 'session_id' => @interaction.context.session.session_id,
99
+ 'body' => action == :create || action == :update ? '{}' : '',
100
+ 'headers' => {
101
+ 'Content-Type' => 'application/json; charset=utf-8',
102
+ 'Content-Language' => 'fr',
103
+ 'Accept-Language' => 'fr',
104
+ 'X-Interaction-ID' => @interaction.interaction_id,
105
+ 'X-Session-ID' => @interaction.context.session.session_id
106
+ },
107
+ } )
108
+ end.and_return( mock_response )
109
+ end
110
+
111
+ # All the other tests in this section run with @mw having no
112
+ # local services "as far as it is concerned" via the before-each
113
+ # code above. We want to make sure that service announcement when
114
+ # on-queue *does* announce locally (because at one point it did
115
+ # not, which was a bug) so this test provides that coverage.
116
+ #
117
+ it 'runs local discovery unless that is knocked out' do
118
+
119
+ # @mw has local discovery knocked out, so build a new
120
+ # one that doesn't. This will have the local discovery data
121
+ # available still.
122
+ #
123
+ @mw = Hoodoo::Services::Middleware.new( RSpecTestServiceExoticStub.new )
124
+
125
+ mock_path = '/2/Version/'
126
+ mock_remote = Hoodoo::Services::Discovery::ForAMQP.new(
127
+ resource: 'Version',
128
+ version: 2
129
+ )
130
+
131
+ # We expect local discovery, so no discover_remote call.
132
+
133
+ expect_any_instance_of( Hoodoo::Services::Discovery::ByFlux ).to_not receive( :discover_remote )
134
+ endpoint = @mw.inter_resource_endpoint_for( 'Version', 2, @interaction )
135
+
136
+ # The endpoint should've been called locally; the implementation at
137
+ # the top of this file sets an empty array with dataset size 99 *and*
138
+ # an estimated dataset size of 88.
139
+
140
+ mock_result = endpoint.list()
141
+ expect( mock_result ).to be_empty
142
+ expect( mock_result.dataset_size ).to eq( 99 )
143
+ expect( mock_result.estimated_dataset_size ).to eq( 88 )
144
+ end
145
+
146
+ it 'complains about a missing Alchemy instance' do
147
+ mock_path = '/2/Version/'
148
+
149
+ mock_remote = Hoodoo::Services::Discovery::ForAMQP.new(
150
+ resource: 'Version',
151
+ version: 2
152
+ )
153
+
154
+ expect_any_instance_of( Hoodoo::Services::Discovery::ByFlux ).to receive(
155
+ :discover_remote
156
+ ).and_return(
157
+ mock_remote
158
+ )
159
+
160
+ endpoint = @mw.inter_resource_endpoint_for( 'Version', 2, @interaction )
161
+
162
+ # Fragile! Hack for test only.
163
+ endpoint.instance_variable_get( '@wrapped_endpoint' ).alchemy = nil
164
+
165
+ mock_response = {
166
+ 'status_code' => 200,
167
+ 'body' => '{"_data":[]}'
168
+ }
169
+
170
+ expect {
171
+ mock_result = endpoint.list()
172
+ }.to raise_error( RuntimeError, 'Hoodoo::Client::Endpoint::AMQP cannot be used unless an Alchemy instance has been provided' )
173
+ end
174
+
175
+ it 'calls #list over Alchemy and handles 200' do
176
+ mock_path = '/2/Version'
177
+ mock_method = 'GET'
178
+ mock_query = { :search => { :foo => :bar } }
179
+
180
+ mock_remote = Hoodoo::Services::Discovery::ForAMQP.new(
181
+ resource: 'Version',
182
+ version: 2
183
+ )
184
+
185
+ mock_response = {
186
+ 'status_code' => 200,
187
+ 'body' => '{"_data":[]}'
188
+ }
189
+
190
+ run_expectations( :list, mock_path + '/', mock_method, mock_query, mock_remote, mock_response )
191
+
192
+ endpoint = @mw.inter_resource_endpoint_for( 'Version', 2, @interaction )
193
+ mock_result = endpoint.list( mock_query )
194
+
195
+ expect( mock_result ).to eq( Hoodoo::Client::AugmentedArray.new )
196
+ end
197
+
198
+ it 'calls #show over Alchemy and handles 200' do
199
+ mock_path = '/2/Version'
200
+ mock_method = 'GET'
201
+ mock_query = { :search => { :foo => :bar } }
202
+
203
+ mock_remote = Hoodoo::Services::Discovery::ForAMQP.new(
204
+ resource: 'Version',
205
+ version: 2
206
+ )
207
+
208
+ mock_response = {
209
+ 'status_code' => 200,
210
+ 'body' => '{}'
211
+ }
212
+
213
+ run_expectations( :show, mock_path + '/ident', mock_method, mock_query, mock_remote, mock_response )
214
+
215
+ endpoint = @mw.inter_resource_endpoint_for( 'Version', 2, @interaction )
216
+ mock_result = endpoint.show( 'ident', mock_query )
217
+
218
+ expect( mock_result ).to eq( Hoodoo::Client::AugmentedHash.new )
219
+ end
220
+
221
+ it 'calls #create over Alchemy and handles 200' do
222
+ mock_path = '/2/Version'
223
+ mock_method = 'POST'
224
+ mock_query = { :search => { :foo => :bar } }
225
+
226
+ mock_remote = Hoodoo::Services::Discovery::ForAMQP.new(
227
+ resource: 'Version',
228
+ version: 2
229
+ )
230
+
231
+ mock_response = {
232
+ 'status_code' => 200,
233
+ 'body' => '{}'
234
+ }
235
+
236
+ run_expectations( :create, mock_path + '/', mock_method, mock_query, mock_remote, mock_response )
237
+
238
+ endpoint = @mw.inter_resource_endpoint_for( 'Version', 2, @interaction )
239
+ mock_result = endpoint.create( {}, mock_query )
240
+
241
+ expect( mock_result ).to eq( Hoodoo::Client::AugmentedHash.new )
242
+ end
243
+
244
+ it 'calls #update over Alchemy and handles 200' do
245
+ mock_path = '/2/Version'
246
+ mock_method = 'PATCH'
247
+ mock_query = { :search => { :foo => :bar } }
248
+
249
+ mock_remote = Hoodoo::Services::Discovery::ForAMQP.new(
250
+ resource: 'Version',
251
+ version: 2
252
+ )
253
+
254
+ mock_response = {
255
+ 'status_code' => 200,
256
+ 'body' => '{}'
257
+ }
258
+
259
+ run_expectations( :update, mock_path + '/ident', mock_method, mock_query, mock_remote, mock_response )
260
+
261
+ endpoint = @mw.inter_resource_endpoint_for( 'Version', 2, @interaction )
262
+ mock_result = endpoint.update( 'ident', {}, mock_query )
263
+
264
+ expect( mock_result ).to eq( Hoodoo::Client::AugmentedHash.new )
265
+ end
266
+
267
+ it 'calls #delete over Alchemy and handles 200' do
268
+ mock_path = '/2/Version'
269
+ mock_method = 'DELETE'
270
+ mock_query = { :search => { :foo => :bar } }
271
+
272
+ mock_remote = Hoodoo::Services::Discovery::ForAMQP.new(
273
+ resource: 'Version',
274
+ version: 2
275
+ )
276
+
277
+ mock_response = {
278
+ 'status_code' => 200,
279
+ 'body' => '{}'
280
+ }
281
+
282
+ run_expectations( :delete, mock_path + '/ident', mock_method, mock_query, mock_remote, mock_response )
283
+
284
+ endpoint = @mw.inter_resource_endpoint_for( 'Version', 2, @interaction )
285
+ mock_result = endpoint.delete( 'ident', mock_query )
286
+
287
+ expect( mock_result ).to eq( Hoodoo::Client::AugmentedHash.new )
288
+ end
289
+
290
+ it 'calls #show over Alchemy and handles 408' do
291
+ mock_path = '/2/Version'
292
+ mock_method = 'GET'
293
+ mock_query = { :search => { :foo => :bar } }
294
+
295
+ mock_remote = Hoodoo::Services::Discovery::ForAMQP.new(
296
+ resource: 'Version',
297
+ version: 2
298
+ )
299
+
300
+ mock_response = AlchemyFlux::TimeoutError
301
+
302
+ run_expectations( :show, mock_path + '/ident', mock_method, mock_query, mock_remote, mock_response )
303
+
304
+ endpoint = @mw.inter_resource_endpoint_for( 'Version', 2, @interaction )
305
+ mock_result = endpoint.show( 'ident', mock_query )
306
+
307
+ expect( mock_result ).to be_a( Hoodoo::Client::AugmentedHash )
308
+ expect( mock_result.platform_errors ).to_not be_nil
309
+
310
+ errors = mock_result.platform_errors.render( Hoodoo::UUID.generate )
311
+
312
+ expect( errors ).to have_key( 'errors' )
313
+ expect( errors[ 'errors' ] ).to be_a( Array )
314
+ expect( errors[ 'errors' ][ 0 ] ).to have_key( 'code' )
315
+ expect( errors[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.timeout' )
316
+ end
317
+
318
+ it 'calls #show over Alchemy and handles 404' do
319
+ mock_path = '/2/Version'
320
+ mock_method = 'GET'
321
+ mock_query = { :search => { :foo => :bar } }
322
+
323
+ mock_remote = Hoodoo::Services::Discovery::ForAMQP.new(
324
+ resource: 'Version',
325
+ version: 2
326
+ )
327
+
328
+ mock_response = AlchemyFlux::MessageNotDeliveredError
329
+
330
+ run_expectations( :show, mock_path + '/ident', mock_method, mock_query, mock_remote, mock_response )
331
+
332
+ endpoint = @mw.inter_resource_endpoint_for( 'Version', 2, @interaction )
333
+ mock_result = endpoint.show( 'ident', mock_query )
334
+
335
+ expect( mock_result ).to be_a( Hoodoo::Client::AugmentedHash )
336
+ expect( mock_result.platform_errors ).to_not be_nil
337
+
338
+ errors = mock_result.platform_errors.render( Hoodoo::UUID.generate )
339
+
340
+ expect( errors ).to have_key( 'errors' )
341
+ expect( errors[ 'errors' ] ).to be_a( Array )
342
+ expect( errors[ 'errors' ][ 0 ] ).to have_key( 'code' )
343
+ expect( errors[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.not_found' )
344
+ end
345
+
346
+ it 'calls #show over Alchemy and handles unusual HTTP status codes' do
347
+ mock_path = '/2/Version'
348
+ mock_method = 'GET'
349
+ mock_query = { :search => { :foo => :bar } }
350
+
351
+ mock_remote = Hoodoo::Services::Discovery::ForAMQP.new(
352
+ resource: 'Version',
353
+ version: 2
354
+ )
355
+
356
+ bad_body_data = '499 Unusual Code'
357
+ mock_response = {
358
+ 'status_code' => 499,
359
+ 'body' => bad_body_data
360
+ }
361
+
362
+ run_expectations( :show, mock_path + '/ident', mock_method, mock_query, mock_remote, mock_response )
363
+
364
+ endpoint = @mw.inter_resource_endpoint_for( 'Version', 2, @interaction )
365
+ mock_result = endpoint.show( 'ident', mock_query )
366
+
367
+ expect( mock_result ).to be_a( Hoodoo::Client::AugmentedHash )
368
+ expect( mock_result.platform_errors ).to_not be_nil
369
+
370
+ errors = mock_result.platform_errors.render( Hoodoo::UUID.generate )
371
+
372
+ expect( errors ).to have_key( 'errors' )
373
+ expect( errors[ 'errors' ] ).to be_a( Array )
374
+ expect( errors[ 'errors' ][ 0 ] ).to_not be_nil
375
+
376
+ expect( errors[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.fault' )
377
+ expect( errors[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'Unexpected raw HTTP status code 499 with non-JSON response' )
378
+ expect( errors[ 'errors' ][ 0 ][ 'reference' ] ).to eq( "#{ bad_body_data }" )
379
+ end
380
+
381
+ it 'calls #show over Alchemy and handles unrecognised return types' do
382
+ mock_path = '/2/Version'
383
+ mock_method = 'GET'
384
+ mock_query = { :search => { :foo => :bar } }
385
+
386
+ mock_remote = Hoodoo::Services::Discovery::ForAMQP.new(
387
+ resource: 'Version',
388
+ version: 2
389
+ )
390
+
391
+ bad_body_data = '500 Internal Server Error'
392
+ mock_response = Object.new
393
+
394
+ run_expectations( :show, mock_path + '/ident', mock_method, mock_query, mock_remote, mock_response )
395
+
396
+ endpoint = @mw.inter_resource_endpoint_for( 'Version', 2, @interaction )
397
+ mock_result = endpoint.show( 'ident', mock_query )
398
+
399
+ expect( mock_result ).to be_a( Hoodoo::Client::AugmentedHash )
400
+ expect( mock_result.platform_errors ).to_not be_nil
401
+
402
+ errors = mock_result.platform_errors.render( Hoodoo::UUID.generate )
403
+
404
+ expect( errors ).to have_key( 'errors' )
405
+ expect( errors[ 'errors' ] ).to be_a( Array )
406
+ expect( errors[ 'errors' ][ 0 ] ).to_not be_nil
407
+
408
+ expect( errors[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.fault' )
409
+ expect( errors[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'Unexpected raw HTTP status code 500 with non-JSON response' )
410
+ expect( errors[ 'errors' ][ 0 ][ 'reference' ] ).to eq( "#{ bad_body_data }" )
411
+ end
412
+
413
+ it 'calls #show over Alchemy and handles 200 status but bad JSON' do
414
+ mock_path = '/2/Version'
415
+ mock_method = 'GET'
416
+ mock_query = { :search => { :foo => :bar } }
417
+
418
+ mock_remote = Hoodoo::Services::Discovery::ForAMQP.new(
419
+ resource: 'Version',
420
+ version: 2
421
+ )
422
+
423
+ bad_body_data = 'Not JSON'
424
+ mock_response = {
425
+ 'status_code' => 200,
426
+ 'body' => bad_body_data
427
+ }
428
+
429
+ run_expectations( :show, mock_path + '/ident', mock_method, mock_query, mock_remote, mock_response )
430
+
431
+ endpoint = @mw.inter_resource_endpoint_for( 'Version', 2, @interaction )
432
+ mock_result = endpoint.show( 'ident', mock_query )
433
+
434
+ expect( mock_result ).to be_a( Hoodoo::Client::AugmentedHash )
435
+ expect( mock_result.platform_errors ).to_not be_nil
436
+
437
+ errors = mock_result.platform_errors.render( Hoodoo::UUID.generate )
438
+
439
+ expect( errors ).to have_key( 'errors' )
440
+ expect( errors[ 'errors' ] ).to be_a( Array )
441
+ expect( errors[ 'errors' ][ 0 ] ).to_not be_nil
442
+
443
+ expect( errors[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.fault' )
444
+ expect( errors[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'Could not parse retrieved body data despite receiving HTTP status code 200' )
445
+ expect( errors[ 'errors' ][ 0 ][ 'reference' ] ).to eq( "#{ bad_body_data }" )
446
+ end
447
+ end
448
+ end
@@ -161,6 +161,20 @@ RSpec.configure do | config |
161
161
  end
162
162
  end
163
163
 
164
+ # Ensure known monkey patches are disabled; tests for the patches
165
+ # will enable them for the duration of the test only.
166
+ #
167
+ [ Hoodoo::Monkey::Patch, Hoodoo::Monkey::Chaos ].each do | monkey |
168
+ monkey.constants.each do | extension_module_name |
169
+ extension_module = monkey.const_get( extension_module_name )
170
+ Hoodoo::Monkey.disable( extension_module: extension_module )
171
+ end
172
+ end
173
+
174
+ # Load all global shared examples.
175
+ #
176
+ Dir[ "#{ File.dirname( __FILE__ ) }/shared_examples/**/*.rb" ].sort.each { | f | require f }
177
+
164
178
  # Connect to PostgreSQL for test purposes. Generally only used within
165
179
  # the "spec_helper.rb" file for environment setup and teardown.
166
180
  #