aggro 0.0.1 → 0.0.2

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 (141) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +8 -0
  3. data/.travis.yml +15 -0
  4. data/Gemfile +9 -0
  5. data/README.md +5 -1
  6. data/Rakefile +10 -0
  7. data/aggro.gemspec +8 -1
  8. data/lib/aggro.rb +191 -7
  9. data/lib/aggro/abstract_store.rb +12 -0
  10. data/lib/aggro/aggregate.rb +98 -0
  11. data/lib/aggro/aggregate_ref.rb +68 -6
  12. data/lib/aggro/attribute_dsl.rb +96 -0
  13. data/lib/aggro/binding_dsl.rb +45 -0
  14. data/lib/aggro/block_helper.rb +14 -0
  15. data/lib/aggro/channel.rb +37 -0
  16. data/lib/aggro/client.rb +12 -0
  17. data/lib/aggro/cluster_config.rb +57 -0
  18. data/lib/aggro/command.rb +16 -0
  19. data/lib/aggro/concurrent_actor.rb +26 -0
  20. data/lib/aggro/event_bus.rb +94 -0
  21. data/lib/aggro/event_dsl.rb +53 -0
  22. data/lib/aggro/event_proxy.rb +23 -0
  23. data/lib/aggro/event_serializer.rb +14 -0
  24. data/lib/aggro/file_store.rb +97 -0
  25. data/lib/aggro/file_store/reader.rb +21 -0
  26. data/lib/aggro/file_store/writer.rb +27 -0
  27. data/lib/aggro/handler/command.rb +60 -0
  28. data/lib/aggro/handler/create_aggregate.rb +42 -0
  29. data/lib/aggro/handler/get_events.rb +30 -0
  30. data/lib/aggro/handler/query.rb +60 -0
  31. data/lib/aggro/handler/start_saga.rb +56 -0
  32. data/lib/aggro/local_node.rb +28 -0
  33. data/lib/aggro/locator.rb +32 -0
  34. data/lib/aggro/message/ask.rb +16 -0
  35. data/lib/aggro/message/command.rb +36 -0
  36. data/lib/aggro/message/create_aggregate.rb +16 -0
  37. data/lib/aggro/message/endpoint.rb +16 -0
  38. data/lib/aggro/message/events.rb +24 -0
  39. data/lib/aggro/message/get_events.rb +16 -0
  40. data/lib/aggro/message/heartbeat.rb +16 -0
  41. data/lib/aggro/message/invalid_target.rb +20 -0
  42. data/lib/aggro/message/ok.rb +20 -0
  43. data/lib/aggro/message/publisher_endpoint_inquiry.rb +16 -0
  44. data/lib/aggro/message/query.rb +36 -0
  45. data/lib/aggro/message/result.rb +16 -0
  46. data/lib/aggro/message/start_saga.rb +28 -0
  47. data/lib/aggro/message/unhandled_operation.rb +20 -0
  48. data/lib/aggro/message/unknown_operation.rb +20 -0
  49. data/lib/aggro/message_parser.rb +10 -0
  50. data/lib/aggro/message_router.rb +26 -0
  51. data/lib/aggro/nanomsg_transport.rb +31 -0
  52. data/lib/aggro/nanomsg_transport/client.rb +35 -0
  53. data/lib/aggro/nanomsg_transport/connection.rb +98 -0
  54. data/lib/aggro/nanomsg_transport/publish.rb +17 -0
  55. data/lib/aggro/nanomsg_transport/publisher.rb +37 -0
  56. data/lib/aggro/nanomsg_transport/raw_reply.rb +18 -0
  57. data/lib/aggro/nanomsg_transport/raw_request.rb +18 -0
  58. data/lib/aggro/nanomsg_transport/reply.rb +17 -0
  59. data/lib/aggro/nanomsg_transport/request.rb +17 -0
  60. data/lib/aggro/nanomsg_transport/server.rb +84 -0
  61. data/lib/aggro/nanomsg_transport/socket_error.rb +20 -0
  62. data/lib/aggro/nanomsg_transport/subscribe.rb +27 -0
  63. data/lib/aggro/nanomsg_transport/subscriber.rb +82 -0
  64. data/lib/aggro/node.rb +29 -0
  65. data/lib/aggro/node_list.rb +39 -0
  66. data/lib/aggro/projection.rb +13 -0
  67. data/lib/aggro/query.rb +11 -0
  68. data/lib/aggro/saga.rb +94 -0
  69. data/lib/aggro/saga_runner.rb +87 -0
  70. data/lib/aggro/saga_runner/start_saga.rb +12 -0
  71. data/lib/aggro/saga_status.rb +29 -0
  72. data/lib/aggro/server.rb +88 -0
  73. data/lib/aggro/subscriber.rb +48 -0
  74. data/lib/aggro/subscription.rb +41 -0
  75. data/lib/aggro/transform/boolean.rb +16 -0
  76. data/lib/aggro/transform/email.rb +26 -0
  77. data/lib/aggro/transform/id.rb +34 -0
  78. data/lib/aggro/transform/integer.rb +22 -0
  79. data/lib/aggro/transform/money.rb +22 -0
  80. data/lib/aggro/transform/noop.rb +16 -0
  81. data/lib/aggro/transform/string.rb +16 -0
  82. data/lib/aggro/transform/time_interval.rb +24 -0
  83. data/lib/aggro/version.rb +1 -1
  84. data/spec/lib/aggro/abstract_store_spec.rb +15 -0
  85. data/spec/lib/aggro/aggregate_ref_spec.rb +63 -12
  86. data/spec/lib/aggro/aggregate_spec.rb +207 -0
  87. data/spec/lib/aggro/channel_spec.rb +87 -0
  88. data/spec/lib/aggro/client_spec.rb +26 -0
  89. data/spec/lib/aggro/cluster_config_spec.rb +33 -0
  90. data/spec/lib/aggro/command_spec.rb +52 -0
  91. data/spec/lib/aggro/concurrent_actor_spec.rb +44 -0
  92. data/spec/lib/aggro/event_bus_spec.rb +20 -0
  93. data/spec/lib/aggro/event_serializer_spec.rb +28 -0
  94. data/spec/lib/aggro/file_store/reader_spec.rb +32 -0
  95. data/spec/lib/aggro/file_store/writer_spec.rb +67 -0
  96. data/spec/lib/aggro/file_store_spec.rb +51 -0
  97. data/spec/lib/aggro/handler/command_spec.rb +78 -0
  98. data/spec/lib/aggro/handler/create_aggregate_spec.rb +64 -0
  99. data/spec/lib/aggro/handler/get_events_handler_spec.rb +45 -0
  100. data/spec/lib/aggro/handler/query_spec.rb +78 -0
  101. data/spec/lib/aggro/handler/start_saga_spec.rb +64 -0
  102. data/spec/lib/aggro/local_node_spec.rb +52 -0
  103. data/spec/lib/aggro/locator_spec.rb +61 -0
  104. data/spec/lib/aggro/message/ask_spec.rb +23 -0
  105. data/spec/lib/aggro/message/command_spec.rb +50 -0
  106. data/spec/lib/aggro/message/create_aggregate_spec.rb +28 -0
  107. data/spec/lib/aggro/message/endpoint_spec.rb +23 -0
  108. data/spec/lib/aggro/message/events_spec.rb +37 -0
  109. data/spec/lib/aggro/message/get_events_spec.rb +33 -0
  110. data/spec/lib/aggro/message/heartbeat_spec.rb +23 -0
  111. data/spec/lib/aggro/message/invalid_target_spec.rb +28 -0
  112. data/spec/lib/aggro/message/ok_spec.rb +27 -0
  113. data/spec/lib/aggro/message/publisher_endpoint_inquiry_spec.rb +23 -0
  114. data/spec/lib/aggro/message/query_spec.rb +50 -0
  115. data/spec/lib/aggro/message/start_saga_spec.rb +37 -0
  116. data/spec/lib/aggro/message/unhandled_operation_spec.rb +28 -0
  117. data/spec/lib/aggro/message/unknown_operation_spec.rb +28 -0
  118. data/spec/lib/aggro/message_parser_spec.rb +16 -0
  119. data/spec/lib/aggro/message_router_spec.rb +35 -0
  120. data/spec/lib/aggro/nanomsg_transport/socket_error_spec.rb +21 -0
  121. data/spec/lib/aggro/nanomsg_transport_spec.rb +37 -0
  122. data/spec/lib/aggro/node_list_spec.rb +38 -0
  123. data/spec/lib/aggro/node_spec.rb +44 -0
  124. data/spec/lib/aggro/projection_spec.rb +22 -0
  125. data/spec/lib/aggro/query_spec.rb +47 -0
  126. data/spec/lib/aggro/saga_runner_spec.rb +84 -0
  127. data/spec/lib/aggro/saga_spec.rb +126 -0
  128. data/spec/lib/aggro/saga_status_spec.rb +56 -0
  129. data/spec/lib/aggro/server_spec.rb +118 -0
  130. data/spec/lib/aggro/subscriber_spec.rb +59 -0
  131. data/spec/lib/aggro/subscription_spec.rb +50 -0
  132. data/spec/lib/aggro/transform/boolean_spec.rb +23 -0
  133. data/spec/lib/aggro/transform/email_spec.rb +13 -0
  134. data/spec/lib/aggro/transform/id_spec.rb +70 -0
  135. data/spec/lib/aggro/transform/integer_spec.rb +30 -0
  136. data/spec/lib/aggro/transform/money_spec.rb +34 -0
  137. data/spec/lib/aggro/transform/string_spec.rb +15 -0
  138. data/spec/lib/aggro/transform/time_interval_spec.rb +29 -0
  139. data/spec/lib/aggro_spec.rb +63 -19
  140. data/spec/spec_helper.rb +21 -2
  141. metadata +283 -3
