diff --git a/.gitignore b/.gitignore
index 7cd3efe..d406c47 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,3 @@
.DS_Store
test/debug.log
-test/lib/shoulda*
autotest
diff --git a/Rakefile b/Rakefile
index 4ed5c36..2fbc276 100644
--- a/Rakefile
+++ b/Rakefile
@@ -10,7 +10,7 @@ end
desc 'Default: run unit tests.'
-task :default => :test
+task :default => :test_rails
desc 'Test the GroupedScope plugin.'
Rake::TestTask.new(:test) do |t|
diff --git a/test/lib/shoulda.rb b/test/lib/shoulda.rb
new file mode 100755
index 0000000..a90ff18
--- /dev/null
+++ b/test/lib/shoulda.rb
@@ -0,0 +1,43 @@
+require 'shoulda/gem/shoulda'
+require 'shoulda/private_helpers'
+require 'shoulda/general'
+require 'shoulda/active_record_helpers'
+require 'shoulda/controller_tests/controller_tests.rb'
+require 'yaml'
+
+shoulda_options = {}
+
+possible_config_paths = []
+possible_config_paths << File.join(ENV["HOME"], ".shoulda.conf") if ENV["HOME"]
+possible_config_paths << "shoulda.conf"
+possible_config_paths << File.join("test", "shoulda.conf")
+possible_config_paths << File.join(RAILS_ROOT, "test", "shoulda.conf") if defined?(RAILS_ROOT)
+
+possible_config_paths.each do |config_file|
+ if File.exists? config_file
+ shoulda_options = YAML.load_file(config_file).symbolize_keys
+ break
+ end
+end
+
+require 'shoulda/color' if shoulda_options[:color]
+
+module Test # :nodoc: all
+ module Unit
+ class TestCase
+
+ include ThoughtBot::Shoulda::General
+ include ThoughtBot::Shoulda::Controller
+
+ extend ThoughtBot::Shoulda::ActiveRecord
+ end
+ end
+end
+
+module ActionController #:nodoc: all
+ module Integration
+ class Session
+ include ThoughtBot::Shoulda::General
+ end
+ end
+end
diff --git a/test/lib/shoulda/active_record_helpers.rb b/test/lib/shoulda/active_record_helpers.rb
new file mode 100755
index 0000000..8133755
--- /dev/null
+++ b/test/lib/shoulda/active_record_helpers.rb
@@ -0,0 +1,576 @@
+module ThoughtBot # :nodoc:
+ module Shoulda # :nodoc:
+ # = Macro test helpers for your active record models
+ #
+ # These helpers will test most of the validations and associations for your ActiveRecord models.
+ #
+ # class UserTest < Test::Unit::TestCase
+ # should_require_attributes :name, :phone_number
+ # should_not_allow_values_for :phone_number, "abcd", "1234"
+ # should_allow_values_for :phone_number, "(123) 456-7890"
+ #
+ # should_protect_attributes :password
+ #
+ # should_have_one :profile
+ # should_have_many :dogs
+ # should_have_many :messes, :through => :dogs
+ # should_belong_to :lover
+ # end
+ #
+ # For all of these helpers, the last parameter may be a hash of options.
+ #
+ module ActiveRecord
+ # Ensures that the model cannot be saved if one of the attributes listed is not present.
+ #
+ # Options:
+ # * :message - value the test expects to find in errors.on(:attribute).
+ # Regexp or string. Default = /blank/
+ #
+ # Example:
+ # should_require_attributes :name, :phone_number
+ #
+ def should_require_attributes(*attributes)
+ message = get_options!(attributes, :message)
+ message ||= /blank/
+ klass = model_class
+
+ attributes.each do |attribute|
+ should "require #{attribute} to be set" do
+ object = klass.new
+ object.send("#{attribute}=", nil)
+ assert !object.valid?, "#{klass.name} does not require #{attribute}."
+ assert object.errors.on(attribute), "#{klass.name} does not require #{attribute}."
+ assert_contains(object.errors.on(attribute), message)
+ end
+ end
+ end
+
+ # Ensures that the model cannot be saved if one of the attributes listed is not unique.
+ # Requires an existing record
+ #
+ # Options:
+ # * :message - value the test expects to find in errors.on(:attribute).
+ # Regexp or string. Default = /taken/
+ #
+ # Example:
+ # should_require_unique_attributes :keyword, :username
+ #
+ def should_require_unique_attributes(*attributes)
+ message, scope = get_options!(attributes, :message, :scoped_to)
+ message ||= /taken/
+
+ klass = model_class
+ attributes.each do |attribute|
+ attribute = attribute.to_sym
+ should "require unique value for #{attribute}#{" scoped to #{scope}" if scope}" do
+ assert existing = klass.find(:first), "Can't find first #{klass}"
+ object = klass.new
+
+ object.send(:"#{attribute}=", existing.send(attribute))
+ if scope
+ assert_respond_to object, :"#{scope}=", "#{klass.name} doesn't seem to have a #{scope} attribute."
+ object.send(:"#{scope}=", existing.send(scope))
+ end
+
+ assert !object.valid?, "#{klass.name} does not require a unique value for #{attribute}."
+ assert object.errors.on(attribute), "#{klass.name} does not require a unique value for #{attribute}."
+
+ assert_contains(object.errors.on(attribute), message)
+
+ # Now test that the object is valid when changing the scoped attribute
+ # TODO: There is a chance that we could change the scoped field
+ # to a value that's already taken. An alternative implementation
+ # could actually find all values for scope and create a unique
+ # one.
+ if scope
+ # Assume the scope is a foreign key if the field is nil
+ object.send(:"#{scope}=", existing.send(scope).nil? ? 1 : existing.send(scope).next)
+ object.errors.clear
+ object.valid?
+ assert_does_not_contain(object.errors.on(attribute), message,
+ "after :#{scope} set to #{object.send(scope.to_sym)}")
+ end
+ end
+ end
+ end
+
+ # Ensures that the attribute cannot be set on mass update.
+ # Requires an existing record.
+ #
+ # should_protect_attributes :password, :admin_flag
+ #
+ def should_protect_attributes(*attributes)
+ get_options!(attributes)
+ klass = model_class
+
+ attributes.each do |attribute|
+ attribute = attribute.to_sym
+ should "protect #{attribute} from mass updates" do
+ protected_attributes = klass.protected_attributes || []
+ assert protected_attributes.include?(attribute.to_s),
+ "#{klass} is protecting #{protected_attributes.to_a.to_sentence}, but not #{attribute}."
+ end
+ end
+ end
+
+ # Ensures that the attribute cannot be set to the given values
+ # Requires an existing record
+ #
+ # Options:
+ # * :message - value the test expects to find in errors.on(:attribute).
+ # Regexp or string. Default = /invalid/
+ #
+ # Example:
+ # should_not_allow_values_for :isbn, "bad 1", "bad 2"
+ #
+ def should_not_allow_values_for(attribute, *bad_values)
+ message = get_options!(bad_values, :message)
+ message ||= /invalid/
+ klass = model_class
+ bad_values.each do |v|
+ should "not allow #{attribute} to be set to #{v.inspect}" do
+ assert object = klass.find(:first), "Can't find first #{klass}"
+ object.send("#{attribute}=", v)
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{v}\""
+ assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\""
+ assert_contains(object.errors.on(attribute), message, "when set to \"#{v}\"")
+ end
+ end
+ end
+
+ # Ensures that the attribute can be set to the given values.
+ # Requires an existing record
+ #
+ # Example:
+ # should_allow_values_for :isbn, "isbn 1 2345 6789 0", "ISBN 1-2345-6789-0"
+ #
+ def should_allow_values_for(attribute, *good_values)
+ get_options!(good_values)
+ klass = model_class
+ good_values.each do |v|
+ should "allow #{attribute} to be set to #{v.inspect}" do
+ assert object = klass.find(:first), "Can't find first #{klass}"
+ object.send("#{attribute}=", v)
+ object.save
+ assert_nil object.errors.on(attribute)
+ end
+ end
+ end
+
+ # Ensures that the length of the attribute is in the given range
+ # Requires an existing record
+ #
+ # Options:
+ # * :short_message - value the test expects to find in errors.on(:attribute).
+ # Regexp or string. Default = /short/
+ # * :long_message - value the test expects to find in errors.on(:attribute).
+ # Regexp or string. Default = /long/
+ #
+ # Example:
+ # should_ensure_length_in_range :password, (6..20)
+ #
+ def should_ensure_length_in_range(attribute, range, opts = {})
+ short_message, long_message = get_options!([opts], :short_message, :long_message)
+ short_message ||= /short/
+ long_message ||= /long/
+
+ klass = model_class
+ min_length = range.first
+ max_length = range.last
+ same_length = (min_length == max_length)
+
+ if min_length > 0
+ should "not allow #{attribute} to be less than #{min_length} chars long" do
+ min_value = "x" * (min_length - 1)
+ assert object = klass.find(:first), "Can't find first #{klass}"
+ object.send("#{attribute}=", min_value)
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{min_value}\""
+ assert object.errors.on(attribute),
+ "There are no errors set on #{attribute} after being set to \"#{min_value}\""
+ assert_contains(object.errors.on(attribute), short_message, "when set to \"#{min_value}\"")
+ end
+ end
+
+ if min_length > 0
+ should "allow #{attribute} to be exactly #{min_length} chars long" do
+ min_value = "x" * min_length
+ assert object = klass.find(:first), "Can't find first #{klass}"
+ object.send("#{attribute}=", min_value)
+ object.save
+ assert_does_not_contain(object.errors.on(attribute), short_message, "when set to \"#{min_value}\"")
+ end
+ end
+
+ should "not allow #{attribute} to be more than #{max_length} chars long" do
+ max_value = "x" * (max_length + 1)
+ assert object = klass.find(:first), "Can't find first #{klass}"
+ object.send("#{attribute}=", max_value)
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{max_value}\""
+ assert object.errors.on(attribute),
+ "There are no errors set on #{attribute} after being set to \"#{max_value}\""
+ assert_contains(object.errors.on(attribute), long_message, "when set to \"#{max_value}\"")
+ end
+
+ unless same_length
+ should "allow #{attribute} to be exactly #{max_length} chars long" do
+ max_value = "x" * max_length
+ assert object = klass.find(:first), "Can't find first #{klass}"
+ object.send("#{attribute}=", max_value)
+ object.save
+ assert_does_not_contain(object.errors.on(attribute), long_message, "when set to \"#{max_value}\"")
+ end
+ end
+ end
+
+ # Ensures that the length of the attribute is at least a certain length
+ # Requires an existing record
+ #
+ # Options:
+ # * :short_message - value the test expects to find in errors.on(:attribute).
+ # Regexp or string. Default = /short/
+ #
+ # Example:
+ # should_ensure_length_at_least :name, 3
+ #
+ def should_ensure_length_at_least(attribute, min_length, opts = {})
+ short_message = get_options!([opts], :short_message)
+ short_message ||= /short/
+
+ klass = model_class
+
+ if min_length > 0
+ min_value = "x" * (min_length - 1)
+ should "not allow #{attribute} to be less than #{min_length} chars long" do
+ assert object = klass.find(:first), "Can't find first #{klass}"
+ object.send("#{attribute}=", min_value)
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{min_value}\""
+ assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{min_value}\""
+ assert_contains(object.errors.on(attribute), short_message, "when set to \"#{min_value}\"")
+ end
+ end
+ should "allow #{attribute} to be at least #{min_length} chars long" do
+ valid_value = "x" * (min_length)
+ assert object = klass.find(:first), "Can't find first #{klass}"
+ object.send("#{attribute}=", valid_value)
+ assert object.save, "Could not save #{klass} with #{attribute} set to \"#{valid_value}\""
+ end
+ end
+
+ # Ensure that the attribute is in the range specified
+ # Requires an existing record
+ #
+ # Options:
+ # * :low_message - value the test expects to find in errors.on(:attribute).
+ # Regexp or string. Default = /included/
+ # * :high_message - value the test expects to find in errors.on(:attribute).
+ # Regexp or string. Default = /included/
+ #
+ # Example:
+ # should_ensure_value_in_range :age, (0..100)
+ #
+ def should_ensure_value_in_range(attribute, range, opts = {})
+ low_message, high_message = get_options!([opts], :low_message, :high_message)
+ low_message ||= /included/
+ high_message ||= /included/
+
+ klass = model_class
+ min = range.first
+ max = range.last
+
+ should "not allow #{attribute} to be less than #{min}" do
+ v = min - 1
+ assert object = klass.find(:first), "Can't find first #{klass}"
+ object.send("#{attribute}=", v)
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{v}\""
+ assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\""
+ assert_contains(object.errors.on(attribute), low_message, "when set to \"#{v}\"")
+ end
+
+ should "allow #{attribute} to be #{min}" do
+ v = min
+ assert object = klass.find(:first), "Can't find first #{klass}"
+ object.send("#{attribute}=", v)
+ object.save
+ assert_does_not_contain(object.errors.on(attribute), low_message, "when set to \"#{v}\"")
+ end
+
+ should "not allow #{attribute} to be more than #{max}" do
+ v = max + 1
+ assert object = klass.find(:first), "Can't find first #{klass}"
+ object.send("#{attribute}=", v)
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{v}\""
+ assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\""
+ assert_contains(object.errors.on(attribute), high_message, "when set to \"#{v}\"")
+ end
+
+ should "allow #{attribute} to be #{max}" do
+ v = max
+ assert object = klass.find(:first), "Can't find first #{klass}"
+ object.send("#{attribute}=", v)
+ object.save
+ assert_does_not_contain(object.errors.on(attribute), high_message, "when set to \"#{v}\"")
+ end
+ end
+
+ # Ensure that the attribute is numeric
+ # Requires an existing record
+ #
+ # Options:
+ # * :message - value the test expects to find in errors.on(:attribute).
+ # Regexp or string. Default = /number/
+ #
+ # Example:
+ # should_only_allow_numeric_values_for :age
+ #
+ def should_only_allow_numeric_values_for(*attributes)
+ message = get_options!(attributes, :message)
+ message ||= /number/
+ klass = model_class
+ attributes.each do |attribute|
+ attribute = attribute.to_sym
+ should "only allow numeric values for #{attribute}" do
+ assert object = klass.find(:first), "Can't find first #{klass}"
+ object.send(:"#{attribute}=", "abcd")
+ assert !object.valid?, "Instance is still valid"
+ assert_contains(object.errors.on(attribute), message)
+ end
+ end
+ end
+
+ # Ensures that the has_many relationship exists. Will also test that the
+ # associated table has the required columns. Works with polymorphic
+ # associations.
+ #
+ # Options:
+ # * :through - association name for has_many :through
+ #
+ # Example:
+ # should_have_many :friends
+ # should_have_many :enemies, :through => :friends
+ #
+ def should_have_many(*associations)
+ through = get_options!(associations, :through)
+ klass = model_class
+ associations.each do |association|
+ name = "have many #{association}"
+ name += " through #{through}" if through
+ should name do
+ reflection = klass.reflect_on_association(association)
+ assert reflection, "#{klass.name} does not have any relationship to #{association}"
+ assert_equal :has_many, reflection.macro
+
+ if through
+ through_reflection = klass.reflect_on_association(through)
+ assert through_reflection, "#{klass.name} does not have any relationship to #{through}"
+ assert_equal(through, reflection.options[:through])
+ end
+
+ unless reflection.options[:through]
+ # This is not a through association, so check for the existence of the foreign key on the other table
+ if reflection.options[:foreign_key]
+ fk = reflection.options[:foreign_key]
+ elsif reflection.options[:as]
+ fk = reflection.options[:as].to_s.foreign_key
+ else
+ fk = reflection.primary_key_name
+ end
+ associated_klass = (reflection.options[:class_name] || association.to_s.classify).constantize
+ assert associated_klass.column_names.include?(fk.to_s), "#{associated_klass.name} does not have a #{fk} foreign key."
+ end
+ end
+ end
+ end
+
+ # Ensure that the has_one relationship exists. Will also test that the
+ # associated table has the required columns. Works with polymorphic
+ # associations.
+ #
+ # Example:
+ # should_have_one :god # unless hindu
+ #
+ def should_have_one(*associations)
+ get_options!(associations)
+ klass = model_class
+ associations.each do |association|
+ should "have one #{association}" do
+ reflection = klass.reflect_on_association(association)
+ assert reflection, "#{klass.name} does not have any relationship to #{association}"
+ assert_equal :has_one, reflection.macro
+
+ associated_klass = (reflection.options[:class_name] || association.to_s.camelize).constantize
+
+ if reflection.options[:foreign_key]
+ fk = reflection.options[:foreign_key]
+ elsif reflection.options[:as]
+ fk = reflection.options[:as].to_s.foreign_key
+ fk_type = fk.gsub(/_id$/, '_type')
+ assert associated_klass.column_names.include?(fk_type),
+ "#{associated_klass.name} does not have a #{fk_type} column."
+ else
+ fk = klass.name.foreign_key
+ end
+ assert associated_klass.column_names.include?(fk.to_s),
+ "#{associated_klass.name} does not have a #{fk} foreign key."
+ end
+ end
+ end
+
+ # Ensures that the has_and_belongs_to_many relationship exists, and that the join
+ # table is in place.
+ #
+ # should_have_and_belong_to_many :posts, :cars
+ #
+ def should_have_and_belong_to_many(*associations)
+ get_options!(associations)
+ klass = model_class
+
+ associations.each do |association|
+ should "should have and belong to many #{association}" do
+ reflection = klass.reflect_on_association(association)
+ assert reflection, "#{klass.name} does not have any relationship to #{association}"
+ assert_equal :has_and_belongs_to_many, reflection.macro
+ table = reflection.options[:join_table]
+ assert ::ActiveRecord::Base.connection.tables.include?(table), "table #{table} doesn't exist"
+ end
+ end
+ end
+
+ # Ensure that the belongs_to relationship exists.
+ #
+ # should_belong_to :parent
+ #
+ def should_belong_to(*associations)
+ get_options!(associations)
+ klass = model_class
+ associations.each do |association|
+ should "belong_to #{association}" do
+ reflection = klass.reflect_on_association(association)
+ assert reflection, "#{klass.name} does not have any relationship to #{association}"
+ assert_equal :belongs_to, reflection.macro
+
+ unless reflection.options[:polymorphic]
+ associated_klass = (reflection.options[:class_name] || association.to_s.classify).constantize
+ fk = reflection.options[:foreign_key] || reflection.primary_key_name
+ assert klass.column_names.include?(fk.to_s), "#{klass.name} does not have a #{fk} foreign key."
+ end
+ end
+ end
+ end
+
+ # Ensure that the given class methods are defined on the model.
+ #
+ # should_have_class_methods :find, :destroy
+ #
+ def should_have_class_methods(*methods)
+ get_options!(methods)
+ klass = model_class
+ methods.each do |method|
+ should "respond to class method ##{method}" do
+ assert_respond_to klass, method, "#{klass.name} does not have class method #{method}"
+ end
+ end
+ end
+
+ # Ensure that the given instance methods are defined on the model.
+ #
+ # should_have_instance_methods :email, :name, :name=
+ #
+ def should_have_instance_methods(*methods)
+ get_options!(methods)
+ klass = model_class
+ methods.each do |method|
+ should "respond to instance method ##{method}" do
+ assert_respond_to klass.new, method, "#{klass.name} does not have instance method #{method}"
+ end
+ end
+ end
+
+ # Ensure that the given columns are defined on the models backing SQL table.
+ #
+ # should_have_db_columns :id, :email, :name, :created_at
+ #
+ def should_have_db_columns(*columns)
+ column_type = get_options!(columns, :type)
+ klass = model_class
+ columns.each do |name|
+ test_name = "have column #{name}"
+ test_name += " of type #{column_type}" if column_type
+ should test_name do
+ column = klass.columns.detect {|c| c.name == name.to_s }
+ assert column, "#{klass.name} does not have column #{name}"
+ end
+ end
+ end
+
+ # Ensure that the given column is defined on the models backing SQL table. The options are the same as
+ # the instance variables defined on the column definition: :precision, :limit, :default, :null,
+ # :primary, :type, :scale, and :sql_type.
+ #
+ # should_have_db_column :email, :type => "string", :default => nil, :precision => nil, :limit => 255,
+ # :null => true, :primary => false, :scale => nil, :sql_type => 'varchar(255)'
+ #
+ def should_have_db_column(name, opts = {})
+ klass = model_class
+ test_name = "have column named :#{name}"
+ test_name += " with options " + opts.inspect unless opts.empty?
+ should test_name do
+ column = klass.columns.detect {|c| c.name == name.to_s }
+ assert column, "#{klass.name} does not have column #{name}"
+ opts.each do |k, v|
+ assert_equal column.instance_variable_get("@#{k}").to_s, v.to_s, ":#{name} column on table for #{klass} does not match option :#{k}"
+ end
+ end
+ end
+
+ # Ensures that there are DB indices on the given columns or tuples of columns.
+ # Also aliased to should_have_index for readability
+ #
+ # should_have_indices :email, :name, [:commentable_type, :commentable_id]
+ # should_have_index :age
+ #
+ def should_have_indices(*columns)
+ table = model_class.name.tableize
+ indices = ::ActiveRecord::Base.connection.indexes(table).map(&:columns)
+
+ columns.each do |column|
+ should "have index on #{table} for #{column.inspect}" do
+ columns = [column].flatten.map(&:to_s)
+ assert_contains(indices, columns)
+ end
+ end
+ end
+
+ alias_method :should_have_index, :should_have_indices
+
+ # Ensures that the model cannot be saved if one of the attributes listed is not accepted.
+ #
+ # Options:
+ # * :message - value the test expects to find in errors.on(:attribute).
+ # Regexp or string. Default = /must be accepted/
+ #
+ # Example:
+ # should_require_acceptance_of :eula
+ #
+ def should_require_acceptance_of(*attributes)
+ message = get_options!(attributes, :message)
+ message ||= /must be accepted/
+ klass = model_class
+
+ attributes.each do |attribute|
+ should "require #{attribute} to be accepted" do
+ object = klass.new
+ object.send("#{attribute}=", false)
+
+ assert !object.valid?, "#{klass.name} does not require acceptance of #{attribute}."
+ assert object.errors.on(attribute), "#{klass.name} does not require acceptance of #{attribute}."
+ assert_contains(object.errors.on(attribute), message)
+ end
+ end
+ end
+
+ private
+
+ include ThoughtBot::Shoulda::Private
+ end
+ end
+end
diff --git a/test/lib/shoulda/color.rb b/test/lib/shoulda/color.rb
new file mode 100755
index 0000000..1ccfad2
--- /dev/null
+++ b/test/lib/shoulda/color.rb
@@ -0,0 +1,77 @@
+require 'test/unit/ui/console/testrunner'
+
+# Completely stolen from redgreen gem
+#
+# Adds colored output to your tests. Specify color: true in
+# your ~/.shoulda.conf file to enable.
+#
+# *Bug*: for some reason, this adds another line of output to the end of
+# every rake task, as though there was another (empty) set of tests.
+# A fix would be most welcome.
+#
+module ThoughtBot::Shoulda::Color
+ COLORS = { :clear => 0, :red => 31, :green => 32, :yellow => 33 } # :nodoc:
+ def self.method_missing(color_name, *args) # :nodoc:
+ color(color_name) + args.first + color(:clear)
+ end
+ def self.color(color) # :nodoc:
+ "\e[#{COLORS[color.to_sym]}m"
+ end
+end
+
+module Test # :nodoc:
+ module Unit # :nodoc:
+ class TestResult # :nodoc:
+ alias :old_to_s :to_s
+ def to_s
+ if old_to_s =~ /\d+ tests, \d+ assertions, (\d+) failures, (\d+) errors/
+ ThoughtBot::Shoulda::Color.send($1.to_i != 0 || $2.to_i != 0 ? :red : :green, $&)
+ end
+ end
+ end
+
+ class AutoRunner # :nodoc:
+ alias :old_initialize :initialize
+ def initialize(standalone)
+ old_initialize(standalone)
+ @runner = proc do |r|
+ Test::Unit::UI::Console::RedGreenTestRunner
+ end
+ end
+ end
+
+ class Failure # :nodoc:
+ alias :old_long_display :long_display
+ def long_display
+ # old_long_display.sub('Failure', ThoughtBot::Shoulda::Color.red('Failure'))
+ ThoughtBot::Shoulda::Color.red(old_long_display)
+ end
+ end
+
+ class Error # :nodoc:
+ alias :old_long_display :long_display
+ def long_display
+ # old_long_display.sub('Error', ThoughtBot::Shoulda::Color.yellow('Error'))
+ ThoughtBot::Shoulda::Color.yellow(old_long_display)
+ end
+ end
+
+ module UI # :nodoc:
+ module Console # :nodoc:
+ class RedGreenTestRunner < Test::Unit::UI::Console::TestRunner # :nodoc:
+ def output_single(something, level=NORMAL)
+ return unless (output?(level))
+ something = case something
+ when '.' then ThoughtBot::Shoulda::Color.green('.')
+ when 'F' then ThoughtBot::Shoulda::Color.red("F")
+ when 'E' then ThoughtBot::Shoulda::Color.yellow("E")
+ else something
+ end
+ @io.write(something)
+ @io.flush
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/test/lib/shoulda/controller_tests/controller_tests.rb b/test/lib/shoulda/controller_tests/controller_tests.rb
new file mode 100755
index 0000000..f0059c0
--- /dev/null
+++ b/test/lib/shoulda/controller_tests/controller_tests.rb
@@ -0,0 +1,467 @@
+module ThoughtBot # :nodoc:
+ module Shoulda # :nodoc:
+ module Controller
+ def self.included(other) # :nodoc:
+ other.class_eval do
+ extend ThoughtBot::Shoulda::Controller::ClassMethods
+ include ThoughtBot::Shoulda::Controller::InstanceMethods
+ ThoughtBot::Shoulda::Controller::ClassMethods::VALID_FORMATS.each do |format|
+ include "ThoughtBot::Shoulda::Controller::#{format.to_s.upcase}".constantize
+ end
+ end
+ end
+
+ # = Macro test helpers for your controllers
+ #
+ # By using the macro helpers you can quickly and easily create concise and easy to read test suites.
+ #
+ # This code segment:
+ # context "on GET to :show for first record" do
+ # setup do
+ # get :show, :id => 1
+ # end
+ #
+ # should_assign_to :user
+ # should_respond_with :success
+ # should_render_template :show
+ # should_not_set_the_flash
+ #
+ # should "do something else really cool" do
+ # assert_equal 1, assigns(:user).id
+ # end
+ # end
+ #
+ # Would produce 5 tests for the +show+ action
+ #
+ # Furthermore, the should_be_restful helper will create an entire set of tests which will verify that your
+ # controller responds restfully to a variety of requested formats.
+ module ClassMethods
+ # Formats tested by #should_be_restful. Defaults to [:html, :xml]
+ VALID_FORMATS = Dir.glob(File.join(File.dirname(__FILE__), 'formats', '*.rb')).map { |f| File.basename(f, '.rb') }.map(&:to_sym) # :doc:
+ VALID_FORMATS.each {|f| require "shoulda/controller_tests/formats/#{f}.rb"}
+
+ # Actions tested by #should_be_restful
+ VALID_ACTIONS = [:index, :show, :new, :edit, :create, :update, :destroy] # :doc:
+
+ # A ResourceOptions object is passed into should_be_restful in order to configure the tests for your controller.
+ #
+ # Example:
+ # class UsersControllerTest < Test::Unit::TestCase
+ # load_all_fixtures
+ #
+ # def setup
+ # ...normal setup code...
+ # @user = User.find(:first)
+ # end
+ #
+ # should_be_restful do |resource|
+ # resource.identifier = :id
+ # resource.klass = User
+ # resource.object = :user
+ # resource.parent = []
+ # resource.actions = [:index, :show, :new, :edit, :update, :create, :destroy]
+ # resource.formats = [:html, :xml]
+ #
+ # resource.create.params = { :name => "bob", :email => 'bob@bob.com', :age => 13}
+ # resource.update.params = { :name => "sue" }
+ #
+ # resource.create.redirect = "user_url(@user)"
+ # resource.update.redirect = "user_url(@user)"
+ # resource.destroy.redirect = "users_url"
+ #
+ # resource.create.flash = /created/i
+ # resource.update.flash = /updated/i
+ # resource.destroy.flash = /removed/i
+ # end
+ # end
+ #
+ # Whenever possible, the resource attributes will be set to sensible defaults.
+ #
+ class ResourceOptions
+ # Configuration options for the create, update, destroy actions under should_be_restful
+ class ActionOptions
+ # String evaled to get the target of the redirection.
+ # All of the instance variables set by the controller will be available to the
+ # evaled code.
+ #
+ # Example:
+ # resource.create.redirect = "user_url(@user.company, @user)"
+ #
+ # Defaults to a generated url based on the name of the controller, the action, and the resource.parents list.
+ attr_accessor :redirect
+
+ # String or Regexp describing a value expected in the flash. Will match against any flash key.
+ #
+ # Defaults:
+ # destroy:: /removed/
+ # create:: /created/
+ # update:: /updated/
+ attr_accessor :flash
+
+ # Hash describing the params that should be sent in with this action.
+ attr_accessor :params
+ end
+
+ # Configuration options for the denied actions under should_be_restful
+ #
+ # Example:
+ # context "The public" do
+ # setup do
+ # @request.session[:logged_in] = false
+ # end
+ #
+ # should_be_restful do |resource|
+ # resource.parent = :user
+ #
+ # resource.denied.actions = [:index, :show, :edit, :new, :create, :update, :destroy]
+ # resource.denied.flash = /get outta here/i
+ # resource.denied.redirect = 'new_session_url'
+ # end
+ # end
+ #
+ class DeniedOptions
+ # String evaled to get the target of the redirection.
+ # All of the instance variables set by the controller will be available to the
+ # evaled code.
+ #
+ # Example:
+ # resource.create.redirect = "user_url(@user.company, @user)"
+ attr_accessor :redirect
+
+ # String or Regexp describing a value expected in the flash. Will match against any flash key.
+ #
+ # Example:
+ # resource.create.flash = /created/
+ attr_accessor :flash
+
+ # Actions that should be denied (only used by resource.denied). Note that these actions will
+ # only be tested if they are also listed in +resource.actions+
+ # The special value of :all will deny all of the REST actions.
+ attr_accessor :actions
+ end
+
+ # Name of key in params that references the primary key.
+ # Will almost always be :id (default), unless you are using a plugin or have patched rails.
+ attr_accessor :identifier
+
+ # Name of the ActiveRecord class this resource is responsible for. Automatically determined from
+ # test class if not explicitly set. UserTest => "User"
+ attr_accessor :klass
+
+ # Name of the instantiated ActiveRecord object that should be used by some of the tests.
+ # Defaults to the underscored name of the AR class. CompanyManager => :company_manager
+ attr_accessor :object
+
+ # Name of the parent AR objects. Can be set as parent= or parents=, and can take either
+ # the name of the parent resource (if there's only one), or an array of names (if there's
+ # more than one).
+ #
+ # Example:
+ # # in the routes...
+ # map.resources :companies do
+ # map.resources :people do
+ # map.resources :limbs
+ # end
+ # end
+ #
+ # # in the tests...
+ # class PeopleControllerTest < Test::Unit::TestCase
+ # should_be_restful do |resource|
+ # resource.parent = :companies
+ # end
+ # end
+ #
+ # class LimbsControllerTest < Test::Unit::TestCase
+ # should_be_restful do |resource|
+ # resource.parents = [:companies, :people]
+ # end
+ # end
+ attr_accessor :parent
+ alias parents parent
+ alias parents= parent=
+
+ # Actions that should be tested. Must be a subset of VALID_ACTIONS (default).
+ # Tests for each actionw will only be generated if the action is listed here.
+ # The special value of :all will test all of the REST actions.
+ #
+ # Example (for a read-only controller):
+ # resource.actions = [:show, :index]
+ attr_accessor :actions
+
+ # Formats that should be tested. Must be a subset of VALID_FORMATS (default).
+ # Each action will be tested against the formats listed here. The special value
+ # of :all will test all of the supported formats.
+ #
+ # Example:
+ # resource.actions = [:html, :xml]
+ attr_accessor :formats
+
+ # ActionOptions object specifying options for the create action.
+ attr_accessor :create
+
+ # ActionOptions object specifying options for the update action.
+ attr_accessor :update
+
+ # ActionOptions object specifying options for the desrtoy action.
+ attr_accessor :destroy
+
+ # DeniedOptions object specifying which actions should return deny a request, and what should happen in that case.
+ attr_accessor :denied
+
+ def initialize # :nodoc:
+ @create = ActionOptions.new
+ @update = ActionOptions.new
+ @destroy = ActionOptions.new
+ @denied = DeniedOptions.new
+
+ @create.flash ||= /created/i
+ @update.flash ||= /updated/i
+ @destroy.flash ||= /removed/i
+ @denied.flash ||= /denied/i
+
+ @create.params ||= {}
+ @update.params ||= {}
+
+ @actions = VALID_ACTIONS
+ @formats = VALID_FORMATS
+ @denied.actions = []
+ end
+
+ def normalize!(target) # :nodoc:
+ @denied.actions = VALID_ACTIONS if @denied.actions == :all
+ @actions = VALID_ACTIONS if @actions == :all
+ @formats = VALID_FORMATS if @formats == :all
+
+ @denied.actions = @denied.actions.map(&:to_sym)
+ @actions = @actions.map(&:to_sym)
+ @formats = @formats.map(&:to_sym)
+
+ ensure_valid_members(@actions, VALID_ACTIONS, 'actions')
+ ensure_valid_members(@denied.actions, VALID_ACTIONS, 'denied.actions')
+ ensure_valid_members(@formats, VALID_FORMATS, 'formats')
+
+ @identifier ||= :id
+ @klass ||= target.name.gsub(/ControllerTest$/, '').singularize.constantize
+ @object ||= @klass.name.tableize.singularize
+ @parent ||= []
+ @parent = [@parent] unless @parent.is_a? Array
+
+ collection_helper = [@parent, @object.to_s.pluralize, 'url'].flatten.join('_')
+ collection_args = @parent.map {|n| "@#{object}.#{n}"}.join(', ')
+ @destroy.redirect ||= "#{collection_helper}(#{collection_args})"
+
+ member_helper = [@parent, @object, 'url'].flatten.join('_')
+ member_args = [@parent.map {|n| "@#{object}.#{n}"}, "@#{object}"].flatten.join(', ')
+ @create.redirect ||= "#{member_helper}(#{member_args})"
+ @update.redirect ||= "#{member_helper}(#{member_args})"
+ @denied.redirect ||= "new_session_url"
+ end
+
+ private
+
+ def ensure_valid_members(ary, valid_members, name) # :nodoc:
+ invalid = ary - valid_members
+ raise ArgumentError, "Unsupported #{name}: #{invalid.inspect}" unless invalid.empty?
+ end
+ end
+
+ # :section: should_be_restful
+ # Generates a full suite of tests for a restful controller.
+ #
+ # The following definition will generate tests for the +index+, +show+, +new+,
+ # +edit+, +create+, +update+ and +destroy+ actions, in both +html+ and +xml+ formats:
+ #
+ # should_be_restful do |resource|
+ # resource.parent = :user
+ #
+ # resource.create.params = { :title => "first post", :body => 'blah blah blah'}
+ # resource.update.params = { :title => "changed" }
+ # end
+ #
+ # This generates about 40 tests, all of the format:
+ # "on GET to :show should assign @user."
+ # "on GET to :show should not set the flash."
+ # "on GET to :show should render 'show' template."
+ # "on GET to :show should respond with success."
+ # "on GET to :show as xml should assign @user."
+ # "on GET to :show as xml should have ContentType set to 'application/xml'."
+ # "on GET to :show as xml should respond with success."
+ # "on GET to :show as xml should return