haveapi 0.26.4 → 0.27.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/Gemfile +7 -3
  4. data/haveapi.gemspec +1 -1
  5. data/lib/haveapi/action.rb +1 -1
  6. data/lib/haveapi/authentication/base.rb +2 -0
  7. data/lib/haveapi/authentication/oauth2/provider.rb +10 -2
  8. data/lib/haveapi/authentication/token/provider.rb +25 -1
  9. data/lib/haveapi/model_adapters/active_record.rb +61 -4
  10. data/lib/haveapi/parameters/typed.rb +94 -10
  11. data/lib/haveapi/params.rb +6 -1
  12. data/lib/haveapi/resource.rb +1 -1
  13. data/lib/haveapi/server.rb +10 -1
  14. data/lib/haveapi/spec/api_builder.rb +8 -3
  15. data/lib/haveapi/spec/spec_methods.rb +20 -10
  16. data/lib/haveapi/version.rb +1 -1
  17. data/spec/action/authorize_spec.rb +317 -0
  18. data/spec/action/dsl_spec.rb +98 -100
  19. data/spec/action/runtime_spec.rb +207 -0
  20. data/spec/action_state_spec.rb +301 -0
  21. data/spec/authentication/basic_spec.rb +108 -0
  22. data/spec/authentication/oauth2_spec.rb +127 -0
  23. data/spec/authentication/token_spec.rb +233 -0
  24. data/spec/authorization_spec.rb +23 -18
  25. data/spec/common_spec.rb +19 -17
  26. data/spec/documentation/auth_filtering_spec.rb +111 -0
  27. data/spec/documentation_spec.rb +165 -2
  28. data/spec/envelope_spec.rb +5 -9
  29. data/spec/extensions/action_exceptions_spec.rb +163 -0
  30. data/spec/hooks_spec.rb +32 -38
  31. data/spec/model_adapters/active_record_spec.rb +411 -0
  32. data/spec/parameters/typed_spec.rb +54 -1
  33. data/spec/params_spec.rb +27 -25
  34. data/spec/resource_spec.rb +36 -22
  35. data/spec/server/integration_spec.rb +71 -0
  36. data/spec/spec_helper.rb +2 -2
  37. data/spec/validators/acceptance_spec.rb +10 -12
  38. data/spec/validators/confirmation_spec.rb +14 -16
  39. data/spec/validators/custom_spec.rb +1 -1
  40. data/spec/validators/exclusion_spec.rb +13 -15
  41. data/spec/validators/format_spec.rb +20 -22
  42. data/spec/validators/inclusion_spec.rb +13 -15
  43. data/spec/validators/length_spec.rb +6 -6
  44. data/spec/validators/numericality_spec.rb +10 -10
  45. data/spec/validators/presence_spec.rb +16 -22
  46. data/test_support/client_test_api.rb +583 -0
  47. data/test_support/client_test_server.rb +59 -0
  48. metadata +16 -3
