gardelea-email_spec 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +257 -0
- data/MIT-LICENSE.txt +19 -0
- data/README.md +277 -0
- data/Rakefile +17 -0
- data/lib/email-spec.rb +1 -0
- data/lib/email_spec.rb +18 -0
- data/lib/email_spec/address_converter.rb +29 -0
- data/lib/email_spec/background_processes.rb +45 -0
- data/lib/email_spec/cucumber.rb +26 -0
- data/lib/email_spec/deliveries.rb +91 -0
- data/lib/email_spec/email_viewer.rb +91 -0
- data/lib/email_spec/errors.rb +7 -0
- data/lib/email_spec/helpers.rb +175 -0
- data/lib/email_spec/mail_ext.rb +11 -0
- data/lib/email_spec/matchers.rb +257 -0
- data/lib/email_spec/test_observer.rb +7 -0
- data/lib/generators/email_spec/steps/USAGE +5 -0
- data/lib/generators/email_spec/steps/steps_generator.rb +14 -0
- data/lib/generators/email_spec/steps/templates/email_steps.rb +206 -0
- data/rails_generators/email_spec/email_spec_generator.rb +17 -0
- data/rails_generators/email_spec/templates/email_steps.rb +195 -0
- metadata +109 -0
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
Bundler::GemHelper.install_tasks
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'cucumber/rake/task'
|
7
|
+
Cucumber::Rake::Task.new(:features)
|
8
|
+
rescue LoadError
|
9
|
+
task :features do
|
10
|
+
abort "Cucumber is not available. In order to run features, you must: sudo gem install cucumber"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'rspec/core/rake_task'
|
15
|
+
RSpec::Core::RakeTask.new
|
16
|
+
|
17
|
+
task :default => [:features, :spec]
|
data/lib/email-spec.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'email_spec'))
|
data/lib/email_spec.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
unless defined?(Pony) or defined?(ActionMailer)
|
2
|
+
Kernel.warn("Neither Pony nor ActionMailer appear to be loaded so email-spec is requiring ActionMailer.")
|
3
|
+
require 'action_mailer'
|
4
|
+
end
|
5
|
+
|
6
|
+
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__))) unless $LOAD_PATH.include?(File.expand_path(File.dirname(__FILE__)))
|
7
|
+
|
8
|
+
require 'rspec'
|
9
|
+
require 'mail'
|
10
|
+
require 'email_spec/background_processes'
|
11
|
+
require 'email_spec/deliveries'
|
12
|
+
require 'email_spec/address_converter'
|
13
|
+
require 'email_spec/email_viewer'
|
14
|
+
require 'email_spec/helpers'
|
15
|
+
require 'email_spec/matchers'
|
16
|
+
require 'email_spec/mail_ext'
|
17
|
+
require 'email_spec/test_observer'
|
18
|
+
require 'email_spec/errors'
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module EmailSpec
|
4
|
+
class AddressConverter
|
5
|
+
include Singleton
|
6
|
+
|
7
|
+
attr_accessor :converter
|
8
|
+
|
9
|
+
# The block provided to conversion should convert to an email
|
10
|
+
# address string or return the input untouched. For example:
|
11
|
+
#
|
12
|
+
# EmailSpec::AddressConverter.instance.conversion do |input|
|
13
|
+
# if input.is_a?(User)
|
14
|
+
# input.email
|
15
|
+
# else
|
16
|
+
# input
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
def conversion(&block)
|
21
|
+
self.converter = block
|
22
|
+
end
|
23
|
+
|
24
|
+
def convert(input)
|
25
|
+
return input unless converter
|
26
|
+
converter.call(input)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module EmailSpec
|
2
|
+
module BackgroundProcesses
|
3
|
+
module DelayedJob
|
4
|
+
def all_emails
|
5
|
+
work_off_queue
|
6
|
+
super
|
7
|
+
end
|
8
|
+
|
9
|
+
def last_email_sent
|
10
|
+
work_off_queue
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
def reset_mailer
|
15
|
+
work_off_queue
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
def mailbox_for(address)
|
20
|
+
work_off_queue
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Later versions of DelayedJob switch from using Delayed::Job to Delayed::Worker
|
27
|
+
# Support both versions for those who haven't upgraded yet
|
28
|
+
def work_off_queue
|
29
|
+
if Delayed::Worker.instance_methods.detect{|iv| iv.to_s == "work_off" }
|
30
|
+
Delayed::Worker.send :public, :work_off
|
31
|
+
worker = Delayed::Worker.new(:max_priority => nil, :min_priority => nil, :quiet => true)
|
32
|
+
worker.work_off
|
33
|
+
else
|
34
|
+
Delayed::Job.work_off
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
module Compatibility
|
40
|
+
if defined?(Delayed) && (defined?(Delayed::Job) || defined?(Delayed::Worker))
|
41
|
+
include EmailSpec::BackgroundProcesses::DelayedJob
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# require this in your env.rb file after you require cucumber/rails/world
|
2
|
+
|
3
|
+
# Global Setup
|
4
|
+
if defined?(ActionMailer)
|
5
|
+
unless [:test, :activerecord, :cache, :file].include?(ActionMailer::Base.delivery_method)
|
6
|
+
ActionMailer::Base.register_observer(EmailSpec::TestObserver)
|
7
|
+
end
|
8
|
+
ActionMailer::Base.perform_deliveries = true
|
9
|
+
|
10
|
+
Before do
|
11
|
+
# Scenario setup
|
12
|
+
case ActionMailer::Base.delivery_method
|
13
|
+
when :test then ActionMailer::Base.deliveries.clear
|
14
|
+
when :cache then ActionMailer::Base.clear_cache
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
After do
|
20
|
+
EmailSpec::EmailViewer.save_and_open_all_raw_emails if ENV['SHOW_EMAILS']
|
21
|
+
EmailSpec::EmailViewer.save_and_open_all_html_emails if ENV['SHOW_HTML_EMAILS']
|
22
|
+
EmailSpec::EmailViewer.save_and_open_all_text_emails if ENV['SHOW_TEXT_EMAILS']
|
23
|
+
end
|
24
|
+
|
25
|
+
World(EmailSpec::Helpers)
|
26
|
+
World(EmailSpec::Matchers)
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module EmailSpec
|
2
|
+
module MailerDeliveries
|
3
|
+
def all_emails
|
4
|
+
deliveries
|
5
|
+
end
|
6
|
+
|
7
|
+
def last_email_sent
|
8
|
+
deliveries.last || raise("No email has been sent!")
|
9
|
+
end
|
10
|
+
|
11
|
+
def reset_mailer
|
12
|
+
if defined?(ActionMailer) && ActionMailer::Base.delivery_method == :cache
|
13
|
+
mailer.clear_cache
|
14
|
+
else
|
15
|
+
deliveries.clear
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def mailbox_for(address)
|
20
|
+
deliveries.select { |email|
|
21
|
+
(email.to && email.to.include?(address)) ||
|
22
|
+
(email.bcc && email.bcc.include?(address)) ||
|
23
|
+
(email.cc && email.cc.include?(address)) }
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def deliveries
|
29
|
+
if ActionMailer::Base.delivery_method == :cache
|
30
|
+
mailer.cached_deliveries
|
31
|
+
else
|
32
|
+
mailer.deliveries
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
module ARMailerDeliveries
|
38
|
+
def all_emails
|
39
|
+
Email.all.map{ |email| parse_to_mail(email) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def last_email_sent
|
43
|
+
if email = Email.last
|
44
|
+
Mail.read(email.mail)
|
45
|
+
else
|
46
|
+
raise("No email has been sent!")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def reset_mailer
|
51
|
+
Email.delete_all
|
52
|
+
end
|
53
|
+
|
54
|
+
def mailbox_for(address)
|
55
|
+
Email.all.select { |email|
|
56
|
+
(email.to && email.to.include?(address)) ||
|
57
|
+
(email.bcc && email.bcc.include?(address)) ||
|
58
|
+
(email.cc && email.cc.include?(address)) }.map{ |email| parse_to_mail(email) }
|
59
|
+
end
|
60
|
+
|
61
|
+
def parse_to_mail(email)
|
62
|
+
Mail.read(email.mail)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
if defined?(Pony)
|
67
|
+
module ::Pony
|
68
|
+
def self.deliveries
|
69
|
+
@deliveries ||= []
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.mail(options)
|
73
|
+
deliveries << build_mail(options)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
module Deliveries
|
79
|
+
if defined?(Pony)
|
80
|
+
def deliveries; Pony::deliveries ; end
|
81
|
+
include EmailSpec::MailerDeliveries
|
82
|
+
elsif ActionMailer::Base.delivery_method == :activerecord
|
83
|
+
include EmailSpec::ARMailerDeliveries
|
84
|
+
else
|
85
|
+
def mailer; ActionMailer::Base; end
|
86
|
+
include EmailSpec::MailerDeliveries
|
87
|
+
end
|
88
|
+
include EmailSpec::BackgroundProcesses::Compatibility
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
@@ -0,0 +1,91 @@
|
|
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.include?('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.include?('text/plain') }
|
36
|
+
if m.respond_to?(:ordered_each) # Rails 2 / TMail
|
37
|
+
m.ordered_each{|k,v| f.write "#{k}: #{v}\n" }
|
38
|
+
else # Rails 3 / Mail
|
39
|
+
f.write(text_part.header.to_s + "\n")
|
40
|
+
end
|
41
|
+
|
42
|
+
f.write text_part.body
|
43
|
+
else
|
44
|
+
f.write m.to_s
|
45
|
+
end
|
46
|
+
f.write "\n" + '='*80 + "\n"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
open_in_text_editor(filename)
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.save_and_open_email(mail)
|
54
|
+
filename = tmp_email_filename
|
55
|
+
|
56
|
+
File.open(filename, "w") do |f|
|
57
|
+
f.write mail.to_s
|
58
|
+
end
|
59
|
+
|
60
|
+
open_in_text_editor(filename)
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.save_and_open_email_attachments_list(mail)
|
64
|
+
filename = tmp_email_filename
|
65
|
+
|
66
|
+
File.open(filename, "w") do |f|
|
67
|
+
mail.attachments.each_with_index do |attachment, index|
|
68
|
+
info = "#{index + 1}:"
|
69
|
+
info += "\n\tfilename: #{attachment.original_filename}"
|
70
|
+
info += "\n\tcontent type: #{attachment.content_type}"
|
71
|
+
info += "\n\tsize: #{attachment.size}"
|
72
|
+
f.write info + "\n"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
open_in_text_editor(filename)
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.open_in_text_editor(filename)
|
80
|
+
Launchy.open(URI.parse("file://#{File.expand_path(filename)}"), :application => :editor)
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.open_in_browser(filename)
|
84
|
+
Launchy.open(URI.parse("file://#{File.expand_path(filename)}"))
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.tmp_email_filename(extension = '.txt')
|
88
|
+
"#{Rails.root}/tmp/email-#{Time.now.to_i}#{extension}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'email_spec/deliveries'
|
3
|
+
|
4
|
+
module EmailSpec
|
5
|
+
|
6
|
+
module Helpers
|
7
|
+
include Deliveries
|
8
|
+
|
9
|
+
def visit_in_email(link_text)
|
10
|
+
visit(parse_email_for_link(current_email, link_text))
|
11
|
+
end
|
12
|
+
|
13
|
+
def click_email_link_matching(regex, email = current_email)
|
14
|
+
url = links_in_email(email).detect { |link| link =~ regex }
|
15
|
+
raise "No link found matching #{regex.inspect} in #{email.default_part_body}" unless url
|
16
|
+
visit request_uri(url)
|
17
|
+
end
|
18
|
+
|
19
|
+
def click_first_link_in_email(email = current_email)
|
20
|
+
link = links_in_email(email).first
|
21
|
+
visit request_uri(link)
|
22
|
+
end
|
23
|
+
|
24
|
+
def open_email(address, opts={})
|
25
|
+
set_current_email(find_email!(address, opts))
|
26
|
+
end
|
27
|
+
|
28
|
+
alias_method :open_email_for, :open_email
|
29
|
+
|
30
|
+
def open_last_email
|
31
|
+
set_current_email(last_email_sent)
|
32
|
+
end
|
33
|
+
|
34
|
+
def open_last_email_for(address)
|
35
|
+
set_current_email(mailbox_for(address).last)
|
36
|
+
end
|
37
|
+
|
38
|
+
def current_email(address=nil)
|
39
|
+
address = convert_address(address)
|
40
|
+
email = address ? email_spec_hash[:current_emails][address] : email_spec_hash[:current_email]
|
41
|
+
raise RSpec::Expectations::ExpectationNotMetError, "Expected an open email but none was found. Did you forget to call open_email?" unless email
|
42
|
+
email
|
43
|
+
end
|
44
|
+
|
45
|
+
def current_email_attachments(address=nil)
|
46
|
+
current_email(address).attachments || Array.new
|
47
|
+
end
|
48
|
+
|
49
|
+
def unread_emails_for(address)
|
50
|
+
mailbox_for(address) - read_emails_for(address)
|
51
|
+
end
|
52
|
+
|
53
|
+
def read_emails_for(address)
|
54
|
+
email_spec_hash[:read_emails][convert_address(address)] ||= []
|
55
|
+
end
|
56
|
+
|
57
|
+
# Should be able to accept String or Regexp options.
|
58
|
+
def find_email(address, opts={})
|
59
|
+
address = convert_address(address)
|
60
|
+
if opts[:with_subject]
|
61
|
+
expected_subject = (opts[:with_subject].is_a?(String) ? Regexp.escape(opts[:with_subject]) : opts[:with_subject])
|
62
|
+
mailbox_for(address).find { |m| m.subject =~ Regexp.new(expected_subject) }
|
63
|
+
elsif opts[:with_text]
|
64
|
+
expected_text = (opts[:with_text].is_a?(String) ? Regexp.escape(opts[:with_text]) : opts[:with_text])
|
65
|
+
mailbox_for(address).find { |m| m.default_part_body =~ Regexp.new(expected_text) }
|
66
|
+
else
|
67
|
+
mailbox_for(address).first
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def links_in_email(email)
|
72
|
+
URI.extract(email.default_part_body.to_s, ['http', 'https'])
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def email_spec_hash
|
78
|
+
@email_spec_hash ||= {:read_emails => {}, :unread_emails => {}, :current_emails => {}, :current_email => nil}
|
79
|
+
end
|
80
|
+
|
81
|
+
def find_email!(address, opts={})
|
82
|
+
email = find_email(address, opts)
|
83
|
+
if current_email_address.nil?
|
84
|
+
raise EmailSpec::NoEmailAddressProvided, "No email address has been provided. Make sure current_email_address is returning something."
|
85
|
+
elsif email.nil?
|
86
|
+
error = "#{opts.keys.first.to_s.humanize.downcase unless opts.empty?} #{('"' + opts.values.first.to_s + '"') unless opts.empty?}"
|
87
|
+
raise EmailSpec::CouldNotFindEmailError, "Could not find email #{error} in the mailbox for #{current_email_address}. \n Found the following emails:\n\n #{all_emails.to_s}"
|
88
|
+
end
|
89
|
+
email
|
90
|
+
end
|
91
|
+
|
92
|
+
def set_current_email(email)
|
93
|
+
return unless email
|
94
|
+
[email.to, email.cc, email.bcc].compact.flatten.each do |to|
|
95
|
+
read_emails_for(to) << email
|
96
|
+
email_spec_hash[:current_emails][to] = email
|
97
|
+
end
|
98
|
+
email_spec_hash[:current_email] = email
|
99
|
+
end
|
100
|
+
|
101
|
+
def parse_email_for_link(email, text_or_regex)
|
102
|
+
email.should have_body_text(text_or_regex)
|
103
|
+
|
104
|
+
url = parse_email_for_explicit_link(email, text_or_regex)
|
105
|
+
url ||= parse_email_for_anchor_text_link(email, text_or_regex)
|
106
|
+
|
107
|
+
raise "No link found matching #{text_or_regex.inspect} in #{email}" unless url
|
108
|
+
url
|
109
|
+
end
|
110
|
+
|
111
|
+
def request_uri(link)
|
112
|
+
return unless link
|
113
|
+
url = URI::parse(link)
|
114
|
+
url.fragment ? (url.request_uri + "#" + url.fragment) : url.request_uri
|
115
|
+
end
|
116
|
+
|
117
|
+
# e.g. confirm in http://confirm
|
118
|
+
def parse_email_for_explicit_link(email, regex)
|
119
|
+
regex = /#{Regexp.escape(regex)}/ unless regex.is_a?(Regexp)
|
120
|
+
url = links_in_email(email).detect { |link| link =~ regex }
|
121
|
+
request_uri(url)
|
122
|
+
end
|
123
|
+
|
124
|
+
# e.g. Click here in <a href="http://confirm">Click here</a>
|
125
|
+
def parse_email_for_anchor_text_link(email, link_text)
|
126
|
+
if textify_images(email.default_part_body) =~ %r{<a[^>]*href=['"]?([^'"]*)['"]?[^>]*?>[^<]*?#{link_text}[^<]*?</a>}
|
127
|
+
URI.split($1)[5..-1].compact!.join("?").gsub("&", "&")
|
128
|
+
# sub correct ampersand after rails switches it (http://dev.rubyonrails.org/ticket/4002)
|
129
|
+
else
|
130
|
+
return nil
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def textify_images(email_body)
|
135
|
+
email_body.to_s.gsub(%r{<img[^>]*alt=['"]?([^'"]*)['"]?[^>]*?/>}) { $1 }
|
136
|
+
end
|
137
|
+
|
138
|
+
def parse_email_count(amount)
|
139
|
+
case amount
|
140
|
+
when "no"
|
141
|
+
0
|
142
|
+
when "an"
|
143
|
+
1
|
144
|
+
else
|
145
|
+
amount.to_i
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
attr_reader :last_email_address
|
150
|
+
|
151
|
+
def convert_address(address)
|
152
|
+
@last_email_address = (address || current_email_address)
|
153
|
+
AddressConverter.instance.convert(@last_email_address)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Overwrite this method to set default email address, for example:
|
157
|
+
# last_email_address || @current_user.email
|
158
|
+
def current_email_address
|
159
|
+
last_email_address
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
def mailbox_for(address)
|
164
|
+
super(convert_address(address)) # super resides in Deliveries
|
165
|
+
end
|
166
|
+
|
167
|
+
def email_spec_deprecate(text)
|
168
|
+
puts ""
|
169
|
+
puts "DEPRECATION: #{text.split.join(' ')}"
|
170
|
+
puts ""
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|