tape 0.0.1
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.
- data/MIT-LICENSE +19 -0
- data/README.md +43 -0
- data/lib/tape.rb +33 -0
- data/lib/tape/adapters.asfdsf +19 -0
- data/lib/tape/adapters/action_mailer/active_record.rb +45 -0
- data/lib/tape/adapters/action_mailer/cache.rb +28 -0
- data/lib/tape/adapters/action_mailer/maildir.rb +38 -0
- data/lib/tape/adapters/action_mailer/test.rb +28 -0
- data/lib/tape/adapters/base.rb +39 -0
- data/lib/tape/adapters/pony.rb +36 -0
- data/lib/tape/cucumber.rb +3 -0
- data/lib/tape/email_spec/address_converter.rb +31 -0
- data/lib/tape/email_spec/helpers.rb +175 -0
- data/lib/tape/email_spec/matchers.rb +259 -0
- data/lib/tape/email_viewer.rb +87 -0
- data/lib/tape/mail_ext.rb +15 -0
- metadata +148 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2008-2009 Ben Mabey
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
Tape
|
2
|
+
====
|
3
|
+
|
4
|
+
A rewrite of email\_spec that borrows a lot but gets rid of bloat. Currently ships with the following adapters:
|
5
|
+
|
6
|
+
* ActionMailer (:test, :active\_record, :maildir)
|
7
|
+
* Pony
|
8
|
+
|
9
|
+
Implementing your own adapter is easy. See below.
|
10
|
+
|
11
|
+
It does *NOT* handle Delayed::Job implicitly. Trigger your workers before checking for mail.
|
12
|
+
|
13
|
+
Compatibility
|
14
|
+
-------------
|
15
|
+
|
16
|
+
For convenience, `email_spec` helpers and matchers are available in the `Tape::EmailSpec` module. Use them if you want to migrate an existing project from `email_spec` to `tape`.
|
17
|
+
|
18
|
+
Example
|
19
|
+
-------
|
20
|
+
|
21
|
+
require 'tape'
|
22
|
+
|
23
|
+
# This will set up your adapter
|
24
|
+
Tape.configure 'action_mailer/test'
|
25
|
+
|
26
|
+
# Get all mails
|
27
|
+
Tape.adapter.all
|
28
|
+
|
29
|
+
# Get last mail
|
30
|
+
Tape.adapter.last
|
31
|
+
|
32
|
+
# Clear mails
|
33
|
+
Tape.adapter.reset
|
34
|
+
|
35
|
+
|
36
|
+
Implementing an adapter
|
37
|
+
-----------------------
|
38
|
+
|
39
|
+
Adapters inherit from Tape::Adapters::Base and implement only three self-explanatory methods:
|
40
|
+
|
41
|
+
* all
|
42
|
+
* last
|
43
|
+
* reset
|
data/lib/tape.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
module Tape
|
2
|
+
|
3
|
+
module EmailSpec
|
4
|
+
autoload :Helpers, 'tape/email_spec/helpers'
|
5
|
+
autoload :Matchers, 'tape/email_spec/matchers'
|
6
|
+
autoload :AddressConverter, 'tape/email_spec/address_converter'
|
7
|
+
end
|
8
|
+
|
9
|
+
module Adapters
|
10
|
+
autoload :Base, 'tape/adapters/base'
|
11
|
+
autoload :Pony, 'tape/adapters/pony'
|
12
|
+
|
13
|
+
module ActionMailer
|
14
|
+
autoload :Cache, 'tape/adapters/action_mailer/cache'
|
15
|
+
autoload :Maildir, 'tape/adapters/action_mailer/maildir'
|
16
|
+
autoload :Test, 'tape/adapters/action_mailer/test'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.configure(adapter, options = {})
|
21
|
+
klass = "Tape::Adapters::#{adapter.to_s.camelize}".constantize
|
22
|
+
@adapter = klass.new(options)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.adapter
|
26
|
+
@adapter || begin
|
27
|
+
raise "You need to call Tape.configure to set up your adapter"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
require 'tape/mail_ext'
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Tape
|
2
|
+
|
3
|
+
module Adapters
|
4
|
+
|
5
|
+
module ActionMailer
|
6
|
+
|
7
|
+
# For ActionMailer with delivery_method = :activerecord
|
8
|
+
class ActiveRecord
|
9
|
+
|
10
|
+
def initialize(options)
|
11
|
+
super options
|
12
|
+
|
13
|
+
# Default to Email if no model is configured.
|
14
|
+
unless self.options[:model]
|
15
|
+
self.options[:model] if const_defined?(Email)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def all
|
20
|
+
model.all.map { |email| parse email.mail }
|
21
|
+
end
|
22
|
+
|
23
|
+
def last
|
24
|
+
if email = model.last
|
25
|
+
parse mail
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def reset
|
30
|
+
model.delete_all
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def model
|
36
|
+
options[:model]
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Tape
|
2
|
+
|
3
|
+
module Adapteres
|
4
|
+
|
5
|
+
module ActionMailer
|
6
|
+
|
7
|
+
# For ActionMailer with delivery_method = :cache
|
8
|
+
class Cache < Base
|
9
|
+
|
10
|
+
def all
|
11
|
+
ActionMailer::Base.cached_deliveries
|
12
|
+
end
|
13
|
+
|
14
|
+
def last
|
15
|
+
ActionMailer::Base.cached_deliveries.last
|
16
|
+
end
|
17
|
+
|
18
|
+
def reset
|
19
|
+
ActionMailer::Base.clear_cache
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'maildir'
|
2
|
+
|
3
|
+
module Tape
|
4
|
+
|
5
|
+
module Adapters
|
6
|
+
|
7
|
+
module ActionMailer
|
8
|
+
|
9
|
+
# For ActionMailer with delivery_method = :maildir.
|
10
|
+
class Maildir < Base
|
11
|
+
|
12
|
+
def initialize(options = {})
|
13
|
+
super options
|
14
|
+
@maildir = ::Maildir.new(options[:path])
|
15
|
+
@maildir.serializer = ::Maildir::Serializer::Mail.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def all
|
19
|
+
@maildir.list(:new).collect(&:data)
|
20
|
+
end
|
21
|
+
|
22
|
+
def last
|
23
|
+
# FIXME: Sort by date
|
24
|
+
all.last
|
25
|
+
end
|
26
|
+
|
27
|
+
def reset
|
28
|
+
@maildir.list(:new).each do |message|
|
29
|
+
message.destroy
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Tape
|
2
|
+
|
3
|
+
module Adapters
|
4
|
+
|
5
|
+
module ActionMailer
|
6
|
+
|
7
|
+
# For ActionMailer with delivery_method = :test
|
8
|
+
class Test < Base
|
9
|
+
|
10
|
+
def all
|
11
|
+
ActionMailer::Base.deliveries
|
12
|
+
end
|
13
|
+
|
14
|
+
def last
|
15
|
+
ActionMailer::Base.deliveries.last
|
16
|
+
end
|
17
|
+
|
18
|
+
def reset
|
19
|
+
ActionMailer::Base.deliveries.clear
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Tape
|
2
|
+
|
3
|
+
module Adapters
|
4
|
+
|
5
|
+
class Base
|
6
|
+
|
7
|
+
attr_accessor :options
|
8
|
+
|
9
|
+
def initialize(options)
|
10
|
+
self.options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def all
|
14
|
+
raise "Not implemented"
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns the last sent mail or nil if there is none.
|
18
|
+
def last
|
19
|
+
raise "Not implemented"
|
20
|
+
end
|
21
|
+
|
22
|
+
def reset
|
23
|
+
raise "Not implemented"
|
24
|
+
end
|
25
|
+
|
26
|
+
def by_recipient(address)
|
27
|
+
all.select do |mail|
|
28
|
+
mail.destinations.include?(address)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def parse(mail)
|
33
|
+
Mail.read mail
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Tape
|
2
|
+
|
3
|
+
module Adapters
|
4
|
+
# Using Pony
|
5
|
+
class Pony < Base
|
6
|
+
|
7
|
+
def all
|
8
|
+
::Pony.deliveries
|
9
|
+
end
|
10
|
+
|
11
|
+
def last
|
12
|
+
::Pony.deliveries.last
|
13
|
+
end
|
14
|
+
|
15
|
+
def reset
|
16
|
+
::Pony.deliveries.clear
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
# Monkey-patch pony to not really send mails but store them.
|
26
|
+
if defined?(Pony)
|
27
|
+
module ::Pony
|
28
|
+
def self.deliveries
|
29
|
+
@deliveries ||= []
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.mail(options)
|
33
|
+
deliveries << build_mail(options)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module Tape
|
4
|
+
module EmailSpec
|
5
|
+
class AddressConverter
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
attr_accessor :converter
|
9
|
+
|
10
|
+
# The block provided to conversion should convert to an email
|
11
|
+
# address string or return the input untouched. For example:
|
12
|
+
#
|
13
|
+
# EmailSpec::AddressConverter.instance.conversion do |input|
|
14
|
+
# if input.is_a?(User)
|
15
|
+
# input.email
|
16
|
+
# else
|
17
|
+
# input
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
def conversion(&block)
|
22
|
+
self.converter = block
|
23
|
+
end
|
24
|
+
|
25
|
+
def convert(input)
|
26
|
+
return input unless converter
|
27
|
+
converter.call(input)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Tape
|
4
|
+
|
5
|
+
module EmailSpec
|
6
|
+
|
7
|
+
module Helpers
|
8
|
+
|
9
|
+
def all_emails
|
10
|
+
Tape.adapter.all
|
11
|
+
end
|
12
|
+
|
13
|
+
def last_email_sent
|
14
|
+
Tape.adapter.last
|
15
|
+
end
|
16
|
+
|
17
|
+
def visit_in_email(link_text)
|
18
|
+
visit(parse_email_for_link(current_email, link_text))
|
19
|
+
end
|
20
|
+
|
21
|
+
def click_email_link_matching(regex, email = current_email)
|
22
|
+
url = links_in_email(email).detect { |link| link =~ regex }
|
23
|
+
raise "No link found matching #{regex.inspect} in #{email.default_part_body}" unless url
|
24
|
+
visit request_uri(url)
|
25
|
+
end
|
26
|
+
|
27
|
+
def click_first_link_in_email(email = current_email)
|
28
|
+
link = links_in_email(email).first
|
29
|
+
visit request_uri(link)
|
30
|
+
end
|
31
|
+
|
32
|
+
def open_email(address, opts={})
|
33
|
+
set_current_email(find_email!(address, opts))
|
34
|
+
end
|
35
|
+
|
36
|
+
alias_method :open_email_for, :open_email
|
37
|
+
|
38
|
+
def open_last_email
|
39
|
+
set_current_email(last_email_sent)
|
40
|
+
end
|
41
|
+
|
42
|
+
def open_last_email_for(address)
|
43
|
+
set_current_email(mailbox_for(address).last)
|
44
|
+
end
|
45
|
+
|
46
|
+
def current_email(address=nil)
|
47
|
+
address = convert_address(address)
|
48
|
+
email = address ? email_spec_hash[:current_emails][address] : email_spec_hash[:current_email]
|
49
|
+
raise RSpec::Expectations::ExpectationNotMetError, "Expected an open email but none was found. Did you forget to call open_email?" unless email
|
50
|
+
email
|
51
|
+
end
|
52
|
+
|
53
|
+
def current_email_attachments(address=nil)
|
54
|
+
current_email(address).attachments || Array.new
|
55
|
+
end
|
56
|
+
|
57
|
+
def unread_emails_for(address)
|
58
|
+
mailbox_for(address) - read_emails_for(address)
|
59
|
+
end
|
60
|
+
|
61
|
+
def read_emails_for(address)
|
62
|
+
email_spec_hash[:read_emails][convert_address(address)] ||= []
|
63
|
+
end
|
64
|
+
|
65
|
+
def find_email(address, opts={})
|
66
|
+
address = convert_address(address)
|
67
|
+
if opts[:with_subject]
|
68
|
+
mailbox_for(address).find { |m| m.subject =~ Regexp.new(opts[:with_subject]) }
|
69
|
+
elsif opts[:with_text]
|
70
|
+
mailbox_for(address).find { |m| m.default_part_body =~ Regexp.new(opts[:with_text]) }
|
71
|
+
else
|
72
|
+
mailbox_for(address).first
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def links_in_email(email)
|
77
|
+
URI.extract(email.default_part_body.to_s, ['http', 'https'])
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def email_spec_hash
|
83
|
+
@email_spec_hash ||= {:read_emails => {}, :unread_emails => {}, :current_emails => {}, :current_email => nil}
|
84
|
+
end
|
85
|
+
|
86
|
+
def find_email!(address, opts={})
|
87
|
+
email = find_email(address, opts)
|
88
|
+
if email.nil?
|
89
|
+
error = "#{opts.keys.first.to_s.humanize unless opts.empty?} #{('"' + opts.values.first.to_s.humanize + '"') unless opts.empty?}"
|
90
|
+
raise RSpec::Expectations::ExpectationNotMetError, "Could not find email #{error}. \n Found the following emails:\n\n #{all_emails.to_s}"
|
91
|
+
end
|
92
|
+
email
|
93
|
+
end
|
94
|
+
|
95
|
+
def set_current_email(email)
|
96
|
+
return unless email
|
97
|
+
[email.to, email.cc, email.bcc].compact.flatten.each do |to|
|
98
|
+
read_emails_for(to) << email
|
99
|
+
email_spec_hash[:current_emails][to] = email
|
100
|
+
end
|
101
|
+
email_spec_hash[:current_email] = email
|
102
|
+
end
|
103
|
+
|
104
|
+
def parse_email_for_link(email, text_or_regex)
|
105
|
+
email.should have_body_text(text_or_regex)
|
106
|
+
|
107
|
+
url = parse_email_for_explicit_link(email, text_or_regex)
|
108
|
+
url ||= parse_email_for_anchor_text_link(email, text_or_regex)
|
109
|
+
|
110
|
+
raise "No link found matching #{text_or_regex.inspect} in #{email}" unless url
|
111
|
+
url
|
112
|
+
end
|
113
|
+
|
114
|
+
def request_uri(link)
|
115
|
+
return unless link
|
116
|
+
url = URI::parse(link)
|
117
|
+
url.fragment ? (url.request_uri + "#" + url.fragment) : url.request_uri
|
118
|
+
end
|
119
|
+
|
120
|
+
# e.g. confirm in http://confirm
|
121
|
+
def parse_email_for_explicit_link(email, regex)
|
122
|
+
regex = /#{Regexp.escape(regex)}/ unless regex.is_a?(Regexp)
|
123
|
+
url = links_in_email(email).detect { |link| link =~ regex }
|
124
|
+
request_uri(url)
|
125
|
+
end
|
126
|
+
|
127
|
+
# e.g. Click here in <a href="http://confirm">Click here</a>
|
128
|
+
def parse_email_for_anchor_text_link(email, link_text)
|
129
|
+
if textify_images(email.default_part_body) =~ %r{<a[^>]*href=['"]?([^'"]*)['"]?[^>]*?>[^<]*?#{link_text}[^<]*?</a>}
|
130
|
+
URI.split($1)[5..-1].compact!.join("?").gsub("&", "&")
|
131
|
+
# sub correct ampersand after rails switches it (http://dev.rubyonrails.org/ticket/4002)
|
132
|
+
else
|
133
|
+
return nil
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def textify_images(email_body)
|
138
|
+
email_body.to_s.gsub(%r{<img[^>]*alt=['"]?([^'"]*)['"]?[^>]*?/>}) { $1 }
|
139
|
+
end
|
140
|
+
|
141
|
+
def parse_email_count(amount)
|
142
|
+
case amount
|
143
|
+
when "no"
|
144
|
+
0
|
145
|
+
when "an"
|
146
|
+
1
|
147
|
+
else
|
148
|
+
amount.to_i
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
attr_reader :last_email_address
|
153
|
+
|
154
|
+
def convert_address(address)
|
155
|
+
@last_email_address = (address || current_email_address)
|
156
|
+
AddressConverter.instance.convert(@last_email_address)
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
def mailbox_for(address)
|
161
|
+
super(convert_address(address)) # super resides in Deliveries
|
162
|
+
end
|
163
|
+
|
164
|
+
def email_spec_deprecate(text)
|
165
|
+
puts ""
|
166
|
+
puts "DEPRECATION: #{text.split.join(' ')}"
|
167
|
+
puts ""
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
|
@@ -0,0 +1,259 @@
|
|
1
|
+
module Tape
|
2
|
+
module EmailSpec
|
3
|
+
module Matchers
|
4
|
+
class ReplyTo
|
5
|
+
def initialize(email)
|
6
|
+
@expected_reply_to = Mail::ReplyToField.new(email).addrs.first
|
7
|
+
end
|
8
|
+
|
9
|
+
def description
|
10
|
+
"have reply to as #{@expected_reply_to.address}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def matches?(email)
|
14
|
+
@email = email
|
15
|
+
@actual_reply_to = (email.reply_to || []).first
|
16
|
+
!@actual_reply_to.nil? &&
|
17
|
+
@actual_reply_to == @expected_reply_to.address
|
18
|
+
end
|
19
|
+
|
20
|
+
def failure_message
|
21
|
+
"expected #{@email.inspect} to reply to #{@expected_reply_to.address.inspect}, but it replied to #{@actual_reply_to.inspect}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def negative_failure_message
|
25
|
+
"expected #{@email.inspect} not to deliver to #{@expected_reply_to.address.inspect}, but it did"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def reply_to(email)
|
30
|
+
ReplyTo.new(email)
|
31
|
+
end
|
32
|
+
|
33
|
+
alias :have_reply_to :reply_to
|
34
|
+
|
35
|
+
class DeliverTo
|
36
|
+
def initialize(expected_email_addresses_or_objects_that_respond_to_email)
|
37
|
+
emails = expected_email_addresses_or_objects_that_respond_to_email.map do |email_or_object|
|
38
|
+
email_or_object.kind_of?(String) ? email_or_object : email_or_object.email
|
39
|
+
end
|
40
|
+
|
41
|
+
@expected_recipients = Mail::ToField.new(emails).addrs.map(&:to_s).sort
|
42
|
+
end
|
43
|
+
|
44
|
+
def description
|
45
|
+
"be delivered to #{@expected_recipients.inspect}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def matches?(email)
|
49
|
+
@email = email
|
50
|
+
@actual_recipients = (email.header[:to].addrs || []).map(&:to_s).sort
|
51
|
+
@actual_recipients == @expected_recipients
|
52
|
+
end
|
53
|
+
|
54
|
+
def failure_message
|
55
|
+
"expected #{@email.inspect} to deliver to #{@expected_recipients.inspect}, but it delivered to #{@actual_recipients.inspect}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def negative_failure_message
|
59
|
+
"expected #{@email.inspect} not to deliver to #{@expected_recipients.inspect}, but it did"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def deliver_to(*expected_email_addresses_or_objects_that_respond_to_email)
|
64
|
+
DeliverTo.new(expected_email_addresses_or_objects_that_respond_to_email.flatten)
|
65
|
+
end
|
66
|
+
|
67
|
+
alias :be_delivered_to :deliver_to
|
68
|
+
|
69
|
+
class DeliverFrom
|
70
|
+
|
71
|
+
def initialize(email)
|
72
|
+
@expected_sender = Mail::FromField.new(email).addrs.first
|
73
|
+
end
|
74
|
+
|
75
|
+
def description
|
76
|
+
"be delivered from #{@expected_sender}"
|
77
|
+
end
|
78
|
+
|
79
|
+
def matches?(email)
|
80
|
+
@email = email
|
81
|
+
@actual_sender = (email.header[:from].addrs || []).first
|
82
|
+
|
83
|
+
!@actual_sender.nil? &&
|
84
|
+
@actual_sender.to_s == @expected_sender.to_s
|
85
|
+
end
|
86
|
+
|
87
|
+
def failure_message
|
88
|
+
%(expected #{@email.inspect} to deliver from "#{@expected_sender.to_s}", but it delivered from "#{@actual_sender.to_s}")
|
89
|
+
end
|
90
|
+
|
91
|
+
def negative_failure_message
|
92
|
+
%(expected #{@email.inspect} not to deliver from "#{@expected_sender.to_s}", but it did)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def deliver_from(email)
|
97
|
+
DeliverFrom.new(email)
|
98
|
+
end
|
99
|
+
|
100
|
+
alias :be_delivered_from :deliver_from
|
101
|
+
|
102
|
+
class BccTo
|
103
|
+
|
104
|
+
def initialize(expected_email_addresses_or_objects_that_respond_to_email)
|
105
|
+
emails = expected_email_addresses_or_objects_that_respond_to_email.map do |email_or_object|
|
106
|
+
email_or_object.kind_of?(String) ? email_or_object : email_or_object.email
|
107
|
+
end
|
108
|
+
|
109
|
+
@expected_email_addresses = emails.sort
|
110
|
+
end
|
111
|
+
|
112
|
+
def description
|
113
|
+
"be bcc'd to #{@expected_email_addresses.inspect}"
|
114
|
+
end
|
115
|
+
|
116
|
+
def matches?(email)
|
117
|
+
@email = email
|
118
|
+
@actual_recipients = (Array(email.bcc) || []).sort
|
119
|
+
@actual_recipients == @expected_email_addresses
|
120
|
+
end
|
121
|
+
|
122
|
+
def failure_message
|
123
|
+
"expected #{@email.inspect} to bcc to #{@expected_email_addresses.inspect}, but it was bcc'd to #{@actual_recipients.inspect}"
|
124
|
+
end
|
125
|
+
|
126
|
+
def negative_failure_message
|
127
|
+
"expected #{@email.inspect} not to bcc to #{@expected_email_addresses.inspect}, but it did"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def bcc_to(*expected_email_addresses_or_objects_that_respond_to_email)
|
132
|
+
BccTo.new(expected_email_addresses_or_objects_that_respond_to_email.flatten)
|
133
|
+
end
|
134
|
+
|
135
|
+
class CcTo
|
136
|
+
|
137
|
+
def initialize(expected_email_addresses_or_objects_that_respond_to_email)
|
138
|
+
emails = expected_email_addresses_or_objects_that_respond_to_email.map do |email_or_object|
|
139
|
+
email_or_object.kind_of?(String) ? email_or_object : email_or_object.email
|
140
|
+
end
|
141
|
+
|
142
|
+
@expected_email_addresses = emails.sort
|
143
|
+
end
|
144
|
+
|
145
|
+
def description
|
146
|
+
"be cc'd to #{@expected_email_addresses.inspect}"
|
147
|
+
end
|
148
|
+
|
149
|
+
def matches?(email)
|
150
|
+
@email = email
|
151
|
+
@actual_recipients = (Array(email.cc) || []).sort
|
152
|
+
@actual_recipients == @expected_email_addresses
|
153
|
+
end
|
154
|
+
|
155
|
+
def failure_message
|
156
|
+
"expected #{@email.inspect} to cc to #{@expected_email_addresses.inspect}, but it was cc'd to #{@actual_recipients.inspect}"
|
157
|
+
end
|
158
|
+
|
159
|
+
def negative_failure_message
|
160
|
+
"expected #{@email.inspect} not to cc to #{@expected_email_addresses.inspect}, but it did"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def cc_to(*expected_email_addresses_or_objects_that_respond_to_email)
|
165
|
+
CcTo.new(expected_email_addresses_or_objects_that_respond_to_email.flatten)
|
166
|
+
end
|
167
|
+
|
168
|
+
RSpec::Matchers.define :have_subject do
|
169
|
+
match do |given|
|
170
|
+
given_subject = given.subject
|
171
|
+
expected_subject = expected.first
|
172
|
+
|
173
|
+
if expected_subject.is_a?(String)
|
174
|
+
description { "have subject of #{expected_subject.inspect}" }
|
175
|
+
failure_message_for_should { "expected the subject to be #{expected_subject.inspect} but was #{given_subject.inspect}" }
|
176
|
+
failure_message_for_should_not { "expected the subject not to be #{expected_subject.inspect} but was" }
|
177
|
+
|
178
|
+
given_subject == expected_subject
|
179
|
+
else
|
180
|
+
description { "have subject matching #{expected_subject.inspect}" }
|
181
|
+
failure_message_for_should { "expected the subject to match #{expected_subject.inspect}, but did not. Actual subject was: #{given_subject.inspect}" }
|
182
|
+
failure_message_for_should_not { "expected the subject not to match #{expected_subject.inspect} but #{given_subject.inspect} does match it." }
|
183
|
+
|
184
|
+
!!(given_subject =~ expected_subject)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
RSpec::Matchers.define :include_email_with_subject do
|
190
|
+
match do |given_emails|
|
191
|
+
expected_subject = expected.first
|
192
|
+
|
193
|
+
if expected_subject.is_a?(String)
|
194
|
+
description { "include email with subject of #{expected_subject.inspect}" }
|
195
|
+
failure_message_for_should { "expected at least one email to have the subject #{expected_subject.inspect} but none did. Subjects were #{given_emails.map(&:subject).inspect}" }
|
196
|
+
failure_message_for_should_not { "expected no email with the subject #{expected_subject.inspect} but found at least one. Subjects were #{given_emails.map(&:subject).inspect}" }
|
197
|
+
|
198
|
+
given_emails.map(&:subject).include?(expected_subject)
|
199
|
+
else
|
200
|
+
description { "include email with subject matching #{expected_subject.inspect}" }
|
201
|
+
failure_message_for_should { "expected at least one email to have a subject matching #{expected_subject.inspect}, but none did. Subjects were #{given_emails.map(&:subject).inspect}" }
|
202
|
+
failure_message_for_should_not { "expected no email to have a subject matching #{expected_subject.inspect} but found at least one. Subjects were #{given_emails.map(&:subject).inspect}" }
|
203
|
+
|
204
|
+
!!(given_emails.any?{ |mail| mail.subject =~ expected_subject })
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
RSpec::Matchers.define :have_body_text do
|
210
|
+
match do |given|
|
211
|
+
expected_text = expected.first
|
212
|
+
|
213
|
+
if expected_text.is_a?(String)
|
214
|
+
normalized_body = given.default_part_body.to_s.gsub(/\s+/, " ")
|
215
|
+
normalized_expected = expected_text.gsub(/\s+/, " ")
|
216
|
+
description { "have body including #{normalized_expected.inspect}" }
|
217
|
+
failure_message_for_should { "expected the body to contain #{normalized_expected.inspect} but was #{normalized_body.inspect}" }
|
218
|
+
failure_message_for_should_not { "expected the body not to contain #{normalized_expected.inspect} but was #{normalized_body.inspect}" }
|
219
|
+
|
220
|
+
normalized_body.include?(normalized_expected)
|
221
|
+
else
|
222
|
+
given_body = given.default_part_body.to_s
|
223
|
+
description { "have body matching #{expected_text.inspect}" }
|
224
|
+
failure_message_for_should { "expected the body to match #{expected_text.inspect}, but did not. Actual body was: #{given_body.inspect}" }
|
225
|
+
failure_message_for_should_not { "expected the body not to match #{expected_text.inspect} but #{given_body.inspect} does match it." }
|
226
|
+
|
227
|
+
!!(given_body =~ expected_text)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def mail_headers_hash(email_headers)
|
233
|
+
email_headers.fields.inject({}) { |hash, field| hash[field.field.class::FIELD_NAME] = field.to_s; hash }
|
234
|
+
end
|
235
|
+
|
236
|
+
RSpec::Matchers.define :have_header do
|
237
|
+
match do |given|
|
238
|
+
given_header = given.header
|
239
|
+
expected_name, expected_value = *expected
|
240
|
+
|
241
|
+
if expected_value.is_a?(String)
|
242
|
+
description { "have header #{expected_name}: #{expected_value}" }
|
243
|
+
|
244
|
+
failure_message_for_should { "expected the headers to include '#{expected_name}: #{expected_value}' but they were #{mail_headers_hash(given_header).inspect}" }
|
245
|
+
failure_message_for_should_not { "expected the headers not to include '#{expected_name}: #{expected_value}' but they were #{mail_headers_hash(given_header).inspect}" }
|
246
|
+
|
247
|
+
given_header[expected_name].to_s == expected_value
|
248
|
+
else
|
249
|
+
description { "have header #{expected_name} with value matching #{expected_value.inspect}" }
|
250
|
+
failure_message_for_should { "expected the headers to include '#{expected_name}' with a value matching #{expected_value.inspect} but they were #{mail_headers_hash(given_header).inspect}" }
|
251
|
+
failure_message_for_should_not { "expected the headers not to include '#{expected_name}' with a value matching #{expected_value.inspect} but they were #{mail_headers_hash(given_header).inspect}" }
|
252
|
+
|
253
|
+
given_header[expected_name].to_s =~ expected_value
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module EmailSpec
|
2
|
+
class EmailViewer
|
3
|
+
extend Deliveries
|
4
|
+
|
5
|
+
def self.save_and_open_all_raw_emails
|
6
|
+
filename = tmp_email_filename
|
7
|
+
|
8
|
+
File.open(filename, "w") do |f|
|
9
|
+
all_emails.each do |m|
|
10
|
+
f.write m.to_s
|
11
|
+
f.write "\n" + '='*80 + "\n"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
open_in_text_editor(filename)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.save_and_open_all_html_emails
|
19
|
+
all_emails.each_with_index do |m, index|
|
20
|
+
if m.multipart? && html_part = m.parts.detect{ |p| p.content_type == 'text/html' }
|
21
|
+
filename = tmp_email_filename("-#{index}.html")
|
22
|
+
File.open(filename, "w") do |f|
|
23
|
+
f.write m.parts[1].body
|
24
|
+
end
|
25
|
+
open_in_browser(filename)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.save_and_open_all_text_emails
|
31
|
+
filename = tmp_email_filename
|
32
|
+
|
33
|
+
File.open(filename, "w") do |f|
|
34
|
+
all_emails.each do |m|
|
35
|
+
if m.multipart? && text_part = m.parts.detect{ |p| p.content_type == 'text/plain' }
|
36
|
+
m.ordered_each{|k,v| f.write "#{k}: #{v}\n" }
|
37
|
+
f.write text_part.body
|
38
|
+
else
|
39
|
+
f.write m.to_s
|
40
|
+
end
|
41
|
+
f.write "\n" + '='*80 + "\n"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
open_in_text_editor(filename)
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.save_and_open_email(mail)
|
49
|
+
filename = tmp_email_filename
|
50
|
+
|
51
|
+
File.open(filename, "w") do |f|
|
52
|
+
f.write mail.to_s
|
53
|
+
end
|
54
|
+
|
55
|
+
open_in_text_editor(filename)
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.save_and_open_email_attachments_list(mail)
|
59
|
+
filename = tmp_email_filename
|
60
|
+
|
61
|
+
File.open(filename, "w") do |f|
|
62
|
+
mail.attachments.each_with_index do |attachment, index|
|
63
|
+
info = "#{index + 1}:"
|
64
|
+
info += "\n\tfilename: #{attachment.original_filename}"
|
65
|
+
info += "\n\tcontent type: #{attachment.content_type}"
|
66
|
+
info += "\n\tsize: #{attachment.size}"
|
67
|
+
f.write info + "\n"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
open_in_text_editor(filename)
|
72
|
+
end
|
73
|
+
|
74
|
+
# TODO: use the launchy gem for this stuff...
|
75
|
+
def self.open_in_text_editor(filename)
|
76
|
+
`open #{filename}`
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.open_in_browser(filename)
|
80
|
+
`open #{filename}`
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.tmp_email_filename(extension = '.txt')
|
84
|
+
"#{Rails.root}/tmp/email-#{Time.now.to_i}#{extension}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
metadata
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tape
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Alexander Flatter
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-05-10 00:00:00 +02:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: activesupport
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ">="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: "3.0"
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: mail
|
29
|
+
prerelease: false
|
30
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: "0"
|
36
|
+
type: :runtime
|
37
|
+
version_requirements: *id002
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: rake
|
40
|
+
prerelease: false
|
41
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: "0"
|
47
|
+
type: :development
|
48
|
+
version_requirements: *id003
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: actionmailer
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: "0"
|
58
|
+
type: :development
|
59
|
+
version_requirements: *id004
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: actionmailer-maildir
|
62
|
+
prerelease: false
|
63
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: "0"
|
69
|
+
type: :development
|
70
|
+
version_requirements: *id005
|
71
|
+
- !ruby/object:Gem::Dependency
|
72
|
+
name: rspec
|
73
|
+
prerelease: false
|
74
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: 2.0.0
|
80
|
+
type: :development
|
81
|
+
version_requirements: *id006
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: fakefs
|
84
|
+
prerelease: false
|
85
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: "0"
|
91
|
+
type: :development
|
92
|
+
version_requirements: *id007
|
93
|
+
description: ""
|
94
|
+
email:
|
95
|
+
- aflatter@farbenmeer.net
|
96
|
+
executables: []
|
97
|
+
|
98
|
+
extensions: []
|
99
|
+
|
100
|
+
extra_rdoc_files: []
|
101
|
+
|
102
|
+
files:
|
103
|
+
- lib/tape/adapters/base.rb
|
104
|
+
- lib/tape/adapters/action_mailer/test.rb
|
105
|
+
- lib/tape/adapters/action_mailer/active_record.rb
|
106
|
+
- lib/tape/adapters/action_mailer/cache.rb
|
107
|
+
- lib/tape/adapters/action_mailer/maildir.rb
|
108
|
+
- lib/tape/adapters/pony.rb
|
109
|
+
- lib/tape/email_viewer.rb
|
110
|
+
- lib/tape/cucumber.rb
|
111
|
+
- lib/tape/adapters.asfdsf
|
112
|
+
- lib/tape/mail_ext.rb
|
113
|
+
- lib/tape/email_spec/helpers.rb
|
114
|
+
- lib/tape/email_spec/address_converter.rb
|
115
|
+
- lib/tape/email_spec/matchers.rb
|
116
|
+
- lib/tape.rb
|
117
|
+
- MIT-LICENSE
|
118
|
+
- README.md
|
119
|
+
has_rdoc: true
|
120
|
+
homepage: http://github.com/aflatter/tape
|
121
|
+
licenses: []
|
122
|
+
|
123
|
+
post_install_message:
|
124
|
+
rdoc_options: []
|
125
|
+
|
126
|
+
require_paths:
|
127
|
+
- lib
|
128
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: "0"
|
134
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
135
|
+
none: false
|
136
|
+
requirements:
|
137
|
+
- - ">="
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: 1.3.6
|
140
|
+
requirements: []
|
141
|
+
|
142
|
+
rubyforge_project:
|
143
|
+
rubygems_version: 1.5.0
|
144
|
+
signing_key:
|
145
|
+
specification_version: 3
|
146
|
+
summary: ""
|
147
|
+
test_files: []
|
148
|
+
|