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