haveapi 0.27.3 → 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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/haveapi.gemspec +1 -1
  4. data/lib/haveapi/action.rb +125 -36
  5. data/lib/haveapi/actions/paginable.rb +3 -1
  6. data/lib/haveapi/authentication/basic/provider.rb +2 -0
  7. data/lib/haveapi/authentication/chain.rb +11 -7
  8. data/lib/haveapi/authentication/oauth2/config.rb +25 -3
  9. data/lib/haveapi/authentication/oauth2/provider.rb +92 -11
  10. data/lib/haveapi/authentication/oauth2/revoke_endpoint.rb +44 -3
  11. data/lib/haveapi/authentication/token/provider.rb +53 -15
  12. data/lib/haveapi/authorization.rb +42 -18
  13. data/lib/haveapi/client_examples/php_client.rb +1 -1
  14. data/lib/haveapi/client_examples/ruby_client.rb +1 -1
  15. data/lib/haveapi/context.rb +10 -4
  16. data/lib/haveapi/example.rb +15 -16
  17. data/lib/haveapi/extensions/action_exceptions.rb +6 -6
  18. data/lib/haveapi/model_adapters/active_record.rb +140 -68
  19. data/lib/haveapi/model_adapters/hash.rb +1 -1
  20. data/lib/haveapi/parameters/resource.rb +35 -3
  21. data/lib/haveapi/parameters/typed.rb +26 -7
  22. data/lib/haveapi/params.rb +27 -8
  23. data/lib/haveapi/resource.rb +4 -1
  24. data/lib/haveapi/resources/action_state.rb +8 -1
  25. data/lib/haveapi/route.rb +2 -2
  26. data/lib/haveapi/server.rb +137 -45
  27. data/lib/haveapi/validator.rb +2 -2
  28. data/lib/haveapi/validator_chain.rb +1 -0
  29. data/lib/haveapi/validators/confirmation.rb +1 -0
  30. data/lib/haveapi/validators/format.rb +6 -2
  31. data/lib/haveapi/validators/length.rb +2 -0
  32. data/lib/haveapi/validators/numericality.rb +2 -0
  33. data/lib/haveapi/validators/presence.rb +1 -1
  34. data/lib/haveapi/version.rb +1 -1
  35. data/lib/haveapi/views/version_page/client_auth.erb +1 -1
  36. data/lib/haveapi/views/version_page/client_example.erb +3 -3
  37. data/lib/haveapi/views/version_page/client_init.erb +1 -1
  38. data/lib/haveapi/views/version_page.erb +2 -2
  39. data/lib/haveapi/views/version_sidebar.erb +4 -2
  40. data/spec/action/authorize_spec.rb +99 -0
  41. data/spec/action/runtime_spec.rb +426 -0
  42. data/spec/action_state_spec.rb +52 -0
  43. data/spec/authentication/basic_spec.rb +29 -0
  44. data/spec/authentication/oauth2_spec.rb +329 -0
  45. data/spec/authentication/token_spec.rb +195 -0
  46. data/spec/authentication/token_version_routes_spec.rb +164 -0
  47. data/spec/authorization_spec.rb +66 -0
  48. data/spec/documentation/auth_filtering_spec.rb +195 -1
  49. data/spec/documentation/current_user_html_escaping_spec.rb +47 -0
  50. data/spec/documentation/examples_spec.rb +97 -0
  51. data/spec/documentation/host_html_escaping_spec.rb +41 -0
  52. data/spec/documentation_spec.rb +13 -0
  53. data/spec/extensions/action_exceptions_spec.rb +30 -0
  54. data/spec/model_adapters/active_record_spec.rb +406 -1
  55. data/spec/parameters/typed_spec.rb +42 -0
  56. data/spec/params_spec.rb +41 -0
  57. data/spec/server/integration_spec.rb +90 -0
  58. data/spec/validator_chain_spec.rb +39 -0
  59. data/spec/validators/confirmation_spec.rb +14 -0
  60. data/spec/validators/format_spec.rb +7 -0
  61. data/spec/validators/length_spec.rb +6 -0
  62. data/spec/validators/numericality_spec.rb +7 -0
  63. data/spec/validators/presence_spec.rb +2 -0
  64. data/test_support/client_test_api.rb +28 -0
  65. metadata +8 -4
  66. data/shell.nix +0 -20
@@ -1,5 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ module ServerIntegrationSpec
4
+ module State
5
+ class << self
6
+ def reset!
7
+ writes.clear
8
+ end
9
+
10
+ def writes
11
+ @writes ||= []
12
+ end
13
+ end
14
+ end
15
+ end
16
+
3
17
  describe HaveAPI::Server do
4
18
  describe 'integration' do
5
19
  api do