@@ -0,0 +1,317 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ module AuthorizeSpec
6
+ User = Struct.new(:id, :login, :admin) do
7
+ def admin?
8
+ admin
9
+ end
10
+ end
11
+
12
+ class BasicProvider < HaveAPI::Authentication::Basic::Provider
13
+ protected
14
+
15
+ def find_user(_request, username, password)
16
+ return nil unless password == 'pass'
17
+
18
+ case username
19
+ when 'user'
20
+ User.new(1, 'user', false)
21
+ when 'admin'
22
+ User.new(2, 'admin', true)
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ describe AuthorizeSpec do
29
+ api do
30
+ define_resource(:Item) do
31
+ version 1
32
+ route 'items'
33
+
34
+ define_action(:AdminOnly) do
35
+ route 'admin_only'
36
+ http_method :get
37
+
38
+ authorize do |user|
39
+ allow if user&.admin?
40
+ deny
41
+ end
42
+
43
+ output do
44
+ string :msg
45
+ end
46
+
47
+ def exec
48
+ { msg: 'ok' }
49
+ end
50
+ end
51
+
52
+ define_action(:Echo) do
53
+ route 'echo'
54
+ http_method :post
55
+
56
+ authorize do |user|
57
+ unless user&.admin?
58
+ input blacklist: %i[secret nested.hidden]
59
+ output blacklist: %i[secret nested.hidden]
60
+ end
61
+
62
+ allow
63
+ end
64
+
65
+ input do
66
+ string :public
67
+ string :secret
68
+ string :'nested.visible'
69
+ string :'nested.hidden'
70
+ end
71
+
72
+ output do
73
+ string :public
74
+ string :secret
75
+ string :'nested.visible'
76
+ string :'nested.hidden'
77
+ bool :seen_secret
78
+ bool :seen_nested_hidden
79
+ end
80
+
81
+ def exec
82
+ {
83
+ public: input[:public],
84
+ secret: input[:secret],
85
+ 'nested.visible': input[:'nested.visible'],
86
+ 'nested.hidden': input[:'nested.hidden'],
87
+ seen_secret: input.has_key?(:secret),
88
+ seen_nested_hidden: input.has_key?(:'nested.hidden')
89
+ }
90
+ end
91
+ end
92
+
93
+ items = [
94
+ { id: 1, owner_id: 1, name: 'u1' },
95
+ { id: 2, owner_id: 2, name: 'u2' },
96
+ { id: 3, owner_id: 1, name: 'u1b' }
97
+ ].freeze
98
+
99
+ define_action(:List) do
100
+ route 'list'
101
+ http_method :get
102
+
103
+ authorize do |user|
104
+ restrict owner_id: user.id
105
+ allow
106
+ end
107
+
108
+ input do
109
+ integer :owner_id
110
+ end
111
+
112
+ output(:object_list) do
113
+ integer :id
114
+ integer :owner_id
115
+ string :name
116
+ end
117
+
118
+ define_method(:exec) do
119
+ restrictions = with_restricted(owner_id: input[:owner_id])
120
+ items.select { |item| item[:owner_id] == restrictions[:owner_id] }
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ default_version 1
127
+ auth_chain AuthorizeSpec::BasicProvider
128
+
129
+ let(:echo_input) do
130
+ {
131
+ item: {
132
+ public: 'pub',
133
+ secret: 'shh',
134
+ 'nested.visible': 'visible',
135
+ 'nested.hidden': 'hidden'
136
+ }
137
+ }
138
+ end
139
+
140
+ def call_get_action(resource, action, params = {})
141
+ env 'rack.input', StringIO.new('')
142
+ call_api(resource, action, params)
143
+ end
144
+
145
+ it 'denies non-admins from admin-only action' do
146
+ login('user', 'pass')
147
+ call_get_action([:Item], :admin_only, {})
148
+
149
+ expect(last_response.status).to eq(403)
150
+ expect(api_response).to be_failed
151
+ expect(api_response.message).to match(/not authorized|forbidden|denied|access denied/i)
152
+ end
153
+
154
+ it 'allows admins to access admin-only action' do
155
+ login('admin', 'pass')
156
+ call_get_action([:Item], :admin_only, {})
157
+
158
+ expect(last_response.status).to eq(200)
159
+ expect(api_response).to be_ok
160
+ expect(api_response[:item][:msg]).to eq('ok')
161
+ end
162
+
163
+ it 'filters output fields for non-admins' do
164
+ login('user', 'pass')
165
+ call_api([:Item], :echo, echo_input)
166
+
167
+ expect(last_response.status).to eq(200)
168
+ expect(api_response).to be_ok
169
+
170
+ item = api_response[:item]
171
+ expect(item).to include(:public, :'nested.visible', :seen_secret, :seen_nested_hidden)
172
+ expect(item).not_to have_key(:secret)
173
+ expect(item).not_to have_key(:'nested.hidden')
174
+ expect(item[:seen_secret]).to be(false)
175
+ expect(item[:seen_nested_hidden]).to be(false)
176
+ end
177
+
178
+ it 'ignores forbidden input fields for non-admins' do
179
+ login('user', 'pass')
180
+ call_api([:Item], :echo, echo_input)
181
+
182
+ expect(last_response.status).to eq(200)
183
+ expect(api_response).to be_ok
184
+ expect(api_response[:item][:seen_secret]).to be(false)
185
+ expect(api_response[:item][:seen_nested_hidden]).to be(false)
186
+ end
187
+
188
+ it 'accepts allowed input for non-admins' do
189
+ login('user', 'pass')
190
+ call_api([:Item], :echo, { item: { public: 'pub', 'nested.visible': 'visible' } })
191
+
192
+ expect(last_response.status).to eq(200)
193
+ expect(api_response).to be_ok
194
+ expect(api_response[:item][:public]).to eq('pub')
195
+ expect(api_response[:item]).not_to have_key(:secret)
196
+ expect(api_response[:item]).not_to have_key(:'nested.hidden')
197
+ expect(api_response[:item][:seen_secret]).to be(false)
198
+ expect(api_response[:item][:seen_nested_hidden]).to be(false)
199
+ end
200
+
201
+ it 'shows full output for admins' do
202
+ login('admin', 'pass')
203
+ call_api([:Item], :echo, echo_input)
204
+
205
+ expect(last_response.status).to eq(200)
206
+ expect(api_response).to be_ok
207
+
208
+ item = api_response[:item]
209
+ expect(item[:secret]).to eq('shh')
210
+ expect(item[:'nested.hidden']).to eq('hidden')
211
+ expect(item[:seen_secret]).to be(true)
212
+ expect(item[:seen_nested_hidden]).to be(true)
213
+ end
214
+
215
+ it 'does not cache authorization filters between requests' do
216
+ login('user', 'pass')
217
+ call_api([:Item], :echo, echo_input)
218
+
219
+ expect(api_response).to be_ok
220
+ expect(api_response[:item]).not_to have_key(:secret)
221
+
222
+ login('admin', 'pass')
223
+ call_api([:Item], :echo, echo_input)
224
+
225
+ expect(api_response).to be_ok
226
+ expect(api_response[:item]).to have_key(:secret)
227
+ end
228
+
229
+ it 'restricts list results to the current user' do
230
+ login('user', 'pass')
231
+ call_get_action([:Item], :list, {})
232
+
233
+ expect(last_response.status).to eq(200)
234
+ expect(api_response).to be_ok
235
+
236
+ owners = api_response[:items].map { |item| item[:owner_id] }.uniq
237
+ expect(owners).to eq([1])
238
+ end
239
+
240
+ it 'restricts list results to the admin user id' do
241
+ login('admin', 'pass')
242
+ call_get_action([:Item], :list, {})
243
+
244
+ expect(last_response.status).to eq(200)
245
+ expect(api_response).to be_ok
246
+
247
+ owners = api_response[:items].map { |item| item[:owner_id] }.uniq
248
+ expect(owners).to eq([2])
249
+ end
250
+
251
+ it 'overrides client-supplied filters with restrictions' do
252
+ login('user', 'pass')
253
+ call_get_action([:Item], :list, { item: { owner_id: 2 } })
254
+
255
+ expect(last_response.status).to eq(200)
256
+ expect(api_response).to be_ok
257
+
258
+ owners = api_response[:items].map { |item| item[:owner_id] }.uniq
259
+ expect(owners).to eq([1])
260
+ end
261
+
262
+ it 'hides admin-only actions from non-admin documentation' do
263
+ login('user', 'pass')
264
+ call_api(:options, '/v1/')
265
+
266
+ expect(last_response.status).to eq(200)
267
+ expect(api_response).to be_ok
268
+
269
+ actions = api_response[:resources][:item][:actions]
270
+ expect(actions).to have_key(:echo)
271
+ expect(actions).to have_key(:list)
272
+ expect(actions).not_to have_key(:admin_only)
273
+ end
274
+
275
+ it 'shows admin-only actions for admin documentation' do
276
+ login('admin', 'pass')
277
+ call_api(:options, '/v1/')
278
+
279
+ expect(last_response.status).to eq(200)
280
+ expect(api_response).to be_ok
281
+
282
+ actions = api_response[:resources][:item][:actions]
283
+ expect(actions).to have_key(:admin_only)
284
+ end
285
+
286
+ it 'filters action docs input and output for non-admins' do
287
+ login('user', 'pass')
288
+ call_api(:options, '/v1/items/echo?method=POST')
289
+
290
+ expect(last_response.status).to eq(200)
291
+ expect(api_response).to be_ok
292
+
293
+ input_params = api_response[:input][:parameters]
294
+ output_params = api_response[:output][:parameters]
295
+
296
+ expect(input_params).not_to have_key(:secret)
297
+ expect(input_params).not_to have_key(:'nested.hidden')
298
+ expect(output_params).not_to have_key(:secret)
299
+ expect(output_params).not_to have_key(:'nested.hidden')
300
+ end
301
+
302
+ it 'shows full action docs input and output for admins' do
303
+ login('admin', 'pass')
304
+ call_api(:options, '/v1/items/echo?method=POST')
305
+
306
+ expect(last_response.status).to eq(200)
307
+ expect(api_response).to be_ok
308
+
309
+ input_params = api_response[:input][:parameters]
310
+ output_params = api_response[:output][:parameters]
311
+
312
+ expect(input_params).to have_key(:secret)
313
+ expect(input_params).to have_key(:'nested.hidden')
314
+ expect(output_params).to have_key(:secret)
315
+ expect(output_params).to have_key(:'nested.hidden')
316
+ end
317
+ end
@@ -1,15 +1,27 @@
1
1
  describe HaveAPI::Action do
