gardelea-email_spec 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|