rainchen-email_spec 0.1.4

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.
Files changed (90) hide show
  1. data/History.txt +75 -0
  2. data/MIT-LICENSE.txt +19 -0
  3. data/README.rdoc +125 -0
  4. data/Rakefile +50 -0
  5. data/examples/rails_root/Rakefile +7 -0
  6. data/examples/rails_root/app/controllers/application.rb +15 -0
  7. data/examples/rails_root/app/controllers/welcome_controller.rb +9 -0
  8. data/examples/rails_root/app/helpers/application_helper.rb +3 -0
  9. data/examples/rails_root/app/helpers/welcome_helper.rb +2 -0
  10. data/examples/rails_root/app/models/user.rb +2 -0
  11. data/examples/rails_root/app/models/user_mailer.rb +12 -0
  12. data/examples/rails_root/app/views/user_mailer/signup.erb +3 -0
  13. data/examples/rails_root/app/views/welcome/confirm.html.erb +1 -0
  14. data/examples/rails_root/app/views/welcome/index.html.erb +8 -0
  15. data/examples/rails_root/app/views/welcome/signup.html.erb +1 -0
  16. data/examples/rails_root/config/boot.rb +109 -0
  17. data/examples/rails_root/config/database.yml +22 -0
  18. data/examples/rails_root/config/environment.rb +22 -0
  19. data/examples/rails_root/config/environments/development.rb +17 -0
  20. data/examples/rails_root/config/environments/production.rb +24 -0
  21. data/examples/rails_root/config/environments/test.rb +33 -0
  22. data/examples/rails_root/config/initializers/inflections.rb +10 -0
  23. data/examples/rails_root/config/initializers/mime_types.rb +5 -0
  24. data/examples/rails_root/config/initializers/new_rails_defaults.rb +17 -0
  25. data/examples/rails_root/config/routes.rb +43 -0
  26. data/examples/rails_root/cucumber.yml +1 -0
  27. data/examples/rails_root/db/migrate/20090125013728_create_users.rb +11 -0
  28. data/examples/rails_root/db/schema.rb +19 -0
  29. data/examples/rails_root/doc/README_FOR_APP +5 -0
  30. data/examples/rails_root/features/errors.feature +42 -0
  31. data/examples/rails_root/features/example.feature +59 -0
  32. data/examples/rails_root/features/step_definitions/email_steps.rb +74 -0
  33. data/examples/rails_root/features/step_definitions/user_steps.rb +23 -0
  34. data/examples/rails_root/features/step_definitions/webrat_steps.rb +104 -0
  35. data/examples/rails_root/features/support/env.rb +21 -0
  36. data/examples/rails_root/public/404.html +30 -0
  37. data/examples/rails_root/public/422.html +30 -0
  38. data/examples/rails_root/public/500.html +33 -0
  39. data/examples/rails_root/public/dispatch.rb +10 -0
  40. data/examples/rails_root/public/favicon.ico +0 -0
  41. data/examples/rails_root/public/images/rails.png +0 -0
  42. data/examples/rails_root/public/javascripts/application.js +2 -0
  43. data/examples/rails_root/public/javascripts/controls.js +963 -0
  44. data/examples/rails_root/public/javascripts/dragdrop.js +973 -0
  45. data/examples/rails_root/public/javascripts/effects.js +1128 -0
  46. data/examples/rails_root/public/javascripts/prototype.js +4320 -0
  47. data/examples/rails_root/public/robots.txt +5 -0
  48. data/examples/rails_root/script/about +4 -0
  49. data/examples/rails_root/script/autospec +5 -0
  50. data/examples/rails_root/script/console +3 -0
  51. data/examples/rails_root/script/cucumber +7 -0
  52. data/examples/rails_root/script/dbconsole +3 -0
  53. data/examples/rails_root/script/destroy +3 -0
  54. data/examples/rails_root/script/generate +3 -0
  55. data/examples/rails_root/script/performance/benchmarker +3 -0
  56. data/examples/rails_root/script/performance/profiler +3 -0
  57. data/examples/rails_root/script/performance/request +3 -0
  58. data/examples/rails_root/script/plugin +3 -0
  59. data/examples/rails_root/script/process/inspector +3 -0
  60. data/examples/rails_root/script/process/reaper +3 -0
  61. data/examples/rails_root/script/process/spawner +3 -0
  62. data/examples/rails_root/script/runner +3 -0
  63. data/examples/rails_root/script/server +3 -0
  64. data/examples/rails_root/script/spec +5 -0
  65. data/examples/rails_root/script/spec_server +125 -0
  66. data/examples/rails_root/spec/controllers/welcome_controller_spec.rb +15 -0
  67. data/examples/rails_root/spec/model_factory.rb +6 -0
  68. data/examples/rails_root/spec/models/user_mailer_spec.rb +58 -0
  69. data/examples/rails_root/spec/models/user_spec.rb +5 -0
  70. data/examples/rails_root/spec/rcov.opts +2 -0
  71. data/examples/rails_root/spec/spec.opts +4 -0
  72. data/examples/rails_root/spec/spec_helper.rb +51 -0
  73. data/examples/rails_root/stories/all.rb +4 -0
  74. data/examples/rails_root/stories/helper.rb +3 -0
  75. data/examples/rails_root/vendor/plugins/email_spec/rails_generators/email_spec/email_spec_generator.rb +17 -0
  76. data/examples/rails_root/vendor/plugins/email_spec/rails_generators/email_spec/templates/email_steps.rb +74 -0
  77. data/install.rb +0 -0
  78. data/lib/email_spec.rb +14 -0
  79. data/lib/email_spec/address_converter.rb +27 -0
  80. data/lib/email_spec/cucumber.rb +19 -0
  81. data/lib/email_spec/deliveries.rb +55 -0
  82. data/lib/email_spec/email_viewer.rb +70 -0
  83. data/lib/email_spec/helpers.rb +129 -0
  84. data/lib/email_spec/matchers.rb +122 -0
  85. data/rails_generators/email_spec/email_spec_generator.rb +17 -0
  86. data/rails_generators/email_spec/templates/email_steps.rb +74 -0
  87. data/spec/email_spec/matchers_spec.rb +159 -0
  88. data/spec/spec.opts +4 -0
  89. data/spec/spec_helper.rb +4 -0
  90. metadata +176 -0