2
- context 'DSL' do
2
+ def stub_resource_class(const_name)
3
+ resource_class = Class.new(HaveAPI::Resource)
4
+ stub_const(const_name, resource_class)
5
+ resource_class
6
+ end
7
+
8
+ def build_action(resource_const, action_const, superclass = HaveAPI::Action, &block)
9
+ klass = Class.new(superclass)
10
+ stub_const("#{resource_const}::#{action_const}", klass)
11
+ klass.superclass.delayed_inherited(klass)
12
+ klass.class_exec(&block) if block
13
+ klass
14
+ end
15
+
16
+ context 'with DSL' do
3
17
  it 'inherits input' do
4
- class Resource < HaveAPI::Resource
5
- class InputAction < HaveAPI::Action
6
- input do
7
- string :param
8
- end
18
+ stub_resource_class('Resource')
19
+ input_action_class = build_action('Resource', 'InputAction') do
20
+ input do
21
+ string :param
9
22
  end
10
-
11
- class SubInputAction < InputAction; end
12
23
  end
24
+ build_action('Resource', 'SubInputAction', input_action_class)
13
25
 
14
26
  # Invokes execution of input/output blocks
15
27
  Resource.routes
@@ -17,15 +29,13 @@ describe HaveAPI::Action do
17
29
  end
