email_spectacular 1.0.0

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.
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'email_spectacular/dsl'
4
+ require 'email_spectacular/failure_descriptions'
5
+ require 'email_spectacular/matchers'
6
+
7
+ module EmailSpectacular
8
+ class EmailFilter
9
+ include DSL
10
+ include Matchers
11
+ include FailureDescriptions
12
+ end
13
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'email_spectacular/email_filter'
4
+
5
+ module EmailSpectacular
6
+ # Backing class for {#have_been_sent} declarative syntax for specifying email
7
+ # expectations. Provides ability to assert emails have been sent in a given test
8
+ # that match particular attribute values, such as sender, receiver or contents.
9
+ #
10
+ # Implements the RSpec Matcher interface
11
+ #
12
+ # @author Aleck Greenham
13
+ #
14
+ # @see EmailSpectacular::RSpec#email
15
+ # @see EmailSpectacular::RSpec#have_been_sent
16
+ class Expectation < EmailFilter
17
+ # Creates a new EmailSpectacular::Expectation object
18
+ #
19
+ # @return [EmailSpectacular::Expectation] new expectation object
20
+ def initialize
21
+ @failure_message = 'Expected email to be sent'
22
+ super
23
+ end
24
+
25
+ # Declares that RSpec should not attempt to diff the actual and expected values
26
+ # to put in the failure message. This class takes care of diffing and presenting
27
+ # the differences, itself.
28
+ #
29
+ # @return [false] Always returns false
30
+ def diffable?
31
+ false
32
+ end
33
+
34
+ # Whether at least one email was sent during the current test that matches the
35
+ # constructed expectation
36
+ #
37
+ # @return [Boolean] True when a matching email was sent
38
+ def matches?(emails)
39
+ @emails = emails
40
+ matching_emails(emails, @scopes).any?
41
+ end
42
+
43
+ # Message to display to StdOut by RSpec if the equality check fails. Includes a
44
+ # complete a human-readable summary of the differences between what emails were
45
+ # expected to be sent, and what were actually sent (if any).
46
+ #
47
+ # This method is only used when the positive assertion is used, i.e.
48
+ # <tt>expect(email).to have_been_sent<tt>.
49
+ #
50
+ # For the failure message used for negative assertions, i.e.
51
+ # <tt>expect(email).to_not have_been_sent</tt>, see #failure_message_when_negated
52
+ #
53
+ # @see #failure_message_when_negated
54
+ #
55
+ # @return [String] message Full failure message with explanation of the differences
56
+ # between what emails were expected and what was actually sent
57
+ def failure_message
58
+ attribute, expected_value =
59
+ attribute_and_expected_value(@scopes, @emails)
60
+
61
+ describe_failed_assertion(
62
+ @emails,
63
+ attribute,
64
+ expected_value
65
+ )
66
+ end
67
+
68
+ # Failure message to display for negative RSpec assertions, i.e.
69
+ # <tt>expect(email).to_not have_been_sent</tt>.
70
+ #
71
+ # For the failure message displayed for positive assertions, see #failure_message.
72
+ #
73
+ # @see #failure_message
74
+ #
75
+ # @return [String] message Full failure message with explanation of the differences
76
+ # between what emails were expected and what was actually sent
77
+ def failure_message_when_negated
78
+ field_descriptions = attribute_descriptions(@scopes.keys)
79
+ value_descriptions = value_descriptions(@scopes.values)
80
+
81
+ expectation_description(
82
+ 'Expected no emails to be sent',
83
+ field_descriptions,
84
+ value_descriptions
85
+ )
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'email_spectacular/parser'
4
+ require 'email_spectacular/matchers'
5
+
6
+ module EmailSpectacular
7
+ # Module containing the helper methods to describe the difference between the expected
8
+ # and actual emails sent.
9
+ #
10
+ # @author Aleck Greenham
11
+ module FailureDescriptions # rubocop:disable Metrics/ModuleLength
12
+ include Parser
13
+
14
+ def self.included(base) # rubocop:disable Metrics/MethodLength
15
+ base.class_eval do
16
+ protected
17
+
18
+ def attribute_and_expected_value(scopes, emails)
19
+ scopes.each do |attribute, expected|
20
+ matching_emails =
21
+ emails.select do |email|
22
+ email_matches?(email, EmailSpectacular::Matchers::MATCHERS[attribute], expected)
23
+ end
24
+
25
+ return [attribute, expected] if matching_emails.empty?
26
+ end
27
+
28
+ [nil, nil]
29
+ end
30
+
31
+ def describe_failed_assertion(emails, attribute_name, attribute_value)
32
+ field_descriptions = attribute_descriptions([attribute_name])
33
+ value_descriptions = value_descriptions([attribute_value])
34
+
35
+ base_clause = expectation_description(
36
+ 'Expected an email to be sent',
37
+ field_descriptions,
38
+ value_descriptions
39
+ )
40
+
41
+ if emails.empty?
42
+ "#{base_clause} However, no emails were sent."
43
+ else
44
+ email_values = sent_email_values(emails, attribute_name)
45
+
46
+ if email_values.any?
47
+ base_clause + " However, #{email_pluralisation(emails)} sent " \
48
+ "#{result_description(field_descriptions, [to_sentence(email_values)])}."
49
+ else
50
+ base_clause
51
+ end
52
+ end
53
+ end
54
+
55
+ def attribute_descriptions(attributes)
56
+ attributes.map do |attr|
57
+ attr.to_s.tr('_', ' ')
58
+ end
59
+ end
60
+
61
+ def value_descriptions(values)
62
+ values.map do |value|
63
+ case value
64
+ when String
65
+ "'#{value}'"
66
+ when Array
67
+ to_sentence(value.map { |val| "'#{val}'" })
68
+ else
69
+ value
70
+ end
71
+ end
72
+ end
73
+
74
+ def expectation_description(base_clause, field_descriptions, value_descriptions)
75
+ description = base_clause
76
+
77
+ additional_clauses = []
78
+
79
+ field_descriptions.each.with_index do |field_description, index|
80
+ clause = ''
81
+ clause += " #{field_description}" unless field_description.empty?
82
+
83
+ if (value_description = value_descriptions[index])
84
+ clause += " #{value_description}"
85
+ end
86
+
87
+ additional_clauses.push(clause) unless clause.empty?
88
+ end
89
+
90
+ description + additional_clauses.join('') + '.'
91
+ end
92
+
93
+ private
94
+
95
+ def result_description(field_descriptions, values)
96
+ to_sentence(
97
+ field_descriptions.map.with_index do |field_description, index|
98
+ value = values[index]
99
+
100
+ if ['matching selector', 'with link', 'with image'].include?(field_description)
101
+ "with body #{value}"
102
+ else
103
+ "#{field_description} #{value}"
104
+ end
105
+ end
106
+ )
107
+ end
108
+
109
+ def sent_email_values(emails, attribute)
110
+ emails.each_with_object([]) do |email, memo|
111
+ if %i[matching_selector with_link with_image].include?(attribute)
112
+ memo << email_body(email)
113
+ else
114
+ matcher = EmailSpectacular::Matchers::MATCHERS[attribute]
115
+
116
+ value =
117
+ case matcher
118
+ when String, Symbol
119
+ email.send(matcher)
120
+ when Hash
121
+ matcher[:actual].call(email, parsed_emails(email))
122
+ else
123
+ raise ArgumentError, "Failure related to an unknown or unsupported email attribute #{attribute}"
124
+ end
125
+
126
+ value = value.is_a?(String) ? "'#{value}'" : value.map { |element| "'#{element}'" }
127
+ memo << value
128
+ end
129
+ end
130
+ end
131
+
132
+ def email_pluralisation(emails)
133
+ emails.length > 2 ? "#{emails.length} were" : '1 was'
134
+ end
135
+
136
+ def to_sentence(items)
137
+ case items.length
138
+ when 0, 1
139
+ items.join('')
140
+ when 2
141
+ items.join(' and ')
142
+ else
143
+ items[0..(items.length - 3)].join(', ') + items[(items.length - 3)..items.length - 1].join(' and ')
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'email_spectacular/parser'
4
+
5
+ module EmailSpectacular
6
+ # Module containing helper methods for matching expectations against emails
7
+ #
8
+ # @author Aleck Greenham
9
+ module Matchers
10
+ include Parser
11
+
12
+ MATCHERS = {
13
+ to: :to,
14
+ from: :from,
15
+ with_subject: :subject,
16
+ with_text: {
17
+ match: ->(_, email, value) { value.all? { |text| email.has_text?(text) } },
18
+ actual: ->(_, email) { email.text }
19
+ },
20
+ matching_selector: {
21
+ match: ->(_, email, value) { value.all? { |selector| email.has_selector?(selector) } },
22
+ actual: ->(_, email) { email.native },
23
+ actual_name: :with_body
24
+ },
25
+ with_link: {
26
+ match: ->(_, email, value) { value.all? { |url| email.has_selector?("a[href='#{url}']") } },
27
+ actual: ->(_, email) { email.native },
28
+ actual_name: :with_body
29
+ },
30
+ with_image: {
31
+ match: ->(_, email, value) { value.all? { |url| email.has_selector?("img[src='#{url}']") } },
32
+ actual: ->(_, email) { email.native },
33
+ actual_name: :with_body
34
+ }
35
+ }.freeze
36
+
37
+ def self.included(base) # rubocop:disable Metrics/MethodLength
38
+ base.class_eval do
39
+ def matching_emails(emails, scopes)
40
+ if scopes.any?
41
+ emails.select do |email|
42
+ scopes.all? do |attribute, expected|
43
+ email_matches?(email, MATCHERS[attribute], expected)
44
+ end
45
+ end
46
+ else
47
+ emails
48
+ end
49
+ end
50
+
51
+ def email_matches?(email, assertion, expected)
52
+ case assertion
53
+ when :to
54
+ !(expected & email.send(assertion)).empty?
55
+ when String, Symbol
56
+ email.send(assertion).include?(expected)
57
+ when Hash
58
+ assertion[:match].call(email, parsed_emails(email), expected)
59
+ else
60
+ raise "Unsupported assertion mapping '#{assertion}' of type #{assertion.class.name}"
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara'
4
+
5
+ module EmailSpectacular
6
+ # Module for parsing email bodies
7
+ #
8
+ # @author Aleck Greenham
9
+ module Parser
10
+ def parsed_emails(email)
11
+ parser(email)
12
+ end
13
+
14
+ def parser(email)
15
+ Capybara::Node::Simple.new(email_body(email))
16
+ end
17
+
18
+ def email_body(email)
19
+ if email.parts.first
20
+ email.parts.first.body.decoded
21
+ else
22
+ email.body.encoded
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'email_spectacular/expectation'
4
+
5
+ module EmailSpectacular
6
+ # Module containing email helper methods that can be mixed into the RSpec test scope
7
+ #
8
+ # @author Aleck Greenham
9
+ module RSpec
10
+ # Syntactic sugar for referencing the list of emails sent since the start of the
11
+ # test
12
+ #
13
+ # @example Asserting email has been sent
14
+ # expect(email).to have_been_sent.to('test@email.com')
15
+ #
16
+ # @return [Array<Mail::Message>] List of sent emails
17
+ def email
18
+ ActionMailer::Base.deliveries
19
+ end
20
+
21
+ # Clears the list of sent emails.
22
+ #
23
+ # @return void
24
+ def clear_emails
25
+ ActionMailer::Base.deliveries = []
26
+ end
27
+
28
+ # Creates a new email expectation that allows asserting emails should have specific
29
+ # attributes.
30
+ #
31
+ # @see EmailSpectacular::Expectation
32
+ #
33
+ # @example Asserting email has been sent
34
+ # expect(email).to have_been_sent.to('test@email.com')
35
+ def have_been_sent # rubocop:disable Naming/PredicateName
36
+ EmailSpectacular::Expectation.new
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EmailSpectacular
4
+ VERSION = '1.0.0'
5
+ end
@@ -0,0 +1,429 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'email_spectacular/rspec'
4
+
5
+ require_relative './support/email_mock'
6
+
7
+ RSpec.describe 'have_sent_email' do
8
+ include EmailSpectacular::RSpec
9
+
10
+ context 'when no emails have been sent' do
11
+ subject { [] }
12
+
13
+ it 'then the positive assertion fails' do
14
+ expect do
15
+ expect(subject).to have_been_sent
16
+ end.to raise_error.with_message(
17
+ 'Expected an email to be sent. However, no emails were sent.'
18
+ )
19
+ end
20
+
21
+ it 'then the negative assertion passes' do
22
+ expect do
23
+ expect(subject).to_not have_been_sent
24
+ end.to_not raise_error
25
+ end
26
+
27
+ it "then a non-matching 'to' assertion fails" do
28
+ expect do
29
+ expect(subject).to have_been_sent.to('test@email.com')
30
+ end.to raise_error.with_message(
31
+ 'Expected an email to be sent to \'test@email.com\'. However, no emails were sent.'
32
+ )
33
+ end
34
+
35
+ it "then a non-matching 'from' assertion fails" do
36
+ expect do
37
+ expect(subject).to have_been_sent.from('test@email.com')
38
+ end.to raise_error.with_message(
39
+ 'Expected an email to be sent from \'test@email.com\'. However, no emails were sent.'
40
+ )
41
+ end
42
+
43
+ it "then a non-matching 'with_subject' assertion fails" do
44
+ expect do
45
+ expect(subject).to have_been_sent.with_subject('Subject')
46
+ end.to raise_error.with_message(
47
+ 'Expected an email to be sent with subject \'Subject\'. However, no emails were sent.'
48
+ )
49
+ end
50
+
51
+ it "then a non-matching 'with_text' assertion fails" do
52
+ expect do
53
+ expect(subject).to have_been_sent.with_text('Text')
54
+ end.to raise_error.with_message(
55
+ 'Expected an email to be sent with text \'Text\'. However, no emails were sent.'
56
+ )
57
+ end
58
+
59
+ it "then a non-matching 'matching_selector' assertion fails" do
60
+ expect do
61
+ expect(subject).to have_been_sent.matching_selector('h1')
62
+ end.to raise_error.with_message(
63
+ 'Expected an email to be sent matching selector \'h1\'. However, no emails were sent.'
64
+ )
65
+ end
66
+
67
+ it "then a non-matching 'with_link' assertion fails" do
68
+ expect do
69
+ expect(subject).to have_been_sent.with_link('www.example.com')
70
+ end.to raise_error.with_message(
71
+ 'Expected an email to be sent with link \'www.example.com\'. However, no emails were sent.'
72
+ )
73
+ end
74
+
75
+ it "then a non-matching 'with_image' assertion fails" do
76
+ expect do
77
+ expect(subject).to have_been_sent.with_image('www.example.com')
78
+ end.to raise_error.with_message(
79
+ 'Expected an email to be sent with image \'www.example.com\'. However, no emails were sent.'
80
+ )
81
+ end
82
+ end
83
+
84
+ context 'when an email has been sent' do
85
+ subject { [EmailMock.new] }
86
+
87
+ it 'then the unqualified assertion passes' do
88
+ expect do
89
+ expect(subject).to have_been_sent
90
+ end.to_not raise_error
91
+ end
92
+
93
+ it 'then the unqualified negative assertion fails' do
94
+ expect do
95
+ expect(subject).to_not have_been_sent
96
+ end.to raise_error('Expected no emails to be sent.')
97
+ end
98
+ end
99
+
100
+ context 'when a matching email has been sent' do
101
+ subject { [EmailMock.new] }
102
+
103
+ it "then a positive 'to' assertion passes" do
104
+ expect do
105
+ expect(subject).to have_been_sent.to(subject[0].to[0])
106
+ end.to_not raise_error
107
+ end
108
+
109
+ it "then a negative 'to' assertion fails" do
110
+ expect do
111
+ expect(subject).to_not have_been_sent.to(subject[0].to[0])
112
+ end.to raise_error.with_message(
113
+ "Expected no emails to be sent to '#{subject[0].to[0]}'."
114
+ )
115
+ end
116
+
117
+ it "then a positive 'from' assertion passes" do
118
+ expect do
119
+ expect(subject).to have_been_sent.from(subject[0].from[0])
120
+ end.to_not raise_error
121
+ end
122
+
123
+ it "then a negative 'from' assertion fails" do
124
+ expect do
125
+ expect(subject).to_not have_been_sent.from(subject[0].from[0])
126
+ end.to raise_error.with_message(
127
+ "Expected no emails to be sent from '#{subject[0].from[0]}'."
128
+ )
129
+ end
130
+
131
+ it "then a positive 'with_subject' assertion passes" do
132
+ expect do
133
+ expect(subject).to have_been_sent.with_subject(subject[0].subject)
134
+ end.to_not raise_error
135
+ end
136
+
137
+ it "then a negative 'with_subject' assertion fails" do
138
+ expect do
139
+ expect(subject).to_not have_been_sent.with_subject(subject[0].subject)
140
+ end.to raise_error.with_message(
141
+ "Expected no emails to be sent with subject '#{subject[0].subject}'."
142
+ )
143
+ end
144
+
145
+ it "then a positive 'with_text' assertion passes" do
146
+ expect do
147
+ expect(subject).to have_been_sent.with_text(subject[0].text)
148
+ end.to_not raise_error
149
+ end
150
+
151
+ it "then a negative 'with_text' assertion fails" do
152
+ expect do
153
+ expect(subject).to_not have_been_sent.with_text(subject[0].text)
154
+ end.to raise_error.with_message(
155
+ "Expected no emails to be sent with text '#{subject[0].text}'."
156
+ )
157
+ end
158
+
159
+ it "then a positive 'matching_selector' assertion passes" do
160
+ expect do
161
+ expect(subject).to have_been_sent.matching_selector('h1')
162
+ end.to_not raise_error
163
+ end
164
+
165
+ it "then a negative 'matching_selector' assertion fails" do
166
+ expect do
167
+ expect(subject).to_not have_been_sent.matching_selector('h1')
168
+ end.to raise_error.with_message(
169
+ "Expected no emails to be sent matching selector 'h1'."
170
+ )
171
+ end
172
+
173
+ it "then a positive 'with_link' assertion passes" do
174
+ expect do
175
+ expect(subject).to have_been_sent.with_link('www.test.com')
176
+ end.to_not raise_error
177
+ end
178
+
179
+ it "then a negative 'with_link' assertion fails" do
180
+ expect do
181
+ expect(subject).to_not have_been_sent.with_link('www.test.com')
182
+ end.to raise_error.with_message(
183
+ "Expected no emails to be sent with link 'www.test.com'."
184
+ )
185
+ end
186
+
187
+ it "then a positive 'with_image' assertion passes" do
188
+ expect do
189
+ expect(subject).to have_been_sent.with_image('www.test.com')
190
+ end.to_not raise_error
191
+ end
192
+
193
+ it "then a negative 'with_image' assertion fails" do
194
+ expect do
195
+ expect(subject).to_not have_been_sent.with_image('www.test.com')
196
+ end.to raise_error.with_message(
197
+ "Expected no emails to be sent with image 'www.test.com'."
198
+ )
199
+ end
200
+ end
201
+
202
+ context 'when a non-matching email has been sent' do
203
+ subject { [EmailMock.new] }
204
+
205
+ it "then a positive 'to' assertion fails" do
206
+ expect do
207
+ expect(subject).to have_been_sent.to('other@email.com')
208
+ end.to raise_error.with_message(
209
+ "Expected an email to be sent to 'other@email.com'. However, 1 was sent to '#{subject[0].to[0]}'."
210
+ )
211
+ end
212
+
213
+ it "then a negative 'to' assertion passes" do
214
+ expect do
215
+ expect(subject).to_not have_been_sent.to('other@email.com')
216
+ end.to_not raise_error
217
+ end
218
+
219
+ it "then a positive 'from' assertion fails" do
220
+ expect do
221
+ expect(subject).to have_been_sent.from('other@email.com')
222
+ end.to raise_error.with_message(
223
+ "Expected an email to be sent from 'other@email.com'. However, 1 was sent from '#{subject[0].from[0]}'."
224
+ )
225
+ end
226
+
227
+ it "then a negative 'from' assertion passes" do
228
+ expect do
229
+ expect(subject).to_not have_been_sent.from('other@email.com')
230
+ end.to_not raise_error
231
+ end
232
+
233
+ it "then a positive 'with_subject' assertion fails" do
234
+ expect do
235
+ expect(subject).to have_been_sent.with_subject('Other Subject')
236
+ end.to raise_error.with_message(
237
+ "Expected an email to be sent with subject 'Other Subject'. However, 1 was " \
238
+ "sent with subject '#{subject[0].subject}'."
239
+ )
240
+ end
241
+
242
+ it "then a negative 'with_subject' assertion passes" do
243
+ expect do
244
+ expect(subject).to_not have_been_sent.with_subject('Other Subject')
245
+ end.to_not raise_error
246
+ end
247
+
248
+ it "then a positive 'with_text' assertion fails" do
249
+ expect do
250
+ expect(subject).to have_been_sent.with_text('Other text')
251
+ end.to raise_error.with_message(
252
+ "Expected an email to be sent with text 'Other text'. However, 1 was sent with text '#{subject[0].text}'."
253
+ )
254
+ end
255
+
256
+ it "then a negative 'with_text' assertion passes" do
257
+ expect do
258
+ expect(subject).to_not have_been_sent.with_text('Other text')
259
+ end.to_not raise_error
260
+ end
261
+
262
+ it "then a positive 'matching_selector' assertion fails" do
263
+ expect do
264
+ expect(subject).to have_been_sent.matching_selector('.other')
265
+ end.to raise_error.with_message(
266
+ "Expected an email to be sent matching selector '.other'. However, 1 was sent with body #{subject[0].body}."
267
+ )
268
+ end
269
+
270
+ it "then a negative 'matching_selector' assertion passes" do
271
+ expect do
272
+ expect(subject).to_not have_been_sent.matching_selector('.other')
273
+ end.to_not raise_error
274
+ end
275
+
276
+ it "then a positive 'with_link' assertion fails" do
277
+ expect do
278
+ expect(subject).to have_been_sent.with_link('www.other.com')
279
+ end.to raise_error.with_message(
280
+ "Expected an email to be sent with link 'www.other.com'. However, 1 was sent with body #{subject[0].body}."
281
+ )
282
+ end
283
+
284
+ it "then a negative 'with_link' assertion passes" do
285
+ expect do
286
+ expect(subject).to_not have_been_sent.with_link('www.other.com')
287
+ end.to_not raise_error
288
+ end
289
+
290
+ it "then a positive 'with_image' assertion fails" do
291
+ expect do
292
+ expect(subject).to have_been_sent.with_image('www.other.com')
293
+ end.to raise_error.with_message(
294
+ "Expected an email to be sent with image 'www.other.com'. However, 1 was sent with body #{subject[0].body}."
295
+ )
296
+ end
297
+
298
+ it "then a negative 'with_image' assertion passes" do
299
+ expect do
300
+ expect(subject).to_not have_been_sent.with_image('www.other.com')
301
+ end.to_not raise_error
302
+ end
303
+ end
304
+
305
+ context 'when multiple emails have been sent' do
306
+ subject { [EmailMock.new, EmailMock.new(to: 'other@email.com')] }
307
+
308
+ it 'then a positive assertion matching the first email passes' do
309
+ expect do
310
+ expect(subject).to have_been_sent.to(subject[0].to[0])
311
+ end.to_not raise_error
312
+ end
313
+
314
+ it 'then a negative assertion matching the first email fails' do
315
+ expect do
316
+ expect(subject).to_not have_been_sent.to(subject[0].to[0])
317
+ end.to raise_error.with_message(
318
+ "Expected no emails to be sent to '#{subject[0].to[0]}'."
319
+ )
320
+ end
321
+
322
+ it 'then a positive assertion matching the second email passes' do
323
+ expect do
324
+ expect(subject).to have_been_sent.to(subject[1].to)
325
+ end.to_not raise_error
326
+ end
327
+
328
+ it 'then a negative assertion matching the second email fails' do
329
+ expect do
330
+ expect(subject).to_not have_been_sent.to(subject[1].to)
331
+ end.to raise_error.with_message(
332
+ "Expected no emails to be sent to '#{subject[1].to[0]}'."
333
+ )
334
+ end
335
+ end
336
+
337
+ context 'when using multiple qualifiers' do
338
+ subject { [EmailMock.new] }
339
+
340
+ it 'then a positive assertions correctly matches a matching email' do
341
+ expect do
342
+ expect(subject).to have_been_sent.to(subject[0].to[0]).from(subject[0].from[0])
343
+ end.to_not raise_error
344
+ end
345
+
346
+ it "then a positive assertions don't match an email if the first qualifier isn't satisfied" do
347
+ expect do
348
+ expect(subject).to have_been_sent.to('other@email.com').from(subject[0].from[0])
349
+ end.to raise_error.with_message(
350
+ "Expected an email to be sent to 'other@email.com'. However, 1 was sent to '#{subject[0].to[0]}'."
351
+ )
352
+ end
353
+
354
+ it "then a positive assertions don't match an email if the last qualifier isn't satisfied" do
355
+ expect do
356
+ expect(subject).to have_been_sent.to(subject[0].to[0]).from('other@email.com')
357
+ end.to raise_error.with_message(
358
+ "Expected an email to be sent from 'other@email.com'. However, 1 was sent from '#{subject[0].from[0]}'."
359
+ )
360
+ end
361
+
362
+ it 'then a negative assertions correctly matches a matching email' do
363
+ expect do
364
+ expect(subject).to_not have_been_sent.to(subject[0].to[0]).from(subject[0].from[0])
365
+ end.to raise_error.with_message(
366
+ "Expected no emails to be sent to '#{subject[0].to[0]}' from '#{subject[0].from[0]}'."
367
+ )
368
+ end
369
+
370
+ it "then a negative assertions don't match an email if the first qualifier isn't satisfied" do
371
+ expect do
372
+ expect(subject).to_not have_been_sent.to('other@email.com').from(subject[0].from[0])
373
+ end.to_not raise_error
374
+ end
375
+
376
+ it "then a negative assertions don't match an email if the last qualifier isn't satisfied" do
377
+ expect do
378
+ expect(subject).to_not have_been_sent.to(subject[0].to[0]).from('other@email.com')
379
+ end.to_not raise_error
380
+ end
381
+ end
382
+
383
+ context 'when using the and method' do
384
+ subject { [EmailMock.new] }
385
+
386
+ it 'then a positive assertion will fail if the first qualifier is not satisfied' do
387
+ expect do
388
+ expect(subject).to have_been_sent.with_text('Other').and('Email')
389
+ end.to raise_error.with_message(
390
+ "Expected an email to be sent with text 'Other' and 'Email'. However, 1 was " \
391
+ "sent with text '#{subject[0].text}'."
392
+ )
393
+ end
394
+
395
+ it 'then a positive assertion will fail if the second qualifier is not satisfied' do
396
+ expect do
397
+ expect(subject).to have_been_sent.with_text('Test').and('Other')
398
+ end.to raise_error.with_message(
399
+ "Expected an email to be sent with text 'Test' and 'Other'. However, 1 was sent with text '#{subject[0].text}'."
400
+ )
401
+ end
402
+
403
+ it 'then a positive assertion will pass if both qualifiers are satisfied' do
404
+ expect do
405
+ expect(subject).to have_been_sent.with_text('Test').and('Email')
406
+ end.to_not raise_error
407
+ end
408
+
409
+ it 'then a negative assertion will pass if the first qualifier is not satisfied' do
410
+ expect do
411
+ expect(subject).to_not have_been_sent.with_text('Other').and('Email')
412
+ end.to_not raise_error
413
+ end
414
+
415
+ it 'then a negative assertion will pass if the second qualifier is not satisfied' do
416
+ expect do
417
+ expect(subject).to_not have_been_sent.with_text('Test').and('Other')
418
+ end.to_not raise_error
419
+ end
420
+
421
+ it 'then a negative assertion will fail if both qualifiers are satisfied' do
422
+ expect do
423
+ expect(subject).to_not have_been_sent.with_text('Test').and('Email')
424
+ end.to raise_error.with_message(
425
+ 'Expected no emails to be sent with text \'Test\' and \'Email\'.'
426
+ )
427
+ end
428
+ end
429
+ end