haveapi 0.27.2 → 0.28.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.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/haveapi.gemspec +1 -1
- data/lib/haveapi/action.rb +125 -36
- data/lib/haveapi/actions/paginable.rb +3 -1
- data/lib/haveapi/authentication/basic/provider.rb +2 -0
- data/lib/haveapi/authentication/chain.rb +11 -7
- data/lib/haveapi/authentication/oauth2/config.rb +25 -3
- data/lib/haveapi/authentication/oauth2/provider.rb +92 -11
- data/lib/haveapi/authentication/oauth2/revoke_endpoint.rb +44 -3
- data/lib/haveapi/authentication/token/provider.rb +53 -15
- data/lib/haveapi/authorization.rb +42 -18
- data/lib/haveapi/client_examples/php_client.rb +1 -1
- data/lib/haveapi/client_examples/ruby_client.rb +1 -1
- data/lib/haveapi/context.rb +10 -4
- data/lib/haveapi/example.rb +15 -16
- data/lib/haveapi/extensions/action_exceptions.rb +6 -6
- data/lib/haveapi/model_adapters/active_record.rb +150 -71
- data/lib/haveapi/model_adapters/hash.rb +1 -1
- data/lib/haveapi/parameters/resource.rb +50 -6
- data/lib/haveapi/parameters/typed.rb +40 -13
- data/lib/haveapi/params.rb +27 -8
- data/lib/haveapi/resource.rb +4 -1
- data/lib/haveapi/resources/action_state.rb +13 -5
- data/lib/haveapi/route.rb +2 -2
- data/lib/haveapi/server.rb +137 -45
- data/lib/haveapi/validator.rb +2 -2
- data/lib/haveapi/validator_chain.rb +1 -0
- data/lib/haveapi/validators/confirmation.rb +1 -0
- data/lib/haveapi/validators/format.rb +6 -2
- data/lib/haveapi/validators/length.rb +2 -0
- data/lib/haveapi/validators/numericality.rb +2 -0
- data/lib/haveapi/validators/presence.rb +1 -1
- data/lib/haveapi/version.rb +1 -1
- data/lib/haveapi/views/version_page/client_auth.erb +1 -1
- data/lib/haveapi/views/version_page/client_example.erb +3 -3
- data/lib/haveapi/views/version_page/client_init.erb +1 -1
- data/lib/haveapi/views/version_page.erb +2 -2
- data/lib/haveapi/views/version_sidebar.erb +4 -2
- data/spec/action/authorize_spec.rb +99 -0
- data/spec/action/runtime_spec.rb +426 -0
- data/spec/action_state_spec.rb +52 -0
- data/spec/authentication/basic_spec.rb +29 -0
- data/spec/authentication/oauth2_spec.rb +329 -0
- data/spec/authentication/token_spec.rb +195 -0
- data/spec/authentication/token_version_routes_spec.rb +164 -0
- data/spec/authorization_spec.rb +66 -0
- data/spec/documentation/auth_filtering_spec.rb +195 -1
- data/spec/documentation/current_user_html_escaping_spec.rb +47 -0
- data/spec/documentation/examples_spec.rb +97 -0
- data/spec/documentation/host_html_escaping_spec.rb +41 -0
- data/spec/documentation_spec.rb +13 -0
- data/spec/extensions/action_exceptions_spec.rb +30 -0
- data/spec/model_adapters/active_record_spec.rb +408 -3
- data/spec/parameters/typed_spec.rb +75 -7
- data/spec/params_spec.rb +41 -0
- data/spec/server/integration_spec.rb +90 -0
- data/spec/validator_chain_spec.rb +39 -0
- data/spec/validators/confirmation_spec.rb +14 -0
- data/spec/validators/format_spec.rb +7 -0
- data/spec/validators/length_spec.rb +6 -0
- data/spec/validators/numericality_spec.rb +7 -0
- data/spec/validators/presence_spec.rb +2 -0
- data/test_support/client_test_api.rb +31 -3
- metadata +8 -4
- data/shell.nix +0 -20
data/spec/action/runtime_spec.rb
CHANGED
|
@@ -30,6 +30,289 @@ describe HaveAPI::Action do
|
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
define_action(:OptionalShape) do
|
|
34
|
+
route 'optional_shape'
|
|
35
|
+
http_method :post
|
|
36
|
+
authorize { allow }
|
|
37
|
+
|
|
38
|
+
input do
|
|
39
|
+
string :label
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
output do
|
|
43
|
+
bool :ok
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def exec
|
|
47
|
+
{ ok: true }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
define_action(:Batch) do
|
|
52
|
+
route 'batch'
|
|
53
|
+
http_method :post
|
|
54
|
+
authorize { allow }
|
|
55
|
+
|
|
56
|
+
input(:hash_list) do
|
|
57
|
+
string :label, required: true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
meta(:global) do
|
|
61
|
+
input do
|
|
62
|
+
bool :confirmed, required: true
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
output(:hash) do
|
|
67
|
+
integer :count
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def exec
|
|
71
|
+
{ count: input.size }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
define_action(:OutputOnlyObjectMeta) do
|
|
76
|
+
route 'output_only_object_meta'
|
|
77
|
+
http_method :post
|
|
78
|
+
authorize { allow }
|
|
79
|
+
|
|
80
|
+
input do
|
|
81
|
+
string :msg
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
meta(:object) do
|
|
85
|
+
output do
|
|
86
|
+
string :etag
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
output do
|
|
91
|
+
string :msg
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def exec
|
|
95
|
+
{ msg: input[:msg] }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
define_action(:DigitPath) do
|
|
100
|
+
route 'ipv4/{ip4_id}'
|
|
101
|
+
http_method :get
|
|
102
|
+
|
|
103
|
+
authorize do |_user, path_params|
|
|
104
|
+
deny if path_params['ip4_id'] == '1'
|
|
105
|
+
|
|
106
|
+
allow
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
output do
|
|
110
|
+
string :value
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def exec
|
|
114
|
+
{ value: 'ok' }
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
define_action(:ColonPath) do
|
|
119
|
+
route 'accounts/:account_id/secret'
|
|
120
|
+
http_method :get
|
|
121
|
+
|
|
122
|
+
authorize do |_user, path_params|
|
|
123
|
+
deny unless path_params['account_id'] == '1'
|
|
124
|
+
|
|
125
|
+
allow
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
output do
|
|
129
|
+
string :value
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def exec
|
|
133
|
+
{ value: params['account_id'] }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
define_action(:BodyShadow) do
|
|
138
|
+
route 'profiles/{profile_id}'
|
|
139
|
+
http_method :put
|
|
140
|
+
|
|
141
|
+
authorize do |_user, path_params|
|
|
142
|
+
deny unless path_params['profile_id'] == '2'
|
|
143
|
+
|
|
144
|
+
allow
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
input do
|
|
148
|
+
string :name
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
output do
|
|
152
|
+
string :route_profile_id
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def exec
|
|
156
|
+
{ route_profile_id: params['profile_id'] }
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
define_action(:FilteredDefault) do
|
|
161
|
+
route 'filtered_default'
|
|
162
|
+
http_method :post
|
|
163
|
+
|
|
164
|
+
authorize do
|
|
165
|
+
input blacklist: [:admin]
|
|
166
|
+
allow
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
input do
|
|
170
|
+
string :name
|
|
171
|
+
bool :admin, default: true, fill: true
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
output do
|
|
175
|
+
bool :saw_admin
|
|
176
|
+
bool :admin
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def exec
|
|
180
|
+
{ saw_admin: input.has_key?(:admin), admin: input[:admin] }
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
define_action(:MetadataInput) do
|
|
185
|
+
route 'metadata_input'
|
|
186
|
+
http_method :post
|
|
187
|
+
|
|
188
|
+
authorize do
|
|
189
|
+
input blacklist: [:confirmed]
|
|
190
|
+
allow
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
meta(:global) do
|
|
194
|
+
input do
|
|
195
|
+
bool :confirmed
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
output do
|
|
200
|
+
bool :saw_confirmed
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def exec
|
|
204
|
+
{ saw_confirmed: meta.has_key?(:confirmed) }
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
define_action(:MetadataOutput) do
|
|
209
|
+
route 'metadata_output'
|
|
210
|
+
http_method :post
|
|
211
|
+
|
|
212
|
+
authorize do
|
|
213
|
+
output blacklist: [:secret_status]
|
|
214
|
+
allow
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
meta(:global) do
|
|
218
|
+
output do
|
|
219
|
+
string :public_status
|
|
220
|
+
string :secret_status
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
output do
|
|
225
|
+
bool :ok
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def exec
|
|
229
|
+
set_meta(public_status: 'queued', secret_status: 'internal-token')
|
|
230
|
+
{ ok: true }
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
define_action(:UnnamespacedInput) do
|
|
235
|
+
route 'unnamespaced_input'
|
|
236
|
+
http_method :post
|
|
237
|
+
|
|
238
|
+
authorize do
|
|
239
|
+
input blacklist: [:secret]
|
|
240
|
+
allow
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
input(:hash, namespace: false) do
|
|
244
|
+
string :public
|
|
245
|
+
string :secret
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
output do
|
|
249
|
+
bool :params_saw_secret
|
|
250
|
+
bool :input_saw_secret
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def exec
|
|
254
|
+
{
|
|
255
|
+
params_saw_secret: params.has_key?(:secret),
|
|
256
|
+
input_saw_secret: input.has_key?(:secret)
|
|
257
|
+
}
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
define_action(:TopLevelBody) do
|
|
262
|
+
route 'top_level_body'
|
|
263
|
+
http_method :post
|
|
264
|
+
|
|
265
|
+
authorize do
|
|
266
|
+
input blacklist: [:secret]
|
|
267
|
+
allow
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
input do
|
|
271
|
+
string :public
|
|
272
|
+
string :secret
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
output do
|
|
276
|
+
bool :params_saw_secret
|
|
277
|
+
bool :input_saw_secret
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def exec
|
|
281
|
+
{
|
|
282
|
+
params_saw_secret: params.has_key?(:secret),
|
|
283
|
+
input_saw_secret: input.has_key?(:secret)
|
|
284
|
+
}
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
define_action(:Closed) do
|
|
289
|
+
route 'closed'
|
|
290
|
+
http_method :post
|
|
291
|
+
auth false
|
|
292
|
+
|
|
293
|
+
output do
|
|
294
|
+
bool :ok
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def exec
|
|
298
|
+
{ ok: true }
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
define_action(:Show) do
|
|
303
|
+
route '{test_id}'
|
|
304
|
+
http_method :get
|
|
305
|
+
authorize { allow }
|
|
306
|
+
|
|
307
|
+
output do
|
|
308
|
+
string :id
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def exec
|
|
312
|
+
{ id: params['test_id'] }
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
33
316
|
define_action(:Order) do
|
|
34
317
|
http_method :post
|
|
35
318
|
authorize { allow }
|
|
@@ -164,6 +447,149 @@ describe HaveAPI::Action do
|
|
|
164
447
|
expect(api_response.response).to have_key(:_meta)
|
|
165
448
|
end
|
|
166
449
|
|
|
450
|
+
it 'rejects optional-only input namespaces with invalid shapes' do
|
|
451
|
+
call_api([:Test], :optional_shape, { test: 'not-a-hash' })
|
|
452
|
+
|
|
453
|
+
expect(last_response.status).to eq(400)
|
|
454
|
+
expect(api_response).not_to be_ok
|
|
455
|
+
expect(api_response.message).to eq('invalid input layout')
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
it 'rejects list inputs with invalid element shapes' do
|
|
459
|
+
call_api([:Test], :batch, { tests: ['not-a-hash'], _meta: { confirmed: true } })
|
|
460
|
+
|
|
461
|
+
expect(last_response.status).to eq(400)
|
|
462
|
+
expect(api_response).not_to be_ok
|
|
463
|
+
expect(api_response.message).to eq('invalid input layout')
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
it 'validates global metadata on list input actions' do
|
|
467
|
+
call_api([:Test], :batch, { tests: [{ label: 'one' }] })
|
|
468
|
+
|
|
469
|
+
expect(last_response.status).to eq(400)
|
|
470
|
+
expect(api_response).not_to be_ok
|
|
471
|
+
expect(api_response.errors[:confirmed]).to include('required parameter missing')
|
|
472
|
+
|
|
473
|
+
call_api([:Test], :batch, { tests: [{ label: 'one' }], _meta: { confirmed: 'maybe' } })
|
|
474
|
+
|
|
475
|
+
expect(last_response.status).to eq(400)
|
|
476
|
+
expect(api_response).not_to be_ok
|
|
477
|
+
expect(api_response.errors[:confirmed].first).to include('not a valid boolean')
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
it 'rejects malformed metadata namespaces' do
|
|
481
|
+
call_api([:Test], :echo, { test: { msg: 'hi' }, _meta: 'not-a-hash' })
|
|
482
|
+
|
|
483
|
+
expect(last_response.status).to eq(400)
|
|
484
|
+
expect(api_response).not_to be_ok
|
|
485
|
+
expect(api_response.message).to eq('invalid input layout')
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
it 'ignores object metadata definitions without input' do
|
|
489
|
+
call_api([:Test], :output_only_object_meta, {
|
|
490
|
+
test: {
|
|
491
|
+
msg: 'hi',
|
|
492
|
+
_meta: {
|
|
493
|
+
etag: 'client-value'
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
expect(last_response.status).to eq(200)
|
|
499
|
+
expect(api_response).to be_ok
|
|
500
|
+
expect(api_response[:test][:msg]).to eq('hi')
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
it 'uses path parameters containing digits for authorization' do
|
|
504
|
+
get '/v1/tests/ipv4/1', {}, input: ''
|
|
505
|
+
|
|
506
|
+
expect(last_response.status).to eq(403)
|
|
507
|
+
expect(api_response).not_to be_ok
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
it 'uses colon-style route parameters for authorization' do
|
|
511
|
+
get '/v1/tests/accounts/2/secret', {}, input: ''
|
|
512
|
+
|
|
513
|
+
expect(last_response.status).to eq(403)
|
|
514
|
+
expect(api_response).not_to be_ok
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
it 'does not let JSON body keys shadow route parameters for authorization' do
|
|
518
|
+
call_api(:put, '/v1/tests/profiles/1', {
|
|
519
|
+
profile_id: '2',
|
|
520
|
+
test: {
|
|
521
|
+
name: 'attacker'
|
|
522
|
+
}
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
expect(last_response.status).to eq(403)
|
|
526
|
+
expect(api_response).not_to be_ok
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
it 'does not reintroduce filtered input defaults' do
|
|
530
|
+
call_api([:Test], :filtered_default, { test: { name: 'acct' } })
|
|
531
|
+
|
|
532
|
+
expect(last_response.status).to eq(200)
|
|
533
|
+
expect(api_response).to be_ok
|
|
534
|
+
expect(api_response[:test][:saw_admin]).to be(false)
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
it 'applies input authorization filters to metadata input' do
|
|
538
|
+
call_api([:Test], :metadata_input, { _meta: { confirmed: true } })
|
|
539
|
+
|
|
540
|
+
expect(last_response.status).to eq(200)
|
|
541
|
+
expect(api_response).to be_ok
|
|
542
|
+
expect(api_response[:test][:saw_confirmed]).to be(false)
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
it 'applies output authorization filters to global metadata' do
|
|
546
|
+
call_api([:Test], :metadata_output, {})
|
|
547
|
+
|
|
548
|
+
expect(last_response.status).to eq(200)
|
|
549
|
+
expect(api_response).to be_ok
|
|
550
|
+
expect(api_response.response[:_meta]).to include(public_status: 'queued')
|
|
551
|
+
expect(api_response.response[:_meta]).not_to have_key(:secret_status)
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
it 'filters unnamespaced input in the safe params view' do
|
|
555
|
+
call_api([:Test], :unnamespaced_input, { public: 'ok', secret: 'hidden' })
|
|
556
|
+
|
|
557
|
+
expect(last_response.status).to eq(200)
|
|
558
|
+
expect(api_response).to be_ok
|
|
559
|
+
expect(api_response[:test][:params_saw_secret]).to be(false)
|
|
560
|
+
expect(api_response[:test][:input_saw_secret]).to be(false)
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
it 'does not expose top-level JSON keys outside the input namespace as safe params' do
|
|
564
|
+
call_api([:Test], :top_level_body, {
|
|
565
|
+
secret: 'top-level hidden',
|
|
566
|
+
test: {
|
|
567
|
+
public: 'ok',
|
|
568
|
+
secret: 'nested hidden'
|
|
569
|
+
}
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
expect(last_response.status).to eq(200)
|
|
573
|
+
expect(api_response).to be_ok
|
|
574
|
+
expect(api_response[:test][:params_saw_secret]).to be(false)
|
|
575
|
+
expect(api_response[:test][:input_saw_secret]).to be(false)
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
it 'denies OPTIONS for actions without an authorization block without raising' do
|
|
579
|
+
call_api(:options, '/v1/tests/closed?method=POST')
|
|
580
|
+
|
|
581
|
+
expect(last_response.status).to eq(403)
|
|
582
|
+
expect(api_response).not_to be_ok
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
it 'rejects invalid route argument encoding in action descriptions' do
|
|
586
|
+
call_api(:options, '/v1/tests/%FF?method=GET')
|
|
587
|
+
|
|
588
|
+
expect(last_response.status).to eq(400)
|
|
589
|
+
expect(api_response).not_to be_ok
|
|
590
|
+
expect(api_response.message).to eq('invalid path parameter encoding')
|
|
591
|
+
end
|
|
592
|
+
|
|
167
593
|
it 'runs prepare, pre_exec, exec in order' do
|
|
168
594
|
action_class(:order).calls.clear
|
|
169
595
|
|
data/spec/action_state_spec.rb
CHANGED
|
@@ -2,6 +2,17 @@ require 'time'
|
|
|
2
2
|
|
|
3
3
|
module ActionStateSpec
|
|
4
4
|
FIXED_TIME = Time.utc(2020, 1, 1, 0, 0, 0)
|
|
5
|
+
User = Struct.new(:id)
|
|
6
|
+
|
|
7
|
+
class BasicProvider < HaveAPI::Authentication::Basic::Provider
|
|
8
|
+
protected
|
|
9
|
+
|
|
10
|
+
def find_user(_request, username, password)
|
|
11
|
+
return unless username == 'user' && password == 'pass'
|
|
12
|
+
|
|
13
|
+
User.new(1)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
5
16
|
|
|
6
17
|
class State
|
|
7
18
|
attr_reader :id, :label, :created_at, :updated_at, :status, :progress, :poll_calls
|
|
@@ -209,6 +220,16 @@ describe HaveAPI::Resources::ActionState do
|
|
|
209
220
|
expect(state.poll_calls).to eq(0)
|
|
210
221
|
end
|
|
211
222
|
|
|
223
|
+
it 'rejects excessive poll timeout values' do
|
|
224
|
+
ActionStateSpec::Backend.add_state(ActionStateSpec::State.new(id: 1))
|
|
225
|
+
|
|
226
|
+
get_action '/v1/action_states/1/poll', action_state: { timeout: 31 }
|
|
227
|
+
|
|
228
|
+
expect(last_response.status).to eq(400)
|
|
229
|
+
expect(api_response).not_to be_ok
|
|
230
|
+
expect(api_response.errors[:timeout].first).to include('range <0, 30>')
|
|
231
|
+
end
|
|
232
|
+
|
|
212
233
|
it 'poll returns immediately when update_in check mismatches' do
|
|
213
234
|
state = ActionStateSpec::State.new(
|
|
214
235
|
id: 1,
|
|
@@ -298,4 +319,35 @@ describe HaveAPI::Resources::ActionState do
|
|
|
298
319
|
expect(api_response.message).to eq('not supported')
|
|
299
320
|
end
|
|
300
321
|
end
|
|
322
|
+
|
|
323
|
+
context 'with action_state backend and authentication' do
|
|
324
|
+
empty_api
|
|
325
|
+
use_version 1
|
|
326
|
+
default_version 1
|
|
327
|
+
action_state ActionStateSpec::Backend
|
|
328
|
+
auth_chain ActionStateSpec::BasicProvider
|
|
329
|
+
|
|
330
|
+
before do
|
|
331
|
+
ActionStateSpec::Backend.reset!
|
|
332
|
+
ActionStateSpec::Backend.add_state(ActionStateSpec::State.new(id: 1))
|
|
333
|
+
header 'Accept', 'application/json'
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
it 'requires authentication for action state resources' do
|
|
337
|
+
get_action '/v1/action_states'
|
|
338
|
+
|
|
339
|
+
expect(last_response.status).to eq(401)
|
|
340
|
+
expect(api_response).not_to be_ok
|
|
341
|
+
expect(ActionStateSpec::Backend.list_calls).to be_empty
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
it 'passes authenticated users to the action state backend' do
|
|
345
|
+
login('user', 'pass')
|
|
346
|
+
get_action '/v1/action_states'
|
|
347
|
+
|
|
348
|
+
expect(last_response.status).to eq(200)
|
|
349
|
+
expect(api_response).to be_ok
|
|
350
|
+
expect(ActionStateSpec::Backend.list_calls.last[:user].id).to eq(1)
|
|
351
|
+
end
|
|
352
|
+
end
|
|
301
353
|
end
|
|
@@ -105,4 +105,33 @@ describe HaveAPI::Authentication::Basic::Provider do
|
|
|
105
105
|
expect(api_response).to be_failed
|
|
106
106
|
expect(seen_users.last).to be_nil
|
|
107
107
|
end
|
|
108
|
+
|
|
109
|
+
it 'treats malformed Authorization headers as failed authentication' do
|
|
110
|
+
invalid = (+"Basic \xFF").force_encoding(Encoding::UTF_8)
|
|
111
|
+
header 'Authorization', invalid
|
|
112
|
+
|
|
113
|
+
call_api(:post, '/v1/secures/ping', {})
|
|
114
|
+
|
|
115
|
+
expect(last_response.status).to eq(401)
|
|
116
|
+
expect(api_response).to be_failed
|
|
117
|
+
expect(seen_users.last).to be_nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'returns an authentication error envelope from _login' do
|
|
121
|
+
get '/_login'
|
|
122
|
+
|
|
123
|
+
expect(last_response.status).to eq(401)
|
|
124
|
+
expect(last_response.headers['www-authenticate']).to include('Basic realm=')
|
|
125
|
+
expect(api_response).to be_failed
|
|
126
|
+
expect(api_response.message).to include('authenticate')
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'returns an authentication error envelope from _logout' do
|
|
130
|
+
get '/_logout'
|
|
131
|
+
|
|
132
|
+
expect(last_response.status).to eq(401)
|
|
133
|
+
expect(last_response.headers['www-authenticate']).to include('Basic realm=')
|
|
134
|
+
expect(api_response).to be_failed
|
|
135
|
+
expect(api_response.message).to include('authenticate')
|
|
136
|
+
end
|
|
108
137
|
end
|