haveapi 0.4.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +11 -0
  3. data/Rakefile +1 -0
  4. data/haveapi.gemspec +1 -1
  5. data/lib/haveapi/authentication/chain.rb +4 -0
  6. data/lib/haveapi/hooks.rb +68 -11
  7. data/lib/haveapi/model_adapters/active_record.rb +1 -1
  8. data/lib/haveapi/{params/param.rb → parameters/typed.rb} +4 -1
  9. data/lib/haveapi/params.rb +2 -2
  10. data/lib/haveapi/spec/api_builder.rb +75 -0
  11. data/lib/haveapi/spec/api_response.rb +41 -0
  12. data/lib/haveapi/spec/helpers.rb +5 -99
  13. data/lib/haveapi/spec/mock_action.rb +32 -0
  14. data/lib/haveapi/spec/spec_methods.rb +121 -0
  15. data/lib/haveapi/version.rb +1 -1
  16. data/lib/haveapi/views/main_layout.erb +3 -1
  17. data/lib/haveapi.rb +1 -1
  18. data/spec/action/dsl_spec.rb +199 -0
  19. data/spec/authorization_spec.rb +113 -0
  20. data/spec/common_spec.rb +47 -0
  21. data/spec/documentation_spec.rb +29 -0
  22. data/spec/envelope_spec.rb +33 -0
  23. data/spec/hooks_spec.rb +146 -0
  24. data/spec/parameters/typed_spec.rb +93 -0
  25. data/spec/params_spec.rb +190 -0
  26. data/spec/resource_spec.rb +63 -0
  27. data/spec/spec_helper.rb +21 -0
  28. data/spec/validators/acceptance_spec.rb +29 -0
  29. data/spec/validators/confirmation_spec.rb +46 -0
  30. data/spec/validators/custom_spec.rb +6 -0
  31. data/spec/validators/exclusion_spec.rb +32 -0
  32. data/spec/validators/format_spec.rb +54 -0
  33. data/spec/validators/inclusion_spec.rb +43 -0
  34. data/spec/validators/length_spec.rb +45 -0
  35. data/spec/validators/numericality_spec.rb +70 -0
  36. data/spec/validators/presence_spec.rb +47 -0
  37. metadata +27 -4
  38. /data/lib/haveapi/{params → parameters}/resource.rb +0 -0