18
30
 
19
31
  it 'inherits output' do
20
- class Resource < HaveAPI::Resource
21
- class OutputAction < HaveAPI::Action
22
- output do
23
- string :param
24
- end
32
+ stub_resource_class('Resource')
33
+ output_action_class = build_action('Resource', 'OutputAction') do
34
+ output do
35
+ string :param
25
36
  end
26
-
27
- class SubOutputAction < OutputAction; end
28
37
  end
38
+ build_action('Resource', 'SubOutputAction', output_action_class)
29
39
 
30
40
  # Invokes execution of input/output blocks
31
41
  Resource.routes
@@ -33,15 +43,14 @@ describe HaveAPI::Action do
33
43
  end
34
44
 
35
45
  it 'chains input' do
36
- class Resource < HaveAPI::Resource
37
- class InputChainAction < HaveAPI::Action
38
- input do
39
- string :param1
40
- end
46
+ stub_resource_class('Resource')
47
+ build_action('Resource', 'InputChainAction') do
48
+ input do
49
+ string :param1
50
+ end
41
51
 
42
- input do
43
- string :param2
44
- end
52
+ input do
53
+ string :param2
45
54
  end
46
55
  end
47
56
 
@@ -53,15 +62,14 @@ describe HaveAPI::Action do
53
62
  end
54
63
 
55
64
  it 'chains output' do
56
- class Resource < HaveAPI::Resource
57
- class OutputChainAction < HaveAPI::Action
58
- output do
59
- string :param1
60
- end
65
+ stub_resource_class('Resource')
66
+ build_action('Resource', 'OutputChainAction') do
67
+ output do
68
+ string :param1
69
+ end
61
70
 
62
- output do
63
- string :param2
64
- end
71
+ output do
72
+ string :param2
65
73
  end
66
74
  end
67
75
 
@@ -73,43 +81,41 @@ describe HaveAPI::Action do
73
81
  end
74
82
 
75
83
  it 'can combine chaining and inheritance' do
76
- class Resource < HaveAPI::Resource
77
- class BaseAction < HaveAPI::Action
78
- input do
79
- string :inbase1
80
- end
81
-
82
- input do
83
- string :inbase2
84
- end
84
+ stub_resource_class('Resource')
85
+ base_action_class = build_action('Resource', 'BaseAction') do
86
+ input do
87
+ string :inbase1
88
+ end
85
89
 
86
- output do
87
- string :outbase1
88
- end
90
+ input do
91
+ string :inbase2
92
+ end
89
93
 
90
- output do
91
- string :outbase2
92
- end
94
+ output do
95
+ string :outbase1
93
96
  end
94
97
 