@@ -24,10 +38,38 @@ describe HaveAPI::Server do
24
38
  end
25
39
  end
26
40
  end
41
+
42
+ define_resource(:Transfer) do
43
+ version 1
44
+ auth false
45
+
46
+ define_action(:Create) do
47
+ route ''
48
+ http_method :post
49
+ authorize { allow }
50
+
51
+ input(:hash) do
52
+ integer :amount, required: true
53
+ end
54
+
55
+ output(:hash) do
56
+ bool :created
57
+ end
58
+
59
+ def exec
60
+ ServerIntegrationSpec::State.writes << input[:amount]
61
+ { created: true }
62
+ end
63
+ end
64
+ end
27
65
  end
28
66
 
29
67
  default_version 1
30
68
 
69
+ before do
70
+ ServerIntegrationSpec::State.reset!
71
+ end
72
+
31
73
  it 'returns 406 for unsupported Accept' do
32
74
  header 'Accept', 'text/plain'
33
75
  options '/v1/'
@@ -45,6 +87,54 @@ describe HaveAPI::Server do
45
87
  expect(api_response.message).to match(/Bad JSON syntax/)
46
88
  end
47
89
 
90
+ it 'returns 400 for non-object JSON bodies' do
91
+ header 'Content-Type', 'application/json'
92
+ header 'Accept', 'application/json'
93
+
94
+ ['[]', '"msg"', '123', 'true', 'null'].each do |body|
95
+ post '/v1/tests/echo', body
96
+
97
+ expect(last_response.status).to eq(400)
98
+ expect(api_response).not_to be_ok
99
+ expect(api_response.message).to eq('JSON body must be an object')
100
+ end
101
+ end
102
+
103
+ it 'does not accept query-string input for non-GET actions' do
104
+ header 'Accept', 'application/json'
105
+ post '/v1/transfers?transfer[amount]=250', nil, {
106
+ 'CONTENT_TYPE' => 'application/x-www-form-urlencoded'
107
+ }
108
+
109
+ expect(last_response.status).to eq(400)
110
+ expect(api_response).not_to be_ok
111
+ expect(ServerIntegrationSpec::State.writes).to be_empty
112
+ end
113
+
114
+ it 'rejects non-JSON content types for JSON action bodies' do
115
+ header 'Accept', 'application/json'
116
+ post '/v1/transfers', '{"transfer":{"amount":250}}', {
117
+ 'CONTENT_TYPE' => 'text/plain'
118
+ }
119
+
120
+ expect(last_response.status).to eq(415)
121
+ expect(api_response).not_to be_ok
122
+ expect(api_response.message).to eq('Unsupported Content-Type')
123
+ expect(ServerIntegrationSpec::State.writes).to be_empty
124
+ end
125
+
126
+ it 'returns 400 for malformed Accept headers' do
127
+ invalid_accept = (+"\xFF").force_encoding(Encoding::UTF_8)
128
+ header 'Accept', invalid_accept
129
+ header 'Content-Type', 'application/json'
130
+
131
+ post '/v1/tests/echo', '{}'
132
+
133
+ expect(last_response.status).to eq(400)
134
+ expect(api_response).not_to be_ok
135
+ expect(api_response.message).to eq('Bad Accept header')
136
+ end
137
+
48
138
  it 'returns JSON envelope for unknown route' do
49
139
  header 'Accept', 'application/json'
50
140
  get '/does-not-exist'
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HaveAPI::Validators
4
+ class CloneProbe < HaveAPI::Validator
5
+ name :clone_probe
6
+ takes :clone_probe
7
+
8
+ class << self
9
+ attr_accessor :seen
10
+ end
11
+
12
+ self.seen = []
13
+
14
+ def setup
15
+ @message = 'invalid'
16
+ end
17
+
18
+ def describe
19
+ {}
20
+ end
21
+
22
+ def valid?(_v)
23
+ self.class.seen << object_id
24
+ true
25
+ end
26
+ end
27
+ end
28
+
29
+ describe HaveAPI::ValidatorChain do
30
+ it 'validates with per-call validator clones' do
31
+ chain = described_class.new(clone_probe: true)
32
+ original = chain.instance_variable_get(:@validators).first
33
+
34
+ HaveAPI::Validators::CloneProbe.seen.clear
35
+ expect(chain.validate('value', {})).to be true
36
+
37
+ expect(HaveAPI::Validators::CloneProbe.seen).not_to include(original.object_id)
38
+ end
39
+ end
@@ -41,4 +41,18 @@ describe HaveAPI::Validators::Confirmation do
41
41
  expect(validator.validate('bar', { other_param: 'foo' })).to be true
42
42
  end
43
43
  end