data/install.rb ADDED
File without changes
data/lib/email_spec.rb ADDED
@@ -0,0 +1,14 @@
1
+ unless defined?(ActionMailer)
2
+ Kernel.warn("ActionMailer did not appear to be loaded so email-spec is requiring it.")
3
+ require 'actionmailer'
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 'email_spec/deliveries'
9
+ require 'email_spec/address_converter'
10
+ require 'email_spec/email_viewer'
11
+ require 'email_spec/helpers'
12
+ require 'email_spec/matchers'
13
+
14
+
@@ -0,0 +1,27 @@
1
+ module EmailSpec
2
+ class AddressConverter
3
+ include Singleton
4
+
5
+ attr_accessor :converter
6
+
7
+ # The block provided to conversion should convert to an email
8
+ # address string or return the input untouched. For example:
9
+ #
10
+ # EmailSpec::AddressConverter.instance.conversion do |input|
11
+ # if input.is_a?(User)
12
+ # input.email
13
+ # else
14
+ # input
15
+ # end
16
+ # end
17
+ #
18
+ def conversion(&block)
19
+ self.converter = block
20
+ end
21
+
22
+ def convert(input)
23
+ return input unless converter
24
+ converter.call(input)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ # require this in your env.rb file after you require cucumber/rails/world
2
+
3
+ # Global Setup
4
+ ActionMailer::Base.delivery_method = :test unless ActionMailer::Base.delivery_method == :activerecord
5
+ ActionMailer::Base.perform_deliveries = true
6
+
7
+ Before do
8
+ # Scenario setup
9
+ ActionMailer::Base.deliveries.clear if ActionMailer::Base.delivery_method == :test
10
+ end
11
+
12
+ After do
13
+ EmailSpec::EmailViewer.save_and_open_all_raw_emails if ENV['SHOW_EMAILS']
14
+ EmailSpec::EmailViewer.save_and_open_all_html_emails if ENV['SHOW_HTML_EMAILS']
15
+ EmailSpec::EmailViewer.save_and_open_all_text_emails if ENV['SHOW_TEXT_EMAILS']
16
+ end
17
+
18
+ World(EmailSpec::Helpers)
19
+ World(EmailSpec::Matchers)
@@ -0,0 +1,55 @@
1
+ module EmailSpec
2
+ module TestDeliveries
3
+ def all_emails
4
+ ActionMailer::Base.deliveries
5
+ end
6
+
7
+ def last_email_sent
8
+ ActionMailer::Base.deliveries.last || raise("No email has been sent!")
9
+ end
10
+
11
+ def reset_mailer
12
+ ActionMailer::Base.deliveries.clear
13
+ end
14
+
15
+ def mailbox_for(address)
16
+ address = AddressConverter.instance.convert(address)
17
+ ActionMailer::Base.deliveries.select { |m| m.to.include?(address) }
18
+ end
19
+ end
20
+
21
+ module ARMailerDeliveries
22
+ def all_emails
23
+ Email.all.map{ |email| parse_to_tmail(email) }
24
+ end
25
+
26
+ def last_email_sent
27
+ if email = Email.last
28
+ TMail::Mail.parse(email.mail)
29
+ else
30
+ raise("No email has been sent!")
31
+ end
32
+ end
33
+
34
+ def reset_mailer
35
+ Email.delete_all
36
+ end
37
+
38
+ def mailbox_for(address)
39
+ address = AddressConverter.instance.convert(address)
40
+ Email.all.select { |email| email.to.include?(address) }.map{ |email| parse_to_tmail(email) }
41
+ end
42
+
43
+ def parse_to_tmail(email)
44
+ TMail::Mail.parse(email.mail)
45
+ end
46
+ end
47
+
48
+ module Deliveries
49
+ if ActionMailer::Base.delivery_method == :activerecord
50
+ include EmailSpec::ARMailerDeliveries
51
+ else
52
+ include EmailSpec::TestDeliveries
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,70 @@
1
+ module EmailSpec
2
+ class EmailViewer
3
+ extend Deliveries
4
+
5
+ def self.save_and_open_all_raw_emails
6
+ filename = "#{RAILS_ROOT}/tmp/email-#{Time.now.to_i}.txt"
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 = "#{RAILS_ROOT}/tmp/email-#{Time.now.to_i}.txt"
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.open_in_text_editor(filename)
59
+ `mate #{filename}`
60
+ end
61
+
62
+ def self.open_in_browser(filename)
63
+ `open #{filename}`
64
+ end
65
+
66
+ def self.tmp_email_filename(extension = '.txt')
67
+ "#{RAILS_ROOT}/tmp/email-#{Time.now.to_i}#{extension}"
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,129 @@
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.body}" unless url
16
+ request_uri = URI::parse(url).request_uri
17
+ visit request_uri
18
+ end
19
+
20
+ def click_first_link_in_email(email = current_email)
21
+ link = links_in_email(email).first
22
+ request_uri = URI::parse(link).request_uri
23
+ visit request_uri
24
+ end
25
+
26
+ def open_email(address, opts={})
27
+ address = convert_address(address)
28
+ set_current_email(find_email!(address, opts))
29
+ end
30
+
31
+ alias_method :open_email_for, :open_email
32
+
33
+ def open_last_email
34
+ set_current_email(last_email_sent)
35
+ end
36
+
37
+ def open_last_email_for(address)
38
+ address = convert_address(address)
39
+ set_current_email(mailbox_for(address).last)
40
+ end
41
+
42
+ def current_email(address=nil)
43
+ address = convert_address(address)
44
+ email = address ? email_spec_hash[:current_emails][address] : email_spec_hash[:current_email]
45
+ raise Spec::Expectations::ExpectationNotMetError, "Expected an open email but none was found. Did you forget to call open_email?" unless email
46
+ email
47
+ end
48
+
49
+ def unread_emails_for(address)
50
+ address = convert_address(address)
51
+ mailbox_for(address) - read_emails_for(address)
52
+ end
53
+
54
+ def read_emails_for(address)
55
+ address = convert_address(address)
56
+ email_spec_hash[:read_emails][address] ||= []
57
+ end
58
+
59
+ def find_email(address, opts={})
60
+ address = convert_address(address)
61
+ if opts[:with_subject]
62
+ email = mailbox_for(address).find { |m| m.subject =~ Regexp.new(opts[:with_subject]) }
63
+ elsif opts[:with_text]
64
+ email = mailbox_for(address).find { |m| m.body =~ Regexp.new(opts[:with_text]) }
65
+ else
66
+ email = mailbox_for(address).first
67
+ end
68
+ end
69
+
70
+ def links_in_email(email)
71
+ URI.extract(email.body, ['http', 'https'])
72
+ end
73
+
74
+ private
75
+
76
+ def email_spec_hash
77
+ @email_spec_hash ||= {:read_emails => {}, :unread_emails => {}, :current_emails => {}, :current_email => nil}
78
+ end
79
+
80
+ def find_email!(address, opts={})
81
+ address = convert_address(address)
82
+ email = find_email(address, opts)
83
+ if email.nil?
84
+ error = "#{opts.keys.first.to_s.humanize unless opts.empty?} #{('"' + opts.values.first.to_s.humanize + '"') unless opts.empty?}"
85
+ raise Spec::Expectations::ExpectationNotMetError, "Could not find email #{error}. \n Found the following emails:\n\n #{all_emails.to_s}"
86
+ end
87
+ email
88
+ end
89
+
90
+ def set_current_email(email)
91
+ return unless email
92
+ email.to.each do |to|
93
+ read_emails_for(to) << email
94
+ email_spec_hash[:current_emails][to] = email
95
+ end
96
+ email_spec_hash[:current_email] = email
97
+ end
98
+
99
+ def parse_email_for_link(email, text_or_regex)
100
+ email.should have_body_text(text_or_regex)
101
+
102
+ url = parse_email_for_explicit_link(email, text_or_regex)
103
+ url ||= parse_email_for_anchor_text_link(email, text_or_regex)
104
+
105
+ raise "No link found matching #{text_or_regex.inspect} in #{email}" unless url
106
+ url
107
+ end
108
+
109
+ # e.g. confirm in http://confirm
110
+ def parse_email_for_explicit_link(email, regex)
111
+ regex = /#{Regexp.escape(regex)}/ unless regex.is_a?(Regexp)
112
+ url = links_in_email(email).detect { |link| link =~ regex }
113
+ URI::parse(url).request_uri if url
114
+ end
115
+
116
+ # e.g. Click here in <a href="http://confirm">Click here</a>
117
+ def parse_email_for_anchor_text_link(email, link_text)
118
+ email.body =~ %r{<a[^>]*href=['"]?([^'"]*)['"]?[^>]*?>[^<]*?#{link_text}[^<]*?</a>}
119
+ URI.split($~[1])[5..-1].compact!.join("?").gsub("&amp;", "&")
120
+ # sub correct ampersand after rails switches it (http://dev.rubyonrails.org/ticket/4002)
121
+ end
122
+
123
+
124
+ def convert_address(address)
125
+ AddressConverter.instance.convert(address)
126
+ end
127
+ end
128
+ end
129
+
@@ -0,0 +1,122 @@
1
+ module EmailSpec
2
+
3
+ module Matchers
4
+
5
+ class DeliverTo
6
+
7
+ def initialize(expected_email_addresses_or_objects_that_respond_to_email)
8
+ emails = expected_email_addresses_or_objects_that_respond_to_email.map do |email_or_object|
9
+ email_or_object.kind_of?(String) ? email_or_object : email_or_object.email
10
+ end
11
+
12
+ @expected_email_addresses = emails.sort
13
+ end
14
+
15
+ def description
16
+ "be delivered to #{@expected_email_addresses.inspect}"
17
+ end
18
+
19
+ def matches?(email)
20
+ @email = email
21
+ @actual_recipients = (email.to || []).sort
22
+ @actual_recipients == @expected_email_addresses
23
+ end
24
+
25
+ def failure_message
26
+ "expected #{@email.inspect} to deliver to #{@expected_email_addresses.inspect}, but it delievered to #{@actual_recipients.inspect}"
27
+ end
28
+
29
+ def negative_failure_message
30
+ "expected #{@email.inspect} not to deliver to #{@expected_email_addresses.inspect}, but it did"
31
+ end
32
+ end
33
+
34
+ def deliver_to(*expected_email_addresses_or_objects_that_respond_to_email)
35
+ DeliverTo.new(expected_email_addresses_or_objects_that_respond_to_email.flatten)
36
+ end
37
+
38
+ alias :be_delivered_to :deliver_to
39
+
40
+ def have_subject(expected)
41
+ simple_matcher do |given, matcher|
42
+ given_subject = given.subject
43
+
44
+ if expected.is_a?(String)
45
+ matcher.description = "have subject of #{expected.inspect}"
46
+ matcher.failure_message = "expected the subject to be #{expected.inspect} but was #{given_subject.inspect}"
47
+ matcher.negative_failure_message = "expected the subject not to be #{expected.inspect} but was"
48
+
49
+ given_subject == expected
50
+ else
51
+ matcher.description = "have subject matching #{expected.inspect}"
52
+ matcher.failure_message = "expected the subject to match #{expected.inspect}, but did not. Actual subject was: #{given_subject.inspect}"
53
+ matcher.negative_failure_message = "expected the subject not to match #{expected.inspect} but #{given_subject.inspect} does match it."
54
+
55
+ !!(given_subject =~ expected)
56
+ end
57
+ end
58
+ end
59
+
60
+ def include_email_with_subject(expected)
61
+ simple_matcher do |given_emails, matcher|
62
+
63
+ if expected.is_a?(String)
64
+ matcher.description = "include email with subject of #{expected.inspect}"
65
+ matcher.failure_message = "expected at least one email to have the subject #{expected.inspect} but none did. Subjects were #{given_emails.map(&:subject).inspect}"
66
+ matcher.negative_failure_message = "expected no email with the subject #{expected.inspect} but found at least one. Subjects were #{given_emails.map(&:subject).inspect}"
67
+
68
+ given_emails.map(&:subject).include?(expected)
69
+ else
70
+ matcher.description = "include email with subject matching #{expected.inspect}"
71
+ matcher.failure_message = "expected at least one email to have a subject matching #{expected.inspect}, but none did. Subjects were #{given_emails.map(&:subject).inspect}"
72
+ matcher.negative_failure_message = "expected no email to have a subject matching #{expected.inspect} but found at least one. Subjects were #{given_emails.map(&:subject).inspect}"
73
+
74
+ !!(given_emails.any?{ |mail| mail.subject =~ expected })
75
+ end
76
+ end
77
+ end
78
+
79
+ def have_body_text(expected)
80
+ simple_matcher do |given, matcher|
81
+
82
+ if expected.is_a?(String)
83
+ normalized_body = given.body.gsub(/\s+/, " ")
84
+ normalized_expected = expected.gsub(/\s+/, " ")
85
+ matcher.description = "have body including #{normalized_expected.inspect}"
86
+ matcher.failure_message = "expected the body to contain #{normalized_expected.inspect} but was #{normalized_body.inspect}"
87
+ matcher.negative_failure_message = "expected the body not to contain #{normalized_expected.inspect} but was #{normalized_body.inspect}"
88
+
89
+ normalized_body.include?(normalized_expected)
90
+ else
91
+ given_body = given.body
92
+ matcher.description = "have body matching #{expected.inspect}"
93
+ matcher.failure_message = "expected the body to match #{expected.inspect}, but did not. Actual body was: #{given_body.inspect}"
94
+ matcher.negative_failure_message = "expected the body not to match #{expected.inspect} but #{given_body.inspect} does match it."
95
+
96
+ !!(given_body =~ expected)
97
+ end
98
+ end
99
+ end
100
+
101
+ def have_header(expected_name, expected_value)
102
+ simple_matcher do |given, matcher|
103
+ given_header = given.header
104
+
105
+ if expected_value.is_a?(String)
106
+ matcher.description = "have header #{expected_name}: #{expected_value}"
107
+ matcher.failure_message = "expected the headers to include '#{expected_name}: #{expected_value}' but they were #{given_header.inspect}"
108
+ matcher.negative_failure_message = "expected the headers not to include '#{expected_name}: #{expected_value}' but they were #{given_header.inspect}"
109
+
110
+ given_header[expected_name].to_s == expected_value
111
+ else
112
+ matcher.description = "have header #{expected_name} with value matching #{expected_value.inspect}"
113
+ matcher.failure_message = "expected the headers to include '#{expected_name}' with a value matching #{expected_value.inspect} but they were #{given_header.inspect}"
114
+ matcher.negative_failure_message = "expected the headers not to include '#{expected_name}' with a value matching #{expected_value.inspect} but they were #{given_header.inspect}"
115
+
116
+ given_header[expected_name].to_s =~ expected_value
117
+ end
118
+ end
119
+ end
120
+
121
+ end
122
+ end