haveapi 0.26.5 → 0.27.1
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/.rspec +1 -0
- data/Gemfile +7 -3
- data/haveapi.gemspec +1 -1
- data/lib/haveapi/action.rb +1 -1
- data/lib/haveapi/authentication/base.rb +2 -0
- data/lib/haveapi/authentication/oauth2/provider.rb +10 -2
- data/lib/haveapi/authentication/token/provider.rb +25 -1
- data/lib/haveapi/model_adapters/active_record.rb +66 -4
- data/lib/haveapi/parameters/resource.rb +8 -1
- data/lib/haveapi/parameters/typed.rb +94 -10
- data/lib/haveapi/params.rb +6 -1
- data/lib/haveapi/resource.rb +1 -1
- data/lib/haveapi/server.rb +10 -1
- data/lib/haveapi/spec/api_builder.rb +8 -3
- data/lib/haveapi/spec/spec_methods.rb +20 -10
- data/lib/haveapi/version.rb +1 -1
- data/spec/action/authorize_spec.rb +317 -0
- data/spec/action/dsl_spec.rb +98 -100
- data/spec/action/runtime_spec.rb +207 -0
- data/spec/action_state_spec.rb +301 -0
- data/spec/authentication/basic_spec.rb +108 -0
- data/spec/authentication/oauth2_spec.rb +127 -0
- data/spec/authentication/token_spec.rb +233 -0
- data/spec/authorization_spec.rb +23 -18
- data/spec/common_spec.rb +19 -17
- data/spec/documentation/auth_filtering_spec.rb +111 -0
- data/spec/documentation_spec.rb +165 -2
- data/spec/envelope_spec.rb +5 -9
- data/spec/extensions/action_exceptions_spec.rb +163 -0
- data/spec/hooks_spec.rb +32 -38
- data/spec/model_adapters/active_record_spec.rb +413 -0
- data/spec/parameters/typed_spec.rb +54 -1
- data/spec/params_spec.rb +27 -25
- data/spec/resource_spec.rb +36 -22
- data/spec/server/integration_spec.rb +71 -0
- data/spec/spec_helper.rb +2 -2
- data/spec/validators/acceptance_spec.rb +10 -12
- data/spec/validators/confirmation_spec.rb +14 -16
- data/spec/validators/custom_spec.rb +1 -1
- data/spec/validators/exclusion_spec.rb +13 -15
- data/spec/validators/format_spec.rb +20 -22
- data/spec/validators/inclusion_spec.rb +13 -15
- data/spec/validators/length_spec.rb +6 -6
- data/spec/validators/numericality_spec.rb +10 -10
- data/spec/validators/presence_spec.rb +16 -22
- data/test_support/client_test_api.rb +607 -0
- data/test_support/client_test_server.rb +59 -0
- metadata +16 -3
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
describe HaveAPI::Action do
|
|
4
|
+
describe 'runtime' do
|
|
5
|
+
api do
|
|
6
|
+
define_resource(:Test) do
|
|
7
|
+
version 1
|
|
8
|
+
auth false
|
|
9
|
+
|
|
10
|
+
define_action(:Echo) do
|
|
11
|
+
http_method :post
|
|
12
|
+
authorize { allow }
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
attr_accessor :calls
|
|
16
|
+
end
|
|
17
|
+
self.calls = []
|
|
18
|
+
|
|
19
|
+
input do
|
|
20
|
+
string :msg
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
output do
|
|
24
|
+
string :msg
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def exec
|
|
28
|
+
self.class.calls << :exec
|
|
29
|
+
{ msg: input[:msg] }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
define_action(:Order) do
|
|
34
|
+
http_method :post
|
|
35
|
+
authorize { allow }
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
attr_accessor :calls
|
|
39
|
+
end
|
|
40
|
+
self.calls = []
|
|
41
|
+
|
|
42
|
+
output do
|
|
43
|
+
bool :ok
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def prepare
|
|
47
|
+
self.class.calls << :prepare
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def pre_exec
|
|
51
|
+
self.class.calls << :pre_exec
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def exec
|
|
55
|
+
self.class.calls << :exec
|
|
56
|
+
{ ok: true }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
define_action(:Abort) do
|
|
61
|
+
http_method :post
|
|
62
|
+
authorize { allow }
|
|
63
|
+
|
|
64
|
+
class << self
|
|
65
|
+
attr_accessor :calls
|
|
66
|
+
end
|
|
67
|
+
self.calls = []
|
|
68
|
+
|
|
69
|
+
output do
|
|
70
|
+
bool :ok
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def exec
|
|
74
|
+
self.class.calls << :exec
|
|
75
|
+
error!('nope')
|
|
76
|
+
self.class.calls << :after_error
|
|
77
|
+
{ ok: true }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
define_action(:Boom) do
|
|
82
|
+
http_method :post
|
|
83
|
+
authorize { allow }
|
|
84
|
+
|
|
85
|
+
class << self
|
|
86
|
+
attr_accessor :calls
|
|
87
|
+
end
|
|
88
|
+
self.calls = []
|
|
89
|
+
|
|
90
|
+
output do
|
|
91
|
+
bool :ok
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def exec
|
|
95
|
+
self.class.calls << :exec
|
|
96
|
+
raise 'boom'
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
define_action(:Block) do
|
|
101
|
+
http_method :post
|
|
102
|
+
authorize { allow }
|
|
103
|
+
blocking true
|
|
104
|
+
|
|
105
|
+
class << self
|
|
106
|
+
attr_accessor :calls
|
|
107
|
+
end
|
|
108
|
+
self.calls = []
|
|
109
|
+
|
|
110
|
+
output do
|
|
111
|
+
bool :ok
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def exec
|
|
115
|
+
self.class.calls << :exec
|
|
116
|
+
{ ok: true }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def state_id
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
default_version 1
|
|
127
|
+
|
|
128
|
+
def action_class(name)
|
|
129
|
+
action, = find_action(1, :Test, name)
|
|
130
|
+
action
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def with_exec_exception_hook
|
|
134
|
+
hooks = HaveAPI::Hooks.hooks
|
|
135
|
+
action_hooks = hooks[HaveAPI::Action][:exec_exception]
|
|
136
|
+
original = action_hooks[:listeners].dup
|
|
137
|
+
|
|
138
|
+
HaveAPI::Action.connect_hook(:exec_exception) do |ret, _context, e|
|
|
139
|
+
ret[:status] = false
|
|
140
|
+
ret[:message] = e.message
|
|
141
|
+
ret[:http_status] = 422
|
|
142
|
+
ret
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
yield
|
|
146
|
+
ensure
|
|
147
|
+
action_hooks[:listeners] = original
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it '_meta.no suppresses metadata' do
|
|
151
|
+
action_class(:echo).calls.clear
|
|
152
|
+
|
|
153
|
+
call_api([:Test], :echo, { test: { msg: 'hi' }, _meta: { no: true } })
|
|
154
|
+
|
|
155
|
+
expect(last_response.status).to eq(200)
|
|
156
|
+
expect(api_response).to be_ok
|
|
157
|
+
expect(api_response.response).to have_key(:test)
|
|
158
|
+
expect(api_response.response).not_to have_key(:_meta)
|
|
159
|
+
|
|
160
|
+
call_api([:Test], :echo, { test: { msg: 'hi' }, _meta: { no: false } })
|
|
161
|
+
|
|
162
|
+
expect(last_response.status).to eq(200)
|
|
163
|
+
expect(api_response).to be_ok
|
|
164
|
+
expect(api_response.response).to have_key(:_meta)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it 'runs prepare, pre_exec, exec in order' do
|
|
168
|
+
action_class(:order).calls.clear
|
|
169
|
+
|
|
170
|
+
call_api([:Test], :order, {})
|
|
171
|
+
|
|
172
|
+
expect(api_response).to be_ok
|
|
173
|
+
expect(action_class(:order).calls).to eq(%i[prepare pre_exec exec])
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it 'aborts execution when error! is called' do
|
|
177
|
+
action_class(:abort).calls.clear
|
|
178
|
+
|
|
179
|
+
call_api([:Test], :abort, {})
|
|
180
|
+
|
|
181
|
+
expect(api_response).not_to be_ok
|
|
182
|
+
expect(api_response.message).to eq('nope')
|
|
183
|
+
expect(action_class(:abort).calls).to include(:exec)
|
|
184
|
+
expect(action_class(:abort).calls).not_to include(:after_error)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it 'routes exec exceptions through hook' do
|
|
188
|
+
with_exec_exception_hook do
|
|
189
|
+
call_api([:Test], :boom, {})
|
|
190
|
+
|
|
191
|
+
expect(last_response.status).to eq(422)
|
|
192
|
+
expect(api_response).not_to be_ok
|
|
193
|
+
expect(api_response.message).to eq('boom')
|
|
194
|
+
expect(last_response.body).not_to match(/<html/i)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
it 'adds action_state_id to meta for blocking actions' do
|
|
199
|
+
call_api([:Test], :block, {})
|
|
200
|
+
|
|
201
|
+
expect(api_response).to be_ok
|
|
202
|
+
meta = api_response.response[:_meta]
|
|
203
|
+
expect(meta).to be_a(Hash)
|
|
204
|
+
expect(meta).to have_key(:action_state_id)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
require 'time'
|
|
2
|
+
|
|
3
|
+
module ActionStateSpec
|
|
4
|
+
FIXED_TIME = Time.utc(2020, 1, 1, 0, 0, 0)
|
|
5
|
+
|
|
6
|
+
class State
|
|
7
|
+
attr_reader :id, :label, :created_at, :updated_at, :status, :progress, :poll_calls
|
|
8
|
+
|
|
9
|
+
def initialize(id:, label: 'job', status: true, finished: false, can_cancel: false,
|
|
10
|
+
progress: {}, valid: true, cancel_ret: true)
|
|
11
|
+
@id = id
|
|
12
|
+
@label = label
|
|
13
|
+
@status = status
|
|
14
|
+
@finished = finished
|
|
15
|
+
@can_cancel = can_cancel
|
|
16
|
+
@progress = progress
|
|
17
|
+
@valid = valid
|
|
18
|
+
@cancel_ret = cancel_ret
|
|
19
|
+
@poll_calls = 0
|
|
20
|
+
@created_at = FIXED_TIME
|
|
21
|
+
@updated_at = FIXED_TIME
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def valid?
|
|
25
|
+
@valid
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def finished?
|
|
29
|
+
@finished
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def can_cancel?
|
|
33
|
+
@can_cancel
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def poll(_input)
|
|
37
|
+
@poll_calls += 1
|
|
38
|
+
|
|
39
|
+
if @progress[:current]
|
|
40
|
+
@progress = @progress.merge(current: @progress[:current] + 1)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
@updated_at = Time.utc(2020, 1, 1, 0, 0, @poll_calls)
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def cancel
|
|
48
|
+
@cancel_ret
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
module Backend
|
|
53
|
+
class << self
|
|
54
|
+
attr_reader :states, :list_calls, :new_calls
|
|
55
|
+
|
|
56
|
+
def reset!
|
|
57
|
+
@states = {}
|
|
58
|
+
@list_calls = []
|
|
59
|
+
@new_calls = []
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def add_state(state)
|
|
63
|
+
@states[state.id] = state
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def list_pending(user, from_id, limit, order)
|
|
67
|
+
@list_calls << {
|
|
68
|
+
user: user,
|
|
69
|
+
from_id: from_id,
|
|
70
|
+
limit: limit,
|
|
71
|
+
order: order
|
|
72
|
+
}
|
|
73
|
+
@states.values
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def new(user, id:)
|
|
77
|
+
@new_calls << { user: user, id: id.to_i }
|
|
78
|
+
@states[id.to_i] || State.new(id: id.to_i, valid: false)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe HaveAPI::Resources::ActionState do
|
|
85
|
+
def get_action(path, params = nil)
|
|
86
|
+
if params
|
|
87
|
+
get path, params, input: ''
|
|
88
|
+
else
|
|
89
|
+
get path, {}, input: ''
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
context 'without action_state backend' do
|
|
94
|
+
empty_api
|
|
95
|
+
use_version 1
|
|
96
|
+
default_version 1
|
|
97
|
+
|
|
98
|
+
it 'is not mounted without action_state backend' do
|
|
99
|
+
header 'Accept', 'application/json'
|
|
100
|
+
get_action '/v1/action_states'
|
|
101
|
+
|
|
102
|
+
expect(last_response.status).to eq(404)
|
|
103
|
+
expect(api_response).not_to be_ok
|
|
104
|
+
expect(api_response.message).to eq('Action not found')
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
context 'with action_state backend' do
|
|
109
|
+
empty_api
|
|
110
|
+
use_version 1
|
|
111
|
+
default_version 1
|
|
112
|
+
action_state ActionStateSpec::Backend
|
|
113
|
+
|
|
114
|
+
before do
|
|
115
|
+
ActionStateSpec::Backend.reset!
|
|
116
|
+
header 'Accept', 'application/json'
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'lists pending states and passes paging options' do
|
|
120
|
+
ActionStateSpec::Backend.add_state(ActionStateSpec::State.new(id: 1))
|
|
121
|
+
ActionStateSpec::Backend.add_state(ActionStateSpec::State.new(id: 2))
|
|
122
|
+
|
|
123
|
+
get_action '/v1/action_states', action_state: { from_id: 10, limit: 2, order: 'oldest' }
|
|
124
|
+
|
|
125
|
+
expect(last_response.status).to eq(200)
|
|
126
|
+
expect(api_response).to be_ok
|
|
127
|
+
|
|
128
|
+
states = api_response[:action_states]
|
|
129
|
+
expect(states).to be_a(Array)
|
|
130
|
+
expect(states.size).to eq(2)
|
|
131
|
+
|
|
132
|
+
states.each do |state|
|
|
133
|
+
expect(state.keys).to contain_exactly(
|
|
134
|
+
:id, :label, :status, :finished, :current, :total, :unit,
|
|
135
|
+
:can_cancel, :created_at, :updated_at
|
|
136
|
+
)
|
|
137
|
+
expect(state[:created_at]).to eq(ActionStateSpec::FIXED_TIME.iso8601)
|
|
138
|
+
expect(state[:updated_at]).to eq(ActionStateSpec::FIXED_TIME.iso8601)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
call = ActionStateSpec::Backend.list_calls.last
|
|
142
|
+
expect(call[:user]).to be_nil
|
|
143
|
+
expect(call[:from_id]).to eq(10)
|
|
144
|
+
expect(call[:limit]).to eq(2)
|
|
145
|
+
expect(call[:order]).to eq(:oldest)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'defaults order to newest' do
|
|
149
|
+
ActionStateSpec::Backend.add_state(ActionStateSpec::State.new(id: 1))
|
|
150
|
+
|
|
151
|
+
get_action '/v1/action_states'
|
|
152
|
+
|
|
153
|
+
expect(ActionStateSpec::Backend.list_calls.last[:order]).to eq(:newest)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it 'shows state when valid' do
|
|
157
|
+
ActionStateSpec::Backend.add_state(
|
|
158
|
+
ActionStateSpec::State.new(id: 1, progress: {}, can_cancel: true)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
get_action '/v1/action_states/1'
|
|
162
|
+
|
|
163
|
+
expect(api_response).to be_ok
|
|
164
|
+
state = api_response[:action_state]
|
|
165
|
+
expect(state[:id]).to eq(1)
|
|
166
|
+
expect(state[:current]).to eq(0)
|
|
167
|
+
expect(state[:total]).to eq(0)
|
|
168
|
+
expect(state[:unit]).to be_nil
|
|
169
|
+
expect(state[:can_cancel]).to be(true)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it 'returns error when state invalid' do
|
|
173
|
+
get_action '/v1/action_states/999'
|
|
174
|
+
|
|
175
|
+
expect(api_response).not_to be_ok
|
|
176
|
+
expect(api_response.message).to eq('action state not found')
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it 'poll returns immediately if finished' do
|
|
180
|
+
state = ActionStateSpec::State.new(
|
|
181
|
+
id: 1,
|
|
182
|
+
finished: true,
|
|
183
|
+
progress: { current: 5, total: 10, unit: 'items' }
|
|
184
|
+
)
|
|
185
|
+
ActionStateSpec::Backend.add_state(state)
|
|
186
|
+
|
|
187
|
+
get_action '/v1/action_states/1/poll', action_state: { timeout: 5 }
|
|
188
|
+
|
|
189
|
+
expect(api_response).to be_ok
|
|
190
|
+
ret = api_response[:action_state]
|
|
191
|
+
expect(ret[:finished]).to be(true)
|
|
192
|
+
expect(ret[:current]).to eq(5)
|
|
193
|
+
expect(ret[:total]).to eq(10)
|
|
194
|
+
expect(ret[:unit]).to eq('items')
|
|
195
|
+
expect(state.poll_calls).to eq(0)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
it 'poll returns immediately on timeout without polling' do
|
|
199
|
+
state = ActionStateSpec::State.new(
|
|
200
|
+
id: 1,
|
|
201
|
+
finished: false,
|
|
202
|
+
progress: { current: 0, total: 10 }
|
|
203
|
+
)
|
|
204
|
+
ActionStateSpec::Backend.add_state(state)
|
|
205
|
+
|
|
206
|
+
get_action '/v1/action_states/1/poll', action_state: { timeout: 0 }
|
|
207
|
+
|
|
208
|
+
expect(api_response).to be_ok
|
|
209
|
+
expect(state.poll_calls).to eq(0)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it 'poll returns immediately when update_in check mismatches' do
|
|
213
|
+
state = ActionStateSpec::State.new(
|
|
214
|
+
id: 1,
|
|
215
|
+
status: true,
|
|
216
|
+
progress: { current: 1, total: 10 }
|
|
217
|
+
)
|
|
218
|
+
ActionStateSpec::Backend.add_state(state)
|
|
219
|
+
|
|
220
|
+
get_action '/v1/action_states/1/poll', action_state: {
|
|
221
|
+
timeout: 10,
|
|
222
|
+
update_in: 5,
|
|
223
|
+
status: false,
|
|
224
|
+
current: 999,
|
|
225
|
+
total: 10
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
expect(api_response).to be_ok
|
|
229
|
+
ret = api_response[:action_state]
|
|
230
|
+
expect(ret[:status]).to be(true)
|
|
231
|
+
expect(ret[:current]).to eq(1)
|
|
232
|
+
expect(ret[:total]).to eq(10)
|
|
233
|
+
expect(state.poll_calls).to eq(0)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
it 'poll uses state.poll when available' do
|
|
237
|
+
state = ActionStateSpec::State.new(
|
|
238
|
+
id: 1,
|
|
239
|
+
finished: false,
|
|
240
|
+
progress: { current: 0, total: 10 }
|
|
241
|
+
)
|
|
242
|
+
ActionStateSpec::Backend.add_state(state)
|
|
243
|
+
|
|
244
|
+
get_action '/v1/action_states/1/poll', action_state: { timeout: 10 }
|
|
245
|
+
|
|
246
|
+
expect(api_response).to be_ok
|
|
247
|
+
ret = api_response[:action_state]
|
|
248
|
+
expect(state.poll_calls).to eq(1)
|
|
249
|
+
expect(ret[:current]).to eq(1)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
it 'poll returns error when state invalid' do
|
|
253
|
+
get_action '/v1/action_states/999/poll', action_state: { timeout: 0 }
|
|
254
|
+
|
|
255
|
+
expect(api_response).not_to be_ok
|
|
256
|
+
expect(api_response.message).to eq('action state not found')
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
it 'cancel returns ok for true' do
|
|
260
|
+
ActionStateSpec::Backend.add_state(ActionStateSpec::State.new(id: 1, cancel_ret: true))
|
|
261
|
+
|
|
262
|
+
call_api(:post, '/v1/action_states/1/cancel', {})
|
|
263
|
+
|
|
264
|
+
expect(api_response).to be_ok
|
|
265
|
+
expect(api_response.response[:_meta]).to be_a(Hash)
|
|
266
|
+
expect(api_response.response[:_meta]).to have_key(:action_state_id)
|
|
267
|
+
expect(api_response[:action_state]).to eq({})
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
it 'cancel returns action_state_id for numeric return' do
|
|
271
|
+
ActionStateSpec::Backend.add_state(ActionStateSpec::State.new(id: 1, cancel_ret: 123))
|
|
272
|
+
|
|
273
|
+
call_api(:post, '/v1/action_states/1/cancel', {})
|
|
274
|
+
|
|
275
|
+
expect(api_response).to be_ok
|
|
276
|
+
expect(api_response.response[:_meta][:action_state_id]).to eq(123)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
it 'cancel returns error for false' do
|
|
280
|
+
ActionStateSpec::Backend.add_state(ActionStateSpec::State.new(id: 1, cancel_ret: false))
|
|
281
|
+
|
|
282
|
+
call_api(:post, '/v1/action_states/1/cancel', {})
|
|
283
|
+
|
|
284
|
+
expect(api_response).not_to be_ok
|
|
285
|
+
expect(api_response.message).to eq('cancellation failed')
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
it 'cancel returns error for NotImplementedError' do
|
|
289
|
+
state = ActionStateSpec::State.new(id: 1)
|
|
290
|
+
def state.cancel
|
|
291
|
+
raise NotImplementedError, 'not supported'
|
|
292
|
+
end
|
|
293
|
+
ActionStateSpec::Backend.add_state(state)
|
|
294
|
+
|
|
295
|
+
call_api(:post, '/v1/action_states/1/cancel', {})
|
|
296
|
+
|
|
297
|
+
expect(api_response).not_to be_ok
|
|
298
|
+
expect(api_response.message).to eq('not supported')
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
module AuthSpecBasic
|
|
6
|
+
User = Struct.new(:id, :login)
|
|
7
|
+
|
|
8
|
+
class Provider < HaveAPI::Authentication::Basic::Provider
|
|
9
|
+
protected
|
|
10
|
+
|
|
11
|
+
def find_user(_request, username, password)
|
|
12
|
+
return User.new(1, username) if username == 'user' && password == 'pass'
|
|
13
|
+
return nil unless username == 'error'
|
|
14
|
+
|
|
15
|
+
# Exercise the rescue path in Basic::Provider#authenticate
|
|
16
|
+
raise HaveAPI::AuthenticationError, 'backend failed'
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe HaveAPI::Authentication::Basic::Provider do
|
|
22
|
+
api do
|
|
23
|
+
define_resource(:Secure) do
|
|
24
|
+
version 1
|
|
25
|
+
desc 'Secured resource'
|
|
26
|
+
|
|
27
|
+
define_action(:Ping) do
|
|
28
|
+
route 'ping'
|
|
29
|
+
http_method :post
|
|
30
|
+
auth true
|
|
31
|
+
|
|
32
|
+
input(:hash) do
|
|
33
|
+
# accept empty JSON body
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
output(:hash) do
|
|
37
|
+
integer :user_id
|
|
38
|
+
string :login
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
authorize { allow }
|
|
42
|
+
|
|
43
|
+
def exec
|
|
44
|
+
{
|
|
45
|
+
user_id: current_user.id,
|
|
46
|
+
login: current_user.login
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
default_version 1
|
|
54
|
+
auth_chain AuthSpecBasic::Provider
|
|
55
|
+
|
|
56
|
+
let(:seen_users) { [] }
|
|
57
|
+
let(:api_instance) do
|
|
58
|
+
app
|
|
59
|
+
instance_variable_get(:@api)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
before do
|
|
63
|
+
api_instance.connect_hook(:post_authenticated) do |ret, current_user|
|
|
64
|
+
seen_users << current_user
|
|
65
|
+
ret
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'returns 401 without credentials' do
|
|
70
|
+
call_api(:post, '/v1/secures/ping', {})
|
|
71
|
+
|
|
72
|
+
expect(last_response.status).to eq(401)
|
|
73
|
+
expect(last_response.headers['www-authenticate']).to include('Basic realm=')
|
|
74
|
+
|
|
75
|
+
expect(api_response).to be_failed
|
|
76
|
+
expect(api_response.message).to include('authenticate')
|
|
77
|
+
expect(seen_users.last).to be_nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'returns 401 with wrong credentials' do
|
|
81
|
+
login('user', 'wrong')
|
|
82
|
+
call_api(:post, '/v1/secures/ping', {})
|
|
83
|
+
|
|
84
|
+
expect(last_response.status).to eq(401)
|
|
85
|
+
expect(api_response).to be_failed
|
|
86
|
+
expect(seen_users.last).to be_nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'authenticates with correct credentials' do
|
|
90
|
+
login('user', 'pass')
|
|
91
|
+
call_api(:post, '/v1/secures/ping', {})
|
|
92
|
+
|
|
93
|
+
expect(last_response.status).to eq(200)
|
|
94
|
+
expect(api_response).to be_ok
|
|
95
|
+
expect(api_response[:secure][:user_id]).to eq(1)
|
|
96
|
+
expect(api_response[:secure][:login]).to eq('user')
|
|
97
|
+
expect(seen_users.last).to be_a(AuthSpecBasic::User)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'handles AuthenticationError raised by backend' do
|
|
101
|
+
login('error', 'pass')
|
|
102
|
+
call_api(:post, '/v1/secures/ping', {})
|
|
103
|
+
|
|
104
|
+
expect(last_response.status).to eq(401)
|
|
105
|
+
expect(api_response).to be_failed
|
|
106
|
+
expect(seen_users.last).to be_nil
|
|
107
|
+
end
|
|
108
|
+
end
|