haveapi 0.28.1 → 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 +64 -50
- 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 +271 -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 +167 -12
- 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
|
|
|
@@ -457,7 +620,7 @@ describe HaveAPI::Action do
|
|
|
457
620
|
it 'rejects optional-only input namespaces with invalid shapes' do
|
|
458
621
|
call_api([:Test], :optional_shape, { test: 'not-a-hash' })
|
|
459
622
|
|
|
460
|
-
expect(last_response.status).to eq(
|
|
623
|
+
expect(last_response.status).to eq(200)
|
|
461
624
|
expect(api_response).not_to be_ok
|
|
462
625
|
expect(api_response.message).to eq('invalid input layout')
|
|
463
626
|
end
|
|
@@ -465,7 +628,7 @@ describe HaveAPI::Action do
|
|
|
465
628
|
it 'rejects list inputs with invalid element shapes' do
|
|
466
629
|
call_api([:Test], :batch, { tests: ['not-a-hash'], _meta: { confirmed: true } })
|
|
467
630
|
|
|
468
|
-
expect(last_response.status).to eq(
|
|
631
|
+
expect(last_response.status).to eq(200)
|
|
469
632
|
expect(api_response).not_to be_ok
|
|
470
633
|
expect(api_response.message).to eq('invalid input layout')
|
|
471
634
|
end
|
|
@@ -473,13 +636,13 @@ describe HaveAPI::Action do
|
|
|
473
636
|
it 'validates global metadata on list input actions' do
|
|
474
637
|
call_api([:Test], :batch, { tests: [{ label: 'one' }] })
|
|
475
638
|
|
|
476
|
-
expect(last_response.status).to eq(
|
|
639
|
+
expect(last_response.status).to eq(200)
|
|
477
640
|
expect(api_response).not_to be_ok
|
|
478
641
|
expect(api_response.errors[:confirmed]).to include('required parameter missing')
|
|
479
642
|
|
|
480
643
|
call_api([:Test], :batch, { tests: [{ label: 'one' }], _meta: { confirmed: 'maybe' } })
|
|
481
644
|
|
|
482
|
-
expect(last_response.status).to eq(
|
|
645
|
+
expect(last_response.status).to eq(200)
|
|
483
646
|
expect(api_response).not_to be_ok
|
|
484
647
|
expect(api_response.errors[:confirmed].first).to include('not a valid boolean')
|
|
485
648
|
end
|
|
@@ -487,7 +650,7 @@ describe HaveAPI::Action do
|
|
|
487
650
|
it 'rejects malformed metadata namespaces' do
|
|
488
651
|
call_api([:Test], :echo, { test: { msg: 'hi' }, _meta: 'not-a-hash' })
|
|
489
652
|
|
|
490
|
-
expect(last_response.status).to eq(
|
|
653
|
+
expect(last_response.status).to eq(200)
|
|
491
654
|
expect(api_response).not_to be_ok
|
|
492
655
|
expect(api_response.message).to eq('invalid input layout')
|
|
493
656
|
end
|
|
@@ -558,16 +721,68 @@ describe HaveAPI::Action do
|
|
|
558
721
|
expect(api_response.response[:_meta]).not_to have_key(:secret_status)
|
|
559
722
|
end
|
|
560
723
|
|
|
561
|
-
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
|
|
562
744
|
call_api([:Test], :unnamespaced_input, { public: 'ok', secret: 'hidden' })
|
|
563
745
|
|
|
564
746
|
expect(last_response.status).to eq(200)
|
|
565
747
|
expect(api_response).to be_ok
|
|
566
|
-
expect(api_response[:test][:
|
|
748
|
+
expect(api_response[:test][:has_params_method]).to be(false)
|
|
567
749
|
expect(api_response[:test][:input_saw_secret]).to be(false)
|
|
568
750
|
end
|
|
569
751
|
|
|
570
|
-
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
|
|
571
786
|
call_api([:Test], :top_level_body, {
|
|
572
787
|
secret: 'top-level hidden',
|
|
573
788
|
test: {
|
|
@@ -578,10 +793,50 @@ describe HaveAPI::Action do
|
|
|
578
793
|
|
|
579
794
|
expect(last_response.status).to eq(200)
|
|
580
795
|
expect(api_response).to be_ok
|
|
581
|
-
expect(api_response[:test][:
|
|
796
|
+
expect(api_response[:test][:has_params_method]).to be(false)
|
|
582
797
|
expect(api_response[:test][:input_saw_secret]).to be(false)
|
|
583
798
|
end
|
|
584
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
|
+
|
|
585
840
|
it 'denies OPTIONS for actions without an authorization block without raising' do
|
|
586
841
|
call_api(:options, '/v1/tests/closed?method=POST')
|
|
587
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
|