@@ -0,0 +1,16 @@
1
+ RSpec.describe MessageParser do
2
+ subject(:router) { MessageRouter.new }
3
+
4
+ let(:message_class) { spy(parse: double) }
5
+
6
+ before do
7
+ stub_const 'Aggro::MESSAGE_TYPES', '01' => message_class
8
+ end
9
+
10
+ describe '.parse' do
11
+ it 'should parse messages via message parse function' do
12
+ MessageParser.parse '01'
13
+ expect(message_class).to have_received(:parse).with '01'
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,35 @@
1
+ RSpec.describe MessageRouter do
2
+ subject(:router) { MessageRouter.new }
3
+
4
+ let(:message) { message_class.new }
5
+ let(:message_class) { Class.new }
6
+
7
+ before do
8
+ stub_const 'Aggro::MESSAGE_TYPES', '01' => message_class
9
+ end
10
+
11
+ describe '#attach_handler' do
12
+ it 'should attach a given callable to handle a specific message type' do
13
+ callable = ->(parsed) { parsed }
14
+
15
+ router.attach_handler message_class, callable
16
+
17
+ expect(router.handles?(message_class)).to be_truthy
18
+ end
19
+
20
+ it 'should attach a given block to handle a specific message type' do
21
+ router.attach_handler(message_class) { |parsed| parsed }
22
+
23
+ expect(router.handles?(message_class)).to be_truthy
24
+ end
25
+ end
26
+
27
+ describe '#route' do
28
+ it 'should route messages to the correct code' do
29
+ router.attach_handler(message_class) { |msg| @msg = msg }
30
+ router.route message
31
+
32
+ expect(@msg).to eq message
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,21 @@
1
+ RSpec.describe NanomsgTransport::SocketError do
2
+ subject(:error) { NanomsgTransport::SocketError.new 5 }
3
+
4
+ let(:nanomsg_api) { spy nn_strerror: 'No u' }
5
+
6
+ before do
7
+ stub_const 'NNCore::LibNanomsg', nanomsg_api
8
+ end
9
+
10
+ describe '#to_s' do
11
+ it 'should ask nanomsg for the meaning of the errno' do
12
+ error.to_s
13
+
14
+ expect(nanomsg_api).to have_received(:nn_strerror).with 5
15
+ end
16
+
17
+ it 'should provide a human readable error message' do
18
+ expect(error.to_s).to eq "Last nanomsg API call failed with 'No u'"
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,37 @@
1
+ RSpec.describe NanomsgTransport do
2
+ let(:host) { 'tcp://127.0.0.1:7250' }
3
+ let(:message) { SecureRandom.hex }
4
+
5
+ xit 'should work for REQREP' do
6
+ server = NanomsgTransport.server(host) { |rec| @rec = rec }.start
7
+ client = NanomsgTransport.client host
8
+
9
+ client.post message
10
+
11
+ server.stop
12
+ client.close_socket
13
+
14
+ expect(@rec).to eq message
15
+ end
16
+
17
+ it 'should work for PUBSUB' do
18
+ publisher = NanomsgTransport.publisher(host)
19
+ publisher.open_socket
20
+
21
+ @reced = []
22
+
23
+ subscriber = NanomsgTransport.subscriber(host) { |rec| @reced << rec }
24
+ subscriber.add_subscription('foo').start
25
+
26
+ sleep 0.1
27
+
28
+ publisher.publish 'foobar'
29
+ publisher.publish 'bazbar'
30
+
31
+ publisher.close_socket
32
+ subscriber.stop
33
+
34
+ expect(@reced).to include 'foobar'
35
+ expect(@reced).to_not include 'bazbar'
36
+ end
37
+ end
@@ -0,0 +1,38 @@
1
+ RSpec.describe NodeList do
2
+ subject(:node_list) { NodeList.new }
3
+
4
+ let(:second_node_list) { NodeList.new }
5
+
6
+ let(:id) { SecureRandom.uuid }
7
+
8
+ let(:servers) { 10.times.map { |i| "localhost:#{5000 + i}" } }
9
+ let(:nodes) { servers.map { |server| Node.new(server, server) } }
10
+
11
+ describe '#add' do
12
+ it 'should add the node to the list of nodes' do
13
+ nodes.each { |node| node_list.add node }
14
+
15
+ expect(node_list.nodes.length).to eq nodes.length
16
+ end
17
+ end
18
+
19
+ describe '#nodes_for' do
20
+ before do
21
+ nodes.each { |node| node_list.add node }
22
+ end
23
+
24
+ it 'should get a number of nodes equal to the replication factor' do
25
+ expect(node_list.nodes_for(id, 4).length).to eq 4
26
+ end
27
+
28
+ it 'should have a default replication factor of 3' do
29
+ expect(node_list.nodes_for(id).length).to eq 3
30
+ end
31
+
32
+ it 'should give the same servers regardless of order of node addition' do
33
+ nodes.shuffle.each { |node| second_node_list.add node }
34
+
35
+ expect(node_list.nodes_for(id)).to eq second_node_list.nodes_for(id)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,44 @@
1
+ RSpec.describe Node do
2
+ subject(:node) { Node.new('flashing-sparkle') }
3
+
4
+ describe '#client' do
5
+ it 'should return a client for the node using the current transport' do
6
+ expect(node.client).to be_a Client
7
+ end
8
+ end
9
+
10
+ describe '#publisher_endpoint' do
11
+ context 'node returns an Endpoint message' do
12
+ it 'should ask the node for the publisher endpoint' do
13
+ endpoint = Message::Endpoint.new('endpoint')
14
+ allow(node).to receive(:client).and_return(double post: endpoint)
15
+
16
+ expect(node.publisher_endpoint).to eq 'endpoint'
17
+ end
18
+ end
19
+
20
+ context 'node does not return an Endpoint message' do
21
+ it 'should ask the node for the publisher endpoint' do
22
+ allow(node).to receive(:client).and_return(double post: 'not endpoint')
23
+
24
+ expect { node.publisher_endpoint }.to raise_error
25
+ end
26
+ end
27
+ end
28
+
29
+ describe '#to_s' do
30
+ let(:moved_node) { Node.new('flashing-sparkle') }
31
+ let(:other_node) { Node.new('dancing-sparkle') }
32
+
33
+ let(:ring) { ConsistentHashing::Ring.new }
34
+ let(:hasher) { ring.method(:hash_key) }
35
+
36
+ it 'should be consistently hashed the same if id matches' do
37
+ expect(hasher.call(node)).to eq hasher.call(moved_node)
38
+ end
39
+
40
+ it 'should be consistently hashed differently if id differs' do
41
+ expect(hasher.call(node)).to_not eq hasher.call(other_node)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,22 @@
1
+ RSpec.describe Projection do
2
+ class CatSerializer
3
+ include Aggro::Projection
4
+ end
5
+
6
+ subject(:projection) { CatSerializer.new id }
7
+
8
+ let(:id) { SecureRandom.uuid }
9
+ let(:event_bus) { spy }
10
+
11
+ before do
12
+ allow(Aggro).to receive(:event_bus).and_return(event_bus)
13
+ end
14
+
15
+ describe '.new' do
16
+ it 'should subscribe itself to events for the given ID' do
17
+ projection
18
+
19
+ expect(event_bus).to have_received(:subscribe).with id, projection
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,47 @@
1
+ RSpec.describe Query do
2
+ class TestQuery
3
+ include Query
4
+
5
+ string :term
6
+ validates :term, presence: true
7
+
8
+ integer :times
9
+ end
10
+
11
+ subject(:query) { TestQuery.new term: 'puppy', times: '100' }
12
+
13
+ describe '#attributes' do
14
+ it 'should return a hash of attributes' do
15
+ expect(query.attributes).to be_a Hash
16
+ expect(query.attributes[:term]).to eq 'puppy'
17
+ end
18
+ end
19
+
20
+ describe '#serialized_attributes' do
21
+ it 'should return a hash of attributes run through the transforms' do
22
+ expect(query.serialized_attributes).to be_a Hash
23
+ expect(query.serialized_attributes[:times]).to eq 100
24
+ end
25
+ end
26
+
27
+ describe '#to_details' do
28
+ it 'should return a hash containing the query name and arguments' do
29
+ details = query.to_details
30
+
31
+ expect(details).to be_a Hash
32
+ expect(details[:name]).to eq 'TestQuery'
33
+ expect(details[:args][:term]).to eq 'puppy'
34
+ expect(details[:args][:times]).to eq 100
35
+ end
36
+ end
37
+
38
+ describe '#valid?' do
39
+ it 'should return true if query is valid' do
40
+ expect(query.valid?).to be_truthy
41
+ end
42
+
43
+ it 'should return false if query is not valid' do
44
+ expect(TestQuery.new.valid?).to be_falsey
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,84 @@
1
+ RSpec.describe SagaRunner do
2
+ subject(:runner) { SagaRunner.new id }
3
+
4
+ let(:id) { SecureRandom.uuid }
5
+
6
+ let(:causation_id) { SecureRandom.uuid }
7
+ let(:correlation_id) { SecureRandom.uuid }
8
+ let(:saga) { spy causation_id: causation_id, correlation_id: correlation_id }
9
+ let(:handler) { -> { @ran = true } }
10
+ let(:saga_class) do
11
+ double new: saga, initial: :first_step, handler_for_step: handler
12
+ end
13
+
14
+ before do
15
+ stub_const 'TestSaga', saga_class
16
+
17
+ config = double(server_node?: true, node_name: SecureRandom.uuid, nodes: [])
18
+ allow(Aggro).to receive(:cluster_config).and_return config
19
+ end
20
+
21
+ describe '#apply_command' do
22
+ context 'the command is a StartSaga' do
23
+ let(:details) { { test: 'foo' } }
24
+ let(:command) do
25
+ SagaRunner::StartSaga.new name: 'TestSaga', id: id, details: details
26
+ end
27
+
28
+ it 'should create a new saga based on the given name' do
29
+ runner.send :apply_command, command
30
+ expect(saga_class).to have_received(:new)
31
+ end
32
+
33
+ it 'should set @runner on the saga' do
34
+ runner.send :apply_command, command
35
+ expect(saga.instance_variable_get(:@runner)).to eq runner
36
+ end
37
+
38
+ it 'should execute the initial step' do
39
+ runner.send :apply_command, command
40
+ expect(saga.instance_variable_get(:@ran)).to be_truthy
41
+ end
42
+ end
43
+ end
44
+
45
+ describe '#reject' do
46
+ let(:proxy) { spy }
47
+
48
+ before do
49
+ allow(runner).to receive(:did).and_return proxy
50
+ runner.instance_variable_set :@saga, saga
51
+ end
52
+
53
+ it 'should call the rejected event' do
54
+ runner.reject 'reason'
55
+
56
+ expect(proxy).to have_received(:rejected).with reason: 'reason'
57
+ end
58
+ end
59
+
60
+ describe '#resolve' do
61
+ let(:proxy) { spy }
62
+
63
+ before do
64
+ allow(runner).to receive(:did).and_return proxy
65
+ runner.instance_variable_set :@saga, saga
66
+ end
67
+
68
+ it 'should call the resolved event' do
69
+ runner.resolve 'value'
70
+
71
+ expect(proxy).to have_received(:resolved).with value: 'value'
72
+ end
73
+ end
74
+
75
+ describe '#did' do
76
+ it 'should set the @_context with @details' do
77
+ runner.instance_variable_set :@details, foo: 'bar'
78
+
79
+ runner.send :did
80
+
81
+ expect(runner.instance_variable_get(:@_context)[:foo]).to eq 'bar'
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,126 @@
1
+ RSpec.describe Saga do
2
+ class PizzaMaker
3
+ include Saga
4
+
5
+ generate_id :pizza_id
6
+ id :oven_id
7
+ string :dough_type
8
+
9
+ initial :prepare_dough
10
+
11
+ step :prepare_dough do
12
+ pizza = Pizza.create.command(command1)
13
+
14
+ bind pizza do
15
+ def started_to_be_made
16
+ transition_to :add_toppings
17
+ end
18
+
19
+ def failed_to_start_making
20
+ reject "I couldn't makea the pizza"
21
+ end
22
+ end
23
+ end
24
+
25
+ step :add_toppings do
26
+ pizza = Pizza.find(pizza_id).command(command2)
27
+
28
+ bind pizza do
29
+ def started_to_be_made
30
+ transition_to :cook
31
+ end
32
+
33
+ def failed_to_add_toppings
34
+ reject "I couldn't topa the pizza"
35
+ end
36
+ end
37
+ end
38
+
39
+ step :cook do
40
+ oven = Oven.find(oven_id).command(command3)
41
+
42
+ bind oven do
43
+ def cooked_thing
44
+ resolve pizza_id
45
+ end
46
+
47
+ def failed_to_cook_thing
48
+ reject "I couldn't cooka the pizza"
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ subject(:saga) { PizzaMaker.new(oven_id: oven_id, dough_type: 'yummy') }
55
+
56
+ let(:oven_id) { SecureRandom.uuid }
57
+
58
+ let(:client) { spy }
59
+ let(:node) { double(client: client) }
60
+ let(:fake_locator) { double primary_node: node }
61
+ let(:locator_class) { double new: fake_locator }
62
+
63
+ let(:fake_status) { double }
64
+ let(:saga_status_class) { double new: fake_status }
65
+
66
+ before do
67
+ stub_const 'Aggro::Locator', locator_class
68
+ stub_const 'Aggro::SagaStatus', saga_status_class
69
+
70
+ allow(client).to receive(:post).and_return Message::OK.new
71
+ end
72
+
73
+ describe '#start' do
74
+ before do
75
+ saga.start
76
+
77
+ expect(client).to have_received(:post).with Message::StartSaga
78
+ end
79
+
80
+ it 'should return a SagaStatus' do
81
+ expect(saga.start).to eq fake_status
82
+ end
83
+ end
84
+
85
+ describe '#transition' do
86
+ it 'should call transition on the runner' do
87
+ runner = spy transition: true
88
+ saga.instance_variable_set(:@runner, runner)
89
+ saga.send :transition, :add_toppings
90
+
91
+ expect(runner).to have_received(:transition).with :add_toppings
92
+ end
93
+
94
+ it 'should fail if no runner' do
95
+ expect { saga.send :transition, :add_toppings }.to raise_error
96
+ end
97
+ end
98
+
99
+ describe '#resolve' do
100
+ it 'should call transition on the runner' do
101
+ runner = spy resolve: true
102
+ saga.instance_variable_set(:@runner, runner)
103
+ saga.send :resolve, 'value'
104
+
105
+ expect(runner).to have_received(:resolve).with 'value'
106
+ end
107
+
108
+ it 'should fail if no runner' do
109
+ expect { saga.send :resolve, 'value' }.to raise_error
110
+ end
111
+ end
112
+
113
+ describe '#reject' do
114
+ it 'should call transition on the runner' do
115
+ runner = spy reject: true
116
+ saga.instance_variable_set(:@runner, runner)
117
+ saga.send :reject, 'reason'
118
+
119
+ expect(runner).to have_received(:reject).with 'reason'
120
+ end
121
+
122
+ it 'should fail if no runner' do
123
+ expect { saga.send :reject, 'reason' }.to raise_error
124
+ end
125
+ end
126
+ end