@@ -0,0 +1,199 @@
1
+ describe HaveAPI::Action do
2
+ context 'DSL' do
3
+ it 'inherits input' do
4
+ class Resource < HaveAPI::Resource
5
+ class InputAction < HaveAPI::Action
6
+ input do
7
+ string :param
8
+ end
9
+ end
10
+
11
+ class SubInputAction < InputAction ; end
12
+ end
13
+
14
+ # Invokes execution of input/output blocks
15
+ Resource.routes
16
+ expect(Resource::SubInputAction.input.params.first.name).to eq(:param)
17
+ end
18
+
19
+ it 'inherits output' do
20
+ class Resource < HaveAPI::Resource
21
+ class OutputAction < HaveAPI::Action
22
+ output do
23
+ string :param
24
+ end
25
+ end
26
+
27
+ class SubOutputAction < OutputAction ; end
28
+ end
29
+
30
+ # Invokes execution of input/output blocks
31
+ Resource.routes
32
+ expect(Resource::SubOutputAction.output.params.first.name).to eq(:param)
33
+ end
34
+
35
+ it 'chains input' do
36
+ class Resource < HaveAPI::Resource
37
+ class InputChainAction < HaveAPI::Action
38
+ input do
39
+ string :param1
40
+ end
41
+
42
+ input do
43
+ string :param2
44
+ end
45
+ end
46
+ end
47
+
48
+ # Invokes execution of input/output blocks
49
+ Resource.routes
50
+
51
+ params = Resource::InputChainAction.input.params.map { |p| p.name }
52
+ expect(params).to contain_exactly(:param1, :param2)
53
+ end
54
+
55
+ it 'chains output' do
56
+ class Resource < HaveAPI::Resource
57
+ class OutputChainAction < HaveAPI::Action
58
+ output do
59
+ string :param1
60
+ end
61
+
62
+ output do
63
+ string :param2
64
+ end
65
+ end
66
+ end
67
+
68
+ # Invokes execution of input/output blocks
69
+ Resource.routes
70
+
71
+ params = Resource::OutputChainAction.output.params.map { |p| p.name }
72
+ expect(params).to contain_exactly(:param1, :param2)
73
+ end
74
+
75
+ 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
85
+
86
+ output do
87
+ string :outbase1
88
+ end
89
+
90
+ output do
91
+ string :outbase2
92
+ end
93
+ end
94
+
95
+ class SubAction < BaseAction
96
+ input do
97
+ string :insub1
98
+ string :insub2
99
+ end
100
+
101
+ input do
102
+ string :insub3
103
+ end
104
+
105
+ output do
106
+ string :outsub1
107
+ string :outsub2
108
+ end
109
+
110
+ output do
111
+ string :outsub3
112
+ end
113
+ end
114
+ end
115
+
116
+ # Invokes execution of input/output blocks
117
+ Resource.routes
118
+
119
+ input = Resource::SubAction.input.params.map { |p| p.name }
120
+ output = Resource::SubAction.output.params.map { |p| p.name }
121
+
122
+ expect(input).to contain_exactly(*%i(inbase1 inbase2 insub1 insub2 insub3))
123
+ expect(output).to contain_exactly(*%i(outbase1 outbase2 outsub1 outsub2 outsub3))
124
+ end
125
+
126
+ 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
154
+ end
155
+
156
+ expect(Resource::DefaultLayoutAction.input.layout).to eq(:object)
157
+ expect(Resource::DefaultLayoutAction.output.layout).to eq(:object)
158
+
159
+ expect(Resource::ObjectLayoutAction.input.layout).to eq(:object)
160
+ expect(Resource::ObjectLayoutAction.output.layout).to eq(:object)
161
+
162
+ expect(Resource::ObjectListLayoutAction.input.layout).to eq(:object_list)
163
+ expect(Resource::ObjectListLayoutAction.output.layout).to eq(:object_list)
164
+
165
+ expect(Resource::HashLayoutAction.input.layout).to eq(:hash)
166
+ expect(Resource::HashLayoutAction.output.layout).to eq(:hash)
167
+
168
+ expect(Resource::HashListLayoutAction.input.layout).to eq(:hash_list)
169
+ expect(Resource::HashListLayoutAction.output.layout).to eq(:hash_list)
170
+
171
+ expect(Resource::CombinedLayoutAction.input.layout).to eq(:hash)
172
+ expect(Resource::CombinedLayoutAction.output.layout).to eq(:object_list)
173
+ end
174
+
175
+ it 'catches exceptions in input' do
176
+ class ExResourceIn < HaveAPI::Resource
177
+ class ExInputAction < HaveAPI::Action
178
+ input do
179
+ fail 'this is terrible!'
180
+ end
181
+ end
182
+ end
183
+
184
+ expect { ExResourceIn.routes }.to raise_error(HaveAPI::BuildError)
185
+ end
186
+
187
+ it 'catches exceptions in output' do
188
+ class ExResourceOut < HaveAPI::Resource
189
+ class ExOutputAction < HaveAPI::Action
190
+ output do
191
+ fail 'this is terrible!'
192
+ end
193
+ end
194
+ end
195
+
196
+ expect { ExResourceOut.routes }.to raise_error(HaveAPI::BuildError)
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,113 @@
1
+ describe HaveAPI::Authorization do
2
+ class Resource < HaveAPI::Resource
3
+ class Index < HaveAPI::Actions::Default::Index
4
+ input do
5
+ string :param1
6
+ string :param2
7
+ end
8
+
9
+ output do
10
+ string :param1
11
+ string :param2
12
+ end
13
+ end
14
+
15
+ routes
16
+ end
17
+
18
+ it 'defaults to deny' do
19
+ auth = HaveAPI::Authorization.new {}
20
+ expect(auth.authorized?(nil)).to be false
21
+ end
22
+
23
+ it 'can authorize' do
24
+ auth = HaveAPI::Authorization.new { allow }
25
+ expect(auth.authorized?(nil)).to be true
26
+ end
27
+
28
+ it 'applies restrictions' do
29
+ auth = HaveAPI::Authorization.new do
30
+ restrict filter: true
31
+ allow
32
+ end
33
+
34
+ expect(auth.authorized?(nil)).to be true
35
+ expect(auth.restrictions[:filter]).to be true
36
+ end
37
+
38
+ it 'whitelists input' do
39
+ auth = HaveAPI::Authorization.new do
40
+ input whitelist: %i(param1)
41
+ allow
42
+ end
43
+
44
+ expect(auth.authorized?(nil)).to be true
45
+
46
+ action = Resource::Index
47
+
48
+ expect(auth.filter_input(
49
+ action.input.params,
50
+ action.model_adapter(action.input.layout).input({
51
+ param1: '123',
52
+ param2: '456',
53
+ })
54
+ ).keys).to contain_exactly(:param1)
55
+ end
56
+
57
+ it 'blacklists input' do
58
+ auth = HaveAPI::Authorization.new do
59
+ input blacklist: %i(param1)
60
+ allow
61
+ end
62
+
63
+ expect(auth.authorized?(nil)).to be true
64
+
65
+ action = Resource::Index
66
+
67
+ expect(auth.filter_input(
68
+ action.input.params,
69
+ action.model_adapter(action.input.layout).input({
70
+ param1: '123',
71
+ param2: '456',
72
+ })
73
+ ).keys).to contain_exactly(:param2)
74
+ end
75
+
76
+ it 'whitelists output' do
77
+ auth = HaveAPI::Authorization.new do
78
+ output whitelist: %i(param1)
79
+ allow
80
+ end
81
+
82
+ expect(auth.authorized?(nil)).to be true
83
+
84
+ action = Resource::Index
85
+
86
+ expect(auth.filter_output(
87
+ action.output.params,
88
+ action.model_adapter(action.output.layout).output(nil, {
89
+ param1: '123',
90
+ param2: '456',
91
+ })
92
+ ).keys).to contain_exactly(:param1)
93
+ end
94
+
95
+ it 'blacklists output' do
96
+ auth = HaveAPI::Authorization.new do
97
+ output blacklist: %i(param1)
98
+ allow
99
+ end
100
+
101
+ expect(auth.authorized?(nil)).to be true
102
+
103
+ action = Resource::Index
104
+
105
+ expect(auth.filter_output(
106
+ action.output.params,
107
+ action.model_adapter(action.output.layout).output(nil, {
108
+ param1: '123',
109
+ param2: '456',
110
+ })
111
+ ).keys).to contain_exactly(:param2)
112
+ end
113
+ end
@@ -0,0 +1,47 @@
1
+ describe HaveAPI::Common do
2
+ class Test1 < HaveAPI::Common
3
+ has_attr :attr1
4
+ has_attr :attr2, 42
5
+ end
6
+
7
+ it 'defines attributes' do
8
+ expect(Test1.attr1).to be_nil
9
+ expect(Test1.attr2).to eq(42)
10
+ end
11
+
12
+ class Test2 < HaveAPI::Common
13
+ has_attr :attr1
14
+ has_attr :attr2, 42
15
+ end
16
+
17
+ it 'sets attributes' do
18
+ Test2.attr1 'val1'
19
+ Test2.attr2 663
20
+
21
+ expect(Test2.attr1).to eq('val1')
22
+ expect(Test2.attr2).to eq(663)
23
+ end
24
+
25
+ class Test3 < HaveAPI::Common
26
+ has_attr :attr1
27
+ has_attr :attr2, 42
28
+ has_attr :attr3
29
+
30
+ attr1 'foo'
31
+ attr2 :bar
32
+
33
+ def self.inherited(subclass)
34
+ inherit_attrs(subclass)
35
+ end
36
+ end
37
+
38
+ class SubTest3 < Test3
39
+ attr3 'bar'
40
+ end
41
+
42
+ it 'inherites attributes' do
43
+ expect(SubTest3.attr1).to eq('foo')
44
+ expect(SubTest3.attr2).to eq(:bar)
45
+ expect(SubTest3.attr3).to eq('bar')
46
+ end
47
+ end
@@ -0,0 +1,29 @@
1
+ describe 'Documentation' do
2
+ it 'responds to OPTIONS /' do
3
+
4
+ end
5
+
6
+ it 'responds to OPTIONS /?describe=versions' do
7
+
8
+ end
9
+
10
+ it 'responds to OPTIONS /?describe=default' do
11
+
12
+ end
13
+
14
+ it 'responds to OPTIONS /<version>' do
15
+
16
+ end
17
+
18
+ it 'responds to OPTIONS /<every action>?method=<method>' do
19
+
20
+ end
21
+
22
+ it 'has online doc' do
23
+
24
+ end
25
+
26
+ it 'has online doc for every version' do
27
+
28
+ end
29
+ end
@@ -0,0 +1,33 @@
1
+ describe 'Envelope' do
2
+ context 'documentation' do
3
+ empty_api
4
+
5
+ it 'returns correct envelope' do
6
+ call_api(:options, '/')
7
+ expect(api_response.envelope.keys).to contain_exactly(
8
+ *%i(version status response message errors)
9
+ )
10
+ end
11
+
12
+ it 'succeeds' do
13
+ call_api(:options, '/')
14
+ expect(api_response).to be_ok
15
+ end
16
+ end
17
+
18
+ context 'data' do
19
+ empty_api
20
+
21
+ it 'returns correct envelope' do
22
+ call_api(:get, '/unknown_resource')
23
+ expect(api_response.envelope.keys).to contain_exactly(
24
+ *%i(status response message errors)
25
+ )
26
+ end
27
+
28
+ it 'fails' do
29
+ call_api(:get, '/unknown_resource')
30
+ expect(api_response).to_not be_ok
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,146 @@
1
+ describe HaveAPI::Hooks do
2
+ class ClassLevel
3
+ include HaveAPI::Hookable
4
+
5
+ has_hook :simple_hook
6
+ has_hook :arg_hook
7
+ has_hook :ret_hook
8
+ has_hook :context_hook
9
+ end
10
+
11
+ class InstanceLevel
12
+ include HaveAPI::Hookable
13
+
14
+ has_hook :simple_hook
15
+ has_hook :arg_hook
16
+ has_hook :ret_hook
17
+ has_hook :context_hook
18
+ end
19
+
20
+ def connect_hook(name, &block)
21
+ @obj.connect_hook(name, &block)
22
+ end
23
+
24
+ def call_hooks(*args)
25
+ case @level
26
+ when :class
27
+ @obj.call_hooks(*args)
28
+
29
+ when :instance
30
+ if @method
31
+ @obj.method(@method).call(*args)
32
+
33
+ else
34
+ @obj.call_hooks_for(*args)
35
+ end
36
+
37
+ else
38
+ fail "unknown level '#{@level}'"
39
+ end
40
+ end
41
+
42
+ shared_examples(:common) do
43
+ it 'calls hooks' do
44
+ called = false
45
+
46
+ connect_hook(:simple_hook) do |ret|
47
+ called = true
48
+ ret
49
+ end
50
+
51
+ call_hooks(:simple_hook)
52
+ expect(called).to be true
53
+ end
54
+
55
+ it 'passes arguments' do
56
+ called = false
57
+
58
+ connect_hook(:arg_hook) do |ret, a, b, c|
59
+ called = true
60
+ expect([a, b, c]).to eq([1, 2, 3])
61
+ ret
62
+ end
63
+
64
+ call_hooks(:arg_hook, args: [1, 2, 3])
65
+ expect(called).to be true
66
+ end
67
+
68
+ it 'chains hooks' do
69
+ arr = []
70
+
71
+ 5.times do |i|
72
+ connect_hook(:simple_hook) do |ret|
73
+ arr << i
74
+ ret
75
+ end
76
+ end
77
+
78
+ call_hooks(:simple_hook)
79
+ expect(arr).to eq([0, 1, 2, 3, 4])
80
+ end
81
+
82
+ it 'chains return value' do
83
+ 5.times do
84
+ connect_hook(:ret_hook) do |ret|
85
+ ret[:counter] += 1
86
+ ret
87
+ end
88
+ end
89
+
90
+ sum = call_hooks(:ret_hook, initial: {counter: 0})
91
+ expect(sum[:counter]).to eq(5)
92
+ end
93
+
94
+ it 'executes block in given context' do
95
+ class CustomEnv
96
+ def foo
97
+ 'bar'
98
+ end
99
+ end
100
+
101
+ connect_hook(:context_hook) do |ret|
102
+ ret[:val] = foo
103
+ ret
104
+ end
105
+
106
+ res = call_hooks(:context_hook, CustomEnv.new)
107
+ expect(res[:val]).to eq('bar')
108
+ end
109
+ end
110
+
111
+ context 'on class level' do
112
+ before(:each) do
113
+ @obj = ClassLevel
114
+ @level = :class
115
+ end
116
+
117
+ include_examples :common
118
+ end
119
+
120
+ context 'on instance level' do
121
+ context 'all hooks' do
122
+ before(:each) do
123
+ @obj = InstanceLevel.new
124
+ @level = :instance
125
+ end
126
+
127
+ include_examples :common
128
+ end
129
+
130
+ context 'only class hooks' do
131
+ # FIXME: class hooks fail (that is correct, no class hooks are defined)
132
+ # must find a way to test failure
133
+ #include_examples :common
134
+ end
135
+
136
+ context 'only instance hooks' do
137
+ before(:each) do
138
+ @obj = InstanceLevel.new
139
+ @level = :instance
140
+ @method = :call_instance_hooks_for
141
+ end
142
+
143
+ include_examples :common
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,93 @@
1
+ require 'time'
2
+
3
+ describe 'Parameters::Typed' do
4
+ def p_type(type)
5
+ HaveAPI::Parameters::Typed.new(:param1, type: type)
6
+ end
7
+
8
+ def p_arg(arg = {})
9
+ HaveAPI::Parameters::Typed.new(*[:param1, arg])
10
+ end
11
+
12
+ it 'does not change provided arguments' do
13
+ kwargs = {
14
+ label: 'Param 1',
15
+ desc: 'Desc',
16
+ required: true,
17
+ }
18
+ p_arg(kwargs)
19
+ expect(kwargs.keys).to contain_exactly(*%i(label desc required))
20
+ end
21
+
22
+ it 'automatically sets label' do
23
+ p = p_arg
24
+ expect(p.label).to eq('Param1')
25
+ end
26
+
27
+ it 'accepts custom label' do
28
+ p = p_arg(label: 'Custom')
29
+ expect(p.label).to eq('Custom')
30
+ end
31
+
32
+ it 'rejects unknown parameters' do
33
+ expect do
34
+ p_arg(shiny: true)
35
+ end.to raise_error(RuntimeError)
36
+ end
37
+
38
+ it 'can be required' do
39
+ p = p_arg(required: true)
40
+ expect(p.required?).to be true
41
+ end
42
+
43
+ it 'can be optional' do
44
+ p = p_arg
45
+ expect(p.optional?).to be true
46
+
47
+ p = p_arg(required: false)
48
+ expect(p.optional?).to be true
49
+
50
+ p = p_arg(required: nil)
51
+ expect(p.optional?).to be true
52
+ end
53
+
54
+ it 'cleans input value' do
55
+ # Integer
56
+ p = p_type(Integer)
57
+ expect(p.clean('42')).to eq(42)
58
+
59
+ # Float
60
+ p = p_type(Float)
61
+ expect(p.clean('3.1456')).to eq(3.1456)
62
+
63
+ # Boolean
64
+ p = p_type(Boolean)
65
+
66
+ %w(true t yes y 1).each do |v|
67
+ expect(p.clean(v)).to be true
68
+ end
69
+
70
+ %w(false f no n 0).each do |v|
71
+ expect(p.clean(v)).to be false
72
+ end
73
+
74
+ # Datetime
75
+ p = p_type(Datetime)
76
+ t = Time.now
77
+ t2 = Time.iso8601(t.iso8601)
78
+
79
+ expect(p.clean(t.iso8601)).to eq(t2)
80
+ expect { p.clean('bzz') }.to raise_error(HaveAPI::ValidationError)
81
+
82
+ # String, Text
83
+ p = p_type(String)
84
+ expect(p.clean('bzz')).to eq('bzz')
85
+
86
+ # Defaults
87
+ p = p_type(String)
88
+ expect(p.clean(nil)).to be_nil
89
+
90
+ p.patch(default: 'bazinga')
91
+ expect(p.clean(nil)).to eq('bazinga')
92
+ end
93
+ end