95
- class SubAction < BaseAction
96
- input do
97
- string :insub1
98
- string :insub2
99
- end
98
+ output do
99
+ string :outbase2
100
+ end
101
+ end
102
+ build_action('Resource', 'SubAction', base_action_class) do
103
+ input do
104
+ string :insub1
105
+ string :insub2
106
+ end
100
107
 
101
- input do
102
- string :insub3
103
- end
108
+ input do
109
+ string :insub3
110
+ end
104
111
 
105
- output do
106
- string :outsub1
107
- string :outsub2
108
- end
112
+ output do
113
+ string :outsub1
114
+ string :outsub2
115
+ end
109
116
 
110
- output do
111
- string :outsub3
112
- end
117
+ output do
118
+ string :outsub3
113
119
  end
114
120
  end
115
121
 
@@ -119,38 +125,32 @@ describe HaveAPI::Action do
119
125
  input = Resource::SubAction.input.params.map(&:name)
120
126
  output = Resource::SubAction.output.params.map(&:name)
121
127
 
122
- expect(input).to contain_exactly(*%i[inbase1 inbase2 insub1 insub2 insub3])
123
- expect(output).to contain_exactly(*%i[outbase1 outbase2 outsub1 outsub2 outsub3])
128
+ expect(input).to match_array(%i[inbase1 inbase2 insub1 insub2 insub3])
129
+ expect(output).to match_array(%i[outbase1 outbase2 outsub1 outsub2 outsub3])
124
130
  end
125
131
 
126
132
  it 'sets layout' do
127
- class Resource < HaveAPI::Resource
128
- class DefaultLayoutAction < HaveAPI::Action; end
129
-
130
- class ObjectLayoutAction < HaveAPI::Action
131
- input(:object) {}
132
- output(:object) {}
133
- end
134
-
135
- class ObjectListLayoutAction < HaveAPI::Action
136
- input(:object_list) {}
137
- output(:object_list) {}
138
- end
139
-
140
- class HashLayoutAction < HaveAPI::Action
141
- input(:hash) {}
142
- output(:hash) {}
143
- end
144
-
145
- class HashListLayoutAction < HaveAPI::Action
146
- input(:hash_list) {}
147
- output(:hash_list) {}
148
- end
149
-
150
- class CombinedLayoutAction < HaveAPI::Action
151
- input(:hash) {}
152
- output(:object_list) {}
153
- end
133
+ stub_resource_class('Resource')
134
+ build_action('Resource', 'DefaultLayoutAction')
135
+ build_action('Resource', 'ObjectLayoutAction') do
136
+ input(:object) {}
137
+ output(:object) {}
138
+ end
139
+ build_action('Resource', 'ObjectListLayoutAction') do
140
+ input(:object_list) {}
141
+ output(:object_list) {}
142
+ end
143
+ build_action('Resource', 'HashLayoutAction') do
144
+ input(:hash) {}
145
+ output(:hash) {}
146
+ end
147
+ build_action('Resource', 'HashListLayoutAction') do
148
+ input(:hash_list) {}
149
+ output(:hash_list) {}
150
+ end
151
+ build_action('Resource', 'CombinedLayoutAction') do
152
+ input(:hash) {}
153
+ output(:object_list) {}
154
154
  end
155
155
 
156
156
  expect(Resource::DefaultLayoutAction.input.layout).to eq(:object)
@@ -173,11 +173,10 @@ describe HaveAPI::Action do
173
173
  end
174
174
 
175
175
  it 'catches exceptions in input' do
176
- class ExResourceIn < HaveAPI::Resource
177
- class ExInputAction < HaveAPI::Action
178
- input do
179
- raise 'this is terrible!'
180
- end
176
+ stub_resource_class('ExResourceIn')
177
+ build_action('ExResourceIn', 'ExInputAction') do
178
+ input do
179
+ raise 'this is terrible!'
181
180
  end
182
181
  end
183
182
 
@@ -185,11 +184,10 @@ describe HaveAPI::Action do
185
184
  end
186
185
 
187
186
  it 'catches exceptions in output' do
188
- class ExResourceOut < HaveAPI::Resource
189
- class ExOutputAction < HaveAPI::Action
190
- output do
191
- raise 'this is terrible!'
192
- end
187
+ stub_resource_class('ExResourceOut')
188
+ build_action('ExResourceOut', 'ExOutputAction') do
189
+ output do
190
+ raise 'this is terrible!'
193
191
  end
194
192
  end
195
193