farscape 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,92 @@
1
+ require 'spec_helper'
2
+ require 'json'
3
+ require 'active_support/core_ext/string/filters'
4
+
5
+ describe Farscape::RepresentorAgent do
6
+ let(:entry_point) { "http://localhost:#{RAILS_PORT}"}
7
+
8
+ let(:drds_link) { Farscape::Agent.new(entry_point).enter.transitions["drds"] }
9
+ let(:can_do_hash) { {conditions: 'can_do_anything'} }
10
+ let(:name) { 'Brave New DRD' }
11
+ let(:attrs) { {name: name, status: 'activated', old_status: 'activated'} }
12
+
13
+ context 'can do anything' do
14
+
15
+ it 'includes appropriate transitions' do
16
+ drds_resource = drds_link.invoke { |req| req.parameters = can_do_hash }
17
+ expect(drds_resource.transitions.keys).to include('create')
18
+ end
19
+
20
+ it 'can create a drd' do
21
+ name = 'Brave New DRD'
22
+ drds_resource = drds_link.invoke { |req| req.parameters = can_do_hash }
23
+ drd = drds_resource.transitions['create'].invoke do |req|
24
+ req.attributes = attrs
25
+ req.parameters = can_do_hash
26
+ end
27
+ expect(drd.transitions['self'].invoke.attributes['name']).to eq(name)
28
+ drd.transitions['delete'].invoke # Cleanup, failure here should imply failure in 'can delete a drd'
29
+ end
30
+
31
+ context 'an existing drd' do
32
+ before do
33
+ drds_resource = drds_link.invoke { |req| req.parameters = can_do_hash }
34
+ @drd = drds_resource.transitions['create'].invoke do |req|
35
+ req.attributes = attrs
36
+ req.parameters = can_do_hash
37
+ end
38
+ end
39
+
40
+
41
+ it 'can delete a drd' do
42
+ # NB We compare attributes as the self link will differ between the two calls
43
+ recall_drds = ->() { @drd.transitions['self'].invoke { |r| r.parameters = can_do_hash }.to_hash[:attributes] }
44
+ drd_attributes = @drd.to_hash[:attributes]
45
+ self_attributes = recall_drds.call
46
+ expect(self_attributes).to eq(drd_attributes)
47
+ @drd.transitions['delete'].invoke
48
+ expect { recall_drds.call }.to raise_error( Farscape::Exceptions::NotFound )
49
+ end
50
+
51
+ it 'returns proper errors' do
52
+ # NB We compare attributes as the self link will differ between the two calls
53
+ recall_drds = ->() { @drd.transitions['self'].invoke { |r| r.parameters = can_do_hash }.to_hash[:attributes] }
54
+ drd_attributes = @drd.to_hash[:attributes]
55
+ @drd.transitions['delete'].invoke
56
+ begin
57
+ recall_drds.call
58
+ raise Exception.new('Moya responded with a resource that\'s been deleted')
59
+ rescue Farscape::Exceptions::NotFound => e
60
+ expect(e.error_description).to eq('The server has not found anything matching the Request-URI.
61
+ No indication is given of whether the condition is temporary or permanent.
62
+ This status code is commonly used when the server does not wish to reveal exactly why the request has been refused, or when no other response is applicable.'.squish)
63
+ expect(e.representor.to_hash).to eq(e.message)
64
+ end
65
+ end
66
+
67
+ it 'can update a drd' do
68
+ new_kind = "sentinel"
69
+ begin
70
+ @drd.transitions['update'].invoke do |r|
71
+ r.attributes = {kind: new_kind}
72
+ r.parameters = can_do_hash
73
+ end
74
+ rescue
75
+ #TODO: Farscape handles redirects
76
+ end
77
+ kindof = @drd.transitions['self'].invoke.attributes['kind']
78
+ expect(kindof).to eq(new_kind)
79
+ end
80
+
81
+ it 'can toggle a drds activation state' do
82
+ status = @drd.attributes['status']
83
+ action = status == 'activated' ? 'deactivate' : 'activate'
84
+ @drd.transitions[action].invoke { |r| r.parameters = can_do_hash }
85
+ new_status = @drd.transitions['self'].invoke { |r| r.parameters = can_do_hash }.attributes['status']
86
+ expect(status).to_not eq(new_status)
87
+ end
88
+
89
+ end
90
+ end
91
+
92
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+ require 'farscape/logger'
3
+
4
+ describe(Farscape) do
5
+
6
+ describe 'logger' do
7
+
8
+ after(:each) { Farscape.logger = nil }
9
+
10
+ it 'defaults to the built-in Ruby logger' do
11
+ Farscape.logger = nil
12
+ expect(Farscape.logger).to be_a(::Logger)
13
+ end
14
+
15
+ it 'can be set to any kind of logger' do
16
+ custom_logger = Object.new
17
+ Farscape.logger = custom_logger
18
+ expect(Farscape.logger).to eq(custom_logger)
19
+ end
20
+
21
+ end
22
+
23
+ end
@@ -0,0 +1,344 @@
1
+ require 'spec_helper'
2
+
3
+ module TestMiddleware
4
+ class NoGetNoProblem
5
+ def initialize(app, config = {})
6
+ @app = app
7
+ @config = config
8
+ end
9
+ def call(env)
10
+ @app.call(env).on_complete do |env|
11
+ unless @config[:permissive]
12
+ raise StandardError, "Shazam!" if env[:method] == :get
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ class Saboteur
19
+ def initialize(app, *)
20
+ @app = app
21
+ end
22
+ def call(env)
23
+ env[:sabotaged] = true
24
+ @app.call(env)
25
+ end
26
+ end
27
+
28
+ class SabotageDetector
29
+ def initialize(app, *)
30
+ @app = app
31
+ end
32
+ def call(env)
33
+ raise 'Sabotage detected' if env[:sabotaged]
34
+ @app.call(env)
35
+ end
36
+ end
37
+
38
+ end
39
+
40
+ describe Farscape::Agent do
41
+
42
+ before(:each) do
43
+ Farscape.clear
44
+ detector_plugin = {name: :Detector, type: :sebacean, middleware: [TestMiddleware::SabotageDetector], default_state: :disabled}
45
+ saboteur_middleware = {class: TestMiddleware::Saboteur, before: :sebacean}
46
+ saboteur_plugin = {name: :Saboteur, type: :scarran, middleware: [saboteur_middleware]}
47
+ Farscape.register_plugin(detector_plugin)
48
+ Farscape.register_plugin(saboteur_plugin)
49
+ end
50
+
51
+ after(:all) { Farscape.clear }
52
+
53
+ let(:entry_point) { "http://localhost:#{RAILS_PORT}"}
54
+ let(:can_do_hash) { {conditions: 'can_do_anything'} }
55
+
56
+ it 'accepts a using directive that enables a plugin' do
57
+ expect { Farscape::Agent.new(entry_point).using(:Detector).enter }.to raise_error('Sabotage detected')
58
+ end
59
+
60
+ it 'accepts an omitting directive that disables a plugin' do
61
+ agent = Farscape::Agent.new(entry_point).using(:Detector)
62
+ agent = agent.omitting(:sebacean)
63
+ expect { agent.enter }.to_not raise_error
64
+ end
65
+
66
+ it 'allows showing the list of registered plugins' do
67
+ expect(Farscape::Agent.new(entry_point).plugins).to eq(Farscape.plugins)
68
+ end
69
+
70
+ it 'allows showing only those enabled for the agent' do
71
+ agent = Farscape::Agent.new(entry_point).using(:Detector)
72
+ expect(agent.enabled_plugins[:Detector]).to_not be_nil
73
+ expect(Farscape.disabled_plugins[:Detector]).to_not be_nil
74
+ end
75
+
76
+ it 'allows showing only those disabled for the agent' do
77
+ agent = Farscape::Agent.new(entry_point).using(:Detector)
78
+ agent = agent.omitting(:sebacean)
79
+ expect(agent.disabled_plugins.keys).to eq([:Detector])
80
+ expect(Farscape.enabled_plugins.keys).to eq([:Saboteur])
81
+ end
82
+
83
+ it 'allows using on TransitionAgent objects' do
84
+ transition = Farscape::Agent.new(entry_point).enter.transitions["drds"].using(:Detector)
85
+ expect { transition.invoke { |req| req.parameters = can_do_hash } }.to raise_error('Sabotage detected')
86
+ expect(transition.disabled_plugins).to eq({})
87
+ end
88
+
89
+ it 'allows using on RepresentorAgent objects' do
90
+ representor = Farscape::Agent.new(entry_point).enter.using(:Detector)
91
+ transition = representor.transitions["drds"]
92
+ expect { transition.invoke { |req| req.parameters = can_do_hash } }.to raise_error('Sabotage detected')
93
+ expect(representor.disabled_plugins).to eq({})
94
+ end
95
+
96
+ end
97
+
98
+ describe Farscape do
99
+ after(:all) { Farscape.clear }
100
+
101
+ context 'configuring plugins' do
102
+
103
+ before(:each) { Farscape.clear }
104
+
105
+ it 'can register a plugin' do
106
+ plugin = {name: :Peacekeeper, type: :sebacean}
107
+ expect(Farscape.register_plugin(plugin)).to be
108
+ expect(Farscape.plugins).to eq({Peacekeeper: plugin})
109
+ end
110
+
111
+ it 'can preemptively disable a plugin by name' do
112
+ Farscape.disable!(:Peacekeeper)
113
+ plugin = {name: :Peacekeeper, type: :sebacean}
114
+ Farscape.register_plugin(plugin)
115
+ expect(Farscape.enabled_plugins).to be_empty
116
+ expect(Farscape.disabled?(plugin)).to be true
117
+ expect(Farscape.enabled?(plugin)).to be false
118
+ end
119
+
120
+ it 'can preemptively disable a plugin by type' do
121
+ Farscape.disable!(:sebacean)
122
+ plugin = {name: :Peacekeeper, type: :sebacean}
123
+ Farscape.register_plugin(plugin)
124
+ expect(Farscape.enabled_plugins).to be_empty
125
+ expect(Farscape.disabled?(plugin)).to be true
126
+ expect(Farscape.enabled?(plugin)).to be false
127
+ end
128
+
129
+ it "can disable a plugin after it's added" do
130
+ Farscape.register_plugin(name: :Peacekeeper, type: :sebacean)
131
+ Farscape.register_plugin(name: :Imperium, type: :scarran)
132
+ Farscape.disable!(:sebacean)
133
+ expect(Farscape.enabled_plugins).to be_one
134
+ end
135
+
136
+ context 'adding middleware' do
137
+
138
+ before(:each) { Farscape.clear }
139
+ after(:all) { Farscape.clear }
140
+
141
+ it 'can add middleware' do
142
+ plugin = {name: :Peacekeeper, type: :sebacean, middleware: [TestMiddleware::NoGetNoProblem]}
143
+ Farscape.register_plugin(plugin)
144
+
145
+ expect(Farscape.middleware_stack.map{ |elt| elt[:class] } ).to eq([TestMiddleware::NoGetNoProblem])
146
+ end
147
+
148
+ it 'removes middleware when the source plugin is disabled' do
149
+ plugin = {name: :Peacekeeper, type: :sebacean, middleware: [TestMiddleware::NoGetNoProblem]}
150
+ Farscape.register_plugin(plugin)
151
+ Farscape.disable!( :sebacean )
152
+
153
+ expect(Farscape.middleware_stack).to be_none
154
+ end
155
+
156
+ it 'uses the middleware when making requests' do
157
+ plugin = {name: :Peacekeeper, type: :sebacean, middleware: [TestMiddleware::NoGetNoProblem]}
158
+ Farscape.register_plugin(plugin)
159
+
160
+ expect{Farscape::Agent.new("http://localhost:#{RAILS_PORT}").enter}.to raise_error('Shazam!')
161
+ end
162
+
163
+ it 'can configure middleware' do
164
+ plugin = {name: :Peacekeeper, type: :sebacean, middleware: [{class: TestMiddleware::NoGetNoProblem, config: {permissive: true}}]}
165
+ Farscape.register_plugin(plugin)
166
+
167
+ expect{Farscape::Agent.new("http://localhost:#{RAILS_PORT}").enter}.not_to raise_error
168
+ end
169
+
170
+ it 'adds middleware to disabled when disabled' do
171
+ plugin = {name: :Peacekeeper, default_state: :disabled, type: :sebacean, middleware: [{class: TestMiddleware::NoGetNoProblem, config: {permissive: true}}]}
172
+ Farscape.register_plugin(plugin)
173
+
174
+ expect(Farscape.enabled_plugins).to eq({})
175
+ expect(Farscape.disabled_plugins).to eq(plugin[:name] => plugin)
176
+ end
177
+
178
+ [:sebacean, TestMiddleware::SabotageDetector,'TestMiddleware::SabotageDetector',['TestMiddleware::SabotageDetector']].each do |form|
179
+ it "honors the before: option in middleware when given as #{form.inspect}" do
180
+ saboteur_middleware = {class: TestMiddleware::Saboteur, before: form}
181
+ saboteur_plugin = {name: :Saboteur, type: :scarran, middleware: [saboteur_middleware]}
182
+ detector_plugin = {name: :Detector, type: :sebacean, middleware: [TestMiddleware::SabotageDetector]}
183
+ [saboteur_plugin, detector_plugin].shuffle.each { |plugin| Farscape.register_plugin(plugin) }
184
+
185
+ expect(Farscape.middleware_stack.map{ |m| m[:class] }).to eq( [TestMiddleware::Saboteur, TestMiddleware::SabotageDetector] )
186
+ expect{Farscape::Agent.new("http://localhost:#{RAILS_PORT}").enter}.to raise_error('Sabotage detected')
187
+ end
188
+ end
189
+
190
+ [:sebacean, TestMiddleware::SabotageDetector,'TestMiddleware::SabotageDetector',['TestMiddleware::SabotageDetector']].each do |form|
191
+ it "honors the after: option in middleware when given as #{form.inspect}" do
192
+ saboteur_middleware = {class: TestMiddleware::Saboteur, after: form}
193
+ saboteur_plugin = {name: :Saboteur, type: :scarran, middleware: [saboteur_middleware]}
194
+ detector_plugin = {name: :Detector, type: :sebacean, middleware: [TestMiddleware::SabotageDetector]}
195
+ [saboteur_plugin, detector_plugin].shuffle.each { |plugin| Farscape.register_plugin(plugin) }
196
+
197
+ expect(Farscape.middleware_stack.map{ |m| m[:class] }).to eq( [TestMiddleware::SabotageDetector, TestMiddleware::Saboteur] )
198
+ expect{Farscape::Agent.new("http://localhost:#{RAILS_PORT}").enter}.not_to raise_error
199
+ end
200
+ end
201
+
202
+ it 'doesn\'t disable everything with one' do
203
+ detector_plugin = {name: :Detector, type: :sebacean, middleware: [TestMiddleware::SabotageDetector], default_state: :disabled}
204
+ saboteur_middleware = {class: TestMiddleware::Saboteur}
205
+ saboteur_plugin = {name: :Saboteur, type: :scarran, middleware: [saboteur_middleware]}
206
+ Farscape.register_plugin(detector_plugin)
207
+ Farscape.register_plugin(saboteur_plugin)
208
+
209
+ expect(Farscape.enabled_plugins.keys).to eq([:Saboteur])
210
+ end
211
+
212
+ it 'doesn\'t allow sneaky enabling' do
213
+ detector_plugin = {name: :Detector, type: :sebacean, middleware: [TestMiddleware::SabotageDetector], default_state: :disabled}
214
+ Farscape.register_plugin(detector_plugin)
215
+
216
+ expect(Farscape.disabled_plugins.keys).to eq([:Detector])
217
+
218
+ detector_plugin = {name: :Detector, type: :sebacean, middleware: [TestMiddleware::SabotageDetector], default_state: :enabled}
219
+ Farscape.register_plugin(detector_plugin)
220
+
221
+ expect(Farscape.disabled_plugins.keys).to eq([:Detector])
222
+ end
223
+
224
+ end
225
+
226
+ context 'extensions' do
227
+ before(:each) { Farscape.clear }
228
+ after(:all) { Farscape.clear }
229
+
230
+ let(:entry_point) { "http://localhost:#{RAILS_PORT}"}
231
+ let(:can_do_hash) { {conditions: 'can_do_anything'} }
232
+
233
+ it 'allows extensions' do
234
+ module Peacekeeper
235
+ def pacify!
236
+ raise 'none shall pass' unless enabled_plugins.include?(:Saboteur)
237
+ end
238
+ end
239
+ Farscape.register_plugin(name: :Peacekeeper, type: :security, extensions: {Agent: [Peacekeeper]})
240
+ expect { Farscape::Agent.new.pacify! }.to raise_error('none shall pass')
241
+ end
242
+
243
+ it 'allows extensions with altering existing methods' do
244
+ module Peacemaker
245
+ def self.extended(base)
246
+ base.instance_eval do
247
+ @original_transitions = method(:transitions)
248
+ def transitions
249
+ raise 'none shall pass' if @original_transitions.call.keys.include?("drds")
250
+ @original_transitions.call
251
+ end
252
+ end
253
+ end
254
+ end
255
+ Farscape.register_plugin(name: :Peacekeeper, type: :security, extensions: {RepresentorAgent: [Peacemaker]})
256
+ expect { Farscape::Agent.new.enter(entry_point).transitions.keys }.to raise_error('none shall pass')
257
+ expect(Farscape::Agent.new.enter(entry_point).omitting(:Peacekeeper).transitions.keys).to include("drds")
258
+ end
259
+
260
+ it 'allows discovery extensions' do
261
+ module ServiceCatalogue
262
+ def self.extended(base)
263
+ base.instance_eval do
264
+ @original_enter = method(:enter)
265
+ @dispatches = {
266
+ :moya => "http://localhost:#{RAILS_PORT}"
267
+ }
268
+ def enter(entry=nil)
269
+ @entry_point ||= entry
270
+ @entry_point = @dispatches[@entry_point] || @entry_point
271
+ @original_enter.call
272
+ end
273
+ end
274
+ end
275
+ end
276
+ Farscape.register_plugin(name: :Wormlet, type: :discovery, extensions: {Agent: [ServiceCatalogue]})
277
+ expect(Farscape::Agent.new(:moya).enter.transitions.keys).to include("drds")
278
+ expect(Farscape::Agent.new.enter(:moya).transitions.keys).to include("drds")
279
+ expect { Farscape::Agent.new.omitting(:discovery).enter(:moya).transitions.keys }.to raise_error(NoMethodError)
280
+ end
281
+
282
+ end
283
+
284
+ context 'workflow' do
285
+
286
+ before(:each) { Farscape.clear }
287
+
288
+ let(:entry_point) { "http://localhost:#{RAILS_PORT}"}
289
+ let(:can_do_hash) { {conditions: 'can_do_anything'} }
290
+
291
+ it 'manages enabling and disabling plugins' do
292
+ registration = [{name: :Peacekeeper, type: :sebacean, middleware: [TestMiddleware::NoGetNoProblem]}]
293
+ registration_keys = registration.map { |plugin| plugin[:name] }
294
+ Farscape.register_plugins(registration)
295
+
296
+ expect(Farscape.plugins.keys).to eq( registration_keys )
297
+ expect(Farscape.enabled_plugins.keys).to eq( registration_keys )
298
+ expect(Farscape.disabled_plugins.keys).to eq([])
299
+
300
+ Farscape.disable!(type: :sebacean)
301
+
302
+ expect(Farscape.enabled_plugins.keys).to eq([])
303
+ expect(Farscape.disabled_plugins.keys).to eq( registration_keys )
304
+
305
+ Farscape.enable!(name: :Peacekeeper)
306
+
307
+ expect(Farscape.enabled_plugins.keys).to eq( registration_keys )
308
+ expect(Farscape.disabled_plugins.keys).to eq([])
309
+ end
310
+
311
+ it 'managed complex enabling and disabling on instances' do
312
+ registration = [{name: :Peacekeeper, type: :sebacean, middleware: [TestMiddleware::NoGetNoProblem]}]
313
+ registration_keys = registration.map { |plugin| plugin[:name] }
314
+ Farscape.register_plugins(registration)
315
+ peacekeeper_plugin = {:Peacekeeper=>{:name=>:Peacekeeper, :type=>:sebacean, :middleware=>[TestMiddleware::NoGetNoProblem], :enabled=>true}}
316
+ disabled_plugin = {:Peacekeeper=>{:name=>:Peacekeeper, :type=>:sebacean, :middleware=>[TestMiddleware::NoGetNoProblem], :enabled=>false}}
317
+
318
+ expect(Farscape.enabled_plugins).to eq(peacekeeper_plugin)
319
+
320
+ agent = Farscape::Agent.new.omitting(name: :Peacekeeper)
321
+
322
+ expect(agent.plugins).to eq(disabled_plugin)
323
+ expect(agent.enabled_plugins).to eq({})
324
+ expect(agent.disabled_plugins).to eq(disabled_plugin)
325
+
326
+ resource = agent.enter(entry_point).transitions["drds"].invoke { |builder| builder.parameters = can_do_hash }
327
+ expect(resource.plugins).to eq(disabled_plugin)
328
+ expect(resource.enabled_plugins).to eq({})
329
+ expect(resource.plugins).to eq(disabled_plugin)
330
+
331
+ transition = resource.using(name: :Peacekeeper).transitions["items"]
332
+ details = resource.using(name: :Peacekeeper).embedded["items"].first
333
+
334
+ expect { transition.invoke }.to raise_error('Shazam!')
335
+ expect(details.plugins).to eq(peacekeeper_plugin)
336
+ expect(details.enabled_plugins).to eq(peacekeeper_plugin)
337
+ expect(details.disabled_plugins).to eq({})
338
+ end
339
+
340
+ end
341
+
342
+ end
343
+
344
+ end
@@ -0,0 +1,79 @@
1
+ require 'spec_helper'
2
+
3
+ describe Farscape::TransitionAgent do
4
+ let(:client) { double }
5
+ let(:agent) { double(media_type: :hale, client: client) }
6
+ let(:transition) { double(uri: 'com:mdsol', templated?: false) }
7
+ let(:arg) { { additional_fields: { key1: 'value1', key2: 'value2' } } }
8
+ let(:transition_agent) { described_class.new(transition, agent) }
9
+ let(:field_list) { double(name: 'additional_fields') }
10
+ let(:call_options) { { url: 'com:mdsol', headers: { Accept: 'application/vnd.hale+json' } } }
11
+
12
+ before do
13
+ allow(agent).to receive(:get_accept_header).and_return(Accept: 'application/vnd.hale+json')
14
+ allow(agent).to receive(:find_exception).and_return(nil)
15
+ allow(transition).to receive(:interface_method).and_return(http_method)
16
+ allow(transition).to receive(:parameters).and_return([ field_list ])
17
+ allow(transition).to receive(:attributes).and_return([])
18
+ allow_any_instance_of(described_class).to receive(:handle_extensions).and_return(nil)
19
+ end
20
+
21
+ context 'GET method' do
22
+ let(:http_method) { "get" }
23
+
24
+ it 'stores parameters in params' do
25
+ expect(client).to receive(:invoke).with(call_options.merge(method: http_method, params: arg))
26
+ transition_agent.invoke(arg)
27
+ end
28
+ end
29
+
30
+ context 'POST method' do
31
+ let(:http_method) { "post" }
32
+
33
+ it 'stores parameters in body' do
34
+ expect(client).to receive(:invoke).with(call_options.merge(method: http_method, body: arg))
35
+ transition_agent.invoke(arg)
36
+ end
37
+ end
38
+
39
+ # see https://tools.ietf.org/html/rfc6570#section-3.2.6
40
+ context "URL with a path segment" do
41
+ let(:transition) do
42
+ Representors::Transition.new(
43
+ templated: true,
44
+ rel: "find",
45
+ href: "https://example.com/api/v1/issues{/issue_uuid}"
46
+ )
47
+ end
48
+ let(:issue_uuid) { SecureRandom.uuid }
49
+
50
+ context "GET" do
51
+ let(:http_method) { "get" }
52
+
53
+ it "interpolates a templated URI" do
54
+ options = call_options.merge(
55
+ method: http_method,
56
+ url: "https://example.com/api/v1/issues/#{issue_uuid}",
57
+ params: arg
58
+ )
59
+ expect(client).to receive(:invoke).with(options)
60
+ transition_agent.invoke(arg.merge(issue_uuid: issue_uuid))
61
+ end
62
+ end
63
+
64
+ context "POST" do
65
+ let(:http_method) { "post" }
66
+
67
+ it "does not interpolate a templated URI" do
68
+ params = arg.merge(issue_uuid: issue_uuid)
69
+ options = call_options.merge(
70
+ method: http_method,
71
+ url: "https://example.com/api/v1/issues",
72
+ body: params
73
+ )
74
+ expect(client).to receive(:invoke).with(options)
75
+ transition_agent.invoke(params)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,125 @@
1
+ require_relative '../../../lib/farscape/helpers/partially_ordered_list.rb'
2
+
3
+ describe(PartiallyOrderedList) do
4
+
5
+ it 'sorts a list given a complete ordering' do
6
+ list = described_class.new { |a,b| a <=> b }
7
+ 1.upto(10) { |i| list.add(i) }
8
+ expect(list.to_a).to eq([*1..10])
9
+ end
10
+
11
+ it 'sorts a shuffled list given a complete ordering' do
12
+ list = described_class.new { |a,b| a <=> b }
13
+ [*1..10].shuffle.each { |i| list.add(i) }
14
+ expect(list.to_a).to eq([*1..10])
15
+ end
16
+
17
+ it 'returns an arbitrary list when nothing is ordered' do
18
+ list = described_class.new {}
19
+ (1..20).each { |i| list.add(i) }
20
+ expect(list.count).to eq(20)
21
+ end
22
+
23
+
24
+ it 'finds an order for a list satisfying a disjoint ordering' do
25
+ list = described_class.new do |a,b|
26
+ if a % 2 == b % 2
27
+ a <=> b
28
+ end
29
+ end
30
+ [*1..20].shuffle.each { |i| list.add(i) }
31
+ expect(list.partition(&:odd?)).to eq([*1..20].partition(&:odd?))
32
+ end
33
+
34
+ it 'finds one possible transitive ordering' do
35
+ list = described_class.new do |a,b|
36
+ if a % 2 == b % 2
37
+ a <=> b
38
+ elsif [a,b] == [1,8]
39
+ 1
40
+ elsif [a,b] == [8,1]
41
+ -1
42
+ end
43
+ end
44
+ [*1..20].shuffle.each { |i| list.add(i) }
45
+ expect(list.partition(&:odd?)).to eq([*1..20].partition(&:odd?))
46
+ expect(list.find_index(8)).to be < list.find_index(1)
47
+ end
48
+
49
+ it 'finds the only possible transitive ordering' do
50
+ list = described_class.new do |a,b|
51
+ if a % 2 == b % 2
52
+ a <=> b
53
+ elsif [a,b] == [1,20]
54
+ 1
55
+ elsif [a,b] == [20,1]
56
+ -1
57
+ end
58
+ end
59
+ [*1..20].shuffle.each { |i| list.add(i) }
60
+ expect(list.to_a).to eq([2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19])
61
+ end
62
+
63
+ it 'raises on circular ordering' do
64
+ list = described_class.new do |a,b|
65
+ if [a,b] == [1,10]
66
+ 1
67
+ elsif [a,b] == [10,1]
68
+ -1
69
+ else
70
+ a <=> b
71
+ end
72
+ end
73
+ [*1..10].shuffle.each { |i| list.add(i) }
74
+ expect{ list.to_a }.to raise_error(PartiallyOrderedList::CircularOrderingError)
75
+ end
76
+
77
+ it 'deletes' do
78
+ list = described_class.new { |a,b| a <=> b }
79
+ [*1..10].shuffle.each { |i| list.add(i) }
80
+ list.delete(5)
81
+ expect(list.to_a).to eq( [1,2,3,4,6,7,8,9,10] )
82
+ end
83
+
84
+ it 'deletes when there is a cached ordering' do
85
+ list = described_class.new {}
86
+ list.add 1
87
+ list.add 2
88
+ list.to_a
89
+ list.delete 1
90
+ expect(list.to_a).to eq([2])
91
+ end
92
+
93
+ it 'invalidates cached ordering when a new item is added' do
94
+ list = described_class.new {}
95
+ 1.upto(10) { |i| list.add(i) }
96
+ list.to_a
97
+ list.add(11)
98
+ expect(list.count).to eq(11)
99
+ end
100
+
101
+ it 'returns an enumerator when each is called without a block' do
102
+ expect(described_class.new{}.each).to be_a(Enumerator)
103
+ end
104
+
105
+ it 'raises if new is called without a block' do
106
+ expect{described_class.new}.to raise_error(ArgumentError)
107
+ end
108
+
109
+ it 're-sorts as needed' do
110
+ list = described_class.new do |a,b|
111
+ if a % 2 == b % 2
112
+ a <=> b
113
+ elsif [a,b] == [1,20]
114
+ 1
115
+ elsif [a,b] == [20,1]
116
+ -1
117
+ end
118
+ end
119
+ [*1..19].shuffle.each { |i| list.add(i) }
120
+ list.to_a
121
+ list.add(20)
122
+ expect(list.to_a).to eq([2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19])
123
+ end
124
+
125
+ end