ruby_event_store-rspec 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fd0942f48e1aea9cc5a5a67ffb5bbe5816c774f9d41db4bc0fda35e5061dd9d6
4
+ data.tar.gz: 7376fe3939f833e8747fa3dd511de534a07616c45fd2f71704d05d325ae6d681
5
+ SHA512:
6
+ metadata.gz: cd0d6771ff5f9716171c2eb33209d5844bed93b24acef8704259d8784d97bbd756cb41ef7d20b3d265a35f90b3d5b9a514987017091f8aec51550958b0e1f5bf
7
+ data.tar.gz: 2c1e227d7560312c0cc9f8934d1e501cbff3df854f109adf421c37909398931ccaefa1cc470e0aaae34578e141121160eb66853ac0dc902c11859704c8aeda5f
@@ -0,0 +1 @@
1
+ ../.mutant.yml
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
3
+ gemspec name: 'ruby_event_store-rspec'
4
+
5
+ eval_gemfile '../support/bundler/Gemfile.shared'
6
+
7
+ gem 'ruby_event_store', path: '../ruby_event_store'
8
+ gem 'aggregate_root', path: '../aggregate_root'
9
+
10
+ gem 'diff-lcs', '= 1.3.0'
@@ -0,0 +1,113 @@
1
+ PATH
2
+ remote: ../aggregate_root
3
+ specs:
4
+ aggregate_root (2.0.0)
5
+ ruby_event_store (= 2.0.0)
6
+
7
+ PATH
8
+ remote: ../ruby_event_store
9
+ specs:
10
+ ruby_event_store (2.0.0)
11
+ concurrent-ruby (~> 1.0, >= 1.1.6)
12
+
13
+ PATH
14
+ remote: .
15
+ specs:
16
+ ruby_event_store-rspec (2.0.0)
17
+ rspec (~> 3.0)
18
+
19
+ GEM
20
+ remote: https://rubygems.org/
21
+ remote: https://oss:7AXfeZdAfCqL1PvHm2nvDJO6Zd9UW8IK@gem.mutant.dev/
22
+ specs:
23
+ abstract_type (0.0.7)
24
+ adamantium (0.2.0)
25
+ ice_nine (~> 0.11.0)
26
+ memoizable (~> 0.4.0)
27
+ anima (0.3.2)
28
+ abstract_type (~> 0.0.7)
29
+ adamantium (~> 0.2)
30
+ equalizer (~> 0.0.11)
31
+ ast (2.4.1)
32
+ concord (0.1.6)
33
+ adamantium (~> 0.2.0)
34
+ equalizer (~> 0.0.9)
35
+ concurrent-ruby (1.1.7)
36
+ diff-lcs (1.3)
37
+ equalizer (0.0.11)
38
+ ice_nine (0.11.2)
39
+ memoizable (0.4.2)
40
+ thread_safe (~> 0.3, >= 0.3.1)
41
+ mprelude (0.1.0)
42
+ abstract_type (~> 0.0.7)
43
+ adamantium (~> 0.2.0)
44
+ concord (~> 0.1.5)
45
+ equalizer (~> 0.0.9)
46
+ ice_nine (~> 0.11.1)
47
+ procto (~> 0.0.2)
48
+ mutant (0.10.22)
49
+ abstract_type (~> 0.0.7)
50
+ adamantium (~> 0.2.0)
51
+ anima (~> 0.3.1)
52
+ ast (~> 2.2)
53
+ concord (~> 0.1.5)
54
+ diff-lcs (~> 1.3)
55
+ equalizer (~> 0.0.9)
56
+ ice_nine (~> 0.11.1)
57
+ memoizable (~> 0.4.2)
58
+ mprelude (~> 0.1.0)
59
+ parser (~> 3.0.0)
60
+ procto (~> 0.0.2)
61
+ unparser (~> 0.5.6)
62
+ variable (~> 0.0.1)
63
+ mutant-license (0.1.1.2.1627430819213747598431630701693729869473.0)
64
+ mutant-rspec (0.10.22)
65
+ mutant (= 0.10.22)
66
+ rspec-core (>= 3.8.0, < 4.0.0)
67
+ parser (3.0.0.0)
68
+ ast (~> 2.4.1)
69
+ procto (0.0.3)
70
+ rake (13.0.3)
71
+ rspec (3.10.0)
72
+ rspec-core (~> 3.10.0)
73
+ rspec-expectations (~> 3.10.0)
74
+ rspec-mocks (~> 3.10.0)
75
+ rspec-core (3.10.1)
76
+ rspec-support (~> 3.10.0)
77
+ rspec-expectations (3.10.1)
78
+ diff-lcs (>= 1.2.0, < 2.0)
79
+ rspec-support (~> 3.10.0)
80
+ rspec-mocks (3.10.1)
81
+ diff-lcs (>= 1.2.0, < 2.0)
82
+ rspec-support (~> 3.10.0)
83
+ rspec-support (3.10.1)
84
+ thread_safe (0.3.6)
85
+ unparser (0.5.6)
86
+ abstract_type (~> 0.0.7)
87
+ adamantium (~> 0.2.0)
88
+ anima (~> 0.3.1)
89
+ concord (~> 0.1.5)
90
+ diff-lcs (~> 1.3)
91
+ equalizer (~> 0.0.9)
92
+ mprelude (~> 0.1.0)
93
+ parser (>= 3.0.0)
94
+ procto (~> 0.0.2)
95
+ variable (0.0.1)
96
+ equalizer (~> 0.0.11)
97
+
98
+ PLATFORMS
99
+ ruby
100
+
101
+ DEPENDENCIES
102
+ aggregate_root!
103
+ diff-lcs (= 1.3.0)
104
+ mutant (~> 0.10.21)
105
+ mutant-license!
106
+ mutant-rspec (~> 0.10.21)
107
+ rake (>= 10.0)
108
+ rspec (~> 3.6)
109
+ ruby_event_store!
110
+ ruby_event_store-rspec!
111
+
112
+ BUNDLED WITH
113
+ 2.1.4
@@ -0,0 +1,21 @@
1
+ GEM_VERSION = $(shell cat ../RES_VERSION)
2
+ GEM_NAME = ruby_event_store-rspec
3
+ REQUIRE = ruby_event_store/rspec
4
+ IGNORE = RubyEventStore::RSpec::Matchers\#differ \
5
+ RubyEventStore::RSpec::Matchers\#formatter \
6
+ RubyEventStore::RSpec::Matchers\#have_published \
7
+ RubyEventStore::RSpec::Matchers\#have_applied \
8
+ RubyEventStore::RSpec::Matchers\#have_subscribed_to_events \
9
+ RubyEventStore::RSpec::Matchers\#publish \
10
+ RubyEventStore::RSpec::Matchers\#be_an_event \
11
+ RubyEventStore::RSpec::Publish\#last_event \
12
+ RubyEventStore::RSpec::Matchers::ListPhraser.all_but_last
13
+
14
+ SUBJECT ?= RubyEventStore::RSpec*
15
+ DATABASE_URL ?= sqlite3::memory:
16
+
17
+ include ../support/make/install.mk
18
+ include ../support/make/test.mk
19
+ include ../support/make/mutant.mk
20
+ include ../support/make/gem.mk
21
+ include ../support/make/help.mk
@@ -0,0 +1,206 @@
1
+ # RailsEventStore::RSpec
2
+
3
+ ## Installation
4
+
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ group :test do
9
+ gem 'ruby_event_store-rspec'
10
+ end
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### be_event
16
+
17
+ The `be_event` matcher enables you to make expectations on a domain event. It exposes fluent interface.
18
+
19
+ ```ruby
20
+ OrderPlaced = Class.new(RubyEventStore::Event)
21
+ domain_event = OrderPlaced.new(
22
+ data: {
23
+ order_id: 42,
24
+ net_value: BigDecimal.new("1999.0")
25
+ },
26
+ metadata: {
27
+ remote_ip: '1.2.3.4'
28
+ }
29
+ )
30
+
31
+ expect(domain_event)
32
+ .to(be_an_event(OrderPlaced)
33
+ .with_data(order_id: 42, net_value: BigDecimal.new("1999.0"))
34
+ .with_metadata(remote_ip: '1.2.3.4'))
35
+ ```
36
+
37
+ By default the behaviour of `with_data` and `with_metadata` is not strict, that is the expectation is met when all specified values for keys match. Additional data or metadata that is not specified to be expected does not change the outcome.
38
+
39
+ ```ruby
40
+ domain_event = OrderPlaced.new(
41
+ data: {
42
+ order_id: 42,
43
+ net_value: BigDecimal.new("1999.0")
44
+ }
45
+ )
46
+
47
+ # this would pass even though data contains also net_value
48
+ expect(domain_event).to be_an_event(OrderPlaced).with_data(order_id: 42)
49
+ ```
50
+
51
+ This matcher is both [composable](http://rspec.info/blog/2014/01/new-in-rspec-3-composable-matchers/) and accepting [built-in matchers](https://relishapp.com/rspec/rspec-expectations/v/3-6/docs/built-in-matchers) as a part of an expectation.
52
+
53
+ ```ruby
54
+ expect(domain_event).to be_an_event(OrderPlaced).with_data(order_id: kind_of(Integer))
55
+ expect([domain_event]).to include(an_event(OrderPlaced))
56
+ ```
57
+
58
+ If you depend on matching the exact data or metadata, there's a `strict` modifier.
59
+
60
+ ```ruby
61
+ domain_event = OrderPlaced.new(
62
+ data: {
63
+ order_id: 42,
64
+ net_value: BigDecimal.new("1999.0")
65
+ }
66
+ )
67
+
68
+ # this would fail as data contains unexpected net_value
69
+ expect(domain_event).to be_an_event(OrderPlaced).with_data(order_id: 42).strict
70
+ ```
71
+
72
+ Mind that `strict` makes both `with_data` and `with_metadata` behave in a stricter way. If you need to mix both, i.e. strict data but non-strict metadata then consider composing matchers.
73
+
74
+ ```ruby
75
+ expect(domain_event)
76
+ .to(be_event(OrderPlaced).with_data(order_id: 42, net_value: BigDecimal.new("1999.0")).strict
77
+ .and(an_event(OrderPlaced).with_metadata(timestamp: kind_of(Time)))
78
+ ```
79
+
80
+ You may have noticed the same matcher being referenced as `be_event`, `be_an_event` and `an_event`. There's also just `event`. Use whichever reads better grammatically.
81
+
82
+ ### have_published
83
+
84
+ Use this matcher to target `event_store` and reading from streams specifically.
85
+ In a simplest form it would read all streams backward up to a page limit (100 events) and check whether the expectation holds true. Its behaviour can be best compared to the `include` matcher — it is satisfied by at least one element present in the collection. You're encouraged to compose it with `be_event`.
86
+
87
+ ```ruby
88
+ event_store = RailsEventStore::Client.new
89
+ event_store.publish(OrderPlaced.new(data: { order_id: 42 }))
90
+
91
+ expect(event_store).to have_published(an_event(OrderPlaced))
92
+ ```
93
+
94
+ Expectation can be narrowed to the specific stream.
95
+
96
+ ```ruby
97
+ event_store = RailsEventStore::Client.new
98
+ event_store.publish(OrderPlaced.new(data: { order_id: 42 }), stream_name: "Order$42")
99
+
100
+ expect(event_store).to have_published(an_event(OrderPlaced)).in_stream("Order$42")
101
+ ```
102
+
103
+ It is sometimes important to ensure no additional events have been published. Luckliy there's a modifier to cover that usecase.
104
+
105
+ ```ruby
106
+ expect(event_store).not_to have_published(an_event(OrderPlaced)).once
107
+ expect(event_store).to have_published(an_event(OrderPlaced)).exactly(2).times
108
+ ```
109
+
110
+ Finally you can make expectation on several events at once.
111
+
112
+ ```ruby
113
+ expect(event_store).to have_published(
114
+ an_event(OrderPlaced),
115
+ an_event(OrderExpired).with_data(expired_at: be_between(Date.yesterday, Date.tomorrow))
116
+ )
117
+ ```
118
+
119
+ If there's a usecase not covered by examples above or you need a different set of events to make expectations on you can always resort to a more verbose approach and skip `have_published`.
120
+
121
+ ```ruby
122
+ expect(event_store.read_events_forward("OrderAuditLog$42", count: 2)).to eq([
123
+ an_event(OrderPlaced),
124
+ an_event(OrderExpired)
125
+ ])
126
+ ```
127
+
128
+ ### have_applied
129
+
130
+ This matcher is intended to be used on [aggregate root](https://github.com/RailsEventStore/rails_event_store/tree/master/aggregate_root#usage). Behaviour is almost identical to `have_published` counterpart, except the concept of stream. Expecations are made against internal unpublished events collection.
131
+
132
+ ```ruby
133
+ class Order
134
+ include AggregateRoot
135
+ HasBeenAlreadySubmitted = Class.new(StandardError)
136
+ HasExpired = Class.new(StandardError)
137
+
138
+ def initialize
139
+ self.state = :new
140
+ # any other code here
141
+ end
142
+
143
+ def submit
144
+ raise HasBeenAlreadySubmitted if state == :submitted
145
+ raise HasExpired if state == :expired
146
+ apply OrderSubmitted.new(data: {delivery_date: Time.now + 24.hours})
147
+ end
148
+
149
+ def expire
150
+ apply OrderExpired.new
151
+ end
152
+
153
+ private
154
+ attr_accessor :state
155
+
156
+ def apply_order_submitted(event)
157
+ self.state = :submitted
158
+ end
159
+
160
+ def apply_order_expired(event)
161
+ self.state = :expired
162
+ end
163
+ end
164
+ ```
165
+
166
+ ```ruby
167
+ aggregate_root = Order.new
168
+ aggregate_root.submit
169
+
170
+ expect(aggregate_root).to have_applied(event(OrderSubmitted)).once
171
+ ```
172
+
173
+ ### have_subscribed_to_events
174
+
175
+ Use this matcher to make sure that a handler has subscribed to events in target `event_store` or not.
176
+
177
+ ```ruby
178
+ event_store = RailsEventStore::Client.new
179
+
180
+ # it will pass if Handler has subscribed to both events
181
+ expect(Handler).to have_subscribed_to_events(FooEvent, BarEvent).in(event_store)
182
+
183
+ # it will fail if Handler has subscribed to any event
184
+ expect(Handler).not_to have_subscribed_to_events(FooEvent, BarEvent).in(event_store)
185
+ ```
186
+ ## Code status
187
+
188
+ [![Build Status](https://travis-ci.org/RailsEventStore/rails_event_store-rspec.svg?branch=master)](https://travis-ci.org/RailsEventStore/rails_event_store-rspec)
189
+ [![Gem Version](https://badge.fury.io/rb/rails_event_store-rspec.svg)](https://badge.fury.io/rb/rails_event_store-rspec)
190
+
191
+ We're aiming for 100% mutation coverage in this project. This is why:
192
+
193
+ * [Why I want to introduce mutation testing to the rails_event_store gem](https://blog.arkency.com/2015/04/why-i-want-to-introduce-mutation-testing-to-the-rails-event-store-gem/)
194
+ * [Mutation testing and continuous integration](https://blog.arkency.com/2015/05/mutation-testing-and-continuous-integration/)
195
+
196
+ Whenever you fix a bug or add a new feature, we require that the coverage doesn't go down.
197
+
198
+ ## Development
199
+
200
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
201
+
202
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
203
+
204
+ ## Contributing
205
+
206
+ Bug reports and pull requests are welcome on GitHub at https://github.com/RailsEventStore/rails_event_store-rspec.
@@ -0,0 +1,13 @@
1
+ require 'ruby_event_store/rspec'
2
+
3
+ warn <<~EOW
4
+ The 'rails_event_store-rspec' gem has been renamed.
5
+
6
+ Please change your Gemfile or gemspec
7
+ to reflect its new name:
8
+
9
+ 'ruby_event_store-rspec'
10
+
11
+ EOW
12
+
13
+ RailsEventStore::RSpec = RubyEventStore::RSpec
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+
5
+ module RubyEventStore
6
+ module RSpec
7
+ NotSupported = Class.new(StandardError)
8
+ end
9
+ end
10
+
11
+ require "ruby_event_store/rspec/version"
12
+ require "ruby_event_store/rspec/be_event"
13
+ require "ruby_event_store/rspec/have_published"
14
+ require "ruby_event_store/rspec/have_applied"
15
+ require "ruby_event_store/rspec/have_subscribed_to_events"
16
+ require "ruby_event_store/rspec/publish"
17
+ require "ruby_event_store/rspec/apply"
18
+ require "ruby_event_store/rspec/matchers"
19
+
20
+ ::RSpec.configure do |config|
21
+ config.include ::RubyEventStore::RSpec::Matchers
22
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyEventStore
4
+ module RSpec
5
+ class Apply
6
+ def in(aggregate)
7
+ @aggregate = aggregate
8
+ self
9
+ end
10
+
11
+ def strict
12
+ @matcher = ::RSpec::Matchers::BuiltIn::Match.new(@expected)
13
+ self
14
+ end
15
+
16
+ def matches?(event_proc)
17
+ raise_aggregate_not_set unless @aggregate
18
+ before = @aggregate.unpublished_events.to_a
19
+ event_proc.call
20
+ @applied_events = @aggregate.unpublished_events.to_a - before
21
+ if match_events?
22
+ @matcher.matches?(@applied_events)
23
+ else
24
+ !@applied_events.empty?
25
+ end
26
+ end
27
+
28
+ def failure_message
29
+ if match_events?
30
+ <<-EOS
31
+ expected block to have applied:
32
+
33
+ #{@expected}
34
+
35
+ but applied:
36
+
37
+ #{@applied_events}
38
+ EOS
39
+ else
40
+ "expected block to have applied any events"
41
+ end
42
+ end
43
+
44
+ def failure_message_when_negated
45
+ if match_events?
46
+ <<-EOS
47
+ expected block not to have applied:
48
+
49
+ #{@expected}
50
+
51
+ but applied:
52
+
53
+ #{@applied_events}
54
+ EOS
55
+ else
56
+ "expected block not to have applied any events"
57
+ end
58
+ end
59
+
60
+ def description
61
+ "apply events"
62
+ end
63
+
64
+ def supports_block_expectations?
65
+ true
66
+ end
67
+
68
+ private
69
+
70
+ def initialize(*expected)
71
+ @expected = expected
72
+ @matcher = ::RSpec::Matchers::BuiltIn::Include.new(*expected)
73
+ end
74
+
75
+ def match_events?
76
+ !@expected.empty?
77
+ end
78
+
79
+ def raise_aggregate_not_set
80
+ raise SyntaxError, "You have to set the aggregate instance with `in`, e.g. `expect { ... }.to apply(an_event(MyEvent)).in(aggregate)`"
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyEventStore
4
+ module RSpec
5
+ class BeEvent
6
+ class KindMatcher
7
+ def initialize(expected)
8
+ @expected = expected
9
+ end
10
+
11
+ def matches?(actual)
12
+ @expected === actual
13
+ end
14
+ end
15
+
16
+ class DataMatcher
17
+ def initialize(expected, strict:)
18
+ @strict = strict
19
+ @expected = expected
20
+ end
21
+
22
+ def matches?(actual)
23
+ return true unless @expected
24
+ matcher = @strict ? ::RSpec::Matchers::BuiltIn::Match : ::RSpec::Matchers::BuiltIn::Include
25
+ matcher.new(@expected).matches?(actual)
26
+ end
27
+ end
28
+
29
+ class FailureMessage
30
+ class ExpectedLine
31
+ def initialize(expected_klass, expected_metadata, expected_data)
32
+ @expected_klass = expected_klass
33
+ @expected_metadata = expected_metadata
34
+ @expected_data = expected_data
35
+ end
36
+
37
+ def to_s
38
+ ["\nexpected: ", @expected_klass, with, metadata, data]
39
+ end
40
+
41
+ private
42
+
43
+ def with
44
+ " with" if [@expected_data, @expected_metadata].any?
45
+ end
46
+
47
+ def data
48
+ [" data: ", @expected_data] if @expected_data
49
+ end
50
+
51
+ def metadata
52
+ [" metadata: ", @expected_metadata] if @expected_metadata
53
+ end
54
+ end
55
+
56
+ class ActualLine
57
+ def initialize(actual_klass, actual_metadata, actual_data, expected_metadata, expected_data)
58
+ @actual_klass = actual_klass
59
+ @actual_metadata = actual_metadata
60
+ @actual_data = actual_data
61
+ @expected_metadata = expected_metadata
62
+ @expected_data = expected_data
63
+ end
64
+
65
+ def to_s
66
+ ["\n got: ", @actual_klass, with, metadata, data, "\n"]
67
+ end
68
+
69
+ private
70
+
71
+ def with
72
+ " with" if [@expected_data, @expected_metadata].any?
73
+ end
74
+
75
+ def data
76
+ [" data: ", @actual_data] if @expected_data
77
+ end
78
+
79
+ def metadata
80
+ [" metadata: ", @actual_metadata] if @expected_metadata
81
+ end
82
+ end
83
+
84
+ class Diff
85
+ def initialize(actual, expected, label, differ:)
86
+ @actual = actual
87
+ @expected = expected
88
+ @label = label
89
+ @differ = differ
90
+ end
91
+
92
+ def to_s
93
+ @expected && ["\n#{@label} diff:", @differ.diff_as_string(@actual.to_s, @expected.to_s)]
94
+ end
95
+ end
96
+
97
+ def initialize(expected_klass, actual_klass, expected_data, actual_data, expected_metadata, actual_metadata, differ:)
98
+ @expected_klass = expected_klass
99
+ @actual_klass = actual_klass
100
+ @expected_data = expected_data
101
+ @actual_data = actual_data
102
+ @expected_metadata = expected_metadata
103
+ @actual_metadata = actual_metadata
104
+ @differ = differ
105
+ end
106
+
107
+ def to_s
108
+ [
109
+ ExpectedLine.new(@expected_klass, @expected_metadata, @expected_data),
110
+ ActualLine.new(@actual_klass, @actual_metadata.to_h, @actual_data, @expected_metadata, @expected_data),
111
+ Diff.new(@actual_metadata.to_h, @expected_metadata, "Metadata", differ: @differ),
112
+ Diff.new(@actual_data, @expected_data, "Data", differ: @differ)
113
+ ].map(&:to_s).join
114
+ end
115
+ end
116
+
117
+ include ::RSpec::Matchers::Composable
118
+
119
+ def initialize(expected, differ:, formatter:)
120
+ @expected = expected
121
+ @differ = differ
122
+ @formatter = formatter
123
+ end
124
+
125
+ def matches?(actual)
126
+ @actual = actual
127
+ matches_kind && matches_data && matches_metadata
128
+ end
129
+
130
+ def with_data(expected_data)
131
+ @expected_data = expected_data
132
+ self
133
+ end
134
+
135
+ def with_metadata(expected_metadata)
136
+ @expected_metadata = expected_metadata
137
+ self
138
+ end
139
+
140
+ def failure_message
141
+ actual_data = actual.data if actual.respond_to?(:data)
142
+ actual_metadata = actual.metadata if actual.respond_to?(:metadata)
143
+ FailureMessage.new(expected, actual.class, expected_data, actual_data, expected_metadata, actual_metadata, differ: differ).to_s
144
+ end
145
+
146
+ def failure_message_when_negated
147
+ %Q{
148
+ expected: not a kind of #{expected}
149
+ got: #{actual.class}
150
+ }
151
+ end
152
+
153
+ def strict
154
+ @strict = true
155
+ self
156
+ end
157
+
158
+ def description
159
+ "be an event #{formatter.(expected)}#{data_and_metadata_expectations_description}"
160
+ end
161
+
162
+ def data_and_metadata_expectations_description
163
+ predicate = strict? ? "matching" : "including"
164
+ expectation_list = []
165
+ expectation_list << "with data #{predicate} #{formatter.(expected_data)}" if expected_data
166
+ expectation_list << "with metadata #{predicate} #{formatter.(expected_metadata)}" if expected_metadata
167
+ " (#{expectation_list.join(" and ")})" if expectation_list.any?
168
+ end
169
+
170
+ private
171
+
172
+ def matches_kind
173
+ KindMatcher.new(expected).matches?(actual)
174
+ end
175
+
176
+ def matches_data
177
+ DataMatcher.new(expected_data, strict: strict?).matches?(actual.data)
178
+ end
179
+
180
+ def matches_metadata
181
+ DataMatcher.new(expected_metadata, strict: strict?).matches?(actual.metadata.to_h)
182
+ end
183
+
184
+ attr_reader :expected_metadata, :expected_data, :actual, :expected, :differ, :formatter
185
+
186
+ def strict?
187
+ @strict
188
+ end
189
+ end
190
+ end
191
+ end
192
+
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyEventStore
4
+ module RSpec
5
+ class HaveApplied
6
+ def initialize(mandatory_expected, *optional_expected, differ:, phraser:)
7
+ @expected = [mandatory_expected, *optional_expected]
8
+ @matcher = ::RSpec::Matchers::BuiltIn::Include.new(*expected)
9
+ @differ = differ
10
+ @phraser = phraser
11
+ end
12
+
13
+ def matches?(aggregate_root)
14
+ @events = aggregate_root.unpublished_events.to_a
15
+ matcher.matches?(events) && matches_count?
16
+ end
17
+
18
+ def exactly(count)
19
+ @count = count
20
+ self
21
+ end
22
+
23
+ def times
24
+ self
25
+ end
26
+ alias :time :times
27
+
28
+ def once
29
+ exactly(1)
30
+ end
31
+
32
+ def strict
33
+ @matcher = ::RSpec::Matchers::BuiltIn::Match.new(expected)
34
+ self
35
+ end
36
+
37
+ def failure_message
38
+ "expected #{expected} to be applied, diff:" +
39
+ differ.diff_as_string(expected.to_s, events.to_s)
40
+ end
41
+
42
+ def failure_message_when_negated
43
+ "expected #{expected} not to be applied, diff:" +
44
+ differ.diff_as_string(expected.inspect, events.inspect)
45
+ end
46
+
47
+ def description
48
+ "have applied events that have to (#{phraser.(expected)})"
49
+ end
50
+
51
+ private
52
+
53
+ def matches_count?
54
+ return true unless count
55
+ raise NotSupported if expected.size > 1
56
+ events.select { |e| expected.first === e }.size.equal?(count)
57
+ end
58
+
59
+ attr_reader :differ, :phraser, :expected, :events, :count, :matcher
60
+ end
61
+ end
62
+ end
63
+
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyEventStore
4
+ module RSpec
5
+ class HavePublished
6
+ def initialize(mandatory_expected, *optional_expected, differ:, phraser:)
7
+ @expected = [mandatory_expected, *optional_expected]
8
+ @matcher = ::RSpec::Matchers::BuiltIn::Include.new(*expected)
9
+ @differ = differ
10
+ @phraser = phraser
11
+ end
12
+
13
+ def matches?(event_store)
14
+ @events = event_store.read
15
+ @events = events.stream(stream_name) if stream_name
16
+ @events = events.from(start) if start
17
+ @events = events.each
18
+ @matcher.matches?(events) && matches_count?
19
+ end
20
+
21
+ def exactly(count)
22
+ @count = count
23
+ self
24
+ end
25
+
26
+ def in_stream(stream_name)
27
+ @stream_name = stream_name
28
+ self
29
+ end
30
+
31
+ def times
32
+ self
33
+ end
34
+ alias :time :times
35
+
36
+ def from(event_id)
37
+ @start = event_id
38
+ self
39
+ end
40
+
41
+ def once
42
+ exactly(1)
43
+ end
44
+
45
+ def failure_message
46
+ "expected #{expected} to be published, diff:" +
47
+ differ.diff_as_string(expected.to_s, events.to_a.to_s)
48
+ end
49
+
50
+ def failure_message_when_negated
51
+ "expected #{expected} not to be published, diff:" +
52
+ differ.diff_as_string(expected.to_s, events.to_a.to_s)
53
+ end
54
+
55
+ def description
56
+ "have published events that have to (#{phraser.(expected)})"
57
+ end
58
+
59
+ def strict
60
+ @matcher = ::RSpec::Matchers::BuiltIn::Match.new(expected)
61
+ self
62
+ end
63
+
64
+ private
65
+
66
+ def matches_count?
67
+ return true unless count
68
+ raise NotSupported if expected.size > 1
69
+ events.select { |e| expected.first === e }.size.equal?(count)
70
+ end
71
+
72
+ attr_reader :differ, :phraser, :stream_name, :expected, :count, :events, :start
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyEventStore
4
+ module RSpec
5
+ class HaveSubscribedToEvents
6
+ def initialize(mandatory_expected, *optional_expected, differ:, phraser:)
7
+ @expected = [mandatory_expected, *optional_expected]
8
+ @matcher = ::RSpec::Matchers::BuiltIn::ContainExactly.new(expected)
9
+ @differ = differ
10
+ @phraser = phraser
11
+ end
12
+
13
+ def matches?(handler)
14
+ @handler = handler
15
+ @subscribed_to = expected.select do |event|
16
+ event_store.subscribers_for(event).include?(handler)
17
+ end
18
+
19
+ matcher.matches?(subscribed_to)
20
+ end
21
+
22
+ def in(event_store)
23
+ @event_store = event_store
24
+ self
25
+ end
26
+
27
+ def failure_message
28
+ "expected #{handler} to be subscribed to events, diff:" +
29
+ differ.diff_as_string(expected.to_s, subscribed_to.to_s)
30
+ end
31
+
32
+ def failure_message_when_negated
33
+ "expected #{handler} not to be subscribed to events, diff:" +
34
+ differ.diff_as_string(expected.to_s, subscribed_to.to_s)
35
+ end
36
+
37
+ def description
38
+ "have subscribed to events that have to (#{phraser.(expected)})"
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :expected, :handler, :subscribed_to,
44
+ :differ, :phraser, :matcher, :event_store
45
+ end
46
+ end
47
+ end
48
+
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyEventStore
4
+ module RSpec
5
+ module Matchers
6
+ class ListPhraser
7
+ class << self
8
+ def call(object)
9
+ items = Array(object).compact.map { |o| format(o) }
10
+ return "" if items.empty?
11
+ if items.one?
12
+ items.join
13
+ else
14
+ "#{items[all_but_last].join(", ")} and #{items.fetch(-1)}"
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def all_but_last
21
+ (0...-1)
22
+ end
23
+
24
+ def format(object)
25
+ if object.respond_to?(:description)
26
+ ::RSpec::Support::ObjectFormatter.format(object)
27
+ else
28
+ "be a #{object}"
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ def be_an_event(expected)
35
+ BeEvent.new(expected, differ: differ, formatter: formatter)
36
+ end
37
+ alias :be_event :be_an_event
38
+ alias :an_event :be_an_event
39
+ alias :event :be_an_event
40
+
41
+ def have_published(*expected)
42
+ HavePublished.new(*expected, differ: differ, phraser: phraser)
43
+ end
44
+
45
+ def have_applied(*expected)
46
+ HaveApplied.new(*expected, differ: differ, phraser: phraser)
47
+ end
48
+
49
+ def have_subscribed_to_events(*expected)
50
+ HaveSubscribedToEvents.new(*expected, differ: differ, phraser: phraser)
51
+ end
52
+
53
+ def publish(*expected)
54
+ Publish.new(*expected)
55
+ end
56
+
57
+ def apply(*expected)
58
+ Apply.new(*expected)
59
+ end
60
+
61
+ private
62
+
63
+ def formatter
64
+ ::RSpec::Support::ObjectFormatter.public_method(:format)
65
+ end
66
+
67
+ def differ
68
+ ::RSpec::Support::Differ.new(color: ::RSpec::Matchers.configuration.color?)
69
+ end
70
+
71
+ def phraser
72
+ ListPhraser
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyEventStore
4
+ module RSpec
5
+ class Publish
6
+ def in(event_store)
7
+ @event_store = event_store
8
+ self
9
+ end
10
+
11
+ def in_stream(stream)
12
+ @stream = stream
13
+ self
14
+ end
15
+
16
+ def matches?(event_proc)
17
+ raise_event_store_not_set unless @event_store
18
+ spec = @event_store.read
19
+ spec = spec.stream(@stream) if @stream
20
+ last_event_before_block = spec.last
21
+ event_proc.call
22
+ spec = spec.from(last_event_before_block.event_id) if last_event_before_block
23
+ @published_events = spec.to_a
24
+ if match_events?
25
+ ::RSpec::Matchers::BuiltIn::Include.new(*@expected).matches?(@published_events)
26
+ else
27
+ !@published_events.empty?
28
+ end
29
+ end
30
+
31
+ def failure_message
32
+ if match_events?
33
+ <<-EOS
34
+ expected block to have published:
35
+
36
+ #{@expected}
37
+
38
+ #{"in stream #{@stream} " if @stream}but published:
39
+
40
+ #{@published_events}
41
+ EOS
42
+ else
43
+ "expected block to have published any events"
44
+ end
45
+ end
46
+
47
+ def failure_message_when_negated
48
+ if match_events?
49
+ <<-EOS
50
+ expected block not to have published:
51
+
52
+ #{@expected}
53
+
54
+ #{"in stream #{@stream} " if @stream}but published:
55
+
56
+ #{@published_events}
57
+ EOS
58
+ else
59
+ "expected block not to have published any events"
60
+ end
61
+ end
62
+
63
+ def description
64
+ "publish events"
65
+ end
66
+
67
+ def supports_block_expectations?
68
+ true
69
+ end
70
+
71
+ private
72
+
73
+ def initialize(*expected)
74
+ @expected = expected
75
+ end
76
+
77
+ def match_events?
78
+ !@expected.empty?
79
+ end
80
+
81
+ def raise_event_store_not_set
82
+ raise SyntaxError, "You have to set the event store instance with `in`, e.g. `expect { ... }.to publish(an_event(MyEvent)).in(event_store)`"
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyEventStore
4
+ module RSpec
5
+ VERSION = "2.0.0"
6
+ end
7
+ end
@@ -0,0 +1,36 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'rails_event_store-rspec'
7
+ spec.version = '2.0.0'
8
+ spec.licenses = ['MIT']
9
+ spec.authors = ['Arkency']
10
+ spec.email = ['dev@arkency.com']
11
+
12
+ spec.summary = %q{RSpec matchers for RailsEventStore}
13
+ spec.homepage = 'https://railseventstore.org'
14
+ spec.metadata = {
15
+ "homepage_uri" => "https://railseventstore.org/",
16
+ "changelog_uri" => "https://github.com/RailsEventStore/rails_event_store/releases",
17
+ "source_code_uri" => "https://github.com/RailsEventStore/rails_event_store",
18
+ "bug_tracker_uri" => "https://github.com/RailsEventStore/rails_event_store/issues",
19
+ }
20
+
21
+ spec.files = ['lib/rails_event_store/rspec.rb']
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.add_runtime_dependency 'rspec', '~> 3.0'
25
+ spec.add_runtime_dependency 'ruby_event_store-rspec', '= 2.0.0'
26
+
27
+ spec.post_install_message = <<~EOW
28
+ The 'rails_event_store-rspec' gem has been renamed.
29
+
30
+ Please change your Gemfile or gemspec
31
+ to reflect its new name:
32
+
33
+ 'ruby_event_store-rspec'
34
+
35
+ EOW
36
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ruby_event_store/rspec/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'ruby_event_store-rspec'
8
+ spec.version = RubyEventStore::RSpec::VERSION
9
+ spec.licenses = ['MIT']
10
+ spec.authors = ['Arkency']
11
+ spec.email = ['dev@arkency.com']
12
+
13
+ spec.summary = %q{RSpec matchers for RailsEventStore}
14
+ spec.homepage = 'https://railseventstore.org'
15
+ spec.metadata = {
16
+ "homepage_uri" => "https://railseventstore.org/",
17
+ "changelog_uri" => "https://github.com/RailsEventStore/rails_event_store/releases",
18
+ "source_code_uri" => "https://github.com/RailsEventStore/rails_event_store",
19
+ "bug_tracker_uri" => "https://github.com/RailsEventStore/rails_event_store/issues",
20
+ }
21
+
22
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
23
+ spec.require_paths = ['lib']
24
+
25
+ spec.add_runtime_dependency 'rspec', '~> 3.0'
26
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby_event_store-rspec
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Arkency
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-12-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ description:
28
+ email:
29
+ - dev@arkency.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".mutant.yml"
35
+ - Gemfile
36
+ - Gemfile.lock
37
+ - Makefile
38
+ - README.md
39
+ - lib/rails_event_store/rspec.rb
40
+ - lib/ruby_event_store/rspec.rb
41
+ - lib/ruby_event_store/rspec/apply.rb
42
+ - lib/ruby_event_store/rspec/be_event.rb
43
+ - lib/ruby_event_store/rspec/have_applied.rb
44
+ - lib/ruby_event_store/rspec/have_published.rb
45
+ - lib/ruby_event_store/rspec/have_subscribed_to_events.rb
46
+ - lib/ruby_event_store/rspec/matchers.rb
47
+ - lib/ruby_event_store/rspec/publish.rb
48
+ - lib/ruby_event_store/rspec/version.rb
49
+ - rails_event_store-rspec.gemspec
50
+ - ruby_event_store-rspec.gemspec
51
+ homepage: https://railseventstore.org
52
+ licenses:
53
+ - MIT
54
+ metadata:
55
+ homepage_uri: https://railseventstore.org/
56
+ changelog_uri: https://github.com/RailsEventStore/rails_event_store/releases
57
+ source_code_uri: https://github.com/RailsEventStore/rails_event_store
58
+ bug_tracker_uri: https://github.com/RailsEventStore/rails_event_store/issues
59
+ post_install_message:
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubygems_version: 3.1.4
75
+ signing_key:
76
+ specification_version: 4
77
+ summary: RSpec matchers for RailsEventStore
78
+ test_files: []