hoodoo 1.7.0 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
  #