email_spectacular 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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