44
+
45
+ context 'with a string parameter reference' do
46
+ let(:validator) do
47
+ described_class.new(:confirm, {
48
+ param: 'other_param',
49
+ equal: false
50
+ })
51
+ end
52
+
53
+ it 'looks up normalized input keys' do
54
+ expect(validator.validate('foo', { other_param: 'foo' })).to be false
55
+ expect(validator.validate('bar', { other_param: 'foo' })).to be true
56
+ end
57
+ end
44
58
  end
@@ -49,4 +49,11 @@ describe HaveAPI::Validators::Format do
49
49
  expect(validator.valid?('b')).to be true
50
50
  end
51
51
  end
52
+
53
+ it 'rejects values that cannot be converted to strings' do
54
+ validator = described_class.new(:format, /\A[a-z]+\z/)
55
+
56
+ expect(validator.valid?(['abc'])).to be false
57
+ expect(validator.valid?({ value: 'abc' })).to be false
58
+ end
52
59
  end
@@ -42,4 +42,10 @@ describe HaveAPI::Validators::Length do
42
42
  expect(v.valid?('a' * 4)).to be true
43
43
  expect(v.valid?('a' * 5)).to be false
44
44
  end
45
+
46
+ it 'rejects values without length' do
47
+ validator = described_class.new(:length, { min: 2 })
48
+
49
+ expect(validator.valid?(1)).to be false
50
+ end
45
51
  end
@@ -77,4 +77,11 @@ describe HaveAPI::Validators::Numericality do
77
77
  v = described_class.new(:number, { min: 5 })
78
78
  expect(v.valid?('abc')).to be false
79
79
  end
80
+
81
+ it 'rejects non-numeric values before numeric operations' do
82
+ validator = described_class.new(:number, { odd: true })
83
+
84
+ expect(validator.valid?(['5'])).to be false
85
+ expect(validator.valid?({ value: 5 })).to be false
86
+ end
80
87
  end
@@ -8,6 +8,8 @@ describe HaveAPI::Validators::Presence do
8
8
  expect(validator.valid?(nil)).to be false
9
9
  expect(validator.valid?('')).to be false
10
10
  expect(validator.valid?(" \t" * 4)).to be false
11
+ expect(validator.valid?([])).to be false
12
+ expect(validator.valid?({})).to be false
11
13
  end
12
14
  end
13
15
 
@@ -270,6 +270,7 @@ module HaveAPI
270
270
 
271
271
  define_action(:Index, superclass: HaveAPI::Actions::Default::Index) do
272
272
  extend DocFilter
273
+
273
274
  resolve { |obj| obj[:id] }
274
275
  output(:object_list) { use :all }
275
276
  authorize { allow }
@@ -285,6 +286,7 @@ module HaveAPI
285
286
 
286
287
  define_action(:Show, superclass: HaveAPI::Actions::Default::Show) do
287
288
  extend DocFilter
289
+
288
290
  resolve { |obj| obj[:id] }
289
291
  output(:object) { use :all }
290
292
  authorize { allow }
@@ -298,6 +300,7 @@ module HaveAPI
298
300
 
299
301
  define_action(:Create, superclass: HaveAPI::Actions::Default::Create) do
300
302
  extend DocFilter
303
+
301
304
  resolve { |obj| obj[:id] }
302
305
  input(:hash) do
303
306
  string :name, required: true
@@ -324,6 +327,7 @@ module HaveAPI
324
327
 
325
328
  define_action(:Index, superclass: HaveAPI::Actions::Default::Index) do
326
329
  extend DocFilter
330
+
327
331
  resolve { |obj| [obj[:project_id], obj[:id]] }
328
332
  output(:object_list) { use :all }
329
333
  authorize { allow }
@@ -339,6 +343,7 @@ module HaveAPI
339
343
 
340
344
  define_action(:Show, superclass: HaveAPI::Actions::Default::Show) do
341
345
  extend DocFilter
346
+
342
347
  resolve { |obj| [obj[:project_id], obj[:id]] }
343
348
  output(:object) { use :all }
344
349
  authorize { allow }
@@ -352,6 +357,7 @@ module HaveAPI
352
357
 
353
358
  define_action(:Create, superclass: HaveAPI::Actions::Default::Create) do
354
359
  extend DocFilter
360
+
355
361
  resolve { |obj| [obj[:project_id], obj[:id]] }
356
362
  input(:hash) do
357
363
  string :label, required: true
@@ -371,6 +377,7 @@ module HaveAPI
371
377
 
372
378
  define_action(:Update, superclass: HaveAPI::Actions::Default::Update) do
373
379
  extend DocFilter
380
+
374
381
  resolve { |obj| [obj[:project_id], obj[:id]] }
375
382
  input(:hash) do
