diff --git a/lib/generators/rspec/mailbox/mailbox_generator.rb b/lib/generators/rspec/mailbox/mailbox_generator.rb new file mode 100644 index 0000000000..1277326e3b --- /dev/null +++ b/lib/generators/rspec/mailbox/mailbox_generator.rb @@ -0,0 +1,14 @@ +require 'generators/rspec' + +module Rspec + module Generators + # @private + class MailboxGenerator < Base + def create_mailbox_spec + template('mailbox_spec.rb.erb', + File.join('spec/mailboxes', class_path, "#{file_name}_mailbox_spec.rb") + ) + end + end + end +end diff --git a/lib/generators/rspec/mailbox/templates/mailbox_spec.rb.erb b/lib/generators/rspec/mailbox/templates/mailbox_spec.rb.erb new file mode 100644 index 0000000000..3766bd3915 --- /dev/null +++ b/lib/generators/rspec/mailbox/templates/mailbox_spec.rb.erb @@ -0,0 +1,7 @@ +require 'rails_helper' + +<% module_namespacing do -%> +RSpec.describe <%= class_name %>Mailbox, <%= type_metatag(:mailbox) %> do + pending "add some examples to (or delete) #{__FILE__}" +end +<% end -%> diff --git a/lib/rspec/rails/configuration.rb b/lib/rspec/rails/configuration.rb index 6869b88437..c61df85886 100644 --- a/lib/rspec/rails/configuration.rb +++ b/lib/rspec/rails/configuration.rb @@ -34,7 +34,8 @@ class Configuration :routing => %w[spec routing], :view => %w[spec views], :feature => %w[spec features], - :system => %w[spec system] + :system => %w[spec system], + :mailbox => %w[spec mailboxes] } # Sets up the different example group modules for the different spec types @@ -140,6 +141,10 @@ def filter_rails_from_backtrace! if defined?(ActiveJob) config.include RSpec::Rails::JobExampleGroup, :type => :job end + + if defined?(ActionMailbox) + config.include RSpec::Rails::MailboxExampleGroup, :type => :mailbox + end end # rubocop:enable Style/MethodLength diff --git a/lib/rspec/rails/example.rb b/lib/rspec/rails/example.rb index 4ad8507a95..56e4160848 100644 --- a/lib/rspec/rails/example.rb +++ b/lib/rspec/rails/example.rb @@ -9,3 +9,4 @@ require 'rspec/rails/example/job_example_group' require 'rspec/rails/example/feature_example_group' require 'rspec/rails/example/system_example_group' +require 'rspec/rails/example/mailbox_example_group' diff --git a/lib/rspec/rails/example/mailbox_example_group.rb b/lib/rspec/rails/example/mailbox_example_group.rb new file mode 100644 index 0000000000..97150b8f88 --- /dev/null +++ b/lib/rspec/rails/example/mailbox_example_group.rb @@ -0,0 +1,74 @@ +module RSpec + module Rails + # @api public + # Container module for mailbox spec functionality. + module MailboxExampleGroup + extend ActiveSupport::Concern + + if RSpec::Rails::FeatureCheck.has_action_mailbox? + require 'action_mailbox/test_helper' + extend ::ActionMailbox::TestHelper + + def self.create_inbound_email(arg) + case arg + when Hash + create_inbound_email_from_mail(arg) + else + create_inbound_email_from_source(arg.to_s) + end + end + else + def self.create_inbound_email(_arg) + raise "Could not load ActionMailer::TestHelper" + end + end + + class_methods do + # @private + def mailbox_class + described_class + end + end + + included do + subject { described_class } + end + + # Verify the status of any inbound email + # + # @example + # describe ForwardsMailbox do + # it "can describe what happened to the inbound email" do + # mail = process(args) + # + # # can use any of: + # expect(mail).to have_been_delivered + # expect(mail).to have_bounced + # expect(mail).to have_failed + # end + # end + def have_been_delivered + satisfy('have been delivered', &:delivered?) + end + + def have_bounced + satisfy('have bounced', &:bounced?) + end + + def have_failed + satisfy('have failed', &:failed?) + end + + # Process an inbound email message directly, bypassing routing. + # + # @param message [Hash, Mail::Message] a mail message or hash of + # attributes used to build one + # @return [ActionMaibox::InboundMessage] + def process(message) + MailboxExampleGroup.create_inbound_email(message).tap do |mail| + self.class.mailbox_class.receive(mail) + end + end + end + end +end diff --git a/lib/rspec/rails/feature_check.rb b/lib/rspec/rails/feature_check.rb index ed92131771..35006f2c8c 100644 --- a/lib/rspec/rails/feature_check.rb +++ b/lib/rspec/rails/feature_check.rb @@ -38,6 +38,10 @@ def has_action_mailer_show_preview? ::ActionMailer::Base.respond_to?(:show_previews=) end + def has_action_mailbox? + defined?(::ActionMailbox) + end + def has_1_9_hash_syntax? ::Rails::VERSION::STRING > '4.0' end diff --git a/lib/rspec/rails/matchers.rb b/lib/rspec/rails/matchers.rb index dd37321e80..c90412519e 100644 --- a/lib/rspec/rails/matchers.rb +++ b/lib/rspec/rails/matchers.rb @@ -20,6 +20,11 @@ module Matchers require 'rspec/rails/matchers/relation_match_array' require 'rspec/rails/matchers/be_valid' require 'rspec/rails/matchers/have_http_status' + if RSpec::Rails::FeatureCheck.has_active_job? require 'rspec/rails/matchers/active_job' end + +if RSpec::Rails::FeatureCheck.has_action_mailbox? + require 'rspec/rails/matchers/action_mailbox' +end diff --git a/lib/rspec/rails/matchers/action_mailbox.rb b/lib/rspec/rails/matchers/action_mailbox.rb new file mode 100644 index 0000000000..c5392cb225 --- /dev/null +++ b/lib/rspec/rails/matchers/action_mailbox.rb @@ -0,0 +1,64 @@ +module RSpec + module Rails + module Matchers + # Namespace for various implementations of ActionMailbox features + # + # @api private + module ActionMailbox + # @private + class Base < RSpec::Rails::Matchers::BaseMatcher + private + + def create_inbound_email(message) + RSpec::Rails::MailboxExampleGroup.create_inbound_email(message) + end + end + + # @private + class ReceiveInboundEmail < Base + def initialize(message) + super() + + @inbound_email = create_inbound_email(message) + end + + def matches?(mailbox) + @mailbox = mailbox + @receiver = ApplicationMailbox.router.send(:match_to_mailbox, inbound_email) + + @receiver == @mailbox + end + + def failure_message + "expected #{describe_inbound_email} to route to #{mailbox}".tap do |msg| + if receiver + msg << ", but routed to #{receiver} instead" + end + end + end + + def failure_message_when_negated + "expected #{describe_inbound_email} not to route to #{mailbox}" + end + + private + + attr_reader :inbound_email, :mailbox, :receiver + + def describe_inbound_email + "mail to #{inbound_email.mail.to.to_sentence}" + end + end + end + + # @api public + # Passes if the given inbound email would be routed to the subject inbox. + # + # @param message [Hash, Mail::Message] a mail message or hash of + # attributes used to build one + def receive_inbound_email(message) + ActionMailbox::ReceiveInboundEmail.new(message) + end + end + end +end diff --git a/spec/generators/rspec/mailbox/mailbox_generator_spec.rb b/spec/generators/rspec/mailbox/mailbox_generator_spec.rb new file mode 100644 index 0000000000..3a80a3c3ac --- /dev/null +++ b/spec/generators/rspec/mailbox/mailbox_generator_spec.rb @@ -0,0 +1,18 @@ +# Generators are not automatically loaded by Rails +require 'generators/rspec/mailbox/mailbox_generator' +require 'support/generators' + +RSpec.describe Rspec::Generators::MailboxGenerator, :type => :generator, :skip => !RSpec::Rails::FeatureCheck.has_action_mailbox? do + setup_default_destination + + describe 'the generated files' do + before { run_generator %w[forwards] } + + subject { file('spec/mailboxes/forwards_mailbox_spec.rb') } + + it { is_expected.to exist } + it { is_expected.to contain(/require 'rails_helper'/) } + it { is_expected.to contain(/describe ForwardsMailbox, #{type_metatag(:mailbox)}/) } + + end +end diff --git a/spec/rspec/rails/example/mailbox_example_group_spec.rb b/spec/rspec/rails/example/mailbox_example_group_spec.rb new file mode 100644 index 0000000000..42a9adaf17 --- /dev/null +++ b/spec/rspec/rails/example/mailbox_example_group_spec.rb @@ -0,0 +1,83 @@ +require "spec_helper" +require "rspec/rails/feature_check" + +class ApplicationMailbox + class << self + attr_accessor :received + + def receive(*) + self.received += 1 + end + end + + self.received = 0 +end + +module RSpec + module Rails + describe MailboxExampleGroup, :skip => !RSpec::Rails::FeatureCheck.has_active_job? do + it_behaves_like "an rspec-rails example group mixin", :mailbox, + './spec/mailboxes/', '.\\spec\\mailboxes\\' + + def group_for(klass) + RSpec::Core::ExampleGroup.describe klass do + include MailboxExampleGroup + end + end + + let(:group) { group_for(::ApplicationMailbox) } + let(:example) { group.new } + + describe '#have_been_delivered' do + it 'raises on undelivered mail' do + expect { + expect(double('IncomingEmail', :delivered? => false)).to example.have_been_delivered + }.to raise_error(/have been delivered/) + end + + it 'does not raise otherwise' do + expect(double('IncomingEmail', :delivered? => true)).to example.have_been_delivered + end + end + + describe '#have_bounced' do + it 'raises on unbounced mail' do + expect { + expect(double('IncomingEmail', :bounced? => false)).to example.have_bounced + }.to raise_error(/have bounced/) + end + + it 'does not raise otherwise' do + expect(double('IncomingEmail', :bounced? => true)).to example.have_bounced + end + end + + describe '#have_failed' do + it 'raises on unfailed mail' do + expect { + expect(double('IncomingEmail', :failed? => false)).to example.have_failed + }.to raise_error(/have failed/) + end + + it 'does not raise otherwise' do + expect(double('IncomingEmail', :failed? => true)).to example.have_failed + end + end + + describe '#process' do + before do + allow(RSpec::Rails::MailboxExampleGroup).to receive(:create_inbound_email) do |attributes| + mail = double('Mail::Message', attributes) + double('InboundEmail', :mail => mail) + end + end + + it 'sends mail to the mailbox' do + expect { + example.process(:to => ['test@example.com']) + }.to change(::ApplicationMailbox, :received).by(1) + end + end + end + end +end diff --git a/spec/rspec/rails/matchers/action_mailbox_spec.rb b/spec/rspec/rails/matchers/action_mailbox_spec.rb new file mode 100644 index 0000000000..99577d13d2 --- /dev/null +++ b/spec/rspec/rails/matchers/action_mailbox_spec.rb @@ -0,0 +1,52 @@ +require "spec_helper" +require "rspec/rails/feature_check" + +class ApplicationMailbox + class Router + def match_to_mailbox(*) + Inbox + end + end + + def self.router + Router.new + end +end + +class Inbox < ApplicationMailbox; end +class Otherbox < ApplicationMailbox; end + +RSpec.describe "ActionMailbox matchers", :skip => !RSpec::Rails::FeatureCheck.has_active_job? do + include RSpec::Rails::Matchers::ActionMailbox + + describe "receive_inbound_email" do + let(:to) { ['to@example.com'] } + + before do + allow(RSpec::Rails::MailboxExampleGroup).to receive(:create_inbound_email) do |attributes| + mail = double('Mail::Message', attributes) + double('InboundEmail', :mail => mail) + end + end + + it "passes when it receives inbound email" do + expect(Inbox).to receive_inbound_email(:to => to) + end + + it "passes when negated when it doesn't receive inbound email" do + expect(Otherbox).not_to receive_inbound_email(:to => to) + end + + it "fails when it doesn't receive inbound email" do + expect { + expect(Otherbox).to receive_inbound_email(:to => to) + }.to raise_error(/expected mail to to@example.com to route to Otherbox, but routed to Inbox/) + end + + it "fails when negated when it receives inbound email" do + expect { + expect(Inbox).not_to receive_inbound_email(:to => to) + }.to raise_error(/expected mail to to@example.com not to route to Inbox/) + end + end +end