haveapi 0.28.0 → 0.28.2
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 +4 -4
- data/haveapi.gemspec +1 -1
- data/lib/haveapi/action.rb +67 -51
- data/lib/haveapi/authentication/token/provider.rb +12 -0
- data/lib/haveapi/authorization.rb +21 -0
- data/lib/haveapi/context.rb +17 -2
- data/lib/haveapi/example.rb +1 -0
- data/lib/haveapi/extensions/exception_mailer.rb +6 -2
- data/lib/haveapi/metadata.rb +1 -1
- data/lib/haveapi/model_adapters/active_record.rb +9 -4
- data/lib/haveapi/parameters/resource.rb +5 -4
- data/lib/haveapi/parameters/typed.rb +34 -2
- data/lib/haveapi/params.rb +16 -14
- data/lib/haveapi/resources/action_state.rb +3 -3
- data/lib/haveapi/server.rb +43 -8
- data/lib/haveapi/spec/api_builder.rb +10 -0
- data/lib/haveapi/spec/mock_action.rb +10 -9
- data/lib/haveapi/spec/spec_methods.rb +4 -0
- data/lib/haveapi/version.rb +1 -1
- data/spec/action/runtime_spec.rb +278 -16
- data/spec/action/validation_http_status_spec.rb +76 -0
- data/spec/action_state_spec.rb +28 -5
- data/spec/documentation/auth_filtering_spec.rb +14 -2
- data/spec/model_adapters/active_record_spec.rb +179 -11
- data/spec/parameters/typed_spec.rb +54 -0
- data/spec/server/integration_spec.rb +1 -1
- data/test_support/client_test_api.rb +8 -8
- metadata +4 -3
|
@@ -54,6 +54,16 @@ module HaveAPI::Spec
|
|
|
54
54
|
opt(:action_state, backend)
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# Set action state authentication mode.
|
|
58
|
+
def action_state_auth(mode)
|
|
59
|
+
opt(:action_state_auth, mode)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Set HTTP status for action validation errors. Nil preserves legacy 200.
|
|
63
|
+
def validation_error_http_status(status)
|
|
64
|
+
opt(:validation_error_http_status, status)
|
|
65
|
+
end
|
|
66
|
+
|
|
57
67
|
# Set a custom mount path.
|
|
58
68
|
def mount_to(path)
|
|
59
69
|
opt(:mount, path)
|
|
@@ -9,15 +9,16 @@ module HaveAPI::Spec
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def call(input, user: nil, &)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
context = HaveAPI::Context.new(
|
|
13
|
+
@server,
|
|
14
|
+
version: @v,
|
|
15
|
+
action: @action,
|
|
16
|
+
path: @path,
|
|
17
|
+
input:,
|
|
18
|
+
user:,
|
|
19
|
+
endpoint: true
|
|
20
|
+
)
|
|
21
|
+
action = @action.new(nil, @v, {}, input, context)
|
|
21
22
|
|
|
22
23
|
unless action.authorized?(user)
|
|
23
24
|
raise 'Access denied. Insufficient permissions.'
|
|
@@ -15,6 +15,10 @@ module HaveAPI::Spec
|
|
|
15
15
|
@api.default_version = default if default
|
|
16
16
|
as = get_opt(:action_state)
|
|
17
17
|
@api.action_state = as if as
|
|
18
|
+
asa = get_opt(:action_state_auth)
|
|
19
|
+
@api.action_state_auth = asa if asa
|
|
20
|
+
ves = get_opt(:validation_error_http_status)
|
|
21
|
+
@api.validation_error_http_status = ves if ves
|
|
18
22
|
@api.mount(get_opt(:mount) || '/')
|
|
19
23
|
@api.app
|
|
20
24
|
end
|
data/lib/haveapi/version.rb
CHANGED
data/spec/action/runtime_spec.rb
CHANGED
|
@@ -130,7 +130,7 @@ describe HaveAPI::Action do
|
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
def exec
|
|
133
|
-
{ value:
|
|
133
|
+
{ value: path_params['account_id'] }
|
|
134
134
|
end
|
|
135
135
|
end
|
|
136
136
|
|
|
@@ -153,7 +153,7 @@ describe HaveAPI::Action do
|
|
|
153
153
|
end
|
|
154
154
|
|
|
155
155
|
def exec
|
|
156
|
-
{ route_profile_id:
|
|
156
|
+
{ route_profile_id: path_params['profile_id'] }
|
|
157
157
|
end
|
|
158
158
|
end
|
|
159
159
|
|
|
@@ -231,6 +231,59 @@ describe HaveAPI::Action do
|
|
|
231
231
|
end
|
|
232
232
|
end
|
|
233
233
|
|
|
234
|
+
define_action(:MetadataSpecificOutput) do
|
|
235
|
+
route 'metadata_specific_output'
|
|
236
|
+
http_method :post
|
|
237
|
+
|
|
238
|
+
authorize do
|
|
239
|
+
output whitelist: [:ok]
|
|
240
|
+
meta_output blacklist: [:secret_status]
|
|
241
|
+
allow
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
meta(:global) do
|
|
245
|
+
output do
|
|
246
|
+
string :public_status
|
|
247
|
+
string :secret_status
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
output do
|
|
252
|
+
bool :ok
|
|
253
|
+
string :hidden_body
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def exec
|
|
257
|
+
set_meta(public_status: 'queued', secret_status: 'internal-token')
|
|
258
|
+
{ ok: true, hidden_body: 'body-secret' }
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
define_action(:WhitelistedIndex, superclass: HaveAPI::Actions::Default::Index) do
|
|
263
|
+
route 'whitelisted'
|
|
264
|
+
|
|
265
|
+
authorize do
|
|
266
|
+
output whitelist: [:name]
|
|
267
|
+
allow
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
output(:hash_list) do
|
|
271
|
+
string :name
|
|
272
|
+
string :secret
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def count
|
|
276
|
+
2
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def exec
|
|
280
|
+
[
|
|
281
|
+
{ name: 'one', secret: 'hidden' },
|
|
282
|
+
{ name: 'two', secret: 'hidden' }
|
|
283
|
+
]
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
234
287
|
define_action(:UnnamespacedInput) do
|
|
235
288
|
route 'unnamespaced_input'
|
|
236
289
|
http_method :post
|
|
@@ -246,18 +299,70 @@ describe HaveAPI::Action do
|
|
|
246
299
|
end
|
|
247
300
|
|
|
248
301
|
output do
|
|
249
|
-
bool :
|
|
302
|
+
bool :has_params_method
|
|
250
303
|
bool :input_saw_secret
|
|
251
304
|
end
|
|
252
305
|
|
|
253
306
|
def exec
|
|
254
307
|
{
|
|
255
|
-
|
|
308
|
+
has_params_method: respond_to?(:params),
|
|
256
309
|
input_saw_secret: input.has_key?(:secret)
|
|
257
310
|
}
|
|
258
311
|
end
|
|
259
312
|
end
|
|
260
313
|
|
|
314
|
+
define_action(:UnnamespacedPathInput) do
|
|
315
|
+
route 'unnamespaced_input/{test_id}'
|
|
316
|
+
http_method :put
|
|
317
|
+
authorize { allow }
|
|
318
|
+
|
|
319
|
+
input(:hash, namespace: false) do
|
|
320
|
+
string :public
|
|
321
|
+
string :test_id
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
output do
|
|
325
|
+
bool :input_saw_test_id
|
|
326
|
+
string :input_test_id, nullable: true
|
|
327
|
+
string :path_test_id
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def exec
|
|
331
|
+
{
|
|
332
|
+
input_saw_test_id: input.has_key?(:test_id),
|
|
333
|
+
input_test_id: input[:test_id],
|
|
334
|
+
path_test_id: path_params['test_id']
|
|
335
|
+
}
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
define_action(:QueryPathInput) do
|
|
340
|
+
route 'query_input/{test_id}'
|
|
341
|
+
http_method :get
|
|
342
|
+
authorize { allow }
|
|
343
|
+
|
|
344
|
+
input(:hash, namespace: false) do
|
|
345
|
+
string :filter
|
|
346
|
+
string :test_id
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
output do
|
|
350
|
+
bool :has_params_method
|
|
351
|
+
bool :input_saw_test_id
|
|
352
|
+
string :filter
|
|
353
|
+
string :path_test_id
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def exec
|
|
357
|
+
{
|
|
358
|
+
has_params_method: respond_to?(:params),
|
|
359
|
+
input_saw_test_id: input.has_key?(:test_id),
|
|
360
|
+
filter: input[:filter],
|
|
361
|
+
path_test_id: path_params['test_id']
|
|
362
|
+
}
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
261
366
|
define_action(:TopLevelBody) do
|
|
262
367
|
route 'top_level_body'
|
|
263
368
|
http_method :post
|
|
@@ -273,18 +378,76 @@ describe HaveAPI::Action do
|
|
|
273
378
|
end
|
|
274
379
|
|
|
275
380
|
output do
|
|
276
|
-
bool :
|
|
381
|
+
bool :has_params_method
|
|
277
382
|
bool :input_saw_secret
|
|
278
383
|
end
|
|
279
384
|
|
|
280
385
|
def exec
|
|
281
386
|
{
|
|
282
|
-
|
|
387
|
+
has_params_method: respond_to?(:params),
|
|
283
388
|
input_saw_secret: input.has_key?(:secret)
|
|
284
389
|
}
|
|
285
390
|
end
|
|
286
391
|
end
|
|
287
392
|
|
|
393
|
+
define_action(:CustomPayload) do
|
|
394
|
+
route 'custom_payload'
|
|
395
|
+
http_method :post
|
|
396
|
+
authorize { allow }
|
|
397
|
+
|
|
398
|
+
input do
|
|
399
|
+
custom :payload
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
output do
|
|
403
|
+
bool :top_string_key
|
|
404
|
+
bool :top_symbol_key
|
|
405
|
+
bool :nested_string_key
|
|
406
|
+
bool :nested_symbol_key
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def exec
|
|
410
|
+
payload = input[:payload]
|
|
411
|
+
nested = payload['response']
|
|
412
|
+
|
|
413
|
+
{
|
|
414
|
+
top_string_key: payload.has_key?('rawId'),
|
|
415
|
+
top_symbol_key: payload.has_key?(:rawId),
|
|
416
|
+
nested_string_key: nested.has_key?('clientDataJSON'),
|
|
417
|
+
nested_symbol_key: nested.has_key?(:clientDataJSON)
|
|
418
|
+
}
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
define_action(:CustomPayloadSymbols) do
|
|
423
|
+
route 'custom_payload_symbols'
|
|
424
|
+
http_method :post
|
|
425
|
+
authorize { allow }
|
|
426
|
+
|
|
427
|
+
input do
|
|
428
|
+
custom :payload, symbolize_keys: true
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
output do
|
|
432
|
+
bool :top_string_key
|
|
433
|
+
bool :top_symbol_key
|
|
434
|
+
bool :nested_string_key
|
|
435
|
+
bool :nested_symbol_key
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def exec
|
|
439
|
+
payload = input[:payload]
|
|
440
|
+
nested = payload[:response]
|
|
441
|
+
|
|
442
|
+
{
|
|
443
|
+
top_string_key: payload.has_key?('rawId'),
|
|
444
|
+
top_symbol_key: payload.has_key?(:rawId),
|
|
445
|
+
nested_string_key: nested.has_key?('clientDataJSON'),
|
|
446
|
+
nested_symbol_key: nested.has_key?(:clientDataJSON)
|
|
447
|
+
}
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
288
451
|
define_action(:Closed) do
|
|
289
452
|
route 'closed'
|
|
290
453
|
http_method :post
|
|
@@ -309,7 +472,7 @@ describe HaveAPI::Action do
|
|
|
309
472
|
end
|
|
310
473
|
|
|
311
474
|
def exec
|
|
312
|
-
{ id:
|
|
475
|
+
{ id: path_params['test_id'] }
|
|
313
476
|
end
|
|
314
477
|
end
|
|
315
478
|
|
|
@@ -430,6 +593,13 @@ describe HaveAPI::Action do
|
|
|
430
593
|
action_hooks[:listeners] = original
|
|
431
594
|
end
|
|
432
595
|
|
|
596
|
+
it 'builds path params with string and symbol keys' do
|
|
597
|
+
expect(action_class(:show).path_params('/tests/{test_id}', 123)).to eq({
|
|
598
|
+
'test_id' => '123',
|
|
599
|
+
test_id: '123'
|
|
600
|
+
})
|
|
601
|
+
end
|
|
602
|
+
|
|
433
603
|
it '_meta.no suppresses metadata' do
|
|
434
604
|
action_class(:echo).calls.clear
|
|
435
605
|
|
|
@@ -450,7 +620,7 @@ describe HaveAPI::Action do
|
|
|
450
620
|
it 'rejects optional-only input namespaces with invalid shapes' do
|
|
451
621
|
call_api([:Test], :optional_shape, { test: 'not-a-hash' })
|
|
452
622
|
|
|
453
|
-
expect(last_response.status).to eq(
|
|
623
|
+
expect(last_response.status).to eq(200)
|
|
454
624
|
expect(api_response).not_to be_ok
|
|
455
625
|
expect(api_response.message).to eq('invalid input layout')
|
|
456
626
|
end
|
|
@@ -458,7 +628,7 @@ describe HaveAPI::Action do
|
|
|
458
628
|
it 'rejects list inputs with invalid element shapes' do
|
|
459
629
|
call_api([:Test], :batch, { tests: ['not-a-hash'], _meta: { confirmed: true } })
|
|
460
630
|
|
|
461
|
-
expect(last_response.status).to eq(
|
|
631
|
+
expect(last_response.status).to eq(200)
|
|
462
632
|
expect(api_response).not_to be_ok
|
|
463
633
|
expect(api_response.message).to eq('invalid input layout')
|
|
464
634
|
end
|
|
@@ -466,13 +636,13 @@ describe HaveAPI::Action do
|
|
|
466
636
|
it 'validates global metadata on list input actions' do
|
|
467
637
|
call_api([:Test], :batch, { tests: [{ label: 'one' }] })
|
|
468
638
|
|
|
469
|
-
expect(last_response.status).to eq(
|
|
639
|
+
expect(last_response.status).to eq(200)
|
|
470
640
|
expect(api_response).not_to be_ok
|
|
471
641
|
expect(api_response.errors[:confirmed]).to include('required parameter missing')
|
|
472
642
|
|
|
473
643
|
call_api([:Test], :batch, { tests: [{ label: 'one' }], _meta: { confirmed: 'maybe' } })
|
|
474
644
|
|
|
475
|
-
expect(last_response.status).to eq(
|
|
645
|
+
expect(last_response.status).to eq(200)
|
|
476
646
|
expect(api_response).not_to be_ok
|
|
477
647
|
expect(api_response.errors[:confirmed].first).to include('not a valid boolean')
|
|
478
648
|
end
|
|
@@ -480,7 +650,7 @@ describe HaveAPI::Action do
|
|
|
480
650
|
it 'rejects malformed metadata namespaces' do
|
|
481
651
|
call_api([:Test], :echo, { test: { msg: 'hi' }, _meta: 'not-a-hash' })
|
|
482
652
|
|
|
483
|
-
expect(last_response.status).to eq(
|
|
653
|
+
expect(last_response.status).to eq(200)
|
|
484
654
|
expect(api_response).not_to be_ok
|
|
485
655
|
expect(api_response.message).to eq('invalid input layout')
|
|
486
656
|
end
|
|
@@ -551,16 +721,68 @@ describe HaveAPI::Action do
|
|
|
551
721
|
expect(api_response.response[:_meta]).not_to have_key(:secret_status)
|
|
552
722
|
end
|
|
553
723
|
|
|
554
|
-
it '
|
|
724
|
+
it 'applies metadata-specific output filters to global metadata' do
|
|
725
|
+
call_api([:Test], :metadata_specific_output, {})
|
|
726
|
+
|
|
727
|
+
expect(last_response.status).to eq(200)
|
|
728
|
+
expect(api_response).to be_ok
|
|
729
|
+
expect(api_response[:test]).to include(ok: true)
|
|
730
|
+
expect(api_response[:test]).not_to have_key(:hidden_body)
|
|
731
|
+
expect(api_response.response[:_meta]).to include(public_status: 'queued')
|
|
732
|
+
expect(api_response.response[:_meta]).not_to have_key(:secret_status)
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
it 'keeps global metadata when body output uses a whitelist' do
|
|
736
|
+
get '/v1/tests/whitelisted', { _meta: { count: true } }, input: ''
|
|
737
|
+
|
|
738
|
+
expect(last_response.status).to eq(200)
|
|
739
|
+
expect(api_response).to be_ok
|
|
740
|
+
expect(api_response.response[:_meta]).to include(total_count: 2)
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
it 'filters unnamespaced input without exposing legacy params' do
|
|
555
744
|
call_api([:Test], :unnamespaced_input, { public: 'ok', secret: 'hidden' })
|
|
556
745
|
|
|
557
746
|
expect(last_response.status).to eq(200)
|
|
558
747
|
expect(api_response).to be_ok
|
|
559
|
-
expect(api_response[:test][:
|
|
748
|
+
expect(api_response[:test][:has_params_method]).to be(false)
|
|
560
749
|
expect(api_response[:test][:input_saw_secret]).to be(false)
|
|
561
750
|
end
|
|
562
751
|
|
|
563
|
-
it '
|
|
752
|
+
it 'keeps path parameters out of unnamespaced body input' do
|
|
753
|
+
call_api(:put, '/v1/tests/unnamespaced_input/route-id', { public: 'ok' })
|
|
754
|
+
|
|
755
|
+
expect(last_response.status).to eq(200)
|
|
756
|
+
expect(api_response).to be_ok
|
|
757
|
+
expect(api_response[:test][:input_saw_test_id]).to be(false)
|
|
758
|
+
expect(api_response[:test][:path_test_id]).to eq('route-id')
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
it 'exposes client values through input without changing path identity' do
|
|
762
|
+
call_api(:put, '/v1/tests/unnamespaced_input/route-id', {
|
|
763
|
+
public: 'ok',
|
|
764
|
+
test_id: 'body-id'
|
|
765
|
+
})
|
|
766
|
+
|
|
767
|
+
expect(last_response.status).to eq(200)
|
|
768
|
+
expect(api_response).to be_ok
|
|
769
|
+
expect(api_response[:test][:input_saw_test_id]).to be(true)
|
|
770
|
+
expect(api_response[:test][:input_test_id]).to eq('body-id')
|
|
771
|
+
expect(api_response[:test][:path_test_id]).to eq('route-id')
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
it 'keeps route ids out of GET query input' do
|
|
775
|
+
get '/v1/tests/query_input/route-id', { filter: 'visible' }, input: ''
|
|
776
|
+
|
|
777
|
+
expect(last_response.status).to eq(200)
|
|
778
|
+
expect(api_response).to be_ok
|
|
779
|
+
expect(api_response[:test][:has_params_method]).to be(false)
|
|
780
|
+
expect(api_response[:test][:input_saw_test_id]).to be(false)
|
|
781
|
+
expect(api_response[:test][:filter]).to eq('visible')
|
|
782
|
+
expect(api_response[:test][:path_test_id]).to eq('route-id')
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
it 'does not expose top-level JSON keys outside the input namespace' do
|
|
564
786
|
call_api([:Test], :top_level_body, {
|
|
565
787
|
secret: 'top-level hidden',
|
|
566
788
|
test: {
|
|
@@ -571,10 +793,50 @@ describe HaveAPI::Action do
|
|
|
571
793
|
|
|
572
794
|
expect(last_response.status).to eq(200)
|
|
573
795
|
expect(api_response).to be_ok
|
|
574
|
-
expect(api_response[:test][:
|
|
796
|
+
expect(api_response[:test][:has_params_method]).to be(false)
|
|
575
797
|
expect(api_response[:test][:input_saw_secret]).to be(false)
|
|
576
798
|
end
|
|
577
799
|
|
|
800
|
+
it 'keeps custom payload field names string-keyed by default' do
|
|
801
|
+
call_api([:Test], :custom_payload, {
|
|
802
|
+
test: {
|
|
803
|
+
payload: {
|
|
804
|
+
rawId: 'credential-id',
|
|
805
|
+
response: {
|
|
806
|
+
clientDataJSON: 'client-data'
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
})
|
|
811
|
+
|
|
812
|
+
expect(last_response.status).to eq(200)
|
|
813
|
+
expect(api_response).to be_ok
|
|
814
|
+
expect(api_response[:test][:top_string_key]).to be(true)
|
|
815
|
+
expect(api_response[:test][:top_symbol_key]).to be(false)
|
|
816
|
+
expect(api_response[:test][:nested_string_key]).to be(true)
|
|
817
|
+
expect(api_response[:test][:nested_symbol_key]).to be(false)
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
it 'symbolizes custom payload field names when requested' do
|
|
821
|
+
call_api([:Test], :custom_payload_symbols, {
|
|
822
|
+
test: {
|
|
823
|
+
payload: {
|
|
824
|
+
rawId: 'credential-id',
|
|
825
|
+
response: {
|
|
826
|
+
clientDataJSON: 'client-data'
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
})
|
|
831
|
+
|
|
832
|
+
expect(last_response.status).to eq(200)
|
|
833
|
+
expect(api_response).to be_ok
|
|
834
|
+
expect(api_response[:test][:top_string_key]).to be(false)
|
|
835
|
+
expect(api_response[:test][:top_symbol_key]).to be(true)
|
|
836
|
+
expect(api_response[:test][:nested_string_key]).to be(false)
|
|
837
|
+
expect(api_response[:test][:nested_symbol_key]).to be(true)
|
|
838
|
+
end
|
|
839
|
+
|
|
578
840
|
it 'denies OPTIONS for actions without an authorization block without raising' do
|
|
579
841
|
call_api(:options, '/v1/tests/closed?method=POST')
|
|
580
842
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
describe HaveAPI::Action do
|
|
4
|
+
describe 'validation error HTTP status' do
|
|
5
|
+
context 'with compatibility behavior' do
|
|
6
|
+
api do
|
|
7
|
+
define_resource(:ValidationStatus) do
|
|
8
|
+
version 1
|
|
9
|
+
auth false
|
|
10
|
+
|
|
11
|
+
define_action(:Create, superclass: HaveAPI::Actions::Default::Create) do
|
|
12
|
+
authorize { allow }
|
|
13
|
+
|
|
14
|
+
input do
|
|
15
|
+
string :name, required: true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
output do
|
|
19
|
+
string :name
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def exec
|
|
23
|
+
{ name: input[:name] }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
default_version 1
|
|
30
|
+
|
|
31
|
+
it 'keeps validation envelopes on HTTP 200 by default' do
|
|
32
|
+
call_api([:ValidationStatus], :create, { validation_status: {} })
|
|
33
|
+
|
|
34
|
+
expect(last_response.status).to eq(200)
|
|
35
|
+
expect(api_response).not_to be_ok
|
|
36
|
+
expect(api_response.errors[:name]).to include('required parameter missing')
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
context 'with configured HTTP 400 status' do
|
|
41
|
+
api do
|
|
42
|
+
define_resource(:StrictValidationStatus) do
|
|
43
|
+
version 1
|
|
44
|
+
auth false
|
|
45
|
+
|
|
46
|
+
define_action(:Create, superclass: HaveAPI::Actions::Default::Create) do
|
|
47
|
+
authorize { allow }
|
|
48
|
+
|
|
49
|
+
input do
|
|
50
|
+
string :name, required: true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
output do
|
|
54
|
+
string :name
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def exec
|
|
58
|
+
{ name: input[:name] }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
default_version 1
|
|
65
|
+
validation_error_http_status 400
|
|
66
|
+
|
|
67
|
+
it 'returns HTTP 400 for validation envelopes' do
|
|
68
|
+
call_api([:StrictValidationStatus], :create, { strict_validation_status: {} })
|
|
69
|
+
|
|
70
|
+
expect(last_response.status).to eq(400)
|
|
71
|
+
expect(api_response).not_to be_ok
|
|
72
|
+
expect(api_response.errors[:name]).to include('required parameter missing')
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/spec/action_state_spec.rb
CHANGED
|
@@ -225,7 +225,7 @@ describe HaveAPI::Resources::ActionState do
|
|
|
225
225
|
|
|
226
226
|
get_action '/v1/action_states/1/poll', action_state: { timeout: 31 }
|
|
227
227
|
|
|
228
|
-
expect(last_response.status).to eq(
|
|
228
|
+
expect(last_response.status).to eq(200)
|
|
229
229
|
expect(api_response).not_to be_ok
|
|
230
230
|
expect(api_response.errors[:timeout].first).to include('range <0, 30>')
|
|
231
231
|
end
|
|
@@ -333,12 +333,12 @@ describe HaveAPI::Resources::ActionState do
|
|
|
333
333
|
header 'Accept', 'application/json'
|
|
334
334
|
end
|
|
335
335
|
|
|
336
|
-
it '
|
|
336
|
+
it 'uses backend-defined anonymous behavior by default' do
|
|
337
337
|
get_action '/v1/action_states'
|
|
338
338
|
|
|
339
|
-
expect(last_response.status).to eq(
|
|
340
|
-
expect(api_response).
|
|
341
|
-
expect(ActionStateSpec::Backend.list_calls).to
|
|
339
|
+
expect(last_response.status).to eq(200)
|
|
340
|
+
expect(api_response).to be_ok
|
|
341
|
+
expect(ActionStateSpec::Backend.list_calls.last[:user]).to be_nil
|
|
342
342
|
end
|
|
343
343
|
|
|
344
344
|
it 'passes authenticated users to the action state backend' do
|
|
@@ -350,4 +350,27 @@ describe HaveAPI::Resources::ActionState do
|
|
|
350
350
|
expect(ActionStateSpec::Backend.list_calls.last[:user].id).to eq(1)
|
|
351
351
|
end
|
|
352
352
|
end
|
|
353
|
+
|
|
354
|
+
context 'with action_state auth required' do
|
|
355
|
+
empty_api
|
|
356
|
+
use_version 1
|
|
357
|
+
default_version 1
|
|
358
|
+
action_state ActionStateSpec::Backend
|
|
359
|
+
action_state_auth :required
|
|
360
|
+
auth_chain ActionStateSpec::BasicProvider
|
|
361
|
+
|
|
362
|
+
before do
|
|
363
|
+
ActionStateSpec::Backend.reset!
|
|
364
|
+
ActionStateSpec::Backend.add_state(ActionStateSpec::State.new(id: 1))
|
|
365
|
+
header 'Accept', 'application/json'
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
it 'requires authentication before reaching the action state backend' do
|
|
369
|
+
get_action '/v1/action_states'
|
|
370
|
+
|
|
371
|
+
expect(last_response.status).to eq(401)
|
|
372
|
+
expect(api_response).not_to be_ok
|
|
373
|
+
expect(ActionStateSpec::Backend.list_calls).to be_empty
|
|
374
|
+
end
|
|
375
|
+
end
|
|
353
376
|
end
|
|
@@ -142,7 +142,7 @@ describe DocAuthFilteringSpec do
|
|
|
142
142
|
|
|
143
143
|
# rubocop:disable RSpec/NoExpectationExample
|
|
144
144
|
example 'admin-only public result' do
|
|
145
|
-
authorize { |user| user
|
|
145
|
+
authorize { |user| user.login == 'admin' }
|
|
146
146
|
response({ msg: 'ADMIN_ONLY_RESULT' })
|
|
147
147
|
end
|
|
148
148
|
# rubocop:enable RSpec/NoExpectationExample
|
|
@@ -242,13 +242,25 @@ describe DocAuthFilteringSpec do
|
|
|
242
242
|
expect(api_response[:method]).to eq('GET')
|
|
243
243
|
end
|
|
244
244
|
|
|
245
|
-
it '
|
|
245
|
+
it 'includes examples for anonymous version docs without evaluating example auth' do
|
|
246
246
|
header 'Authorization', nil
|
|
247
247
|
call_api(:options, '/v1/')
|
|
248
248
|
|
|
249
249
|
expect(last_response.status).to eq(200)
|
|
250
250
|
expect(api_response).to be_ok
|
|
251
251
|
|
|
252
|
+
examples = api_response[:resources][:secure][:actions][:public][:examples]
|
|
253
|
+
expect(examples.size).to eq(1)
|
|
254
|
+
expect(examples.first[:response][:msg]).to eq('ADMIN_ONLY_RESULT')
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
it 'hides examples denied to authenticated users from version docs' do
|
|
258
|
+
login('user', 'pass')
|
|
259
|
+
call_api(:options, '/v1/')
|
|
260
|
+
|
|
261
|
+
expect(last_response.status).to eq(200)
|
|
262
|
+
expect(api_response).to be_ok
|
|
263
|
+
|
|
252
264
|
examples = api_response[:resources][:secure][:actions][:public][:examples]
|
|
253
265
|
expect(examples).to be_empty
|
|
254
266
|
end
|