376
383
  bool :done
@@ -391,6 +398,7 @@ module HaveAPI
391
398
 
392
399
  define_action(:Run) do
393
400
  extend DocFilter
401
+
394
402
  route '{task_id}/run'
395
403
  http_method :post
396
404
  blocking true
@@ -420,6 +428,7 @@ module HaveAPI
420
428
 
421
429
  define_action(:Fail) do
422
430
  extend DocFilter
431
+
423
432
  route 'fail'
424
433
  http_method :get
425
434
  output(:hash) {}
@@ -430,8 +439,23 @@ module HaveAPI
430
439
  end
431
440
  end
432
441
 
442
+ define_action(:Slow) do
443
+ extend DocFilter
444
+
445
+ route 'slow'
446
+ http_method :get
447
+ output(:hash) {}
448
+ authorize { allow }
449
+
450
+ def exec
451
+ sleep 1
452
+ {}
453
+ end
454
+ end
455
+
433
456
  define_action(:Echo) do
434
457
  extend DocFilter
458
+
435
459
  route 'echo'
436
460
  http_method :post
437
461
  input(:hash) do
@@ -459,6 +483,7 @@ module HaveAPI
459
483
 
460
484
  define_action(:EchoOptional) do
461
485
  extend DocFilter
486
+
462
487
  route 'echo_optional'
463
488
  http_method :post
464
489
  input(:hash) do
@@ -483,6 +508,7 @@ module HaveAPI
483
508
 
484
509
  define_action(:EchoOptionalGet) do
485
510
  extend DocFilter
511
+
486
512
  route 'echo_optional_get'
487
513
  http_method :get
488
514
  input(:hash) do
@@ -507,6 +533,7 @@ module HaveAPI
507
533
 
508
534
  define_action(:EchoResource) do
509
535
  extend DocFilter
536
+
510
537
  route 'echo_resource'
511
538
  http_method :post
512
539
  input(:hash) do
@@ -524,6 +551,7 @@ module HaveAPI
524
551
 
525
552
  define_action(:EchoResourceOptional) do
526
553
  extend DocFilter
554
+
527
555
  route 'echo_resource_optional'
528
556
  http_method :get
529
557
  input(:hash) do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: haveapi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.27.3
4
+ version: 0.28.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jakub Skokan
@@ -29,14 +29,14 @@ dependencies:
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: 0.27.3
32
+ version: 0.28.0
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 0.27.3
39
+ version: 0.28.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: json
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -293,7 +293,6 @@ files:
293
293
  - lib/haveapi/views/version_sidebar.erb
294
294
  - lib/haveapi/views/version_sidebar/auth_nav.erb
295
295
  - lib/haveapi/views/version_sidebar/resource_nav.erb
296
- - shell.nix
297
296
  - spec/.rubocop.yml
298
297
  - spec/action/authorize_spec.rb
299
298
  - spec/action/dsl_spec.rb
@@ -302,9 +301,13 @@ files:
302
301
  - spec/authentication/basic_spec.rb
303
302
  - spec/authentication/oauth2_spec.rb
304
303
  - spec/authentication/token_spec.rb
304
+ - spec/authentication/token_version_routes_spec.rb
305
305
  - spec/authorization_spec.rb
306
306
  - spec/common_spec.rb
307
307
  - spec/documentation/auth_filtering_spec.rb
308
+ - spec/documentation/current_user_html_escaping_spec.rb
309
+ - spec/documentation/examples_spec.rb
310
+ - spec/documentation/host_html_escaping_spec.rb
308
311
  - spec/documentation_spec.rb
309
312
  - spec/envelope_spec.rb
310
313
  - spec/extensions/action_exceptions_spec.rb
@@ -315,6 +318,7 @@ files:
315
318
  - spec/resource_spec.rb
316
319
  - spec/server/integration_spec.rb
317
320
  - spec/spec_helper.rb
321
+ - spec/validator_chain_spec.rb
318
322
  - spec/validators/acceptance_spec.rb
319
323
  - spec/validators/confirmation_spec.rb
320
324
  - spec/validators/custom_spec.rb
data/shell.nix DELETED
@@ -1,20 +0,0 @@
1
- let
2
- pkgs = import <nixpkgs> {};
3
- stdenv = pkgs.stdenv;
4
-
5
- in stdenv.mkDerivation rec {
6
- name = "haveapi";
7
-
8
- buildInputs = with pkgs;[
9
- ruby_3_3
10
- git
11
- openssl
12
- ];
13
-
14
- shellHook = ''
15
- export GEM_HOME=$(pwd)/../../.gems
16
- export PATH="$GEM_HOME/bin:$PATH"
17
- gem install bundler
18
- bundle install
19
- '';
20
- }