aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/test/cases
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/test/cases')
-rw-r--r--activerecord/test/cases/aaa_create_tables_test.rb72
-rw-r--r--activerecord/test/cases/active_schema_test_mysql.rb46
-rw-r--r--activerecord/test/cases/adapter_test.rb106
-rw-r--r--activerecord/test/cases/adapter_test_sqlserver.rb95
-rw-r--r--activerecord/test/cases/aggregations_test.rb128
-rw-r--r--activerecord/test/cases/ar_schema_test.rb33
-rw-r--r--activerecord/test/cases/associations/callbacks_test.rb146
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb110
-rw-r--r--activerecord/test/cases/associations/eager_singularization_test.rb145
-rw-r--r--activerecord/test/cases/associations/eager_test.rb447
-rw-r--r--activerecord/test/cases/associations/extension_test.rb47
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb88
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb559
-rwxr-xr-xactiverecord/test/cases/associations_test.rb2177
-rwxr-xr-xactiverecord/test/cases/attribute_methods_test.rb146
-rwxr-xr-xactiverecord/test/cases/base_test.rb1831
-rw-r--r--activerecord/test/cases/binary_test.rb32
-rw-r--r--activerecord/test/cases/calculations_test.rb251
-rw-r--r--activerecord/test/cases/callbacks_test.rb400
-rw-r--r--activerecord/test/cases/class_inheritable_attributes_test.rb32
-rw-r--r--activerecord/test/cases/column_alias_test.rb17
-rw-r--r--activerecord/test/cases/connection_test_firebird.rb8
-rw-r--r--activerecord/test/cases/connection_test_mysql.rb30
-rw-r--r--activerecord/test/cases/copy_table_test_sqlite.rb69
-rw-r--r--activerecord/test/cases/datatype_test_postgresql.rb203
-rw-r--r--activerecord/test/cases/date_time_test.rb37
-rw-r--r--activerecord/test/cases/default_test_firebird.rb16
-rw-r--r--activerecord/test/cases/defaults_test.rb67
-rwxr-xr-xactiverecord/test/cases/deprecated_finder_test.rb30
-rw-r--r--activerecord/test/cases/finder_test.rb650
-rwxr-xr-xactiverecord/test/cases/fixtures_test.rb594
-rwxr-xr-xactiverecord/test/cases/inheritance_test.rb211
-rw-r--r--activerecord/test/cases/json_serialization_test.rb180
-rwxr-xr-xactiverecord/test/cases/lifecycle_test.rb137
-rw-r--r--activerecord/test/cases/locking_test.rb282
-rw-r--r--activerecord/test/cases/method_scoping_test.rb416
-rw-r--r--activerecord/test/cases/migration_test.rb987
-rw-r--r--activerecord/test/cases/migration_test_firebird.rb124
-rw-r--r--activerecord/test/cases/mixin_test.rb95
-rw-r--r--activerecord/test/cases/modules_test.rb34
-rw-r--r--activerecord/test/cases/multiple_db_test.rb60
-rw-r--r--activerecord/test/cases/pk_test.rb101
-rw-r--r--activerecord/test/cases/query_cache_test.rb124
-rwxr-xr-xactiverecord/test/cases/readonly_test.rb107
-rw-r--r--activerecord/test/cases/reflection_test.rb175
-rw-r--r--activerecord/test/cases/reserved_word_test_mysql.rb177
-rw-r--r--activerecord/test/cases/schema_authorization_test_postgresql.rb75
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb131
-rw-r--r--activerecord/test/cases/schema_test_postgresql.rb64
-rw-r--r--activerecord/test/cases/serialization_test.rb47
-rw-r--r--activerecord/test/cases/synonym_test_oracle.rb17
-rw-r--r--activerecord/test/cases/table_name_test_sqlserver.rb23
-rw-r--r--activerecord/test/cases/threaded_connections_test.rb48
-rw-r--r--activerecord/test/cases/transactions_test.rb281
-rwxr-xr-xactiverecord/test/cases/unconnected_test.rb32
-rwxr-xr-xactiverecord/test/cases/validations_test.rb1407
-rw-r--r--activerecord/test/cases/xml_serialization_test.rb202
57 files changed, 14149 insertions, 0 deletions
diff --git a/activerecord/test/cases/aaa_create_tables_test.rb b/activerecord/test/cases/aaa_create_tables_test.rb
new file mode 100644
index 0000000000..56c9cabac1
--- /dev/null
+++ b/activerecord/test/cases/aaa_create_tables_test.rb
@@ -0,0 +1,72 @@
+# The filename begins with "aaa" to ensure this is the first test.
+require 'abstract_unit'
+
+class AAACreateTablesTest < ActiveSupport::TestCase
+ self.use_transactional_fixtures = false
+
+ def setup
+ @base_path = "#{File.dirname(__FILE__)}/fixtures/db_definitions"
+ end
+
+ def test_drop_and_create_main_tables
+ recreate ActiveRecord::Base unless use_migrations?
+ assert true
+ end
+
+ def test_load_schema
+ if ActiveRecord::Base.connection.supports_migrations?
+ eval(File.read("#{File.dirname(__FILE__)}/fixtures/db_definitions/schema.rb"))
+ else
+ recreate ActiveRecord::Base, '3'
+ end
+ assert true
+ end
+
+ def test_drop_and_create_courses_table
+ if Course.connection.supports_migrations?
+ eval(File.read("#{File.dirname(__FILE__)}/fixtures/db_definitions/schema2.rb"))
+ end
+ recreate Course, '2' unless use_migrations_for_courses?
+ assert true
+ end
+
+ private
+ def use_migrations?
+ unittest_sql_filename = ActiveRecord::Base.connection.adapter_name.downcase + ".sql"
+ not File.exist? "#{@base_path}/#{unittest_sql_filename}"
+ end
+
+ def use_migrations_for_courses?
+ unittest2_sql_filename = ActiveRecord::Base.connection.adapter_name.downcase + "2.sql"
+ not File.exist? "#{@base_path}/#{unittest2_sql_filename}"
+ end
+
+ def recreate(base, suffix = nil)
+ connection = base.connection
+ adapter_name = connection.adapter_name.downcase + suffix.to_s
+ execute_sql_file "#{@base_path}/#{adapter_name}.drop.sql", connection
+ execute_sql_file "#{@base_path}/#{adapter_name}.sql", connection
+ end
+
+ def execute_sql_file(path, connection)
+ # OpenBase has a different format for sql files
+ if current_adapter?(:OpenBaseAdapter) then
+ File.read(path).split("go").each_with_index do |sql, i|
+ begin
+ # OpenBase does not support comments embedded in sql
+ connection.execute(sql,"SQL statement ##{i}") unless sql.blank?
+ rescue ActiveRecord::StatementInvalid
+ #$stderr.puts "warning: #{$!}"
+ end
+ end
+ else
+ File.read(path).split(';').each_with_index do |sql, i|
+ begin
+ connection.execute("\n\n-- statement ##{i}\n#{sql}\n") unless sql.blank?
+ rescue ActiveRecord::StatementInvalid
+ #$stderr.puts "warning: #{$!}"
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/active_schema_test_mysql.rb b/activerecord/test/cases/active_schema_test_mysql.rb
new file mode 100644
index 0000000000..2611153fda
--- /dev/null
+++ b/activerecord/test/cases/active_schema_test_mysql.rb
@@ -0,0 +1,46 @@
+require 'abstract_unit'
+
+class ActiveSchemaTest < ActiveSupport::TestCase
+ def setup
+ ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
+ alias_method :execute_without_stub, :execute
+ def execute(sql, name = nil) return sql end
+ end
+ end
+
+ def teardown
+ ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
+ remove_method :execute
+ alias_method :execute, :execute_without_stub
+ end
+ end
+
+ def test_drop_table
+ assert_equal "DROP TABLE `people`", drop_table(:people)
+ end
+
+ if current_adapter?(:MysqlAdapter)
+ def test_create_mysql_database_with_encoding
+ assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt)
+ assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'})
+ assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci})
+ end
+ end
+
+ def test_add_column
+ assert_equal "ALTER TABLE `people` ADD `last_name` varchar(255)", add_column(:people, :last_name, :string)
+ end
+
+ def test_add_column_with_limit
+ assert_equal "ALTER TABLE `people` ADD `key` varchar(32)", add_column(:people, :key, :string, :limit => 32)
+ end
+
+ def test_drop_table_with_specific_database
+ assert_equal "DROP TABLE `otherdb`.`people`", drop_table('otherdb.people')
+ end
+
+ private
+ def method_missing(method_symbol, *arguments)
+ ActiveRecord::Base.connection.send(method_symbol, *arguments)
+ end
+end
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
new file mode 100644
index 0000000000..d93a99d7cd
--- /dev/null
+++ b/activerecord/test/cases/adapter_test.rb
@@ -0,0 +1,106 @@
+require 'abstract_unit'
+
+class AdapterTest < ActiveSupport::TestCase
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def test_tables
+ if @connection.respond_to?(:tables)
+ tables = @connection.tables
+ assert tables.include?("accounts")
+ assert tables.include?("authors")
+ assert tables.include?("tasks")
+ assert tables.include?("topics")
+ else
+ warn "#{@connection.class} does not respond to #tables"
+ end
+ end
+
+ def test_indexes
+ idx_name = "accounts_idx"
+
+ if @connection.respond_to?(:indexes)
+ indexes = @connection.indexes("accounts")
+ assert indexes.empty?
+
+ @connection.add_index :accounts, :firm_id, :name => idx_name
+ indexes = @connection.indexes("accounts")
+ assert_equal "accounts", indexes.first.table
+ # OpenBase does not have the concept of a named index
+ # Indexes are merely properties of columns.
+ assert_equal idx_name, indexes.first.name unless current_adapter?(:OpenBaseAdapter)
+ assert !indexes.first.unique
+ assert_equal ["firm_id"], indexes.first.columns
+ else
+ warn "#{@connection.class} does not respond to #indexes"
+ end
+
+ ensure
+ @connection.remove_index(:accounts, :name => idx_name) rescue nil
+ end
+
+ def test_current_database
+ if @connection.respond_to?(:current_database)
+ assert_equal ENV['ARUNIT_DB_NAME'] || "activerecord_unittest", @connection.current_database
+ end
+ end
+
+ if current_adapter?(:MysqlAdapter)
+ def test_charset
+ assert_not_nil @connection.charset
+ assert_not_equal 'character_set_database', @connection.charset
+ assert_equal @connection.show_variable('character_set_database'), @connection.charset
+ end
+
+ def test_collation
+ assert_not_nil @connection.collation
+ assert_not_equal 'collation_database', @connection.collation
+ assert_equal @connection.show_variable('collation_database'), @connection.collation
+ end
+
+ def test_show_nonexistent_variable_returns_nil
+ assert_nil @connection.show_variable('foo_bar_baz')
+ end
+ end
+
+ def test_table_alias
+ def @connection.test_table_alias_length() 10; end
+ class << @connection
+ alias_method :old_table_alias_length, :table_alias_length
+ alias_method :table_alias_length, :test_table_alias_length
+ end
+
+ assert_equal 'posts', @connection.table_alias_for('posts')
+ assert_equal 'posts_comm', @connection.table_alias_for('posts_comments')
+ assert_equal 'dbo_posts', @connection.table_alias_for('dbo.posts')
+
+ class << @connection
+ remove_method :table_alias_length
+ alias_method :table_alias_length, :old_table_alias_length
+ end
+ end
+
+ # test resetting sequences in odd tables in postgreSQL
+ if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!)
+ require 'fixtures/movie'
+ require 'fixtures/subscriber'
+
+ def test_reset_empty_table_with_custom_pk
+ Movie.delete_all
+ Movie.connection.reset_pk_sequence! 'movies'
+ assert_equal 1, Movie.create(:name => 'fight club').id
+ end
+
+ if ActiveRecord::Base.connection.adapter_name != "FrontBase"
+ def test_reset_table_with_non_integer_pk
+ Subscriber.delete_all
+ Subscriber.connection.reset_pk_sequence! 'subscribers'
+ sub = Subscriber.new(:name => 'robert drake')
+ sub.id = 'bob drake'
+ assert_nothing_raised { sub.save! }
+ end
+ end
+ end
+
+end
diff --git a/activerecord/test/cases/adapter_test_sqlserver.rb b/activerecord/test/cases/adapter_test_sqlserver.rb
new file mode 100644
index 0000000000..494fc98a2f
--- /dev/null
+++ b/activerecord/test/cases/adapter_test_sqlserver.rb
@@ -0,0 +1,95 @@
+require 'abstract_unit'
+require 'fixtures/default'
+require 'fixtures/post'
+require 'fixtures/task'
+
+class SqlServerAdapterTest < ActiveSupport::TestCase
+ class TableWithRealColumn < ActiveRecord::Base; end
+
+ fixtures :posts, :tasks
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def teardown
+ @connection.execute("SET LANGUAGE us_english") rescue nil
+ end
+
+ def test_real_column_has_float_type
+ assert_equal :float, TableWithRealColumn.columns_hash["real_number"].type
+ end
+
+ # SQL Server 2000 has a bug where some unambiguous date formats are not
+ # correctly identified if the session language is set to german
+ def test_date_insertion_when_language_is_german
+ @connection.execute("SET LANGUAGE deutsch")
+
+ assert_nothing_raised do
+ Task.create(:starting => Time.utc(2000, 1, 31, 5, 42, 0), :ending => Date.new(2006, 12, 31))
+ end
+ end
+
+ def test_indexes_with_descending_order
+ # Make sure we have an index with descending order
+ @connection.execute "CREATE INDEX idx_credit_limit ON accounts (credit_limit DESC)" rescue nil
+ assert_equal ["credit_limit"], @connection.indexes('accounts').first.columns
+ ensure
+ @connection.execute "DROP INDEX accounts.idx_credit_limit"
+ end
+
+ def test_execute_without_block_closes_statement
+ assert_all_statements_used_are_closed do
+ @connection.execute("SELECT 1")
+ end
+ end
+
+ def test_execute_with_block_closes_statement
+ assert_all_statements_used_are_closed do
+ @connection.execute("SELECT 1") do |sth|
+ assert !sth.finished?, "Statement should still be alive within block"
+ end
+ end
+ end
+
+ def test_insert_with_identity_closes_statement
+ assert_all_statements_used_are_closed do
+ @connection.insert("INSERT INTO accounts ([id], [firm_id],[credit_limit]) values (999, 1, 50)")
+ end
+ end
+
+ def test_insert_without_identity_closes_statement
+ assert_all_statements_used_are_closed do
+ @connection.insert("INSERT INTO accounts ([firm_id],[credit_limit]) values (1, 50)")
+ end
+ end
+
+ def test_active_closes_statement
+ assert_all_statements_used_are_closed do
+ @connection.active?
+ end
+ end
+
+ def assert_all_statements_used_are_closed(&block)
+ existing_handles = []
+ ObjectSpace.each_object(DBI::StatementHandle) {|handle| existing_handles << handle}
+ GC.disable
+
+ yield
+
+ used_handles = []
+ ObjectSpace.each_object(DBI::StatementHandle) {|handle| used_handles << handle unless existing_handles.include? handle}
+
+ assert_block "No statements were used within given block" do
+ used_handles.size > 0
+ end
+
+ ObjectSpace.each_object(DBI::StatementHandle) do |handle|
+ assert_block "Statement should have been closed within given block" do
+ handle.finished?
+ end
+ end
+ ensure
+ GC.enable
+ end
+end
diff --git a/activerecord/test/cases/aggregations_test.rb b/activerecord/test/cases/aggregations_test.rb
new file mode 100644
index 0000000000..66fa46b3f0
--- /dev/null
+++ b/activerecord/test/cases/aggregations_test.rb
@@ -0,0 +1,128 @@
+require 'abstract_unit'
+require 'models/customer'
+
+class AggregationsTest < ActiveSupport::TestCase
+ fixtures :customers
+
+ def test_find_single_value_object
+ assert_equal 50, customers(:david).balance.amount
+ assert_kind_of Money, customers(:david).balance
+ assert_equal 300, customers(:david).balance.exchange_to("DKK").amount
+ end
+
+ def test_find_multiple_value_object
+ assert_equal customers(:david).address_street, customers(:david).address.street
+ assert(
+ customers(:david).address.close_to?(Address.new("Different Street", customers(:david).address_city, customers(:david).address_country))
+ )
+ end
+
+ def test_change_single_value_object
+ customers(:david).balance = Money.new(100)
+ customers(:david).save
+ assert_equal 100, customers(:david).reload.balance.amount
+ end
+
+ def test_immutable_value_objects
+ customers(:david).balance = Money.new(100)
+ assert_raise(ActiveSupport::FrozenObjectError) { customers(:david).balance.instance_eval { @amount = 20 } }
+ end
+
+ def test_inferred_mapping
+ assert_equal "35.544623640962634", customers(:david).gps_location.latitude
+ assert_equal "-105.9309951055148", customers(:david).gps_location.longitude
+
+ customers(:david).gps_location = GpsLocation.new("39x-110")
+
+ assert_equal "39", customers(:david).gps_location.latitude
+ assert_equal "-110", customers(:david).gps_location.longitude
+
+ customers(:david).save
+
+ customers(:david).reload
+
+ assert_equal "39", customers(:david).gps_location.latitude
+ assert_equal "-110", customers(:david).gps_location.longitude
+ end
+
+ def test_reloaded_instance_refreshes_aggregations
+ assert_equal "35.544623640962634", customers(:david).gps_location.latitude
+ assert_equal "-105.9309951055148", customers(:david).gps_location.longitude
+
+ Customer.update_all("gps_location = '24x113'")
+ customers(:david).reload
+ assert_equal '24x113', customers(:david)['gps_location']
+
+ assert_equal GpsLocation.new('24x113'), customers(:david).gps_location
+ end
+
+ def test_gps_equality
+ assert GpsLocation.new('39x110') == GpsLocation.new('39x110')
+ end
+
+ def test_gps_inequality
+ assert GpsLocation.new('39x110') != GpsLocation.new('39x111')
+ end
+
+ def test_allow_nil_gps_is_nil
+ assert_equal nil, customers(:zaphod).gps_location
+ end
+
+ def test_allow_nil_gps_set_to_nil
+ customers(:david).gps_location = nil
+ customers(:david).save
+ customers(:david).reload
+ assert_equal nil, customers(:david).gps_location
+ end
+
+ def test_allow_nil_set_address_attributes_to_nil
+ customers(:zaphod).address = nil
+ assert_equal nil, customers(:zaphod).attributes[:address_street]
+ assert_equal nil, customers(:zaphod).attributes[:address_city]
+ assert_equal nil, customers(:zaphod).attributes[:address_country]
+ end
+
+ def test_allow_nil_address_set_to_nil
+ customers(:zaphod).address = nil
+ customers(:zaphod).save
+ customers(:zaphod).reload
+ assert_equal nil, customers(:zaphod).address
+ end
+
+ def test_nil_raises_error_when_allow_nil_is_false
+ assert_raise(NoMethodError) { customers(:david).balance = nil }
+ end
+
+ def test_allow_nil_address_loaded_when_only_some_attributes_are_nil
+ customers(:zaphod).address_street = nil
+ customers(:zaphod).save
+ customers(:zaphod).reload
+ assert_kind_of Address, customers(:zaphod).address
+ assert customers(:zaphod).address.street.nil?
+ end
+
+ def test_nil_assignment_results_in_nil
+ customers(:david).gps_location = GpsLocation.new('39x111')
+ assert_not_equal nil, customers(:david).gps_location
+ customers(:david).gps_location = nil
+ assert_equal nil, customers(:david).gps_location
+ end
+end
+
+class OverridingAggregationsTest < ActiveSupport::TestCase
+ class Name; end
+ class DifferentName; end
+
+ class Person < ActiveRecord::Base
+ composed_of :composed_of, :mapping => %w(person_first_name first_name)
+ end
+
+ class DifferentPerson < Person
+ composed_of :composed_of, :class_name => 'DifferentName', :mapping => %w(different_person_first_name first_name)
+ end
+
+ def test_composed_of_aggregation_redefinition_reflections_should_differ_and_not_inherited
+ assert_not_equal Person.reflect_on_aggregation(:composed_of),
+ DifferentPerson.reflect_on_aggregation(:composed_of)
+ end
+end
diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb
new file mode 100644
index 0000000000..3b290333e9
--- /dev/null
+++ b/activerecord/test/cases/ar_schema_test.rb
@@ -0,0 +1,33 @@
+require 'abstract_unit'
+require 'active_record/schema'
+
+if ActiveRecord::Base.connection.supports_migrations?
+
+ class ActiveRecordSchemaTest < ActiveSupport::TestCase
+ self.use_transactional_fixtures = false
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def teardown
+ @connection.drop_table :fruits rescue nil
+ end
+
+ def test_schema_define
+ ActiveRecord::Schema.define(:version => 7) do
+ create_table :fruits do |t|
+ t.column :color, :string
+ t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle
+ t.column :texture, :string
+ t.column :flavor, :string
+ end
+ end
+
+ assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" }
+ assert_nothing_raised { @connection.select_all "SELECT * FROM schema_info" }
+ assert_equal 7, @connection.select_one("SELECT version FROM schema_info")['version'].to_i
+ end
+ end
+
+end
diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb
new file mode 100644
index 0000000000..0b510df8d9
--- /dev/null
+++ b/activerecord/test/cases/associations/callbacks_test.rb
@@ -0,0 +1,146 @@
+require 'abstract_unit'
+require 'models/post'
+require 'models/comment'
+require 'models/author'
+require 'models/category'
+require 'models/project'
+require 'models/developer'
+
+class AssociationCallbacksTest < ActiveSupport::TestCase
+ fixtures :posts, :authors, :projects, :developers
+
+ def setup
+ @david = authors(:david)
+ @thinking = posts(:thinking)
+ @authorless = posts(:authorless)
+ assert @david.post_log.empty?
+ end
+
+ def test_adding_macro_callbacks
+ @david.posts_with_callbacks << @thinking
+ assert_equal ["before_adding#{@thinking.id}", "after_adding#{@thinking.id}"], @david.post_log
+ @david.posts_with_callbacks << @thinking
+ assert_equal ["before_adding#{@thinking.id}", "after_adding#{@thinking.id}", "before_adding#{@thinking.id}",
+ "after_adding#{@thinking.id}"], @david.post_log
+ end
+
+ def test_adding_with_proc_callbacks
+ @david.posts_with_proc_callbacks << @thinking
+ assert_equal ["before_adding#{@thinking.id}", "after_adding#{@thinking.id}"], @david.post_log
+ @david.posts_with_proc_callbacks << @thinking
+ assert_equal ["before_adding#{@thinking.id}", "after_adding#{@thinking.id}", "before_adding#{@thinking.id}",
+ "after_adding#{@thinking.id}"], @david.post_log
+ end
+
+ def test_removing_with_macro_callbacks
+ first_post, second_post = @david.posts_with_callbacks[0, 2]
+ @david.posts_with_callbacks.delete(first_post)
+ assert_equal ["before_removing#{first_post.id}", "after_removing#{first_post.id}"], @david.post_log
+ @david.posts_with_callbacks.delete(second_post)
+ assert_equal ["before_removing#{first_post.id}", "after_removing#{first_post.id}", "before_removing#{second_post.id}",
+ "after_removing#{second_post.id}"], @david.post_log
+ end
+
+ def test_removing_with_proc_callbacks
+ first_post, second_post = @david.posts_with_callbacks[0, 2]
+ @david.posts_with_proc_callbacks.delete(first_post)
+ assert_equal ["before_removing#{first_post.id}", "after_removing#{first_post.id}"], @david.post_log
+ @david.posts_with_proc_callbacks.delete(second_post)
+ assert_equal ["before_removing#{first_post.id}", "after_removing#{first_post.id}", "before_removing#{second_post.id}",
+ "after_removing#{second_post.id}"], @david.post_log
+ end
+
+ def test_multiple_callbacks
+ @david.posts_with_multiple_callbacks << @thinking
+ assert_equal ["before_adding#{@thinking.id}", "before_adding_proc#{@thinking.id}", "after_adding#{@thinking.id}",
+ "after_adding_proc#{@thinking.id}"], @david.post_log
+ @david.posts_with_multiple_callbacks << @thinking
+ assert_equal ["before_adding#{@thinking.id}", "before_adding_proc#{@thinking.id}", "after_adding#{@thinking.id}",
+ "after_adding_proc#{@thinking.id}", "before_adding#{@thinking.id}", "before_adding_proc#{@thinking.id}",
+ "after_adding#{@thinking.id}", "after_adding_proc#{@thinking.id}"], @david.post_log
+ end
+
+ def test_has_many_callbacks_with_create
+ morten = Author.create :name => "Morten"
+ post = morten.posts_with_proc_callbacks.create! :title => "Hello", :body => "How are you doing?"
+ assert_equal ["before_adding<new>", "after_adding#{post.id}"], morten.post_log
+ end
+
+ def test_has_many_callbacks_with_create!
+ morten = Author.create! :name => "Morten"
+ post = morten.posts_with_proc_callbacks.create :title => "Hello", :body => "How are you doing?"
+ assert_equal ["before_adding<new>", "after_adding#{post.id}"], morten.post_log
+ end
+
+ def test_has_many_callbacks_for_save_on_parent
+ jack = Author.new :name => "Jack"
+ post = jack.posts_with_callbacks.build :title => "Call me back!", :body => "Before you wake up and after you sleep"
+
+ callback_log = ["before_adding<new>", "after_adding#{jack.posts_with_callbacks.first.id}"]
+ assert_equal callback_log, jack.post_log
+ assert jack.save
+ assert_equal 1, jack.posts_with_callbacks.count
+ assert_equal callback_log, jack.post_log
+ end
+
+ def test_has_and_belongs_to_many_add_callback
+ david = developers(:david)
+ ar = projects(:active_record)
+ assert ar.developers_log.empty?
+ ar.developers_with_callbacks << david
+ assert_equal ["before_adding#{david.id}", "after_adding#{david.id}"], ar.developers_log
+ ar.developers_with_callbacks << david
+ assert_equal ["before_adding#{david.id}", "after_adding#{david.id}", "before_adding#{david.id}",
+ "after_adding#{david.id}"], ar.developers_log
+ end
+
+ def test_has_and_belongs_to_many_remove_callback
+ david = developers(:david)
+ jamis = developers(:jamis)
+ activerecord = projects(:active_record)
+ assert activerecord.developers_log.empty?
+ activerecord.developers_with_callbacks.delete(david)
+ assert_equal ["before_removing#{david.id}", "after_removing#{david.id}"], activerecord.developers_log
+
+ activerecord.developers_with_callbacks.delete(jamis)
+ assert_equal ["before_removing#{david.id}", "after_removing#{david.id}", "before_removing#{jamis.id}",
+ "after_removing#{jamis.id}"], activerecord.developers_log
+ end
+
+ def test_has_and_belongs_to_many_remove_callback_on_clear
+ activerecord = projects(:active_record)
+ assert activerecord.developers_log.empty?
+ if activerecord.developers_with_callbacks.size == 0
+ activerecord.developers << developers(:david)
+ activerecord.developers << developers(:jamis)
+ activerecord.reload
+ assert activerecord.developers_with_callbacks.size == 2
+ end
+ log_array = activerecord.developers_with_callbacks.collect {|d| ["before_removing#{d.id}","after_removing#{d.id}"]}.flatten.sort
+ assert activerecord.developers_with_callbacks.clear
+ assert_equal log_array, activerecord.developers_log.sort
+ end
+
+ def test_has_many_and_belongs_to_many_callbacks_for_save_on_parent
+ project = Project.new :name => "Callbacks"
+ project.developers_with_callbacks.build :name => "Jack", :salary => 95000
+
+ callback_log = ["before_adding<new>", "after_adding<new>"]
+ assert_equal callback_log, project.developers_log
+ assert project.save
+ assert_equal 1, project.developers_with_callbacks.size
+ assert_equal callback_log, project.developers_log
+ end
+
+ def test_dont_add_if_before_callback_raises_exception
+ assert !@david.unchangable_posts.include?(@authorless)
+ begin
+ @david.unchangable_posts << @authorless
+ rescue Exception => e
+ end
+ assert @david.post_log.empty?
+ assert !@david.unchangable_posts.include?(@authorless)
+ @david.reload
+ assert !@david.unchangable_posts.include?(@authorless)
+ end
+end
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
new file mode 100644
index 0000000000..0ca0b21084
--- /dev/null
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -0,0 +1,110 @@
+require 'abstract_unit'
+require 'models/post'
+require 'models/comment'
+require 'models/author'
+require 'models/category'
+require 'models/categorization'
+require 'models/company'
+require 'models/topic'
+require 'models/reply'
+
+class CascadedEagerLoadingTest < ActiveSupport::TestCase
+ fixtures :authors, :mixins, :companies, :posts, :topics
+
+ def test_eager_association_loading_with_cascaded_two_levels
+ authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id")
+ assert_equal 2, authors.size
+ assert_equal 5, authors[0].posts.size
+ assert_equal 1, authors[1].posts.size
+ assert_equal 9, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
+ end
+
+ def test_eager_association_loading_with_cascaded_two_levels_and_one_level
+ authors = Author.find(:all, :include=>[{:posts=>:comments}, :categorizations], :order=>"authors.id")
+ assert_equal 2, authors.size
+ assert_equal 5, authors[0].posts.size
+ assert_equal 1, authors[1].posts.size
+ assert_equal 9, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
+ assert_equal 1, authors[0].categorizations.size
+ assert_equal 2, authors[1].categorizations.size
+ end
+
+ def test_eager_association_loading_with_cascaded_two_levels_with_two_has_many_associations
+ authors = Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id")
+ assert_equal 2, authors.size
+ assert_equal 5, authors[0].posts.size
+ assert_equal 1, authors[1].posts.size
+ assert_equal 9, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
+ end
+
+ def test_eager_association_loading_with_cascaded_two_levels_and_self_table_reference
+ authors = Author.find(:all, :include=>{:posts=>[:comments, :author]}, :order=>"authors.id")
+ assert_equal 2, authors.size
+ assert_equal 5, authors[0].posts.size
+ assert_equal authors(:david).name, authors[0].name
+ assert_equal [authors(:david).name], authors[0].posts.collect{|post| post.author.name}.uniq
+ end
+
+ def test_eager_association_loading_with_cascaded_two_levels_with_condition
+ authors = Author.find(:all, :include=>{:posts=>:comments}, :conditions=>"authors.id=1", :order=>"authors.id")
+ assert_equal 1, authors.size
+ assert_equal 5, authors[0].posts.size
+ end
+
+ def test_eager_association_loading_with_cascaded_three_levels_by_ping_pong
+ firms = Firm.find(:all, :include=>{:account=>{:firm=>:account}}, :order=>"companies.id")
+ assert_equal 2, firms.size
+ assert_equal firms.first.account, firms.first.account.firm.account
+ assert_equal companies(:first_firm).account, assert_no_queries { firms.first.account.firm.account }
+ assert_equal companies(:first_firm).account.firm.account, assert_no_queries { firms.first.account.firm.account }
+ end
+
+ def test_eager_association_loading_with_has_many_sti
+ topics = Topic.find(:all, :include => :replies, :order => 'topics.id')
+ assert_equal topics(:first, :second), topics
+ assert_no_queries do
+ assert_equal 1, topics[0].replies.size
+ assert_equal 0, topics[1].replies.size
+ end
+ end
+
+ def test_eager_association_loading_with_belongs_to_sti
+ replies = Reply.find(:all, :include => :topic, :order => 'topics.id')
+ assert_equal [topics(:second)], replies
+ assert_equal topics(:first), assert_no_queries { replies.first.topic }
+ end
+
+ def test_eager_association_loading_with_multiple_stis_and_order
+ author = Author.find(:first, :include => { :posts => [ :special_comments , :very_special_comment ] }, :order => 'authors.name, comments.body, very_special_comments_posts.body', :conditions => 'posts.id = 4')
+ assert_equal authors(:david), author
+ assert_no_queries do
+ author.posts.first.special_comments
+ author.posts.first.very_special_comment
+ end
+ end
+
+ def test_eager_association_loading_of_stis_with_multiple_references
+ authors = Author.find(:all, :include => { :posts => { :special_comments => { :post => [ :special_comments, :very_special_comment ] } } }, :order => 'comments.body, very_special_comments_posts.body', :conditions => 'posts.id = 4')
+ assert_equal [authors(:david)], authors
+ assert_no_queries do
+ authors.first.posts.first.special_comments.first.post.special_comments
+ authors.first.posts.first.special_comments.first.post.very_special_comment
+ end
+ end
+end
+
+require 'models/vertex'
+require 'models/edge'
+class CascadedEagerLoadingTest < ActiveSupport::TestCase
+ fixtures :edges, :vertices
+
+ def test_eager_association_loading_with_recursive_cascading_four_levels_has_many_through
+ source = Vertex.find(:first, :include=>{:sinks=>{:sinks=>{:sinks=>:sinks}}}, :order => 'vertices.id')
+ assert_equal vertices(:vertex_4), assert_no_queries { source.sinks.first.sinks.first.sinks.first }
+ end
+
+ def test_eager_association_loading_with_recursive_cascading_four_levels_has_and_belongs_to_many
+ sink = Vertex.find(:first, :include=>{:sources=>{:sources=>{:sources=>:sources}}}, :order => 'vertices.id DESC')
+ assert_equal vertices(:vertex_1), assert_no_queries { sink.sources.first.sources.first.sources.first.sources.first }
+ end
+end
diff --git a/activerecord/test/cases/associations/eager_singularization_test.rb b/activerecord/test/cases/associations/eager_singularization_test.rb
new file mode 100644
index 0000000000..c1d89a2484
--- /dev/null
+++ b/activerecord/test/cases/associations/eager_singularization_test.rb
@@ -0,0 +1,145 @@
+require 'abstract_unit'
+
+class Virus < ActiveRecord::Base
+ belongs_to :octopus
+end
+class Octopus < ActiveRecord::Base
+ has_one :virus
+end
+class Pass < ActiveRecord::Base
+ belongs_to :bus
+end
+class Bus < ActiveRecord::Base
+ has_many :passes
+end
+class Mess < ActiveRecord::Base
+ has_and_belongs_to_many :crises
+end
+class Crisis < ActiveRecord::Base
+ has_and_belongs_to_many :messes
+ has_many :analyses, :dependent => :destroy
+ has_many :successes, :through => :analyses
+ has_many :dresses, :dependent => :destroy
+ has_many :compresses, :through => :dresses
+end
+class Analysis < ActiveRecord::Base
+ belongs_to :crisis
+ belongs_to :success
+end
+class Success < ActiveRecord::Base
+ has_many :analyses, :dependent => :destroy
+ has_many :crises, :through => :analyses
+end
+class Dress < ActiveRecord::Base
+ belongs_to :crisis
+ has_many :compresses
+end
+class Compress < ActiveRecord::Base
+ belongs_to :dress
+end
+
+
+class EagerSingularizationTest < ActiveSupport::TestCase
+
+ def setup
+ if ActiveRecord::Base.connection.supports_migrations?
+ ActiveRecord::Base.connection.create_table :viri do |t|
+ t.column :octopus_id, :integer
+ t.column :species, :string
+ end
+ ActiveRecord::Base.connection.create_table :octopi do |t|
+ t.column :species, :string
+ end
+ ActiveRecord::Base.connection.create_table :passes do |t|
+ t.column :bus_id, :integer
+ t.column :rides, :integer
+ end
+ ActiveRecord::Base.connection.create_table :buses do |t|
+ t.column :name, :string
+ end
+ ActiveRecord::Base.connection.create_table :crises_messes, :id => false do |t|
+ t.column :crisis_id, :integer
+ t.column :mess_id, :integer
+ end
+ ActiveRecord::Base.connection.create_table :messes do |t|
+ t.column :name, :string
+ end
+ ActiveRecord::Base.connection.create_table :crises do |t|
+ t.column :name, :string
+ end
+ ActiveRecord::Base.connection.create_table :successes do |t|
+ t.column :name, :string
+ end
+ ActiveRecord::Base.connection.create_table :analyses do |t|
+ t.column :crisis_id, :integer
+ t.column :success_id, :integer
+ end
+ ActiveRecord::Base.connection.create_table :dresses do |t|
+ t.column :crisis_id, :integer
+ end
+ ActiveRecord::Base.connection.create_table :compresses do |t|
+ t.column :dress_id, :integer
+ end
+ @have_tables = true
+ else
+ @have_tables = false
+ end
+ end
+
+ def teardown
+ ActiveRecord::Base.connection.drop_table :viri
+ ActiveRecord::Base.connection.drop_table :octopi
+ ActiveRecord::Base.connection.drop_table :passes
+ ActiveRecord::Base.connection.drop_table :buses
+ ActiveRecord::Base.connection.drop_table :crises_messes
+ ActiveRecord::Base.connection.drop_table :messes
+ ActiveRecord::Base.connection.drop_table :crises
+ ActiveRecord::Base.connection.drop_table :successes
+ ActiveRecord::Base.connection.drop_table :analyses
+ ActiveRecord::Base.connection.drop_table :dresses
+ ActiveRecord::Base.connection.drop_table :compresses
+ end
+
+ def test_eager_no_extra_singularization_belongs_to
+ return unless @have_tables
+ assert_nothing_raised do
+ Virus.find(:all, :include => :octopus)
+ end
+ end
+
+ def test_eager_no_extra_singularization_has_one
+ return unless @have_tables
+ assert_nothing_raised do
+ Octopus.find(:all, :include => :virus)
+ end
+ end
+
+ def test_eager_no_extra_singularization_has_many
+ return unless @have_tables
+ assert_nothing_raised do
+ Bus.find(:all, :include => :passes)
+ end
+ end
+
+ def test_eager_no_extra_singularization_has_and_belongs_to_many
+ return unless @have_tables
+ assert_nothing_raised do
+ Crisis.find(:all, :include => :messes)
+ Mess.find(:all, :include => :crises)
+ end
+ end
+
+ def test_eager_no_extra_singularization_has_many_through_belongs_to
+ return unless @have_tables
+ assert_nothing_raised do
+ Crisis.find(:all, :include => :successes)
+ end
+ end
+
+ def test_eager_no_extra_singularization_has_many_through_has_many
+ return unless @have_tables
+ assert_nothing_raised do
+ Crisis.find(:all, :include => :compresses)
+ end
+ end
+end
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
new file mode 100644
index 0000000000..0e351816d3
--- /dev/null
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -0,0 +1,447 @@
+require 'abstract_unit'
+require 'models/post'
+require 'models/comment'
+require 'models/author'
+require 'models/category'
+require 'models/company'
+require 'models/person'
+require 'models/reader'
+
+class EagerAssociationTest < ActiveSupport::TestCase
+ fixtures :posts, :comments, :authors, :categories, :categories_posts,
+ :companies, :accounts, :tags, :people, :readers
+
+ def test_loading_with_one_association
+ posts = Post.find(:all, :include => :comments)
+ post = posts.find { |p| p.id == 1 }
+ assert_equal 2, post.comments.size
+ assert post.comments.include?(comments(:greetings))
+
+ post = Post.find(:first, :include => :comments, :conditions => "posts.title = 'Welcome to the weblog'")
+ assert_equal 2, post.comments.size
+ assert post.comments.include?(comments(:greetings))
+ end
+
+ def test_loading_conditions_with_or
+ posts = authors(:david).posts.find(:all, :include => :comments, :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'")
+ assert_nil posts.detect { |p| p.author_id != authors(:david).id },
+ "expected to find only david's posts"
+ end
+
+ def test_with_ordering
+ list = Post.find(:all, :include => :comments, :order => "posts.id DESC")
+ [:eager_other, :sti_habtm, :sti_post_and_comments, :sti_comments,
+ :authorless, :thinking, :welcome
+ ].each_with_index do |post, index|
+ assert_equal posts(post), list[index]
+ end
+ end
+
+ def test_with_two_tables_in_from_without_getting_double_quoted
+ posts = Post.find(:all,
+ :select => "posts.*",
+ :from => "authors, posts",
+ :include => :comments,
+ :conditions => "posts.author_id = authors.id",
+ :order => "posts.id"
+ )
+
+ assert_equal 2, posts.first.comments.size
+ end
+
+ def test_loading_with_multiple_associations
+ posts = Post.find(:all, :include => [ :comments, :author, :categories ], :order => "posts.id")
+ assert_equal 2, posts.first.comments.size
+ assert_equal 2, posts.first.categories.size
+ assert posts.first.comments.include?(comments(:greetings))
+ end
+
+ def test_loading_from_an_association
+ posts = authors(:david).posts.find(:all, :include => :comments, :order => "posts.id")
+ assert_equal 2, posts.first.comments.size
+ end
+
+ def test_loading_with_no_associations
+ assert_nil Post.find(posts(:authorless).id, :include => :author).author
+ end
+
+ def test_eager_association_loading_with_belongs_to
+ comments = Comment.find(:all, :include => :post)
+ assert_equal 10, comments.length
+ titles = comments.map { |c| c.post.title }
+ assert titles.include?(posts(:welcome).title)
+ assert titles.include?(posts(:sti_post_and_comments).title)
+ end
+
+ def test_eager_association_loading_with_belongs_to_and_limit
+ comments = Comment.find(:all, :include => :post, :limit => 5, :order => 'comments.id')
+ assert_equal 5, comments.length
+ assert_equal [1,2,3,5,6], comments.collect { |c| c.id }
+ end
+
+ def test_eager_association_loading_with_belongs_to_and_limit_and_conditions
+ comments = Comment.find(:all, :include => :post, :conditions => 'post_id = 4', :limit => 3, :order => 'comments.id')
+ assert_equal 3, comments.length
+ assert_equal [5,6,7], comments.collect { |c| c.id }
+ end
+
+ def test_eager_association_loading_with_belongs_to_and_limit_and_offset
+ comments = Comment.find(:all, :include => :post, :limit => 3, :offset => 2, :order => 'comments.id')
+ assert_equal 3, comments.length
+ assert_equal [3,5,6], comments.collect { |c| c.id }
+ end
+
+ def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions
+ comments = Comment.find(:all, :include => :post, :conditions => 'post_id = 4', :limit => 3, :offset => 1, :order => 'comments.id')
+ assert_equal 3, comments.length
+ assert_equal [6,7,8], comments.collect { |c| c.id }
+ end
+
+ def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions_array
+ comments = Comment.find(:all, :include => :post, :conditions => ['post_id = ?',4], :limit => 3, :offset => 1, :order => 'comments.id')
+ assert_equal 3, comments.length
+ assert_equal [6,7,8], comments.collect { |c| c.id }
+ end
+
+ def test_eager_association_loading_with_belongs_to_and_limit_and_multiple_associations
+ posts = Post.find(:all, :include => [:author, :very_special_comment], :limit => 1, :order => 'posts.id')
+ assert_equal 1, posts.length
+ assert_equal [1], posts.collect { |p| p.id }
+ end
+
+ def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_multiple_associations
+ posts = Post.find(:all, :include => [:author, :very_special_comment], :limit => 1, :offset => 1, :order => 'posts.id')
+ assert_equal 1, posts.length
+ assert_equal [2], posts.collect { |p| p.id }
+ end
+
+ def test_eager_association_loading_with_belongs_to_inferred_foreign_key_from_association_name
+ author_favorite = AuthorFavorite.find(:first, :include => :favorite_author)
+ assert_equal authors(:mary), assert_no_queries { author_favorite.favorite_author }
+ end
+
+ def test_eager_association_loading_with_explicit_join
+ posts = Post.find(:all, :include => :comments, :joins => "INNER JOIN authors ON posts.author_id = authors.id AND authors.name = 'Mary'", :limit => 1, :order => 'author_id')
+ assert_equal 1, posts.length
+ end
+
+ def test_eager_with_has_many_through
+ posts_with_comments = people(:michael).posts.find(:all, :include => :comments)
+ posts_with_author = people(:michael).posts.find(:all, :include => :author )
+ posts_with_comments_and_author = people(:michael).posts.find(:all, :include => [ :comments, :author ])
+ assert_equal 2, posts_with_comments.inject(0) { |sum, post| sum += post.comments.size }
+ assert_equal authors(:david), assert_no_queries { posts_with_author.first.author }
+ assert_equal authors(:david), assert_no_queries { posts_with_comments_and_author.first.author }
+ end
+
+ def test_eager_with_has_many_through_an_sti_join_model
+ author = Author.find(:first, :include => :special_post_comments, :order => 'authors.id')
+ assert_equal [comments(:does_it_hurt)], assert_no_queries { author.special_post_comments }
+ end
+
+ def test_eager_with_has_many_through_an_sti_join_model_with_conditions_on_both
+ author = Author.find(:first, :include => :special_nonexistant_post_comments, :order => 'authors.id')
+ assert_equal [], author.special_nonexistant_post_comments
+ end
+
+ def test_eager_with_has_many_through_join_model_with_conditions
+ assert_equal Author.find(:first, :include => :hello_post_comments,
+ :order => 'authors.id').hello_post_comments.sort_by(&:id),
+ Author.find(:first, :order => 'authors.id').hello_post_comments.sort_by(&:id)
+ end
+
+ def test_eager_with_has_many_and_limit
+ posts = Post.find(:all, :order => 'posts.id asc', :include => [ :author, :comments ], :limit => 2)
+ assert_equal 2, posts.size
+ assert_equal 3, posts.inject(0) { |sum, post| sum += post.comments.size }
+ end
+
+ def test_eager_with_has_many_and_limit_and_conditions
+ if current_adapter?(:OpenBaseAdapter)
+ posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => "FETCHBLOB(posts.body) = 'hello'", :order => "posts.id")
+ else
+ posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => "posts.body = 'hello'", :order => "posts.id")
+ end
+ assert_equal 2, posts.size
+ assert_equal [4,5], posts.collect { |p| p.id }
+ end
+
+ def test_eager_with_has_many_and_limit_and_conditions_array
+ if current_adapter?(:OpenBaseAdapter)
+ posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "FETCHBLOB(posts.body) = ?", 'hello' ], :order => "posts.id")
+ else
+ posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "posts.body = ?", 'hello' ], :order => "posts.id")
+ end
+ assert_equal 2, posts.size
+ assert_equal [4,5], posts.collect { |p| p.id }
+ end
+
+ def test_eager_with_has_many_and_limit_and_conditions_array_on_the_eagers
+ posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ])
+ assert_equal 2, posts.size
+
+ count = Post.count(:include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ])
+ assert_equal count, posts.size
+ end
+
+ def test_eager_with_has_many_and_limit_ond_high_offset
+ posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => [ "authors.name = ?", 'David' ])
+ assert_equal 0, posts.size
+ end
+
+ def test_count_eager_with_has_many_and_limit_ond_high_offset
+ posts = Post.count(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => [ "authors.name = ?", 'David' ])
+ assert_equal 0, posts
+ end
+
+ def test_eager_with_has_many_and_limit_with_no_results
+ posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => "posts.title = 'magic forest'")
+ assert_equal 0, posts.size
+ end
+
+ def test_eager_count_performed_on_a_has_many_association_with_multi_table_conditional
+ author = authors(:david)
+ author_posts_without_comments = author.posts.select { |post| post.comments.blank? }
+ assert_equal author_posts_without_comments.size, author.posts.count(:all, :include => :comments, :conditions => 'comments.id is null')
+ end
+
+ def test_eager_with_has_and_belongs_to_many_and_limit
+ posts = Post.find(:all, :include => :categories, :order => "posts.id", :limit => 3)
+ assert_equal 3, posts.size
+ assert_equal 2, posts[0].categories.size
+ assert_equal 1, posts[1].categories.size
+ assert_equal 0, posts[2].categories.size
+ assert posts[0].categories.include?(categories(:technology))
+ assert posts[1].categories.include?(categories(:general))
+ end
+
+ def test_eager_with_has_many_and_limit_and_conditions_on_the_eagers
+ posts = authors(:david).posts.find(:all,
+ :include => :comments,
+ :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'",
+ :limit => 2
+ )
+ assert_equal 2, posts.size
+
+ count = Post.count(
+ :include => [ :comments, :author ],
+ :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')",
+ :limit => 2
+ )
+ assert_equal count, posts.size
+ end
+
+ def test_eager_with_has_many_and_limit_and_scoped_conditions_on_the_eagers
+ posts = nil
+ Post.with_scope(:find => {
+ :include => :comments,
+ :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'"
+ }) do
+ posts = authors(:david).posts.find(:all, :limit => 2)
+ assert_equal 2, posts.size
+ end
+
+ Post.with_scope(:find => {
+ :include => [ :comments, :author ],
+ :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')"
+ }) do
+ count = Post.count(:limit => 2)
+ assert_equal count, posts.size
+ end
+ end
+
+ def test_eager_with_has_many_and_limit_and_scoped_and_explicit_conditions_on_the_eagers
+ Post.with_scope(:find => { :conditions => "1=1" }) do
+ posts = authors(:david).posts.find(:all,
+ :include => :comments,
+ :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'",
+ :limit => 2
+ )
+ assert_equal 2, posts.size
+
+ count = Post.count(
+ :include => [ :comments, :author ],
+ :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')",
+ :limit => 2
+ )
+ assert_equal count, posts.size
+ end
+ end
+
+ def test_eager_with_scoped_order_using_association_limiting_without_explicit_scope
+ posts_with_explicit_order = Post.find(:all, :conditions => 'comments.id is not null', :include => :comments, :order => 'posts.id DESC', :limit => 2)
+ posts_with_scoped_order = Post.with_scope(:find => {:order => 'posts.id DESC'}) do
+ Post.find(:all, :conditions => 'comments.id is not null', :include => :comments, :limit => 2)
+ end
+ assert_equal posts_with_explicit_order, posts_with_scoped_order
+ end
+
+ def test_eager_association_loading_with_habtm
+ posts = Post.find(:all, :include => :categories, :order => "posts.id")
+ assert_equal 2, posts[0].categories.size
+ assert_equal 1, posts[1].categories.size
+ assert_equal 0, posts[2].categories.size
+ assert posts[0].categories.include?(categories(:technology))
+ assert posts[1].categories.include?(categories(:general))
+ end
+
+ def test_eager_with_inheritance
+ posts = SpecialPost.find(:all, :include => [ :comments ])
+ end
+
+ def test_eager_has_one_with_association_inheritance
+ post = Post.find(4, :include => [ :very_special_comment ])
+ assert_equal "VerySpecialComment", post.very_special_comment.class.to_s
+ end
+
+ def test_eager_has_many_with_association_inheritance
+ post = Post.find(4, :include => [ :special_comments ])
+ post.special_comments.each do |special_comment|
+ assert_equal "SpecialComment", special_comment.class.to_s
+ end
+ end
+
+ def test_eager_habtm_with_association_inheritance
+ post = Post.find(6, :include => [ :special_categories ])
+ assert_equal 1, post.special_categories.size
+ post.special_categories.each do |special_category|
+ assert_equal "SpecialCategory", special_category.class.to_s
+ end
+ end
+
+ def test_eager_with_has_one_dependent_does_not_destroy_dependent
+ assert_not_nil companies(:first_firm).account
+ f = Firm.find(:first, :include => :account,
+ :conditions => ["companies.name = ?", "37signals"])
+ assert_not_nil f.account
+ assert_equal companies(:first_firm, :reload).account, f.account
+ end
+
+ def test_eager_with_multi_table_conditional_properly_counts_the_records_when_using_size
+ author = authors(:david)
+ posts_with_no_comments = author.posts.select { |post| post.comments.blank? }
+ assert_equal posts_with_no_comments.size, author.posts_with_no_comments.size
+ assert_equal posts_with_no_comments, author.posts_with_no_comments
+ end
+
+ def test_eager_with_invalid_association_reference
+ assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
+ post = Post.find(6, :include=> :monkeys )
+ }
+ assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
+ post = Post.find(6, :include=>[ :monkeys ])
+ }
+ assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
+ post = Post.find(6, :include=>[ 'monkeys' ])
+ }
+ assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") {
+ post = Post.find(6, :include=>[ :monkeys, :elephants ])
+ }
+ end
+
+ def find_all_ordered(className, include=nil)
+ className.find(:all, :order=>"#{className.table_name}.#{className.primary_key}", :include=>include)
+ end
+
+ def test_limited_eager_with_order
+ assert_equal posts(:thinking, :sti_comments), Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => 'UPPER(posts.title)', :limit => 2, :offset => 1)
+ assert_equal posts(:sti_post_and_comments, :sti_comments), Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => 'UPPER(posts.title) DESC', :limit => 2, :offset => 1)
+ end
+
+ def test_limited_eager_with_multiple_order_columns
+ assert_equal posts(:thinking, :sti_comments), Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => 'UPPER(posts.title), posts.id', :limit => 2, :offset => 1)
+ assert_equal posts(:sti_post_and_comments, :sti_comments), Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => 'UPPER(posts.title) DESC, posts.id', :limit => 2, :offset => 1)
+ end
+
+ def test_eager_with_multiple_associations_with_same_table_has_many_and_habtm
+ # Eager includes of has many and habtm associations aren't necessarily sorted in the same way
+ def assert_equal_after_sort(item1, item2, item3 = nil)
+ assert_equal(item1.sort{|a,b| a.id <=> b.id}, item2.sort{|a,b| a.id <=> b.id})
+ assert_equal(item3.sort{|a,b| a.id <=> b.id}, item2.sort{|a,b| a.id <=> b.id}) if item3
+ end
+ # Test regular association, association with conditions, association with
+ # STI, and association with conditions assured not to be true
+ post_types = [:posts, :other_posts, :special_posts]
+ # test both has_many and has_and_belongs_to_many
+ [Author, Category].each do |className|
+ d1 = find_all_ordered(className)
+ # test including all post types at once
+ d2 = find_all_ordered(className, post_types)
+ d1.each_index do |i|
+ assert_equal(d1[i], d2[i])
+ assert_equal_after_sort(d1[i].posts, d2[i].posts)
+ post_types[1..-1].each do |post_type|
+ # test including post_types together
+ d3 = find_all_ordered(className, [:posts, post_type])
+ assert_equal(d1[i], d3[i])
+ assert_equal_after_sort(d1[i].posts, d3[i].posts)
+ assert_equal_after_sort(d1[i].send(post_type), d2[i].send(post_type), d3[i].send(post_type))
+ end
+ end
+ end
+ end
+
+ def test_eager_with_multiple_associations_with_same_table_has_one
+ d1 = find_all_ordered(Firm)
+ d2 = find_all_ordered(Firm, :account)
+ d1.each_index do |i|
+ assert_equal(d1[i], d2[i])
+ assert_equal(d1[i].account, d2[i].account)
+ end
+ end
+
+ def test_eager_with_multiple_associations_with_same_table_belongs_to
+ firm_types = [:firm, :firm_with_basic_id, :firm_with_other_name, :firm_with_condition]
+ d1 = find_all_ordered(Client)
+ d2 = find_all_ordered(Client, firm_types)
+ d1.each_index do |i|
+ assert_equal(d1[i], d2[i])
+ firm_types.each { |type| assert_equal(d1[i].send(type), d2[i].send(type)) }
+ end
+ end
+ def test_eager_with_valid_association_as_string_not_symbol
+ assert_nothing_raised { Post.find(:all, :include => 'comments') }
+ end
+
+ def test_preconfigured_includes_with_belongs_to
+ author = posts(:welcome).author_with_posts
+ assert_equal 5, author.posts.size
+ end
+
+ def test_preconfigured_includes_with_has_one
+ comment = posts(:sti_comments).very_special_comment_with_post
+ assert_equal posts(:sti_comments), comment.post
+ end
+
+ def test_preconfigured_includes_with_has_many
+ posts = authors(:david).posts_with_comments
+ one = posts.detect { |p| p.id == 1 }
+ assert_equal 5, posts.size
+ assert_equal 2, one.comments.size
+ end
+
+ def test_preconfigured_includes_with_habtm
+ posts = authors(:david).posts_with_categories
+ one = posts.detect { |p| p.id == 1 }
+ assert_equal 5, posts.size
+ assert_equal 2, one.categories.size
+ end
+
+ def test_preconfigured_includes_with_has_many_and_habtm
+ posts = authors(:david).posts_with_comments_and_categories
+ one = posts.detect { |p| p.id == 1 }
+ assert_equal 5, posts.size
+ assert_equal 2, one.comments.size
+ assert_equal 2, one.categories.size
+ end
+
+ def test_count_with_include
+ if current_adapter?(:SQLServerAdapter, :SybaseAdapter)
+ assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "len(comments.body) > 15")
+ elsif current_adapter?(:OpenBaseAdapter)
+ assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "length(FETCHBLOB(comments.body)) > 15")
+ else
+ assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "length(comments.body) > 15")
+ end
+ end
+end
diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb
new file mode 100644
index 0000000000..a34f1bf2bf
--- /dev/null
+++ b/activerecord/test/cases/associations/extension_test.rb
@@ -0,0 +1,47 @@
+require 'abstract_unit'
+require 'models/post'
+require 'models/comment'
+require 'models/project'
+require 'models/developer'
+
+class AssociationsExtensionsTest < ActiveSupport::TestCase
+ fixtures :projects, :developers, :developers_projects, :comments, :posts
+
+ def test_extension_on_has_many
+ assert_equal comments(:more_greetings), posts(:welcome).comments.find_most_recent
+ end
+
+ def test_extension_on_habtm
+ assert_equal projects(:action_controller), developers(:david).projects.find_most_recent
+ end
+
+ def test_named_extension_on_habtm
+ assert_equal projects(:action_controller), developers(:david).projects_extended_by_name.find_most_recent
+ end
+
+ def test_named_two_extensions_on_habtm
+ assert_equal projects(:action_controller), developers(:david).projects_extended_by_name_twice.find_most_recent
+ assert_equal projects(:active_record), developers(:david).projects_extended_by_name_twice.find_least_recent
+ end
+
+ def test_named_extension_and_block_on_habtm
+ assert_equal projects(:action_controller), developers(:david).projects_extended_by_name_and_block.find_most_recent
+ assert_equal projects(:active_record), developers(:david).projects_extended_by_name_and_block.find_least_recent
+ end
+
+ def test_marshalling_extensions
+ david = developers(:david)
+ assert_equal projects(:action_controller), david.projects.find_most_recent
+
+ david = Marshal.load(Marshal.dump(david))
+ assert_equal projects(:action_controller), david.projects.find_most_recent
+ end
+
+ def test_marshalling_named_extensions
+ david = developers(:david)
+ assert_equal projects(:action_controller), david.projects_extended_by_name.find_most_recent
+
+ david = Marshal.load(Marshal.dump(david))
+ assert_equal projects(:action_controller), david.projects_extended_by_name.find_most_recent
+ end
+end
diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb
new file mode 100644
index 0000000000..da594e459e
--- /dev/null
+++ b/activerecord/test/cases/associations/inner_join_association_test.rb
@@ -0,0 +1,88 @@
+require 'abstract_unit'
+require 'fixtures/post'
+require 'fixtures/comment'
+require 'fixtures/author'
+require 'fixtures/category'
+require 'fixtures/categorization'
+
+class InnerJoinAssociationTest < ActiveSupport::TestCase
+ fixtures :authors, :posts, :comments, :categories, :categories_posts, :categorizations
+
+ def test_construct_finder_sql_creates_inner_joins
+ sql = Author.send(:construct_finder_sql, :joins => :posts)
+ assert_match /INNER JOIN .?posts.? ON .?posts.?.author_id = authors.id/, sql
+ end
+
+ def test_construct_finder_sql_cascades_inner_joins
+ sql = Author.send(:construct_finder_sql, :joins => {:posts => :comments})
+ assert_match /INNER JOIN .?posts.? ON .?posts.?.author_id = authors.id/, sql
+ assert_match /INNER JOIN .?comments.? ON .?comments.?.post_id = posts.id/, sql
+ end
+
+ def test_construct_finder_sql_inner_joins_through_associations
+ sql = Author.send(:construct_finder_sql, :joins => :categorized_posts)
+ assert_match /INNER JOIN .?categorizations.?.*INNER JOIN .?posts.?/, sql
+ end
+
+ def test_construct_finder_sql_applies_association_conditions
+ sql = Author.send(:construct_finder_sql, :joins => :categories_like_general, :conditions => "TERMINATING_MARKER")
+ assert_match /INNER JOIN .?categories.? ON.*AND.*.?General.?.*TERMINATING_MARKER/, sql
+ end
+
+ def test_construct_finder_sql_unpacks_nested_joins
+ sql = Author.send(:construct_finder_sql, :joins => {:posts => [[:comments]]})
+ assert_no_match /inner join.*inner join.*inner join/i, sql, "only two join clauses should be present"
+ assert_match /INNER JOIN .?posts.? ON .?posts.?.author_id = authors.id/, sql
+ assert_match /INNER JOIN .?comments.? ON .?comments.?.post_id = .?posts.?.id/, sql
+ end
+
+ def test_construct_finder_sql_ignores_empty_joins_hash
+ sql = Author.send(:construct_finder_sql, :joins => {})
+ assert_no_match /JOIN/i, sql
+ end
+
+ def test_construct_finder_sql_ignores_empty_joins_array
+ sql = Author.send(:construct_finder_sql, :joins => [])
+ assert_no_match /JOIN/i, sql
+ end
+
+ def test_find_with_implicit_inner_joins_honors_readonly_without_select
+ authors = Author.find(:all, :joins => :posts)
+ assert !authors.empty?, "expected authors to be non-empty"
+ assert authors.all? {|a| a.readonly? }, "expected all authors to be readonly"
+ end
+
+ def test_find_with_implicit_inner_joins_honors_readonly_with_select
+ authors = Author.find(:all, :select => 'authors.*', :joins => :posts)
+ assert !authors.empty?, "expected authors to be non-empty"
+ assert authors.all? {|a| !a.readonly? }, "expected no authors to be readonly"
+ end
+
+ def test_find_with_implicit_inner_joins_honors_readonly_false
+ authors = Author.find(:all, :joins => :posts, :readonly => false)
+ assert !authors.empty?, "expected authors to be non-empty"
+ assert authors.all? {|a| !a.readonly? }, "expected no authors to be readonly"
+ end
+
+ def test_find_with_implicit_inner_joins_does_not_set_associations
+ authors = Author.find(:all, :select => 'authors.*', :joins => :posts)
+ assert !authors.empty?, "expected authors to be non-empty"
+ assert authors.all? {|a| !a.send(:instance_variable_names).include?("@posts")}, "expected no authors to have the @posts association loaded"
+ end
+
+ def test_count_honors_implicit_inner_joins
+ real_count = Author.find(:all).sum{|a| a.posts.count }
+ assert_equal real_count, Author.count(:joins => :posts), "plain inner join count should match the number of referenced posts records"
+ end
+
+ def test_calculate_honors_implicit_inner_joins
+ real_count = Author.find(:all).sum{|a| a.posts.count }
+ assert_equal real_count, Author.calculate(:count, 'authors.id', :joins => :posts), "plain inner join count should match the number of referenced posts records"
+ end
+
+ def test_calculate_honors_implicit_inner_joins_and_distinct_and_conditions
+ real_count = Author.find(:all).select {|a| a.posts.any? {|p| p.title =~ /^Welcome/} }.length
+ authors_with_welcoming_post_titles = Author.calculate(:count, 'authors.id', :joins => :posts, :distinct => true, :conditions => "posts.title like 'Welcome%'")
+ assert_equal real_count, authors_with_welcoming_post_titles, "inner join and conditions should have only returned authors posting titles starting with 'Welcome'"
+ end
+end
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
new file mode 100644
index 0000000000..c6a1e356a5
--- /dev/null
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -0,0 +1,559 @@
+require 'abstract_unit'
+require 'fixtures/tag'
+require 'fixtures/tagging'
+require 'fixtures/post'
+require 'fixtures/item'
+require 'fixtures/comment'
+require 'fixtures/author'
+require 'fixtures/category'
+require 'fixtures/categorization'
+require 'fixtures/vertex'
+require 'fixtures/edge'
+require 'fixtures/book'
+require 'fixtures/citation'
+
+class AssociationsJoinModelTest < ActiveSupport::TestCase
+ self.use_transactional_fixtures = false
+ fixtures :posts, :authors, :categories, :categorizations, :comments, :tags, :taggings, :author_favorites, :vertices, :items, :books
+
+ def test_has_many
+ assert authors(:david).categories.include?(categories(:general))
+ end
+
+ def test_has_many_inherited
+ assert authors(:mary).categories.include?(categories(:sti_test))
+ end
+
+ def test_inherited_has_many
+ assert categories(:sti_test).authors.include?(authors(:mary))
+ end
+
+ def test_has_many_uniq_through_join_model
+ assert_equal 2, authors(:mary).categorized_posts.size
+ assert_equal 1, authors(:mary).unique_categorized_posts.size
+ end
+
+ def test_has_many_uniq_through_count
+ author = authors(:mary)
+ assert !authors(:mary).unique_categorized_posts.loaded?
+ assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count }
+ assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count(:title) }
+ assert_queries(1) { assert_equal 0, author.unique_categorized_posts.count(:title, :conditions => "title is NULL") }
+ assert !authors(:mary).unique_categorized_posts.loaded?
+ end
+
+ def test_polymorphic_has_many
+ assert posts(:welcome).taggings.include?(taggings(:welcome_general))
+ end
+
+ def test_polymorphic_has_one
+ assert_equal taggings(:welcome_general), posts(:welcome).tagging
+ end
+
+ def test_polymorphic_belongs_to
+ assert_equal posts(:welcome), posts(:welcome).taggings.first.taggable
+ end
+
+ def test_polymorphic_has_many_going_through_join_model
+ assert_equal tags(:general), tag = posts(:welcome).tags.first
+ assert_no_queries do
+ tag.tagging
+ end
+ end
+
+ def test_count_polymorphic_has_many
+ assert_equal 1, posts(:welcome).taggings.count
+ assert_equal 1, posts(:welcome).tags.count
+ end
+
+ def test_polymorphic_has_many_going_through_join_model_with_find
+ assert_equal tags(:general), tag = posts(:welcome).tags.find(:first)
+ assert_no_queries do
+ tag.tagging
+ end
+ end
+
+ def test_polymorphic_has_many_going_through_join_model_with_include_on_source_reflection
+ assert_equal tags(:general), tag = posts(:welcome).funky_tags.first
+ assert_no_queries do
+ tag.tagging
+ end
+ end
+
+ def test_polymorphic_has_many_going_through_join_model_with_include_on_source_reflection_with_find
+ assert_equal tags(:general), tag = posts(:welcome).funky_tags.find(:first)
+ assert_no_queries do
+ tag.tagging
+ end
+ end
+
+ def test_polymorphic_has_many_going_through_join_model_with_disabled_include
+ assert_equal tags(:general), tag = posts(:welcome).tags.add_joins_and_select.first
+ assert_queries 1 do
+ tag.tagging
+ end
+ end
+
+ def test_polymorphic_has_many_going_through_join_model_with_custom_select_and_joins
+ assert_equal tags(:general), tag = posts(:welcome).tags.add_joins_and_select.first
+ tag.author_id
+ end
+
+ def test_polymorphic_has_many_going_through_join_model_with_custom_foreign_key
+ assert_equal tags(:misc), taggings(:welcome_general).super_tag
+ assert_equal tags(:misc), posts(:welcome).super_tags.first
+ end
+
+ def test_polymorphic_has_many_create_model_with_inheritance_and_custom_base_class
+ post = SubStiPost.create :title => 'SubStiPost', :body => 'SubStiPost body'
+ assert_instance_of SubStiPost, post
+
+ tagging = tags(:misc).taggings.create(:taggable => post)
+ assert_equal "SubStiPost", tagging.taggable_type
+ end
+
+ def test_polymorphic_has_many_going_through_join_model_with_inheritance
+ assert_equal tags(:general), posts(:thinking).tags.first
+ end
+
+ def test_polymorphic_has_many_going_through_join_model_with_inheritance_with_custom_class_name
+ assert_equal tags(:general), posts(:thinking).funky_tags.first
+ end
+
+ def test_polymorphic_has_many_create_model_with_inheritance
+ post = posts(:thinking)
+ assert_instance_of SpecialPost, post
+
+ tagging = tags(:misc).taggings.create(:taggable => post)
+ assert_equal "Post", tagging.taggable_type
+ end
+
+ def test_polymorphic_has_one_create_model_with_inheritance
+ tagging = tags(:misc).create_tagging(:taggable => posts(:thinking))
+ assert_equal "Post", tagging.taggable_type
+ end
+
+ def test_set_polymorphic_has_many
+ tagging = tags(:misc).taggings.create
+ posts(:thinking).taggings << tagging
+ assert_equal "Post", tagging.taggable_type
+ end
+
+ def test_set_polymorphic_has_one
+ tagging = tags(:misc).taggings.create
+ posts(:thinking).tagging = tagging
+ assert_equal "Post", tagging.taggable_type
+ end
+
+ def test_create_polymorphic_has_many_with_scope
+ old_count = posts(:welcome).taggings.count
+ tagging = posts(:welcome).taggings.create(:tag => tags(:misc))
+ assert_equal "Post", tagging.taggable_type
+ assert_equal old_count+1, posts(:welcome).taggings.count
+ end
+
+ def test_create_bang_polymorphic_with_has_many_scope
+ old_count = posts(:welcome).taggings.count
+ tagging = posts(:welcome).taggings.create!(:tag => tags(:misc))
+ assert_equal "Post", tagging.taggable_type
+ assert_equal old_count+1, posts(:welcome).taggings.count
+ end
+
+ def test_create_polymorphic_has_one_with_scope
+ old_count = Tagging.count
+ tagging = posts(:welcome).tagging.create(:tag => tags(:misc))
+ assert_equal "Post", tagging.taggable_type
+ assert_equal old_count+1, Tagging.count
+ end
+
+ def test_delete_polymorphic_has_many_with_delete_all
+ assert_equal 1, posts(:welcome).taggings.count
+ posts(:welcome).taggings.first.update_attribute :taggable_type, 'PostWithHasManyDeleteAll'
+ post = find_post_with_dependency(1, :has_many, :taggings, :delete_all)
+
+ old_count = Tagging.count
+ post.destroy
+ assert_equal old_count-1, Tagging.count
+ assert_equal 0, posts(:welcome).taggings.count
+ end
+
+ def test_delete_polymorphic_has_many_with_destroy
+ assert_equal 1, posts(:welcome).taggings.count
+ posts(:welcome).taggings.first.update_attribute :taggable_type, 'PostWithHasManyDestroy'
+ post = find_post_with_dependency(1, :has_many, :taggings, :destroy)
+
+ old_count = Tagging.count
+ post.destroy
+ assert_equal old_count-1, Tagging.count
+ assert_equal 0, posts(:welcome).taggings.count
+ end
+
+ def test_delete_polymorphic_has_many_with_nullify
+ assert_equal 1, posts(:welcome).taggings.count
+ posts(:welcome).taggings.first.update_attribute :taggable_type, 'PostWithHasManyNullify'
+ post = find_post_with_dependency(1, :has_many, :taggings, :nullify)
+
+ old_count = Tagging.count
+ post.destroy
+ assert_equal old_count, Tagging.count
+ assert_equal 0, posts(:welcome).taggings.count
+ end
+
+ def test_delete_polymorphic_has_one_with_destroy
+ assert posts(:welcome).tagging
+ posts(:welcome).tagging.update_attribute :taggable_type, 'PostWithHasOneDestroy'
+ post = find_post_with_dependency(1, :has_one, :tagging, :destroy)
+
+ old_count = Tagging.count
+ post.destroy
+ assert_equal old_count-1, Tagging.count
+ assert_nil posts(:welcome).tagging(true)
+ end
+
+ def test_delete_polymorphic_has_one_with_nullify
+ assert posts(:welcome).tagging
+ posts(:welcome).tagging.update_attribute :taggable_type, 'PostWithHasOneNullify'
+ post = find_post_with_dependency(1, :has_one, :tagging, :nullify)
+
+ old_count = Tagging.count
+ post.destroy
+ assert_equal old_count, Tagging.count
+ assert_nil posts(:welcome).tagging(true)
+ end
+
+ def test_has_many_with_piggyback
+ assert_equal "2", categories(:sti_test).authors.first.post_id.to_s
+ end
+
+ def test_include_has_many_through
+ posts = Post.find(:all, :order => 'posts.id')
+ posts_with_authors = Post.find(:all, :include => :authors, :order => 'posts.id')
+ assert_equal posts.length, posts_with_authors.length
+ posts.length.times do |i|
+ assert_equal posts[i].authors.length, assert_no_queries { posts_with_authors[i].authors.length }
+ end
+ end
+
+ def test_include_polymorphic_has_one
+ post = Post.find_by_id(posts(:welcome).id, :include => :tagging)
+ tagging = taggings(:welcome_general)
+ assert_no_queries do
+ assert_equal tagging, post.tagging
+ end
+ end
+
+ def test_include_polymorphic_has_one_defined_in_abstract_parent
+ item = Item.find_by_id(items(:dvd).id, :include => :tagging)
+ tagging = taggings(:godfather)
+ assert_no_queries do
+ assert_equal tagging, item.tagging
+ end
+ end
+
+ def test_include_polymorphic_has_many_through
+ posts = Post.find(:all, :order => 'posts.id')
+ posts_with_tags = Post.find(:all, :include => :tags, :order => 'posts.id')
+ assert_equal posts.length, posts_with_tags.length
+ posts.length.times do |i|
+ assert_equal posts[i].tags.length, assert_no_queries { posts_with_tags[i].tags.length }
+ end
+ end
+
+ def test_include_polymorphic_has_many
+ posts = Post.find(:all, :order => 'posts.id')
+ posts_with_taggings = Post.find(:all, :include => :taggings, :order => 'posts.id')
+ assert_equal posts.length, posts_with_taggings.length
+ posts.length.times do |i|
+ assert_equal posts[i].taggings.length, assert_no_queries { posts_with_taggings[i].taggings.length }
+ end
+ end
+
+ def test_has_many_find_all
+ assert_equal [categories(:general)], authors(:david).categories.find(:all)
+ end
+
+ def test_has_many_find_first
+ assert_equal categories(:general), authors(:david).categories.find(:first)
+ end
+
+ def test_has_many_with_hash_conditions
+ assert_equal categories(:general), authors(:david).categories_like_general.find(:first)
+ end
+
+ def test_has_many_find_conditions
+ assert_equal categories(:general), authors(:david).categories.find(:first, :conditions => "categories.name = 'General'")
+ assert_equal nil, authors(:david).categories.find(:first, :conditions => "categories.name = 'Technology'")
+ end
+
+ def test_has_many_class_methods_called_by_method_missing
+ assert_equal categories(:general), authors(:david).categories.find_all_by_name('General').first
+ assert_equal nil, authors(:david).categories.find_by_name('Technology')
+ end
+
+ def test_has_many_going_through_join_model_with_custom_foreign_key
+ assert_equal [], posts(:thinking).authors
+ assert_equal [authors(:mary)], posts(:authorless).authors
+ end
+
+ def test_belongs_to_polymorphic_with_counter_cache
+ assert_equal 0, posts(:welcome)[:taggings_count]
+ tagging = posts(:welcome).taggings.create(:tag => tags(:general))
+ assert_equal 1, posts(:welcome, :reload)[:taggings_count]
+ tagging.destroy
+ assert posts(:welcome, :reload)[:taggings_count].zero?
+ end
+
+ def test_unavailable_through_reflection
+ assert_raise(ActiveRecord::HasManyThroughAssociationNotFoundError) { authors(:david).nothings }
+ end
+
+ def test_has_many_through_join_model_with_conditions
+ assert_equal [], posts(:welcome).invalid_taggings
+ assert_equal [], posts(:welcome).invalid_tags
+ end
+
+ def test_has_many_polymorphic
+ assert_raise ActiveRecord::HasManyThroughAssociationPolymorphicError do
+ assert_equal posts(:welcome, :thinking), tags(:general).taggables
+ end
+ assert_raise ActiveRecord::EagerLoadPolymorphicError do
+ assert_equal posts(:welcome, :thinking), tags(:general).taggings.find(:all, :include => :taggable)
+ end
+ end
+
+ def test_has_many_polymorphic_with_source_type
+ assert_equal posts(:welcome, :thinking), tags(:general).tagged_posts
+ end
+
+ def test_eager_has_many_polymorphic_with_source_type
+ tag_with_include = Tag.find(tags(:general).id, :include => :tagged_posts)
+ desired = posts(:welcome, :thinking)
+ assert_no_queries do
+ assert_equal desired, tag_with_include.tagged_posts
+ end
+ end
+
+ def test_has_many_through_has_many_find_all
+ assert_equal comments(:greetings), authors(:david).comments.find(:all, :order => 'comments.id').first
+ end
+
+ def test_has_many_through_has_many_find_all_with_custom_class
+ assert_equal comments(:greetings), authors(:david).funky_comments.find(:all, :order => 'comments.id').first
+ end
+
+ def test_has_many_through_has_many_find_first
+ assert_equal comments(:greetings), authors(:david).comments.find(:first, :order => 'comments.id')
+ end
+
+ def test_has_many_through_has_many_find_conditions
+ options = { :conditions => "comments.#{QUOTED_TYPE}='SpecialComment'", :order => 'comments.id' }
+ assert_equal comments(:does_it_hurt), authors(:david).comments.find(:first, options)
+ end
+
+ def test_has_many_through_has_many_find_by_id
+ assert_equal comments(:more_greetings), authors(:david).comments.find(2)
+ end
+
+ def test_has_many_through_polymorphic_has_one
+ assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).tagging }
+ end
+
+ def test_has_many_through_polymorphic_has_many
+ assert_equal taggings(:welcome_general, :thinking_general), authors(:david).taggings.uniq.sort_by { |t| t.id }
+ end
+
+ def test_include_has_many_through_polymorphic_has_many
+ author = Author.find_by_id(authors(:david).id, :include => :taggings)
+ expected_taggings = taggings(:welcome_general, :thinking_general)
+ assert_no_queries do
+ assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id }
+ end
+ end
+
+ def test_has_many_through_has_many_through
+ assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).tags }
+ end
+
+ def test_has_many_through_habtm
+ assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).post_categories }
+ end
+
+ def test_eager_load_has_many_through_has_many
+ author = Author.find :first, :conditions => ['name = ?', 'David'], :include => :comments, :order => 'comments.id'
+ SpecialComment.new; VerySpecialComment.new
+ assert_no_queries do
+ assert_equal [1,2,3,5,6,7,8,9,10], author.comments.collect(&:id)
+ end
+ end
+
+ def test_eager_load_has_many_through_has_many_with_conditions
+ post = Post.find(:first, :include => :invalid_tags)
+ assert_no_queries do
+ post.invalid_tags
+ end
+ end
+
+ def test_eager_belongs_to_and_has_one_not_singularized
+ assert_nothing_raised do
+ Author.find(:first, :include => :author_address)
+ AuthorAddress.find(:first, :include => :author)
+ end
+ end
+
+ def test_self_referential_has_many_through
+ assert_equal [authors(:mary)], authors(:david).favorite_authors
+ assert_equal [], authors(:mary).favorite_authors
+ end
+
+ def test_add_to_self_referential_has_many_through
+ new_author = Author.create(:name => "Bob")
+ authors(:david).author_favorites.create :favorite_author => new_author
+ assert_equal new_author, authors(:david).reload.favorite_authors.first
+ end
+
+ def test_has_many_through_uses_conditions_specified_on_the_has_many_association
+ author = Author.find(:first)
+ assert !author.comments.blank?
+ assert author.nonexistant_comments.blank?
+ end
+
+ def test_has_many_through_uses_correct_attributes
+ assert_nil posts(:thinking).tags.find_by_name("General").attributes["tag_id"]
+ end
+
+ def test_raise_error_when_adding_new_record_to_has_many_through
+ assert_raise(ActiveRecord::HasManyThroughCantAssociateNewRecords) { posts(:thinking).tags << tags(:general).clone }
+ assert_raise(ActiveRecord::HasManyThroughCantAssociateNewRecords) { posts(:thinking).clone.tags << tags(:general) }
+ assert_raise(ActiveRecord::HasManyThroughCantAssociateNewRecords) { posts(:thinking).tags.build }
+ assert_raise(ActiveRecord::HasManyThroughCantAssociateNewRecords) { posts(:thinking).tags.new }
+ end
+
+ def test_create_associate_when_adding_to_has_many_through
+ count = posts(:thinking).tags.count
+ push = Tag.create!(:name => 'pushme')
+ post_thinking = posts(:thinking)
+ assert_nothing_raised { post_thinking.tags << push }
+ assert_nil( wrong = post_thinking.tags.detect { |t| t.class != Tag },
+ message = "Expected a Tag in tags collection, got #{wrong.class}.")
+ assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
+ message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
+ assert_equal(count + 1, post_thinking.tags.size)
+ assert_equal(count + 1, post_thinking.tags(true).size)
+
+ assert_kind_of Tag, post_thinking.tags.create!(:name => 'foo')
+ assert_nil( wrong = post_thinking.tags.detect { |t| t.class != Tag },
+ message = "Expected a Tag in tags collection, got #{wrong.class}.")
+ assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
+ message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
+ assert_equal(count + 2, post_thinking.tags.size)
+ assert_equal(count + 2, post_thinking.tags(true).size)
+
+ assert_nothing_raised { post_thinking.tags.concat(Tag.create!(:name => 'abc'), Tag.create!(:name => 'def')) }
+ assert_nil( wrong = post_thinking.tags.detect { |t| t.class != Tag },
+ message = "Expected a Tag in tags collection, got #{wrong.class}.")
+ assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
+ message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
+ assert_equal(count + 4, post_thinking.tags.size)
+ assert_equal(count + 4, post_thinking.tags(true).size)
+
+ # Raises if the wrong reflection name is used to set the Edge belongs_to
+ assert_nothing_raised { vertices(:vertex_1).sinks << vertices(:vertex_5) }
+ end
+
+ def test_has_many_through_collection_size_doesnt_load_target_if_not_loaded
+ author = authors(:david)
+ assert_equal 9, author.comments.size
+ assert !author.comments.loaded?
+ end
+
+ uses_mocha('has_many_through_collection_size_uses_counter_cache_if_it_exists') do
+ def test_has_many_through_collection_size_uses_counter_cache_if_it_exists
+ author = authors(:david)
+ author.stubs(:read_attribute).with('comments_count').returns(100)
+ assert_equal 100, author.comments.size
+ assert !author.comments.loaded?
+ end
+ end
+
+ def test_adding_junk_to_has_many_through_should_raise_type_mismatch
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { posts(:thinking).tags << "Uhh what now?" }
+ end
+
+ def test_adding_to_has_many_through_should_return_self
+ tags = posts(:thinking).tags
+ assert_equal tags, posts(:thinking).tags.push(tags(:general))
+ end
+
+ def test_delete_associate_when_deleting_from_has_many_through_with_nonstandard_id
+ count = books(:awdr).references.count
+ references_before = books(:awdr).references
+ book = Book.create!(:name => 'Getting Real')
+ book_awdr = books(:awdr)
+ book_awdr.references << book
+ assert_equal(count + 1, book_awdr.references(true).size)
+
+ assert_nothing_raised { book_awdr.references.delete(book) }
+ assert_equal(count, book_awdr.references.size)
+ assert_equal(count, book_awdr.references(true).size)
+ assert_equal(references_before.sort, book_awdr.references.sort)
+ end
+
+ def test_delete_associate_when_deleting_from_has_many_through
+ count = posts(:thinking).tags.count
+ tags_before = posts(:thinking).tags
+ tag = Tag.create!(:name => 'doomed')
+ post_thinking = posts(:thinking)
+ post_thinking.tags << tag
+ assert_equal(count + 1, post_thinking.tags(true).size)
+
+ assert_nothing_raised { post_thinking.tags.delete(tag) }
+ assert_equal(count, post_thinking.tags.size)
+ assert_equal(count, post_thinking.tags(true).size)
+ assert_equal(tags_before.sort, post_thinking.tags.sort)
+ end
+
+ def test_delete_associate_when_deleting_from_has_many_through_with_multiple_tags
+ count = posts(:thinking).tags.count
+ tags_before = posts(:thinking).tags
+ doomed = Tag.create!(:name => 'doomed')
+ doomed2 = Tag.create!(:name => 'doomed2')
+ quaked = Tag.create!(:name => 'quaked')
+ post_thinking = posts(:thinking)
+ post_thinking.tags << doomed << doomed2
+ assert_equal(count + 2, post_thinking.tags(true).size)
+
+ assert_nothing_raised { post_thinking.tags.delete(doomed, doomed2, quaked) }
+ assert_equal(count, post_thinking.tags.size)
+ assert_equal(count, post_thinking.tags(true).size)
+ assert_equal(tags_before.sort, post_thinking.tags.sort)
+ end
+
+ def test_deleting_junk_from_has_many_through_should_raise_type_mismatch
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { posts(:thinking).tags.delete("Uhh what now?") }
+ end
+
+ def test_has_many_through_sum_uses_calculations
+ assert_nothing_raised { authors(:david).comments.sum(:post_id) }
+ end
+
+ def test_has_many_through_has_many_with_sti
+ assert_equal [comments(:does_it_hurt)], authors(:david).special_post_comments
+ end
+
+ def test_uniq_has_many_through_should_retain_order
+ comment_ids = authors(:david).comments.map(&:id)
+ assert_equal comment_ids.sort, authors(:david).ordered_uniq_comments.map(&:id)
+ assert_equal comment_ids.sort.reverse, authors(:david).ordered_uniq_comments_desc.map(&:id)
+ end
+
+ private
+ # create dynamic Post models to allow different dependency options
+ def find_post_with_dependency(post_id, association, association_name, dependency)
+ class_name = "PostWith#{association.to_s.classify}#{dependency.to_s.classify}"
+ Post.find(post_id).update_attribute :type, class_name
+ klass = Object.const_set(class_name, Class.new(ActiveRecord::Base))
+ klass.set_table_name 'posts'
+ klass.send(association, association_name, :as => :taggable, :dependent => dependency)
+ klass.find(post_id)
+ end
+end
diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb
new file mode 100755
index 0000000000..a657da265d
--- /dev/null
+++ b/activerecord/test/cases/associations_test.rb
@@ -0,0 +1,2177 @@
+require 'abstract_unit'
+require 'fixtures/developer'
+require 'fixtures/project'
+require 'fixtures/company'
+require 'fixtures/topic'
+require 'fixtures/reply'
+require 'fixtures/computer'
+require 'fixtures/customer'
+require 'fixtures/order'
+require 'fixtures/categorization'
+require 'fixtures/category'
+require 'fixtures/post'
+require 'fixtures/author'
+require 'fixtures/comment'
+require 'fixtures/tag'
+require 'fixtures/tagging'
+require 'fixtures/person'
+require 'fixtures/reader'
+
+class AssociationsTest < ActiveSupport::TestCase
+ fixtures :accounts, :companies, :developers, :projects, :developers_projects,
+ :computers
+
+ def test_bad_collection_keys
+ assert_raise(ArgumentError, 'ActiveRecord should have barked on bad collection keys') do
+ Class.new(ActiveRecord::Base).has_many(:wheels, :name => 'wheels')
+ end
+ end
+
+ def test_should_construct_new_finder_sql_after_create
+ person = Person.new
+ assert_equal [], person.readers.find(:all)
+ person.save!
+ reader = Reader.create! :person => person, :post => Post.new(:title => "foo", :body => "bar")
+ assert_equal [reader], person.readers.find(:all)
+ end
+
+ def test_force_reload
+ firm = Firm.new("name" => "A New Firm, Inc")
+ firm.save
+ firm.clients.each {|c|} # forcing to load all clients
+ assert firm.clients.empty?, "New firm shouldn't have client objects"
+ assert_equal 0, firm.clients.size, "New firm should have 0 clients"
+
+ client = Client.new("name" => "TheClient.com", "firm_id" => firm.id)
+ client.save
+
+ assert firm.clients.empty?, "New firm should have cached no client objects"
+ assert_equal 0, firm.clients.size, "New firm should have cached 0 clients count"
+
+ assert !firm.clients(true).empty?, "New firm should have reloaded client objects"
+ assert_equal 1, firm.clients(true).size, "New firm should have reloaded clients count"
+ end
+
+ def test_storing_in_pstore
+ require "tmpdir"
+ store_filename = File.join(Dir.tmpdir, "ar-pstore-association-test")
+ File.delete(store_filename) if File.exist?(store_filename)
+ require "pstore"
+ apple = Firm.create("name" => "Apple")
+ natural = Client.new("name" => "Natural Company")
+ apple.clients << natural
+
+ db = PStore.new(store_filename)
+ db.transaction do
+ db["apple"] = apple
+ end
+
+ db = PStore.new(store_filename)
+ db.transaction do
+ assert_equal "Natural Company", db["apple"].clients.first.name
+ end
+ end
+end
+
+class AssociationProxyTest < ActiveSupport::TestCase
+ fixtures :authors, :posts, :categorizations, :categories, :developers, :projects, :developers_projects
+
+ def test_proxy_accessors
+ welcome = posts(:welcome)
+ assert_equal welcome, welcome.author.proxy_owner
+ assert_equal welcome.class.reflect_on_association(:author), welcome.author.proxy_reflection
+ welcome.author.class # force load target
+ assert_equal welcome.author, welcome.author.proxy_target
+
+ david = authors(:david)
+ assert_equal david, david.posts.proxy_owner
+ assert_equal david.class.reflect_on_association(:posts), david.posts.proxy_reflection
+ david.posts.first # force load target
+ assert_equal david.posts, david.posts.proxy_target
+
+ assert_equal david, david.posts_with_extension.testing_proxy_owner
+ assert_equal david.class.reflect_on_association(:posts_with_extension), david.posts_with_extension.testing_proxy_reflection
+ david.posts_with_extension.first # force load target
+ assert_equal david.posts_with_extension, david.posts_with_extension.testing_proxy_target
+ end
+
+ def test_push_does_not_load_target
+ david = authors(:david)
+
+ david.posts << (post = Post.new(:title => "New on Edge", :body => "More cool stuff!"))
+ assert !david.posts.loaded?
+ assert david.posts.include?(post)
+ end
+
+ def test_push_has_many_through_does_not_load_target
+ david = authors(:david)
+
+ david.categories << categories(:technology)
+ assert !david.categories.loaded?
+ assert david.categories.include?(categories(:technology))
+ end
+
+ def test_push_followed_by_save_does_not_load_target
+ david = authors(:david)
+
+ david.posts << (post = Post.new(:title => "New on Edge", :body => "More cool stuff!"))
+ assert !david.posts.loaded?
+ david.save
+ assert !david.posts.loaded?
+ assert david.posts.include?(post)
+ end
+
+ def test_push_does_not_lose_additions_to_new_record
+ josh = Author.new(:name => "Josh")
+ josh.posts << Post.new(:title => "New on Edge", :body => "More cool stuff!")
+ assert josh.posts.loaded?
+ assert_equal 1, josh.posts.size
+ end
+
+ def test_save_on_parent_does_not_load_target
+ david = developers(:david)
+
+ assert !david.projects.loaded?
+ david.update_attribute(:created_at, Time.now)
+ assert !david.projects.loaded?
+ end
+
+ def test_save_on_parent_saves_children
+ developer = Developer.create :name => "Bryan", :salary => 50_000
+ assert_equal 1, developer.reload.audit_logs.size
+ end
+
+ def test_failed_reload_returns_nil
+ p = setup_dangling_association
+ assert_nil p.author.reload
+ end
+
+ def test_failed_reset_returns_nil
+ p = setup_dangling_association
+ assert_nil p.author.reset
+ end
+
+ def test_reload_returns_assocition
+ david = developers(:david)
+ assert_nothing_raised do
+ assert_equal david.projects, david.projects.reload.reload
+ end
+ end
+
+ def setup_dangling_association
+ josh = Author.create(:name => "Josh")
+ p = Post.create(:title => "New on Edge", :body => "More cool stuff!", :author => josh)
+ josh.destroy
+ p
+ end
+end
+
+class HasOneAssociationsTest < ActiveSupport::TestCase
+ fixtures :accounts, :companies, :developers, :projects, :developers_projects
+
+ def setup
+ Account.destroyed_account_ids.clear
+ end
+
+ def test_has_one
+ assert_equal companies(:first_firm).account, Account.find(1)
+ assert_equal Account.find(1).credit_limit, companies(:first_firm).account.credit_limit
+ end
+
+ def test_has_one_cache_nils
+ firm = companies(:another_firm)
+ assert_queries(1) { assert_nil firm.account }
+ assert_queries(0) { assert_nil firm.account }
+
+ firms = Firm.find(:all, :include => :account)
+ assert_queries(0) { firms.each(&:account) }
+ end
+
+ def test_can_marshal_has_one_association_with_nil_target
+ firm = Firm.new
+ assert_nothing_raised do
+ assert_equal firm.attributes, Marshal.load(Marshal.dump(firm)).attributes
+ end
+
+ firm.account
+ assert_nothing_raised do
+ assert_equal firm.attributes, Marshal.load(Marshal.dump(firm)).attributes
+ end
+ end
+
+ def test_proxy_assignment
+ company = companies(:first_firm)
+ assert_nothing_raised { company.account = company.account }
+ end
+
+ def test_triple_equality
+ assert Account === companies(:first_firm).account
+ assert companies(:first_firm).account === Account
+ end
+
+ def test_type_mismatch
+ assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = 1 }
+ assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = Project.find(1) }
+ end
+
+ def test_natural_assignment
+ apple = Firm.create("name" => "Apple")
+ citibank = Account.create("credit_limit" => 10)
+ apple.account = citibank
+ assert_equal apple.id, citibank.firm_id
+ end
+
+ def test_natural_assignment_to_nil
+ old_account_id = companies(:first_firm).account.id
+ companies(:first_firm).account = nil
+ companies(:first_firm).save
+ assert_nil companies(:first_firm).account
+ # account is dependent, therefore is destroyed when reference to owner is lost
+ assert_raises(ActiveRecord::RecordNotFound) { Account.find(old_account_id) }
+ end
+
+ def test_assignment_without_replacement
+ apple = Firm.create("name" => "Apple")
+ citibank = Account.create("credit_limit" => 10)
+ apple.account = citibank
+ assert_equal apple.id, citibank.firm_id
+
+ hsbc = apple.build_account({ :credit_limit => 20}, false)
+ assert_equal apple.id, hsbc.firm_id
+ hsbc.save
+ assert_equal apple.id, citibank.firm_id
+
+ nykredit = apple.create_account({ :credit_limit => 30}, false)
+ assert_equal apple.id, nykredit.firm_id
+ assert_equal apple.id, citibank.firm_id
+ assert_equal apple.id, hsbc.firm_id
+ end
+
+ def test_assignment_without_replacement_on_create
+ apple = Firm.create("name" => "Apple")
+ citibank = Account.create("credit_limit" => 10)
+ apple.account = citibank
+ assert_equal apple.id, citibank.firm_id
+
+ hsbc = apple.create_account({:credit_limit => 10}, false)
+ assert_equal apple.id, hsbc.firm_id
+ hsbc.save
+ assert_equal apple.id, citibank.firm_id
+ end
+
+ def test_dependence
+ num_accounts = Account.count
+
+ firm = Firm.find(1)
+ assert !firm.account.nil?
+ account_id = firm.account.id
+ assert_equal [], Account.destroyed_account_ids[firm.id]
+
+ firm.destroy
+ assert_equal num_accounts - 1, Account.count
+ assert_equal [account_id], Account.destroyed_account_ids[firm.id]
+ end
+
+ def test_exclusive_dependence
+ num_accounts = Account.count
+
+ firm = ExclusivelyDependentFirm.find(9)
+ assert !firm.account.nil?
+ account_id = firm.account.id
+ assert_equal [], Account.destroyed_account_ids[firm.id]
+
+ firm.destroy
+ assert_equal num_accounts - 1, Account.count
+ assert_equal [], Account.destroyed_account_ids[firm.id]
+ end
+
+ def test_dependence_with_nil_associate
+ firm = DependentFirm.new(:name => 'nullify')
+ firm.save!
+ assert_nothing_raised { firm.destroy }
+ end
+
+ def test_succesful_build_association
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.save
+
+ account = firm.build_account("credit_limit" => 1000)
+ assert account.save
+ assert_equal account, firm.account
+ end
+
+ def test_failing_build_association
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.save
+
+ account = firm.build_account
+ assert !account.save
+ assert_equal "can't be empty", account.errors.on("credit_limit")
+ end
+
+ def test_build_association_twice_without_saving_affects_nothing
+ count_of_account = Account.count
+ firm = Firm.find(:first)
+ account1 = firm.build_account("credit_limit" => 1000)
+ account2 = firm.build_account("credit_limit" => 2000)
+
+ assert_equal count_of_account, Account.count
+ end
+
+ def test_create_association
+ firm = Firm.create(:name => "GlobalMegaCorp")
+ account = firm.create_account(:credit_limit => 1000)
+ assert_equal account, firm.reload.account
+ end
+
+ def test_build
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.save
+
+ firm.account = account = Account.new("credit_limit" => 1000)
+ assert_equal account, firm.account
+ assert account.save
+ assert_equal account, firm.account
+ end
+
+ def test_build_before_child_saved
+ firm = Firm.find(1)
+
+ account = firm.account.build("credit_limit" => 1000)
+ assert_equal account, firm.account
+ assert account.new_record?
+ assert firm.save
+ assert_equal account, firm.account
+ assert !account.new_record?
+ end
+
+ def test_build_before_either_saved
+ firm = Firm.new("name" => "GlobalMegaCorp")
+
+ firm.account = account = Account.new("credit_limit" => 1000)
+ assert_equal account, firm.account
+ assert account.new_record?
+ assert firm.save
+ assert_equal account, firm.account
+ assert !account.new_record?
+ end
+
+ def test_failing_build_association
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.save
+
+ firm.account = account = Account.new
+ assert_equal account, firm.account
+ assert !account.save
+ assert_equal account, firm.account
+ assert_equal "can't be empty", account.errors.on("credit_limit")
+ end
+
+ def test_create
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.save
+ firm.account = account = Account.create("credit_limit" => 1000)
+ assert_equal account, firm.account
+ end
+
+ def test_create_before_save
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.account = account = Account.create("credit_limit" => 1000)
+ assert_equal account, firm.account
+ end
+
+ def test_dependence_with_missing_association
+ Account.destroy_all
+ firm = Firm.find(1)
+ assert firm.account.nil?
+ firm.destroy
+ end
+
+ def test_dependence_with_missing_association_and_nullify
+ Account.destroy_all
+ firm = DependentFirm.find(:first)
+ assert firm.account.nil?
+ firm.destroy
+ end
+
+ def test_assignment_before_parent_saved
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.account = a = Account.find(1)
+ assert firm.new_record?
+ assert_equal a, firm.account
+ assert firm.save
+ assert_equal a, firm.account
+ assert_equal a, firm.account(true)
+ end
+
+ def test_finding_with_interpolated_condition
+ firm = Firm.find(:first)
+ superior = firm.clients.create(:name => 'SuperiorCo')
+ superior.rating = 10
+ superior.save
+ assert_equal 10, firm.clients_with_interpolated_conditions.first.rating
+ end
+
+ def test_assignment_before_child_saved
+ firm = Firm.find(1)
+ firm.account = a = Account.new("credit_limit" => 1000)
+ assert !a.new_record?
+ assert_equal a, firm.account
+ assert_equal a, firm.account
+ assert_equal a, firm.account(true)
+ end
+
+ def test_assignment_before_either_saved
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.account = a = Account.new("credit_limit" => 1000)
+ assert firm.new_record?
+ assert a.new_record?
+ assert_equal a, firm.account
+ assert firm.save
+ assert !firm.new_record?
+ assert !a.new_record?
+ assert_equal a, firm.account
+ assert_equal a, firm.account(true)
+ end
+
+ def test_not_resaved_when_unchanged
+ firm = Firm.find(:first, :include => :account)
+ assert_queries(1) { firm.save! }
+
+ firm = Firm.find(:first)
+ firm.account = Account.find(:first)
+ assert_queries(1) { firm.save! }
+
+ firm = Firm.find(:first).clone
+ firm.account = Account.find(:first)
+ assert_queries(2) { firm.save! }
+
+ firm = Firm.find(:first).clone
+ firm.account = Account.find(:first).clone
+ assert_queries(2) { firm.save! }
+ end
+
+ def test_save_still_works_after_accessing_nil_has_one
+ jp = Company.new :name => 'Jaded Pixel'
+ jp.dummy_account.nil?
+
+ assert_nothing_raised do
+ jp.save!
+ end
+ end
+
+end
+
+
+class HasManyAssociationsTest < ActiveSupport::TestCase
+ fixtures :accounts, :companies, :developers, :projects,
+ :developers_projects, :topics, :authors, :comments
+
+ def setup
+ Client.destroyed_client_ids.clear
+ end
+
+ def force_signal37_to_load_all_clients_of_firm
+ companies(:first_firm).clients_of_firm.each {|f| }
+ end
+
+ def test_counting_with_counter_sql
+ assert_equal 2, Firm.find(:first).clients.count
+ end
+
+ def test_counting
+ assert_equal 2, Firm.find(:first).plain_clients.count
+ end
+
+ def test_counting_with_single_conditions
+ assert_equal 2, Firm.find(:first).plain_clients.count(:conditions => '1=1')
+ end
+
+ def test_counting_with_single_hash
+ assert_equal 2, Firm.find(:first).plain_clients.count(:conditions => '1=1')
+ end
+
+ def test_counting_with_column_name_and_hash
+ assert_equal 2, Firm.find(:first).plain_clients.count(:all, :conditions => '1=1')
+ end
+
+ def test_finding
+ assert_equal 2, Firm.find(:first).clients.length
+ end
+
+ def test_find_many_with_merged_options
+ assert_equal 1, companies(:first_firm).limited_clients.size
+ assert_equal 1, companies(:first_firm).limited_clients.find(:all).size
+ assert_equal 2, companies(:first_firm).limited_clients.find(:all, :limit => nil).size
+ end
+
+ def test_dynamic_find_should_respect_association_order
+ assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.find(:first, :conditions => "type = 'Client'")
+ assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.find_by_type('Client')
+ end
+
+ def test_dynamic_find_order_should_override_association_order
+ assert_equal companies(:first_client), companies(:first_firm).clients_sorted_desc.find(:first, :conditions => "type = 'Client'", :order => 'id')
+ assert_equal companies(:first_client), companies(:first_firm).clients_sorted_desc.find_by_type('Client', :order => 'id')
+ end
+
+ def test_dynamic_find_all_should_respect_association_order
+ assert_equal [companies(:second_client), companies(:first_client)], companies(:first_firm).clients_sorted_desc.find(:all, :conditions => "type = 'Client'")
+ assert_equal [companies(:second_client), companies(:first_client)], companies(:first_firm).clients_sorted_desc.find_all_by_type('Client')
+ end
+
+ def test_dynamic_find_all_order_should_override_association_order
+ assert_equal [companies(:first_client), companies(:second_client)], companies(:first_firm).clients_sorted_desc.find(:all, :conditions => "type = 'Client'", :order => 'id')
+ assert_equal [companies(:first_client), companies(:second_client)], companies(:first_firm).clients_sorted_desc.find_all_by_type('Client', :order => 'id')
+ end
+
+ def test_dynamic_find_all_should_respect_association_limit
+ assert_equal 1, companies(:first_firm).limited_clients.find(:all, :conditions => "type = 'Client'").length
+ assert_equal 1, companies(:first_firm).limited_clients.find_all_by_type('Client').length
+ end
+
+ def test_dynamic_find_all_limit_should_override_association_limit
+ assert_equal 2, companies(:first_firm).limited_clients.find(:all, :conditions => "type = 'Client'", :limit => 9_000).length
+ assert_equal 2, companies(:first_firm).limited_clients.find_all_by_type('Client', :limit => 9_000).length
+ end
+
+ def test_triple_equality
+ assert !(Array === Firm.find(:first).clients)
+ assert Firm.find(:first).clients === Array
+ end
+
+ def test_finding_default_orders
+ assert_equal "Summit", Firm.find(:first).clients.first.name
+ end
+
+ def test_finding_with_different_class_name_and_order
+ assert_equal "Microsoft", Firm.find(:first).clients_sorted_desc.first.name
+ end
+
+ def test_finding_with_foreign_key
+ assert_equal "Microsoft", Firm.find(:first).clients_of_firm.first.name
+ end
+
+ def test_finding_with_condition
+ assert_equal "Microsoft", Firm.find(:first).clients_like_ms.first.name
+ end
+
+ def test_finding_with_condition_hash
+ assert_equal "Microsoft", Firm.find(:first).clients_like_ms_with_hash_conditions.first.name
+ end
+
+ def test_finding_using_sql
+ firm = Firm.find(:first)
+ first_client = firm.clients_using_sql.first
+ assert_not_nil first_client
+ assert_equal "Microsoft", first_client.name
+ assert_equal 1, firm.clients_using_sql.size
+ assert_equal 1, Firm.find(:first).clients_using_sql.size
+ end
+
+ def test_counting_using_sql
+ assert_equal 1, Firm.find(:first).clients_using_counter_sql.size
+ assert Firm.find(:first).clients_using_counter_sql.any?
+ assert_equal 0, Firm.find(:first).clients_using_zero_counter_sql.size
+ assert !Firm.find(:first).clients_using_zero_counter_sql.any?
+ end
+
+ def test_counting_non_existant_items_using_sql
+ assert_equal 0, Firm.find(:first).no_clients_using_counter_sql.size
+ end
+
+ def test_belongs_to_sanity
+ c = Client.new
+ assert_nil c.firm
+
+ if c.firm
+ assert false, "belongs_to failed if check"
+ end
+
+ unless c.firm
+ else
+ assert false, "belongs_to failed unless check"
+ end
+ end
+
+ def test_find_ids
+ firm = Firm.find(:first)
+
+ assert_raises(ActiveRecord::RecordNotFound) { firm.clients.find }
+
+ client = firm.clients.find(2)
+ assert_kind_of Client, client
+
+ client_ary = firm.clients.find([2])
+ assert_kind_of Array, client_ary
+ assert_equal client, client_ary.first
+
+ client_ary = firm.clients.find(2, 3)
+ assert_kind_of Array, client_ary
+ assert_equal 2, client_ary.size
+ assert_equal client, client_ary.first
+
+ assert_raises(ActiveRecord::RecordNotFound) { firm.clients.find(2, 99) }
+ end
+
+ def test_find_string_ids_when_using_finder_sql
+ firm = Firm.find(:first)
+
+ client = firm.clients_using_finder_sql.find("2")
+ assert_kind_of Client, client
+
+ client_ary = firm.clients_using_finder_sql.find(["2"])
+ assert_kind_of Array, client_ary
+ assert_equal client, client_ary.first
+
+ client_ary = firm.clients_using_finder_sql.find("2", "3")
+ assert_kind_of Array, client_ary
+ assert_equal 2, client_ary.size
+ assert client_ary.include?(client)
+ end
+
+ def test_find_all
+ firm = Firm.find(:first)
+ assert_equal 2, firm.clients.find(:all, :conditions => "#{QUOTED_TYPE} = 'Client'").length
+ assert_equal 1, firm.clients.find(:all, :conditions => "name = 'Summit'").length
+ end
+
+ def test_find_all_sanitized
+ firm = Firm.find(:first)
+ summit = firm.clients.find(:all, :conditions => "name = 'Summit'")
+ assert_equal summit, firm.clients.find(:all, :conditions => ["name = ?", "Summit"])
+ assert_equal summit, firm.clients.find(:all, :conditions => ["name = :name", { :name => "Summit" }])
+ end
+
+ def test_find_first
+ firm = Firm.find(:first)
+ client2 = Client.find(2)
+ assert_equal firm.clients.first, firm.clients.find(:first)
+ assert_equal client2, firm.clients.find(:first, :conditions => "#{QUOTED_TYPE} = 'Client'")
+ end
+
+ def test_find_first_sanitized
+ firm = Firm.find(:first)
+ client2 = Client.find(2)
+ assert_equal client2, firm.clients.find(:first, :conditions => ["#{QUOTED_TYPE} = ?", 'Client'])
+ assert_equal client2, firm.clients.find(:first, :conditions => ["#{QUOTED_TYPE} = :type", { :type => 'Client' }])
+ end
+
+ def test_find_in_collection
+ assert_equal Client.find(2).name, companies(:first_firm).clients.find(2).name
+ assert_raises(ActiveRecord::RecordNotFound) { companies(:first_firm).clients.find(6) }
+ end
+
+ def test_find_grouped
+ all_clients_of_firm1 = Client.find(:all, :conditions => "firm_id = 1")
+ grouped_clients_of_firm1 = Client.find(:all, :conditions => "firm_id = 1", :group => "firm_id", :select => 'firm_id, count(id) as clients_count')
+ assert_equal 2, all_clients_of_firm1.size
+ assert_equal 1, grouped_clients_of_firm1.size
+ end
+
+ def test_adding
+ force_signal37_to_load_all_clients_of_firm
+ natural = Client.new("name" => "Natural Company")
+ companies(:first_firm).clients_of_firm << natural
+ assert_equal 2, companies(:first_firm).clients_of_firm.size # checking via the collection
+ assert_equal 2, companies(:first_firm).clients_of_firm(true).size # checking using the db
+ assert_equal natural, companies(:first_firm).clients_of_firm.last
+ end
+
+ def test_adding_using_create
+ first_firm = companies(:first_firm)
+ assert_equal 2, first_firm.plain_clients.size
+ natural = first_firm.plain_clients.create(:name => "Natural Company")
+ assert_equal 3, first_firm.plain_clients.length
+ assert_equal 3, first_firm.plain_clients.size
+ end
+
+ def test_create_with_bang_on_has_many_when_parent_is_new_raises
+ assert_raises(ActiveRecord::RecordNotSaved) do
+ firm = Firm.new
+ firm.plain_clients.create! :name=>"Whoever"
+ end
+ end
+
+ def test_regular_create_on_has_many_when_parent_is_new_raises
+ assert_raises(ActiveRecord::RecordNotSaved) do
+ firm = Firm.new
+ firm.plain_clients.create :name=>"Whoever"
+ end
+ end
+
+ def test_create_with_bang_on_has_many_raises_when_record_not_saved
+ assert_raises(ActiveRecord::RecordInvalid) do
+ firm = Firm.find(:first)
+ firm.plain_clients.create!
+ end
+ end
+
+ def test_create_with_bang_on_habtm_when_parent_is_new_raises
+ assert_raises(ActiveRecord::RecordNotSaved) do
+ Developer.new("name" => "Aredridel").projects.create!
+ end
+ end
+
+ def test_adding_a_mismatch_class
+ assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << nil }
+ assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << 1 }
+ assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << Topic.find(1) }
+ end
+
+ def test_adding_a_collection
+ force_signal37_to_load_all_clients_of_firm
+ companies(:first_firm).clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")])
+ assert_equal 3, companies(:first_firm).clients_of_firm.size
+ assert_equal 3, companies(:first_firm).clients_of_firm(true).size
+ end
+
+ def test_adding_before_save
+ no_of_firms = Firm.count
+ no_of_clients = Client.count
+
+ new_firm = Firm.new("name" => "A New Firm, Inc")
+ c = Client.new("name" => "Apple")
+
+ new_firm.clients_of_firm.push Client.new("name" => "Natural Company")
+ assert_equal 1, new_firm.clients_of_firm.size
+ new_firm.clients_of_firm << c
+ assert_equal 2, new_firm.clients_of_firm.size
+
+ assert_equal no_of_firms, Firm.count # Firm was not saved to database.
+ assert_equal no_of_clients, Client.count # Clients were not saved to database.
+ assert new_firm.save
+ assert !new_firm.new_record?
+ assert !c.new_record?
+ assert_equal new_firm, c.firm
+ assert_equal no_of_firms+1, Firm.count # Firm was saved to database.
+ assert_equal no_of_clients+2, Client.count # Clients were saved to database.
+
+ assert_equal 2, new_firm.clients_of_firm.size
+ assert_equal 2, new_firm.clients_of_firm(true).size
+ end
+
+ def test_invalid_adding
+ firm = Firm.find(1)
+ assert !(firm.clients_of_firm << c = Client.new)
+ assert c.new_record?
+ assert !firm.valid?
+ assert !firm.save
+ assert c.new_record?
+ end
+
+ def test_invalid_adding_before_save
+ no_of_firms = Firm.count
+ no_of_clients = Client.count
+ new_firm = Firm.new("name" => "A New Firm, Inc")
+ new_firm.clients_of_firm.concat([c = Client.new, Client.new("name" => "Apple")])
+ assert c.new_record?
+ assert !c.valid?
+ assert !new_firm.valid?
+ assert !new_firm.save
+ assert c.new_record?
+ assert new_firm.new_record?
+ end
+
+ def test_build
+ new_client = companies(:first_firm).clients_of_firm.build("name" => "Another Client")
+ assert_equal "Another Client", new_client.name
+ assert new_client.new_record?
+ assert_equal new_client, companies(:first_firm).clients_of_firm.last
+ assert companies(:first_firm).save
+ assert !new_client.new_record?
+ assert_equal 2, companies(:first_firm).clients_of_firm(true).size
+ end
+
+ def test_build_many
+ new_clients = companies(:first_firm).clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}])
+ assert_equal 2, new_clients.size
+
+ assert companies(:first_firm).save
+ assert_equal 3, companies(:first_firm).clients_of_firm(true).size
+ end
+
+ def test_build_followed_by_save_does_not_load_target
+ new_client = companies(:first_firm).clients_of_firm.build("name" => "Another Client")
+ assert companies(:first_firm).save
+ assert !companies(:first_firm).clients_of_firm.loaded?
+ end
+
+ def test_build_without_loading_association
+ first_topic = topics(:first)
+ Reply.column_names
+
+ assert_equal 1, first_topic.replies.length
+
+ assert_no_queries do
+ first_topic.replies.build(:title => "Not saved", :content => "Superstars")
+ assert_equal 2, first_topic.replies.size
+ end
+
+ assert_equal 2, first_topic.replies.to_ary.size
+ end
+
+ def test_create_without_loading_association
+ first_firm = companies(:first_firm)
+ Firm.column_names
+ Client.column_names
+
+ assert_equal 1, first_firm.clients_of_firm.size
+ first_firm.clients_of_firm.reset
+
+ assert_queries(1) do
+ first_firm.clients_of_firm.create(:name => "Superstars")
+ end
+
+ assert_equal 2, first_firm.clients_of_firm.size
+ end
+
+ def test_invalid_build
+ new_client = companies(:first_firm).clients_of_firm.build
+ assert new_client.new_record?
+ assert !new_client.valid?
+ assert_equal new_client, companies(:first_firm).clients_of_firm.last
+ assert !companies(:first_firm).save
+ assert new_client.new_record?
+ assert_equal 1, companies(:first_firm).clients_of_firm(true).size
+ end
+
+ def test_create
+ force_signal37_to_load_all_clients_of_firm
+ new_client = companies(:first_firm).clients_of_firm.create("name" => "Another Client")
+ assert !new_client.new_record?
+ assert_equal new_client, companies(:first_firm).clients_of_firm.last
+ assert_equal new_client, companies(:first_firm).clients_of_firm(true).last
+ end
+
+ def test_create_many
+ companies(:first_firm).clients_of_firm.create([{"name" => "Another Client"}, {"name" => "Another Client II"}])
+ assert_equal 3, companies(:first_firm).clients_of_firm(true).size
+ end
+
+ def test_create_followed_by_save_does_not_load_target
+ new_client = companies(:first_firm).clients_of_firm.create("name" => "Another Client")
+ assert companies(:first_firm).save
+ assert !companies(:first_firm).clients_of_firm.loaded?
+ end
+
+ def test_find_or_initialize
+ the_client = companies(:first_firm).clients.find_or_initialize_by_name("Yet another client")
+ assert_equal companies(:first_firm).id, the_client.firm_id
+ assert_equal "Yet another client", the_client.name
+ assert the_client.new_record?
+ end
+
+ def test_find_or_create
+ number_of_clients = companies(:first_firm).clients.size
+ the_client = companies(:first_firm).clients.find_or_create_by_name("Yet another client")
+ assert_equal number_of_clients + 1, companies(:first_firm, :reload).clients.size
+ assert_equal the_client, companies(:first_firm).clients.find_or_create_by_name("Yet another client")
+ assert_equal number_of_clients + 1, companies(:first_firm, :reload).clients.size
+ end
+
+ def test_deleting
+ force_signal37_to_load_all_clients_of_firm
+ companies(:first_firm).clients_of_firm.delete(companies(:first_firm).clients_of_firm.first)
+ assert_equal 0, companies(:first_firm).clients_of_firm.size
+ assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+ end
+
+ def test_deleting_before_save
+ new_firm = Firm.new("name" => "A New Firm, Inc.")
+ new_client = new_firm.clients_of_firm.build("name" => "Another Client")
+ assert_equal 1, new_firm.clients_of_firm.size
+ new_firm.clients_of_firm.delete(new_client)
+ assert_equal 0, new_firm.clients_of_firm.size
+ end
+
+ def test_deleting_a_collection
+ force_signal37_to_load_all_clients_of_firm
+ companies(:first_firm).clients_of_firm.create("name" => "Another Client")
+ assert_equal 2, companies(:first_firm).clients_of_firm.size
+ companies(:first_firm).clients_of_firm.delete([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1]])
+ assert_equal 0, companies(:first_firm).clients_of_firm.size
+ assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+ end
+
+ def test_delete_all
+ force_signal37_to_load_all_clients_of_firm
+ companies(:first_firm).clients_of_firm.create("name" => "Another Client")
+ assert_equal 2, companies(:first_firm).clients_of_firm.size
+ companies(:first_firm).clients_of_firm.delete_all
+ assert_equal 0, companies(:first_firm).clients_of_firm.size
+ assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+ end
+
+ def test_delete_all_with_not_yet_loaded_association_collection
+ force_signal37_to_load_all_clients_of_firm
+ companies(:first_firm).clients_of_firm.create("name" => "Another Client")
+ assert_equal 2, companies(:first_firm).clients_of_firm.size
+ companies(:first_firm).clients_of_firm.reset
+ companies(:first_firm).clients_of_firm.delete_all
+ assert_equal 0, companies(:first_firm).clients_of_firm.size
+ assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+ end
+
+ def test_clearing_an_association_collection
+ firm = companies(:first_firm)
+ client_id = firm.clients_of_firm.first.id
+ assert_equal 1, firm.clients_of_firm.size
+
+ firm.clients_of_firm.clear
+
+ assert_equal 0, firm.clients_of_firm.size
+ assert_equal 0, firm.clients_of_firm(true).size
+ assert_equal [], Client.destroyed_client_ids[firm.id]
+
+ # Should not be destroyed since the association is not dependent.
+ assert_nothing_raised do
+ assert Client.find(client_id).firm.nil?
+ end
+ end
+
+ def test_clearing_a_dependent_association_collection
+ firm = companies(:first_firm)
+ client_id = firm.dependent_clients_of_firm.first.id
+ assert_equal 1, firm.dependent_clients_of_firm.size
+
+ # :dependent means destroy is called on each client
+ firm.dependent_clients_of_firm.clear
+
+ assert_equal 0, firm.dependent_clients_of_firm.size
+ assert_equal 0, firm.dependent_clients_of_firm(true).size
+ assert_equal [client_id], Client.destroyed_client_ids[firm.id]
+
+ # Should be destroyed since the association is dependent.
+ assert Client.find_by_id(client_id).nil?
+ end
+
+ def test_clearing_an_exclusively_dependent_association_collection
+ firm = companies(:first_firm)
+ client_id = firm.exclusively_dependent_clients_of_firm.first.id
+ assert_equal 1, firm.exclusively_dependent_clients_of_firm.size
+
+ assert_equal [], Client.destroyed_client_ids[firm.id]
+
+ # :exclusively_dependent means each client is deleted directly from
+ # the database without looping through them calling destroy.
+ firm.exclusively_dependent_clients_of_firm.clear
+
+ assert_equal 0, firm.exclusively_dependent_clients_of_firm.size
+ assert_equal 0, firm.exclusively_dependent_clients_of_firm(true).size
+ # no destroy-filters should have been called
+ assert_equal [], Client.destroyed_client_ids[firm.id]
+
+ # Should be destroyed since the association is exclusively dependent.
+ assert Client.find_by_id(client_id).nil?
+ end
+
+ def test_dependent_association_respects_optional_conditions_on_delete
+ firm = companies(:odegy)
+ Client.create(:client_of => firm.id, :name => "BigShot Inc.")
+ Client.create(:client_of => firm.id, :name => "SmallTime Inc.")
+ # only one of two clients is included in the association due to the :conditions key
+ assert_equal 2, Client.find_all_by_client_of(firm.id).size
+ assert_equal 1, firm.dependent_conditional_clients_of_firm.size
+ firm.destroy
+ # only the correctly associated client should have been deleted
+ assert_equal 1, Client.find_all_by_client_of(firm.id).size
+ end
+
+ def test_dependent_association_respects_optional_sanitized_conditions_on_delete
+ firm = companies(:odegy)
+ Client.create(:client_of => firm.id, :name => "BigShot Inc.")
+ Client.create(:client_of => firm.id, :name => "SmallTime Inc.")
+ # only one of two clients is included in the association due to the :conditions key
+ assert_equal 2, Client.find_all_by_client_of(firm.id).size
+ assert_equal 1, firm.dependent_sanitized_conditional_clients_of_firm.size
+ firm.destroy
+ # only the correctly associated client should have been deleted
+ assert_equal 1, Client.find_all_by_client_of(firm.id).size
+ end
+
+ def test_clearing_without_initial_access
+ firm = companies(:first_firm)
+
+ firm.clients_of_firm.clear
+
+ assert_equal 0, firm.clients_of_firm.size
+ assert_equal 0, firm.clients_of_firm(true).size
+ end
+
+ def test_deleting_a_item_which_is_not_in_the_collection
+ force_signal37_to_load_all_clients_of_firm
+ summit = Client.find_by_name('Summit')
+ companies(:first_firm).clients_of_firm.delete(summit)
+ assert_equal 1, companies(:first_firm).clients_of_firm.size
+ assert_equal 1, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 2, summit.client_of
+ end
+
+ def test_deleting_type_mismatch
+ david = Developer.find(1)
+ david.projects.reload
+ assert_raises(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(1) }
+ end
+
+ def test_deleting_self_type_mismatch
+ david = Developer.find(1)
+ david.projects.reload
+ assert_raises(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(Project.find(1).developers) }
+ end
+
+ def test_destroy_all
+ force_signal37_to_load_all_clients_of_firm
+ assert !companies(:first_firm).clients_of_firm.empty?, "37signals has clients after load"
+ companies(:first_firm).clients_of_firm.destroy_all
+ assert companies(:first_firm).clients_of_firm.empty?, "37signals has no clients after destroy all"
+ assert companies(:first_firm).clients_of_firm(true).empty?, "37signals has no clients after destroy all and refresh"
+ end
+
+ def test_dependence
+ firm = companies(:first_firm)
+ assert_equal 2, firm.clients.size
+ firm.destroy
+ assert Client.find(:all, :conditions => "firm_id=#{firm.id}").empty?
+ end
+
+ def test_destroy_dependent_when_deleted_from_association
+ firm = Firm.find(:first)
+ assert_equal 2, firm.clients.size
+
+ client = firm.clients.first
+ firm.clients.delete(client)
+
+ assert_raise(ActiveRecord::RecordNotFound) { Client.find(client.id) }
+ assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find(client.id) }
+ assert_equal 1, firm.clients.size
+ end
+
+ def test_three_levels_of_dependence
+ topic = Topic.create "title" => "neat and simple"
+ reply = topic.replies.create "title" => "neat and simple", "content" => "still digging it"
+ silly_reply = reply.replies.create "title" => "neat and simple", "content" => "ain't complaining"
+
+ assert_nothing_raised { topic.destroy }
+ end
+
+ uses_transaction :test_dependence_with_transaction_support_on_failure
+ def test_dependence_with_transaction_support_on_failure
+ firm = companies(:first_firm)
+ clients = firm.clients
+ assert_equal 2, clients.length
+ clients.last.instance_eval { def before_destroy() raise "Trigger rollback" end }
+
+ firm.destroy rescue "do nothing"
+
+ assert_equal 2, Client.find(:all, :conditions => "firm_id=#{firm.id}").size
+ end
+
+ def test_dependence_on_account
+ num_accounts = Account.count
+ companies(:first_firm).destroy
+ assert_equal num_accounts - 1, Account.count
+ end
+
+ def test_depends_and_nullify
+ num_accounts = Account.count
+ num_companies = Company.count
+
+ core = companies(:rails_core)
+ assert_equal accounts(:rails_core_account), core.account
+ assert_equal companies(:leetsoft, :jadedpixel), core.companies
+ core.destroy
+ assert_nil accounts(:rails_core_account).reload.firm_id
+ assert_nil companies(:leetsoft).reload.client_of
+ assert_nil companies(:jadedpixel).reload.client_of
+
+
+ assert_equal num_accounts, Account.count
+ end
+
+ def test_included_in_collection
+ assert companies(:first_firm).clients.include?(Client.find(2))
+ end
+
+ def test_adding_array_and_collection
+ assert_nothing_raised { Firm.find(:first).clients + Firm.find(:all).last.clients }
+ end
+
+ def test_find_all_without_conditions
+ firm = companies(:first_firm)
+ assert_equal 2, firm.clients.find(:all).length
+ end
+
+ def test_replace_with_less
+ firm = Firm.find(:first)
+ firm.clients = [companies(:first_client)]
+ assert firm.save, "Could not save firm"
+ firm.reload
+ assert_equal 1, firm.clients.length
+ end
+
+ def test_replace_with_less_and_dependent_nullify
+ num_companies = Company.count
+ companies(:rails_core).companies = []
+ assert_equal num_companies, Company.count
+ end
+
+ def test_replace_with_new
+ firm = Firm.find(:first)
+ firm.clients = [companies(:second_client), Client.new("name" => "New Client")]
+ firm.save
+ firm.reload
+ assert_equal 2, firm.clients.length
+ assert !firm.clients.include?(:first_client)
+ end
+
+ def test_replace_on_new_object
+ firm = Firm.new("name" => "New Firm")
+ firm.clients = [companies(:second_client), Client.new("name" => "New Client")]
+ assert firm.save
+ firm.reload
+ assert_equal 2, firm.clients.length
+ assert firm.clients.include?(Client.find_by_name("New Client"))
+ end
+
+ def test_get_ids
+ assert_equal [companies(:first_client).id, companies(:second_client).id], companies(:first_firm).client_ids
+ end
+
+ def test_assign_ids
+ firm = Firm.new("name" => "Apple")
+ firm.client_ids = [companies(:first_client).id, companies(:second_client).id]
+ firm.save
+ firm.reload
+ assert_equal 2, firm.clients.length
+ assert firm.clients.include?(companies(:second_client))
+ end
+
+ def test_assign_ids_ignoring_blanks
+ firm = Firm.create!(:name => 'Apple')
+ firm.client_ids = [companies(:first_client).id, nil, companies(:second_client).id, '']
+ firm.save!
+
+ assert_equal 2, firm.clients(true).size
+ assert firm.clients.include?(companies(:second_client))
+ end
+
+ def test_get_ids_for_through
+ assert_equal [comments(:eager_other_comment1).id], authors(:mary).comment_ids
+ end
+
+ def test_assign_ids_for_through
+ assert_raise(NoMethodError) { authors(:mary).comment_ids = [123] }
+ end
+
+ def test_dynamic_find_should_respect_association_order_for_through
+ assert_equal Comment.find(10), authors(:david).comments_desc.find(:first, :conditions => "comments.type = 'SpecialComment'")
+ assert_equal Comment.find(10), authors(:david).comments_desc.find_by_type('SpecialComment')
+ end
+
+ def test_dynamic_find_order_should_override_association_order_for_through
+ assert_equal Comment.find(3), authors(:david).comments_desc.find(:first, :conditions => "comments.type = 'SpecialComment'", :order => 'comments.id')
+ assert_equal Comment.find(3), authors(:david).comments_desc.find_by_type('SpecialComment', :order => 'comments.id')
+ end
+
+ def test_dynamic_find_all_should_respect_association_order_for_through
+ assert_equal [Comment.find(10), Comment.find(7), Comment.find(6), Comment.find(3)], authors(:david).comments_desc.find(:all, :conditions => "comments.type = 'SpecialComment'")
+ assert_equal [Comment.find(10), Comment.find(7), Comment.find(6), Comment.find(3)], authors(:david).comments_desc.find_all_by_type('SpecialComment')
+ end
+
+ def test_dynamic_find_all_order_should_override_association_order_for_through
+ assert_equal [Comment.find(3), Comment.find(6), Comment.find(7), Comment.find(10)], authors(:david).comments_desc.find(:all, :conditions => "comments.type = 'SpecialComment'", :order => 'comments.id')
+ assert_equal [Comment.find(3), Comment.find(6), Comment.find(7), Comment.find(10)], authors(:david).comments_desc.find_all_by_type('SpecialComment', :order => 'comments.id')
+ end
+
+ def test_dynamic_find_all_should_respect_association_limit_for_through
+ assert_equal 1, authors(:david).limited_comments.find(:all, :conditions => "comments.type = 'SpecialComment'").length
+ assert_equal 1, authors(:david).limited_comments.find_all_by_type('SpecialComment').length
+ end
+
+ def test_dynamic_find_all_order_should_override_association_limit_for_through
+ assert_equal 4, authors(:david).limited_comments.find(:all, :conditions => "comments.type = 'SpecialComment'", :limit => 9_000).length
+ assert_equal 4, authors(:david).limited_comments.find_all_by_type('SpecialComment', :limit => 9_000).length
+ end
+
+end
+
+class BelongsToAssociationsTest < ActiveSupport::TestCase
+ fixtures :accounts, :companies, :developers, :projects, :topics,
+ :developers_projects, :computers, :authors, :posts, :tags, :taggings
+
+ def test_belongs_to
+ Client.find(3).firm.name
+ assert_equal companies(:first_firm).name, Client.find(3).firm.name
+ assert !Client.find(3).firm.nil?, "Microsoft should have a firm"
+ end
+
+ def test_proxy_assignment
+ account = Account.find(1)
+ assert_nothing_raised { account.firm = account.firm }
+ end
+
+ def test_triple_equality
+ assert Client.find(3).firm === Firm
+ assert Firm === Client.find(3).firm
+ end
+
+ def test_type_mismatch
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { Account.find(1).firm = 1 }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { Account.find(1).firm = Project.find(1) }
+ end
+
+ def test_natural_assignment
+ apple = Firm.create("name" => "Apple")
+ citibank = Account.create("credit_limit" => 10)
+ citibank.firm = apple
+ assert_equal apple.id, citibank.firm_id
+ end
+
+ def test_no_unexpected_aliasing
+ first_firm = companies(:first_firm)
+ another_firm = companies(:another_firm)
+
+ citibank = Account.create("credit_limit" => 10)
+ citibank.firm = first_firm
+ original_proxy = citibank.firm
+ citibank.firm = another_firm
+
+ assert_equal first_firm.object_id, original_proxy.object_id
+ assert_equal another_firm.object_id, citibank.firm.object_id
+ end
+
+ def test_creating_the_belonging_object
+ citibank = Account.create("credit_limit" => 10)
+ apple = citibank.create_firm("name" => "Apple")
+ assert_equal apple, citibank.firm
+ citibank.save
+ citibank.reload
+ assert_equal apple, citibank.firm
+ end
+
+ def test_building_the_belonging_object
+ citibank = Account.create("credit_limit" => 10)
+ apple = citibank.build_firm("name" => "Apple")
+ citibank.save
+ assert_equal apple.id, citibank.firm_id
+ end
+
+ def test_natural_assignment_to_nil
+ client = Client.find(3)
+ client.firm = nil
+ client.save
+ assert_nil client.firm(true)
+ assert_nil client.client_of
+ end
+
+ def test_with_different_class_name
+ assert_equal Company.find(1).name, Company.find(3).firm_with_other_name.name
+ assert_not_nil Company.find(3).firm_with_other_name, "Microsoft should have a firm"
+ end
+
+ def test_with_condition
+ assert_equal Company.find(1).name, Company.find(3).firm_with_condition.name
+ assert_not_nil Company.find(3).firm_with_condition, "Microsoft should have a firm"
+ end
+
+ def test_belongs_to_counter
+ debate = Topic.create("title" => "debate")
+ assert_equal 0, debate.send(:read_attribute, "replies_count"), "No replies yet"
+
+ trash = debate.replies.create("title" => "blah!", "content" => "world around!")
+ assert_equal 1, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply created"
+
+ trash.destroy
+ assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply deleted"
+ end
+
+ def test_belongs_to_counter_with_reassigning
+ t1 = Topic.create("title" => "t1")
+ t2 = Topic.create("title" => "t2")
+ r1 = Reply.new("title" => "r1", "content" => "r1")
+ r1.topic = t1
+
+ assert r1.save
+ assert_equal 1, Topic.find(t1.id).replies.size
+ assert_equal 0, Topic.find(t2.id).replies.size
+
+ r1.topic = Topic.find(t2.id)
+
+ assert r1.save
+ assert_equal 0, Topic.find(t1.id).replies.size
+ assert_equal 1, Topic.find(t2.id).replies.size
+
+ r1.topic = nil
+
+ assert_equal 0, Topic.find(t1.id).replies.size
+ assert_equal 0, Topic.find(t2.id).replies.size
+
+ r1.topic = t1
+
+ assert_equal 1, Topic.find(t1.id).replies.size
+ assert_equal 0, Topic.find(t2.id).replies.size
+
+ r1.destroy
+
+ assert_equal 0, Topic.find(t1.id).replies.size
+ assert_equal 0, Topic.find(t2.id).replies.size
+ end
+
+ def test_belongs_to_counter_after_save
+ topic = Topic.create!(:title => "monday night")
+ topic.replies.create!(:title => "re: monday night", :content => "football")
+ assert_equal 1, Topic.find(topic.id)[:replies_count]
+
+ topic.save!
+ assert_equal 1, Topic.find(topic.id)[:replies_count]
+ end
+
+ def test_belongs_to_counter_after_update_attributes
+ topic = Topic.create!(:title => "37s")
+ topic.replies.create!(:title => "re: 37s", :content => "rails")
+ assert_equal 1, Topic.find(topic.id)[:replies_count]
+
+ topic.update_attributes(:title => "37signals")
+ assert_equal 1, Topic.find(topic.id)[:replies_count]
+ end
+
+ def test_belongs_to_counter_after_save
+ topic = Topic.create("title" => "monday night")
+ topic.replies.create("title" => "re: monday night", "content" => "football")
+ assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count")
+
+ topic.save
+ assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count")
+ end
+
+ def test_belongs_to_counter_after_update_attributes
+ topic = Topic.create("title" => "37s")
+ topic.replies.create("title" => "re: 37s", "content" => "rails")
+ assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count")
+
+ topic.update_attributes("title" => "37signals")
+ assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count")
+ end
+
+ def test_assignment_before_parent_saved
+ client = Client.find(:first)
+ apple = Firm.new("name" => "Apple")
+ client.firm = apple
+ assert_equal apple, client.firm
+ assert apple.new_record?
+ assert client.save
+ assert apple.save
+ assert !apple.new_record?
+ assert_equal apple, client.firm
+ assert_equal apple, client.firm(true)
+ end
+
+ def test_assignment_before_child_saved
+ final_cut = Client.new("name" => "Final Cut")
+ firm = Firm.find(1)
+ final_cut.firm = firm
+ assert final_cut.new_record?
+ assert final_cut.save
+ assert !final_cut.new_record?
+ assert !firm.new_record?
+ assert_equal firm, final_cut.firm
+ assert_equal firm, final_cut.firm(true)
+ end
+
+ def test_assignment_before_either_saved
+ final_cut = Client.new("name" => "Final Cut")
+ apple = Firm.new("name" => "Apple")
+ final_cut.firm = apple
+ assert final_cut.new_record?
+ assert apple.new_record?
+ assert final_cut.save
+ assert !final_cut.new_record?
+ assert !apple.new_record?
+ assert_equal apple, final_cut.firm
+ assert_equal apple, final_cut.firm(true)
+ end
+
+ def test_new_record_with_foreign_key_but_no_object
+ c = Client.new("firm_id" => 1)
+ assert_equal Firm.find(:first), c.firm_with_basic_id
+ end
+
+ def test_forgetting_the_load_when_foreign_key_enters_late
+ c = Client.new
+ assert_nil c.firm_with_basic_id
+
+ c.firm_id = 1
+ assert_equal Firm.find(:first), c.firm_with_basic_id
+ end
+
+ def test_field_name_same_as_foreign_key
+ computer = Computer.find(1)
+ assert_not_nil computer.developer, ":foreign key == attribute didn't lock up" # '
+ end
+
+ def test_counter_cache
+ topic = Topic.create :title => "Zoom-zoom-zoom"
+ assert_equal 0, topic[:replies_count]
+
+ reply = Reply.create(:title => "re: zoom", :content => "speedy quick!")
+ reply.topic = topic
+
+ assert_equal 1, topic.reload[:replies_count]
+ assert_equal 1, topic.replies.size
+
+ topic[:replies_count] = 15
+ assert_equal 15, topic.replies.size
+ end
+
+ def test_custom_counter_cache
+ reply = Reply.create(:title => "re: zoom", :content => "speedy quick!")
+ assert_equal 0, reply[:replies_count]
+
+ silly = SillyReply.create(:title => "gaga", :content => "boo-boo")
+ silly.reply = reply
+
+ assert_equal 1, reply.reload[:replies_count]
+ assert_equal 1, reply.replies.size
+
+ reply[:replies_count] = 17
+ assert_equal 17, reply.replies.size
+ end
+
+ def test_store_two_association_with_one_save
+ num_orders = Order.count
+ num_customers = Customer.count
+ order = Order.new
+
+ customer1 = order.billing = Customer.new
+ customer2 = order.shipping = Customer.new
+ assert order.save
+ assert_equal customer1, order.billing
+ assert_equal customer2, order.shipping
+
+ order.reload
+
+ assert_equal customer1, order.billing
+ assert_equal customer2, order.shipping
+
+ assert_equal num_orders +1, Order.count
+ assert_equal num_customers +2, Customer.count
+ end
+
+
+ def test_store_association_in_two_relations_with_one_save
+ num_orders = Order.count
+ num_customers = Customer.count
+ order = Order.new
+
+ customer = order.billing = order.shipping = Customer.new
+ assert order.save
+ assert_equal customer, order.billing
+ assert_equal customer, order.shipping
+
+ order.reload
+
+ assert_equal customer, order.billing
+ assert_equal customer, order.shipping
+
+ assert_equal num_orders +1, Order.count
+ assert_equal num_customers +1, Customer.count
+ end
+
+ def test_store_association_in_two_relations_with_one_save_in_existing_object
+ num_orders = Order.count
+ num_customers = Customer.count
+ order = Order.create
+
+ customer = order.billing = order.shipping = Customer.new
+ assert order.save
+ assert_equal customer, order.billing
+ assert_equal customer, order.shipping
+
+ order.reload
+
+ assert_equal customer, order.billing
+ assert_equal customer, order.shipping
+
+ assert_equal num_orders +1, Order.count
+ assert_equal num_customers +1, Customer.count
+ end
+
+ def test_store_association_in_two_relations_with_one_save_in_existing_object_with_values
+ num_orders = Order.count
+ num_customers = Customer.count
+ order = Order.create
+
+ customer = order.billing = order.shipping = Customer.new
+ assert order.save
+ assert_equal customer, order.billing
+ assert_equal customer, order.shipping
+
+ order.reload
+
+ customer = order.billing = order.shipping = Customer.new
+
+ assert order.save
+ order.reload
+
+ assert_equal customer, order.billing
+ assert_equal customer, order.shipping
+
+ assert_equal num_orders +1, Order.count
+ assert_equal num_customers +2, Customer.count
+ end
+
+
+ def test_association_assignment_sticks
+ post = Post.find(:first)
+
+ author1, author2 = Author.find(:all, :limit => 2)
+ assert_not_nil author1
+ assert_not_nil author2
+
+ # make sure the association is loaded
+ post.author
+
+ # set the association by id, directly
+ post.author_id = author2.id
+
+ # save and reload
+ post.save!
+ post.reload
+
+ # the author id of the post should be the id we set
+ assert_equal post.author_id, author2.id
+ end
+
+end
+
+
+class ProjectWithAfterCreateHook < ActiveRecord::Base
+ set_table_name 'projects'
+ has_and_belongs_to_many :developers,
+ :class_name => "DeveloperForProjectWithAfterCreateHook",
+ :join_table => "developers_projects",
+ :foreign_key => "project_id",
+ :association_foreign_key => "developer_id"
+
+ after_create :add_david
+
+ def add_david
+ david = DeveloperForProjectWithAfterCreateHook.find_by_name('David')
+ david.projects << self
+ end
+end
+
+class DeveloperForProjectWithAfterCreateHook < ActiveRecord::Base
+ set_table_name 'developers'
+ has_and_belongs_to_many :projects,
+ :class_name => "ProjectWithAfterCreateHook",
+ :join_table => "developers_projects",
+ :association_foreign_key => "project_id",
+ :foreign_key => "developer_id"
+end
+
+
+class HasAndBelongsToManyAssociationsTest < ActiveSupport::TestCase
+ fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects
+
+ def test_has_and_belongs_to_many
+ david = Developer.find(1)
+
+ assert !david.projects.empty?
+ assert_equal 2, david.projects.size
+
+ active_record = Project.find(1)
+ assert !active_record.developers.empty?
+ assert_equal 3, active_record.developers.size
+ assert active_record.developers.include?(david)
+ end
+
+ def test_triple_equality
+ assert !(Array === Developer.find(1).projects)
+ assert Developer.find(1).projects === Array
+ end
+
+ def test_adding_single
+ jamis = Developer.find(2)
+ jamis.projects.reload # causing the collection to load
+ action_controller = Project.find(2)
+ assert_equal 1, jamis.projects.size
+ assert_equal 1, action_controller.developers.size
+
+ jamis.projects << action_controller
+
+ assert_equal 2, jamis.projects.size
+ assert_equal 2, jamis.projects(true).size
+ assert_equal 2, action_controller.developers(true).size
+ end
+
+ def test_adding_type_mismatch
+ jamis = Developer.find(2)
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { jamis.projects << nil }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { jamis.projects << 1 }
+ end
+
+ def test_adding_from_the_project
+ jamis = Developer.find(2)
+ action_controller = Project.find(2)
+ action_controller.developers.reload
+ assert_equal 1, jamis.projects.size
+ assert_equal 1, action_controller.developers.size
+
+ action_controller.developers << jamis
+
+ assert_equal 2, jamis.projects(true).size
+ assert_equal 2, action_controller.developers.size
+ assert_equal 2, action_controller.developers(true).size
+ end
+
+ def test_adding_from_the_project_fixed_timestamp
+ jamis = Developer.find(2)
+ action_controller = Project.find(2)
+ action_controller.developers.reload
+ assert_equal 1, jamis.projects.size
+ assert_equal 1, action_controller.developers.size
+ updated_at = jamis.updated_at
+
+ action_controller.developers << jamis
+
+ assert_equal updated_at, jamis.updated_at
+ assert_equal 2, jamis.projects(true).size
+ assert_equal 2, action_controller.developers.size
+ assert_equal 2, action_controller.developers(true).size
+ end
+
+ def test_adding_multiple
+ aredridel = Developer.new("name" => "Aredridel")
+ aredridel.save
+ aredridel.projects.reload
+ aredridel.projects.push(Project.find(1), Project.find(2))
+ assert_equal 2, aredridel.projects.size
+ assert_equal 2, aredridel.projects(true).size
+ end
+
+ def test_adding_a_collection
+ aredridel = Developer.new("name" => "Aredridel")
+ aredridel.save
+ aredridel.projects.reload
+ aredridel.projects.concat([Project.find(1), Project.find(2)])
+ assert_equal 2, aredridel.projects.size
+ assert_equal 2, aredridel.projects(true).size
+ end
+
+ def test_adding_uses_default_values_on_join_table
+ ac = projects(:action_controller)
+ assert !developers(:jamis).projects.include?(ac)
+ developers(:jamis).projects << ac
+
+ assert developers(:jamis, :reload).projects.include?(ac)
+ project = developers(:jamis).projects.detect { |p| p == ac }
+ assert_equal 1, project.access_level.to_i
+ end
+
+ def test_habtm_attribute_access_and_respond_to
+ project = developers(:jamis).projects[0]
+ assert project.has_attribute?("name")
+ assert project.has_attribute?("joined_on")
+ assert project.has_attribute?("access_level")
+ assert project.respond_to?("name")
+ assert project.respond_to?("name=")
+ assert project.respond_to?("name?")
+ assert project.respond_to?("joined_on")
+ # given that the 'join attribute' won't be persisted, I don't
+ # think we should define the mutators
+ #assert project.respond_to?("joined_on=")
+ assert project.respond_to?("joined_on?")
+ assert project.respond_to?("access_level")
+ #assert project.respond_to?("access_level=")
+ assert project.respond_to?("access_level?")
+ end
+
+ def test_habtm_adding_before_save
+ no_of_devels = Developer.count
+ no_of_projects = Project.count
+ aredridel = Developer.new("name" => "Aredridel")
+ aredridel.projects.concat([Project.find(1), p = Project.new("name" => "Projekt")])
+ assert aredridel.new_record?
+ assert p.new_record?
+ assert aredridel.save
+ assert !aredridel.new_record?
+ assert_equal no_of_devels+1, Developer.count
+ assert_equal no_of_projects+1, Project.count
+ assert_equal 2, aredridel.projects.size
+ assert_equal 2, aredridel.projects(true).size
+ end
+
+ def test_habtm_saving_multiple_relationships
+ new_project = Project.new("name" => "Grimetime")
+ amount_of_developers = 4
+ developers = (0...amount_of_developers).collect {|i| Developer.create(:name => "JME #{i}") }.reverse
+
+ new_project.developer_ids = [developers[0].id, developers[1].id]
+ new_project.developers_with_callback_ids = [developers[2].id, developers[3].id]
+ assert new_project.save
+
+ new_project.reload
+ assert_equal amount_of_developers, new_project.developers.size
+ assert_equal developers, new_project.developers
+ end
+
+ def test_habtm_unique_order_preserved
+ assert_equal developers(:poor_jamis, :jamis, :david), projects(:active_record).non_unique_developers
+ assert_equal developers(:poor_jamis, :jamis, :david), projects(:active_record).developers
+ end
+
+ def test_build
+ devel = Developer.find(1)
+ proj = devel.projects.build("name" => "Projekt")
+ assert_equal devel.projects.last, proj
+ assert proj.new_record?
+ devel.save
+ assert !proj.new_record?
+ assert_equal devel.projects.last, proj
+ assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated
+ end
+
+ def test_build_by_new_record
+ devel = Developer.new(:name => "Marcel", :salary => 75000)
+ proj1 = devel.projects.build(:name => "Make bed")
+ proj2 = devel.projects.build(:name => "Lie in it")
+ assert_equal devel.projects.last, proj2
+ assert proj2.new_record?
+ devel.save
+ assert !devel.new_record?
+ assert !proj2.new_record?
+ assert_equal devel.projects.last, proj2
+ assert_equal Developer.find_by_name("Marcel").projects.last, proj2 # prove join table is updated
+ end
+
+ def test_create
+ devel = Developer.find(1)
+ proj = devel.projects.create("name" => "Projekt")
+ assert_equal devel.projects.last, proj
+ assert !proj.new_record?
+ assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated
+ end
+
+ def test_create_by_new_record
+ devel = Developer.new(:name => "Marcel", :salary => 75000)
+ proj1 = devel.projects.build(:name => "Make bed")
+ proj2 = devel.projects.build(:name => "Lie in it")
+ assert_equal devel.projects.last, proj2
+ assert proj2.new_record?
+ devel.save
+ assert !devel.new_record?
+ assert !proj2.new_record?
+ assert_equal devel.projects.last, proj2
+ assert_equal Developer.find_by_name("Marcel").projects.last, proj2 # prove join table is updated
+ end
+
+ def test_uniq_after_the_fact
+ developers(:jamis).projects << projects(:active_record)
+ developers(:jamis).projects << projects(:active_record)
+ assert_equal 3, developers(:jamis).projects.size
+ assert_equal 1, developers(:jamis).projects.uniq.size
+ end
+
+ def test_uniq_before_the_fact
+ projects(:active_record).developers << developers(:jamis)
+ projects(:active_record).developers << developers(:david)
+ assert_equal 3, projects(:active_record, :reload).developers.size
+ end
+
+ def test_deleting
+ david = Developer.find(1)
+ active_record = Project.find(1)
+ david.projects.reload
+ assert_equal 2, david.projects.size
+ assert_equal 3, active_record.developers.size
+
+ david.projects.delete(active_record)
+
+ assert_equal 1, david.projects.size
+ assert_equal 1, david.projects(true).size
+ assert_equal 2, active_record.developers(true).size
+ end
+
+ def test_deleting_array
+ david = Developer.find(1)
+ david.projects.reload
+ david.projects.delete(Project.find(:all))
+ assert_equal 0, david.projects.size
+ assert_equal 0, david.projects(true).size
+ end
+
+ def test_deleting_with_sql
+ david = Developer.find(1)
+ active_record = Project.find(1)
+ active_record.developers.reload
+ assert_equal 3, active_record.developers_by_sql.size
+
+ active_record.developers_by_sql.delete(david)
+ assert_equal 2, active_record.developers_by_sql(true).size
+ end
+
+ def test_deleting_array_with_sql
+ active_record = Project.find(1)
+ active_record.developers.reload
+ assert_equal 3, active_record.developers_by_sql.size
+
+ active_record.developers_by_sql.delete(Developer.find(:all))
+ assert_equal 0, active_record.developers_by_sql(true).size
+ end
+
+ def test_deleting_all
+ david = Developer.find(1)
+ david.projects.reload
+ david.projects.clear
+ assert_equal 0, david.projects.size
+ assert_equal 0, david.projects(true).size
+ end
+
+ def test_removing_associations_on_destroy
+ david = DeveloperWithBeforeDestroyRaise.find(1)
+ assert !david.projects.empty?
+ assert_nothing_raised { david.destroy }
+ assert david.projects.empty?
+ assert DeveloperWithBeforeDestroyRaise.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = 1").empty?
+ end
+
+ def test_additional_columns_from_join_table
+ assert_date_from_db Date.new(2004, 10, 10), Developer.find(1).projects.first.joined_on.to_date
+ end
+
+ def test_destroy_all
+ david = Developer.find(1)
+ david.projects.reload
+ assert !david.projects.empty?
+ david.projects.destroy_all
+ assert david.projects.empty?
+ assert david.projects(true).empty?
+ end
+
+ def test_deprecated_push_with_attributes_was_removed
+ jamis = developers(:jamis)
+ assert_raise(NoMethodError) do
+ jamis.projects.push_with_attributes(projects(:action_controller), :joined_on => Date.today)
+ end
+ end
+
+ def test_associations_with_conditions
+ assert_equal 3, projects(:active_record).developers.size
+ assert_equal 1, projects(:active_record).developers_named_david.size
+ assert_equal 1, projects(:active_record).developers_named_david_with_hash_conditions.size
+
+ assert_equal developers(:david), projects(:active_record).developers_named_david.find(developers(:david).id)
+ assert_equal developers(:david), projects(:active_record).developers_named_david_with_hash_conditions.find(developers(:david).id)
+ assert_equal developers(:david), projects(:active_record).salaried_developers.find(developers(:david).id)
+
+ projects(:active_record).developers_named_david.clear
+ assert_equal 2, projects(:active_record, :reload).developers.size
+ end
+
+ def test_find_in_association
+ # Using sql
+ assert_equal developers(:david), projects(:active_record).developers.find(developers(:david).id), "SQL find"
+
+ # Using ruby
+ active_record = projects(:active_record)
+ active_record.developers.reload
+ assert_equal developers(:david), active_record.developers.find(developers(:david).id), "Ruby find"
+ end
+
+ def test_find_in_association_with_custom_finder_sql
+ assert_equal developers(:david), projects(:active_record).developers_with_finder_sql.find(developers(:david).id), "SQL find"
+
+ active_record = projects(:active_record)
+ active_record.developers_with_finder_sql.reload
+ assert_equal developers(:david), active_record.developers_with_finder_sql.find(developers(:david).id), "Ruby find"
+ end
+
+ def test_find_in_association_with_custom_finder_sql_and_string_id
+ assert_equal developers(:david), projects(:active_record).developers_with_finder_sql.find(developers(:david).id.to_s), "SQL find"
+ end
+
+ def test_find_with_merged_options
+ assert_equal 1, projects(:active_record).limited_developers.size
+ assert_equal 1, projects(:active_record).limited_developers.find(:all).size
+ assert_equal 3, projects(:active_record).limited_developers.find(:all, :limit => nil).size
+ end
+
+ def test_dynamic_find_should_respect_association_order
+ # Developers are ordered 'name DESC, id DESC'
+ low_id_jamis = developers(:jamis)
+ middle_id_jamis = developers(:poor_jamis)
+ high_id_jamis = projects(:active_record).developers.create(:name => 'Jamis')
+
+ assert_equal high_id_jamis, projects(:active_record).developers.find(:first, :conditions => "name = 'Jamis'")
+ assert_equal high_id_jamis, projects(:active_record).developers.find_by_name('Jamis')
+ end
+
+ def test_dynamic_find_order_should_override_association_order
+ # Developers are ordered 'name DESC, id DESC'
+ low_id_jamis = developers(:jamis)
+ middle_id_jamis = developers(:poor_jamis)
+ high_id_jamis = projects(:active_record).developers.create(:name => 'Jamis')
+
+ assert_equal low_id_jamis, projects(:active_record).developers.find(:first, :conditions => "name = 'Jamis'", :order => 'id')
+ assert_equal low_id_jamis, projects(:active_record).developers.find_by_name('Jamis', :order => 'id')
+ end
+
+ def test_dynamic_find_all_should_respect_association_order
+ # Developers are ordered 'name DESC, id DESC'
+ low_id_jamis = developers(:jamis)
+ middle_id_jamis = developers(:poor_jamis)
+ high_id_jamis = projects(:active_record).developers.create(:name => 'Jamis')
+
+ assert_equal [high_id_jamis, middle_id_jamis, low_id_jamis], projects(:active_record).developers.find(:all, :conditions => "name = 'Jamis'")
+ assert_equal [high_id_jamis, middle_id_jamis, low_id_jamis], projects(:active_record).developers.find_all_by_name('Jamis')
+ end
+
+ def test_dynamic_find_all_order_should_override_association_order
+ # Developers are ordered 'name DESC, id DESC'
+ low_id_jamis = developers(:jamis)
+ middle_id_jamis = developers(:poor_jamis)
+ high_id_jamis = projects(:active_record).developers.create(:name => 'Jamis')
+
+ assert_equal [low_id_jamis, middle_id_jamis, high_id_jamis], projects(:active_record).developers.find(:all, :conditions => "name = 'Jamis'", :order => 'id')
+ assert_equal [low_id_jamis, middle_id_jamis, high_id_jamis], projects(:active_record).developers.find_all_by_name('Jamis', :order => 'id')
+ end
+
+ def test_dynamic_find_all_should_respect_association_limit
+ assert_equal 1, projects(:active_record).limited_developers.find(:all, :conditions => "name = 'Jamis'").length
+ assert_equal 1, projects(:active_record).limited_developers.find_all_by_name('Jamis').length
+ end
+
+ def test_dynamic_find_all_order_should_override_association_limit
+ assert_equal 2, projects(:active_record).limited_developers.find(:all, :conditions => "name = 'Jamis'", :limit => 9_000).length
+ assert_equal 2, projects(:active_record).limited_developers.find_all_by_name('Jamis', :limit => 9_000).length
+ end
+
+ def test_new_with_values_in_collection
+ jamis = DeveloperForProjectWithAfterCreateHook.find_by_name('Jamis')
+ david = DeveloperForProjectWithAfterCreateHook.find_by_name('David')
+ project = ProjectWithAfterCreateHook.new(:name => "Cooking with Bertie")
+ project.developers << jamis
+ project.save!
+ project.reload
+
+ assert project.developers.include?(jamis)
+ assert project.developers.include?(david)
+ end
+
+ def test_find_in_association_with_options
+ developers = projects(:active_record).developers.find(:all)
+ assert_equal 3, developers.size
+
+ assert_equal developers(:poor_jamis), projects(:active_record).developers.find(:first, :conditions => "salary < 10000")
+ assert_equal developers(:jamis), projects(:active_record).developers.find(:first, :order => "salary DESC")
+ end
+
+ def test_replace_with_less
+ david = developers(:david)
+ david.projects = [projects(:action_controller)]
+ assert david.save
+ assert_equal 1, david.projects.length
+ end
+
+ def test_replace_with_new
+ david = developers(:david)
+ david.projects = [projects(:action_controller), Project.new("name" => "ActionWebSearch")]
+ david.save
+ assert_equal 2, david.projects.length
+ assert !david.projects.include?(projects(:active_record))
+ end
+
+ def test_replace_on_new_object
+ new_developer = Developer.new("name" => "Matz")
+ new_developer.projects = [projects(:action_controller), Project.new("name" => "ActionWebSearch")]
+ new_developer.save
+ assert_equal 2, new_developer.projects.length
+ end
+
+ def test_consider_type
+ developer = Developer.find(:first)
+ special_project = SpecialProject.create("name" => "Special Project")
+
+ other_project = developer.projects.first
+ developer.special_projects << special_project
+ developer.reload
+
+ assert developer.projects.include?(special_project)
+ assert developer.special_projects.include?(special_project)
+ assert !developer.special_projects.include?(other_project)
+ end
+
+ def test_update_attributes_after_push_without_duplicate_join_table_rows
+ developer = Developer.new("name" => "Kano")
+ project = SpecialProject.create("name" => "Special Project")
+ assert developer.save
+ developer.projects << project
+ developer.update_attribute("name", "Bruza")
+ assert_equal 1, Developer.connection.select_value(<<-end_sql).to_i
+ SELECT count(*) FROM developers_projects
+ WHERE project_id = #{project.id}
+ AND developer_id = #{developer.id}
+ end_sql
+ end
+
+ def test_updating_attributes_on_non_rich_associations
+ welcome = categories(:technology).posts.first
+ welcome.title = "Something else"
+ assert welcome.save!
+ end
+
+ def test_habtm_respects_select
+ categories(:technology).select_testing_posts(true).each do |o|
+ assert_respond_to o, :correctness_marker
+ end
+ assert_respond_to categories(:technology).select_testing_posts.find(:first), :correctness_marker
+ end
+
+ def test_updating_attributes_on_rich_associations
+ david = projects(:action_controller).developers.first
+ david.name = "DHH"
+ assert_raises(ActiveRecord::ReadOnlyRecord) { david.save! }
+ end
+
+ def test_updating_attributes_on_rich_associations_with_limited_find_from_reflection
+ david = projects(:action_controller).selected_developers.first
+ david.name = "DHH"
+ assert_nothing_raised { david.save! }
+ end
+
+
+ def test_updating_attributes_on_rich_associations_with_limited_find
+ david = projects(:action_controller).developers.find(:all, :select => "developers.*").first
+ david.name = "DHH"
+ assert david.save!
+ end
+
+ def test_join_table_alias
+ assert_equal 3, Developer.find(:all, :include => {:projects => :developers}, :conditions => 'developers_projects_join.joined_on IS NOT NULL').size
+ end
+
+ def test_join_with_group
+ group = Developer.columns.inject([]) do |g, c|
+ g << "developers.#{c.name}"
+ g << "developers_projects_2.#{c.name}"
+ end
+ Project.columns.each { |c| group << "projects.#{c.name}" }
+
+ assert_equal 3, Developer.find(:all, :include => {:projects => :developers}, :conditions => 'developers_projects_join.joined_on IS NOT NULL', :group => group.join(",")).size
+ end
+
+ def test_get_ids
+ assert_equal projects(:active_record, :action_controller).map(&:id).sort, developers(:david).project_ids.sort
+ assert_equal [projects(:active_record).id], developers(:jamis).project_ids
+ end
+
+ def test_assign_ids
+ developer = Developer.new("name" => "Joe")
+ developer.project_ids = projects(:active_record, :action_controller).map(&:id)
+ developer.save
+ developer.reload
+ assert_equal 2, developer.projects.length
+ assert_equal projects(:active_record), developer.projects[0]
+ assert_equal projects(:action_controller), developer.projects[1]
+ end
+
+ def test_assign_ids_ignoring_blanks
+ developer = Developer.new("name" => "Joe")
+ developer.project_ids = [projects(:active_record).id, nil, projects(:action_controller).id, '']
+ developer.save
+ developer.reload
+ assert_equal 2, developer.projects.length
+ assert_equal projects(:active_record), developer.projects[0]
+ assert_equal projects(:action_controller), developer.projects[1]
+ end
+
+ def test_select_limited_ids_list
+ # Set timestamps
+ Developer.transaction do
+ Developer.find(:all, :order => 'id').each_with_index do |record, i|
+ record.update_attributes(:created_at => 5.years.ago + (i * 5.minutes))
+ end
+ end
+
+ join_base = ActiveRecord::Associations::ClassMethods::JoinDependency::JoinBase.new(Project)
+ join_dep = ActiveRecord::Associations::ClassMethods::JoinDependency.new(join_base, :developers, nil)
+ projects = Project.send(:select_limited_ids_list, {:order => 'developers.created_at'}, join_dep)
+ assert !projects.include?("'"), projects
+ assert_equal %w(1 2), projects.scan(/\d/).sort
+ end
+
+ def test_scoped_find_on_through_association_doesnt_return_read_only_records
+ tag = Post.find(1).tags.find_by_name("General")
+
+ assert_nothing_raised do
+ tag.save!
+ end
+ end
+end
+
+
+class OverridingAssociationsTest < ActiveSupport::TestCase
+ class Person < ActiveRecord::Base; end
+ class DifferentPerson < ActiveRecord::Base; end
+
+ class PeopleList < ActiveRecord::Base
+ has_and_belongs_to_many :has_and_belongs_to_many, :before_add => :enlist
+ has_many :has_many, :before_add => :enlist
+ belongs_to :belongs_to
+ has_one :has_one
+ end
+
+ class DifferentPeopleList < PeopleList
+ # Different association with the same name, callbacks should be omitted here.
+ has_and_belongs_to_many :has_and_belongs_to_many, :class_name => 'DifferentPerson'
+ has_many :has_many, :class_name => 'DifferentPerson'
+ belongs_to :belongs_to, :class_name => 'DifferentPerson'
+ has_one :has_one, :class_name => 'DifferentPerson'
+ end
+
+ def test_habtm_association_redefinition_callbacks_should_differ_and_not_inherited
+ # redeclared association on AR descendant should not inherit callbacks from superclass
+ callbacks = PeopleList.read_inheritable_attribute(:before_add_for_has_and_belongs_to_many)
+ assert_equal([:enlist], callbacks)
+ callbacks = DifferentPeopleList.read_inheritable_attribute(:before_add_for_has_and_belongs_to_many)
+ assert_equal([], callbacks)
+ end
+
+ def test_has_many_association_redefinition_callbacks_should_differ_and_not_inherited
+ # redeclared association on AR descendant should not inherit callbacks from superclass
+ callbacks = PeopleList.read_inheritable_attribute(:before_add_for_has_many)
+ assert_equal([:enlist], callbacks)
+ callbacks = DifferentPeopleList.read_inheritable_attribute(:before_add_for_has_many)
+ assert_equal([], callbacks)
+ end
+
+ def test_habtm_association_redefinition_reflections_should_differ_and_not_inherited
+ assert_not_equal(
+ PeopleList.reflect_on_association(:has_and_belongs_to_many),
+ DifferentPeopleList.reflect_on_association(:has_and_belongs_to_many)
+ )
+ end
+
+ def test_has_many_association_redefinition_reflections_should_differ_and_not_inherited
+ assert_not_equal(
+ PeopleList.reflect_on_association(:has_many),
+ DifferentPeopleList.reflect_on_association(:has_many)
+ )
+ end
+
+ def test_belongs_to_association_redefinition_reflections_should_differ_and_not_inherited
+ assert_not_equal(
+ PeopleList.reflect_on_association(:belongs_to),
+ DifferentPeopleList.reflect_on_association(:belongs_to)
+ )
+ end
+
+ def test_has_one_association_redefinition_reflections_should_differ_and_not_inherited
+ assert_not_equal(
+ PeopleList.reflect_on_association(:has_one),
+ DifferentPeopleList.reflect_on_association(:has_one)
+ )
+ end
+end
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
new file mode 100755
index 0000000000..4a09a4d63e
--- /dev/null
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -0,0 +1,146 @@
+require 'abstract_unit'
+require 'fixtures/topic'
+
+class AttributeMethodsTest < ActiveSupport::TestCase
+ fixtures :topics
+ def setup
+ @old_suffixes = ActiveRecord::Base.send(:attribute_method_suffixes).dup
+ @target = Class.new(ActiveRecord::Base)
+ @target.table_name = 'topics'
+ end
+
+ def teardown
+ ActiveRecord::Base.send(:attribute_method_suffixes).clear
+ ActiveRecord::Base.attribute_method_suffix *@old_suffixes
+ end
+
+
+ def test_match_attribute_method_query_returns_match_data
+ assert_not_nil md = @target.match_attribute_method?('title=')
+ assert_equal 'title', md.pre_match
+ assert_equal ['='], md.captures
+
+ %w(_hello_world ist! _maybe?).each do |suffix|
+ @target.class_eval "def attribute#{suffix}(*args) args end"
+ @target.attribute_method_suffix suffix
+
+ assert_not_nil md = @target.match_attribute_method?("title#{suffix}")
+ assert_equal 'title', md.pre_match
+ assert_equal [suffix], md.captures
+ end
+ end
+
+ def test_declared_attribute_method_affects_respond_to_and_method_missing
+ topic = @target.new(:title => 'Budget')
+ assert topic.respond_to?('title')
+ assert_equal 'Budget', topic.title
+ assert !topic.respond_to?('title_hello_world')
+ assert_raise(NoMethodError) { topic.title_hello_world }
+
+ %w(_hello_world _it! _candidate= able?).each do |suffix|
+ @target.class_eval "def attribute#{suffix}(*args) args end"
+ @target.attribute_method_suffix suffix
+
+ meth = "title#{suffix}"
+ assert topic.respond_to?(meth)
+ assert_equal ['title'], topic.send(meth)
+ assert_equal ['title', 'a'], topic.send(meth, 'a')
+ assert_equal ['title', 1, 2, 3], topic.send(meth, 1, 2, 3)
+ end
+ end
+
+ def test_should_unserialize_attributes_for_frozen_records
+ myobj = {:value1 => :value2}
+ topic = Topic.create("content" => myobj)
+ topic.freeze
+ assert_equal myobj, topic.content
+ end
+
+ def test_kernel_methods_not_implemented_in_activerecord
+ %w(test name display y).each do |method|
+ assert_equal false, ActiveRecord::Base.instance_method_already_implemented?(method), "##{method} is defined"
+ end
+ end
+
+ def test_primary_key_implemented
+ assert_equal true, Class.new(ActiveRecord::Base).instance_method_already_implemented?('id')
+ end
+
+ def test_defined_kernel_methods_implemented_in_model
+ %w(test name display y).each do |method|
+ klass = Class.new ActiveRecord::Base
+ klass.class_eval "def #{method}() 'defined #{method}' end"
+ assert_equal true, klass.instance_method_already_implemented?(method), "##{method} is not defined"
+ end
+ end
+
+ def test_defined_kernel_methods_implemented_in_model_abstract_subclass
+ %w(test name display y).each do |method|
+ abstract = Class.new ActiveRecord::Base
+ abstract.class_eval "def #{method}() 'defined #{method}' end"
+ abstract.abstract_class = true
+ klass = Class.new abstract
+ assert_equal true, klass.instance_method_already_implemented?(method), "##{method} is not defined"
+ end
+ end
+
+ def test_raises_dangerous_attribute_error_when_defining_activerecord_method_in_model
+ %w(save create_or_update).each do |method|
+ klass = Class.new ActiveRecord::Base
+ klass.class_eval "def #{method}() 'defined #{method}' end"
+ assert_raises ActiveRecord::DangerousAttributeError do
+ klass.instance_method_already_implemented?(method)
+ end
+ end
+ end
+
+ def test_only_time_related_columns_are_meant_to_be_cached_by_default
+ expected = %w(datetime timestamp time date).sort
+ assert_equal expected, ActiveRecord::Base.attribute_types_cached_by_default.map(&:to_s).sort
+end
+
+ def test_declaring_attributes_as_cached_adds_them_to_the_attributes_cached_by_default
+ default_attributes = Topic.cached_attributes
+ Topic.cache_attributes :replies_count
+ expected = default_attributes + ["replies_count"]
+ assert_equal expected.sort, Topic.cached_attributes.sort
+ Topic.instance_variable_set "@cached_attributes", nil
+ end
+
+ def test_time_related_columns_are_actually_cached
+ column_types = %w(datetime timestamp time date).map(&:to_sym)
+ column_names = Topic.columns.select{|c| column_types.include?(c.type) }.map(&:name)
+
+ assert_equal column_names.sort, Topic.cached_attributes.sort
+ assert_equal time_related_columns_on_topic.sort, Topic.cached_attributes.sort
+ end
+
+ def test_accessing_cached_attributes_caches_the_converted_values_and_nothing_else
+ t = topics(:first)
+ cache = t.instance_variable_get "@attributes_cache"
+
+ assert_not_nil cache
+ assert cache.empty?
+
+ all_columns = Topic.columns.map(&:name)
+ cached_columns = time_related_columns_on_topic
+ uncached_columns = all_columns - cached_columns
+
+ all_columns.each do |attr_name|
+ attribute_gets_cached = Topic.cache_attribute?(attr_name)
+ val = t.send attr_name unless attr_name == "type"
+ if attribute_gets_cached
+ assert cached_columns.include?(attr_name)
+ assert_equal val, cache[attr_name]
+ else
+ assert uncached_columns.include?(attr_name)
+ assert !cache.include?(attr_name)
+ end
+ end
+ end
+
+ private
+ def time_related_columns_on_topic
+ Topic.columns.select{|c| [:time, :date, :datetime, :timestamp].include?(c.type)}.map(&:name)
+ end
+end
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
new file mode 100755
index 0000000000..00e563aaee
--- /dev/null
+++ b/activerecord/test/cases/base_test.rb
@@ -0,0 +1,1831 @@
+require 'abstract_unit'
+require 'fixtures/topic'
+require 'fixtures/reply'
+require 'fixtures/company'
+require 'fixtures/customer'
+require 'fixtures/developer'
+require 'fixtures/project'
+require 'fixtures/default'
+require 'fixtures/auto_id'
+require 'fixtures/column_name'
+require 'fixtures/subscriber'
+require 'fixtures/keyboard'
+require 'fixtures/post'
+require 'fixtures/minimalistic'
+require 'fixtures/warehouse_thing'
+require 'rexml/document'
+
+class Category < ActiveRecord::Base; end
+class Smarts < ActiveRecord::Base; end
+class CreditCard < ActiveRecord::Base
+ class PinNumber < ActiveRecord::Base
+ class CvvCode < ActiveRecord::Base; end
+ class SubCvvCode < CvvCode; end
+ end
+ class SubPinNumber < PinNumber; end
+ class Brand < Category; end
+end
+class MasterCreditCard < ActiveRecord::Base; end
+class Post < ActiveRecord::Base; end
+class Computer < ActiveRecord::Base; end
+class NonExistentTable < ActiveRecord::Base; end
+class TestOracleDefault < ActiveRecord::Base; end
+
+class LoosePerson < ActiveRecord::Base
+ self.table_name = 'people'
+ self.abstract_class = true
+ attr_protected :credit_rating, :administrator
+end
+
+class LooseDescendant < LoosePerson
+ attr_protected :phone_number
+end
+
+class LooseDescendantSecond< LoosePerson
+ attr_protected :phone_number
+ attr_protected :name
+end
+
+class TightPerson < ActiveRecord::Base
+ self.table_name = 'people'
+ attr_accessible :name, :address
+end
+
+class TightDescendant < TightPerson
+ attr_accessible :phone_number
+end
+
+class ReadonlyTitlePost < Post
+ attr_readonly :title
+end
+
+class Booleantest < ActiveRecord::Base; end
+
+class Task < ActiveRecord::Base
+ attr_protected :starting
+end
+
+class TopicWithProtectedContentAndAccessibleAuthorName < ActiveRecord::Base
+ self.table_name = 'topics'
+ attr_accessible :author_name
+ attr_protected :content
+end
+
+class BasicsTest < ActiveSupport::TestCase
+ fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things'
+
+ def test_table_exists
+ assert !NonExistentTable.table_exists?
+ assert Topic.table_exists?
+ end
+
+ def test_set_attributes
+ topic = Topic.find(1)
+ topic.attributes = { "title" => "Budget", "author_name" => "Jason" }
+ topic.save
+ assert_equal("Budget", topic.title)
+ assert_equal("Jason", topic.author_name)
+ assert_equal(topics(:first).author_email_address, Topic.find(1).author_email_address)
+ end
+
+ def test_integers_as_nil
+ test = AutoId.create('value' => '')
+ assert_nil AutoId.find(test.id).value
+ end
+
+ def test_set_attributes_with_block
+ topic = Topic.new do |t|
+ t.title = "Budget"
+ t.author_name = "Jason"
+ end
+
+ assert_equal("Budget", topic.title)
+ assert_equal("Jason", topic.author_name)
+ end
+
+ def test_respond_to?
+ topic = Topic.find(1)
+ assert topic.respond_to?("title")
+ assert topic.respond_to?("title?")
+ assert topic.respond_to?("title=")
+ assert topic.respond_to?(:title)
+ assert topic.respond_to?(:title?)
+ assert topic.respond_to?(:title=)
+ assert topic.respond_to?("author_name")
+ assert topic.respond_to?("attribute_names")
+ assert !topic.respond_to?("nothingness")
+ assert !topic.respond_to?(:nothingness)
+ end
+
+ def test_array_content
+ topic = Topic.new
+ topic.content = %w( one two three )
+ topic.save
+
+ assert_equal(%w( one two three ), Topic.find(topic.id).content)
+ end
+
+ def test_hash_content
+ topic = Topic.new
+ topic.content = { "one" => 1, "two" => 2 }
+ topic.save
+
+ assert_equal 2, Topic.find(topic.id).content["two"]
+
+ topic.content["three"] = 3
+ topic.save
+
+ assert_equal 3, Topic.find(topic.id).content["three"]
+ end
+
+ def test_update_array_content
+ topic = Topic.new
+ topic.content = %w( one two three )
+
+ topic.content.push "four"
+ assert_equal(%w( one two three four ), topic.content)
+
+ topic.save
+
+ topic = Topic.find(topic.id)
+ topic.content << "five"
+ assert_equal(%w( one two three four five ), topic.content)
+ end
+
+ def test_case_sensitive_attributes_hash
+ # DB2 is not case-sensitive
+ return true if current_adapter?(:DB2Adapter)
+
+ assert_equal @loaded_fixtures['computers']['workstation'].to_hash, Computer.find(:first).attributes
+ end
+
+ def test_create
+ topic = Topic.new
+ topic.title = "New Topic"
+ topic.save
+ topic_reloaded = Topic.find(topic.id)
+ assert_equal("New Topic", topic_reloaded.title)
+ end
+
+ def test_save!
+ topic = Topic.new(:title => "New Topic")
+ assert topic.save!
+
+ reply = Reply.new
+ assert_raise(ActiveRecord::RecordInvalid) { reply.save! }
+ end
+
+ def test_save_null_string_attributes
+ topic = Topic.find(1)
+ topic.attributes = { "title" => "null", "author_name" => "null" }
+ topic.save!
+ topic.reload
+ assert_equal("null", topic.title)
+ assert_equal("null", topic.author_name)
+ end
+
+ def test_save_nil_string_attributes
+ topic = Topic.find(1)
+ topic.title = nil
+ topic.save!
+ topic.reload
+ assert_nil topic.title
+ end
+
+ def test_save_for_record_with_only_primary_key
+ minimalistic = Minimalistic.new
+ assert_nothing_raised { minimalistic.save }
+ end
+
+ def test_save_for_record_with_only_primary_key_that_is_provided
+ assert_nothing_raised { Minimalistic.create!(:id => 2) }
+ end
+
+ def test_hashes_not_mangled
+ new_topic = { :title => "New Topic" }
+ new_topic_values = { :title => "AnotherTopic" }
+
+ topic = Topic.new(new_topic)
+ assert_equal new_topic[:title], topic.title
+
+ topic.attributes= new_topic_values
+ assert_equal new_topic_values[:title], topic.title
+ end
+
+ def test_create_many
+ topics = Topic.create([ { "title" => "first" }, { "title" => "second" }])
+ assert_equal 2, topics.size
+ assert_equal "first", topics.first.title
+ end
+
+ def test_create_columns_not_equal_attributes
+ topic = Topic.new
+ topic.title = 'Another New Topic'
+ topic.send :write_attribute, 'does_not_exist', 'test'
+ assert_nothing_raised { topic.save }
+ end
+
+ def test_create_through_factory
+ topic = Topic.create("title" => "New Topic")
+ topicReloaded = Topic.find(topic.id)
+ assert_equal(topic, topicReloaded)
+ end
+
+ def test_update
+ topic = Topic.new
+ topic.title = "Another New Topic"
+ topic.written_on = "2003-12-12 23:23:00"
+ topic.save
+ topicReloaded = Topic.find(topic.id)
+ assert_equal("Another New Topic", topicReloaded.title)
+
+ topicReloaded.title = "Updated topic"
+ topicReloaded.save
+
+ topicReloadedAgain = Topic.find(topic.id)
+
+ assert_equal("Updated topic", topicReloadedAgain.title)
+ end
+
+ def test_update_columns_not_equal_attributes
+ topic = Topic.new
+ topic.title = "Still another topic"
+ topic.save
+
+ topicReloaded = Topic.find(topic.id)
+ topicReloaded.title = "A New Topic"
+ topicReloaded.send :write_attribute, 'does_not_exist', 'test'
+ assert_nothing_raised { topicReloaded.save }
+ end
+
+ def test_update_for_record_with_only_primary_key
+ minimalistic = minimalistics(:first)
+ assert_nothing_raised { minimalistic.save }
+ end
+
+ def test_write_attribute
+ topic = Topic.new
+ topic.send(:write_attribute, :title, "Still another topic")
+ assert_equal "Still another topic", topic.title
+
+ topic.send(:write_attribute, "title", "Still another topic: part 2")
+ assert_equal "Still another topic: part 2", topic.title
+ end
+
+ def test_read_attribute
+ topic = Topic.new
+ topic.title = "Don't change the topic"
+ assert_equal "Don't change the topic", topic.send(:read_attribute, "title")
+ assert_equal "Don't change the topic", topic["title"]
+
+ assert_equal "Don't change the topic", topic.send(:read_attribute, :title)
+ assert_equal "Don't change the topic", topic[:title]
+ end
+
+ def test_read_attribute_when_false
+ topic = topics(:first)
+ topic.approved = false
+ assert !topic.approved?, "approved should be false"
+ topic.approved = "false"
+ assert !topic.approved?, "approved should be false"
+ end
+
+ def test_read_attribute_when_true
+ topic = topics(:first)
+ topic.approved = true
+ assert topic.approved?, "approved should be true"
+ topic.approved = "true"
+ assert topic.approved?, "approved should be true"
+ end
+
+ def test_read_write_boolean_attribute
+ topic = Topic.new
+ # puts ""
+ # puts "New Topic"
+ # puts topic.inspect
+ topic.approved = "false"
+ # puts "Expecting false"
+ # puts topic.inspect
+ assert !topic.approved?, "approved should be false"
+ topic.approved = "false"
+ # puts "Expecting false"
+ # puts topic.inspect
+ assert !topic.approved?, "approved should be false"
+ topic.approved = "true"
+ # puts "Expecting true"
+ # puts topic.inspect
+ assert topic.approved?, "approved should be true"
+ topic.approved = "true"
+ # puts "Expecting true"
+ # puts topic.inspect
+ assert topic.approved?, "approved should be true"
+ # puts ""
+ end
+
+ def test_query_attribute_string
+ [nil, "", " "].each do |value|
+ assert_equal false, Topic.new(:author_name => value).author_name?
+ end
+
+ assert_equal true, Topic.new(:author_name => "Name").author_name?
+ end
+
+ def test_query_attribute_number
+ [nil, 0, "0"].each do |value|
+ assert_equal false, Developer.new(:salary => value).salary?
+ end
+
+ assert_equal true, Developer.new(:salary => 1).salary?
+ assert_equal true, Developer.new(:salary => "1").salary?
+ end
+
+ def test_query_attribute_boolean
+ [nil, "", false, "false", "f", 0].each do |value|
+ assert_equal false, Topic.new(:approved => value).approved?
+ end
+
+ [true, "true", "1", 1].each do |value|
+ assert_equal true, Topic.new(:approved => value).approved?
+ end
+ end
+
+ def test_query_attribute_with_custom_fields
+ object = Company.find_by_sql(<<-SQL).first
+ SELECT c1.*, c2.ruby_type as string_value, c2.rating as int_value
+ FROM companies c1, companies c2
+ WHERE c1.firm_id = c2.id
+ AND c1.id = 2
+ SQL
+
+ assert_equal "Firm", object.string_value
+ assert object.string_value?
+
+ object.string_value = " "
+ assert !object.string_value?
+
+ assert_equal 1, object.int_value.to_i
+ assert object.int_value?
+
+ object.int_value = "0"
+ assert !object.int_value?
+ end
+
+
+ def test_reader_for_invalid_column_names
+ Topic.send(:define_read_method, "mumub-jumbo".to_sym, "mumub-jumbo", nil)
+ assert !Topic.generated_methods.include?("mumub-jumbo")
+ end
+
+ def test_non_attribute_access_and_assignment
+ topic = Topic.new
+ assert !topic.respond_to?("mumbo")
+ assert_raises(NoMethodError) { topic.mumbo }
+ assert_raises(NoMethodError) { topic.mumbo = 5 }
+ end
+
+ def test_preserving_date_objects
+ # SQL Server doesn't have a separate column type just for dates, so all are returned as time
+ return true if current_adapter?(:SQLServerAdapter)
+
+ if current_adapter?(:SybaseAdapter, :OracleAdapter)
+ # Sybase ctlib does not (yet?) support the date type; use datetime instead.
+ # Oracle treats all dates/times as Time.
+ assert_kind_of(
+ Time, Topic.find(1).last_read,
+ "The last_read attribute should be of the Time class"
+ )
+ else
+ assert_kind_of(
+ Date, Topic.find(1).last_read,
+ "The last_read attribute should be of the Date class"
+ )
+ end
+ end
+
+ def test_preserving_time_objects
+ assert_kind_of(
+ Time, Topic.find(1).bonus_time,
+ "The bonus_time attribute should be of the Time class"
+ )
+
+ assert_kind_of(
+ Time, Topic.find(1).written_on,
+ "The written_on attribute should be of the Time class"
+ )
+
+ # For adapters which support microsecond resolution.
+ if current_adapter?(:PostgreSQLAdapter)
+ assert_equal 11, Topic.find(1).written_on.sec
+ assert_equal 223300, Topic.find(1).written_on.usec
+ assert_equal 9900, Topic.find(2).written_on.usec
+ end
+ end
+
+ def test_custom_mutator
+ topic = Topic.find(1)
+ # This mutator is protected in the class definition
+ topic.send(:approved=, true)
+ assert topic.instance_variable_get("@custom_approved")
+ end
+
+ def test_destroy
+ topic = Topic.find(1)
+ assert_equal topic, topic.destroy, 'topic.destroy did not return self'
+ assert topic.frozen?, 'topic not frozen after destroy'
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) }
+ end
+
+ def test_record_not_found_exception
+ assert_raises(ActiveRecord::RecordNotFound) { topicReloaded = Topic.find(99999) }
+ end
+
+ def test_initialize_with_attributes
+ topic = Topic.new({
+ "title" => "initialized from attributes", "written_on" => "2003-12-12 23:23"
+ })
+
+ assert_equal("initialized from attributes", topic.title)
+ end
+
+ def test_initialize_with_invalid_attribute
+ begin
+ topic = Topic.new({ "title" => "test",
+ "last_read(1i)" => "2005", "last_read(2i)" => "2", "last_read(3i)" => "31"})
+ rescue ActiveRecord::MultiparameterAssignmentErrors => ex
+ assert_equal(1, ex.errors.size)
+ assert_equal("last_read", ex.errors[0].attribute)
+ end
+ end
+
+ def test_load
+ topics = Topic.find(:all, :order => 'id')
+ assert_equal(2, topics.size)
+ assert_equal(topics(:first).title, topics.first.title)
+ end
+
+ def test_load_with_condition
+ topics = Topic.find(:all, :conditions => "author_name = 'Mary'")
+
+ assert_equal(1, topics.size)
+ assert_equal(topics(:second).title, topics.first.title)
+ end
+
+ def test_table_name_guesses
+ classes = [Category, Smarts, CreditCard, CreditCard::PinNumber, CreditCard::PinNumber::CvvCode, CreditCard::SubPinNumber, CreditCard::Brand, MasterCreditCard]
+
+ assert_equal "topics", Topic.table_name
+
+ assert_equal "categories", Category.table_name
+ assert_equal "smarts", Smarts.table_name
+ assert_equal "credit_cards", CreditCard.table_name
+ assert_equal "credit_card_pin_numbers", CreditCard::PinNumber.table_name
+ assert_equal "credit_card_pin_number_cvv_codes", CreditCard::PinNumber::CvvCode.table_name
+ assert_equal "credit_card_pin_numbers", CreditCard::SubPinNumber.table_name
+ assert_equal "categories", CreditCard::Brand.table_name
+ assert_equal "master_credit_cards", MasterCreditCard.table_name
+
+ ActiveRecord::Base.pluralize_table_names = false
+ classes.each(&:reset_table_name)
+
+ assert_equal "category", Category.table_name
+ assert_equal "smarts", Smarts.table_name
+ assert_equal "credit_card", CreditCard.table_name
+ assert_equal "credit_card_pin_number", CreditCard::PinNumber.table_name
+ assert_equal "credit_card_pin_number_cvv_code", CreditCard::PinNumber::CvvCode.table_name
+ assert_equal "credit_card_pin_number", CreditCard::SubPinNumber.table_name
+ assert_equal "category", CreditCard::Brand.table_name
+ assert_equal "master_credit_card", MasterCreditCard.table_name
+
+ ActiveRecord::Base.pluralize_table_names = true
+ classes.each(&:reset_table_name)
+
+ ActiveRecord::Base.table_name_prefix = "test_"
+ Category.reset_table_name
+ assert_equal "test_categories", Category.table_name
+ ActiveRecord::Base.table_name_suffix = "_test"
+ Category.reset_table_name
+ assert_equal "test_categories_test", Category.table_name
+ ActiveRecord::Base.table_name_prefix = ""
+ Category.reset_table_name
+ assert_equal "categories_test", Category.table_name
+ ActiveRecord::Base.table_name_suffix = ""
+ Category.reset_table_name
+ assert_equal "categories", Category.table_name
+
+ ActiveRecord::Base.pluralize_table_names = false
+ ActiveRecord::Base.table_name_prefix = "test_"
+ Category.reset_table_name
+ assert_equal "test_category", Category.table_name
+ ActiveRecord::Base.table_name_suffix = "_test"
+ Category.reset_table_name
+ assert_equal "test_category_test", Category.table_name
+ ActiveRecord::Base.table_name_prefix = ""
+ Category.reset_table_name
+ assert_equal "category_test", Category.table_name
+ ActiveRecord::Base.table_name_suffix = ""
+ Category.reset_table_name
+ assert_equal "category", Category.table_name
+
+ ActiveRecord::Base.pluralize_table_names = true
+ classes.each(&:reset_table_name)
+ end
+
+ def test_destroy_all
+ assert_equal 2, Topic.count
+
+ Topic.destroy_all "author_name = 'Mary'"
+ assert_equal 1, Topic.count
+ end
+
+ def test_destroy_many
+ assert_equal 3, Client.count
+ Client.destroy([2, 3])
+ assert_equal 1, Client.count
+ end
+
+ def test_delete_many
+ Topic.delete([1, 2])
+ assert_equal 0, Topic.count
+ end
+
+ def test_boolean_attributes
+ assert ! Topic.find(1).approved?
+ assert Topic.find(2).approved?
+ end
+
+ def test_increment_counter
+ Topic.increment_counter("replies_count", 1)
+ assert_equal 2, Topic.find(1).replies_count
+
+ Topic.increment_counter("replies_count", 1)
+ assert_equal 3, Topic.find(1).replies_count
+ end
+
+ def test_decrement_counter
+ Topic.decrement_counter("replies_count", 2)
+ assert_equal -1, Topic.find(2).replies_count
+
+ Topic.decrement_counter("replies_count", 2)
+ assert_equal -2, Topic.find(2).replies_count
+ end
+
+ def test_update_all
+ assert_equal 2, Topic.update_all("content = 'bulk updated!'")
+ assert_equal "bulk updated!", Topic.find(1).content
+ assert_equal "bulk updated!", Topic.find(2).content
+
+ assert_equal 2, Topic.update_all(['content = ?', 'bulk updated again!'])
+ assert_equal "bulk updated again!", Topic.find(1).content
+ assert_equal "bulk updated again!", Topic.find(2).content
+
+ assert_equal 2, Topic.update_all(['content = ?', nil])
+ assert_nil Topic.find(1).content
+ end
+
+ def test_update_all_with_hash
+ assert_not_nil Topic.find(1).last_read
+ assert_equal 2, Topic.update_all(:content => 'bulk updated with hash!', :last_read => nil)
+ assert_equal "bulk updated with hash!", Topic.find(1).content
+ assert_equal "bulk updated with hash!", Topic.find(2).content
+ assert_nil Topic.find(1).last_read
+ assert_nil Topic.find(2).last_read
+ end
+
+ def test_update_all_with_non_standard_table_name
+ assert_equal 1, WarehouseThing.update_all(['value = ?', 0], ['id = ?', 1])
+ assert_equal 0, WarehouseThing.find(1).value
+ end
+
+ if current_adapter?(:MysqlAdapter)
+ def test_update_all_with_order_and_limit
+ assert_equal 1, Topic.update_all("content = 'bulk updated!'", nil, :limit => 1, :order => 'id DESC')
+ end
+ end
+
+ def test_update_all_ignores_order_limit_from_association
+ author = Author.find(1)
+ assert_nothing_raised do
+ assert_equal author.posts_with_comments_and_categories.length, author.posts_with_comments_and_categories.update_all("body = 'bulk update!'")
+ end
+ end
+
+ def test_update_many
+ topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } }
+ updated = Topic.update(topic_data.keys, topic_data.values)
+
+ assert_equal 2, updated.size
+ assert_equal "1 updated", Topic.find(1).content
+ assert_equal "2 updated", Topic.find(2).content
+ end
+
+ def test_delete_all
+ assert_equal 2, Topic.delete_all
+ end
+
+ def test_update_by_condition
+ Topic.update_all "content = 'bulk updated!'", ["approved = ?", true]
+ assert_equal "Have a nice day", Topic.find(1).content
+ assert_equal "bulk updated!", Topic.find(2).content
+ end
+
+ def test_attribute_present
+ t = Topic.new
+ t.title = "hello there!"
+ t.written_on = Time.now
+ assert t.attribute_present?("title")
+ assert t.attribute_present?("written_on")
+ assert !t.attribute_present?("content")
+ end
+
+ def test_attribute_keys_on_new_instance
+ t = Topic.new
+ assert_equal nil, t.title, "The topics table has a title column, so it should be nil"
+ assert_raise(NoMethodError) { t.title2 }
+ end
+
+ def test_class_name
+ assert_equal "Firm", ActiveRecord::Base.class_name("firms")
+ assert_equal "Category", ActiveRecord::Base.class_name("categories")
+ assert_equal "AccountHolder", ActiveRecord::Base.class_name("account_holder")
+
+ ActiveRecord::Base.pluralize_table_names = false
+ assert_equal "Firms", ActiveRecord::Base.class_name( "firms" )
+ ActiveRecord::Base.pluralize_table_names = true
+
+ ActiveRecord::Base.table_name_prefix = "test_"
+ assert_equal "Firm", ActiveRecord::Base.class_name( "test_firms" )
+ ActiveRecord::Base.table_name_suffix = "_tests"
+ assert_equal "Firm", ActiveRecord::Base.class_name( "test_firms_tests" )
+ ActiveRecord::Base.table_name_prefix = ""
+ assert_equal "Firm", ActiveRecord::Base.class_name( "firms_tests" )
+ ActiveRecord::Base.table_name_suffix = ""
+ assert_equal "Firm", ActiveRecord::Base.class_name( "firms" )
+ end
+
+ def test_null_fields
+ assert_nil Topic.find(1).parent_id
+ assert_nil Topic.create("title" => "Hey you").parent_id
+ end
+
+ def test_default_values
+ topic = Topic.new
+ assert topic.approved?
+ assert_nil topic.written_on
+ assert_nil topic.bonus_time
+ assert_nil topic.last_read
+
+ topic.save
+
+ topic = Topic.find(topic.id)
+ assert topic.approved?
+ assert_nil topic.last_read
+
+ # Oracle has some funky default handling, so it requires a bit of
+ # extra testing. See ticket #2788.
+ if current_adapter?(:OracleAdapter)
+ test = TestOracleDefault.new
+ assert_equal "X", test.test_char
+ assert_equal "hello", test.test_string
+ assert_equal 3, test.test_int
+ end
+ end
+
+ # Oracle, SQLServer, and Sybase do not have a TIME datatype.
+ unless current_adapter?(:SQLServerAdapter, :OracleAdapter, :SybaseAdapter)
+ def test_utc_as_time_zone
+ Topic.default_timezone = :utc
+ attributes = { "bonus_time" => "5:42:00AM" }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.utc(2000, 1, 1, 5, 42, 0), topic.bonus_time
+ Topic.default_timezone = :local
+ end
+
+ def test_utc_as_time_zone_and_new
+ Topic.default_timezone = :utc
+ attributes = { "bonus_time(1i)"=>"2000",
+ "bonus_time(2i)"=>"1",
+ "bonus_time(3i)"=>"1",
+ "bonus_time(4i)"=>"10",
+ "bonus_time(5i)"=>"35",
+ "bonus_time(6i)"=>"50" }
+ topic = Topic.new(attributes)
+ assert_equal Time.utc(2000, 1, 1, 10, 35, 50), topic.bonus_time
+ Topic.default_timezone = :local
+ end
+ end
+
+ def test_default_values_on_empty_strings
+ topic = Topic.new
+ topic.approved = nil
+ topic.last_read = nil
+
+ topic.save
+
+ topic = Topic.find(topic.id)
+ assert_nil topic.last_read
+
+ # Sybase adapter does not allow nulls in boolean columns
+ if current_adapter?(:SybaseAdapter)
+ assert topic.approved == false
+ else
+ assert_nil topic.approved
+ end
+ end
+
+ def test_equality
+ assert_equal Topic.find(1), Topic.find(2).topic
+ end
+
+ def test_equality_of_new_records
+ assert_not_equal Topic.new, Topic.new
+ end
+
+ def test_hashing
+ assert_equal [ Topic.find(1) ], [ Topic.find(2).topic ] & [ Topic.find(1) ]
+ end
+
+ def test_destroy_new_record
+ client = Client.new
+ client.destroy
+ assert client.frozen?
+ end
+
+ def test_destroy_record_with_associations
+ client = Client.find(3)
+ client.destroy
+ assert client.frozen?
+ assert_kind_of Firm, client.firm
+ assert_raises(ActiveSupport::FrozenObjectError) { client.name = "something else" }
+ end
+
+ def test_update_attribute
+ assert !Topic.find(1).approved?
+ Topic.find(1).update_attribute("approved", true)
+ assert Topic.find(1).approved?
+
+ Topic.find(1).update_attribute(:approved, false)
+ assert !Topic.find(1).approved?
+ end
+
+ def test_update_attributes
+ topic = Topic.find(1)
+ assert !topic.approved?
+ assert_equal "The First Topic", topic.title
+
+ topic.update_attributes("approved" => true, "title" => "The First Topic Updated")
+ topic.reload
+ assert topic.approved?
+ assert_equal "The First Topic Updated", topic.title
+
+ topic.update_attributes(:approved => false, :title => "The First Topic")
+ topic.reload
+ assert !topic.approved?
+ assert_equal "The First Topic", topic.title
+ end
+
+ def test_update_attributes!
+ reply = Reply.find(2)
+ assert_equal "The Second Topic's of the day", reply.title
+ assert_equal "Have a nice day", reply.content
+
+ reply.update_attributes!("title" => "The Second Topic's of the day updated", "content" => "Have a nice evening")
+ reply.reload
+ assert_equal "The Second Topic's of the day updated", reply.title
+ assert_equal "Have a nice evening", reply.content
+
+ reply.update_attributes!(:title => "The Second Topic's of the day", :content => "Have a nice day")
+ reply.reload
+ assert_equal "The Second Topic's of the day", reply.title
+ assert_equal "Have a nice day", reply.content
+
+ assert_raise(ActiveRecord::RecordInvalid) { reply.update_attributes!(:title => nil, :content => "Have a nice evening") }
+ end
+
+ def test_mass_assignment_should_raise_exception_if_accessible_and_protected_attribute_writers_are_both_used
+ topic = TopicWithProtectedContentAndAccessibleAuthorName.new
+ assert_raises(RuntimeError) { topic.attributes = { "author_name" => "me" } }
+ assert_raises(RuntimeError) { topic.attributes = { "content" => "stuff" } }
+ end
+
+ def test_mass_assignment_protection
+ firm = Firm.new
+ firm.attributes = { "name" => "Next Angle", "rating" => 5 }
+ assert_equal 1, firm.rating
+ end
+
+ def test_mass_assignment_protection_against_class_attribute_writers
+ [:logger, :configurations, :primary_key_prefix_type, :table_name_prefix, :table_name_suffix, :pluralize_table_names, :colorize_logging,
+ :default_timezone, :allow_concurrency, :schema_format, :verification_timeout, :lock_optimistically, :record_timestamps].each do |method|
+ assert Task.respond_to?(method)
+ assert Task.respond_to?("#{method}=")
+ assert Task.new.respond_to?(method)
+ assert !Task.new.respond_to?("#{method}=")
+ end
+ end
+
+ def test_customized_primary_key_remains_protected
+ subscriber = Subscriber.new(:nick => 'webster123', :name => 'nice try')
+ assert_nil subscriber.id
+
+ keyboard = Keyboard.new(:key_number => 9, :name => 'nice try')
+ assert_nil keyboard.id
+ end
+
+ def test_customized_primary_key_remains_protected_when_referred_to_as_id
+ subscriber = Subscriber.new(:id => 'webster123', :name => 'nice try')
+ assert_nil subscriber.id
+
+ keyboard = Keyboard.new(:id => 9, :name => 'nice try')
+ assert_nil keyboard.id
+ end
+
+ def test_mass_assignment_protection_on_defaults
+ firm = Firm.new
+ firm.attributes = { "id" => 5, "type" => "Client" }
+ assert_nil firm.id
+ assert_equal "Firm", firm[:type]
+ end
+
+ def test_mass_assignment_accessible
+ reply = Reply.new("title" => "hello", "content" => "world", "approved" => true)
+ reply.save
+
+ assert reply.approved?
+
+ reply.approved = false
+ reply.save
+
+ assert !reply.approved?
+ end
+
+ def test_mass_assignment_protection_inheritance
+ assert_nil LoosePerson.accessible_attributes
+ assert_equal Set.new([ 'credit_rating', 'administrator' ]), LoosePerson.protected_attributes
+
+ assert_nil LooseDescendant.accessible_attributes
+ assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number' ]), LooseDescendant.protected_attributes
+
+ assert_nil LooseDescendantSecond.accessible_attributes
+ assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', 'name' ]), LooseDescendantSecond.protected_attributes, 'Running attr_protected twice in one class should merge the protections'
+
+ assert_nil TightPerson.protected_attributes
+ assert_equal Set.new([ 'name', 'address' ]), TightPerson.accessible_attributes
+
+ assert_nil TightDescendant.protected_attributes
+ assert_equal Set.new([ 'name', 'address', 'phone_number' ]), TightDescendant.accessible_attributes
+ end
+
+ def test_readonly_attributes
+ assert_equal Set.new([ 'title' ]), ReadonlyTitlePost.readonly_attributes
+
+ post = ReadonlyTitlePost.create(:title => "cannot change this", :body => "changeable")
+ post.reload
+ assert_equal "cannot change this", post.title
+
+ post.update_attributes(:title => "try to change", :body => "changed")
+ post.reload
+ assert_equal "cannot change this", post.title
+ assert_equal "changed", post.body
+ end
+
+ def test_multiparameter_attributes_on_date
+ attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "24" }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ # note that extra #to_date call allows test to pass for Oracle, which
+ # treats dates/times the same
+ assert_date_from_db Date.new(2004, 6, 24), topic.last_read.to_date
+ end
+
+ def test_multiparameter_attributes_on_date_with_empty_date
+ attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "" }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ # note that extra #to_date call allows test to pass for Oracle, which
+ # treats dates/times the same
+ assert_date_from_db Date.new(2004, 6, 1), topic.last_read.to_date
+ end
+
+ def test_multiparameter_attributes_on_date_with_all_empty
+ attributes = { "last_read(1i)" => "", "last_read(2i)" => "", "last_read(3i)" => "" }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_nil topic.last_read
+ end
+
+ def test_multiparameter_attributes_on_time
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
+ end
+
+ def test_multiparameter_attributes_on_time_with_empty_seconds
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => ""
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
+ end
+
+ def test_multiparameter_mass_assignment_protector
+ task = Task.new
+ time = Time.mktime(2000, 1, 1, 1)
+ task.starting = time
+ attributes = { "starting(1i)" => "2004", "starting(2i)" => "6", "starting(3i)" => "24" }
+ task.attributes = attributes
+ assert_equal time, task.starting
+ end
+
+ def test_multiparameter_assignment_of_aggregation
+ customer = Customer.new
+ address = Address.new("The Street", "The City", "The Country")
+ attributes = { "address(1)" => address.street, "address(2)" => address.city, "address(3)" => address.country }
+ customer.attributes = attributes
+ assert_equal address, customer.address
+ end
+
+ def test_attributes_on_dummy_time
+ # Oracle, SQL Server, and Sybase do not have a TIME datatype.
+ return true if current_adapter?(:SQLServerAdapter, :OracleAdapter, :SybaseAdapter)
+
+ attributes = {
+ "bonus_time" => "5:42:00AM"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time
+ end
+
+ def test_boolean
+ b_false = Booleantest.create({ "value" => false })
+ false_id = b_false.id
+ b_true = Booleantest.create({ "value" => true })
+ true_id = b_true.id
+
+ b_false = Booleantest.find(false_id)
+ assert !b_false.value?
+ b_true = Booleantest.find(true_id)
+ assert b_true.value?
+ end
+
+ def test_boolean_cast_from_string
+ b_false = Booleantest.create({ "value" => "0" })
+ false_id = b_false.id
+ b_true = Booleantest.create({ "value" => "1" })
+ true_id = b_true.id
+
+ b_false = Booleantest.find(false_id)
+ assert !b_false.value?
+ b_true = Booleantest.find(true_id)
+ assert b_true.value?
+ end
+
+ def test_clone
+ topic = Topic.find(1)
+ cloned_topic = nil
+ assert_nothing_raised { cloned_topic = topic.clone }
+ assert_equal topic.title, cloned_topic.title
+ assert cloned_topic.new_record?
+
+ # test if the attributes have been cloned
+ topic.title = "a"
+ cloned_topic.title = "b"
+ assert_equal "a", topic.title
+ assert_equal "b", cloned_topic.title
+
+ # test if the attribute values have been cloned
+ topic.title = {"a" => "b"}
+ cloned_topic = topic.clone
+ cloned_topic.title["a"] = "c"
+ assert_equal "b", topic.title["a"]
+
+ #test if attributes set as part of after_initialize are cloned correctly
+ assert_equal topic.author_email_address, cloned_topic.author_email_address
+
+ # test if saved clone object differs from original
+ cloned_topic.save
+ assert !cloned_topic.new_record?
+ assert cloned_topic.id != topic.id
+ end
+
+ def test_clone_with_aggregate_of_same_name_as_attribute
+ dev = DeveloperWithAggregate.find(1)
+ assert_kind_of DeveloperSalary, dev.salary
+
+ clone = nil
+ assert_nothing_raised { clone = dev.clone }
+ assert_kind_of DeveloperSalary, clone.salary
+ assert_equal dev.salary.amount, clone.salary.amount
+ assert clone.new_record?
+
+ # test if the attributes have been cloned
+ original_amount = clone.salary.amount
+ dev.salary.amount = 1
+ assert_equal original_amount, clone.salary.amount
+
+ assert clone.save
+ assert !clone.new_record?
+ assert clone.id != dev.id
+ end
+
+ def test_clone_preserves_subtype
+ clone = nil
+ assert_nothing_raised { clone = Company.find(3).clone }
+ assert_kind_of Client, clone
+ end
+
+ def test_bignum
+ company = Company.find(1)
+ company.rating = 2147483647
+ company.save
+ company = Company.find(1)
+ assert_equal 2147483647, company.rating
+ end
+
+ # TODO: extend defaults tests to other databases!
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_default
+ default = Default.new
+
+ # fixed dates / times
+ assert_equal Date.new(2004, 1, 1), default.fixed_date
+ assert_equal Time.local(2004, 1,1,0,0,0,0), default.fixed_time
+
+ # char types
+ assert_equal 'Y', default.char1
+ assert_equal 'a varchar field', default.char2
+ assert_equal 'a text field', default.char3
+ end
+
+ class Geometric < ActiveRecord::Base; end
+ def test_geometric_content
+
+ # accepted format notes:
+ # ()'s aren't required
+ # values can be a mix of float or integer
+
+ g = Geometric.new(
+ :a_point => '(5.0, 6.1)',
+ #:a_line => '((2.0, 3), (5.5, 7.0))' # line type is currently unsupported in postgresql
+ :a_line_segment => '(2.0, 3), (5.5, 7.0)',
+ :a_box => '2.0, 3, 5.5, 7.0',
+ :a_path => '[(2.0, 3), (5.5, 7.0), (8.5, 11.0)]', # [ ] is an open path
+ :a_polygon => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))',
+ :a_circle => '<(5.3, 10.4), 2>'
+ )
+
+ assert g.save
+
+ # Reload and check that we have all the geometric attributes.
+ h = Geometric.find(g.id)
+
+ assert_equal '(5,6.1)', h.a_point
+ assert_equal '[(2,3),(5.5,7)]', h.a_line_segment
+ assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner
+ assert_equal '[(2,3),(5.5,7),(8.5,11)]', h.a_path
+ assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon
+ assert_equal '<(5.3,10.4),2>', h.a_circle
+
+ # use a geometric function to test for an open path
+ objs = Geometric.find_by_sql ["select isopen(a_path) from geometrics where id = ?", g.id]
+ assert_equal objs[0].isopen, 't'
+
+ # test alternate formats when defining the geometric types
+
+ g = Geometric.new(
+ :a_point => '5.0, 6.1',
+ #:a_line => '((2.0, 3), (5.5, 7.0))' # line type is currently unsupported in postgresql
+ :a_line_segment => '((2.0, 3), (5.5, 7.0))',
+ :a_box => '(2.0, 3), (5.5, 7.0)',
+ :a_path => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))', # ( ) is a closed path
+ :a_polygon => '2.0, 3, 5.5, 7.0, 8.5, 11.0',
+ :a_circle => '((5.3, 10.4), 2)'
+ )
+
+ assert g.save
+
+ # Reload and check that we have all the geometric attributes.
+ h = Geometric.find(g.id)
+
+ assert_equal '(5,6.1)', h.a_point
+ assert_equal '[(2,3),(5.5,7)]', h.a_line_segment
+ assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner
+ assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_path
+ assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon
+ assert_equal '<(5.3,10.4),2>', h.a_circle
+
+ # use a geometric function to test for an closed path
+ objs = Geometric.find_by_sql ["select isclosed(a_path) from geometrics where id = ?", g.id]
+ assert_equal objs[0].isclosed, 't'
+ end
+ end
+
+ class NumericData < ActiveRecord::Base
+ self.table_name = 'numeric_data'
+ end
+
+ def test_numeric_fields
+ m = NumericData.new(
+ :bank_balance => 1586.43,
+ :big_bank_balance => BigDecimal("1000234000567.95"),
+ :world_population => 6000000000,
+ :my_house_population => 3
+ )
+ assert m.save
+
+ m1 = NumericData.find(m.id)
+ assert_not_nil m1
+
+ # As with migration_test.rb, we should make world_population >= 2**62
+ # to cover 64-bit platforms and test it is a Bignum, but the main thing
+ # is that it's an Integer.
+ assert_kind_of Integer, m1.world_population
+ assert_equal 6000000000, m1.world_population
+
+ assert_kind_of Fixnum, m1.my_house_population
+ assert_equal 3, m1.my_house_population
+
+ assert_kind_of BigDecimal, m1.bank_balance
+ assert_equal BigDecimal("1586.43"), m1.bank_balance
+
+ assert_kind_of BigDecimal, m1.big_bank_balance
+ assert_equal BigDecimal("1000234000567.95"), m1.big_bank_balance
+ end
+
+ def test_auto_id
+ auto = AutoId.new
+ auto.save
+ assert (auto.id > 0)
+ end
+
+ def quote_column_name(name)
+ "<#{name}>"
+ end
+
+ def test_quote_keys
+ ar = AutoId.new
+ source = {"foo" => "bar", "baz" => "quux"}
+ actual = ar.send(:quote_columns, self, source)
+ inverted = actual.invert
+ assert_equal("<foo>", inverted["bar"])
+ assert_equal("<baz>", inverted["quux"])
+ end
+
+ def test_sql_injection_via_find
+ assert_raises(ActiveRecord::RecordNotFound, ActiveRecord::StatementInvalid) do
+ Topic.find("123456 OR id > 0")
+ end
+ end
+
+ def test_column_name_properly_quoted
+ col_record = ColumnName.new
+ col_record.references = 40
+ assert col_record.save
+ col_record.references = 41
+ assert col_record.save
+ assert_not_nil c2 = ColumnName.find(col_record.id)
+ assert_equal(41, c2.references)
+ end
+
+ def test_quoting_arrays
+ replies = Reply.find(:all, :conditions => [ "id IN (?)", topics(:first).replies.collect(&:id) ])
+ assert_equal topics(:first).replies.size, replies.size
+
+ replies = Reply.find(:all, :conditions => [ "id IN (?)", [] ])
+ assert_equal 0, replies.size
+ end
+
+ MyObject = Struct.new :attribute1, :attribute2
+
+ def test_serialized_attribute
+ myobj = MyObject.new('value1', 'value2')
+ topic = Topic.create("content" => myobj)
+ Topic.serialize("content", MyObject)
+ assert_equal(myobj, topic.content)
+ end
+
+ def test_nil_serialized_attribute_with_class_constraint
+ myobj = MyObject.new('value1', 'value2')
+ topic = Topic.new
+ assert_nil topic.content
+ end
+
+ def test_should_raise_exception_on_serialized_attribute_with_type_mismatch
+ myobj = MyObject.new('value1', 'value2')
+ topic = Topic.new(:content => myobj)
+ assert topic.save
+ Topic.serialize(:content, Hash)
+ assert_raise(ActiveRecord::SerializationTypeMismatch) { Topic.find(topic.id).content }
+ ensure
+ Topic.serialize(:content)
+ end
+
+ def test_serialized_attribute_with_class_constraint
+ settings = { "color" => "blue" }
+ Topic.serialize(:content, Hash)
+ topic = Topic.new(:content => settings)
+ assert topic.save
+ assert_equal(settings, Topic.find(topic.id).content)
+ ensure
+ Topic.serialize(:content)
+ end
+
+ def test_quote
+ author_name = "\\ \001 ' \n \\n \""
+ topic = Topic.create('author_name' => author_name)
+ assert_equal author_name, Topic.find(topic.id).author_name
+ end
+
+ if RUBY_VERSION < '1.9'
+ def test_quote_chars
+ str = 'The Narrator'
+ topic = Topic.create(:author_name => str)
+ assert_equal str, topic.author_name
+
+ assert_kind_of ActiveSupport::Multibyte::Chars, str.chars
+ topic = Topic.find_by_author_name(str.chars)
+
+ assert_kind_of Topic, topic
+ assert_equal str, topic.author_name, "The right topic should have been found by name even with name passed as Chars"
+ end
+ end
+
+ def test_class_level_destroy
+ should_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world")
+ Topic.find(1).replies << should_be_destroyed_reply
+
+ Topic.destroy(1)
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1) }
+ assert_raise(ActiveRecord::RecordNotFound) { Reply.find(should_be_destroyed_reply.id) }
+ end
+
+ def test_class_level_delete
+ should_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world")
+ Topic.find(1).replies << should_be_destroyed_reply
+
+ Topic.delete(1)
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1) }
+ assert_nothing_raised { Reply.find(should_be_destroyed_reply.id) }
+ end
+
+ def test_increment_attribute
+ assert_equal 50, accounts(:signals37).credit_limit
+ accounts(:signals37).increment! :credit_limit
+ assert_equal 51, accounts(:signals37, :reload).credit_limit
+
+ accounts(:signals37).increment(:credit_limit).increment!(:credit_limit)
+ assert_equal 53, accounts(:signals37, :reload).credit_limit
+ end
+
+ def test_increment_nil_attribute
+ assert_nil topics(:first).parent_id
+ topics(:first).increment! :parent_id
+ assert_equal 1, topics(:first).parent_id
+ end
+
+ def test_increment_attribute_by
+ assert_equal 50, accounts(:signals37).credit_limit
+ accounts(:signals37).increment! :credit_limit, 5
+ assert_equal 55, accounts(:signals37, :reload).credit_limit
+
+ accounts(:signals37).increment(:credit_limit, 1).increment!(:credit_limit, 3)
+ assert_equal 59, accounts(:signals37, :reload).credit_limit
+ end
+
+ def test_decrement_attribute
+ assert_equal 50, accounts(:signals37).credit_limit
+
+ accounts(:signals37).decrement!(:credit_limit)
+ assert_equal 49, accounts(:signals37, :reload).credit_limit
+
+ accounts(:signals37).decrement(:credit_limit).decrement!(:credit_limit)
+ assert_equal 47, accounts(:signals37, :reload).credit_limit
+ end
+
+ def test_decrement_attribute_by
+ assert_equal 50, accounts(:signals37).credit_limit
+ accounts(:signals37).decrement! :credit_limit, 5
+ assert_equal 45, accounts(:signals37, :reload).credit_limit
+
+ accounts(:signals37).decrement(:credit_limit, 1).decrement!(:credit_limit, 3)
+ assert_equal 41, accounts(:signals37, :reload).credit_limit
+ end
+
+ def test_toggle_attribute
+ assert !topics(:first).approved?
+ topics(:first).toggle!(:approved)
+ assert topics(:first).approved?
+ topic = topics(:first)
+ topic.toggle(:approved)
+ assert !topic.approved?
+ topic.reload
+ assert topic.approved?
+ end
+
+ def test_reload
+ t1 = Topic.find(1)
+ t2 = Topic.find(1)
+ t1.title = "something else"
+ t1.save
+ t2.reload
+ assert_equal t1.title, t2.title
+ end
+
+ def test_define_attr_method_with_value
+ k = Class.new( ActiveRecord::Base )
+ k.send(:define_attr_method, :table_name, "foo")
+ assert_equal "foo", k.table_name
+ end
+
+ def test_define_attr_method_with_block
+ k = Class.new( ActiveRecord::Base )
+ k.send(:define_attr_method, :primary_key) { "sys_" + original_primary_key }
+ assert_equal "sys_id", k.primary_key
+ end
+
+ def test_set_table_name_with_value
+ k = Class.new( ActiveRecord::Base )
+ k.table_name = "foo"
+ assert_equal "foo", k.table_name
+ k.set_table_name "bar"
+ assert_equal "bar", k.table_name
+ end
+
+ def test_set_table_name_with_block
+ k = Class.new( ActiveRecord::Base )
+ k.set_table_name { "ks" }
+ assert_equal "ks", k.table_name
+ end
+
+ def test_set_primary_key_with_value
+ k = Class.new( ActiveRecord::Base )
+ k.primary_key = "foo"
+ assert_equal "foo", k.primary_key
+ k.set_primary_key "bar"
+ assert_equal "bar", k.primary_key
+ end
+
+ def test_set_primary_key_with_block
+ k = Class.new( ActiveRecord::Base )
+ k.set_primary_key { "sys_" + original_primary_key }
+ assert_equal "sys_id", k.primary_key
+ end
+
+ def test_set_inheritance_column_with_value
+ k = Class.new( ActiveRecord::Base )
+ k.inheritance_column = "foo"
+ assert_equal "foo", k.inheritance_column
+ k.set_inheritance_column "bar"
+ assert_equal "bar", k.inheritance_column
+ end
+
+ def test_set_inheritance_column_with_block
+ k = Class.new( ActiveRecord::Base )
+ k.set_inheritance_column { original_inheritance_column + "_id" }
+ assert_equal "type_id", k.inheritance_column
+ end
+
+ def test_count_with_join
+ res = Post.count_by_sql "SELECT COUNT(*) FROM posts LEFT JOIN comments ON posts.id=comments.post_id WHERE posts.#{QUOTED_TYPE} = 'Post'"
+
+ res2 = Post.count(:conditions => "posts.#{QUOTED_TYPE} = 'Post'", :joins => "LEFT JOIN comments ON posts.id=comments.post_id")
+ assert_equal res, res2
+
+ res3 = nil
+ assert_nothing_raised do
+ res3 = Post.count(:conditions => "posts.#{QUOTED_TYPE} = 'Post'",
+ :joins => "LEFT JOIN comments ON posts.id=comments.post_id")
+ end
+ assert_equal res, res3
+
+ res4 = Post.count_by_sql "SELECT COUNT(p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id"
+ res5 = nil
+ assert_nothing_raised do
+ res5 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id",
+ :joins => "p, comments co",
+ :select => "p.id")
+ end
+
+ assert_equal res4, res5
+
+ unless current_adapter?(:SQLite2Adapter, :DeprecatedSQLiteAdapter)
+ res6 = Post.count_by_sql "SELECT COUNT(DISTINCT p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id"
+ res7 = nil
+ assert_nothing_raised do
+ res7 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id",
+ :joins => "p, comments co",
+ :select => "p.id",
+ :distinct => true)
+ end
+ assert_equal res6, res7
+ end
+ end
+
+ def test_clear_association_cache_stored
+ firm = Firm.find(1)
+ assert_kind_of Firm, firm
+
+ firm.clear_association_cache
+ assert_equal Firm.find(1).clients.collect{ |x| x.name }.sort, firm.clients.collect{ |x| x.name }.sort
+ end
+
+ def test_clear_association_cache_new_record
+ firm = Firm.new
+ client_stored = Client.find(3)
+ client_new = Client.new
+ client_new.name = "The Joneses"
+ clients = [ client_stored, client_new ]
+
+ firm.clients << clients
+ assert_equal clients.map(&:name).to_set, firm.clients.map(&:name).to_set
+
+ firm.clear_association_cache
+ assert_equal clients.map(&:name).to_set, firm.clients.map(&:name).to_set
+ end
+
+ def test_interpolate_sql
+ assert_nothing_raised { Category.new.send(:interpolate_sql, 'foo@bar') }
+ assert_nothing_raised { Category.new.send(:interpolate_sql, 'foo bar) baz') }
+ assert_nothing_raised { Category.new.send(:interpolate_sql, 'foo bar} baz') }
+ end
+
+ def test_scoped_find_conditions
+ scoped_developers = Developer.with_scope(:find => { :conditions => 'salary > 90000' }) do
+ Developer.find(:all, :conditions => 'id < 5')
+ end
+ assert !scoped_developers.include?(developers(:david)) # David's salary is less than 90,000
+ assert_equal 3, scoped_developers.size
+ end
+
+ def test_scoped_find_limit_offset
+ scoped_developers = Developer.with_scope(:find => { :limit => 3, :offset => 2 }) do
+ Developer.find(:all, :order => 'id')
+ end
+ assert !scoped_developers.include?(developers(:david))
+ assert !scoped_developers.include?(developers(:jamis))
+ assert_equal 3, scoped_developers.size
+
+ # Test without scoped find conditions to ensure we get the whole thing
+ developers = Developer.find(:all, :order => 'id')
+ assert_equal Developer.count, developers.size
+ end
+
+ def test_scoped_find_order
+ # Test order in scope
+ scoped_developers = Developer.with_scope(:find => { :limit => 1, :order => 'salary DESC' }) do
+ Developer.find(:all)
+ end
+ assert_equal 'Jamis', scoped_developers.first.name
+ assert scoped_developers.include?(developers(:jamis))
+ # Test scope without order and order in find
+ scoped_developers = Developer.with_scope(:find => { :limit => 1 }) do
+ Developer.find(:all, :order => 'salary DESC')
+ end
+ # Test scope order + find order, find has priority
+ scoped_developers = Developer.with_scope(:find => { :limit => 3, :order => 'id DESC' }) do
+ Developer.find(:all, :order => 'salary ASC')
+ end
+ assert scoped_developers.include?(developers(:poor_jamis))
+ assert scoped_developers.include?(developers(:david))
+ assert scoped_developers.include?(developers(:dev_10))
+ # Test without scoped find conditions to ensure we get the right thing
+ developers = Developer.find(:all, :order => 'id', :limit => 1)
+ assert scoped_developers.include?(developers(:david))
+ end
+
+ def test_scoped_find_limit_offset_including_has_many_association
+ topics = Topic.with_scope(:find => {:limit => 1, :offset => 1, :include => :replies}) do
+ Topic.find(:all, :order => "topics.id")
+ end
+ assert_equal 1, topics.size
+ assert_equal 2, topics.first.id
+ end
+
+ def test_scoped_find_order_including_has_many_association
+ developers = Developer.with_scope(:find => { :order => 'developers.salary DESC', :include => :projects }) do
+ Developer.find(:all)
+ end
+ assert developers.size >= 2
+ for i in 1...developers.size
+ assert developers[i-1].salary >= developers[i].salary
+ end
+ end
+
+ def test_abstract_class
+ assert !ActiveRecord::Base.abstract_class?
+ assert LoosePerson.abstract_class?
+ assert !LooseDescendant.abstract_class?
+ end
+
+ def test_base_class
+ assert_equal LoosePerson, LoosePerson.base_class
+ assert_equal LooseDescendant, LooseDescendant.base_class
+ assert_equal TightPerson, TightPerson.base_class
+ assert_equal TightPerson, TightDescendant.base_class
+
+ assert_equal Post, Post.base_class
+ assert_equal Post, SpecialPost.base_class
+ assert_equal Post, StiPost.base_class
+ assert_equal SubStiPost, SubStiPost.base_class
+ end
+
+ def test_descends_from_active_record
+ # Tries to call Object.abstract_class?
+ assert_raise(NoMethodError) do
+ ActiveRecord::Base.descends_from_active_record?
+ end
+
+ # Abstract subclass of AR::Base.
+ assert LoosePerson.descends_from_active_record?
+
+ # Concrete subclass of an abstract class.
+ assert LooseDescendant.descends_from_active_record?
+
+ # Concrete subclass of AR::Base.
+ assert TightPerson.descends_from_active_record?
+
+ # Concrete subclass of a concrete class but has no type column.
+ assert TightDescendant.descends_from_active_record?
+
+ # Concrete subclass of AR::Base.
+ assert Post.descends_from_active_record?
+
+ # Abstract subclass of a concrete class which has a type column.
+ # This is pathological, as you'll never have Sub < Abstract < Concrete.
+ assert !StiPost.descends_from_active_record?
+
+ # Concrete subclasses an abstract class which has a type column.
+ assert !SubStiPost.descends_from_active_record?
+ end
+
+ def test_find_on_abstract_base_class_doesnt_use_type_condition
+ old_class = LooseDescendant
+ Object.send :remove_const, :LooseDescendant
+
+ descendant = old_class.create!
+ assert_not_nil LoosePerson.find(descendant.id), "Should have found instance of LooseDescendant when finding abstract LoosePerson: #{descendant.inspect}"
+ ensure
+ unless Object.const_defined?(:LooseDescendant)
+ Object.const_set :LooseDescendant, old_class
+ end
+ end
+
+ def test_assert_queries
+ query = lambda { ActiveRecord::Base.connection.execute 'select count(*) from developers' }
+ assert_queries(2) { 2.times { query.call } }
+ assert_queries 1, &query
+ assert_no_queries { assert true }
+ end
+
+ def test_to_xml
+ xml = REXML::Document.new(topics(:first).to_xml(:indent => 0))
+ bonus_time_in_current_timezone = topics(:first).bonus_time.xmlschema
+ written_on_in_current_timezone = topics(:first).written_on.xmlschema
+ last_read_in_current_timezone = topics(:first).last_read.xmlschema
+
+ assert_equal "topic", xml.root.name
+ assert_equal "The First Topic" , xml.elements["//title"].text
+ assert_equal "David" , xml.elements["//author-name"].text
+
+ assert_equal "1", xml.elements["//id"].text
+ assert_equal "integer" , xml.elements["//id"].attributes['type']
+
+ assert_equal "1", xml.elements["//replies-count"].text
+ assert_equal "integer" , xml.elements["//replies-count"].attributes['type']
+
+ assert_equal written_on_in_current_timezone, xml.elements["//written-on"].text
+ assert_equal "datetime" , xml.elements["//written-on"].attributes['type']
+
+ assert_equal "--- Have a nice day\n" , xml.elements["//content"].text
+ assert_equal "yaml" , xml.elements["//content"].attributes['type']
+
+ assert_equal "david@loudthinking.com", xml.elements["//author-email-address"].text
+
+ assert_equal nil, xml.elements["//parent-id"].text
+ assert_equal "integer", xml.elements["//parent-id"].attributes['type']
+ assert_equal "true", xml.elements["//parent-id"].attributes['nil']
+
+ if current_adapter?(:SybaseAdapter, :SQLServerAdapter, :OracleAdapter)
+ assert_equal last_read_in_current_timezone, xml.elements["//last-read"].text
+ assert_equal "datetime" , xml.elements["//last-read"].attributes['type']
+ else
+ assert_equal "2004-04-15", xml.elements["//last-read"].text
+ assert_equal "date" , xml.elements["//last-read"].attributes['type']
+ end
+
+ # Oracle and DB2 don't have true boolean or time-only fields
+ unless current_adapter?(:OracleAdapter, :DB2Adapter)
+ assert_equal "false", xml.elements["//approved"].text
+ assert_equal "boolean" , xml.elements["//approved"].attributes['type']
+
+ assert_equal bonus_time_in_current_timezone, xml.elements["//bonus-time"].text
+ assert_equal "datetime" , xml.elements["//bonus-time"].attributes['type']
+ end
+ end
+
+ def test_to_xml_skipping_attributes
+ xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :replies_count])
+ assert_equal "<topic>", xml.first(7)
+ assert !xml.include?(%(<title>The First Topic</title>))
+ assert xml.include?(%(<author-name>David</author-name>))
+
+ xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :author_name, :replies_count])
+ assert !xml.include?(%(<title>The First Topic</title>))
+ assert !xml.include?(%(<author-name>David</author-name>))
+ end
+
+ def test_to_xml_including_has_many_association
+ xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :include => :replies, :except => :replies_count)
+ assert_equal "<topic>", xml.first(7)
+ assert xml.include?(%(<replies type="array"><reply>))
+ assert xml.include?(%(<title>The Second Topic's of the day</title>))
+ end
+
+ def test_array_to_xml_including_has_many_association
+ xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :include => :replies)
+ assert xml.include?(%(<replies type="array"><reply>))
+ end
+
+ def test_array_to_xml_including_methods
+ xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :methods => [ :topic_id ])
+ assert xml.include?(%(<topic-id type="integer">#{topics(:first).topic_id}</topic-id>)), xml
+ assert xml.include?(%(<topic-id type="integer">#{topics(:second).topic_id}</topic-id>)), xml
+ end
+
+ def test_array_to_xml_including_has_one_association
+ xml = [ companies(:first_firm), companies(:rails_core) ].to_xml(:indent => 0, :skip_instruct => true, :include => :account)
+ assert xml.include?(companies(:first_firm).account.to_xml(:indent => 0, :skip_instruct => true))
+ assert xml.include?(companies(:rails_core).account.to_xml(:indent => 0, :skip_instruct => true))
+ end
+
+ def test_array_to_xml_including_belongs_to_association
+ xml = [ companies(:first_client), companies(:second_client), companies(:another_client) ].to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
+ assert xml.include?(companies(:first_client).to_xml(:indent => 0, :skip_instruct => true))
+ assert xml.include?(companies(:second_client).firm.to_xml(:indent => 0, :skip_instruct => true))
+ assert xml.include?(companies(:another_client).firm.to_xml(:indent => 0, :skip_instruct => true))
+ end
+
+ def test_to_xml_including_belongs_to_association
+ xml = companies(:first_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
+ assert !xml.include?("<firm>")
+
+ xml = companies(:second_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
+ assert xml.include?("<firm>")
+ end
+
+ def test_to_xml_including_multiple_associations
+ xml = companies(:first_firm).to_xml(:indent => 0, :skip_instruct => true, :include => [ :clients, :account ])
+ assert_equal "<firm>", xml.first(6)
+ assert xml.include?(%(<account>))
+ assert xml.include?(%(<clients type="array"><client>))
+ end
+
+ def test_to_xml_including_multiple_associations_with_options
+ xml = companies(:first_firm).to_xml(
+ :indent => 0, :skip_instruct => true,
+ :include => { :clients => { :only => :name } }
+ )
+
+ assert_equal "<firm>", xml.first(6)
+ assert xml.include?(%(<client><name>Summit</name></client>))
+ assert xml.include?(%(<clients type="array"><client>))
+ end
+
+ def test_to_xml_including_methods
+ xml = Company.new.to_xml(:methods => :arbitrary_method, :skip_instruct => true)
+ assert_equal "<company>", xml.first(9)
+ assert xml.include?(%(<arbitrary-method>I am Jack's profound disappointment</arbitrary-method>))
+ end
+
+ def test_to_xml_with_block
+ value = "Rockin' the block"
+ xml = Company.new.to_xml(:skip_instruct => true) do |xml|
+ xml.tag! "arbitrary-element", value
+ end
+ assert_equal "<company>", xml.first(9)
+ assert xml.include?(%(<arbitrary-element>#{value}</arbitrary-element>))
+ end
+
+ def test_except_attributes
+ assert_equal(
+ %w( author_name type id approved replies_count bonus_time written_on content author_email_address parent_id last_read).sort,
+ topics(:first).attributes(:except => :title).keys.sort
+ )
+
+ assert_equal(
+ %w( replies_count bonus_time written_on content author_email_address parent_id last_read).sort,
+ topics(:first).attributes(:except => [ :title, :id, :type, :approved, :author_name ]).keys.sort
+ )
+ end
+
+ def test_include_attributes
+ assert_equal(%w( title ), topics(:first).attributes(:only => :title).keys)
+ assert_equal(%w( title author_name type id approved ).sort, topics(:first).attributes(:only => [ :title, :id, :type, :approved, :author_name ]).keys.sort)
+ end
+
+ def test_type_name_with_module_should_handle_beginning
+ assert_equal 'ActiveRecord::Person', ActiveRecord::Base.send(:type_name_with_module, 'Person')
+ assert_equal '::Person', ActiveRecord::Base.send(:type_name_with_module, '::Person')
+ end
+
+ def test_to_param_should_return_string
+ assert_kind_of String, Client.find(:first).to_param
+ end
+
+ def test_inspect_class
+ assert_equal 'ActiveRecord::Base', ActiveRecord::Base.inspect
+ assert_equal 'LoosePerson(abstract)', LoosePerson.inspect
+ assert_match(/^Topic\(id: integer, title: string/, Topic.inspect)
+ end
+
+ def test_inspect_instance
+ topic = topics(:first)
+ assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", written_on: "#{topic.written_on.to_s(:db)}", bonus_time: "#{topic.bonus_time.to_s(:db)}", last_read: "#{topic.last_read.to_s(:db)}", content: "Have a nice day", approved: false, replies_count: 1, parent_id: nil, type: nil>), topic.inspect
+ end
+
+ def test_inspect_new_instance
+ assert_match /Topic id: nil/, Topic.new.inspect
+ end
+
+ def test_inspect_limited_select_instance
+ assert_equal %(#<Topic id: 1>), Topic.find(:first, :select => 'id', :conditions => 'id = 1').inspect
+ assert_equal %(#<Topic id: 1, title: "The First Topic">), Topic.find(:first, :select => 'id, title', :conditions => 'id = 1').inspect
+ end
+
+ def test_inspect_class_without_table
+ assert_equal "NonExistentTable(Table doesn't exist)", NonExistentTable.inspect
+ end
+
+ def test_attribute_for_inspect
+ t = topics(:first)
+ t.title = "The First Topic Now Has A Title With\nNewlines And More Than 50 Characters"
+
+ assert_equal %("#{t.written_on.to_s(:db)}"), t.attribute_for_inspect(:written_on)
+ assert_equal '"The First Topic Now Has A Title With\nNewlines And M..."', t.attribute_for_inspect(:title)
+ end
+
+ def test_becomes
+ assert_kind_of Reply, topics(:first).becomes(Reply)
+ assert_equal "The First Topic", topics(:first).becomes(Reply).title
+ end
+
+ def test_silence_sets_log_level_to_error_in_block
+ original_logger = ActiveRecord::Base.logger
+ log = StringIO.new
+ ActiveRecord::Base.logger = Logger.new(log)
+ ActiveRecord::Base.logger.level = Logger::DEBUG
+ ActiveRecord::Base.silence do
+ ActiveRecord::Base.logger.warn "warn"
+ ActiveRecord::Base.logger.error "error"
+ end
+ assert_equal "error\n", log.string
+ ensure
+ ActiveRecord::Base.logger = original_logger
+ end
+
+ def test_silence_sets_log_level_back_to_level_before_yield
+ original_logger = ActiveRecord::Base.logger
+ log = StringIO.new
+ ActiveRecord::Base.logger = Logger.new(log)
+ ActiveRecord::Base.logger.level = Logger::WARN
+ ActiveRecord::Base.silence do
+ end
+ assert_equal Logger::WARN, ActiveRecord::Base.logger.level
+ ensure
+ ActiveRecord::Base.logger = original_logger
+ end
+
+ def test_benchmark_with_log_level
+ original_logger = ActiveRecord::Base.logger
+ log = StringIO.new
+ ActiveRecord::Base.logger = Logger.new(log)
+ ActiveRecord::Base.logger.level = Logger::WARN
+ ActiveRecord::Base.benchmark("Debug Topic Count", Logger::DEBUG) { Topic.count }
+ ActiveRecord::Base.benchmark("Warn Topic Count", Logger::WARN) { Topic.count }
+ ActiveRecord::Base.benchmark("Error Topic Count", Logger::ERROR) { Topic.count }
+ assert_no_match /Debug Topic Count/, log.string
+ assert_match /Warn Topic Count/, log.string
+ assert_match /Error Topic Count/, log.string
+ ensure
+ ActiveRecord::Base.logger = original_logger
+ end
+
+ def test_benchmark_with_use_silence
+ original_logger = ActiveRecord::Base.logger
+ log = StringIO.new
+ ActiveRecord::Base.logger = Logger.new(log)
+ ActiveRecord::Base.benchmark("Logging", Logger::DEBUG, true) { ActiveRecord::Base.logger.debug "Loud" }
+ ActiveRecord::Base.benchmark("Logging", Logger::DEBUG, false) { ActiveRecord::Base.logger.debug "Quiet" }
+ assert_no_match /Loud/, log.string
+ assert_match /Quiet/, log.string
+ ensure
+ ActiveRecord::Base.logger = original_logger
+ end
+end
diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb
new file mode 100644
index 0000000000..f89660e4e1
--- /dev/null
+++ b/activerecord/test/cases/binary_test.rb
@@ -0,0 +1,32 @@
+require 'abstract_unit'
+
+# Without using prepared statements, it makes no sense to test
+# BLOB data with SQL Server, because the length of a statement is
+# limited to 8KB.
+#
+# Without using prepared statements, it makes no sense to test
+# BLOB data with DB2 or Firebird, because the length of a statement
+# is limited to 32KB.
+unless current_adapter?(:SQLServerAdapter, :SybaseAdapter, :DB2Adapter, :FirebirdAdapter)
+ require 'fixtures/binary'
+
+ class BinaryTest < ActiveSupport::TestCase
+ FIXTURES = %w(flowers.jpg example.log)
+
+ def test_load_save
+ Binary.delete_all
+
+ FIXTURES.each do |filename|
+ data = File.read("#{File.dirname(__FILE__)}/fixtures/#{filename}").freeze
+
+ bin = Binary.new(:data => data)
+ assert_equal data, bin.data, 'Newly assigned data differs from original'
+
+ bin.save!
+ assert_equal data, bin.data, 'Data differs from original after save'
+
+ assert_equal data, bin.reload.data, 'Reloaded data differs from original'
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
new file mode 100644
index 0000000000..7bee644947
--- /dev/null
+++ b/activerecord/test/cases/calculations_test.rb
@@ -0,0 +1,251 @@
+require 'abstract_unit'
+require 'fixtures/company'
+require 'fixtures/topic'
+
+Company.has_many :accounts
+
+class NumericData < ActiveRecord::Base
+ self.table_name = 'numeric_data'
+end
+
+class CalculationsTest < ActiveSupport::TestCase
+ fixtures :companies, :accounts, :topics
+
+ def test_should_sum_field
+ assert_equal 318, Account.sum(:credit_limit)
+ end
+
+ def test_should_average_field
+ value = Account.average(:credit_limit)
+ assert_kind_of Float, value
+ assert_in_delta 53.0, value, 0.001
+ end
+
+ def test_should_return_nil_as_average
+ assert_nil NumericData.average(:bank_balance)
+ end
+
+ def test_should_get_maximum_of_field
+ assert_equal 60, Account.maximum(:credit_limit)
+ end
+
+ def test_should_get_maximum_of_field_with_include
+ assert_equal 50, Account.maximum(:credit_limit, :include => :firm, :conditions => "companies.name != 'Summit'")
+ end
+
+ def test_should_get_maximum_of_field_with_scoped_include
+ Account.with_scope :find => { :include => :firm, :conditions => "companies.name != 'Summit'" } do
+ assert_equal 50, Account.maximum(:credit_limit)
+ end
+ end
+
+ def test_should_get_minimum_of_field
+ assert_equal 50, Account.minimum(:credit_limit)
+ end
+
+ def test_should_group_by_field
+ c = Account.sum(:credit_limit, :group => :firm_id)
+ [1,6,2].each { |firm_id| assert c.keys.include?(firm_id) }
+ end
+
+ def test_should_group_by_summed_field
+ c = Account.sum(:credit_limit, :group => :firm_id)
+ assert_equal 50, c[1]
+ assert_equal 105, c[6]
+ assert_equal 60, c[2]
+ end
+
+ def test_should_order_by_grouped_field
+ c = Account.sum(:credit_limit, :group => :firm_id, :order => "firm_id")
+ assert_equal [1, 2, 6, 9], c.keys.compact
+ end
+
+ def test_should_order_by_calculation
+ c = Account.sum(:credit_limit, :group => :firm_id, :order => "sum_credit_limit desc, firm_id")
+ assert_equal [105, 60, 53, 50, 50], c.keys.collect { |k| c[k] }
+ assert_equal [6, 2, 9, 1], c.keys.compact
+ end
+
+ def test_should_limit_calculation
+ c = Account.sum(:credit_limit, :conditions => "firm_id IS NOT NULL",
+ :group => :firm_id, :order => "firm_id", :limit => 2)
+ assert_equal [1, 2], c.keys.compact
+ end
+
+ def test_should_limit_calculation_with_offset
+ c = Account.sum(:credit_limit, :conditions => "firm_id IS NOT NULL",
+ :group => :firm_id, :order => "firm_id", :limit => 2, :offset => 1)
+ assert_equal [2, 6], c.keys.compact
+ end
+
+ def test_should_group_by_summed_field_having_condition
+ c = Account.sum(:credit_limit, :group => :firm_id,
+ :having => 'sum(credit_limit) > 50')
+ assert_nil c[1]
+ assert_equal 105, c[6]
+ assert_equal 60, c[2]
+ end
+
+ def test_should_group_by_summed_association
+ c = Account.sum(:credit_limit, :group => :firm)
+ assert_equal 50, c[companies(:first_firm)]
+ assert_equal 105, c[companies(:rails_core)]
+ assert_equal 60, c[companies(:first_client)]
+ end
+
+ def test_should_sum_field_with_conditions
+ assert_equal 105, Account.sum(:credit_limit, :conditions => 'firm_id = 6')
+ end
+
+ def test_should_group_by_summed_field_with_conditions
+ c = Account.sum(:credit_limit, :conditions => 'firm_id > 1',
+ :group => :firm_id)
+ assert_nil c[1]
+ assert_equal 105, c[6]
+ assert_equal 60, c[2]
+ end
+
+ def test_should_group_by_summed_field_with_conditions_and_having
+ c = Account.sum(:credit_limit, :conditions => 'firm_id > 1',
+ :group => :firm_id,
+ :having => 'sum(credit_limit) > 60')
+ assert_nil c[1]
+ assert_equal 105, c[6]
+ assert_nil c[2]
+ end
+
+ def test_should_group_by_fields_with_table_alias
+ c = Account.sum(:credit_limit, :group => 'accounts.firm_id')
+ assert_equal 50, c[1]
+ assert_equal 105, c[6]
+ assert_equal 60, c[2]
+ end
+
+ def test_should_calculate_with_invalid_field
+ assert_equal 6, Account.calculate(:count, '*')
+ assert_equal 6, Account.calculate(:count, :all)
+ end
+
+ def test_should_calculate_grouped_with_invalid_field
+ c = Account.count(:all, :group => 'accounts.firm_id')
+ assert_equal 1, c[1]
+ assert_equal 2, c[6]
+ assert_equal 1, c[2]
+ end
+
+ def test_should_calculate_grouped_association_with_invalid_field
+ c = Account.count(:all, :group => :firm)
+ assert_equal 1, c[companies(:first_firm)]
+ assert_equal 2, c[companies(:rails_core)]
+ assert_equal 1, c[companies(:first_client)]
+ end
+
+ uses_mocha 'group_by_non_numeric_foreign_key_association' do
+ def test_should_group_by_association_with_non_numeric_foreign_key
+ ActiveRecord::Base.connection.expects(:select_all).returns([{"count_all" => 1, "firm_id" => "ABC"}])
+
+ firm = mock()
+ firm.expects(:id).returns("ABC")
+ firm.expects(:class).returns(Firm)
+ Company.expects(:find).with(["ABC"]).returns([firm])
+
+ column = mock()
+ column.expects(:name).at_least_once.returns(:firm_id)
+ column.expects(:type_cast).with("ABC").returns("ABC")
+ Account.expects(:columns).at_least_once.returns([column])
+
+ c = Account.count(:all, :group => :firm)
+ assert_equal Firm, c.first.first.class
+ assert_equal 1, c.first.last
+ end
+ end
+
+ def test_should_not_modify_options_when_using_includes
+ options = {:conditions => 'companies.id > 1', :include => :firm}
+ options_copy = options.dup
+
+ Account.count(:all, options)
+ assert_equal options_copy, options
+ end
+
+ def test_should_calculate_grouped_by_function
+ c = Company.count(:all, :group => "UPPER(#{QUOTED_TYPE})")
+ assert_equal 2, c[nil]
+ assert_equal 1, c['DEPENDENTFIRM']
+ assert_equal 3, c['CLIENT']
+ assert_equal 2, c['FIRM']
+ end
+
+ def test_should_calculate_grouped_by_function_with_table_alias
+ c = Company.count(:all, :group => "UPPER(companies.#{QUOTED_TYPE})")
+ assert_equal 2, c[nil]
+ assert_equal 1, c['DEPENDENTFIRM']
+ assert_equal 3, c['CLIENT']
+ assert_equal 2, c['FIRM']
+ end
+
+ def test_should_not_overshadow_enumerable_sum
+ assert_equal 6, [1, 2, 3].sum(&:abs)
+ end
+
+ def test_should_sum_scoped_field
+ assert_equal 15, companies(:rails_core).companies.sum(:id)
+ end
+
+ def test_should_sum_scoped_field_with_conditions
+ assert_equal 8, companies(:rails_core).companies.sum(:id, :conditions => 'id > 7')
+ end
+
+ def test_should_group_by_scoped_field
+ c = companies(:rails_core).companies.sum(:id, :group => :name)
+ assert_equal 7, c['Leetsoft']
+ assert_equal 8, c['Jadedpixel']
+ end
+
+ def test_should_group_by_summed_field_with_conditions_and_having
+ c = companies(:rails_core).companies.sum(:id, :group => :name,
+ :having => 'sum(id) > 7')
+ assert_nil c['Leetsoft']
+ assert_equal 8, c['Jadedpixel']
+ end
+
+ def test_should_reject_invalid_options
+ assert_nothing_raised do
+ [:count, :sum].each do |func|
+ # empty options are valid
+ Company.send(:validate_calculation_options, func)
+ # these options are valid for all calculations
+ [:select, :conditions, :joins, :order, :group, :having, :distinct].each do |opt|
+ Company.send(:validate_calculation_options, func, opt => true)
+ end
+ end
+
+ # :include is only valid on :count
+ Company.send(:validate_calculation_options, :count, :include => true)
+ end
+
+ assert_raises(ArgumentError) { Company.send(:validate_calculation_options, :sum, :foo => :bar) }
+ assert_raises(ArgumentError) { Company.send(:validate_calculation_options, :count, :foo => :bar) }
+ end
+
+ def test_should_count_selected_field_with_include
+ assert_equal 6, Account.count(:distinct => true, :include => :firm)
+ assert_equal 4, Account.count(:distinct => true, :include => :firm, :select => :credit_limit)
+ end
+
+ def test_count_with_column_parameter
+ assert_equal 5, Account.count(:firm_id)
+ end
+
+ def test_count_with_column_and_options_parameter
+ assert_equal 2, Account.count(:firm_id, :conditions => "credit_limit = 50")
+ end
+
+ def test_count_with_no_parameters_isnt_deprecated
+ assert_not_deprecated { Account.count }
+ end
+
+ def test_count_with_too_many_parameters_raises
+ assert_raise(ArgumentError) { Account.count(1, 2, 3) }
+ end
+end
diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb
new file mode 100644
index 0000000000..50c5d4ea62
--- /dev/null
+++ b/activerecord/test/cases/callbacks_test.rb
@@ -0,0 +1,400 @@
+require 'abstract_unit'
+
+class CallbackDeveloper < ActiveRecord::Base
+ set_table_name 'developers'
+
+ class << self
+ def callback_string(callback_method)
+ "history << [#{callback_method.to_sym.inspect}, :string]"
+ end
+
+ def callback_proc(callback_method)
+ Proc.new { |model| model.history << [callback_method, :proc] }
+ end
+
+ def define_callback_method(callback_method)
+ define_method("#{callback_method}_method") do |model|
+ model.history << [callback_method, :method]
+ end
+ end
+
+ def callback_object(callback_method)
+ klass = Class.new
+ klass.send(:define_method, callback_method) do |model|
+ model.history << [callback_method, :object]
+ end
+ klass.new
+ end
+ end
+
+ ActiveRecord::Callbacks::CALLBACKS.each do |callback_method|
+ callback_method_sym = callback_method.to_sym
+ define_callback_method(callback_method_sym)
+ send(callback_method, callback_method_sym)
+ send(callback_method, callback_string(callback_method_sym))
+ send(callback_method, callback_proc(callback_method_sym))
+ send(callback_method, callback_object(callback_method_sym))
+ send(callback_method) { |model| model.history << [callback_method_sym, :block] }
+ end
+
+ def history
+ @history ||= []
+ end
+
+ # after_initialize and after_find are invoked only if instance methods have been defined.
+ def after_initialize
+ end
+
+ def after_find
+ end
+end
+
+class ParentDeveloper < ActiveRecord::Base
+ set_table_name 'developers'
+ attr_accessor :after_save_called
+ before_validation {|record| record.after_save_called = true}
+end
+
+class ChildDeveloper < ParentDeveloper
+
+end
+
+class RecursiveCallbackDeveloper < ActiveRecord::Base
+ set_table_name 'developers'
+
+ before_save :on_before_save
+ after_save :on_after_save
+
+ attr_reader :on_before_save_called, :on_after_save_called
+
+ def on_before_save
+ @on_before_save_called ||= 0
+ @on_before_save_called += 1
+ save unless @on_before_save_called > 1
+ end
+
+ def on_after_save
+ @on_after_save_called ||= 0
+ @on_after_save_called += 1
+ save unless @on_after_save_called > 1
+ end
+end
+
+class ImmutableDeveloper < ActiveRecord::Base
+ set_table_name 'developers'
+
+ validates_inclusion_of :salary, :in => 50000..200000
+
+ before_save :cancel
+ before_destroy :cancel
+
+ def cancelled?
+ @cancelled == true
+ end
+
+ private
+ def cancel
+ @cancelled = true
+ false
+ end
+end
+
+class ImmutableMethodDeveloper < ActiveRecord::Base
+ set_table_name 'developers'
+
+ validates_inclusion_of :salary, :in => 50000..200000
+
+ def cancelled?
+ @cancelled == true
+ end
+
+ def before_save
+ @cancelled = true
+ false
+ end
+
+ def before_destroy
+ @cancelled = true
+ false
+ end
+end
+
+class CallbackCancellationDeveloper < ActiveRecord::Base
+ set_table_name 'developers'
+ def before_create
+ false
+ end
+end
+
+class CallbacksTest < ActiveSupport::TestCase
+ fixtures :developers
+
+ def test_initialize
+ david = CallbackDeveloper.new
+ assert_equal [
+ [ :after_initialize, :string ],
+ [ :after_initialize, :proc ],
+ [ :after_initialize, :object ],
+ [ :after_initialize, :block ],
+ ], david.history
+ end
+
+ def test_find
+ david = CallbackDeveloper.find(1)
+ assert_equal [
+ [ :after_find, :string ],
+ [ :after_find, :proc ],
+ [ :after_find, :object ],
+ [ :after_find, :block ],
+ [ :after_initialize, :string ],
+ [ :after_initialize, :proc ],
+ [ :after_initialize, :object ],
+ [ :after_initialize, :block ],
+ ], david.history
+ end
+
+ def test_new_valid?
+ david = CallbackDeveloper.new
+ david.valid?
+ assert_equal [
+ [ :after_initialize, :string ],
+ [ :after_initialize, :proc ],
+ [ :after_initialize, :object ],
+ [ :after_initialize, :block ],
+ [ :before_validation, :string ],
+ [ :before_validation, :proc ],
+ [ :before_validation, :object ],
+ [ :before_validation, :block ],
+ [ :before_validation_on_create, :string ],
+ [ :before_validation_on_create, :proc ],
+ [ :before_validation_on_create, :object ],
+ [ :before_validation_on_create, :block ],
+ [ :after_validation, :string ],
+ [ :after_validation, :proc ],
+ [ :after_validation, :object ],
+ [ :after_validation, :block ],
+ [ :after_validation_on_create, :string ],
+ [ :after_validation_on_create, :proc ],
+ [ :after_validation_on_create, :object ],
+ [ :after_validation_on_create, :block ]
+ ], david.history
+ end
+
+ def test_existing_valid?
+ david = CallbackDeveloper.find(1)
+ david.valid?
+ assert_equal [
+ [ :after_find, :string ],
+ [ :after_find, :proc ],
+ [ :after_find, :object ],
+ [ :after_find, :block ],
+ [ :after_initialize, :string ],
+ [ :after_initialize, :proc ],
+ [ :after_initialize, :object ],
+ [ :after_initialize, :block ],
+ [ :before_validation, :string ],
+ [ :before_validation, :proc ],
+ [ :before_validation, :object ],
+ [ :before_validation, :block ],
+ [ :before_validation_on_update, :string ],
+ [ :before_validation_on_update, :proc ],
+ [ :before_validation_on_update, :object ],
+ [ :before_validation_on_update, :block ],
+ [ :after_validation, :string ],
+ [ :after_validation, :proc ],
+ [ :after_validation, :object ],
+ [ :after_validation, :block ],
+ [ :after_validation_on_update, :string ],
+ [ :after_validation_on_update, :proc ],
+ [ :after_validation_on_update, :object ],
+ [ :after_validation_on_update, :block ]
+ ], david.history
+ end
+
+ def test_create
+ david = CallbackDeveloper.create('name' => 'David', 'salary' => 1000000)
+ assert_equal [
+ [ :after_initialize, :string ],
+ [ :after_initialize, :proc ],
+ [ :after_initialize, :object ],
+ [ :after_initialize, :block ],
+ [ :before_validation, :string ],
+ [ :before_validation, :proc ],
+ [ :before_validation, :object ],
+ [ :before_validation, :block ],
+ [ :before_validation_on_create, :string ],
+ [ :before_validation_on_create, :proc ],
+ [ :before_validation_on_create, :object ],
+ [ :before_validation_on_create, :block ],
+ [ :after_validation, :string ],
+ [ :after_validation, :proc ],
+ [ :after_validation, :object ],
+ [ :after_validation, :block ],
+ [ :after_validation_on_create, :string ],
+ [ :after_validation_on_create, :proc ],
+ [ :after_validation_on_create, :object ],
+ [ :after_validation_on_create, :block ],
+ [ :before_save, :string ],
+ [ :before_save, :proc ],
+ [ :before_save, :object ],
+ [ :before_save, :block ],
+ [ :before_create, :string ],
+ [ :before_create, :proc ],
+ [ :before_create, :object ],
+ [ :before_create, :block ],
+ [ :after_create, :string ],
+ [ :after_create, :proc ],
+ [ :after_create, :object ],
+ [ :after_create, :block ],
+ [ :after_save, :string ],
+ [ :after_save, :proc ],
+ [ :after_save, :object ],
+ [ :after_save, :block ]
+ ], david.history
+ end
+
+ def test_save
+ david = CallbackDeveloper.find(1)
+ david.save
+ assert_equal [
+ [ :after_find, :string ],
+ [ :after_find, :proc ],
+ [ :after_find, :object ],
+ [ :after_find, :block ],
+ [ :after_initialize, :string ],
+ [ :after_initialize, :proc ],
+ [ :after_initialize, :object ],
+ [ :after_initialize, :block ],
+ [ :before_validation, :string ],
+ [ :before_validation, :proc ],
+ [ :before_validation, :object ],
+ [ :before_validation, :block ],
+ [ :before_validation_on_update, :string ],
+ [ :before_validation_on_update, :proc ],
+ [ :before_validation_on_update, :object ],
+ [ :before_validation_on_update, :block ],
+ [ :after_validation, :string ],
+ [ :after_validation, :proc ],
+ [ :after_validation, :object ],
+ [ :after_validation, :block ],
+ [ :after_validation_on_update, :string ],
+ [ :after_validation_on_update, :proc ],
+ [ :after_validation_on_update, :object ],
+ [ :after_validation_on_update, :block ],
+ [ :before_save, :string ],
+ [ :before_save, :proc ],
+ [ :before_save, :object ],
+ [ :before_save, :block ],
+ [ :before_update, :string ],
+ [ :before_update, :proc ],
+ [ :before_update, :object ],
+ [ :before_update, :block ],
+ [ :after_update, :string ],
+ [ :after_update, :proc ],
+ [ :after_update, :object ],
+ [ :after_update, :block ],
+ [ :after_save, :string ],
+ [ :after_save, :proc ],
+ [ :after_save, :object ],
+ [ :after_save, :block ]
+ ], david.history
+ end
+
+ def test_destroy
+ david = CallbackDeveloper.find(1)
+ david.destroy
+ assert_equal [
+ [ :after_find, :string ],
+ [ :after_find, :proc ],
+ [ :after_find, :object ],
+ [ :after_find, :block ],
+ [ :after_initialize, :string ],
+ [ :after_initialize, :proc ],
+ [ :after_initialize, :object ],
+ [ :after_initialize, :block ],
+ [ :before_destroy, :string ],
+ [ :before_destroy, :proc ],
+ [ :before_destroy, :object ],
+ [ :before_destroy, :block ],
+ [ :after_destroy, :string ],
+ [ :after_destroy, :proc ],
+ [ :after_destroy, :object ],
+ [ :after_destroy, :block ]
+ ], david.history
+ end
+
+ def test_delete
+ david = CallbackDeveloper.find(1)
+ CallbackDeveloper.delete(david.id)
+ assert_equal [
+ [ :after_find, :string ],
+ [ :after_find, :proc ],
+ [ :after_find, :object ],
+ [ :after_find, :block ],
+ [ :after_initialize, :string ],
+ [ :after_initialize, :proc ],
+ [ :after_initialize, :object ],
+ [ :after_initialize, :block ],
+ ], david.history
+ end
+
+ def test_before_save_returning_false
+ david = ImmutableDeveloper.find(1)
+ assert david.valid?
+ assert !david.save
+ assert_raises(ActiveRecord::RecordNotSaved) { david.save! }
+
+ david = ImmutableDeveloper.find(1)
+ david.salary = 10_000_000
+ assert !david.valid?
+ assert !david.save
+ assert_raises(ActiveRecord::RecordInvalid) { david.save! }
+ end
+
+ def test_before_create_returning_false
+ someone = CallbackCancellationDeveloper.new
+ assert someone.valid?
+ assert !someone.save
+ end
+
+ def test_before_destroy_returning_false
+ david = ImmutableDeveloper.find(1)
+ assert !david.destroy
+ assert_not_nil ImmutableDeveloper.find_by_id(1)
+ end
+
+ def test_zzz_callback_returning_false # must be run last since we modify CallbackDeveloper
+ david = CallbackDeveloper.find(1)
+ CallbackDeveloper.before_validation proc { |model| model.history << [:before_validation, :returning_false]; return false }
+ CallbackDeveloper.before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] }
+ david.save
+ assert_equal [
+ [ :after_find, :string ],
+ [ :after_find, :proc ],
+ [ :after_find, :object ],
+ [ :after_find, :block ],
+ [ :after_initialize, :string ],
+ [ :after_initialize, :proc ],
+ [ :after_initialize, :object ],
+ [ :after_initialize, :block ],
+ [ :before_validation, :string ],
+ [ :before_validation, :proc ],
+ [ :before_validation, :object ],
+ [ :before_validation, :block ],
+ [ :before_validation, :returning_false ]
+ ], david.history
+ end
+
+ def test_inheritence_of_callbacks
+ parent = ParentDeveloper.new
+ assert !parent.after_save_called
+ parent.save
+ assert parent.after_save_called
+
+ child = ChildDeveloper.new
+ assert !child.after_save_called
+ child.save
+ assert child.after_save_called
+ end
+
+end
diff --git a/activerecord/test/cases/class_inheritable_attributes_test.rb b/activerecord/test/cases/class_inheritable_attributes_test.rb
new file mode 100644
index 0000000000..4f04f41e76
--- /dev/null
+++ b/activerecord/test/cases/class_inheritable_attributes_test.rb
@@ -0,0 +1,32 @@
+require 'test/unit'
+require 'abstract_unit'
+require 'active_support/core_ext/class/inheritable_attributes'
+
+class A
+ include ClassInheritableAttributes
+end
+
+class B < A
+ write_inheritable_array "first", [ :one, :two ]
+end
+
+class C < A
+ write_inheritable_array "first", [ :three ]
+end
+
+class D < B
+ write_inheritable_array "first", [ :four ]
+end
+
+
+class ClassInheritableAttributesTest < ActiveSupport::TestCase
+ def test_first_level
+ assert_equal [ :one, :two ], B.read_inheritable_attribute("first")
+ assert_equal [ :three ], C.read_inheritable_attribute("first")
+ end
+
+ def test_second_level
+ assert_equal [ :one, :two, :four ], D.read_inheritable_attribute("first")
+ assert_equal [ :one, :two ], B.read_inheritable_attribute("first")
+ end
+end
diff --git a/activerecord/test/cases/column_alias_test.rb b/activerecord/test/cases/column_alias_test.rb
new file mode 100644
index 0000000000..da4b66554a
--- /dev/null
+++ b/activerecord/test/cases/column_alias_test.rb
@@ -0,0 +1,17 @@
+require 'abstract_unit'
+require 'fixtures/topic'
+
+class TestColumnAlias < ActiveSupport::TestCase
+ fixtures :topics
+
+ QUERY = if 'Oracle' == ActiveRecord::Base.connection.adapter_name
+ 'SELECT id AS pk FROM topics WHERE ROWNUM < 2'
+ else
+ 'SELECT id AS pk FROM topics'
+ end
+
+ def test_column_alias
+ records = Topic.connection.select_all(QUERY)
+ assert_equal 'pk', records[0].keys[0]
+ end
+end
diff --git a/activerecord/test/cases/connection_test_firebird.rb b/activerecord/test/cases/connection_test_firebird.rb
new file mode 100644
index 0000000000..eb0222b66f
--- /dev/null
+++ b/activerecord/test/cases/connection_test_firebird.rb
@@ -0,0 +1,8 @@
+require "#{File.dirname(__FILE__)}/abstract_unit"
+
+class FirebirdConnectionTest < ActiveSupport::TestCase
+ def test_charset_properly_set
+ fb_conn = ActiveRecord::Base.connection.instance_variable_get(:@connection)
+ assert_equal 'UTF8', fb_conn.database.character_set
+ end
+end
diff --git a/activerecord/test/cases/connection_test_mysql.rb b/activerecord/test/cases/connection_test_mysql.rb
new file mode 100644
index 0000000000..e49a326d2b
--- /dev/null
+++ b/activerecord/test/cases/connection_test_mysql.rb
@@ -0,0 +1,30 @@
+require "#{File.dirname(__FILE__)}/abstract_unit"
+
+class MysqlConnectionTest < ActiveSupport::TestCase
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def test_no_automatic_reconnection_after_timeout
+ assert @connection.active?
+ @connection.update('set @@wait_timeout=1')
+ sleep 2
+ assert !@connection.active?
+ end
+
+ def test_successful_reconnection_after_timeout_with_manual_reconnect
+ assert @connection.active?
+ @connection.update('set @@wait_timeout=1')
+ sleep 2
+ @connection.reconnect!
+ assert @connection.active?
+ end
+
+ def test_successful_reconnection_after_timeout_with_verify
+ assert @connection.active?
+ @connection.update('set @@wait_timeout=1')
+ sleep 2
+ @connection.verify!(0)
+ assert @connection.active?
+ end
+end
diff --git a/activerecord/test/cases/copy_table_test_sqlite.rb b/activerecord/test/cases/copy_table_test_sqlite.rb
new file mode 100644
index 0000000000..ec58295cd1
--- /dev/null
+++ b/activerecord/test/cases/copy_table_test_sqlite.rb
@@ -0,0 +1,69 @@
+require 'abstract_unit'
+
+class CopyTableTest < ActiveSupport::TestCase
+ fixtures :companies, :comments
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ class << @connection
+ public :copy_table, :table_structure, :indexes
+ end
+ end
+
+ def test_copy_table(from = 'companies', to = 'companies2', options = {})
+ assert_nothing_raised {copy_table(from, to, options)}
+ assert_equal row_count(from), row_count(to)
+
+ if block_given?
+ yield from, to, options
+ else
+ assert_equal column_names(from), column_names(to)
+ end
+
+ @connection.drop_table(to) rescue nil
+ end
+
+ def test_copy_table_renaming_column
+ test_copy_table('companies', 'companies2',
+ :rename => {'client_of' => 'fan_of'}) do |from, to, options|
+ expected = column_values(from, 'client_of')
+ assert expected.any?, 'only nils in resultset; real values are needed'
+ assert_equal expected, column_values(to, 'fan_of')
+ end
+ end
+
+ def test_copy_table_with_index
+ test_copy_table('comments', 'comments_with_index') do
+ @connection.add_index('comments_with_index', ['post_id', 'type'])
+ test_copy_table('comments_with_index', 'comments_with_index2') do
+ assert_equal table_indexes_without_name('comments_with_index'),
+ table_indexes_without_name('comments_with_index2')
+ end
+ end
+ end
+
+ def test_copy_table_without_primary_key
+ test_copy_table('developers_projects', 'programmers_projects')
+ end
+
+protected
+ def copy_table(from, to, options = {})
+ @connection.copy_table(from, to, {:temporary => true}.merge(options))
+ end
+
+ def column_names(table)
+ @connection.table_structure(table).map {|column| column['name']}
+ end
+
+ def column_values(table, column)
+ @connection.select_all("SELECT #{column} FROM #{table} ORDER BY id").map {|row| row[column]}
+ end
+
+ def table_indexes_without_name(table)
+ @connection.indexes('comments_with_index').delete(:name)
+ end
+
+ def row_count(table)
+ @connection.select_one("SELECT COUNT(*) AS count FROM #{table}")['count']
+ end
+end
diff --git a/activerecord/test/cases/datatype_test_postgresql.rb b/activerecord/test/cases/datatype_test_postgresql.rb
new file mode 100644
index 0000000000..5b882f7a2b
--- /dev/null
+++ b/activerecord/test/cases/datatype_test_postgresql.rb
@@ -0,0 +1,203 @@
+require 'abstract_unit'
+
+class PostgresqlArray < ActiveRecord::Base
+end
+
+class PostgresqlMoney < ActiveRecord::Base
+end
+
+class PostgresqlNumber < ActiveRecord::Base
+end
+
+class PostgresqlTime < ActiveRecord::Base
+end
+
+class PostgresqlNetworkAddress < ActiveRecord::Base
+end
+
+class PostgresqlBitString < ActiveRecord::Base
+end
+
+class PostgresqlOid < ActiveRecord::Base
+end
+
+class PostgresqlDataTypeTest < ActiveSupport::TestCase
+ self.use_transactional_fixtures = false
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+
+ @connection.execute("INSERT INTO postgresql_arrays (commission_by_quarter, nicknames) VALUES ( '{35000,21000,18000,17000}', '{foo,bar,baz}' )")
+ @first_array = PostgresqlArray.find(1)
+
+ @connection.execute("INSERT INTO postgresql_moneys (wealth) VALUES ('$567.89')")
+ @connection.execute("INSERT INTO postgresql_moneys (wealth) VALUES ('-$567.89')")
+ @first_money = PostgresqlMoney.find(1)
+ @second_money = PostgresqlMoney.find(2)
+
+ @connection.execute("INSERT INTO postgresql_numbers (single, double) VALUES (123.456, 123456.789)")
+ @first_number = PostgresqlNumber.find(1)
+
+ @connection.execute("INSERT INTO postgresql_times (time_interval) VALUES ('1 year 2 days ago')")
+ @first_time = PostgresqlTime.find(1)
+
+ @connection.execute("INSERT INTO postgresql_network_addresses (cidr_address, inet_address, mac_address) VALUES('192.168.0/24', '172.16.1.254/32', '01:23:45:67:89:0a')")
+ @first_network_address = PostgresqlNetworkAddress.find(1)
+
+ @connection.execute("INSERT INTO postgresql_bit_strings (bit_string, bit_string_varying) VALUES (B'00010101', X'15')")
+ @first_bit_string = PostgresqlBitString.find(1)
+
+ @connection.execute("INSERT INTO postgresql_oids (obj_id) VALUES (1234)")
+ @first_oid = PostgresqlOid.find(1)
+ end
+
+ def test_data_type_of_array_types
+ assert_equal :string, @first_array.column_for_attribute(:commission_by_quarter).type
+ assert_equal :string, @first_array.column_for_attribute(:nicknames).type
+ end
+
+ def test_data_type_of_money_types
+ assert_equal :decimal, @first_money.column_for_attribute(:wealth).type
+ end
+
+ def test_data_type_of_number_types
+ assert_equal :float, @first_number.column_for_attribute(:single).type
+ assert_equal :float, @first_number.column_for_attribute(:double).type
+ end
+
+ def test_data_type_of_time_types
+ assert_equal :string, @first_time.column_for_attribute(:time_interval).type
+ end
+
+ def test_data_type_of_network_address_types
+ assert_equal :string, @first_network_address.column_for_attribute(:cidr_address).type
+ assert_equal :string, @first_network_address.column_for_attribute(:inet_address).type
+ assert_equal :string, @first_network_address.column_for_attribute(:mac_address).type
+ end
+
+ def test_data_type_of_bit_string_types
+ assert_equal :string, @first_bit_string.column_for_attribute(:bit_string).type
+ assert_equal :string, @first_bit_string.column_for_attribute(:bit_string_varying).type
+ end
+
+ def test_data_type_of_oid_types
+ assert_equal :integer, @first_oid.column_for_attribute(:obj_id).type
+ end
+
+ def test_array_values
+ assert_equal '{35000,21000,18000,17000}', @first_array.commission_by_quarter
+ assert_equal '{foo,bar,baz}', @first_array.nicknames
+ end
+
+ def test_money_values
+ assert_equal 567.89, @first_money.wealth
+ assert_equal -567.89, @second_money.wealth
+ end
+
+ def test_number_values
+ assert_equal 123.456, @first_number.single
+ assert_equal 123456.789, @first_number.double
+ end
+
+ def test_time_values
+ assert_equal '-1 years -2 days', @first_time.time_interval
+ end
+
+ def test_network_address_values
+ assert_equal '192.168.0.0/24', @first_network_address.cidr_address
+ assert_equal '172.16.1.254', @first_network_address.inet_address
+ assert_equal '01:23:45:67:89:0a', @first_network_address.mac_address
+ end
+
+ def test_bit_string_values
+ assert_equal '00010101', @first_bit_string.bit_string
+ assert_equal '00010101', @first_bit_string.bit_string_varying
+ end
+
+ def test_oid_values
+ assert_equal 1234, @first_oid.obj_id
+ end
+
+ def test_update_integer_array
+ new_value = '{32800,95000,29350,17000}'
+ assert @first_array.commission_by_quarter = new_value
+ assert @first_array.save
+ assert @first_array.reload
+ assert_equal @first_array.commission_by_quarter, new_value
+ assert @first_array.commission_by_quarter = new_value
+ assert @first_array.save
+ assert @first_array.reload
+ assert_equal @first_array.commission_by_quarter, new_value
+ end
+
+ def test_update_text_array
+ new_value = '{robby,robert,rob,robbie}'
+ assert @first_array.nicknames = new_value
+ assert @first_array.save
+ assert @first_array.reload
+ assert_equal @first_array.nicknames, new_value
+ assert @first_array.nicknames = new_value
+ assert @first_array.save
+ assert @first_array.reload
+ assert_equal @first_array.nicknames, new_value
+ end
+
+ def test_update_money
+ new_value = 123.45
+ assert @first_money.wealth = new_value
+ assert @first_money.save
+ assert @first_money.reload
+ assert_equal @first_money.wealth, new_value
+ end
+
+ def test_update_number
+ new_single = 789.012
+ new_double = 789012.345
+ assert @first_number.single = new_single
+ assert @first_number.double = new_double
+ assert @first_number.save
+ assert @first_number.reload
+ assert_equal @first_number.single, new_single
+ assert_equal @first_number.double, new_double
+ end
+
+ def test_update_time
+ assert @first_time.time_interval = '2 years 3 minutes'
+ assert @first_time.save
+ assert @first_time.reload
+ assert_equal @first_time.time_interval, '2 years 00:03:00'
+ end
+
+ def test_update_network_address
+ new_cidr_address = '10.1.2.3/32'
+ new_inet_address = '10.0.0.0/8'
+ new_mac_address = 'bc:de:f0:12:34:56'
+ assert @first_network_address.cidr_address = new_cidr_address
+ assert @first_network_address.inet_address = new_inet_address
+ assert @first_network_address.mac_address = new_mac_address
+ assert @first_network_address.save
+ assert @first_network_address.reload
+ assert_equal @first_network_address.cidr_address, new_cidr_address
+ assert_equal @first_network_address.inet_address, new_inet_address
+ assert_equal @first_network_address.mac_address, new_mac_address
+ end
+
+ def test_update_bit_string
+ new_bit_string = '11111111'
+ new_bit_string_varying = 'FF'
+ assert @first_bit_string.bit_string = new_bit_string
+ assert @first_bit_string.bit_string_varying = new_bit_string_varying
+ assert @first_bit_string.save
+ assert @first_bit_string.reload
+ assert_equal @first_bit_string.bit_string, new_bit_string
+ assert_equal @first_bit_string.bit_string, @first_bit_string.bit_string_varying
+ end
+
+ def test_update_oid
+ new_value = 567890
+ assert @first_oid.obj_id = new_value
+ assert @first_oid.save
+ assert @first_oid.reload
+ assert_equal @first_oid.obj_id, new_value
+ end
+end
diff --git a/activerecord/test/cases/date_time_test.rb b/activerecord/test/cases/date_time_test.rb
new file mode 100644
index 0000000000..20020b24cc
--- /dev/null
+++ b/activerecord/test/cases/date_time_test.rb
@@ -0,0 +1,37 @@
+require 'abstract_unit'
+require 'fixtures/topic'
+require 'fixtures/task'
+
+class DateTimeTest < ActiveSupport::TestCase
+ def test_saves_both_date_and_time
+ time_values = [1807, 2, 10, 15, 30, 45]
+ now = DateTime.civil(*time_values)
+
+ task = Task.new
+ task.starting = now
+ task.save!
+
+ # check against Time.local_time, since some platforms will return a Time instead of a DateTime
+ assert_equal Time.local_time(*time_values), Task.find(task.id).starting
+ end
+
+ def test_assign_empty_date_time
+ task = Task.new
+ task.starting = ''
+ task.ending = nil
+ assert_nil task.starting
+ assert_nil task.ending
+ end
+
+ def test_assign_empty_date
+ topic = Topic.new
+ topic.last_read = ''
+ assert_nil topic.last_read
+ end
+
+ def test_assign_empty_time
+ topic = Topic.new
+ topic.bonus_time = ''
+ assert_nil topic.bonus_time
+ end
+end
diff --git a/activerecord/test/cases/default_test_firebird.rb b/activerecord/test/cases/default_test_firebird.rb
new file mode 100644
index 0000000000..841027b175
--- /dev/null
+++ b/activerecord/test/cases/default_test_firebird.rb
@@ -0,0 +1,16 @@
+require 'abstract_unit'
+require 'fixtures/default'
+
+class DefaultTest < ActiveSupport::TestCase
+ def test_default_timestamp
+ default = Default.new
+ assert_instance_of(Time, default.default_timestamp)
+ assert_equal(:datetime, default.column_for_attribute(:default_timestamp).type)
+
+ # Variance should be small; increase if required -- e.g., if test db is on
+ # remote host and clocks aren't synchronized.
+ t1 = Time.new
+ accepted_variance = 1.0
+ assert_in_delta(t1.to_f, default.default_timestamp.to_f, accepted_variance)
+ end
+end
diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb
new file mode 100644
index 0000000000..8a5c8e0530
--- /dev/null
+++ b/activerecord/test/cases/defaults_test.rb
@@ -0,0 +1,67 @@
+require 'abstract_unit'
+require 'fixtures/default'
+require 'fixtures/entrant'
+
+class DefaultTest < ActiveSupport::TestCase
+ def test_nil_defaults_for_not_null_columns
+ column_defaults =
+ if current_adapter?(:MysqlAdapter)
+ { 'id' => nil, 'name' => '', 'course_id' => nil }
+ else
+ { 'id' => nil, 'name' => nil, 'course_id' => nil }
+ end
+
+ column_defaults.each do |name, default|
+ column = Entrant.columns_hash[name]
+ assert !column.null, "#{name} column should be NOT NULL"
+ assert_equal default, column.default, "#{name} column should be DEFAULT #{default.inspect}"
+ end
+ end
+
+ if current_adapter?(:MysqlAdapter)
+ # MySQL uses an implicit default 0 rather than NULL unless in strict mode.
+ # We use an implicit NULL so schema.rb is compatible with other databases.
+ def test_mysql_integer_not_null_defaults
+ klass = Class.new(ActiveRecord::Base)
+ klass.table_name = 'test_integer_not_null_default_zero'
+ klass.connection.create_table klass.table_name do |t|
+ t.column :zero, :integer, :null => false, :default => 0
+ t.column :omit, :integer, :null => false
+ end
+
+ assert_equal 0, klass.columns_hash['zero'].default
+ assert !klass.columns_hash['zero'].null
+ # 0 in MySQL 4, nil in 5.
+ assert [0, nil].include?(klass.columns_hash['omit'].default)
+ assert !klass.columns_hash['omit'].null
+
+ assert_raise(ActiveRecord::StatementInvalid) { klass.create! }
+
+ assert_nothing_raised do
+ instance = klass.create!(:omit => 1)
+ assert_equal 0, instance.zero
+ assert_equal 1, instance.omit
+ end
+ ensure
+ klass.connection.drop_table(klass.table_name) rescue nil
+ end
+ end
+
+ if current_adapter?(:PostgreSQLAdapter, :SQLServerAdapter, :FirebirdAdapter, :OpenBaseAdapter, :OracleAdapter)
+ def test_default_integers
+ default = Default.new
+ assert_instance_of Fixnum, default.positive_integer
+ assert_equal 1, default.positive_integer
+ assert_instance_of Fixnum, default.negative_integer
+ assert_equal -1, default.negative_integer
+ assert_instance_of BigDecimal, default.decimal_number
+ assert_equal BigDecimal.new("2.78"), default.decimal_number
+ end
+ end
+
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_multiline_default_text
+ assert_equal "--- []\n\n", Default.columns_hash['multiline_default'].default
+ end
+ end
+end
diff --git a/activerecord/test/cases/deprecated_finder_test.rb b/activerecord/test/cases/deprecated_finder_test.rb
new file mode 100755
index 0000000000..9c5a74cf02
--- /dev/null
+++ b/activerecord/test/cases/deprecated_finder_test.rb
@@ -0,0 +1,30 @@
+require 'abstract_unit'
+require 'fixtures/entrant'
+
+class DeprecatedFinderTest < ActiveSupport::TestCase
+ fixtures :entrants
+
+ def test_deprecated_find_all_was_removed
+ assert_raise(NoMethodError) { Entrant.find_all }
+ end
+
+ def test_deprecated_find_first_was_removed
+ assert_raise(NoMethodError) { Entrant.find_first }
+ end
+
+ def test_deprecated_find_on_conditions_was_removed
+ assert_raise(NoMethodError) { Entrant.find_on_conditions }
+ end
+
+ def test_count
+ assert_equal(0, Entrant.count(:conditions => "id > 3"))
+ assert_equal(1, Entrant.count(:conditions => ["id > ?", 2]))
+ assert_equal(2, Entrant.count(:conditions => ["id > ?", 1]))
+ end
+
+ def test_count_by_sql
+ assert_equal(0, Entrant.count_by_sql("SELECT COUNT(*) FROM entrants WHERE id > 3"))
+ assert_equal(1, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 2]))
+ assert_equal(2, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 1]))
+ end
+end
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
new file mode 100644
index 0000000000..14f494776c
--- /dev/null
+++ b/activerecord/test/cases/finder_test.rb
@@ -0,0 +1,650 @@
+require 'abstract_unit'
+require 'fixtures/author'
+require 'fixtures/comment'
+require 'fixtures/company'
+require 'fixtures/topic'
+require 'fixtures/reply'
+require 'fixtures/entrant'
+require 'fixtures/developer'
+require 'fixtures/post'
+
+class FinderTest < ActiveSupport::TestCase
+ fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors
+
+ def test_find
+ assert_equal(topics(:first).title, Topic.find(1).title)
+ end
+
+ # find should handle strings that come from URLs
+ # (example: Category.find(params[:id]))
+ def test_find_with_string
+ assert_equal(Topic.find(1).title,Topic.find("1").title)
+ end
+
+ def test_exists
+ assert Topic.exists?(1)
+ assert Topic.exists?("1")
+ assert Topic.exists?(:author_name => "David")
+ assert Topic.exists?(:author_name => "Mary", :approved => true)
+ assert Topic.exists?(["parent_id = ?", 1])
+ assert !Topic.exists?(45)
+
+ begin
+ assert !Topic.exists?("foo")
+ rescue ActiveRecord::StatementInvalid
+ # PostgreSQL complains about string comparison with integer field
+ rescue Exception
+ flunk
+ end
+
+ assert_raise(NoMethodError) { Topic.exists?([1,2]) }
+ end
+
+ def test_find_by_array_of_one_id
+ assert_kind_of(Array, Topic.find([ 1 ]))
+ assert_equal(1, Topic.find([ 1 ]).length)
+ end
+
+ def test_find_by_ids
+ assert_equal 2, Topic.find(1, 2).size
+ assert_equal topics(:second).title, Topic.find([2]).first.title
+ end
+
+ def test_find_by_ids_with_limit_and_offset
+ assert_equal 2, Entrant.find([1,3,2], :limit => 2).size
+ assert_equal 1, Entrant.find([1,3,2], :limit => 3, :offset => 2).size
+
+ # Also test an edge case: If you have 11 results, and you set a
+ # limit of 3 and offset of 9, then you should find that there
+ # will be only 2 results, regardless of the limit.
+ devs = Developer.find :all
+ last_devs = Developer.find devs.map(&:id), :limit => 3, :offset => 9
+ assert_equal 2, last_devs.size
+ end
+
+ def test_find_an_empty_array
+ assert_equal [], Topic.find([])
+ end
+
+ def test_find_by_ids_missing_one
+ assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, 2, 45) }
+ end
+
+ def test_find_all_with_limit
+ entrants = Entrant.find(:all, :order => "id ASC", :limit => 2)
+
+ assert_equal(2, entrants.size)
+ assert_equal(entrants(:first).name, entrants.first.name)
+ end
+
+ def test_find_all_with_prepared_limit_and_offset
+ entrants = Entrant.find(:all, :order => "id ASC", :limit => 2, :offset => 1)
+
+ assert_equal(2, entrants.size)
+ assert_equal(entrants(:second).name, entrants.first.name)
+
+ entrants = Entrant.find(:all, :order => "id ASC", :limit => 2, :offset => 2)
+ assert_equal(1, entrants.size)
+ assert_equal(entrants(:third).name, entrants.first.name)
+ end
+
+ def test_find_all_with_limit_and_offset_and_multiple_orderings
+ developers = Developer.find(:all, :order => "salary ASC, id DESC", :limit => 3, :offset => 1)
+ assert_equal ["David", "fixture_10", "fixture_9"], developers.collect {|d| d.name}
+ end
+
+ def test_find_with_limit_and_condition
+ developers = Developer.find(:all, :order => "id DESC", :conditions => "salary = 100000", :limit => 3, :offset =>7)
+ assert_equal(1, developers.size)
+ assert_equal("fixture_3", developers.first.name)
+ end
+
+ def test_find_with_entire_select_statement
+ topics = Topic.find_by_sql "SELECT * FROM topics WHERE author_name = 'Mary'"
+
+ assert_equal(1, topics.size)
+ assert_equal(topics(:second).title, topics.first.title)
+ end
+
+ def test_find_with_prepared_select_statement
+ topics = Topic.find_by_sql ["SELECT * FROM topics WHERE author_name = ?", "Mary"]
+
+ assert_equal(1, topics.size)
+ assert_equal(topics(:second).title, topics.first.title)
+ end
+
+ def test_find_by_sql_with_sti_on_joined_table
+ accounts = Account.find_by_sql("SELECT * FROM accounts INNER JOIN companies ON companies.id = accounts.firm_id")
+ assert_equal [Account], accounts.collect(&:class).uniq
+ end
+
+ def test_find_first
+ first = Topic.find(:first, :conditions => "title = 'The First Topic'")
+ assert_equal(topics(:first).title, first.title)
+ end
+
+ def test_find_first_failing
+ first = Topic.find(:first, :conditions => "title = 'The First Topic!'")
+ assert_nil(first)
+ end
+
+ def test_unexisting_record_exception_handling
+ assert_raises(ActiveRecord::RecordNotFound) {
+ Topic.find(1).parent
+ }
+
+ Topic.find(2).topic
+ end
+
+ def test_find_only_some_columns
+ topic = Topic.find(1, :select => "author_name")
+ assert_raises(ActiveRecord::MissingAttributeError) {topic.title}
+ assert_equal "David", topic.author_name
+ assert !topic.attribute_present?("title")
+ #assert !topic.respond_to?("title")
+ assert topic.attribute_present?("author_name")
+ assert topic.respond_to?("author_name")
+ end
+
+ def test_find_on_blank_conditions
+ [nil, " ", [], {}].each do |blank|
+ assert_nothing_raised { Topic.find(:first, :conditions => blank) }
+ end
+ end
+
+ def test_find_on_blank_bind_conditions
+ [ [""], ["",{}] ].each do |blank|
+ assert_nothing_raised { Topic.find(:first, :conditions => blank) }
+ end
+ end
+
+ def test_find_on_array_conditions
+ assert Topic.find(1, :conditions => ["approved = ?", false])
+ assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => ["approved = ?", true]) }
+ end
+
+ def test_find_on_hash_conditions
+ assert Topic.find(1, :conditions => { :approved => false })
+ assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :approved => true }) }
+ end
+
+ def test_find_on_hash_conditions_with_explicit_table_name
+ assert Topic.find(1, :conditions => { 'topics.approved' => false })
+ assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { 'topics.approved' => true }) }
+ end
+
+ def test_find_on_association_proxy_conditions
+ assert_equal [1, 2, 3, 5, 6, 7, 8, 9, 10], Comment.find_all_by_post_id(authors(:david).posts).map(&:id).sort
+ end
+
+ def test_find_on_hash_conditions_with_range
+ assert_equal [1,2], Topic.find(:all, :conditions => { :id => 1..2 }).map(&:id).sort
+ assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :id => 2..3 }) }
+ end
+
+ def test_find_on_hash_conditions_with_multiple_ranges
+ assert_equal [1,2,3], Comment.find(:all, :conditions => { :id => 1..3, :post_id => 1..2 }).map(&:id).sort
+ assert_equal [1], Comment.find(:all, :conditions => { :id => 1..1, :post_id => 1..10 }).map(&:id).sort
+ end
+
+ def test_find_on_multiple_hash_conditions
+ assert Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => false })
+ assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) }
+ assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "HHC", :replies_count => 1, :approved => false }) }
+ assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) }
+ end
+
+
+ def test_condition_interpolation
+ assert_kind_of Firm, Company.find(:first, :conditions => ["name = '%s'", "37signals"])
+ assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!"])
+ assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!' OR 1=1"])
+ assert_kind_of Time, Topic.find(:first, :conditions => ["id = %d", 1]).written_on
+ end
+
+ def test_condition_array_interpolation
+ assert_kind_of Firm, Company.find(:first, :conditions => ["name = '%s'", "37signals"])
+ assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!"])
+ assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!' OR 1=1"])
+ assert_kind_of Time, Topic.find(:first, :conditions => ["id = %d", 1]).written_on
+ end
+
+ def test_condition_hash_interpolation
+ assert_kind_of Firm, Company.find(:first, :conditions => { :name => "37signals"})
+ assert_nil Company.find(:first, :conditions => { :name => "37signals!"})
+ assert_kind_of Time, Topic.find(:first, :conditions => {:id => 1}).written_on
+ end
+
+ def test_hash_condition_find_malformed
+ assert_raises(ActiveRecord::StatementInvalid) {
+ Company.find(:first, :conditions => { :id => 2, :dhh => true })
+ }
+ end
+
+ def test_hash_condition_find_with_escaped_characters
+ Company.create("name" => "Ain't noth'n like' \#stuff")
+ assert Company.find(:first, :conditions => { :name => "Ain't noth'n like' \#stuff" })
+ end
+
+ def test_hash_condition_find_with_array
+ p1, p2 = Post.find(:all, :limit => 2, :order => 'id asc')
+ assert_equal [p1, p2], Post.find(:all, :conditions => { :id => [p1, p2] }, :order => 'id asc')
+ assert_equal [p1, p2], Post.find(:all, :conditions => { :id => [p1, p2.id] }, :order => 'id asc')
+ end
+
+ def test_hash_condition_find_with_nil
+ topic = Topic.find(:first, :conditions => { :last_read => nil } )
+ assert_not_nil topic
+ assert_nil topic.last_read
+ end
+
+ def test_bind_variables
+ assert_kind_of Firm, Company.find(:first, :conditions => ["name = ?", "37signals"])
+ assert_nil Company.find(:first, :conditions => ["name = ?", "37signals!"])
+ assert_nil Company.find(:first, :conditions => ["name = ?", "37signals!' OR 1=1"])
+ assert_kind_of Time, Topic.find(:first, :conditions => ["id = ?", 1]).written_on
+ assert_raises(ActiveRecord::PreparedStatementInvalid) {
+ Company.find(:first, :conditions => ["id=? AND name = ?", 2])
+ }
+ assert_raises(ActiveRecord::PreparedStatementInvalid) {
+ Company.find(:first, :conditions => ["id=?", 2, 3, 4])
+ }
+ end
+
+ def test_bind_variables_with_quotes
+ Company.create("name" => "37signals' go'es agains")
+ assert Company.find(:first, :conditions => ["name = ?", "37signals' go'es agains"])
+ end
+
+ def test_named_bind_variables_with_quotes
+ Company.create("name" => "37signals' go'es agains")
+ assert Company.find(:first, :conditions => ["name = :name", {:name => "37signals' go'es agains"}])
+ end
+
+ def test_bind_arity
+ assert_nothing_raised { bind '' }
+ assert_raises(ActiveRecord::PreparedStatementInvalid) { bind '', 1 }
+
+ assert_raises(ActiveRecord::PreparedStatementInvalid) { bind '?' }
+ assert_nothing_raised { bind '?', 1 }
+ assert_raises(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 }
+ end
+
+ def test_named_bind_variables
+ assert_equal '1', bind(':a', :a => 1) # ' ruby-mode
+ assert_equal '1 1', bind(':a :a', :a => 1) # ' ruby-mode
+
+ assert_kind_of Firm, Company.find(:first, :conditions => ["name = :name", { :name => "37signals" }])
+ assert_nil Company.find(:first, :conditions => ["name = :name", { :name => "37signals!" }])
+ assert_nil Company.find(:first, :conditions => ["name = :name", { :name => "37signals!' OR 1=1" }])
+ assert_kind_of Time, Topic.find(:first, :conditions => ["id = :id", { :id => 1 }]).written_on
+ end
+
+ def test_bind_enumerable
+ quoted_abc = %(#{ActiveRecord::Base.connection.quote('a')},#{ActiveRecord::Base.connection.quote('b')},#{ActiveRecord::Base.connection.quote('c')})
+
+ assert_equal '1,2,3', bind('?', [1, 2, 3])
+ assert_equal quoted_abc, bind('?', %w(a b c))
+
+ assert_equal '1,2,3', bind(':a', :a => [1, 2, 3])
+ assert_equal quoted_abc, bind(':a', :a => %w(a b c)) # '
+
+ require 'set'
+ assert_equal '1,2,3', bind('?', Set.new([1, 2, 3]))
+ assert_equal quoted_abc, bind('?', Set.new(%w(a b c)))
+
+ assert_equal '1,2,3', bind(':a', :a => Set.new([1, 2, 3]))
+ assert_equal quoted_abc, bind(':a', :a => Set.new(%w(a b c))) # '
+ end
+
+ def test_bind_empty_enumerable
+ quoted_nil = ActiveRecord::Base.connection.quote(nil)
+ assert_equal quoted_nil, bind('?', [])
+ assert_equal " in (#{quoted_nil})", bind(' in (?)', [])
+ assert_equal "foo in (#{quoted_nil})", bind('foo in (?)', [])
+ end
+
+ def test_bind_string
+ assert_equal ActiveRecord::Base.connection.quote(''), bind('?', '')
+ end
+
+ def test_bind_record
+ o = Struct.new(:quoted_id).new(1)
+ assert_equal '1', bind('?', o)
+
+ os = [o] * 3
+ assert_equal '1,1,1', bind('?', os)
+ end
+
+ def test_string_sanitation
+ assert_not_equal "#{ActiveRecord::Base.connection.quoted_string_prefix}'something ' 1=1'", ActiveRecord::Base.sanitize("something ' 1=1")
+ assert_equal "#{ActiveRecord::Base.connection.quoted_string_prefix}'something; select table'", ActiveRecord::Base.sanitize("something; select table")
+ end
+
+ def test_count
+ assert_equal(0, Entrant.count(:conditions => "id > 3"))
+ assert_equal(1, Entrant.count(:conditions => ["id > ?", 2]))
+ assert_equal(2, Entrant.count(:conditions => ["id > ?", 1]))
+ end
+
+ def test_count_by_sql
+ assert_equal(0, Entrant.count_by_sql("SELECT COUNT(*) FROM entrants WHERE id > 3"))
+ assert_equal(1, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 2]))
+ assert_equal(2, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 1]))
+ end
+
+ def test_find_by_one_attribute
+ assert_equal topics(:first), Topic.find_by_title("The First Topic")
+ assert_nil Topic.find_by_title("The First Topic!")
+ end
+
+ def test_find_by_one_attribute_caches_dynamic_finder
+ # ensure this test can run independently of order
+ class << Topic; self; end.send(:remove_method, :find_by_title) if Topic.respond_to?(:find_by_title)
+ assert !Topic.respond_to?(:find_by_title)
+ t = Topic.find_by_title("The First Topic")
+ assert Topic.respond_to?(:find_by_title)
+ end
+
+ def test_dynamic_finder_returns_same_results_after_caching
+ # ensure this test can run independently of order
+ class << Topic; self; end.send(:remove_method, :find_by_title) if Topic.respond_to?(:find_by_title)
+ t = Topic.find_by_title("The First Topic")
+ assert_equal t, Topic.find_by_title("The First Topic") # find_by_title has been cached
+ end
+
+ def test_find_by_one_attribute_with_order_option
+ assert_equal accounts(:signals37), Account.find_by_credit_limit(50, :order => 'id')
+ assert_equal accounts(:rails_core_account), Account.find_by_credit_limit(50, :order => 'id DESC')
+ end
+
+ def test_find_by_one_attribute_with_conditions
+ assert_equal accounts(:rails_core_account), Account.find_by_credit_limit(50, :conditions => ['firm_id = ?', 6])
+ end
+
+ def test_dynamic_finder_on_one_attribute_with_conditions_caches_method
+ # ensure this test can run independently of order
+ class << Account; self; end.send(:remove_method, :find_by_credit_limit) if Account.respond_to?(:find_by_credit_limit)
+ assert !Account.respond_to?(:find_by_credit_limit)
+ a = Account.find_by_credit_limit(50, :conditions => ['firm_id = ?', 6])
+ assert Account.respond_to?(:find_by_credit_limit)
+ end
+
+ def test_dynamic_finder_on_one_attribute_with_conditions_returns_same_results_after_caching
+ # ensure this test can run independently of order
+ class << Account; self; end.send(:remove_method, :find_by_credit_limit) if Account.respond_to?(:find_by_credit_limit)
+ a = Account.find_by_credit_limit(50, :conditions => ['firm_id = ?', 6])
+ assert_equal a, Account.find_by_credit_limit(50, :conditions => ['firm_id = ?', 6]) # find_by_credit_limit has been cached
+ end
+
+ def test_find_by_one_attribute_with_several_options
+ assert_equal accounts(:unknown), Account.find_by_credit_limit(50, :order => 'id DESC', :conditions => ['id != ?', 3])
+ end
+
+ def test_find_by_one_missing_attribute
+ assert_raises(NoMethodError) { Topic.find_by_undertitle("The First Topic!") }
+ end
+
+ def test_find_by_invalid_method_syntax
+ assert_raises(NoMethodError) { Topic.fail_to_find_by_title("The First Topic") }
+ assert_raises(NoMethodError) { Topic.find_by_title?("The First Topic") }
+ assert_raises(NoMethodError) { Topic.fail_to_find_or_create_by_title("Nonexistent Title") }
+ assert_raises(NoMethodError) { Topic.find_or_create_by_title?("Nonexistent Title") }
+ end
+
+ def test_find_by_two_attributes
+ assert_equal topics(:first), Topic.find_by_title_and_author_name("The First Topic", "David")
+ assert_nil Topic.find_by_title_and_author_name("The First Topic", "Mary")
+ end
+
+ def test_find_all_by_one_attribute
+ topics = Topic.find_all_by_content("Have a nice day")
+ assert_equal 2, topics.size
+ assert topics.include?(topics(:first))
+
+ assert_equal [], Topic.find_all_by_title("The First Topic!!")
+ end
+
+ def test_find_all_by_one_attribute_with_options
+ topics = Topic.find_all_by_content("Have a nice day", :order => "id DESC")
+ assert topics(:first), topics.last
+
+ topics = Topic.find_all_by_content("Have a nice day", :order => "id")
+ assert topics(:first), topics.first
+ end
+
+ def test_find_all_by_array_attribute
+ assert_equal 2, Topic.find_all_by_title(["The First Topic", "The Second Topic's of the day"]).size
+ end
+
+ def test_find_all_by_boolean_attribute
+ topics = Topic.find_all_by_approved(false)
+ assert_equal 1, topics.size
+ assert topics.include?(topics(:first))
+
+ topics = Topic.find_all_by_approved(true)
+ assert_equal 1, topics.size
+ assert topics.include?(topics(:second))
+ end
+
+ def test_find_by_nil_attribute
+ topic = Topic.find_by_last_read nil
+ assert_not_nil topic
+ assert_nil topic.last_read
+ end
+
+ def test_find_all_by_nil_attribute
+ topics = Topic.find_all_by_last_read nil
+ assert_equal 1, topics.size
+ assert_nil topics[0].last_read
+ end
+
+ def test_find_by_nil_and_not_nil_attributes
+ topic = Topic.find_by_last_read_and_author_name nil, "Mary"
+ assert_equal "Mary", topic.author_name
+ end
+
+ def test_find_all_by_nil_and_not_nil_attributes
+ topics = Topic.find_all_by_last_read_and_author_name nil, "Mary"
+ assert_equal 1, topics.size
+ assert_equal "Mary", topics[0].author_name
+ end
+
+ def test_find_or_create_from_one_attribute
+ number_of_companies = Company.count
+ sig38 = Company.find_or_create_by_name("38signals")
+ assert_equal number_of_companies + 1, Company.count
+ assert_equal sig38, Company.find_or_create_by_name("38signals")
+ assert !sig38.new_record?
+ end
+
+ def test_find_or_create_from_two_attributes
+ number_of_topics = Topic.count
+ another = Topic.find_or_create_by_title_and_author_name("Another topic","John")
+ assert_equal number_of_topics + 1, Topic.count
+ assert_equal another, Topic.find_or_create_by_title_and_author_name("Another topic", "John")
+ assert !another.new_record?
+ end
+
+ def test_find_or_create_from_one_attribute_and_hash
+ number_of_companies = Company.count
+ sig38 = Company.find_or_create_by_name({:name => "38signals", :firm_id => 17, :client_of => 23})
+ assert_equal number_of_companies + 1, Company.count
+ assert_equal sig38, Company.find_or_create_by_name({:name => "38signals", :firm_id => 17, :client_of => 23})
+ assert !sig38.new_record?
+ assert_equal "38signals", sig38.name
+ assert_equal 17, sig38.firm_id
+ assert_equal 23, sig38.client_of
+ end
+
+ def test_find_or_initialize_from_one_attribute
+ sig38 = Company.find_or_initialize_by_name("38signals")
+ assert_equal "38signals", sig38.name
+ assert sig38.new_record?
+ end
+
+ def test_find_or_initialize_from_one_attribute_should_set_attribute_even_when_protected
+ c = Company.find_or_initialize_by_name_and_rating("Fortune 1000", 1000)
+ assert_equal "Fortune 1000", c.name
+ assert_equal 1000, c.rating
+ assert c.valid?
+ assert c.new_record?
+ end
+
+ def test_find_or_create_from_one_attribute_should_set_attribute_even_when_protected
+ c = Company.find_or_create_by_name_and_rating("Fortune 1000", 1000)
+ assert_equal "Fortune 1000", c.name
+ assert_equal 1000, c.rating
+ assert c.valid?
+ assert !c.new_record?
+ end
+
+ def test_dynamic_find_or_initialize_from_one_attribute_caches_method
+ class << Company; self; end.send(:remove_method, :find_or_initialize_by_name) if Company.respond_to?(:find_or_initialize_by_name)
+ assert !Company.respond_to?(:find_or_initialize_by_name)
+ sig38 = Company.find_or_initialize_by_name("38signals")
+ assert Company.respond_to?(:find_or_initialize_by_name)
+ end
+
+ def test_find_or_initialize_from_two_attributes
+ another = Topic.find_or_initialize_by_title_and_author_name("Another topic","John")
+ assert_equal "Another topic", another.title
+ assert_equal "John", another.author_name
+ assert another.new_record?
+ end
+
+ def test_find_or_initialize_from_one_attribute_and_hash
+ sig38 = Company.find_or_initialize_by_name({:name => "38signals", :firm_id => 17, :client_of => 23})
+ assert_equal "38signals", sig38.name
+ assert_equal 17, sig38.firm_id
+ assert_equal 23, sig38.client_of
+ assert sig38.new_record?
+ end
+
+ def test_find_with_bad_sql
+ assert_raises(ActiveRecord::StatementInvalid) { Topic.find_by_sql "select 1 from badtable" }
+ end
+
+ def test_find_with_invalid_params
+ assert_raises(ArgumentError) { Topic.find :first, :join => "It should be `joins'" }
+ assert_raises(ArgumentError) { Topic.find :first, :conditions => '1 = 1', :join => "It should be `joins'" }
+ end
+
+ def test_dynamic_finder_with_invalid_params
+ assert_raises(ArgumentError) { Topic.find_by_title 'No Title', :join => "It should be `joins'" }
+ end
+
+ def test_find_all_with_limit
+ first_five_developers = Developer.find :all, :order => 'id ASC', :limit => 5
+ assert_equal 5, first_five_developers.length
+ assert_equal 'David', first_five_developers.first.name
+ assert_equal 'fixture_5', first_five_developers.last.name
+
+ no_developers = Developer.find :all, :order => 'id ASC', :limit => 0
+ assert_equal 0, no_developers.length
+ end
+
+ def test_find_all_with_limit_and_offset
+ first_three_developers = Developer.find :all, :order => 'id ASC', :limit => 3, :offset => 0
+ second_three_developers = Developer.find :all, :order => 'id ASC', :limit => 3, :offset => 3
+ last_two_developers = Developer.find :all, :order => 'id ASC', :limit => 2, :offset => 8
+
+ assert_equal 3, first_three_developers.length
+ assert_equal 3, second_three_developers.length
+ assert_equal 2, last_two_developers.length
+
+ assert_equal 'David', first_three_developers.first.name
+ assert_equal 'fixture_4', second_three_developers.first.name
+ assert_equal 'fixture_9', last_two_developers.first.name
+ end
+
+ def test_find_all_with_limit_and_offset_and_multiple_order_clauses
+ first_three_posts = Post.find :all, :order => 'author_id, id', :limit => 3, :offset => 0
+ second_three_posts = Post.find :all, :order => ' author_id,id ', :limit => 3, :offset => 3
+ last_posts = Post.find :all, :order => ' author_id, id ', :limit => 3, :offset => 6
+
+ assert_equal [[0,3],[1,1],[1,2]], first_three_posts.map { |p| [p.author_id, p.id] }
+ assert_equal [[1,4],[1,5],[1,6]], second_three_posts.map { |p| [p.author_id, p.id] }
+ assert_equal [[2,7]], last_posts.map { |p| [p.author_id, p.id] }
+ end
+
+ def test_find_all_with_join
+ developers_on_project_one = Developer.find(
+ :all,
+ :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id',
+ :conditions => 'project_id=1'
+ )
+ assert_equal 3, developers_on_project_one.length
+ developer_names = developers_on_project_one.map { |d| d.name }
+ assert developer_names.include?('David')
+ assert developer_names.include?('Jamis')
+ end
+
+ def test_joins_dont_clobber_id
+ first = Firm.find(
+ :first,
+ :joins => 'INNER JOIN companies AS clients ON clients.firm_id = companies.id',
+ :conditions => 'companies.id = 1'
+ )
+ assert_equal 1, first.id
+ end
+
+ def test_find_by_id_with_conditions_with_or
+ assert_nothing_raised do
+ Post.find([1,2,3],
+ :conditions => "posts.id <= 3 OR posts.#{QUOTED_TYPE} = 'Post'")
+ end
+ end
+
+ # http://dev.rubyonrails.org/ticket/6778
+ def test_find_ignores_previously_inserted_record
+ post = Post.create!(:title => 'test', :body => 'it out')
+ assert_equal [], Post.find_all_by_id(nil)
+ end
+
+ def test_find_by_empty_ids
+ assert_equal [], Post.find([])
+ end
+
+ def test_find_by_empty_in_condition
+ assert_equal [], Post.find(:all, :conditions => ['id in (?)', []])
+ end
+
+ def test_find_by_records
+ p1, p2 = Post.find(:all, :limit => 2, :order => 'id asc')
+ assert_equal [p1, p2], Post.find(:all, :conditions => ['id in (?)', [p1, p2]], :order => 'id asc')
+ assert_equal [p1, p2], Post.find(:all, :conditions => ['id in (?)', [p1, p2.id]], :order => 'id asc')
+ end
+
+ def test_select_value
+ assert_equal "37signals", Company.connection.select_value("SELECT name FROM companies WHERE id = 1")
+ assert_nil Company.connection.select_value("SELECT name FROM companies WHERE id = -1")
+ # make sure we didn't break count...
+ assert_equal 0, Company.count_by_sql("SELECT COUNT(*) FROM companies WHERE name = 'Halliburton'")
+ assert_equal 1, Company.count_by_sql("SELECT COUNT(*) FROM companies WHERE name = '37signals'")
+ end
+
+ def test_select_values
+ assert_equal ["1","2","3","4","5","6","7","8","9"], Company.connection.select_values("SELECT id FROM companies ORDER BY id").map! { |i| i.to_s }
+ assert_equal ["37signals","Summit","Microsoft", "Flamboyant Software", "Ex Nihilo", "RailsCore", "Leetsoft", "Jadedpixel", "Odegy"], Company.connection.select_values("SELECT name FROM companies ORDER BY id")
+ end
+
+ def test_select_rows
+ assert_equal(
+ [["1", nil, nil, "37signals"],
+ ["2", "1", "2", "Summit"],
+ ["3", "1", "1", "Microsoft"]],
+ Company.connection.select_rows("SELECT id, firm_id, client_of, name FROM companies WHERE id IN (1,2,3) ORDER BY id").map! {|i| i.map! {|j| j.to_s unless j.nil?}})
+ assert_equal [["1", "37signals"], ["2", "Summit"], ["3", "Microsoft"]],
+ Company.connection.select_rows("SELECT id, name FROM companies WHERE id IN (1,2,3) ORDER BY id").map! {|i| i.map! {|j| j.to_s unless j.nil?}}
+ end
+
+ protected
+ def bind(statement, *vars)
+ if vars.first.is_a?(Hash)
+ ActiveRecord::Base.send(:replace_named_bind_variables, statement, vars.first)
+ else
+ ActiveRecord::Base.send(:replace_bind_variables, statement, vars)
+ end
+ end
+end
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
new file mode 100755
index 0000000000..51dbee6af3
--- /dev/null
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -0,0 +1,594 @@
+require 'abstract_unit'
+require 'fixtures/post'
+require 'fixtures/binary'
+require 'fixtures/topic'
+require 'fixtures/computer'
+require 'fixtures/developer'
+require 'fixtures/company'
+require 'fixtures/task'
+require 'fixtures/reply'
+require 'fixtures/joke'
+require 'fixtures/course'
+require 'fixtures/category'
+require 'fixtures/parrot'
+require 'fixtures/pirate'
+require 'fixtures/treasure'
+require 'fixtures/matey'
+require 'fixtures/ship'
+
+class FixturesTest < ActiveSupport::TestCase
+ self.use_instantiated_fixtures = true
+ self.use_transactional_fixtures = false
+
+ fixtures :topics, :developers, :accounts, :tasks, :categories, :funny_jokes, :binaries
+
+ FIXTURES = %w( accounts binaries companies customers
+ developers developers_projects entrants
+ movies projects subscribers topics tasks )
+ MATCH_ATTRIBUTE_NAME = /[a-zA-Z][-_\w]*/
+
+ BINARY_FIXTURE_PATH = File.dirname(__FILE__) + '/fixtures/flowers.jpg'
+
+ def test_clean_fixtures
+ FIXTURES.each do |name|
+ fixtures = nil
+ assert_nothing_raised { fixtures = create_fixtures(name) }
+ assert_kind_of(Fixtures, fixtures)
+ fixtures.each { |name, fixture|
+ fixture.each { |key, value|
+ assert_match(MATCH_ATTRIBUTE_NAME, key)
+ }
+ }
+ end
+ end
+
+ def test_multiple_clean_fixtures
+ fixtures_array = nil
+ assert_nothing_raised { fixtures_array = create_fixtures(*FIXTURES) }
+ assert_kind_of(Array, fixtures_array)
+ fixtures_array.each { |fixtures| assert_kind_of(Fixtures, fixtures) }
+ end
+
+ def test_attributes
+ topics = create_fixtures("topics")
+ assert_equal("The First Topic", topics["first"]["title"])
+ assert_nil(topics["second"]["author_email_address"])
+ end
+
+ def test_inserts
+ topics = create_fixtures("topics")
+ first_row = ActiveRecord::Base.connection.select_one("SELECT * FROM topics WHERE author_name = 'David'")
+ assert_equal("The First Topic", first_row["title"])
+
+ second_row = ActiveRecord::Base.connection.select_one("SELECT * FROM topics WHERE author_name = 'Mary'")
+ assert_nil(second_row["author_email_address"])
+ end
+
+ if ActiveRecord::Base.connection.supports_migrations?
+ def test_inserts_with_pre_and_suffix
+ # Reset cache to make finds on the new table work
+ Fixtures.reset_cache
+
+ ActiveRecord::Base.connection.create_table :prefix_topics_suffix do |t|
+ t.column :title, :string
+ t.column :author_name, :string
+ t.column :author_email_address, :string
+ t.column :written_on, :datetime
+ t.column :bonus_time, :time
+ t.column :last_read, :date
+ t.column :content, :string
+ t.column :approved, :boolean, :default => true
+ t.column :replies_count, :integer, :default => 0
+ t.column :parent_id, :integer
+ t.column :type, :string, :limit => 50
+ end
+
+ # Store existing prefix/suffix
+ old_prefix = ActiveRecord::Base.table_name_prefix
+ old_suffix = ActiveRecord::Base.table_name_suffix
+
+ # Set a prefix/suffix we can test against
+ ActiveRecord::Base.table_name_prefix = 'prefix_'
+ ActiveRecord::Base.table_name_suffix = '_suffix'
+
+ topics = create_fixtures("topics")
+
+ first_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_topics_suffix WHERE author_name = 'David'")
+ assert_equal("The First Topic", first_row["title"])
+
+ second_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_topics_suffix WHERE author_name = 'Mary'")
+ assert_nil(second_row["author_email_address"])
+ ensure
+ # Restore prefix/suffix to its previous values
+ ActiveRecord::Base.table_name_prefix = old_prefix
+ ActiveRecord::Base.table_name_suffix = old_suffix
+
+ ActiveRecord::Base.connection.drop_table :prefix_topics_suffix rescue nil
+ end
+ end
+
+ def test_insert_with_datetime
+ topics = create_fixtures("tasks")
+ first = Task.find(1)
+ assert first
+ end
+
+ def test_logger_level_invariant
+ level = ActiveRecord::Base.logger.level
+ create_fixtures('topics')
+ assert_equal level, ActiveRecord::Base.logger.level
+ end
+
+ def test_instantiation
+ topics = create_fixtures("topics")
+ assert_kind_of Topic, topics["first"].find
+ end
+
+ def test_complete_instantiation
+ assert_equal 2, @topics.size
+ assert_equal "The First Topic", @first.title
+ end
+
+ def test_fixtures_from_root_yml_with_instantiation
+ # assert_equal 2, @accounts.size
+ assert_equal 50, @unknown.credit_limit
+ end
+
+ def test_erb_in_fixtures
+ assert_equal 11, @developers.size
+ assert_equal "fixture_5", @dev_5.name
+ end
+
+ def test_empty_yaml_fixture
+ assert_not_nil Fixtures.new( Account.connection, "accounts", 'Account', File.dirname(__FILE__) + "/fixtures/naked/yml/accounts")
+ end
+
+ def test_empty_yaml_fixture_with_a_comment_in_it
+ assert_not_nil Fixtures.new( Account.connection, "companies", 'Company', File.dirname(__FILE__) + "/fixtures/naked/yml/companies")
+ end
+
+ def test_dirty_dirty_yaml_file
+ assert_raises(Fixture::FormatError) do
+ Fixtures.new( Account.connection, "courses", 'Course', File.dirname(__FILE__) + "/fixtures/naked/yml/courses")
+ end
+ end
+
+ def test_empty_csv_fixtures
+ assert_not_nil Fixtures.new( Account.connection, "accounts", 'Account', File.dirname(__FILE__) + "/fixtures/naked/csv/accounts")
+ end
+
+ def test_omap_fixtures
+ assert_nothing_raised do
+ fixtures = Fixtures.new(Account.connection, 'categories', 'Category', File.dirname(__FILE__) + '/fixtures/categories_ordered')
+
+ i = 0
+ fixtures.each do |name, fixture|
+ assert_equal "fixture_no_#{i}", name
+ assert_equal "Category #{i}", fixture['name']
+ i += 1
+ end
+ end
+ end
+
+ def test_yml_file_in_subdirectory
+ assert_equal(categories(:sub_special_1).name, "A special category in a subdir file")
+ assert_equal(categories(:sub_special_1).class, SpecialCategory)
+ end
+
+ def test_subsubdir_file_with_arbitrary_name
+ assert_equal(categories(:sub_special_3).name, "A special category in an arbitrarily named subsubdir file")
+ assert_equal(categories(:sub_special_3).class, SpecialCategory)
+ end
+
+ def test_binary_in_fixtures
+ assert_equal 1, @binaries.size
+ data = File.open(BINARY_FIXTURE_PATH, "rb").read.freeze
+ assert_equal data, @flowers.data
+ end
+end
+
+if Account.connection.respond_to?(:reset_pk_sequence!)
+ class FixturesResetPkSequenceTest < ActiveSupport::TestCase
+ fixtures :accounts
+ fixtures :companies
+
+ def setup
+ @instances = [Account.new(:credit_limit => 50), Company.new(:name => 'RoR Consulting')]
+ Fixtures.reset_cache # make sure tables get reinitialized
+ end
+
+ def test_resets_to_min_pk_with_specified_pk_and_sequence
+ @instances.each do |instance|
+ model = instance.class
+ model.delete_all
+ model.connection.reset_pk_sequence!(model.table_name, model.primary_key, model.sequence_name)
+
+ instance.save!
+ assert_equal 1, instance.id, "Sequence reset for #{model.table_name} failed."
+ end
+ end
+
+ def test_resets_to_min_pk_with_default_pk_and_sequence
+ @instances.each do |instance|
+ model = instance.class
+ model.delete_all
+ model.connection.reset_pk_sequence!(model.table_name)
+
+ instance.save!
+ assert_equal 1, instance.id, "Sequence reset for #{model.table_name} failed."
+ end
+ end
+
+ def test_create_fixtures_resets_sequences_when_not_cached
+ @instances.each do |instance|
+ max_id = create_fixtures(instance.class.table_name).inject(0) do |max_id, (name, fixture)|
+ fixture_id = fixture['id'].to_i
+ fixture_id > max_id ? fixture_id : max_id
+ end
+
+ # Clone the last fixture to check that it gets the next greatest id.
+ instance.save!
+ assert_equal max_id + 1, instance.id, "Sequence reset for #{instance.class.table_name} failed."
+ end
+ end
+ end
+end
+
+class FixturesWithoutInstantiationTest < ActiveSupport::TestCase
+ self.use_instantiated_fixtures = false
+ fixtures :topics, :developers, :accounts
+
+ def test_without_complete_instantiation
+ assert_nil @first
+ assert_nil @topics
+ assert_nil @developers
+ assert_nil @accounts
+ end
+
+ def test_fixtures_from_root_yml_without_instantiation
+ assert_nil @unknown
+ end
+
+ def test_accessor_methods
+ assert_equal "The First Topic", topics(:first).title
+ assert_equal "Jamis", developers(:jamis).name
+ assert_equal 50, accounts(:signals37).credit_limit
+ end
+
+ def test_accessor_methods_with_multiple_args
+ assert_equal 2, topics(:first, :second).size
+ assert_raise(StandardError) { topics([:first, :second]) }
+ end
+
+ uses_mocha 'reloading_fixtures_through_accessor_methods' do
+ def test_reloading_fixtures_through_accessor_methods
+ assert_equal "The First Topic", topics(:first).title
+ @loaded_fixtures['topics']['first'].expects(:find).returns(stub(:title => "Fresh Topic!"))
+ assert_equal "Fresh Topic!", topics(:first, true).title
+ end
+ end
+end
+
+class FixturesWithoutInstanceInstantiationTest < ActiveSupport::TestCase
+ self.use_instantiated_fixtures = true
+ self.use_instantiated_fixtures = :no_instances
+
+ fixtures :topics, :developers, :accounts
+
+ def test_without_instance_instantiation
+ assert_nil @first
+ assert_not_nil @topics
+ assert_not_nil @developers
+ assert_not_nil @accounts
+ end
+end
+
+class TransactionalFixturesTest < ActiveSupport::TestCase
+ self.use_instantiated_fixtures = true
+ self.use_transactional_fixtures = true
+
+ fixtures :topics
+
+ def test_destroy
+ assert_not_nil @first
+ @first.destroy
+ end
+
+ def test_destroy_just_kidding
+ assert_not_nil @first
+ end
+end
+
+class MultipleFixturesTest < ActiveSupport::TestCase
+ fixtures :topics
+ fixtures :developers, :accounts
+
+ def test_fixture_table_names
+ assert_equal %w(topics developers accounts), fixture_table_names
+ end
+end
+
+class SetupTest < ActiveSupport::TestCase
+ # fixtures :topics
+
+ def setup
+ @first = true
+ end
+
+ def test_nothing
+ end
+end
+
+class SetupSubclassTest < SetupTest
+ def setup
+ super
+ @second = true
+ end
+
+ def test_subclassing_should_preserve_setups
+ assert @first
+ assert @second
+ end
+end
+
+
+class OverlappingFixturesTest < ActiveSupport::TestCase
+ fixtures :topics, :developers
+ fixtures :developers, :accounts
+
+ def test_fixture_table_names
+ assert_equal %w(topics developers accounts), fixture_table_names
+ end
+end
+
+class ForeignKeyFixturesTest < ActiveSupport::TestCase
+ fixtures :fk_test_has_pk, :fk_test_has_fk
+
+ # if foreign keys are implemented and fixtures
+ # are not deleted in reverse order then this test
+ # case will raise StatementInvalid
+
+ def test_number1
+ assert true
+ end
+
+ def test_number2
+ assert true
+ end
+end
+
+class SetTableNameFixturesTest < ActiveSupport::TestCase
+ set_fixture_class :funny_jokes => 'Joke'
+ fixtures :funny_jokes
+
+ def test_table_method
+ assert_kind_of Joke, funny_jokes(:a_joke)
+ end
+end
+
+class CustomConnectionFixturesTest < ActiveSupport::TestCase
+ set_fixture_class :courses => Course
+ fixtures :courses
+
+ def test_connection
+ assert_kind_of Course, courses(:ruby)
+ assert_equal Course.connection, courses(:ruby).connection
+ end
+end
+
+class InvalidTableNameFixturesTest < ActiveSupport::TestCase
+ fixtures :funny_jokes
+
+ def test_raises_error
+ assert_raises FixtureClassNotFound do
+ funny_jokes(:a_joke)
+ end
+ end
+end
+
+class CheckEscapedYamlFixturesTest < ActiveSupport::TestCase
+ set_fixture_class :funny_jokes => 'Joke'
+ fixtures :funny_jokes
+
+ def test_proper_escaped_fixture
+ assert_equal "The \\n Aristocrats\nAte the candy\n", funny_jokes(:another_joke).name
+ end
+end
+
+class DevelopersProject; end
+class ManyToManyFixturesWithClassDefined < ActiveSupport::TestCase
+ fixtures :developers_projects
+
+ def test_this_should_run_cleanly
+ assert true
+ end
+end
+
+class FixturesBrokenRollbackTest < ActiveSupport::TestCase
+ def blank_setup; end
+ alias_method :ar_setup_fixtures, :setup_fixtures
+ alias_method :setup_fixtures, :blank_setup
+ alias_method :setup, :blank_setup
+
+ def blank_teardown; end
+ alias_method :ar_teardown_fixtures, :teardown_fixtures
+ alias_method :teardown_fixtures, :blank_teardown
+ alias_method :teardown, :blank_teardown
+
+ def test_no_rollback_in_teardown_unless_transaction_active
+ assert_equal 0, Thread.current['open_transactions']
+ assert_raise(RuntimeError) { ar_setup_fixtures }
+ assert_equal 0, Thread.current['open_transactions']
+ assert_nothing_raised { ar_teardown_fixtures }
+ assert_equal 0, Thread.current['open_transactions']
+ end
+
+ private
+ def load_fixtures
+ raise 'argh'
+ end
+end
+
+class LoadAllFixturesTest < ActiveSupport::TestCase
+ self.fixture_path= File.join(File.dirname(__FILE__), '/fixtures/all')
+ fixtures :all
+
+ def test_all_there
+ assert_equal %w(developers people tasks), fixture_table_names.sort
+ end
+end
+
+class FasterFixturesTest < ActiveSupport::TestCase
+ fixtures :categories, :authors
+
+ def load_extra_fixture(name)
+ fixture = create_fixtures(name)
+ assert fixture.is_a?(Fixtures)
+ @loaded_fixtures[fixture.table_name] = fixture
+ end
+
+ def test_cache
+ assert Fixtures.fixture_is_cached?(ActiveRecord::Base.connection, 'categories')
+ assert Fixtures.fixture_is_cached?(ActiveRecord::Base.connection, 'authors')
+
+ assert_no_queries do
+ create_fixtures('categories')
+ create_fixtures('authors')
+ end
+
+ load_extra_fixture('posts')
+ assert Fixtures.fixture_is_cached?(ActiveRecord::Base.connection, 'posts')
+ self.class.setup_fixture_accessors('posts')
+ assert_equal 'Welcome to the weblog', posts(:welcome).title
+ end
+end
+
+class FoxyFixturesTest < ActiveSupport::TestCase
+ fixtures :parrots, :parrots_pirates, :pirates, :treasures, :mateys, :ships, :computers, :developers
+
+ def test_identifies_strings
+ assert_equal(Fixtures.identify("foo"), Fixtures.identify("foo"))
+ assert_not_equal(Fixtures.identify("foo"), Fixtures.identify("FOO"))
+ end
+
+ def test_identifies_symbols
+ assert_equal(Fixtures.identify(:foo), Fixtures.identify(:foo))
+ end
+
+ TIMESTAMP_COLUMNS = %w(created_at created_on updated_at updated_on)
+
+ def test_populates_timestamp_columns
+ TIMESTAMP_COLUMNS.each do |property|
+ assert_not_nil(parrots(:george).send(property), "should set #{property}")
+ end
+ end
+
+ def test_does_not_populate_timestamp_columns_if_model_has_set_record_timestamps_to_false
+ TIMESTAMP_COLUMNS.each do |property|
+ assert_nil(ships(:black_pearl).send(property), "should not set #{property}")
+ end
+ end
+
+ def test_populates_all_columns_with_the_same_time
+ last = nil
+
+ TIMESTAMP_COLUMNS.each do |property|
+ current = parrots(:george).send(property)
+ last ||= current
+
+ assert_equal(last, current)
+ last = current
+ end
+ end
+
+ def test_only_populates_columns_that_exist
+ assert_not_nil(pirates(:blackbeard).created_on)
+ assert_not_nil(pirates(:blackbeard).updated_on)
+ end
+
+ def test_preserves_existing_fixture_data
+ assert_equal(2.weeks.ago.to_date, pirates(:redbeard).created_on.to_date)
+ assert_equal(2.weeks.ago.to_date, pirates(:redbeard).updated_on.to_date)
+ end
+
+ def test_generates_unique_ids
+ assert_not_nil(parrots(:george).id)
+ assert_not_equal(parrots(:george).id, parrots(:louis).id)
+ end
+
+ def test_automatically_sets_primary_key
+ assert_not_nil(ships(:black_pearl))
+ end
+
+ def test_preserves_existing_primary_key
+ assert_equal(2, ships(:interceptor).id)
+ end
+
+ def test_resolves_belongs_to_symbols
+ assert_equal(parrots(:george), pirates(:blackbeard).parrot)
+ end
+
+ def test_ignores_belongs_to_symbols_if_association_and_foreign_key_are_named_the_same
+ assert_equal(developers(:david), computers(:workstation).developer)
+ end
+
+ def test_supports_join_tables
+ assert(pirates(:blackbeard).parrots.include?(parrots(:george)))
+ assert(pirates(:blackbeard).parrots.include?(parrots(:louis)))
+ assert(parrots(:george).pirates.include?(pirates(:blackbeard)))
+ end
+
+ def test_supports_inline_habtm
+ assert(parrots(:george).treasures.include?(treasures(:diamond)))
+ assert(parrots(:george).treasures.include?(treasures(:sapphire)))
+ assert(!parrots(:george).treasures.include?(treasures(:ruby)))
+ end
+
+ def test_supports_inline_habtm_with_specified_id
+ assert(parrots(:polly).treasures.include?(treasures(:ruby)))
+ assert(parrots(:polly).treasures.include?(treasures(:sapphire)))
+ assert(!parrots(:polly).treasures.include?(treasures(:diamond)))
+ end
+
+ def test_supports_yaml_arrays
+ assert(parrots(:louis).treasures.include?(treasures(:diamond)))
+ assert(parrots(:louis).treasures.include?(treasures(:sapphire)))
+ end
+
+ def test_strips_DEFAULTS_key
+ assert_raise(StandardError) { parrots(:DEFAULTS) }
+
+ # this lets us do YAML defaults and not have an extra fixture entry
+ %w(sapphire ruby).each { |t| assert(parrots(:davey).treasures.include?(treasures(t))) }
+ end
+
+ def test_supports_label_interpolation
+ assert_equal("frederick", parrots(:frederick).name)
+ end
+
+ def test_supports_polymorphic_belongs_to
+ assert_equal(pirates(:redbeard), treasures(:sapphire).looter)
+ assert_equal(parrots(:louis), treasures(:ruby).looter)
+ end
+
+ def test_only_generates_a_pk_if_necessary
+ m = Matey.find(:first)
+ m.pirate = pirates(:blackbeard)
+ m.target = pirates(:redbeard)
+ end
+
+ def test_supports_sti
+ assert_kind_of DeadParrot, parrots(:polly)
+ assert_equal pirates(:blackbeard), parrots(:polly).killer
+ end
+end
+
+class ActiveSupportSubclassWithFixturesTest < ActiveSupport::TestCase
+ fixtures :parrots
+
+ # This seemingly useless assertion catches a bug that caused the fixtures
+ # setup code call nil[]
+ def test_foo
+ assert_equal parrots(:louis), Parrot.find_by_name("King Louis")
+ end
+end
diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb
new file mode 100755
index 0000000000..092503f279
--- /dev/null
+++ b/activerecord/test/cases/inheritance_test.rb
@@ -0,0 +1,211 @@
+require 'abstract_unit'
+require 'fixtures/company'
+require 'fixtures/project'
+require 'fixtures/subscriber'
+
+class InheritanceTest < ActiveSupport::TestCase
+ fixtures :companies, :projects, :subscribers, :accounts
+
+ def test_company_descends_from_active_record
+ assert_raise(NoMethodError) { ActiveRecord::Base.descends_from_active_record? }
+ assert AbstractCompany.descends_from_active_record?, 'AbstractCompany should descend from ActiveRecord::Base'
+ assert Company.descends_from_active_record?, 'Company should descend from ActiveRecord::Base'
+ assert !Class.new(Company).descends_from_active_record?, 'Company subclass should not descend from ActiveRecord::Base'
+ end
+
+ def test_a_bad_type_column
+ #SQLServer need to turn Identity Insert On before manually inserting into the Identity column
+ if current_adapter?(:SQLServerAdapter, :SybaseAdapter)
+ Company.connection.execute "SET IDENTITY_INSERT companies ON"
+ end
+ Company.connection.insert "INSERT INTO companies (id, #{QUOTED_TYPE}, name) VALUES(100, 'bad_class!', 'Not happening')"
+
+ #We then need to turn it back Off before continuing.
+ if current_adapter?(:SQLServerAdapter, :SybaseAdapter)
+ Company.connection.execute "SET IDENTITY_INSERT companies OFF"
+ end
+ assert_raises(ActiveRecord::SubclassNotFound) { Company.find(100) }
+ end
+
+ def test_inheritance_find
+ assert Company.find(1).kind_of?(Firm), "37signals should be a firm"
+ assert Firm.find(1).kind_of?(Firm), "37signals should be a firm"
+ assert Company.find(2).kind_of?(Client), "Summit should be a client"
+ assert Client.find(2).kind_of?(Client), "Summit should be a client"
+ end
+
+ def test_alt_inheritance_find
+ switch_to_alt_inheritance_column
+ test_inheritance_find
+ switch_to_default_inheritance_column
+ end
+
+ def test_inheritance_find_all
+ companies = Company.find(:all, :order => 'id')
+ assert companies[0].kind_of?(Firm), "37signals should be a firm"
+ assert companies[1].kind_of?(Client), "Summit should be a client"
+ end
+
+ def test_alt_inheritance_find_all
+ switch_to_alt_inheritance_column
+ test_inheritance_find_all
+ switch_to_default_inheritance_column
+ end
+
+ def test_inheritance_save
+ firm = Firm.new
+ firm.name = "Next Angle"
+ firm.save
+
+ next_angle = Company.find(firm.id)
+ assert next_angle.kind_of?(Firm), "Next Angle should be a firm"
+ end
+
+ def test_alt_inheritance_save
+ switch_to_alt_inheritance_column
+ test_inheritance_save
+ switch_to_default_inheritance_column
+ end
+
+ def test_inheritance_condition
+ assert_equal 9, Company.count
+ assert_equal 2, Firm.count
+ assert_equal 3, Client.count
+ end
+
+ def test_alt_inheritance_condition
+ switch_to_alt_inheritance_column
+ test_inheritance_condition
+ switch_to_default_inheritance_column
+ end
+
+ def test_finding_incorrect_type_data
+ assert_raises(ActiveRecord::RecordNotFound) { Firm.find(2) }
+ assert_nothing_raised { Firm.find(1) }
+ end
+
+ def test_alt_finding_incorrect_type_data
+ switch_to_alt_inheritance_column
+ test_finding_incorrect_type_data
+ switch_to_default_inheritance_column
+ end
+
+ def test_update_all_within_inheritance
+ Client.update_all "name = 'I am a client'"
+ assert_equal "I am a client", Client.find(:all).first.name
+ assert_equal "37signals", Firm.find(:all).first.name
+ end
+
+ def test_alt_update_all_within_inheritance
+ switch_to_alt_inheritance_column
+ test_update_all_within_inheritance
+ switch_to_default_inheritance_column
+ end
+
+ def test_destroy_all_within_inheritance
+ Client.destroy_all
+ assert_equal 0, Client.count
+ assert_equal 2, Firm.count
+ end
+
+ def test_alt_destroy_all_within_inheritance
+ switch_to_alt_inheritance_column
+ test_destroy_all_within_inheritance
+ switch_to_default_inheritance_column
+ end
+
+ def test_find_first_within_inheritance
+ assert_kind_of Firm, Company.find(:first, :conditions => "name = '37signals'")
+ assert_kind_of Firm, Firm.find(:first, :conditions => "name = '37signals'")
+ assert_nil Client.find(:first, :conditions => "name = '37signals'")
+ end
+
+ def test_alt_find_first_within_inheritance
+ switch_to_alt_inheritance_column
+ test_find_first_within_inheritance
+ switch_to_default_inheritance_column
+ end
+
+ def test_complex_inheritance
+ very_special_client = VerySpecialClient.create("name" => "veryspecial")
+ assert_equal very_special_client, VerySpecialClient.find(:first, :conditions => "name = 'veryspecial'")
+ assert_equal very_special_client, SpecialClient.find(:first, :conditions => "name = 'veryspecial'")
+ assert_equal very_special_client, Company.find(:first, :conditions => "name = 'veryspecial'")
+ assert_equal very_special_client, Client.find(:first, :conditions => "name = 'veryspecial'")
+ assert_equal 1, Client.find(:all, :conditions => "name = 'Summit'").size
+ assert_equal very_special_client, Client.find(very_special_client.id)
+ end
+
+ def test_alt_complex_inheritance
+ switch_to_alt_inheritance_column
+ test_complex_inheritance
+ switch_to_default_inheritance_column
+ end
+
+ def test_eager_load_belongs_to_something_inherited
+ account = Account.find(1, :include => :firm)
+ assert_not_nil account.instance_variable_get("@firm"), "nil proves eager load failed"
+ end
+
+ def test_alt_eager_loading
+ switch_to_alt_inheritance_column
+ test_eager_load_belongs_to_something_inherited
+ switch_to_default_inheritance_column
+ end
+
+ def test_inheritance_without_mapping
+ assert_kind_of SpecialSubscriber, SpecialSubscriber.find("webster132")
+ assert_nothing_raised { s = SpecialSubscriber.new("name" => "And breaaaaathe!"); s.id = 'roger'; s.save }
+ end
+
+ private
+ def switch_to_alt_inheritance_column
+ # we don't want misleading test results, so get rid of the values in the type column
+ Company.find(:all, :order => 'id').each do |c|
+ c['type'] = nil
+ c.save
+ end
+ [ Company, Firm, Client].each { |klass| klass.reset_column_information }
+ Company.set_inheritance_column('ruby_type')
+ end
+ def switch_to_default_inheritance_column
+ [ Company, Firm, Client].each { |klass| klass.reset_column_information }
+ Company.set_inheritance_column('type')
+ end
+end
+
+
+class InheritanceComputeTypeTest < ActiveSupport::TestCase
+ fixtures :companies
+
+ def setup
+ Dependencies.log_activity = true
+ end
+
+ def teardown
+ Dependencies.log_activity = false
+ self.class.const_remove :FirmOnTheFly rescue nil
+ Firm.const_remove :FirmOnTheFly rescue nil
+ end
+
+ def test_instantiation_doesnt_try_to_require_corresponding_file
+ foo = Firm.find(:first).clone
+ foo.ruby_type = foo.type = 'FirmOnTheFly'
+ foo.save!
+
+ # Should fail without FirmOnTheFly in the type condition.
+ assert_raise(ActiveRecord::RecordNotFound) { Firm.find(foo.id) }
+
+ # Nest FirmOnTheFly in the test case where Dependencies won't see it.
+ self.class.const_set :FirmOnTheFly, Class.new(Firm)
+ assert_raise(ActiveRecord::SubclassNotFound) { Firm.find(foo.id) }
+
+ # Nest FirmOnTheFly in Firm where Dependencies will see it.
+ # This is analogous to nesting models in a migration.
+ Firm.const_set :FirmOnTheFly, Class.new(Firm)
+
+ # And instantiate will find the existing constant rather than trying
+ # to require firm_on_the_fly.
+ assert_nothing_raised { assert_kind_of Firm::FirmOnTheFly, Firm.find(foo.id) }
+ end
+end
diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb
new file mode 100644
index 0000000000..49711f625b
--- /dev/null
+++ b/activerecord/test/cases/json_serialization_test.rb
@@ -0,0 +1,180 @@
+require 'abstract_unit'
+require 'fixtures/contact'
+require 'fixtures/post'
+require 'fixtures/author'
+require 'fixtures/tagging'
+require 'fixtures/tag'
+require 'fixtures/comment'
+
+class JsonSerializationTest < ActiveSupport::TestCase
+ def setup
+ @contact = Contact.new(
+ :name => 'Konata Izumi',
+ :age => 16,
+ :avatar => 'binarydata',
+ :created_at => Time.utc(2006, 8, 1),
+ :awesome => true,
+ :preferences => { :shows => 'anime' }
+ )
+ end
+
+ def test_should_encode_all_encodable_attributes
+ json = @contact.to_json
+
+ assert_match %r{"name": "Konata Izumi"}, json
+ assert_match %r{"age": 16}, json
+ assert json.include?(%("created_at": #{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
+ assert_match %r{"awesome": true}, json
+ assert_match %r{"preferences": \{"shows": "anime"\}}, json
+ end
+
+ def test_should_allow_attribute_filtering_with_only
+ json = @contact.to_json(:only => [:name, :age])
+
+ assert_match %r{"name": "Konata Izumi"}, json
+ assert_match %r{"age": 16}, json
+ assert_no_match %r{"awesome": true}, json
+ assert !json.include?(%("created_at": #{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
+ assert_no_match %r{"preferences": \{"shows": "anime"\}}, json
+ end
+
+ def test_should_allow_attribute_filtering_with_except
+ json = @contact.to_json(:except => [:name, :age])
+
+ assert_no_match %r{"name": "Konata Izumi"}, json
+ assert_no_match %r{"age": 16}, json
+ assert_match %r{"awesome": true}, json
+ assert json.include?(%("created_at": #{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
+ assert_match %r{"preferences": \{"shows": "anime"\}}, json
+ end
+
+ def test_methods_are_called_on_object
+ # Define methods on fixture.
+ def @contact.label; "Has cheezburger"; end
+ def @contact.favorite_quote; "Constraints are liberating"; end
+
+ # Single method.
+ assert_match %r{"label": "Has cheezburger"}, @contact.to_json(:only => :name, :methods => :label)
+
+ # Both methods.
+ methods_json = @contact.to_json(:only => :name, :methods => [:label, :favorite_quote])
+ assert_match %r{"label": "Has cheezburger"}, methods_json
+ assert_match %r{"favorite_quote": "Constraints are liberating"}, methods_json
+ end
+end
+
+class DatabaseConnectedJsonEncodingTest < ActiveSupport::TestCase
+ fixtures :authors, :posts, :comments, :tags, :taggings
+
+ def setup
+ @david = authors(:david)
+ @mary = authors(:mary)
+ end
+
+ def test_includes_uses_association_name
+ json = @david.to_json(:include => :posts)
+
+ assert_match %r{"posts": \[}, json
+
+ assert_match %r{"id": 1}, json
+ assert_match %r{"name": "David"}, json
+
+ assert_match %r{"author_id": 1}, json
+ assert_match %r{"title": "Welcome to the weblog"}, json
+ assert_match %r{"body": "Such a lovely day"}, json
+
+ assert_match %r{"title": "So I was thinking"}, json
+ assert_match %r{"body": "Like I hopefully always am"}, json
+ end
+
+ def test_includes_uses_association_name_and_applies_attribute_filters
+ json = @david.to_json(:include => { :posts => { :only => :title } })
+
+ assert_match %r{"name": "David"}, json
+ assert_match %r{"posts": \[}, json
+
+ assert_match %r{"title": "Welcome to the weblog"}, json
+ assert_no_match %r{"body": "Such a lovely day"}, json
+
+ assert_match %r{"title": "So I was thinking"}, json
+ assert_no_match %r{"body": "Like I hopefully always am"}, json
+ end
+
+ def test_includes_fetches_second_level_associations
+ json = @david.to_json(:include => { :posts => { :include => { :comments => { :only => :body } } } })
+
+ assert_match %r{"name": "David"}, json
+ assert_match %r{"posts": \[}, json
+
+ assert_match %r{"comments": \[}, json
+ assert_match %r{\{"body": "Thank you again for the welcome"\}}, json
+ assert_match %r{\{"body": "Don't think too hard"\}}, json
+ assert_no_match %r{"post_id": }, json
+ end
+
+ def test_includes_fetches_nth_level_associations
+ json = @david.to_json(
+ :include => {
+ :posts => {
+ :include => {
+ :taggings => {
+ :include => {
+ :tag => { :only => :name }
+ }
+ }
+ }
+ }
+ })
+
+ assert_match %r{"name": "David"}, json
+ assert_match %r{"posts": \[}, json
+
+ assert_match %r{"taggings": \[}, json
+ assert_match %r{"tag": \{"name": "General"\}}, json
+ end
+
+ def test_should_not_call_methods_on_associations_that_dont_respond
+ def @david.favorite_quote; "Constraints are liberating"; end
+ json = @david.to_json(:include => :posts, :methods => :favorite_quote)
+
+ assert !@david.posts.first.respond_to?(:favorite_quote)
+ assert_match %r{"favorite_quote": "Constraints are liberating"}, json
+ assert_equal %r{"favorite_quote": }.match(json).size, 1
+ end
+
+ def test_should_allow_only_option_for_list_of_authors
+ authors = [@david, @mary]
+
+ assert_equal %([{"name": "David"}, {"name": "Mary"}]), authors.to_json(:only => :name)
+ end
+
+ def test_should_allow_except_option_for_list_of_authors
+ authors = [@david, @mary]
+
+ assert_equal %([{"id": 1}, {"id": 2}]), authors.to_json(:except => [:name, :author_address_id])
+ end
+
+ def test_should_allow_includes_for_list_of_authors
+ authors = [@david, @mary]
+ json = authors.to_json(
+ :only => :name,
+ :include => {
+ :posts => { :only => :id }
+ }
+ )
+
+ ['"name": "David"', '"posts": [', '{"id": 1}', '{"id": 2}', '{"id": 4}',
+ '{"id": 5}', '{"id": 6}', '"name": "Mary"', '"posts": [{"id": 7}]'].each do |fragment|
+ assert json.include?(fragment), json
+ end
+ end
+
+ def test_should_allow_options_for_hash_of_authors
+ authors_hash = {
+ 1 => @david,
+ 2 => @mary
+ }
+
+ assert_equal %({1: {"name": "David"}}), authors_hash.to_json(:only => [1, :name])
+ end
+end
diff --git a/activerecord/test/cases/lifecycle_test.rb b/activerecord/test/cases/lifecycle_test.rb
new file mode 100755
index 0000000000..91c6b8cc43
--- /dev/null
+++ b/activerecord/test/cases/lifecycle_test.rb
@@ -0,0 +1,137 @@
+require 'abstract_unit'
+require 'fixtures/topic'
+require 'fixtures/developer'
+require 'fixtures/reply'
+
+class Topic; def after_find() end end
+class Developer; def after_find() end end
+class SpecialDeveloper < Developer; end
+
+class TopicManualObserver
+ include Singleton
+
+ attr_reader :action, :object, :callbacks
+
+ def initialize
+ Topic.add_observer(self)
+ @callbacks = []
+ end
+
+ def update(callback_method, object)
+ @callbacks << { "callback_method" => callback_method, "object" => object }
+ end
+
+ def has_been_notified?
+ !@callbacks.empty?
+ end
+end
+
+class TopicaObserver < ActiveRecord::Observer
+ observe :topic
+
+ attr_reader :topic
+
+ def after_find(topic)
+ @topic = topic
+ end
+end
+
+class TopicObserver < ActiveRecord::Observer
+ attr_reader :topic
+
+ def after_find(topic)
+ @topic = topic
+ end
+end
+
+class MultiObserver < ActiveRecord::Observer
+ attr_reader :record
+
+ def self.observed_class() [ Topic, Developer ] end
+
+ cattr_reader :last_inherited
+ @@last_inherited = nil
+
+ def observed_class_inherited_with_testing(subclass)
+ observed_class_inherited_without_testing(subclass)
+ @@last_inherited = subclass
+ end
+
+ alias_method_chain :observed_class_inherited, :testing
+
+ def after_find(record)
+ @record = record
+ end
+end
+
+class LifecycleTest < ActiveSupport::TestCase
+ fixtures :topics, :developers
+
+ def test_before_destroy
+ assert_equal 2, Topic.count
+ Topic.find(1).destroy
+ assert_equal 0, Topic.count
+ end
+
+ def test_after_save
+ ActiveRecord::Base.observers = :topic_manual_observer
+ ActiveRecord::Base.instantiate_observers
+
+ topic = Topic.find(1)
+ topic.title = "hello"
+ topic.save
+
+ assert TopicManualObserver.instance.has_been_notified?
+ assert_equal :after_save, TopicManualObserver.instance.callbacks.last["callback_method"]
+ end
+
+ def test_observer_update_on_save
+ ActiveRecord::Base.observers = TopicManualObserver
+ ActiveRecord::Base.instantiate_observers
+
+ topic = Topic.find(1)
+ assert TopicManualObserver.instance.has_been_notified?
+ assert_equal :after_find, TopicManualObserver.instance.callbacks.first["callback_method"]
+ end
+
+ def test_auto_observer
+ topic_observer = TopicaObserver.instance
+
+ topic = Topic.find(1)
+ assert_equal topic.title, topic_observer.topic.title
+ end
+
+ def test_inferred_auto_observer
+ topic_observer = TopicObserver.instance
+
+ topic = Topic.find(1)
+ assert_equal topic.title, topic_observer.topic.title
+ end
+
+ def test_observing_two_classes
+ multi_observer = MultiObserver.instance
+
+ topic = Topic.find(1)
+ assert_equal topic.title, multi_observer.record.title
+
+ developer = Developer.find(1)
+ assert_equal developer.name, multi_observer.record.name
+ end
+
+ def test_observing_subclasses
+ multi_observer = MultiObserver.instance
+
+ developer = SpecialDeveloper.find(1)
+ assert_equal developer.name, multi_observer.record.name
+
+ klass = Class.new(Developer)
+ assert_equal klass, multi_observer.last_inherited
+
+ developer = klass.find(1)
+ assert_equal developer.name, multi_observer.record.name
+ end
+
+ def test_invalid_observer
+ assert_raise(ArgumentError) { Topic.observers = Object.new; Topic.instantiate_observers }
+ end
+end
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
new file mode 100644
index 0000000000..da2b594c5d
--- /dev/null
+++ b/activerecord/test/cases/locking_test.rb
@@ -0,0 +1,282 @@
+require 'abstract_unit'
+require 'fixtures/person'
+require 'fixtures/reader'
+require 'fixtures/legacy_thing'
+
+class LockWithoutDefault < ActiveRecord::Base; end
+
+class LockWithCustomColumnWithoutDefault < ActiveRecord::Base
+ set_table_name :lock_without_defaults_cust
+ set_locking_column :custom_lock_version
+end
+
+class ReadonlyFirstNamePerson < Person
+ attr_readonly :first_name
+end
+
+class OptimisticLockingTest < ActiveSupport::TestCase
+ fixtures :people, :legacy_things
+
+ # need to disable transactional fixtures, because otherwise the sqlite3
+ # adapter (at least) chokes when we try and change the schema in the middle
+ # of a test (see test_increment_counter_*).
+ self.use_transactional_fixtures = false
+
+ def test_lock_existing
+ p1 = Person.find(1)
+ p2 = Person.find(1)
+ assert_equal 0, p1.lock_version
+ assert_equal 0, p2.lock_version
+
+ p1.save!
+ assert_equal 1, p1.lock_version
+ assert_equal 0, p2.lock_version
+
+ assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
+ end
+
+ def test_lock_repeating
+ p1 = Person.find(1)
+ p2 = Person.find(1)
+ assert_equal 0, p1.lock_version
+ assert_equal 0, p2.lock_version
+
+ p1.save!
+ assert_equal 1, p1.lock_version
+ assert_equal 0, p2.lock_version
+
+ assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
+ assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
+ end
+
+ def test_lock_new
+ p1 = Person.new(:first_name => 'anika')
+ assert_equal 0, p1.lock_version
+
+ p1.save!
+ p2 = Person.find(p1.id)
+ assert_equal 0, p1.lock_version
+ assert_equal 0, p2.lock_version
+
+ p1.save!
+ assert_equal 1, p1.lock_version
+ assert_equal 0, p2.lock_version
+
+ assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
+ end
+
+ def test_lock_new_with_nil
+ p1 = Person.new(:first_name => 'anika')
+ p1.save!
+ p1.lock_version = nil # simulate bad fixture or column with no default
+ p1.save!
+ assert_equal 1, p1.lock_version
+ end
+
+
+ def test_lock_column_name_existing
+ t1 = LegacyThing.find(1)
+ t2 = LegacyThing.find(1)
+ assert_equal 0, t1.version
+ assert_equal 0, t2.version
+
+ t1.save!
+ assert_equal 1, t1.version
+ assert_equal 0, t2.version
+
+ assert_raises(ActiveRecord::StaleObjectError) { t2.save! }
+ end
+
+ def test_lock_column_is_mass_assignable
+ p1 = Person.create(:first_name => 'bianca')
+ assert_equal 0, p1.lock_version
+ assert_equal p1.lock_version, Person.new(p1.attributes).lock_version
+
+ p1.save!
+ assert_equal 1, p1.lock_version
+ assert_equal p1.lock_version, Person.new(p1.attributes).lock_version
+ end
+
+ def test_lock_without_default_sets_version_to_zero
+ t1 = LockWithoutDefault.new
+ assert_equal 0, t1.lock_version
+ end
+
+ def test_lock_with_custom_column_without_default_sets_version_to_zero
+ t1 = LockWithCustomColumnWithoutDefault.new
+ assert_equal 0, t1.custom_lock_version
+ end
+
+ def test_readonly_attributes
+ assert_equal Set.new([ 'first_name' ]), ReadonlyFirstNamePerson.readonly_attributes
+
+ p = ReadonlyFirstNamePerson.create(:first_name => "unchangeable name")
+ p.reload
+ assert_equal "unchangeable name", p.first_name
+
+ p.update_attributes(:first_name => "changed name")
+ p.reload
+ assert_equal "unchangeable name", p.first_name
+ end
+
+ { :lock_version => Person, :custom_lock_version => LegacyThing }.each do |name, model|
+ define_method("test_increment_counter_updates_#{name}") do
+ counter_test model, 1 do |id|
+ model.increment_counter :test_count, id
+ end
+ end
+
+ define_method("test_decrement_counter_updates_#{name}") do
+ counter_test model, -1 do |id|
+ model.decrement_counter :test_count, id
+ end
+ end
+
+ define_method("test_update_counters_updates_#{name}") do
+ counter_test model, 1 do |id|
+ model.update_counters id, :test_count => 1
+ end
+ end
+ end
+
+ private
+
+ def add_counter_column_to(model)
+ model.connection.add_column model.table_name, :test_count, :integer, :null => false, :default => 0
+ model.reset_column_information
+ # OpenBase does not set a value to existing rows when adding a not null default column
+ model.update_all(:test_count => 0) if current_adapter?(:OpenBaseAdapter)
+ end
+
+ def remove_counter_column_from(model)
+ model.connection.remove_column model.table_name, :test_count
+ model.reset_column_information
+ end
+
+ def counter_test(model, expected_count)
+ add_counter_column_to(model)
+ object = model.find(:first)
+ assert_equal 0, object.test_count
+ assert_equal 0, object.send(model.locking_column)
+ yield object.id
+ object.reload
+ assert_equal expected_count, object.test_count
+ assert_equal 1, object.send(model.locking_column)
+ ensure
+ remove_counter_column_from(model)
+ end
+end
+
+
+# TODO: test against the generated SQL since testing locking behavior itself
+# is so cumbersome. Will deadlock Ruby threads if the underlying db.execute
+# blocks, so separate script called by Kernel#system is needed.
+# (See exec vs. async_exec in the PostgreSQL adapter.)
+
+# TODO: The SQL Server, Sybase, and OpenBase adapters currently have no support for pessimistic locking
+
+unless current_adapter?(:SQLServerAdapter, :SybaseAdapter, :OpenBaseAdapter)
+ class PessimisticLockingTest < ActiveSupport::TestCase
+ self.use_transactional_fixtures = false
+ fixtures :people, :readers
+
+ def setup
+ # Avoid introspection queries during tests.
+ Person.columns; Reader.columns
+
+ @allow_concurrency = ActiveRecord::Base.allow_concurrency
+ ActiveRecord::Base.allow_concurrency = true
+ end
+
+ def teardown
+ ActiveRecord::Base.allow_concurrency = @allow_concurrency
+ end
+
+ # Test typical find.
+ def test_sane_find_with_lock
+ assert_nothing_raised do
+ Person.transaction do
+ Person.find 1, :lock => true
+ end
+ end
+ end
+
+ # Test scoped lock.
+ def test_sane_find_with_scoped_lock
+ assert_nothing_raised do
+ Person.transaction do
+ Person.with_scope(:find => { :lock => true }) do
+ Person.find 1
+ end
+ end
+ end
+ end
+
+ # PostgreSQL protests SELECT ... FOR UPDATE on an outer join.
+ unless current_adapter?(:PostgreSQLAdapter)
+ # Test locked eager find.
+ def test_eager_find_with_lock
+ assert_nothing_raised do
+ Person.transaction do
+ Person.find 1, :include => :readers, :lock => true
+ end
+ end
+ end
+ end
+
+ # Locking a record reloads it.
+ def test_sane_lock_method
+ assert_nothing_raised do
+ Person.transaction do
+ person = Person.find 1
+ old, person.first_name = person.first_name, 'fooman'
+ person.lock!
+ assert_equal old, person.first_name
+ end
+ end
+ end
+
+ if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
+ def test_no_locks_no_wait
+ first, second = duel { Person.find 1 }
+ assert first.end > second.end
+ end
+
+ def test_second_lock_waits
+ assert [0.2, 1, 5].any? { |zzz|
+ first, second = duel(zzz) { Person.find 1, :lock => true }
+ second.end > first.end
+ }
+ end
+
+ protected
+ def duel(zzz = 5)
+ t0, t1, t2, t3 = nil, nil, nil, nil
+
+ a = Thread.new do
+ t0 = Time.now
+ Person.transaction do
+ yield
+ sleep zzz # block thread 2 for zzz seconds
+ end
+ t1 = Time.now
+ end
+
+ b = Thread.new do
+ sleep zzz / 2.0 # ensure thread 1 tx starts first
+ t2 = Time.now
+ Person.transaction { yield }
+ t3 = Time.now
+ end
+
+ a.join
+ b.join
+
+ assert t1 > t0 + zzz
+ assert t2 > t0
+ assert t3 > t2
+ [t0.to_f..t1.to_f, t2.to_f..t3.to_f]
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb
new file mode 100644
index 0000000000..00304ba5cf
--- /dev/null
+++ b/activerecord/test/cases/method_scoping_test.rb
@@ -0,0 +1,416 @@
+require 'abstract_unit'
+require 'fixtures/developer'
+require 'fixtures/project'
+require 'fixtures/comment'
+require 'fixtures/post'
+require 'fixtures/category'
+
+class MethodScopingTest < ActiveSupport::TestCase
+ fixtures :developers, :projects, :comments, :posts
+
+ def test_set_conditions
+ Developer.with_scope(:find => { :conditions => 'just a test...' }) do
+ assert_equal 'just a test...', Developer.send(:current_scoped_methods)[:find][:conditions]
+ end
+ end
+
+ def test_scoped_find
+ Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
+ assert_nothing_raised { Developer.find(1) }
+ end
+ end
+
+ def test_scoped_find_first
+ Developer.with_scope(:find => { :conditions => "salary = 100000" }) do
+ assert_equal Developer.find(10), Developer.find(:first, :order => 'name')
+ end
+ end
+
+ def test_scoped_find_combines_conditions
+ Developer.with_scope(:find => { :conditions => "salary = 9000" }) do
+ assert_equal developers(:poor_jamis), Developer.find(:first, :conditions => "name = 'Jamis'")
+ end
+ end
+
+ def test_scoped_find_sanitizes_conditions
+ Developer.with_scope(:find => { :conditions => ['salary = ?', 9000] }) do
+ assert_equal developers(:poor_jamis), Developer.find(:first)
+ end
+ end
+
+ def test_scoped_find_combines_and_sanitizes_conditions
+ Developer.with_scope(:find => { :conditions => ['salary = ?', 9000] }) do
+ assert_equal developers(:poor_jamis), Developer.find(:first, :conditions => ['name = ?', 'Jamis'])
+ end
+ end
+
+ def test_scoped_find_all
+ Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
+ assert_equal [developers(:david)], Developer.find(:all)
+ end
+ end
+
+ def test_scoped_count
+ Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
+ assert_equal 1, Developer.count
+ end
+
+ Developer.with_scope(:find => { :conditions => 'salary = 100000' }) do
+ assert_equal 8, Developer.count
+ assert_equal 1, Developer.count(:conditions => "name LIKE 'fixture_1%'")
+ end
+ end
+
+ def test_scoped_find_include
+ # with the include, will retrieve only developers for the given project
+ scoped_developers = Developer.with_scope(:find => { :include => :projects }) do
+ Developer.find(:all, :conditions => 'projects.id = 2')
+ end
+ assert scoped_developers.include?(developers(:david))
+ assert !scoped_developers.include?(developers(:jamis))
+ assert_equal 1, scoped_developers.size
+ end
+
+ def test_scoped_count_include
+ # with the include, will retrieve only developers for the given project
+ Developer.with_scope(:find => { :include => :projects }) do
+ assert_equal 1, Developer.count(:conditions => 'projects.id = 2')
+ end
+ end
+
+ def test_scoped_create
+ new_comment = nil
+
+ VerySpecialComment.with_scope(:create => { :post_id => 1 }) do
+ assert_equal({ :post_id => 1 }, VerySpecialComment.send(:current_scoped_methods)[:create])
+ new_comment = VerySpecialComment.create :body => "Wonderful world"
+ end
+
+ assert Post.find(1).comments.include?(new_comment)
+ end
+
+ def test_immutable_scope
+ options = { :conditions => "name = 'David'" }
+ Developer.with_scope(:find => options) do
+ assert_equal %w(David), Developer.find(:all).map { |d| d.name }
+ options[:conditions] = "name != 'David'"
+ assert_equal %w(David), Developer.find(:all).map { |d| d.name }
+ end
+
+ scope = { :find => { :conditions => "name = 'David'" }}
+ Developer.with_scope(scope) do
+ assert_equal %w(David), Developer.find(:all).map { |d| d.name }
+ scope[:find][:conditions] = "name != 'David'"
+ assert_equal %w(David), Developer.find(:all).map { |d| d.name }
+ end
+ end
+
+ def test_scoped_with_duck_typing
+ scoping = Struct.new(:method_scoping).new(:find => { :conditions => ["name = ?", 'David'] })
+ Developer.with_scope(scoping) do
+ assert_equal %w(David), Developer.find(:all).map { |d| d.name }
+ end
+ end
+
+ def test_ensure_that_method_scoping_is_correctly_restored
+ scoped_methods = Developer.instance_eval('current_scoped_methods')
+
+ begin
+ Developer.with_scope(:find => { :conditions => "name = 'Jamis'" }) do
+ raise "an exception"
+ end
+ rescue
+ end
+ assert_equal scoped_methods, Developer.instance_eval('current_scoped_methods')
+ end
+end
+
+class NestedScopingTest < ActiveSupport::TestCase
+ fixtures :developers, :projects, :comments, :posts
+
+ def test_merge_options
+ Developer.with_scope(:find => { :conditions => 'salary = 80000' }) do
+ Developer.with_scope(:find => { :limit => 10 }) do
+ merged_option = Developer.instance_eval('current_scoped_methods')[:find]
+ assert_equal({ :conditions => 'salary = 80000', :limit => 10 }, merged_option)
+ end
+ end
+ end
+
+ def test_replace_options
+ Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
+ Developer.with_exclusive_scope(:find => { :conditions => "name = 'Jamis'" }) do
+ assert_equal({:find => { :conditions => "name = 'Jamis'" }}, Developer.instance_eval('current_scoped_methods'))
+ assert_equal({:find => { :conditions => "name = 'Jamis'" }}, Developer.send(:scoped_methods)[-1])
+ end
+ end
+ end
+
+ def test_append_conditions
+ Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
+ Developer.with_scope(:find => { :conditions => 'salary = 80000' }) do
+ appended_condition = Developer.instance_eval('current_scoped_methods')[:find][:conditions]
+ assert_equal("( name = 'David' ) AND ( salary = 80000 )", appended_condition)
+ assert_equal(1, Developer.count)
+ end
+ Developer.with_scope(:find => { :conditions => "name = 'Maiha'" }) do
+ assert_equal(0, Developer.count)
+ end
+ end
+ end
+
+ def test_merge_and_append_options
+ Developer.with_scope(:find => { :conditions => 'salary = 80000', :limit => 10 }) do
+ Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
+ merged_option = Developer.instance_eval('current_scoped_methods')[:find]
+ assert_equal({ :conditions => "( salary = 80000 ) AND ( name = 'David' )", :limit => 10 }, merged_option)
+ end
+ end
+ end
+
+ def test_nested_scoped_find
+ Developer.with_scope(:find => { :conditions => "name = 'Jamis'" }) do
+ Developer.with_exclusive_scope(:find => { :conditions => "name = 'David'" }) do
+ assert_nothing_raised { Developer.find(1) }
+ assert_equal('David', Developer.find(:first).name)
+ end
+ assert_equal('Jamis', Developer.find(:first).name)
+ end
+ end
+
+ def test_nested_scoped_find_include
+ Developer.with_scope(:find => { :include => :projects }) do
+ Developer.with_scope(:find => { :conditions => "projects.id = 2" }) do
+ assert_nothing_raised { Developer.find(1) }
+ assert_equal('David', Developer.find(:first).name)
+ end
+ end
+ end
+
+ def test_nested_scoped_find_merged_include
+ # :include's remain unique and don't "double up" when merging
+ Developer.with_scope(:find => { :include => :projects, :conditions => "projects.id = 2" }) do
+ Developer.with_scope(:find => { :include => :projects }) do
+ assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:include].length
+ assert_equal('David', Developer.find(:first).name)
+ end
+ end
+
+ # the nested scope doesn't remove the first :include
+ Developer.with_scope(:find => { :include => :projects, :conditions => "projects.id = 2" }) do
+ Developer.with_scope(:find => { :include => [] }) do
+ assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:include].length
+ assert_equal('David', Developer.find(:first).name)
+ end
+ end
+
+ # mixing array and symbol include's will merge correctly
+ Developer.with_scope(:find => { :include => [:projects], :conditions => "projects.id = 2" }) do
+ Developer.with_scope(:find => { :include => :projects }) do
+ assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:include].length
+ assert_equal('David', Developer.find(:first).name)
+ end
+ end
+ end
+
+ def test_nested_scoped_find_replace_include
+ Developer.with_scope(:find => { :include => :projects }) do
+ Developer.with_exclusive_scope(:find => { :include => [] }) do
+ assert_equal 0, Developer.instance_eval('current_scoped_methods')[:find][:include].length
+ end
+ end
+ end
+
+ def test_three_level_nested_exclusive_scoped_find
+ Developer.with_scope(:find => { :conditions => "name = 'Jamis'" }) do
+ assert_equal('Jamis', Developer.find(:first).name)
+
+ Developer.with_exclusive_scope(:find => { :conditions => "name = 'David'" }) do
+ assert_equal('David', Developer.find(:first).name)
+
+ Developer.with_exclusive_scope(:find => { :conditions => "name = 'Maiha'" }) do
+ assert_equal(nil, Developer.find(:first))
+ end
+
+ # ensure that scoping is restored
+ assert_equal('David', Developer.find(:first).name)
+ end
+
+ # ensure that scoping is restored
+ assert_equal('Jamis', Developer.find(:first).name)
+ end
+ end
+
+ def test_merged_scoped_find
+ poor_jamis = developers(:poor_jamis)
+ Developer.with_scope(:find => { :conditions => "salary < 100000" }) do
+ Developer.with_scope(:find => { :offset => 1 }) do
+ assert_equal(poor_jamis, Developer.find(:first, :order => 'id asc'))
+ end
+ end
+ end
+
+ def test_merged_scoped_find_sanitizes_conditions
+ Developer.with_scope(:find => { :conditions => ["name = ?", 'David'] }) do
+ Developer.with_scope(:find => { :conditions => ['salary = ?', 9000] }) do
+ assert_raise(ActiveRecord::RecordNotFound) { developers(:poor_jamis) }
+ end
+ end
+ end
+
+ def test_nested_scoped_find_combines_and_sanitizes_conditions
+ Developer.with_scope(:find => { :conditions => ["name = ?", 'David'] }) do
+ Developer.with_exclusive_scope(:find => { :conditions => ['salary = ?', 9000] }) do
+ assert_equal developers(:poor_jamis), Developer.find(:first)
+ assert_equal developers(:poor_jamis), Developer.find(:first, :conditions => ['name = ?', 'Jamis'])
+ end
+ end
+ end
+
+ def test_merged_scoped_find_combines_and_sanitizes_conditions
+ Developer.with_scope(:find => { :conditions => ["name = ?", 'David'] }) do
+ Developer.with_scope(:find => { :conditions => ['salary > ?', 9000] }) do
+ assert_equal %w(David), Developer.find(:all).map { |d| d.name }
+ end
+ end
+ end
+
+ def test_immutable_nested_scope
+ options1 = { :conditions => "name = 'Jamis'" }
+ options2 = { :conditions => "name = 'David'" }
+ Developer.with_scope(:find => options1) do
+ Developer.with_exclusive_scope(:find => options2) do
+ assert_equal %w(David), Developer.find(:all).map { |d| d.name }
+ options1[:conditions] = options2[:conditions] = nil
+ assert_equal %w(David), Developer.find(:all).map { |d| d.name }
+ end
+ end
+ end
+
+ def test_immutable_merged_scope
+ options1 = { :conditions => "name = 'Jamis'" }
+ options2 = { :conditions => "salary > 10000" }
+ Developer.with_scope(:find => options1) do
+ Developer.with_scope(:find => options2) do
+ assert_equal %w(Jamis), Developer.find(:all).map { |d| d.name }
+ options1[:conditions] = options2[:conditions] = nil
+ assert_equal %w(Jamis), Developer.find(:all).map { |d| d.name }
+ end
+ end
+ end
+
+ def test_ensure_that_method_scoping_is_correctly_restored
+ Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
+ scoped_methods = Developer.instance_eval('current_scoped_methods')
+ begin
+ Developer.with_scope(:find => { :conditions => "name = 'Maiha'" }) do
+ raise "an exception"
+ end
+ rescue
+ end
+ assert_equal scoped_methods, Developer.instance_eval('current_scoped_methods')
+ end
+ end
+end
+
+class HasManyScopingTest< ActiveSupport::TestCase
+ fixtures :comments, :posts
+
+ def setup
+ @welcome = Post.find(1)
+ end
+
+ def test_forwarding_of_static_methods
+ assert_equal 'a comment...', Comment.what_are_you
+ assert_equal 'a comment...', @welcome.comments.what_are_you
+ end
+
+ def test_forwarding_to_scoped
+ assert_equal 4, Comment.search_by_type('Comment').size
+ assert_equal 2, @welcome.comments.search_by_type('Comment').size
+ end
+
+ def test_forwarding_to_dynamic_finders
+ assert_equal 4, Comment.find_all_by_type('Comment').size
+ assert_equal 2, @welcome.comments.find_all_by_type('Comment').size
+ end
+
+ def test_nested_scope
+ Comment.with_scope(:find => { :conditions => '1=1' }) do
+ assert_equal 'a comment...', @welcome.comments.what_are_you
+ end
+ end
+end
+
+
+class HasAndBelongsToManyScopingTest< ActiveSupport::TestCase
+ fixtures :posts, :categories, :categories_posts
+
+ def setup
+ @welcome = Post.find(1)
+ end
+
+ def test_forwarding_of_static_methods
+ assert_equal 'a category...', Category.what_are_you
+ assert_equal 'a category...', @welcome.categories.what_are_you
+ end
+
+ def test_forwarding_to_dynamic_finders
+ assert_equal 4, Category.find_all_by_type('SpecialCategory').size
+ assert_equal 0, @welcome.categories.find_all_by_type('SpecialCategory').size
+ assert_equal 2, @welcome.categories.find_all_by_type('Category').size
+ end
+
+ def test_nested_scope
+ Category.with_scope(:find => { :conditions => '1=1' }) do
+ assert_equal 'a comment...', @welcome.comments.what_are_you
+ end
+ end
+end
+
+
+=begin
+# We disabled the scoping for has_one and belongs_to as we can't think of a proper use case
+
+
+class BelongsToScopingTest< ActiveSupport::TestCase
+ fixtures :comments, :posts
+
+ def setup
+ @greetings = Comment.find(1)
+ end
+
+ def test_forwarding_of_static_method
+ assert_equal 'a post...', Post.what_are_you
+ assert_equal 'a post...', @greetings.post.what_are_you
+ end
+
+ def test_forwarding_to_dynamic_finders
+ assert_equal 4, Post.find_all_by_type('Post').size
+ assert_equal 1, @greetings.post.find_all_by_type('Post').size
+ end
+
+end
+
+
+class HasOneScopingTest< ActiveSupport::TestCase
+ fixtures :comments, :posts
+
+ def setup
+ @sti_comments = Post.find(4)
+ end
+
+ def test_forwarding_of_static_methods
+ assert_equal 'a comment...', Comment.what_are_you
+ assert_equal 'a very special comment...', @sti_comments.very_special_comment.what_are_you
+ end
+
+ def test_forwarding_to_dynamic_finders
+ assert_equal 1, Comment.find_all_by_type('VerySpecialComment').size
+ assert_equal 1, @sti_comments.very_special_comment.find_all_by_type('VerySpecialComment').size
+ assert_equal 0, @sti_comments.very_special_comment.find_all_by_type('Comment').size
+ end
+
+end
+
+=end
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
new file mode 100644
index 0000000000..c349f1cb1b
--- /dev/null
+++ b/activerecord/test/cases/migration_test.rb
@@ -0,0 +1,987 @@
+require 'abstract_unit'
+require 'bigdecimal/util'
+
+require 'fixtures/person'
+require 'fixtures/topic'
+require File.dirname(__FILE__) + '/fixtures/migrations/1_people_have_last_names'
+require File.dirname(__FILE__) + '/fixtures/migrations/2_we_need_reminders'
+require File.dirname(__FILE__) + '/fixtures/migrations_with_decimal/1_give_me_big_numbers'
+
+if ActiveRecord::Base.connection.supports_migrations?
+ class BigNumber < ActiveRecord::Base; end
+
+ class Reminder < ActiveRecord::Base; end
+
+ class ActiveRecord::Migration
+ class <<self
+ attr_accessor :message_count
+ def puts(text="")
+ self.message_count ||= 0
+ self.message_count += 1
+ end
+ end
+ end
+
+ class MigrationTest < ActiveSupport::TestCase
+ self.use_transactional_fixtures = false
+
+ fixtures :people
+
+ def setup
+ ActiveRecord::Migration.verbose = true
+ PeopleHaveLastNames.message_count = 0
+ end
+
+ def teardown
+ ActiveRecord::Base.connection.initialize_schema_information
+ ActiveRecord::Base.connection.update "UPDATE #{ActiveRecord::Migrator.schema_info_table_name} SET version = 0"
+
+ %w(reminders people_reminders prefix_reminders_suffix).each do |table|
+ Reminder.connection.drop_table(table) rescue nil
+ end
+ Reminder.reset_column_information
+
+ %w(last_name key bio age height wealth birthday favorite_day
+ moment_of_truth male administrator funny).each do |column|
+ Person.connection.remove_column('people', column) rescue nil
+ end
+ Person.connection.remove_column("people", "first_name") rescue nil
+ Person.connection.remove_column("people", "middle_name") rescue nil
+ Person.connection.add_column("people", "first_name", :string, :limit => 40)
+ Person.reset_column_information
+ end
+
+ def test_add_index
+ # Limit size of last_name and key columns to support Firebird index limitations
+ Person.connection.add_column "people", "last_name", :string, :limit => 100
+ Person.connection.add_column "people", "key", :string, :limit => 100
+ Person.connection.add_column "people", "administrator", :boolean
+
+ assert_nothing_raised { Person.connection.add_index("people", "last_name") }
+ assert_nothing_raised { Person.connection.remove_index("people", "last_name") }
+
+ # Orcl nds shrt indx nms. Sybs 2.
+ # OpenBase does not have named indexes. You must specify a single column name
+ unless current_adapter?(:OracleAdapter, :SybaseAdapter, :OpenBaseAdapter)
+ assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"]) }
+ assert_nothing_raised { Person.connection.remove_index("people", :column => ["last_name", "first_name"]) }
+ assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"]) }
+ assert_nothing_raised { Person.connection.remove_index("people", :name => "index_people_on_last_name_and_first_name") }
+ assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"]) }
+ assert_nothing_raised { Person.connection.remove_index("people", "last_name_and_first_name") }
+ assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"]) }
+ assert_nothing_raised { Person.connection.remove_index("people", ["last_name", "first_name"]) }
+ end
+
+ # quoting
+ # Note: changed index name from "key" to "key_idx" since "key" is a Firebird reserved word
+ # OpenBase does not have named indexes. You must specify a single column name
+ unless current_adapter?(:OpenBaseAdapter)
+ assert_nothing_raised { Person.connection.add_index("people", ["key"], :name => "key_idx", :unique => true) }
+ assert_nothing_raised { Person.connection.remove_index("people", :name => "key_idx", :unique => true) }
+ end
+
+ # Sybase adapter does not support indexes on :boolean columns
+ # OpenBase does not have named indexes. You must specify a single column
+ unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter)
+ assert_nothing_raised { Person.connection.add_index("people", %w(last_name first_name administrator), :name => "named_admin") }
+ assert_nothing_raised { Person.connection.remove_index("people", :name => "named_admin") }
+ end
+ end
+
+ def test_create_table_adds_id
+ Person.connection.create_table :testings do |t|
+ t.column :foo, :string
+ end
+
+ assert_equal %w(foo id),
+ Person.connection.columns(:testings).map { |c| c.name }.sort
+ ensure
+ Person.connection.drop_table :testings rescue nil
+ end
+
+ def test_create_table_with_not_null_column
+ assert_nothing_raised do
+ Person.connection.create_table :testings do |t|
+ t.column :foo, :string, :null => false
+ end
+ end
+
+ assert_raises(ActiveRecord::StatementInvalid) do
+ Person.connection.execute "insert into testings (foo) values (NULL)"
+ end
+ ensure
+ Person.connection.drop_table :testings rescue nil
+ end
+
+ def test_create_table_with_defaults
+ # MySQL doesn't allow defaults on TEXT or BLOB columns.
+ mysql = current_adapter?(:MysqlAdapter)
+
+ Person.connection.create_table :testings do |t|
+ t.column :one, :string, :default => "hello"
+ t.column :two, :boolean, :default => true
+ t.column :three, :boolean, :default => false
+ t.column :four, :integer, :default => 1
+ t.column :five, :text, :default => "hello" unless mysql
+ end
+
+ columns = Person.connection.columns(:testings)
+ one = columns.detect { |c| c.name == "one" }
+ two = columns.detect { |c| c.name == "two" }
+ three = columns.detect { |c| c.name == "three" }
+ four = columns.detect { |c| c.name == "four" }
+ five = columns.detect { |c| c.name == "five" } unless mysql
+
+ assert_equal "hello", one.default
+ assert_equal true, two.default
+ assert_equal false, three.default
+ assert_equal 1, four.default
+ assert_equal "hello", five.default unless mysql
+
+ ensure
+ Person.connection.drop_table :testings rescue nil
+ end
+
+ def test_create_table_with_limits
+ assert_nothing_raised do
+ Person.connection.create_table :testings do |t|
+ t.column :foo, :string, :limit => 255
+
+ t.column :default_int, :integer
+
+ t.column :one_int, :integer, :limit => 1
+ t.column :four_int, :integer, :limit => 4
+ t.column :eight_int, :integer, :limit => 8
+ end
+ end
+
+ columns = Person.connection.columns(:testings)
+ foo = columns.detect { |c| c.name == "foo" }
+ assert_equal 255, foo.limit
+
+ default = columns.detect { |c| c.name == "default_int" }
+ one = columns.detect { |c| c.name == "one_int" }
+ four = columns.detect { |c| c.name == "four_int" }
+ eight = columns.detect { |c| c.name == "eight_int" }
+
+ if current_adapter?(:PostgreSQLAdapter)
+ assert_equal 'integer', default.sql_type
+ assert_equal 'smallint', one.sql_type
+ assert_equal 'integer', four.sql_type
+ assert_equal 'bigint', eight.sql_type
+ elsif current_adapter?(:OracleAdapter)
+ assert_equal 'NUMBER(38)', default.sql_type
+ assert_equal 'NUMBER(1)', one.sql_type
+ assert_equal 'NUMBER(4)', four.sql_type
+ assert_equal 'NUMBER(8)', eight.sql_type
+ end
+ ensure
+ Person.connection.drop_table :testings rescue nil
+ end
+
+ # SQL Server, Sybase, and SQLite3 will not allow you to add a NOT NULL
+ # column to a table without a default value.
+ unless current_adapter?(:SQLServerAdapter, :SybaseAdapter, :SQLiteAdapter)
+ def test_add_column_not_null_without_default
+ Person.connection.create_table :testings do |t|
+ t.column :foo, :string
+ end
+ Person.connection.add_column :testings, :bar, :string, :null => false
+
+ assert_raises(ActiveRecord::StatementInvalid) do
+ Person.connection.execute "insert into testings (foo, bar) values ('hello', NULL)"
+ end
+ ensure
+ Person.connection.drop_table :testings rescue nil
+ end
+ end
+
+ def test_add_column_not_null_with_default
+ Person.connection.create_table :testings do |t|
+ t.column :foo, :string
+ end
+
+ con = Person.connection
+ Person.connection.enable_identity_insert("testings", true) if current_adapter?(:SybaseAdapter)
+ Person.connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}) values (1, 'hello')"
+ Person.connection.enable_identity_insert("testings", false) if current_adapter?(:SybaseAdapter)
+ assert_nothing_raised {Person.connection.add_column :testings, :bar, :string, :null => false, :default => "default" }
+
+ assert_raises(ActiveRecord::StatementInvalid) do
+ unless current_adapter?(:OpenBaseAdapter)
+ Person.connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) values (2, 'hello', NULL)"
+ else
+ Person.connection.insert("INSERT INTO testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) VALUES (2, 'hello', NULL)",
+ "Testing Insert","id",2)
+ end
+ end
+ ensure
+ Person.connection.drop_table :testings rescue nil
+ end
+
+ # We specifically do a manual INSERT here, and then test only the SELECT
+ # functionality. This allows us to more easily catch INSERT being broken,
+ # but SELECT actually working fine.
+ def test_native_decimal_insert_manual_vs_automatic
+ correct_value = '0012345678901234567890.0123456789'.to_d
+
+ Person.delete_all
+ Person.connection.add_column "people", "wealth", :decimal, :precision => '30', :scale => '10'
+ Person.reset_column_information
+
+ # Do a manual insertion
+ if current_adapter?(:OracleAdapter)
+ Person.connection.execute "insert into people (id, wealth) values (people_seq.nextval, 12345678901234567890.0123456789)"
+ elsif current_adapter?(:OpenBaseAdapter)
+ Person.connection.execute "insert into people (wealth) values ('12345678901234567890.0123456789')"
+ else
+ Person.connection.execute "insert into people (wealth) values (12345678901234567890.0123456789)"
+ end
+
+ # SELECT
+ row = Person.find(:first)
+ assert_kind_of BigDecimal, row.wealth
+
+ # If this assert fails, that means the SELECT is broken!
+ unless current_adapter?(:SQLite3Adapter)
+ assert_equal correct_value, row.wealth
+ end
+
+ # Reset to old state
+ Person.delete_all
+
+ # Now use the Rails insertion
+ assert_nothing_raised { Person.create :wealth => BigDecimal.new("12345678901234567890.0123456789") }
+
+ # SELECT
+ row = Person.find(:first)
+ assert_kind_of BigDecimal, row.wealth
+
+ # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken!
+ unless current_adapter?(:SQLite3Adapter)
+ assert_equal correct_value, row.wealth
+ end
+
+ # Reset to old state
+ Person.connection.del_column "people", "wealth" rescue nil
+ Person.reset_column_information
+ end
+
+ def test_add_column_with_precision_and_scale
+ Person.connection.add_column 'people', 'wealth', :decimal, :precision => 9, :scale => 7
+ Person.reset_column_information
+
+ wealth_column = Person.columns_hash['wealth']
+ assert_equal 9, wealth_column.precision
+ assert_equal 7, wealth_column.scale
+ end
+
+ def test_native_types
+ Person.delete_all
+ Person.connection.add_column "people", "last_name", :string
+ Person.connection.add_column "people", "bio", :text
+ Person.connection.add_column "people", "age", :integer
+ Person.connection.add_column "people", "height", :float
+ Person.connection.add_column "people", "wealth", :decimal, :precision => '30', :scale => '10'
+ Person.connection.add_column "people", "birthday", :datetime
+ Person.connection.add_column "people", "favorite_day", :date
+ Person.connection.add_column "people", "moment_of_truth", :datetime
+ Person.connection.add_column "people", "male", :boolean
+ Person.reset_column_information
+
+ assert_nothing_raised do
+ Person.create :first_name => 'bob', :last_name => 'bobsen',
+ :bio => "I was born ....", :age => 18, :height => 1.78,
+ :wealth => BigDecimal.new("12345678901234567890.0123456789"),
+ :birthday => 18.years.ago, :favorite_day => 10.days.ago,
+ :moment_of_truth => "1782-10-10 21:40:18", :male => true
+ end
+
+ bob = Person.find(:first)
+ assert_equal 'bob', bob.first_name
+ assert_equal 'bobsen', bob.last_name
+ assert_equal "I was born ....", bob.bio
+ assert_equal 18, bob.age
+
+ # Test for 30 significent digits (beyond the 16 of float), 10 of them
+ # after the decimal place.
+
+ unless current_adapter?(:SQLite3Adapter)
+ assert_equal BigDecimal.new("0012345678901234567890.0123456789"), bob.wealth
+ end
+
+ assert_equal true, bob.male?
+
+ assert_equal String, bob.first_name.class
+ assert_equal String, bob.last_name.class
+ assert_equal String, bob.bio.class
+ assert_equal Fixnum, bob.age.class
+ assert_equal Time, bob.birthday.class
+
+ if current_adapter?(:SQLServerAdapter, :OracleAdapter, :SybaseAdapter)
+ # Sybase, and Oracle don't differentiate between date/time
+ assert_equal Time, bob.favorite_day.class
+ else
+ assert_equal Date, bob.favorite_day.class
+ end
+
+ # Test DateTime column and defaults, including timezone.
+ # FIXME: moment of truth may be Time on 64-bit platforms.
+ if bob.moment_of_truth.is_a?(DateTime)
+ assert_equal DateTime.now.offset, bob.moment_of_truth.offset
+ assert_not_equal 0, bob.moment_of_truth.offset
+ assert_not_equal "Z", bob.moment_of_truth.zone
+ assert_equal DateTime::ITALY, bob.moment_of_truth.start
+ end
+
+ assert_equal TrueClass, bob.male?.class
+ assert_kind_of BigDecimal, bob.wealth
+ end
+
+ if current_adapter?(:MysqlAdapter)
+ def test_unabstracted_database_dependent_types
+ Person.delete_all
+
+ ActiveRecord::Migration.add_column :people, :intelligence_quotient, :tinyint
+ Person.reset_column_information
+ Person.create :intelligence_quotient => 300
+ jonnyg = Person.find(:first)
+ assert_equal 127, jonnyg.intelligence_quotient
+ jonnyg.destroy
+ ensure
+ ActiveRecord::Migration.remove_column :people, :intelligence_quotient rescue nil
+ end
+ end
+
+ def test_add_remove_single_field_using_string_arguments
+ assert !Person.column_methods_hash.include?(:last_name)
+
+ ActiveRecord::Migration.add_column 'people', 'last_name', :string
+
+ Person.reset_column_information
+ assert Person.column_methods_hash.include?(:last_name)
+
+ ActiveRecord::Migration.remove_column 'people', 'last_name'
+
+ Person.reset_column_information
+ assert !Person.column_methods_hash.include?(:last_name)
+ end
+
+ def test_add_remove_single_field_using_symbol_arguments
+ assert !Person.column_methods_hash.include?(:last_name)
+
+ ActiveRecord::Migration.add_column :people, :last_name, :string
+
+ Person.reset_column_information
+ assert Person.column_methods_hash.include?(:last_name)
+
+ ActiveRecord::Migration.remove_column :people, :last_name
+
+ Person.reset_column_information
+ assert !Person.column_methods_hash.include?(:last_name)
+ end
+
+ def test_add_rename
+ Person.delete_all
+
+ begin
+ Person.connection.add_column "people", "girlfriend", :string
+ Person.reset_column_information
+ Person.create :girlfriend => 'bobette'
+
+ Person.connection.rename_column "people", "girlfriend", "exgirlfriend"
+
+ Person.reset_column_information
+ bob = Person.find(:first)
+
+ assert_equal "bobette", bob.exgirlfriend
+ ensure
+ Person.connection.remove_column("people", "girlfriend") rescue nil
+ Person.connection.remove_column("people", "exgirlfriend") rescue nil
+ end
+
+ end
+
+ def test_rename_column_using_symbol_arguments
+ begin
+ names_before = Person.find(:all).map(&:first_name)
+ Person.connection.rename_column :people, :first_name, :nick_name
+ Person.reset_column_information
+ assert Person.column_names.include?("nick_name")
+ assert_equal names_before, Person.find(:all).map(&:nick_name)
+ ensure
+ Person.connection.remove_column("people","nick_name")
+ Person.connection.add_column("people","first_name", :string)
+ end
+ end
+
+ def test_rename_column
+ begin
+ names_before = Person.find(:all).map(&:first_name)
+ Person.connection.rename_column "people", "first_name", "nick_name"
+ Person.reset_column_information
+ assert Person.column_names.include?("nick_name")
+ assert_equal names_before, Person.find(:all).map(&:nick_name)
+ ensure
+ Person.connection.remove_column("people","nick_name")
+ Person.connection.add_column("people","first_name", :string)
+ end
+ end
+
+ def test_rename_column_with_sql_reserved_word
+ begin
+ assert_nothing_raised { Person.connection.rename_column "people", "first_name", "group" }
+ Person.reset_column_information
+ assert Person.column_names.include?("group")
+ ensure
+ Person.connection.remove_column("people", "group") rescue nil
+ Person.connection.add_column("people", "first_name", :string) rescue nil
+ end
+ end
+
+ def test_rename_column_with_an_index
+ ActiveRecord::Base.connection.create_table(:hats) do |table|
+ table.column :hat_name, :string, :limit => 100
+ table.column :hat_size, :integer
+ end
+ Person.connection.add_index :people, :first_name
+ assert_nothing_raised do
+ Person.connection.rename_column "hats", "hat_name", "name"
+ end
+ ensure
+ ActiveRecord::Base.connection.drop_table(:hats)
+ end
+
+ def test_remove_column_with_index
+ ActiveRecord::Base.connection.create_table(:hats) do |table|
+ table.column :hat_name, :string, :limit => 100
+ table.column :hat_size, :integer
+ end
+ ActiveRecord::Base.connection.add_index "hats", "hat_size"
+
+ assert_nothing_raised { Person.connection.remove_column("hats", "hat_size") }
+ ensure
+ ActiveRecord::Base.connection.drop_table(:hats)
+ end
+
+ def test_remove_column_with_multi_column_index
+ ActiveRecord::Base.connection.create_table(:hats) do |table|
+ table.column :hat_name, :string, :limit => 100
+ table.column :hat_size, :integer
+ table.column :hat_style, :string, :limit => 100
+ end
+ ActiveRecord::Base.connection.add_index "hats", ["hat_style", "hat_size"], :unique => true
+
+ assert_nothing_raised { Person.connection.remove_column("hats", "hat_size") }
+ ensure
+ ActiveRecord::Base.connection.drop_table(:hats)
+ end
+
+ def test_change_type_of_not_null_column
+ assert_nothing_raised do
+ Topic.connection.change_column "topics", "written_on", :datetime, :null => false
+ Topic.reset_column_information
+
+ Topic.connection.change_column "topics", "written_on", :datetime, :null => false
+ Topic.reset_column_information
+ end
+ end
+
+ def test_rename_table
+ begin
+ ActiveRecord::Base.connection.create_table :octopuses do |t|
+ t.column :url, :string
+ end
+ ActiveRecord::Base.connection.rename_table :octopuses, :octopi
+
+ # Using explicit id in insert for compatibility across all databases
+ con = ActiveRecord::Base.connection
+ con.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter)
+ assert_nothing_raised { con.execute "INSERT INTO octopi (#{con.quote_column_name('id')}, #{con.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" }
+ con.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter)
+
+ assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', ActiveRecord::Base.connection.select_value("SELECT url FROM octopi WHERE id=1")
+
+ ensure
+ ActiveRecord::Base.connection.drop_table :octopuses rescue nil
+ ActiveRecord::Base.connection.drop_table :octopi rescue nil
+ end
+ end
+
+ def test_change_column_nullability
+ Person.delete_all
+ Person.connection.add_column "people", "funny", :boolean
+ Person.reset_column_information
+ assert Person.columns_hash["funny"].null, "Column 'funny' must initially allow nulls"
+ Person.connection.change_column "people", "funny", :boolean, :null => false, :default => true
+ Person.reset_column_information
+ assert !Person.columns_hash["funny"].null, "Column 'funny' must *not* allow nulls at this point"
+ Person.connection.change_column "people", "funny", :boolean, :null => true
+ Person.reset_column_information
+ assert Person.columns_hash["funny"].null, "Column 'funny' must allow nulls again at this point"
+ end
+
+ def test_rename_table_with_an_index
+ begin
+ ActiveRecord::Base.connection.create_table :octopuses do |t|
+ t.column :url, :string
+ end
+ ActiveRecord::Base.connection.add_index :octopuses, :url
+
+ ActiveRecord::Base.connection.rename_table :octopuses, :octopi
+
+ # Using explicit id in insert for compatibility across all databases
+ con = ActiveRecord::Base.connection
+ con.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter)
+ assert_nothing_raised { con.execute "INSERT INTO octopi (#{con.quote_column_name('id')}, #{con.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" }
+ con.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter)
+
+ assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', ActiveRecord::Base.connection.select_value("SELECT url FROM octopi WHERE id=1")
+ assert ActiveRecord::Base.connection.indexes(:octopi).first.columns.include?("url")
+ ensure
+ ActiveRecord::Base.connection.drop_table :octopuses rescue nil
+ ActiveRecord::Base.connection.drop_table :octopi rescue nil
+ end
+ end
+
+ def test_change_column
+ Person.connection.add_column 'people', 'age', :integer
+ old_columns = Person.connection.columns(Person.table_name, "#{name} Columns")
+ assert old_columns.find { |c| c.name == 'age' and c.type == :integer }
+
+ assert_nothing_raised { Person.connection.change_column "people", "age", :string }
+
+ new_columns = Person.connection.columns(Person.table_name, "#{name} Columns")
+ assert_nil new_columns.find { |c| c.name == 'age' and c.type == :integer }
+ assert new_columns.find { |c| c.name == 'age' and c.type == :string }
+
+ old_columns = Topic.connection.columns(Topic.table_name, "#{name} Columns")
+ assert old_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == true }
+ assert_nothing_raised { Topic.connection.change_column :topics, :approved, :boolean, :default => false }
+ new_columns = Topic.connection.columns(Topic.table_name, "#{name} Columns")
+ assert_nil new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == true }
+ assert new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == false }
+ assert_nothing_raised { Topic.connection.change_column :topics, :approved, :boolean, :default => true }
+ end
+
+ def test_change_column_with_nil_default
+ Person.connection.add_column "people", "contributor", :boolean, :default => true
+ Person.reset_column_information
+ assert Person.new.contributor?
+
+ assert_nothing_raised { Person.connection.change_column "people", "contributor", :boolean, :default => nil }
+ Person.reset_column_information
+ assert !Person.new.contributor?
+ assert_nil Person.new.contributor
+ ensure
+ Person.connection.remove_column("people", "contributor") rescue nil
+ end
+
+ def test_change_column_with_new_default
+ Person.connection.add_column "people", "administrator", :boolean, :default => true
+ Person.reset_column_information
+ assert Person.new.administrator?
+
+ assert_nothing_raised { Person.connection.change_column "people", "administrator", :boolean, :default => false }
+ Person.reset_column_information
+ assert !Person.new.administrator?
+ ensure
+ Person.connection.remove_column("people", "administrator") rescue nil
+ end
+
+ def test_change_column_default
+ Person.connection.change_column_default "people", "first_name", "Tester"
+ Person.reset_column_information
+ assert_equal "Tester", Person.new.first_name
+ end
+
+ def test_change_column_quotes_column_names
+ Person.connection.create_table :testings do |t|
+ t.column :select, :string
+ end
+
+ assert_nothing_raised { Person.connection.change_column :testings, :select, :string, :limit => 10 }
+
+ assert_nothing_raised { Person.connection.execute "insert into testings (#{Person.connection.quote_column_name('select')}) values ('7 chars')" }
+ ensure
+ Person.connection.drop_table :testings rescue nil
+ end
+
+ def test_change_column_default_to_null
+ Person.connection.change_column_default "people", "first_name", nil
+ Person.reset_column_information
+ assert_nil Person.new.first_name
+ end
+
+ def test_add_table
+ assert !Reminder.table_exists?
+
+ WeNeedReminders.up
+
+ assert Reminder.create("content" => "hello world", "remind_at" => Time.now)
+ assert_equal "hello world", Reminder.find(:first).content
+
+ WeNeedReminders.down
+ assert_raises(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
+ end
+
+ def test_add_table_with_decimals
+ Person.connection.drop_table :big_numbers rescue nil
+
+ assert !BigNumber.table_exists?
+ GiveMeBigNumbers.up
+
+ assert BigNumber.create(
+ :bank_balance => 1586.43,
+ :big_bank_balance => BigDecimal("1000234000567.95"),
+ :world_population => 6000000000,
+ :my_house_population => 3,
+ :value_of_e => BigDecimal("2.7182818284590452353602875")
+ )
+
+ b = BigNumber.find(:first)
+ assert_not_nil b
+
+ assert_not_nil b.bank_balance
+ assert_not_nil b.big_bank_balance
+ assert_not_nil b.world_population
+ assert_not_nil b.my_house_population
+ assert_not_nil b.value_of_e
+
+ # TODO: set world_population >= 2**62 to cover 64-bit platforms and test
+ # is_a?(Bignum)
+ assert_kind_of Integer, b.world_population
+ assert_equal 6000000000, b.world_population
+ assert_kind_of Fixnum, b.my_house_population
+ assert_equal 3, b.my_house_population
+ assert_kind_of BigDecimal, b.bank_balance
+ assert_equal BigDecimal("1586.43"), b.bank_balance
+ assert_kind_of BigDecimal, b.big_bank_balance
+ assert_equal BigDecimal("1000234000567.95"), b.big_bank_balance
+
+ # This one is fun. The 'value_of_e' field is defined as 'DECIMAL' with
+ # precision/scale explicitly left out. By the SQL standard, numbers
+ # assigned to this field should be truncated but that's seldom respected.
+ if current_adapter?(:PostgreSQLAdapter, :SQLite2Adapter)
+ # - PostgreSQL changes the SQL spec on columns declared simply as
+ # "decimal" to something more useful: instead of being given a scale
+ # of 0, they take on the compile-time limit for precision and scale,
+ # so the following should succeed unless you have used really wacky
+ # compilation options
+ # - SQLite2 has the default behavior of preserving all data sent in,
+ # so this happens there too
+ assert_kind_of BigDecimal, b.value_of_e
+ assert_equal BigDecimal("2.7182818284590452353602875"), b.value_of_e
+ elsif current_adapter?(:SQLiteAdapter)
+ # - SQLite3 stores a float, in violation of SQL
+ assert_kind_of BigDecimal, b.value_of_e
+ assert_equal BigDecimal("2.71828182845905"), b.value_of_e
+ elsif current_adapter?(:SQLServer)
+ # - SQL Server rounds instead of truncating
+ assert_kind_of Fixnum, b.value_of_e
+ assert_equal 3, b.value_of_e
+ else
+ # - SQL standard is an integer
+ assert_kind_of Fixnum, b.value_of_e
+ assert_equal 2, b.value_of_e
+ end
+
+ GiveMeBigNumbers.down
+ assert_raises(ActiveRecord::StatementInvalid) { BigNumber.find(:first) }
+ end
+
+ def test_migrator
+ assert !Person.column_methods_hash.include?(:last_name)
+ assert !Reminder.table_exists?
+
+ ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/')
+
+ assert_equal 3, ActiveRecord::Migrator.current_version
+ Person.reset_column_information
+ assert Person.column_methods_hash.include?(:last_name)
+ assert Reminder.create("content" => "hello world", "remind_at" => Time.now)
+ assert_equal "hello world", Reminder.find(:first).content
+
+ ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/')
+
+ assert_equal 0, ActiveRecord::Migrator.current_version
+ Person.reset_column_information
+ assert !Person.column_methods_hash.include?(:last_name)
+ assert_raises(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
+ end
+
+ def test_migrator_one_up
+ assert !Person.column_methods_hash.include?(:last_name)
+ assert !Reminder.table_exists?
+
+ ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/', 1)
+
+ Person.reset_column_information
+ assert Person.column_methods_hash.include?(:last_name)
+ assert !Reminder.table_exists?
+
+ ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/', 2)
+
+ assert Reminder.create("content" => "hello world", "remind_at" => Time.now)
+ assert_equal "hello world", Reminder.find(:first).content
+ end
+
+ def test_migrator_one_down
+ ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/')
+
+ ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/', 1)
+
+ Person.reset_column_information
+ assert Person.column_methods_hash.include?(:last_name)
+ assert !Reminder.table_exists?
+ end
+
+ def test_migrator_one_up_one_down
+ ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/', 1)
+ ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/', 0)
+
+ assert !Person.column_methods_hash.include?(:last_name)
+ assert !Reminder.table_exists?
+ end
+
+ def test_migrator_verbosity
+ ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/', 1)
+ assert PeopleHaveLastNames.message_count > 0
+ PeopleHaveLastNames.message_count = 0
+
+ ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/', 0)
+ assert PeopleHaveLastNames.message_count > 0
+ PeopleHaveLastNames.message_count = 0
+ end
+
+ def test_migrator_verbosity_off
+ PeopleHaveLastNames.verbose = false
+ ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/', 1)
+ assert PeopleHaveLastNames.message_count.zero?
+ ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/', 0)
+ assert PeopleHaveLastNames.message_count.zero?
+ end
+
+ def test_migrator_going_down_due_to_version_target
+ ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/', 1)
+ ActiveRecord::Migrator.migrate(File.dirname(__FILE__) + '/fixtures/migrations/', 0)
+
+ assert !Person.column_methods_hash.include?(:last_name)
+ assert !Reminder.table_exists?
+
+ ActiveRecord::Migrator.migrate(File.dirname(__FILE__) + '/fixtures/migrations/')
+
+ Person.reset_column_information
+ assert Person.column_methods_hash.include?(:last_name)
+ assert Reminder.create("content" => "hello world", "remind_at" => Time.now)
+ assert_equal "hello world", Reminder.find(:first).content
+ end
+
+ def test_schema_info_table_name
+ ActiveRecord::Base.table_name_prefix = "prefix_"
+ ActiveRecord::Base.table_name_suffix = "_suffix"
+ Reminder.reset_table_name
+ assert_equal "prefix_schema_info_suffix", ActiveRecord::Migrator.schema_info_table_name
+ ActiveRecord::Base.table_name_prefix = ""
+ ActiveRecord::Base.table_name_suffix = ""
+ Reminder.reset_table_name
+ assert_equal "schema_info", ActiveRecord::Migrator.schema_info_table_name
+ ensure
+ ActiveRecord::Base.table_name_prefix = ""
+ ActiveRecord::Base.table_name_suffix = ""
+ end
+
+ def test_proper_table_name
+ assert_equal "table", ActiveRecord::Migrator.proper_table_name('table')
+ assert_equal "table", ActiveRecord::Migrator.proper_table_name(:table)
+ assert_equal "reminders", ActiveRecord::Migrator.proper_table_name(Reminder)
+ Reminder.reset_table_name
+ assert_equal Reminder.table_name, ActiveRecord::Migrator.proper_table_name(Reminder)
+
+ # Use the model's own prefix/suffix if a model is given
+ ActiveRecord::Base.table_name_prefix = "ARprefix_"
+ ActiveRecord::Base.table_name_suffix = "_ARsuffix"
+ Reminder.table_name_prefix = 'prefix_'
+ Reminder.table_name_suffix = '_suffix'
+ Reminder.reset_table_name
+ assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(Reminder)
+ Reminder.table_name_prefix = ''
+ Reminder.table_name_suffix = ''
+ Reminder.reset_table_name
+
+ # Use AR::Base's prefix/suffix if string or symbol is given
+ ActiveRecord::Base.table_name_prefix = "prefix_"
+ ActiveRecord::Base.table_name_suffix = "_suffix"
+ Reminder.reset_table_name
+ assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name('table')
+ assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name(:table)
+ ActiveRecord::Base.table_name_prefix = ""
+ ActiveRecord::Base.table_name_suffix = ""
+ Reminder.reset_table_name
+ end
+
+ def test_add_drop_table_with_prefix_and_suffix
+ assert !Reminder.table_exists?
+ ActiveRecord::Base.table_name_prefix = 'prefix_'
+ ActiveRecord::Base.table_name_suffix = '_suffix'
+ Reminder.reset_table_name
+ Reminder.reset_sequence_name
+ WeNeedReminders.up
+ assert Reminder.create("content" => "hello world", "remind_at" => Time.now)
+ assert_equal "hello world", Reminder.find(:first).content
+
+ WeNeedReminders.down
+ assert_raises(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
+ ensure
+ ActiveRecord::Base.table_name_prefix = ''
+ ActiveRecord::Base.table_name_suffix = ''
+ Reminder.reset_table_name
+ Reminder.reset_sequence_name
+ end
+
+ def test_create_table_with_binary_column
+ Person.connection.drop_table :binary_testings rescue nil
+
+ assert_nothing_raised {
+ Person.connection.create_table :binary_testings do |t|
+ t.column "data", :binary, :null => false
+ end
+ }
+
+ columns = Person.connection.columns(:binary_testings)
+ data_column = columns.detect { |c| c.name == "data" }
+
+ if current_adapter?(:MysqlAdapter)
+ assert_equal '', data_column.default
+ else
+ assert_nil data_column.default
+ end
+
+ Person.connection.drop_table :binary_testings rescue nil
+ end
+
+ def test_migrator_with_duplicates
+ assert_raises(ActiveRecord::DuplicateMigrationVersionError) do
+ ActiveRecord::Migrator.migrate(File.dirname(__FILE__) + '/fixtures/migrations_with_duplicate/', nil)
+ end
+ end
+
+ def test_migrator_with_missing_version_numbers
+ ActiveRecord::Migrator.migrate(File.dirname(__FILE__) + '/fixtures/migrations_with_missing_versions/', 500)
+ assert !Person.column_methods_hash.include?(:middle_name)
+ assert_equal 4, ActiveRecord::Migrator.current_version
+
+ ActiveRecord::Migrator.migrate(File.dirname(__FILE__) + '/fixtures/migrations_with_missing_versions/', 2)
+ Person.reset_column_information
+ assert !Reminder.table_exists?
+ assert Person.column_methods_hash.include?(:last_name)
+ assert_equal 2, ActiveRecord::Migrator.current_version
+ end
+
+ def test_create_table_with_custom_sequence_name
+ return unless current_adapter? :OracleAdapter
+
+ # table name is 29 chars, the standard sequence name will
+ # be 33 chars and fail
+ assert_raises(ActiveRecord::StatementInvalid) do
+ begin
+ Person.connection.create_table :table_with_name_thats_just_ok do |t|
+ t.column :foo, :string, :null => false
+ end
+ ensure
+ Person.connection.drop_table :table_with_name_thats_just_ok rescue nil
+ end
+ end
+
+ # should be all good w/ a custom sequence name
+ assert_nothing_raised do
+ begin
+ Person.connection.create_table :table_with_name_thats_just_ok,
+ :sequence_name => 'suitably_short_seq' do |t|
+ t.column :foo, :string, :null => false
+ end
+
+ Person.connection.execute("select suitably_short_seq.nextval from dual")
+
+ ensure
+ Person.connection.drop_table :table_with_name_thats_just_ok,
+ :sequence_name => 'suitably_short_seq' rescue nil
+ end
+ end
+
+ # confirm the custom sequence got dropped
+ assert_raises(ActiveRecord::StatementInvalid) do
+ Person.connection.execute("select suitably_short_seq.nextval from dual")
+ end
+ end
+ end
+
+ uses_mocha 'Sexy migration tests' do
+ class SexyMigrationsTest < ActiveSupport::TestCase
+ def test_references_column_type_adds_id
+ with_new_table do |t|
+ t.expects(:column).with('customer_id', :integer, {})
+ t.references :customer
+ end
+ end
+
+ def test_references_column_type_with_polymorphic_adds_type
+ with_new_table do |t|
+ t.expects(:column).with('taggable_type', :string, {})
+ t.expects(:column).with('taggable_id', :integer, {})
+ t.references :taggable, :polymorphic => true
+ end
+ end
+
+ def test_references_column_type_with_polymorphic_and_options_null_is_false_adds_table_flag
+ with_new_table do |t|
+ t.expects(:column).with('taggable_type', :string, {:null => false})
+ t.expects(:column).with('taggable_id', :integer, {:null => false})
+ t.references :taggable, :polymorphic => true, :null => false
+ end
+ end
+
+ def test_belongs_to_works_like_references
+ with_new_table do |t|
+ t.expects(:column).with('customer_id', :integer, {})
+ t.belongs_to :customer
+ end
+ end
+
+ def test_timestamps_creates_updated_at_and_created_at
+ with_new_table do |t|
+ t.expects(:column).with(:created_at, :datetime)
+ t.expects(:column).with(:updated_at, :datetime)
+ t.timestamps
+ end
+ end
+
+ def test_integer_creates_integer_column
+ with_new_table do |t|
+ t.expects(:column).with(:foo, 'integer', {})
+ t.expects(:column).with(:bar, 'integer', {})
+ t.integer :foo, :bar
+ end
+ end
+
+ def test_string_creates_string_column
+ with_new_table do |t|
+ t.expects(:column).with(:foo, 'string', {})
+ t.expects(:column).with(:bar, 'string', {})
+ t.string :foo, :bar
+ end
+ end
+
+ protected
+ def with_new_table
+ Person.connection.create_table :delete_me do |t|
+ yield t
+ end
+ ensure
+ Person.connection.drop_table :delete_me rescue nil
+ end
+
+ end # SexyMigrationsTest
+ end # uses_mocha
+end
diff --git a/activerecord/test/cases/migration_test_firebird.rb b/activerecord/test/cases/migration_test_firebird.rb
new file mode 100644
index 0000000000..5c7dbd7a35
--- /dev/null
+++ b/activerecord/test/cases/migration_test_firebird.rb
@@ -0,0 +1,124 @@
+require 'abstract_unit'
+require 'fixtures/course'
+
+class FirebirdMigrationTest < ActiveSupport::TestCase
+ self.use_transactional_fixtures = false
+
+ def setup
+ # using Course connection for tests -- need a db that doesn't already have a BOOLEAN domain
+ @connection = Course.connection
+ @fireruby_connection = @connection.instance_variable_get(:@connection)
+ end
+
+ def teardown
+ @connection.drop_table :foo rescue nil
+ @connection.execute("DROP DOMAIN D_BOOLEAN") rescue nil
+ end
+
+ def test_create_table_with_custom_sequence_name
+ assert_nothing_raised do
+ @connection.create_table(:foo, :sequence => 'foo_custom_seq') do |f|
+ f.column :bar, :string
+ end
+ end
+ assert !sequence_exists?('foo_seq')
+ assert sequence_exists?('foo_custom_seq')
+
+ assert_nothing_raised { @connection.drop_table(:foo, :sequence => 'foo_custom_seq') }
+ assert !sequence_exists?('foo_custom_seq')
+ ensure
+ FireRuby::Generator.new('foo_custom_seq', @fireruby_connection).drop rescue nil
+ end
+
+ def test_create_table_without_sequence
+ assert_nothing_raised do
+ @connection.create_table(:foo, :sequence => false) do |f|
+ f.column :bar, :string
+ end
+ end
+ assert !sequence_exists?('foo_seq')
+ assert_nothing_raised { @connection.drop_table :foo }
+
+ assert_nothing_raised do
+ @connection.create_table(:foo, :id => false) do |f|
+ f.column :bar, :string
+ end
+ end
+ assert !sequence_exists?('foo_seq')
+ assert_nothing_raised { @connection.drop_table :foo }
+ end
+
+ def test_create_table_with_boolean_column
+ assert !boolean_domain_exists?
+ assert_nothing_raised do
+ @connection.create_table :foo do |f|
+ f.column :bar, :string
+ f.column :baz, :boolean
+ end
+ end
+ assert boolean_domain_exists?
+ end
+
+ def test_add_boolean_column
+ assert !boolean_domain_exists?
+ @connection.create_table :foo do |f|
+ f.column :bar, :string
+ end
+
+ assert_nothing_raised { @connection.add_column :foo, :baz, :boolean }
+ assert boolean_domain_exists?
+ assert_equal :boolean, @connection.columns(:foo).find { |c| c.name == "baz" }.type
+ end
+
+ def test_change_column_to_boolean
+ assert !boolean_domain_exists?
+ # Manually create table with a SMALLINT column, which can be changed to a BOOLEAN
+ @connection.execute "CREATE TABLE foo (bar SMALLINT)"
+ assert_equal :integer, @connection.columns(:foo).find { |c| c.name == "bar" }.type
+
+ assert_nothing_raised { @connection.change_column :foo, :bar, :boolean }
+ assert boolean_domain_exists?
+ assert_equal :boolean, @connection.columns(:foo).find { |c| c.name == "bar" }.type
+ end
+
+ def test_rename_table_with_data_and_index
+ @connection.create_table :foo do |f|
+ f.column :baz, :string, :limit => 50
+ end
+ 100.times { |i| @connection.execute "INSERT INTO foo VALUES (GEN_ID(foo_seq, 1), 'record #{i+1}')" }
+ @connection.add_index :foo, :baz
+
+ assert_nothing_raised { @connection.rename_table :foo, :bar }
+ assert !@connection.tables.include?("foo")
+ assert @connection.tables.include?("bar")
+ assert_equal "index_bar_on_baz", @connection.indexes("bar").first.name
+ assert_equal 100, FireRuby::Generator.new("bar_seq", @fireruby_connection).last
+ assert_equal 100, @connection.select_one("SELECT COUNT(*) FROM bar")["count"]
+ ensure
+ @connection.drop_table :bar rescue nil
+ end
+
+ def test_renaming_table_with_fk_constraint_raises_error
+ @connection.create_table :parent do |p|
+ p.column :name, :string
+ end
+ @connection.create_table :child do |c|
+ c.column :parent_id, :integer
+ end
+ @connection.execute "ALTER TABLE child ADD CONSTRAINT fk_child_parent FOREIGN KEY(parent_id) REFERENCES parent(id)"
+ assert_raise(ActiveRecord::ActiveRecordError) { @connection.rename_table :child, :descendant }
+ ensure
+ @connection.drop_table :child rescue nil
+ @connection.drop_table :descendant rescue nil
+ @connection.drop_table :parent rescue nil
+ end
+
+ private
+ def boolean_domain_exists?
+ !@connection.select_one("SELECT 1 FROM rdb$fields WHERE rdb$field_name = 'D_BOOLEAN'").nil?
+ end
+
+ def sequence_exists?(sequence_name)
+ FireRuby::Generator.exists?(sequence_name, @fireruby_connection)
+ end
+end
diff --git a/activerecord/test/cases/mixin_test.rb b/activerecord/test/cases/mixin_test.rb
new file mode 100644
index 0000000000..0823ffddd6
--- /dev/null
+++ b/activerecord/test/cases/mixin_test.rb
@@ -0,0 +1,95 @@
+require 'abstract_unit'
+
+class Mixin < ActiveRecord::Base
+end
+
+# Let us control what Time.now returns for the TouchTest suite
+class Time
+ @@forced_now_time = nil
+ cattr_accessor :forced_now_time
+
+ class << self
+ def now_with_forcing
+ if @@forced_now_time
+ @@forced_now_time
+ else
+ now_without_forcing
+ end
+ end
+ alias_method_chain :now, :forcing
+ end
+end
+
+
+class TouchTest < ActiveSupport::TestCase
+ fixtures :mixins
+
+ def setup
+ Time.forced_now_time = Time.now
+ end
+
+ def teardown
+ Time.forced_now_time = nil
+ end
+
+ def test_time_mocking
+ five_minutes_ago = 5.minutes.ago
+ Time.forced_now_time = five_minutes_ago
+ assert_equal five_minutes_ago, Time.now
+
+ Time.forced_now_time = nil
+ assert_not_equal five_minutes_ago, Time.now
+ end
+
+ def test_update
+ stamped = Mixin.new
+
+ assert_nil stamped.updated_at
+ assert_nil stamped.created_at
+ stamped.save
+ assert_equal Time.now, stamped.updated_at
+ assert_equal Time.now, stamped.created_at
+ end
+
+ def test_create
+ obj = Mixin.create
+ assert_equal Time.now, obj.updated_at
+ assert_equal Time.now, obj.created_at
+ end
+
+ def test_many_updates
+ stamped = Mixin.new
+
+ assert_nil stamped.updated_at
+ assert_nil stamped.created_at
+ stamped.save
+ assert_equal Time.now, stamped.created_at
+ assert_equal Time.now, stamped.updated_at
+
+ old_updated_at = stamped.updated_at
+
+ Time.forced_now_time = 5.minutes.from_now
+ stamped.save
+
+ assert_equal Time.now, stamped.updated_at
+ assert_equal old_updated_at, stamped.created_at
+ end
+
+ def test_create_turned_off
+ Mixin.record_timestamps = false
+
+ mixin = Mixin.new
+
+ assert_nil mixin.updated_at
+ mixin.save
+ assert_nil mixin.updated_at
+
+ # Make sure Mixin.record_timestamps gets reset, even if this test fails,
+ # so that other tests do not fail because Mixin.record_timestamps == false
+ rescue Exception => e
+ raise e
+ ensure
+ Mixin.record_timestamps = true
+ end
+
+end
diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb
new file mode 100644
index 0000000000..cced0bb895
--- /dev/null
+++ b/activerecord/test/cases/modules_test.rb
@@ -0,0 +1,34 @@
+require 'abstract_unit'
+require 'fixtures/company_in_module'
+
+class ModulesTest < ActiveSupport::TestCase
+ fixtures :accounts, :companies, :projects, :developers
+
+ def test_module_spanning_associations
+ firm = MyApplication::Business::Firm.find(:first)
+ assert !firm.clients.empty?, "Firm should have clients"
+ assert_nil firm.class.table_name.match('::'), "Firm shouldn't have the module appear in its table name"
+ end
+
+ def test_module_spanning_has_and_belongs_to_many_associations
+ project = MyApplication::Business::Project.find(:first)
+ project.developers << MyApplication::Business::Developer.create("name" => "John")
+ assert "John", project.developers.last.name
+ end
+
+ def test_associations_spanning_cross_modules
+ account = MyApplication::Billing::Account.find(:first, :order => 'id')
+ assert_kind_of MyApplication::Business::Firm, account.firm
+ assert_kind_of MyApplication::Billing::Firm, account.qualified_billing_firm
+ assert_kind_of MyApplication::Billing::Firm, account.unqualified_billing_firm
+ assert_kind_of MyApplication::Billing::Nested::Firm, account.nested_qualified_billing_firm
+ assert_kind_of MyApplication::Billing::Nested::Firm, account.nested_unqualified_billing_firm
+ end
+
+ def test_find_account_and_include_company
+ account = MyApplication::Billing::Account.find(1, :include => :firm)
+ assert_kind_of MyApplication::Business::Firm, account.instance_variable_get('@firm')
+ assert_kind_of MyApplication::Business::Firm, account.firm
+ end
+
+end
diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb
new file mode 100644
index 0000000000..5d64a3efaa
--- /dev/null
+++ b/activerecord/test/cases/multiple_db_test.rb
@@ -0,0 +1,60 @@
+require 'abstract_unit'
+require 'fixtures/entrant'
+
+# So we can test whether Course.connection survives a reload.
+require_dependency 'fixtures/course'
+
+class MultipleDbTest < ActiveSupport::TestCase
+ self.use_transactional_fixtures = false
+
+ def setup
+ @courses = create_fixtures("courses") { Course.retrieve_connection }
+ @entrants = create_fixtures("entrants")
+ end
+
+ def test_connected
+ assert_not_nil Entrant.connection
+ assert_not_nil Course.connection
+ end
+
+ def test_proper_connection
+ assert_not_equal(Entrant.connection, Course.connection)
+ assert_equal(Entrant.connection, Entrant.retrieve_connection)
+ assert_equal(Course.connection, Course.retrieve_connection)
+ assert_equal(ActiveRecord::Base.connection, Entrant.connection)
+ end
+
+ def test_find
+ c1 = Course.find(1)
+ assert_equal "Ruby Development", c1.name
+ c2 = Course.find(2)
+ assert_equal "Java Development", c2.name
+ e1 = Entrant.find(1)
+ assert_equal "Ruby Developer", e1.name
+ e2 = Entrant.find(2)
+ assert_equal "Ruby Guru", e2.name
+ e3 = Entrant.find(3)
+ assert_equal "Java Lover", e3.name
+ end
+
+ def test_associations
+ c1 = Course.find(1)
+ assert_equal 2, c1.entrants.count
+ e1 = Entrant.find(1)
+ assert_equal e1.course.id, c1.id
+ c2 = Course.find(2)
+ assert_equal 1, c2.entrants.count
+ e3 = Entrant.find(3)
+ assert_equal e3.course.id, c2.id
+ end
+
+ def test_course_connection_should_survive_dependency_reload
+ assert Course.connection
+
+ Dependencies.clear
+ Object.send(:remove_const, :Course)
+ require_dependency 'fixtures/course'
+
+ assert Course.connection
+ end
+end
diff --git a/activerecord/test/cases/pk_test.rb b/activerecord/test/cases/pk_test.rb
new file mode 100644
index 0000000000..a9e9150f23
--- /dev/null
+++ b/activerecord/test/cases/pk_test.rb
@@ -0,0 +1,101 @@
+require "#{File.dirname(__FILE__)}/abstract_unit"
+require 'fixtures/topic'
+require 'fixtures/reply'
+require 'fixtures/subscriber'
+require 'fixtures/movie'
+require 'fixtures/keyboard'
+require 'fixtures/mixed_case_monkey'
+
+class PrimaryKeysTest < ActiveSupport::TestCase
+ fixtures :topics, :subscribers, :movies, :mixed_case_monkeys
+
+ def test_integer_key
+ topic = Topic.find(1)
+ assert_equal(topics(:first).author_name, topic.author_name)
+ topic = Topic.find(2)
+ assert_equal(topics(:second).author_name, topic.author_name)
+
+ topic = Topic.new
+ topic.title = "New Topic"
+ assert_equal(nil, topic.id)
+ assert_nothing_raised { topic.save! }
+ id = topic.id
+
+ topicReloaded = Topic.find(id)
+ assert_equal("New Topic", topicReloaded.title)
+ end
+
+ def test_customized_primary_key_auto_assigns_on_save
+ Keyboard.delete_all
+ keyboard = Keyboard.new(:name => 'HHKB')
+ assert_nothing_raised { keyboard.save! }
+ assert_equal keyboard.id, Keyboard.find_by_name('HHKB').id
+ end
+
+ def test_customized_primary_key_can_be_get_before_saving
+ keyboard = Keyboard.new
+ assert_nil keyboard.id
+ assert_nothing_raised { assert_nil keyboard.key_number }
+ end
+
+ def test_customized_string_primary_key_settable_before_save
+ subscriber = Subscriber.new
+ assert_nothing_raised { subscriber.id = 'webster123' }
+ assert_equal 'webster123', subscriber.id
+ assert_equal 'webster123', subscriber.nick
+ end
+
+ def test_string_key
+ subscriber = Subscriber.find(subscribers(:first).nick)
+ assert_equal(subscribers(:first).name, subscriber.name)
+ subscriber = Subscriber.find(subscribers(:second).nick)
+ assert_equal(subscribers(:second).name, subscriber.name)
+
+ subscriber = Subscriber.new
+ subscriber.id = "jdoe"
+ assert_equal("jdoe", subscriber.id)
+ subscriber.name = "John Doe"
+ assert_nothing_raised { subscriber.save! }
+ assert_equal("jdoe", subscriber.id)
+
+ subscriberReloaded = Subscriber.find("jdoe")
+ assert_equal("John Doe", subscriberReloaded.name)
+ end
+
+ def test_find_with_more_than_one_string_key
+ assert_equal 2, Subscriber.find(subscribers(:first).nick, subscribers(:second).nick).length
+ end
+
+ def test_primary_key_prefix
+ ActiveRecord::Base.primary_key_prefix_type = :table_name
+ Topic.reset_primary_key
+ assert_equal "topicid", Topic.primary_key
+
+ ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore
+ Topic.reset_primary_key
+ assert_equal "topic_id", Topic.primary_key
+
+ ActiveRecord::Base.primary_key_prefix_type = nil
+ Topic.reset_primary_key
+ assert_equal "id", Topic.primary_key
+ end
+
+ def test_delete_should_quote_pkey
+ assert_nothing_raised { MixedCaseMonkey.delete(1) }
+ end
+ def test_update_counters_should_quote_pkey_and_quote_counter_columns
+ assert_nothing_raised { MixedCaseMonkey.update_counters(1, :fleaCount => 99) }
+ end
+ def test_find_with_one_id_should_quote_pkey
+ assert_nothing_raised { MixedCaseMonkey.find(1) }
+ end
+ def test_find_with_multiple_ids_should_quote_pkey
+ assert_nothing_raised { MixedCaseMonkey.find([1,2]) }
+ end
+ def test_instance_update_should_quote_pkey
+ assert_nothing_raised { MixedCaseMonkey.find(1).save }
+ end
+ def test_instance_destroy_should_quote_pkey
+ assert_nothing_raised { MixedCaseMonkey.find(1).destroy }
+ end
+end
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
new file mode 100644
index 0000000000..242223161c
--- /dev/null
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -0,0 +1,124 @@
+require 'abstract_unit'
+require 'fixtures/topic'
+require 'fixtures/reply'
+require 'fixtures/task'
+require 'fixtures/course'
+require 'fixtures/category'
+require 'fixtures/post'
+
+
+class QueryCacheTest < ActiveSupport::TestCase
+ fixtures :tasks, :topics, :categories, :posts, :categories_posts
+
+ def test_find_queries
+ assert_queries(2) { Task.find(1); Task.find(1) }
+ end
+
+ def test_find_queries_with_cache
+ Task.cache do
+ assert_queries(1) { Task.find(1); Task.find(1) }
+ end
+ end
+
+ def test_count_queries_with_cache
+ Task.cache do
+ assert_queries(1) { Task.count; Task.count }
+ end
+ end
+
+ def test_query_cache_dups_results_correctly
+ Task.cache do
+ now = Time.now.utc
+ task = Task.find 1
+ assert_not_equal now, task.starting
+ task.starting = now
+ task.reload
+ assert_not_equal now, task.starting
+ end
+ end
+
+ def test_cache_is_flat
+ Task.cache do
+ Topic.columns # don't count this query
+ assert_queries(1) { Topic.find(1); Topic.find(1); }
+ end
+
+ ActiveRecord::Base.cache do
+ assert_queries(1) { Task.find(1); Task.find(1) }
+ end
+ end
+
+ def test_cache_does_not_wrap_string_results_in_arrays
+ Task.cache do
+ assert_instance_of String, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks")
+ end
+ end
+end
+
+uses_mocha 'QueryCacheExpiryTest' do
+
+class QueryCacheExpiryTest < ActiveSupport::TestCase
+ fixtures :tasks
+
+ def test_find
+ Task.connection.expects(:clear_query_cache).times(1)
+
+ assert !Task.connection.query_cache_enabled
+ Task.cache do
+ assert Task.connection.query_cache_enabled
+ Task.find(1)
+
+ Task.uncached do
+ assert !Task.connection.query_cache_enabled
+ Task.find(1)
+ end
+
+ assert Task.connection.query_cache_enabled
+ end
+ assert !Task.connection.query_cache_enabled
+ end
+
+ def test_update
+ Task.connection.expects(:clear_query_cache).times(2)
+
+ Task.cache do
+ Task.find(1).save!
+ end
+ end
+
+ def test_destroy
+ Task.connection.expects(:clear_query_cache).times(2)
+
+ Task.cache do
+ Task.find(1).destroy
+ end
+ end
+
+ def test_insert
+ ActiveRecord::Base.connection.expects(:clear_query_cache).times(2)
+
+ Task.cache do
+ Task.create!
+ end
+ end
+
+ def test_cache_is_expired_by_habtm_update
+ ActiveRecord::Base.connection.expects(:clear_query_cache).times(2)
+ ActiveRecord::Base.cache do
+ c = Category.find(:first)
+ p = Post.find(:first)
+ p.categories << c
+ end
+ end
+
+ def test_cache_is_expired_by_habtm_delete
+ ActiveRecord::Base.connection.expects(:clear_query_cache).times(2)
+ ActiveRecord::Base.cache do
+ c = Category.find(:first)
+ p = Post.find(:first)
+ p.categories.delete_all
+ end
+ end
+end
+
+end
diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb
new file mode 100755
index 0000000000..5674a4b442
--- /dev/null
+++ b/activerecord/test/cases/readonly_test.rb
@@ -0,0 +1,107 @@
+require 'abstract_unit'
+require 'fixtures/post'
+require 'fixtures/comment'
+require 'fixtures/developer'
+require 'fixtures/project'
+require 'fixtures/reader'
+require 'fixtures/person'
+
+# Dummy class methods to test implicit association scoping.
+def Comment.foo() find :first end
+def Project.foo() find :first end
+
+
+class ReadOnlyTest < ActiveSupport::TestCase
+ fixtures :posts, :comments, :developers, :projects, :developers_projects
+
+ def test_cant_save_readonly_record
+ dev = Developer.find(1)
+ assert !dev.readonly?
+
+ dev.readonly!
+ assert dev.readonly?
+
+ assert_nothing_raised do
+ dev.name = 'Luscious forbidden fruit.'
+ assert !dev.save
+ dev.name = 'Forbidden.'
+ end
+ assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save }
+ assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save! }
+ end
+
+
+ def test_find_with_readonly_option
+ Developer.find(:all).each { |d| assert !d.readonly? }
+ Developer.find(:all, :readonly => false).each { |d| assert !d.readonly? }
+ Developer.find(:all, :readonly => true).each { |d| assert d.readonly? }
+ end
+
+
+ def test_find_with_joins_option_implies_readonly
+ # Blank joins don't count.
+ Developer.find(:all, :joins => ' ').each { |d| assert !d.readonly? }
+ Developer.find(:all, :joins => ' ', :readonly => false).each { |d| assert !d.readonly? }
+
+ # Others do.
+ Developer.find(:all, :joins => ', projects').each { |d| assert d.readonly? }
+ Developer.find(:all, :joins => ', projects', :readonly => false).each { |d| assert !d.readonly? }
+ end
+
+
+ def test_habtm_find_readonly
+ dev = Developer.find(1)
+ assert !dev.projects.empty?
+ assert dev.projects.all?(&:readonly?)
+ assert dev.projects.find(:all).all?(&:readonly?)
+ assert dev.projects.find(:all, :readonly => true).all?(&:readonly?)
+ end
+
+ def test_has_many_find_readonly
+ post = Post.find(1)
+ assert !post.comments.empty?
+ assert !post.comments.any?(&:readonly?)
+ assert !post.comments.find(:all).any?(&:readonly?)
+ assert post.comments.find(:all, :readonly => true).all?(&:readonly?)
+ end
+
+ def test_has_many_with_through_is_not_implicitly_marked_readonly
+ assert people = Post.find(1).people
+ assert !people.any?(&:readonly?)
+ end
+
+ def test_readonly_scoping
+ Post.with_scope(:find => { :conditions => '1=1' }) do
+ assert !Post.find(1).readonly?
+ assert Post.find(1, :readonly => true).readonly?
+ assert !Post.find(1, :readonly => false).readonly?
+ end
+
+ Post.with_scope(:find => { :joins => ' ' }) do
+ assert !Post.find(1).readonly?
+ assert Post.find(1, :readonly => true).readonly?
+ assert !Post.find(1, :readonly => false).readonly?
+ end
+
+ # Oracle barfs on this because the join includes unqualified and
+ # conflicting column names
+ unless current_adapter?(:OracleAdapter)
+ Post.with_scope(:find => { :joins => ', developers' }) do
+ assert Post.find(1).readonly?
+ assert Post.find(1, :readonly => true).readonly?
+ assert !Post.find(1, :readonly => false).readonly?
+ end
+ end
+
+ Post.with_scope(:find => { :readonly => true }) do
+ assert Post.find(1).readonly?
+ assert Post.find(1, :readonly => true).readonly?
+ assert !Post.find(1, :readonly => false).readonly?
+ end
+ end
+
+ def test_association_collection_method_missing_scoping_not_readonly
+ assert !Developer.find(1).projects.foo.readonly?
+ assert !Post.find(1).comments.foo.readonly?
+ end
+end
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
new file mode 100644
index 0000000000..7790747cea
--- /dev/null
+++ b/activerecord/test/cases/reflection_test.rb
@@ -0,0 +1,175 @@
+require 'abstract_unit'
+require 'fixtures/topic'
+require 'fixtures/customer'
+require 'fixtures/company'
+require 'fixtures/company_in_module'
+require 'fixtures/subscriber'
+
+class ReflectionTest < ActiveSupport::TestCase
+ fixtures :topics, :customers, :companies, :subscribers
+
+ def setup
+ @first = Topic.find(1)
+ end
+
+ def test_column_null_not_null
+ subscriber = Subscriber.find(:first)
+ assert subscriber.column_for_attribute("name").null
+ assert !subscriber.column_for_attribute("nick").null
+ end
+
+ def test_read_attribute_names
+ assert_equal(
+ %w( id title author_name author_email_address bonus_time written_on last_read content approved replies_count parent_id type ).sort,
+ @first.attribute_names
+ )
+ end
+
+ def test_columns
+ assert_equal 12, Topic.columns.length
+ end
+
+ def test_columns_are_returned_in_the_order_they_were_declared
+ column_names = Topic.columns.map { |column| column.name }
+ assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content approved replies_count parent_id type), column_names
+ end
+
+ def test_content_columns
+ content_columns = Topic.content_columns
+ content_column_names = content_columns.map {|column| column.name}
+ assert_equal 8, content_columns.length
+ assert_equal %w(title author_name author_email_address written_on bonus_time last_read content approved).sort, content_column_names.sort
+ end
+
+ def test_column_string_type_and_limit
+ assert_equal :string, @first.column_for_attribute("title").type
+ assert_equal 255, @first.column_for_attribute("title").limit
+ end
+
+ def test_column_null_not_null
+ subscriber = Subscriber.find(:first)
+ assert subscriber.column_for_attribute("name").null
+ assert !subscriber.column_for_attribute("nick").null
+ end
+
+ def test_human_name_for_column
+ assert_equal "Author name", @first.column_for_attribute("author_name").human_name
+ end
+
+ def test_integer_columns
+ assert_equal :integer, @first.column_for_attribute("id").type
+ end
+
+ def test_reflection_klass_for_nested_class_name
+ reflection = ActiveRecord::Reflection::MacroReflection.new(nil, nil, { :class_name => 'MyApplication::Business::Company' }, nil)
+ assert_nothing_raised do
+ assert_equal MyApplication::Business::Company, reflection.klass
+ end
+ end
+
+ def test_aggregation_reflection
+ reflection_for_address = ActiveRecord::Reflection::AggregateReflection.new(
+ :composed_of, :address, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer
+ )
+
+ reflection_for_balance = ActiveRecord::Reflection::AggregateReflection.new(
+ :composed_of, :balance, { :class_name => "Money", :mapping => %w(balance amount) }, Customer
+ )
+
+ reflection_for_gps_location = ActiveRecord::Reflection::AggregateReflection.new(
+ :composed_of, :gps_location, { }, Customer
+ )
+
+ assert Customer.reflect_on_all_aggregations.include?(reflection_for_gps_location)
+ assert Customer.reflect_on_all_aggregations.include?(reflection_for_balance)
+ assert Customer.reflect_on_all_aggregations.include?(reflection_for_address)
+
+ assert_equal reflection_for_address, Customer.reflect_on_aggregation(:address)
+
+ assert_equal Address, Customer.reflect_on_aggregation(:address).klass
+
+ assert_equal Money, Customer.reflect_on_aggregation(:balance).klass
+ end
+
+ def test_has_many_reflection
+ reflection_for_clients = ActiveRecord::Reflection::AssociationReflection.new(:has_many, :clients, { :order => "id", :dependent => :destroy }, Firm)
+
+ assert_equal reflection_for_clients, Firm.reflect_on_association(:clients)
+
+ assert_equal Client, Firm.reflect_on_association(:clients).klass
+ assert_equal 'companies', Firm.reflect_on_association(:clients).table_name
+
+ assert_equal Client, Firm.reflect_on_association(:clients_of_firm).klass
+ assert_equal 'companies', Firm.reflect_on_association(:clients_of_firm).table_name
+ end
+
+ def test_has_one_reflection
+ reflection_for_account = ActiveRecord::Reflection::AssociationReflection.new(:has_one, :account, { :foreign_key => "firm_id", :dependent => :destroy }, Firm)
+ assert_equal reflection_for_account, Firm.reflect_on_association(:account)
+
+ assert_equal Account, Firm.reflect_on_association(:account).klass
+ assert_equal 'accounts', Firm.reflect_on_association(:account).table_name
+ end
+
+ def test_belongs_to_inferred_foreign_key_from_assoc_name
+ Company.belongs_to :foo
+ assert_equal "foo_id", Company.reflect_on_association(:foo).primary_key_name
+ Company.belongs_to :bar, :class_name => "Xyzzy"
+ assert_equal "bar_id", Company.reflect_on_association(:bar).primary_key_name
+ Company.belongs_to :baz, :class_name => "Xyzzy", :foreign_key => "xyzzy_id"
+ assert_equal "xyzzy_id", Company.reflect_on_association(:baz).primary_key_name
+ end
+
+ def test_association_reflection_in_modules
+ assert_reflection MyApplication::Business::Firm,
+ :clients_of_firm,
+ :klass => MyApplication::Business::Client,
+ :class_name => 'Client',
+ :table_name => 'companies'
+
+ assert_reflection MyApplication::Billing::Account,
+ :firm,
+ :klass => MyApplication::Business::Firm,
+ :class_name => 'MyApplication::Business::Firm',
+ :table_name => 'companies'
+
+ assert_reflection MyApplication::Billing::Account,
+ :qualified_billing_firm,
+ :klass => MyApplication::Billing::Firm,
+ :class_name => 'MyApplication::Billing::Firm',
+ :table_name => 'companies'
+
+ assert_reflection MyApplication::Billing::Account,
+ :unqualified_billing_firm,
+ :klass => MyApplication::Billing::Firm,
+ :class_name => 'Firm',
+ :table_name => 'companies'
+
+ assert_reflection MyApplication::Billing::Account,
+ :nested_qualified_billing_firm,
+ :klass => MyApplication::Billing::Nested::Firm,
+ :class_name => 'MyApplication::Billing::Nested::Firm',
+ :table_name => 'companies'
+
+ assert_reflection MyApplication::Billing::Account,
+ :nested_unqualified_billing_firm,
+ :klass => MyApplication::Billing::Nested::Firm,
+ :class_name => 'Nested::Firm',
+ :table_name => 'companies'
+ end
+
+ def test_reflection_of_all_associations
+ assert_equal 17, Firm.reflect_on_all_associations.size
+ assert_equal 15, Firm.reflect_on_all_associations(:has_many).size
+ assert_equal 2, Firm.reflect_on_all_associations(:has_one).size
+ assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size
+ end
+
+ private
+ def assert_reflection(klass, association, options)
+ assert reflection = klass.reflect_on_association(association)
+ options.each do |method, value|
+ assert_equal(value, reflection.send(method))
+ end
+ end
+end
diff --git a/activerecord/test/cases/reserved_word_test_mysql.rb b/activerecord/test/cases/reserved_word_test_mysql.rb
new file mode 100644
index 0000000000..0600c9460e
--- /dev/null
+++ b/activerecord/test/cases/reserved_word_test_mysql.rb
@@ -0,0 +1,177 @@
+require "#{File.dirname(__FILE__)}/abstract_unit"
+
+class Group < ActiveRecord::Base
+ Group.table_name = 'group'
+ belongs_to :select, :class_name => 'Select'
+ has_one :values
+end
+
+class Select < ActiveRecord::Base
+ Select.table_name = 'select'
+ has_many :groups
+end
+
+class Values < ActiveRecord::Base
+ Values.table_name = 'values'
+end
+
+class Distinct < ActiveRecord::Base
+ Distinct.table_name = 'distinct'
+ has_and_belongs_to_many :selects
+ has_many :values, :through => :groups
+end
+
+# a suite of tests to ensure the ConnectionAdapters#MysqlAdapter can handle tables with
+# reserved word names (ie: group, order, values, etc...)
+class MysqlReservedWordTest < ActiveSupport::TestCase
+ def setup
+ @connection = ActiveRecord::Base.connection
+
+ # we call execute directly here (and do similar below) because ActiveRecord::Base#create_table()
+ # will fail with these table names if these test cases fail
+
+ create_tables_directly 'group'=>'id int auto_increment primary key, `order` varchar(255), select_id int',
+ 'select'=>'id int auto_increment primary key',
+ 'values'=>'id int auto_increment primary key, group_id int',
+ 'distinct'=>'id int auto_increment primary key',
+ 'distincts_selects'=>'distinct_id int, select_id int'
+ end
+
+ def teardown
+ drop_tables_directly ['group', 'select', 'values', 'distinct', 'distincts_selects', 'order']
+ end
+
+ # create tables with reserved-word names and columns
+ def test_create_tables
+ assert_nothing_raised {
+ @connection.create_table :order do |t|
+ t.column :group, :string
+ end
+ }
+ end
+
+ # rename tables with reserved-word names
+ def test_rename_tables
+ assert_nothing_raised { @connection.rename_table(:group, :order) }
+ end
+
+ # alter column with a reserved-word name in a table with a reserved-word name
+ def test_change_columns
+ assert_nothing_raised { @connection.change_column_default(:group, :order, 'whatever') }
+ #the quoting here will reveal any double quoting issues in change_column's interaction with the column method in the adapter
+ assert_nothing_raised { @connection.change_column('group', 'order', :Int, :default => 0) }
+ assert_nothing_raised { @connection.rename_column(:group, :order, :values) }
+ end
+
+ # dump structure of table with reserved word name
+ def test_structure_dump
+ assert_nothing_raised { @connection.structure_dump }
+ end
+
+ # introspect table with reserved word name
+ def test_introspect
+ assert_nothing_raised { @connection.columns(:group) }
+ assert_nothing_raised { @connection.indexes(:group) }
+ end
+
+ #fixtures
+ self.use_instantiated_fixtures = true
+ self.use_transactional_fixtures = false
+
+ #fixtures :group
+
+ def test_fixtures
+ f = create_test_fixtures :select, :distinct, :group, :values, :distincts_selects
+
+ assert_nothing_raised {
+ f.each do |x|
+ x.delete_existing_fixtures
+ end
+ }
+
+ assert_nothing_raised {
+ f.each do |x|
+ x.insert_fixtures
+ end
+ }
+ end
+
+ #activerecord model class with reserved-word table name
+ def test_activerecord_model
+ create_test_fixtures :select, :distinct, :group, :values, :distincts_selects
+ x = nil
+ assert_nothing_raised { x = Group.new }
+ x.order = 'x'
+ assert_nothing_raised { x.save }
+ x.order = 'y'
+ assert_nothing_raised { x.save }
+ assert_nothing_raised { y = Group.find_by_order('y') }
+ assert_nothing_raised { y = Group.find(1) }
+ x = Group.find(1)
+ end
+
+ # has_one association with reserved-word table name
+ def test_has_one_associations
+ create_test_fixtures :select, :distinct, :group, :values, :distincts_selects
+ v = nil
+ assert_nothing_raised { v = Group.find(1).values }
+ assert_equal v.id, 2
+ end
+
+ # belongs_to association with reserved-word table name
+ def test_belongs_to_associations
+ create_test_fixtures :select, :distinct, :group, :values, :distincts_selects
+ gs = nil
+ assert_nothing_raised { gs = Select.find(2).groups }
+ assert_equal gs.length, 2
+ assert(gs.collect{|x| x.id}.sort == [2, 3])
+ end
+
+ # has_and_belongs_to_many with reserved-word table name
+ def test_has_and_belongs_to_many
+ create_test_fixtures :select, :distinct, :group, :values, :distincts_selects
+ s = nil
+ assert_nothing_raised { s = Distinct.find(1).selects }
+ assert_equal s.length, 2
+ assert(s.collect{|x|x.id}.sort == [1, 2])
+ end
+
+ # activerecord model introspection with reserved-word table and column names
+ def test_activerecord_introspection
+ assert_nothing_raised { Group.table_exists? }
+ assert_nothing_raised { Group.columns }
+ end
+
+ # Calculations
+ def test_calculations_work_with_reserved_words
+ assert_nothing_raised { Group.count }
+ end
+
+ def test_associations_work_with_reserved_words
+ assert_nothing_raised { Select.find(:all, :include => [:groups]) }
+ end
+
+ #the following functions were added to DRY test cases
+
+ private
+ # custom fixture loader, uses Fixtures#create_fixtures and appends base_path to the current file's path
+ def create_test_fixtures(*fixture_names)
+ fixture_path = "./test/fixtures/reserved_words"
+ Fixtures.create_fixtures(fixture_path, fixture_names)
+ end
+
+ # custom drop table, uses execute on connection to drop a table if it exists. note: escapes table_name
+ def drop_tables_directly(table_names, connection = @connection)
+ table_names.each do |name|
+ connection.execute("DROP TABLE IF EXISTS `#{name}`")
+ end
+ end
+
+ # custom create table, uses execute on connection to create a table, note: escapes table_name, does NOT escape columns
+ def create_tables_directly (tables, connection = @connection)
+ tables.each do |table_name, column_properties|
+ connection.execute("CREATE TABLE `#{table_name}` ( #{column_properties} )")
+ end
+ end
+
+end
diff --git a/activerecord/test/cases/schema_authorization_test_postgresql.rb b/activerecord/test/cases/schema_authorization_test_postgresql.rb
new file mode 100644
index 0000000000..d42611b515
--- /dev/null
+++ b/activerecord/test/cases/schema_authorization_test_postgresql.rb
@@ -0,0 +1,75 @@
+require 'abstract_unit'
+
+class SchemaThing < ActiveRecord::Base
+end
+
+class SchemaAuthorizationTest < ActiveSupport::TestCase
+ self.use_transactional_fixtures = false
+
+ TABLE_NAME = 'schema_things'
+ COLUMNS = [
+ 'id serial primary key',
+ 'name character varying(50)'
+ ]
+ USERS = ['rails_pg_schema_user1', 'rails_pg_schema_user2']
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.execute "SET search_path TO '$user',public"
+ set_session_auth
+ USERS.each do |u|
+ @connection.execute "CREATE ROLE #{u}"
+ @connection.execute "CREATE SCHEMA AUTHORIZATION #{u}"
+ set_session_auth u
+ @connection.execute "CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})"
+ @connection.execute "INSERT INTO #{TABLE_NAME} (name) VALUES ('#{u}')"
+ set_session_auth
+ end
+ end
+
+ def teardown
+ set_session_auth
+ @connection.execute "RESET search_path"
+ USERS.each do |u|
+ @connection.execute "DROP SCHEMA #{u} CASCADE"
+ @connection.execute "DROP ROLE #{u}"
+ end
+ end
+
+ def test_schema_invisible
+ assert_raise(ActiveRecord::StatementInvalid) do
+ set_session_auth
+ @connection.execute "SELECT * FROM #{TABLE_NAME}"
+ end
+ end
+
+ def test_schema_uniqueness
+ assert_nothing_raised do
+ set_session_auth
+ USERS.each do |u|
+ set_session_auth u
+ assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = 1")
+ set_session_auth
+ end
+ end
+ end
+
+ def test_sequence_schema_caching
+ assert_nothing_raised do
+ USERS.each do |u|
+ set_session_auth u
+ st = SchemaThing.new :name => 'TEST1'
+ st.save!
+ st = SchemaThing.new :id => 5, :name => 'TEST2'
+ st.save!
+ set_session_auth
+ end
+ end
+ end
+
+ private
+ def set_session_auth auth = nil
+ @connection.execute "SET SESSION AUTHORIZATION #{auth || 'default'}"
+ end
+
+end
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
new file mode 100644
index 0000000000..2b7226ec3b
--- /dev/null
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -0,0 +1,131 @@
+require 'abstract_unit'
+require "#{File.dirname(__FILE__)}/../lib/active_record/schema_dumper"
+require 'stringio'
+
+if ActiveRecord::Base.connection.respond_to?(:tables)
+
+ class SchemaDumperTest < ActiveSupport::TestCase
+ def standard_dump
+ stream = StringIO.new
+ ActiveRecord::SchemaDumper.ignore_tables = []
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
+ stream.string
+ end
+
+ def test_schema_dump
+ output = standard_dump
+ assert_match %r{create_table "accounts"}, output
+ assert_match %r{create_table "authors"}, output
+ assert_no_match %r{create_table "schema_info"}, output
+ end
+
+ def test_schema_dump_excludes_sqlite_sequence
+ output = standard_dump
+ assert_no_match %r{create_table "sqlite_sequence"}, output
+ end
+
+ def assert_line_up(lines, pattern, required = false)
+ return assert(true) if lines.empty?
+ matches = lines.map { |line| line.match(pattern) }
+ assert matches.all? if required
+ matches.compact!
+ return assert(true) if matches.empty?
+ assert_equal 1, matches.map{ |match| match.offset(0).first }.uniq.length
+ end
+
+ def column_definition_lines(output = standard_dump)
+ output.scan(/^( *)create_table.*?\n(.*?)^\1end/m).map{ |m| m.last.split(/\n/) }
+ end
+
+ def test_types_line_up
+ column_definition_lines.each do |column_set|
+ next if column_set.empty?
+
+ lengths = column_set.map do |column|
+ if match = column.match(/t\.(?:integer|decimal|float|datetime|timestamp|time|date|text|binary|string|boolean)\s+"/)
+ match[0].length
+ end
+ end
+
+ assert_equal 1, lengths.uniq.length
+ end
+ end
+
+ def test_arguments_line_up
+ column_definition_lines.each do |column_set|
+ assert_line_up(column_set, /:default => /)
+ assert_line_up(column_set, /:limit => /)
+ assert_line_up(column_set, /:null => /)
+ end
+ end
+
+ def test_no_dump_errors
+ output = standard_dump
+ assert_no_match %r{\# Could not dump table}, output
+ end
+
+ def test_schema_dump_includes_not_null_columns
+ stream = StringIO.new
+
+ ActiveRecord::SchemaDumper.ignore_tables = [/^[^r]/]
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
+ output = stream.string
+ assert_match %r{:null => false}, output
+ end
+
+ def test_schema_dump_with_string_ignored_table
+ stream = StringIO.new
+
+ ActiveRecord::SchemaDumper.ignore_tables = ['accounts']
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
+ output = stream.string
+ assert_no_match %r{create_table "accounts"}, output
+ assert_match %r{create_table "authors"}, output
+ assert_no_match %r{create_table "schema_info"}, output
+ end
+
+
+ def test_schema_dump_with_regexp_ignored_table
+ stream = StringIO.new
+
+ ActiveRecord::SchemaDumper.ignore_tables = [/^account/]
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
+ output = stream.string
+ assert_no_match %r{create_table "accounts"}, output
+ assert_match %r{create_table "authors"}, output
+ assert_no_match %r{create_table "schema_info"}, output
+ end
+
+
+ def test_schema_dump_illegal_ignored_table_value
+ stream = StringIO.new
+ ActiveRecord::SchemaDumper.ignore_tables = [5]
+ assert_raise(StandardError) do
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
+ end
+ end
+
+ if current_adapter?(:MysqlAdapter)
+ def test_schema_dump_should_not_add_default_value_for_mysql_text_field
+ output = standard_dump
+ assert_match %r{t.text\s+"body",\s+:default => "",\s+:null => false$}, output
+ end
+
+ def test_mysql_schema_dump_should_honor_nonstandard_primary_keys
+ output = standard_dump
+ match = output.match(%r{create_table "movies"(.*)do})
+ assert_not_nil(match, "nonstandardpk table not found")
+ assert_match %r(:primary_key => "movieid"), match[1], "non-standard primary key not preserved"
+ end
+ end
+
+ def test_schema_dump_includes_decimal_options
+ stream = StringIO.new
+ ActiveRecord::SchemaDumper.ignore_tables = [/^[^n]/]
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
+ output = stream.string
+ assert_match %r{:precision => 3,[[:space:]]+:scale => 2,[[:space:]]+:default => 2.78}, output
+ end
+ end
+
+end
diff --git a/activerecord/test/cases/schema_test_postgresql.rb b/activerecord/test/cases/schema_test_postgresql.rb
new file mode 100644
index 0000000000..c38d6c9253
--- /dev/null
+++ b/activerecord/test/cases/schema_test_postgresql.rb
@@ -0,0 +1,64 @@
+require 'abstract_unit'
+
+class SchemaTest < ActiveSupport::TestCase
+ self.use_transactional_fixtures = false
+
+ SCHEMA_NAME = 'test_schema'
+ TABLE_NAME = 'things'
+ COLUMNS = [
+ 'id integer',
+ 'name character varying(50)',
+ 'moment timestamp without time zone default now()'
+ ]
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.execute "CREATE SCHEMA #{SCHEMA_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})"
+ end
+
+ def teardown
+ @connection.execute "DROP SCHEMA #{SCHEMA_NAME} CASCADE"
+ end
+
+ def test_with_schema_prefixed_table_name
+ assert_nothing_raised do
+ assert_equal COLUMNS, columns("#{SCHEMA_NAME}.#{TABLE_NAME}")
+ end
+ end
+
+ def test_with_schema_search_path
+ assert_nothing_raised do
+ with_schema_search_path(SCHEMA_NAME) do
+ assert_equal COLUMNS, columns(TABLE_NAME)
+ end
+ end
+ end
+
+ def test_raise_on_unquoted_schema_name
+ assert_raise(ActiveRecord::StatementInvalid) do
+ with_schema_search_path '$user,public'
+ end
+ end
+
+ def test_without_schema_search_path
+ assert_raise(ActiveRecord::StatementInvalid) { columns(TABLE_NAME) }
+ end
+
+ def test_ignore_nil_schema_search_path
+ assert_nothing_raised { with_schema_search_path nil }
+ end
+
+ private
+ def columns(table_name)
+ @connection.send(:column_definitions, table_name).map do |name, type, default|
+ "#{name} #{type}" + (default ? " default #{default}" : '')
+ end
+ end
+
+ def with_schema_search_path(schema_search_path)
+ @connection.schema_search_path = schema_search_path
+ yield if block_given?
+ ensure
+ @connection.schema_search_path = "'$user', public"
+ end
+end
diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb
new file mode 100644
index 0000000000..e14fa02780
--- /dev/null
+++ b/activerecord/test/cases/serialization_test.rb
@@ -0,0 +1,47 @@
+require 'abstract_unit'
+require 'fixtures/contact'
+
+class SerializationTest < ActiveSupport::TestCase
+ FORMATS = [ :xml, :json ]
+
+ def setup
+ @contact_attributes = {
+ :name => 'aaron stack',
+ :age => 25,
+ :avatar => 'binarydata',
+ :created_at => Time.utc(2006, 8, 1),
+ :awesome => false,
+ :preferences => { :gem => '<strong>ruby</strong>' }
+ }
+
+ @contact = Contact.new(@contact_attributes)
+ end
+
+ def test_serialize_should_be_reversible
+ for format in FORMATS
+ @serialized = Contact.new.send("to_#{format}")
+ contact = Contact.new.send("from_#{format}", @serialized)
+
+ assert_equal @contact_attributes.keys.collect(&:to_s).sort, contact.attributes.keys.collect(&:to_s).sort, "For #{format}"
+ end
+ end
+
+ def test_serialize_should_allow_attribute_only_filtering
+ for format in FORMATS
+ @serialized = Contact.new(@contact_attributes).send("to_#{format}", :only => [ :age, :name ])
+ contact = Contact.new.send("from_#{format}", @serialized)
+ assert_equal @contact_attributes[:name], contact.name, "For #{format}"
+ assert_nil contact.avatar, "For #{format}"
+ end
+ end
+
+ def test_serialize_should_allow_attribute_except_filtering
+ for format in FORMATS
+ @serialized = Contact.new(@contact_attributes).send("to_#{format}", :except => [ :age, :name ])
+ contact = Contact.new.send("from_#{format}", @serialized)
+ assert_nil contact.name, "For #{format}"
+ assert_nil contact.age, "For #{format}"
+ assert_equal @contact_attributes[:awesome], contact.awesome, "For #{format}"
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/cases/synonym_test_oracle.rb b/activerecord/test/cases/synonym_test_oracle.rb
new file mode 100644
index 0000000000..c053bacb69
--- /dev/null
+++ b/activerecord/test/cases/synonym_test_oracle.rb
@@ -0,0 +1,17 @@
+require 'abstract_unit'
+require 'fixtures/topic'
+require 'fixtures/subject'
+
+# confirm that synonyms work just like tables; in this case
+# the "subjects" table in Oracle (defined in oci.sql) is just
+# a synonym to the "topics" table
+
+class TestOracleSynonym < ActiveSupport::TestCase
+
+ def test_oracle_synonym
+ topic = Topic.new
+ subject = Subject.new
+ assert_equal(topic.attributes, subject.attributes)
+ end
+
+end
diff --git a/activerecord/test/cases/table_name_test_sqlserver.rb b/activerecord/test/cases/table_name_test_sqlserver.rb
new file mode 100644
index 0000000000..f52a3da5fb
--- /dev/null
+++ b/activerecord/test/cases/table_name_test_sqlserver.rb
@@ -0,0 +1,23 @@
+require 'abstract_unit'
+require "#{File.dirname(__FILE__)}/../lib/active_record/schema"
+
+if ActiveRecord::Base.connection.supports_migrations?
+ class Order < ActiveRecord::Base
+ self.table_name = '[order]'
+ end
+
+ class TableNameTest < ActiveSupport::TestCase
+ self.use_transactional_fixtures = false
+
+ # Ensures Model.columns works when using SQLServer escape characters.
+ # Enables legacy schemas using SQL reserved words as table names.
+ # Should work with table names with spaces as well ('table name').
+ def test_escaped_table_name
+ assert_nothing_raised do
+ ActiveRecord::Base.connection.select_all 'SELECT * FROM [order]'
+ end
+ assert_equal '[order]', Order.table_name
+ assert_equal 5, Order.columns.length
+ end
+ end
+end
diff --git a/activerecord/test/cases/threaded_connections_test.rb b/activerecord/test/cases/threaded_connections_test.rb
new file mode 100644
index 0000000000..ec90df5294
--- /dev/null
+++ b/activerecord/test/cases/threaded_connections_test.rb
@@ -0,0 +1,48 @@
+require 'abstract_unit'
+require 'fixtures/topic'
+require 'fixtures/reply'
+
+unless %w(FrontBase).include? ActiveRecord::Base.connection.adapter_name
+ class ThreadedConnectionsTest < ActiveSupport::TestCase
+ self.use_transactional_fixtures = false
+
+ fixtures :topics
+
+ def setup
+ @connection = ActiveRecord::Base.remove_connection
+ @connections = []
+ @allow_concurrency = ActiveRecord::Base.allow_concurrency
+ end
+
+ def teardown
+ # clear the connection cache
+ ActiveRecord::Base.send(:clear_all_cached_connections!)
+ # set allow_concurrency to saved value
+ ActiveRecord::Base.allow_concurrency = @allow_concurrency
+ # reestablish old connection
+ ActiveRecord::Base.establish_connection(@connection)
+ end
+
+ def gather_connections(use_threaded_connections)
+ ActiveRecord::Base.allow_concurrency = use_threaded_connections
+ ActiveRecord::Base.establish_connection(@connection)
+
+ 5.times do
+ Thread.new do
+ Topic.find :first
+ @connections << ActiveRecord::Base.active_connections.values.first
+ end.join
+ end
+ end
+
+ def test_threaded_connections
+ gather_connections(true)
+ assert_equal @connections.uniq.length, 5
+ end
+
+ def test_unthreaded_connections
+ gather_connections(false)
+ assert_equal @connections.uniq.length, 1
+ end
+ end
+end
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
new file mode 100644
index 0000000000..f36ee3f4bb
--- /dev/null
+++ b/activerecord/test/cases/transactions_test.rb
@@ -0,0 +1,281 @@
+require 'abstract_unit'
+require 'fixtures/topic'
+require 'fixtures/reply'
+require 'fixtures/developer'
+
+class TransactionTest < ActiveSupport::TestCase
+ self.use_transactional_fixtures = false
+ fixtures :topics, :developers
+
+ def setup
+ @first, @second = Topic.find(1, 2).sort_by { |t| t.id }
+ end
+
+ def test_successful
+ Topic.transaction do
+ @first.approved = true
+ @second.approved = false
+ @first.save
+ @second.save
+ end
+
+ assert Topic.find(1).approved?, "First should have been approved"
+ assert !Topic.find(2).approved?, "Second should have been unapproved"
+ end
+
+ def transaction_with_return
+ Topic.transaction do
+ @first.approved = true
+ @second.approved = false
+ @first.save
+ @second.save
+ return
+ end
+ end
+
+ def test_successful_with_return
+ class << Topic.connection
+ alias :real_commit_db_transaction :commit_db_transaction
+ def commit_db_transaction
+ $committed = true
+ real_commit_db_transaction
+ end
+ end
+
+ $committed = false
+ transaction_with_return
+ assert $committed
+
+ assert Topic.find(1).approved?, "First should have been approved"
+ assert !Topic.find(2).approved?, "Second should have been unapproved"
+ ensure
+ class << Topic.connection
+ alias :commit_db_transaction :real_commit_db_transaction rescue nil
+ end
+ end
+
+ def test_successful_with_instance_method
+ @first.transaction do
+ @first.approved = true
+ @second.approved = false
+ @first.save
+ @second.save
+ end
+
+ assert Topic.find(1).approved?, "First should have been approved"
+ assert !Topic.find(2).approved?, "Second should have been unapproved"
+ end
+
+ def test_failing_on_exception
+ begin
+ Topic.transaction do
+ @first.approved = true
+ @second.approved = false
+ @first.save
+ @second.save
+ raise "Bad things!"
+ end
+ rescue
+ # caught it
+ end
+
+ assert @first.approved?, "First should still be changed in the objects"
+ assert !@second.approved?, "Second should still be changed in the objects"
+
+ assert !Topic.find(1).approved?, "First shouldn't have been approved"
+ assert Topic.find(2).approved?, "Second should still be approved"
+ end
+
+
+ def test_callback_rollback_in_save
+ add_exception_raising_after_save_callback_to_topic
+
+ begin
+ @first.approved = true
+ @first.save
+ flunk
+ rescue => e
+ assert_equal "Make the transaction rollback", e.message
+ assert !Topic.find(1).approved?
+ ensure
+ remove_exception_raising_after_save_callback_to_topic
+ end
+ end
+
+ def test_callback_rollback_in_create
+ new_topic = Topic.new(
+ :title => "A new topic",
+ :author_name => "Ben",
+ :author_email_address => "ben@example.com",
+ :written_on => "2003-07-16t15:28:11.2233+01:00",
+ :last_read => "2004-04-15",
+ :bonus_time => "2005-01-30t15:28:00.00+01:00",
+ :content => "Have a nice day",
+ :approved => false)
+ new_record_snapshot = new_topic.new_record?
+ id_present = new_topic.has_attribute?(Topic.primary_key)
+ id_snapshot = new_topic.id
+
+ # Make sure the second save gets the after_create callback called.
+ 2.times do
+ begin
+ add_exception_raising_after_create_callback_to_topic
+ new_topic.approved = true
+ new_topic.save
+ flunk
+ rescue => e
+ assert_equal "Make the transaction rollback", e.message
+ assert_equal new_record_snapshot, new_topic.new_record?, "The topic should have its old new_record value"
+ assert_equal id_snapshot, new_topic.id, "The topic should have its old id"
+ assert_equal id_present, new_topic.has_attribute?(Topic.primary_key)
+ ensure
+ remove_exception_raising_after_create_callback_to_topic
+ end
+ end
+ end
+
+ def test_nested_explicit_transactions
+ Topic.transaction do
+ Topic.transaction do
+ @first.approved = true
+ @second.approved = false
+ @first.save
+ @second.save
+ end
+ end
+
+ assert Topic.find(1).approved?, "First should have been approved"
+ assert !Topic.find(2).approved?, "Second should have been unapproved"
+ end
+
+ def test_manually_rolling_back_a_transaction
+ Topic.transaction do
+ @first.approved = true
+ @second.approved = false
+ @first.save
+ @second.save
+
+ raise ActiveRecord::Rollback
+ end
+
+ assert @first.approved?, "First should still be changed in the objects"
+ assert !@second.approved?, "Second should still be changed in the objects"
+
+ assert !Topic.find(1).approved?, "First shouldn't have been approved"
+ assert Topic.find(2).approved?, "Second should still be approved"
+ end
+
+ uses_mocha 'mocking connection.commit_db_transaction' do
+ def test_rollback_when_commit_raises
+ Topic.connection.expects(:begin_db_transaction)
+ Topic.connection.expects(:commit_db_transaction).raises('OH NOES')
+ Topic.connection.expects(:rollback_db_transaction)
+
+ assert_raise RuntimeError do
+ Topic.transaction do
+ # do nothing
+ end
+ end
+ end
+ end
+
+ private
+ def add_exception_raising_after_save_callback_to_topic
+ Topic.class_eval { def after_save() raise "Make the transaction rollback" end }
+ end
+
+ def remove_exception_raising_after_save_callback_to_topic
+ Topic.class_eval { remove_method :after_save }
+ end
+
+ def add_exception_raising_after_create_callback_to_topic
+ Topic.class_eval { def after_create() raise "Make the transaction rollback" end }
+ end
+
+ def remove_exception_raising_after_create_callback_to_topic
+ Topic.class_eval { remove_method :after_create }
+ end
+end
+
+if current_adapter?(:PostgreSQLAdapter)
+ class ConcurrentTransactionTest < TransactionTest
+ def setup
+ @allow_concurrency = ActiveRecord::Base.allow_concurrency
+ ActiveRecord::Base.allow_concurrency = true
+ super
+ end
+
+ def teardown
+ super
+ ActiveRecord::Base.allow_concurrency = @allow_concurrency
+ end
+
+ # This will cause transactions to overlap and fail unless they are performed on
+ # separate database connections.
+ def test_transaction_per_thread
+ assert_nothing_raised do
+ threads = (1..3).map do
+ Thread.new do
+ Topic.transaction do
+ topic = Topic.find(1)
+ topic.approved = !topic.approved?
+ topic.save!
+ topic.approved = !topic.approved?
+ topic.save!
+ end
+ end
+ end
+
+ threads.each { |t| t.join }
+ end
+ end
+
+ # Test for dirty reads among simultaneous transactions.
+ def test_transaction_isolation__read_committed
+ # Should be invariant.
+ original_salary = Developer.find(1).salary
+ temporary_salary = 200000
+
+ assert_nothing_raised do
+ threads = (1..3).map do
+ Thread.new do
+ Developer.transaction do
+ # Expect original salary.
+ dev = Developer.find(1)
+ assert_equal original_salary, dev.salary
+
+ dev.salary = temporary_salary
+ dev.save!
+
+ # Expect temporary salary.
+ dev = Developer.find(1)
+ assert_equal temporary_salary, dev.salary
+
+ dev.salary = original_salary
+ dev.save!
+
+ # Expect original salary.
+ dev = Developer.find(1)
+ assert_equal original_salary, dev.salary
+ end
+ end
+ end
+
+ # Keep our eyes peeled.
+ threads << Thread.new do
+ 10.times do
+ sleep 0.05
+ Developer.transaction do
+ # Always expect original salary.
+ assert_equal original_salary, Developer.find(1).salary
+ end
+ end
+ end
+
+ threads.each { |t| t.join }
+ end
+
+ assert_equal original_salary, Developer.find(1).salary
+ end
+ end
+end
diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb
new file mode 100755
index 0000000000..6f475f8364
--- /dev/null
+++ b/activerecord/test/cases/unconnected_test.rb
@@ -0,0 +1,32 @@
+require 'abstract_unit'
+
+class TestRecord < ActiveRecord::Base
+end
+
+class TestUnconnectedAdapter < ActiveSupport::TestCase
+ self.use_transactional_fixtures = false
+
+ def setup
+ @underlying = ActiveRecord::Base.connection
+ @specification = ActiveRecord::Base.remove_connection
+ end
+
+ def teardown
+ @underlying = nil
+ ActiveRecord::Base.establish_connection(@specification)
+ end
+
+ def test_connection_no_longer_established
+ assert_raise(ActiveRecord::ConnectionNotEstablished) do
+ TestRecord.find(1)
+ end
+
+ assert_raise(ActiveRecord::ConnectionNotEstablished) do
+ TestRecord.new.save
+ end
+ end
+
+ def test_underlying_adapter_no_longer_active
+ assert !@underlying.active?, "Removed adapter should no longer be active"
+ end
+end
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
new file mode 100755
index 0000000000..daec199be1
--- /dev/null
+++ b/activerecord/test/cases/validations_test.rb
@@ -0,0 +1,1407 @@
+require 'abstract_unit'
+require 'fixtures/topic'
+require 'fixtures/reply'
+require 'fixtures/person'
+require 'fixtures/developer'
+require 'fixtures/warehouse_thing'
+
+# The following methods in Topic are used in test_conditional_validation_*
+class Topic
+ def condition_is_true
+ return true
+ end
+
+ def condition_is_true_but_its_not
+ return false
+ end
+end
+
+class ProtectedPerson < ActiveRecord::Base
+ set_table_name 'people'
+ attr_accessor :addon
+ attr_protected :first_name
+end
+
+class UniqueReply < Reply
+ validates_uniqueness_of :content, :scope => 'parent_id'
+end
+
+class PlagiarizedReply < Reply
+ validates_acceptance_of :author_name
+end
+
+class SillyUniqueReply < UniqueReply
+end
+
+class Topic < ActiveRecord::Base
+ has_many :unique_replies, :dependent => :destroy, :foreign_key => "parent_id"
+ has_many :silly_unique_replies, :dependent => :destroy, :foreign_key => "parent_id"
+end
+
+class Wizard < ActiveRecord::Base
+ self.abstract_class = true
+
+ validates_uniqueness_of :name
+end
+
+class IneptWizard < Wizard
+ validates_uniqueness_of :city
+end
+
+class Conjurer < IneptWizard
+end
+
+class Thaumaturgist < IneptWizard
+end
+
+class ValidationsTest < ActiveSupport::TestCase
+ fixtures :topics, :developers, 'warehouse-things'
+
+ def setup
+ Topic.write_inheritable_attribute(:validate, nil)
+ Topic.write_inheritable_attribute(:validate_on_create, nil)
+ Topic.write_inheritable_attribute(:validate_on_update, nil)
+ end
+
+ def test_single_field_validation
+ r = Reply.new
+ r.title = "There's no content!"
+ assert !r.valid?, "A reply without content shouldn't be saveable"
+
+ r.content = "Messa content!"
+ assert r.valid?, "A reply with content should be saveable"
+ end
+
+ def test_single_attr_validation_and_error_msg
+ r = Reply.new
+ r.title = "There's no content!"
+ assert !r.valid?
+ assert r.errors.invalid?("content"), "A reply without content should mark that attribute as invalid"
+ assert_equal "Empty", r.errors.on("content"), "A reply without content should contain an error"
+ assert_equal 1, r.errors.count
+ end
+
+ def test_double_attr_validation_and_error_msg
+ r = Reply.new
+ assert !r.valid?
+
+ assert r.errors.invalid?("title"), "A reply without title should mark that attribute as invalid"
+ assert_equal "Empty", r.errors.on("title"), "A reply without title should contain an error"
+
+ assert r.errors.invalid?("content"), "A reply without content should mark that attribute as invalid"
+ assert_equal "Empty", r.errors.on("content"), "A reply without content should contain an error"
+
+ assert_equal 2, r.errors.count
+ end
+
+ def test_error_on_create
+ r = Reply.new
+ r.title = "Wrong Create"
+ assert !r.valid?
+ assert r.errors.invalid?("title"), "A reply with a bad title should mark that attribute as invalid"
+ assert_equal "is Wrong Create", r.errors.on("title"), "A reply with a bad content should contain an error"
+ end
+
+ def test_error_on_update
+ r = Reply.new
+ r.title = "Bad"
+ r.content = "Good"
+ assert r.save, "First save should be successful"
+
+ r.title = "Wrong Update"
+ assert !r.save, "Second save should fail"
+
+ assert r.errors.invalid?("title"), "A reply with a bad title should mark that attribute as invalid"
+ assert_equal "is Wrong Update", r.errors.on("title"), "A reply with a bad content should contain an error"
+ end
+
+ def test_invalid_record_exception
+ assert_raises(ActiveRecord::RecordInvalid) { Reply.create! }
+ assert_raises(ActiveRecord::RecordInvalid) { Reply.new.save! }
+
+ begin
+ r = Reply.new
+ r.save!
+ flunk
+ rescue ActiveRecord::RecordInvalid => invalid
+ assert_equal r, invalid.record
+ end
+ end
+
+ def test_exception_on_create_bang_many
+ assert_raises(ActiveRecord::RecordInvalid) do
+ Reply.create!([ { "title" => "OK" }, { "title" => "Wrong Create" }])
+ end
+ end
+
+ def test_scoped_create_without_attributes
+ Reply.with_scope(:create => {}) do
+ assert_raises(ActiveRecord::RecordInvalid) { Reply.create! }
+ end
+ end
+
+ def test_create_with_exceptions_using_scope_for_protected_attributes
+ assert_nothing_raised do
+ ProtectedPerson.with_scope( :create => { :first_name => "Mary" } ) do
+ person = ProtectedPerson.create! :addon => "Addon"
+ assert_equal person.first_name, "Mary", "scope should ignore attr_protected"
+ end
+ end
+ end
+
+ def test_create_with_exceptions_using_scope_and_empty_attributes
+ assert_nothing_raised do
+ ProtectedPerson.with_scope( :create => { :first_name => "Mary" } ) do
+ person = ProtectedPerson.create!
+ assert_equal person.first_name, "Mary", "should be ok when no attributes are passed to create!"
+ end
+ end
+ end
+
+ def test_single_error_per_attr_iteration
+ r = Reply.new
+ r.save
+
+ errors = []
+ r.errors.each { |attr, msg| errors << [attr, msg] }
+
+ assert errors.include?(["title", "Empty"])
+ assert errors.include?(["content", "Empty"])
+ end
+
+ def test_multiple_errors_per_attr_iteration_with_full_error_composition
+ r = Reply.new
+ r.title = "Wrong Create"
+ r.content = "Mismatch"
+ r.save
+
+ errors = []
+ r.errors.each_full { |error| errors << error }
+
+ assert_equal "Title is Wrong Create", errors[0]
+ assert_equal "Title is Content Mismatch", errors[1]
+ assert_equal 2, r.errors.count
+ end
+
+ def test_errors_on_base
+ r = Reply.new
+ r.content = "Mismatch"
+ r.save
+ r.errors.add_to_base "Reply is not dignifying"
+
+ errors = []
+ r.errors.each_full { |error| errors << error }
+
+ assert_equal "Reply is not dignifying", r.errors.on_base
+
+ assert errors.include?("Title Empty")
+ assert errors.include?("Reply is not dignifying")
+ assert_equal 2, r.errors.count
+ end
+
+ def test_create_without_validation
+ reply = Reply.new
+ assert !reply.save
+ assert reply.save(false)
+ end
+
+ def test_create_without_validation_bang
+ count = Reply.count
+ assert_nothing_raised { Reply.new.save_without_validation! }
+ assert count+1, Reply.count
+ end
+
+ def test_validates_each
+ perform = true
+ hits = 0
+ Topic.validates_each(:title, :content, [:title, :content]) do |record, attr|
+ if perform
+ record.errors.add attr, 'gotcha'
+ hits += 1
+ end
+ end
+ t = Topic.new("title" => "valid", "content" => "whatever")
+ assert !t.save
+ assert_equal 4, hits
+ assert_equal %w(gotcha gotcha), t.errors.on(:title)
+ assert_equal %w(gotcha gotcha), t.errors.on(:content)
+ ensure
+ perform = false
+ end
+
+ def test_no_title_confirmation
+ Topic.validates_confirmation_of(:title)
+
+ t = Topic.new(:author_name => "Plutarch")
+ assert t.valid?
+
+ t.title_confirmation = "Parallel Lives"
+ assert !t.valid?
+
+ t.title_confirmation = nil
+ t.title = "Parallel Lives"
+ assert t.valid?
+
+ t.title_confirmation = "Parallel Lives"
+ assert t.valid?
+ end
+
+ def test_title_confirmation
+ Topic.validates_confirmation_of(:title)
+
+ t = Topic.create("title" => "We should be confirmed","title_confirmation" => "")
+ assert !t.save
+
+ t.title_confirmation = "We should be confirmed"
+ assert t.save
+ end
+
+ def test_terms_of_service_agreement_no_acceptance
+ Topic.validates_acceptance_of(:terms_of_service, :on => :create)
+
+ t = Topic.create("title" => "We should not be confirmed")
+ assert t.save
+ end
+
+ def test_terms_of_service_agreement
+ Topic.validates_acceptance_of(:terms_of_service, :on => :create)
+
+ t = Topic.create("title" => "We should be confirmed","terms_of_service" => "")
+ assert !t.save
+ assert_equal "must be accepted", t.errors.on(:terms_of_service)
+
+ t.terms_of_service = "1"
+ assert t.save
+ end
+
+
+ def test_eula
+ Topic.validates_acceptance_of(:eula, :message => "must be abided", :on => :create)
+
+ t = Topic.create("title" => "We should be confirmed","eula" => "")
+ assert !t.save
+ assert_equal "must be abided", t.errors.on(:eula)
+
+ t.eula = "1"
+ assert t.save
+ end
+
+ def test_terms_of_service_agreement_with_accept_value
+ Topic.validates_acceptance_of(:terms_of_service, :on => :create, :accept => "I agree.")
+
+ t = Topic.create("title" => "We should be confirmed", "terms_of_service" => "")
+ assert !t.save
+ assert_equal "must be accepted", t.errors.on(:terms_of_service)
+
+ t.terms_of_service = "I agree."
+ assert t.save
+ end
+
+ def test_validates_acceptance_of_as_database_column
+ reply = PlagiarizedReply.create("author_name" => "Dan Brown")
+ assert_equal "Dan Brown", reply["author_name"]
+ end
+
+ def test_validates_acceptance_of_with_non_existant_table
+ Object.const_set :IncorporealModel, Class.new(ActiveRecord::Base)
+
+ assert_nothing_raised ActiveRecord::StatementInvalid do
+ IncorporealModel.validates_acceptance_of(:incorporeal_column)
+ end
+ end
+
+ def test_validate_presences
+ Topic.validates_presence_of(:title, :content)
+
+ t = Topic.create
+ assert !t.save
+ assert_equal "can't be blank", t.errors.on(:title)
+ assert_equal "can't be blank", t.errors.on(:content)
+
+ t.title = "something"
+ t.content = " "
+
+ assert !t.save
+ assert_equal "can't be blank", t.errors.on(:content)
+
+ t.content = "like stuff"
+
+ assert t.save
+ end
+
+ def test_validate_uniqueness
+ Topic.validates_uniqueness_of(:title)
+
+ t = Topic.new("title" => "I'm unique!")
+ assert t.save, "Should save t as unique"
+
+ t.content = "Remaining unique"
+ assert t.save, "Should still save t as unique"
+
+ t2 = Topic.new("title" => "I'm unique!")
+ assert !t2.valid?, "Shouldn't be valid"
+ assert !t2.save, "Shouldn't save t2 as unique"
+ assert_equal "has already been taken", t2.errors.on(:title)
+
+ t2.title = "Now Im really also unique"
+ assert t2.save, "Should now save t2 as unique"
+ end
+
+ def test_validate_uniqueness_with_scope
+ Reply.validates_uniqueness_of(:content, :scope => "parent_id")
+
+ t = Topic.create("title" => "I'm unique!")
+
+ r1 = t.replies.create "title" => "r1", "content" => "hello world"
+ assert r1.valid?, "Saving r1"
+
+ r2 = t.replies.create "title" => "r2", "content" => "hello world"
+ assert !r2.valid?, "Saving r2 first time"
+
+ r2.content = "something else"
+ assert r2.save, "Saving r2 second time"
+
+ t2 = Topic.create("title" => "I'm unique too!")
+ r3 = t2.replies.create "title" => "r3", "content" => "hello world"
+ assert r3.valid?, "Saving r3"
+ end
+
+ def test_validate_uniqueness_scoped_to_defining_class
+ t = Topic.create("title" => "What, me worry?")
+
+ r1 = t.unique_replies.create "title" => "r1", "content" => "a barrel of fun"
+ assert r1.valid?, "Saving r1"
+
+ r2 = t.silly_unique_replies.create "title" => "r2", "content" => "a barrel of fun"
+ assert !r2.valid?, "Saving r2"
+
+ # Should succeed as validates_uniqueness_of only applies to
+ # UniqueReply and its subclasses
+ r3 = t.replies.create "title" => "r2", "content" => "a barrel of fun"
+ assert r3.valid?, "Saving r3"
+ end
+
+ def test_validate_uniqueness_with_scope_array
+ Reply.validates_uniqueness_of(:author_name, :scope => [:author_email_address, :parent_id])
+
+ t = Topic.create("title" => "The earth is actually flat!")
+
+ r1 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy@rubyonrails.com", "title" => "You're crazy!", "content" => "Crazy reply"
+ assert r1.valid?, "Saving r1"
+
+ r2 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy@rubyonrails.com", "title" => "You're crazy!", "content" => "Crazy reply again..."
+ assert !r2.valid?, "Saving r2. Double reply by same author."
+
+ r2.author_email_address = "jeremy_alt_email@rubyonrails.com"
+ assert r2.save, "Saving r2 the second time."
+
+ r3 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy_alt_email@rubyonrails.com", "title" => "You're wrong", "content" => "It's cubic"
+ assert !r3.valid?, "Saving r3"
+
+ r3.author_name = "jj"
+ assert r3.save, "Saving r3 the second time."
+
+ r3.author_name = "jeremy"
+ assert !r3.save, "Saving r3 the third time."
+ end
+
+ def test_validate_case_insensitive_uniqueness
+ Topic.validates_uniqueness_of(:title, :parent_id, :case_sensitive => false, :allow_nil => true)
+
+ t = Topic.new("title" => "I'm unique!", :parent_id => 2)
+ assert t.save, "Should save t as unique"
+
+ t.content = "Remaining unique"
+ assert t.save, "Should still save t as unique"
+
+ t2 = Topic.new("title" => "I'm UNIQUE!", :parent_id => 1)
+ assert !t2.valid?, "Shouldn't be valid"
+ assert !t2.save, "Shouldn't save t2 as unique"
+ assert t2.errors.on(:title)
+ assert t2.errors.on(:parent_id)
+ assert_equal "has already been taken", t2.errors.on(:title)
+
+ t2.title = "I'm truly UNIQUE!"
+ assert !t2.valid?, "Shouldn't be valid"
+ assert !t2.save, "Shouldn't save t2 as unique"
+ assert_nil t2.errors.on(:title)
+ assert t2.errors.on(:parent_id)
+
+ t2.parent_id = 3
+ assert t2.save, "Should now save t2 as unique"
+
+ t2.parent_id = nil
+ t2.title = nil
+ assert t2.valid?, "should validate with nil"
+ assert t2.save, "should save with nil"
+ end
+
+ def test_validate_uniqueness_with_non_standard_table_names
+ i1 = WarehouseThing.create(:value => 1000)
+ assert !i1.valid?, "i1 should not be valid"
+ assert i1.errors.on(:value), "Should not be empty"
+ end
+
+
+ def test_validate_straight_inheritance_uniqueness
+ w1 = IneptWizard.create(:name => "Rincewind", :city => "Ankh-Morpork")
+ assert w1.valid?, "Saving w1"
+
+ # Should use validation from base class (which is abstract)
+ w2 = IneptWizard.new(:name => "Rincewind", :city => "Quirm")
+ assert !w2.valid?, "w2 shouldn't be valid"
+ assert w2.errors.on(:name), "Should have errors for name"
+ assert_equal "has already been taken", w2.errors.on(:name), "Should have uniqueness message for name"
+
+ w3 = Conjurer.new(:name => "Rincewind", :city => "Quirm")
+ assert !w3.valid?, "w3 shouldn't be valid"
+ assert w3.errors.on(:name), "Should have errors for name"
+ assert_equal "has already been taken", w3.errors.on(:name), "Should have uniqueness message for name"
+
+ w4 = Conjurer.create(:name => "The Amazing Bonko", :city => "Quirm")
+ assert w4.valid?, "Saving w4"
+
+ w5 = Thaumaturgist.new(:name => "The Amazing Bonko", :city => "Lancre")
+ assert !w5.valid?, "w5 shouldn't be valid"
+ assert w5.errors.on(:name), "Should have errors for name"
+ assert_equal "has already been taken", w5.errors.on(:name), "Should have uniqueness message for name"
+
+ w6 = Thaumaturgist.new(:name => "Mustrum Ridcully", :city => "Quirm")
+ assert !w6.valid?, "w6 shouldn't be valid"
+ assert w6.errors.on(:city), "Should have errors for city"
+ assert_equal "has already been taken", w6.errors.on(:city), "Should have uniqueness message for city"
+ end
+
+ def test_validate_format
+ Topic.validates_format_of(:title, :content, :with => /^Validation\smacros \w+!$/, :message => "is bad data")
+
+ t = Topic.create("title" => "i'm incorrect", "content" => "Validation macros rule!")
+ assert !t.valid?, "Shouldn't be valid"
+ assert !t.save, "Shouldn't save because it's invalid"
+ assert_equal "is bad data", t.errors.on(:title)
+ assert_nil t.errors.on(:content)
+
+ t.title = "Validation macros rule!"
+
+ assert t.save
+ assert_nil t.errors.on(:title)
+
+ assert_raise(ArgumentError) { Topic.validates_format_of(:title, :content) }
+ end
+
+ # testing ticket #3142
+ def test_validate_format_numeric
+ Topic.validates_format_of(:title, :content, :with => /^[1-9][0-9]*$/, :message => "is bad data")
+
+ t = Topic.create("title" => "72x", "content" => "6789")
+ assert !t.valid?, "Shouldn't be valid"
+ assert !t.save, "Shouldn't save because it's invalid"
+ assert_equal "is bad data", t.errors.on(:title)
+ assert_nil t.errors.on(:content)
+
+ t.title = "-11"
+ assert !t.valid?, "Shouldn't be valid"
+
+ t.title = "03"
+ assert !t.valid?, "Shouldn't be valid"
+
+ t.title = "z44"
+ assert !t.valid?, "Shouldn't be valid"
+
+ t.title = "5v7"
+ assert !t.valid?, "Shouldn't be valid"
+
+ t.title = "1"
+
+ assert t.save
+ assert_nil t.errors.on(:title)
+ end
+
+ def test_validates_inclusion_of
+ Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ) )
+
+ assert !Topic.create("title" => "a!", "content" => "abc").valid?
+ assert !Topic.create("title" => "a b", "content" => "abc").valid?
+ assert !Topic.create("title" => nil, "content" => "def").valid?
+
+ t = Topic.create("title" => "a", "content" => "I know you are but what am I?")
+ assert t.valid?
+ t.title = "uhoh"
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "is not included in the list", t.errors["title"]
+
+ assert_raise(ArgumentError) { Topic.validates_inclusion_of( :title, :in => nil ) }
+ assert_raise(ArgumentError) { Topic.validates_inclusion_of( :title, :in => 0) }
+
+ assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of( :title, :in => "hi!" ) }
+ assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of( :title, :in => {} ) }
+ assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of( :title, :in => [] ) }
+ end
+
+ def test_validates_inclusion_of_with_allow_nil
+ Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ), :allow_nil=>true )
+
+ assert !Topic.create("title" => "a!", "content" => "abc").valid?
+ assert !Topic.create("title" => "", "content" => "abc").valid?
+ assert Topic.create("title" => nil, "content" => "abc").valid?
+ end
+
+ def test_numericality_with_getter_method
+ Developer.validates_numericality_of( :salary )
+ developer = Developer.new("name" => "michael", "salary" => nil)
+ developer.instance_eval("def salary; read_attribute('salary') ? read_attribute('salary') : 100000; end")
+ assert developer.valid?
+ end
+
+ def test_validates_length_of_with_allow_nil
+ Topic.validates_length_of( :title, :is => 5, :allow_nil=>true )
+
+ assert !Topic.create("title" => "ab").valid?
+ assert !Topic.create("title" => "").valid?
+ assert Topic.create("title" => nil).valid?
+ assert Topic.create("title" => "abcde").valid?
+ end
+
+ def test_validates_length_of_with_allow_blank
+ Topic.validates_length_of( :title, :is => 5, :allow_blank=>true )
+
+ assert !Topic.create("title" => "ab").valid?
+ assert Topic.create("title" => "").valid?
+ assert Topic.create("title" => nil).valid?
+ assert Topic.create("title" => "abcde").valid?
+ end
+
+ def test_validates_inclusion_of_with_formatted_message
+ Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ), :message => "option %s is not in the list" )
+
+ assert Topic.create("title" => "a", "content" => "abc").valid?
+
+ t = Topic.create("title" => "uhoh", "content" => "abc")
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "option uhoh is not in the list", t.errors["title"]
+ end
+
+ def test_numericality_with_allow_nil_and_getter_method
+ Developer.validates_numericality_of( :salary, :allow_nil => true)
+ developer = Developer.new("name" => "michael", "salary" => nil)
+ developer.instance_eval("def salary; read_attribute('salary') ? read_attribute('salary') : 100000; end")
+ assert developer.valid?
+ end
+
+ def test_validates_exclusion_of
+ Topic.validates_exclusion_of( :title, :in => %w( abe monkey ) )
+
+ assert Topic.create("title" => "something", "content" => "abc").valid?
+ assert !Topic.create("title" => "monkey", "content" => "abc").valid?
+ end
+
+ def test_validates_exclusion_of_with_formatted_message
+ Topic.validates_exclusion_of( :title, :in => %w( abe monkey ), :message => "option %s is restricted" )
+
+ assert Topic.create("title" => "something", "content" => "abc")
+
+ t = Topic.create("title" => "monkey")
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "option monkey is restricted", t.errors["title"]
+ end
+
+ def test_validates_length_of_using_minimum
+ Topic.validates_length_of :title, :minimum => 5
+
+ t = Topic.create("title" => "valid", "content" => "whatever")
+ assert t.valid?
+
+ t.title = "not"
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "is too short (minimum is 5 characters)", t.errors["title"]
+
+ t.title = ""
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "is too short (minimum is 5 characters)", t.errors["title"]
+
+ t.title = nil
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "is too short (minimum is 5 characters)", t.errors["title"]
+ end
+
+ def test_optionally_validates_length_of_using_minimum
+ Topic.validates_length_of :title, :minimum => 5, :allow_nil => true
+
+ t = Topic.create("title" => "valid", "content" => "whatever")
+ assert t.valid?
+
+ t.title = nil
+ assert t.valid?
+ end
+
+ def test_validates_length_of_using_maximum
+ Topic.validates_length_of :title, :maximum => 5
+
+ t = Topic.create("title" => "valid", "content" => "whatever")
+ assert t.valid?
+
+ t.title = "notvalid"
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "is too long (maximum is 5 characters)", t.errors["title"]
+
+ t.title = ""
+ assert t.valid?
+
+ t.title = nil
+ assert !t.valid?
+ end
+
+ def test_optionally_validates_length_of_using_maximum
+ Topic.validates_length_of :title, :maximum => 5, :allow_nil => true
+
+ t = Topic.create("title" => "valid", "content" => "whatever")
+ assert t.valid?
+
+ t.title = nil
+ assert t.valid?
+ end
+
+ def test_validates_length_of_using_within
+ Topic.validates_length_of(:title, :content, :within => 3..5)
+
+ t = Topic.new("title" => "a!", "content" => "I'm ooooooooh so very long")
+ assert !t.valid?
+ assert_equal "is too short (minimum is 3 characters)", t.errors.on(:title)
+ assert_equal "is too long (maximum is 5 characters)", t.errors.on(:content)
+
+ t.title = nil
+ t.content = nil
+ assert !t.valid?
+ assert_equal "is too short (minimum is 3 characters)", t.errors.on(:title)
+ assert_equal "is too short (minimum is 3 characters)", t.errors.on(:content)
+
+ t.title = "abe"
+ t.content = "mad"
+ assert t.valid?
+ end
+
+ def test_optionally_validates_length_of_using_within
+ Topic.validates_length_of :title, :content, :within => 3..5, :allow_nil => true
+
+ t = Topic.create('title' => 'abc', 'content' => 'abcd')
+ assert t.valid?
+
+ t.title = nil
+ assert t.valid?
+ end
+
+ def test_optionally_validates_length_of_using_within_on_create
+ Topic.validates_length_of :title, :content, :within => 5..10, :on => :create, :too_long => "my string is too long: %d"
+
+ t = Topic.create("title" => "thisisnotvalid", "content" => "whatever")
+ assert !t.save
+ assert t.errors.on(:title)
+ assert_equal "my string is too long: 10", t.errors[:title]
+
+ t.title = "butthisis"
+ assert t.save
+
+ t.title = "few"
+ assert t.save
+
+ t.content = "andthisislong"
+ assert t.save
+
+ t.content = t.title = "iamfine"
+ assert t.save
+ end
+
+ def test_optionally_validates_length_of_using_within_on_update
+ Topic.validates_length_of :title, :content, :within => 5..10, :on => :update, :too_short => "my string is too short: %d"
+
+ t = Topic.create("title" => "vali", "content" => "whatever")
+ assert !t.save
+ assert t.errors.on(:title)
+
+ t.title = "not"
+ assert !t.save
+ assert t.errors.on(:title)
+ assert_equal "my string is too short: 5", t.errors[:title]
+
+ t.title = "valid"
+ t.content = "andthisistoolong"
+ assert !t.save
+ assert t.errors.on(:content)
+
+ t.content = "iamfine"
+ assert t.save
+ end
+
+ def test_validates_length_of_using_is
+ Topic.validates_length_of :title, :is => 5
+
+ t = Topic.create("title" => "valid", "content" => "whatever")
+ assert t.valid?
+
+ t.title = "notvalid"
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "is the wrong length (should be 5 characters)", t.errors["title"]
+
+ t.title = ""
+ assert !t.valid?
+
+ t.title = nil
+ assert !t.valid?
+ end
+
+ def test_optionally_validates_length_of_using_is
+ Topic.validates_length_of :title, :is => 5, :allow_nil => true
+
+ t = Topic.create("title" => "valid", "content" => "whatever")
+ assert t.valid?
+
+ t.title = nil
+ assert t.valid?
+ end
+
+ def test_validates_length_of_using_bignum
+ bigmin = 2 ** 30
+ bigmax = 2 ** 32
+ bigrange = bigmin...bigmax
+ assert_nothing_raised do
+ Topic.validates_length_of :title, :is => bigmin + 5
+ Topic.validates_length_of :title, :within => bigrange
+ Topic.validates_length_of :title, :in => bigrange
+ Topic.validates_length_of :title, :minimum => bigmin
+ Topic.validates_length_of :title, :maximum => bigmax
+ end
+ end
+
+ def test_validates_length_with_globally_modified_error_message
+ ActiveRecord::Errors.default_error_messages[:too_short] = 'tu est trops petit hombre %d'
+ Topic.validates_length_of :title, :minimum => 10
+ t = Topic.create(:title => 'too short')
+ assert !t.valid?
+
+ assert_equal 'tu est trops petit hombre 10', t.errors['title']
+ end
+
+ def test_validates_size_of_association
+ assert_nothing_raised { Topic.validates_size_of :replies, :minimum => 1 }
+ t = Topic.new('title' => 'noreplies', 'content' => 'whatever')
+ assert !t.save
+ assert t.errors.on(:replies)
+ reply = t.replies.build('title' => 'areply', 'content' => 'whateveragain')
+ assert t.valid?
+ end
+
+ def test_validates_length_of_nasty_params
+ assert_raise(ArgumentError) { Topic.validates_length_of(:title, :minimum=>6, :maximum=>9) }
+ assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>6, :maximum=>9) }
+ assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>6, :minimum=>9) }
+ assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>6, :is=>9) }
+ assert_raise(ArgumentError) { Topic.validates_length_of(:title, :minimum=>"a") }
+ assert_raise(ArgumentError) { Topic.validates_length_of(:title, :maximum=>"a") }
+ assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>"a") }
+ assert_raise(ArgumentError) { Topic.validates_length_of(:title, :is=>"a") }
+ end
+
+ def test_validates_length_of_custom_errors_for_minimum_with_message
+ Topic.validates_length_of( :title, :minimum=>5, :message=>"boo %d" )
+ t = Topic.create("title" => "uhoh", "content" => "whatever")
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "boo 5", t.errors["title"]
+ end
+
+ def test_validates_length_of_custom_errors_for_minimum_with_too_short
+ Topic.validates_length_of( :title, :minimum=>5, :too_short=>"hoo %d" )
+ t = Topic.create("title" => "uhoh", "content" => "whatever")
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "hoo 5", t.errors["title"]
+ end
+
+ def test_validates_length_of_custom_errors_for_maximum_with_message
+ Topic.validates_length_of( :title, :maximum=>5, :message=>"boo %d" )
+ t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "boo 5", t.errors["title"]
+ end
+
+ def test_validates_length_of_custom_errors_for_maximum_with_too_long
+ Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d" )
+ t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "hoo 5", t.errors["title"]
+ end
+
+ def test_validates_length_of_custom_errors_for_is_with_message
+ Topic.validates_length_of( :title, :is=>5, :message=>"boo %d" )
+ t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "boo 5", t.errors["title"]
+ end
+
+ def test_validates_length_of_custom_errors_for_is_with_wrong_length
+ Topic.validates_length_of( :title, :is=>5, :wrong_length=>"hoo %d" )
+ t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "hoo 5", t.errors["title"]
+ end
+
+ def test_validates_length_of_using_minimum_utf8
+ with_kcode('UTF8') do
+ Topic.validates_length_of :title, :minimum => 5
+
+ t = Topic.create("title" => "一二三四五", "content" => "whatever")
+ assert t.valid?
+
+ t.title = "一二三四"
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "is too short (minimum is 5 characters)", t.errors["title"]
+ end
+ end
+
+ def test_validates_length_of_using_maximum_utf8
+ with_kcode('UTF8') do
+ Topic.validates_length_of :title, :maximum => 5
+
+ t = Topic.create("title" => "一二三四五", "content" => "whatever")
+ assert t.valid?
+
+ t.title = "一二34五六"
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "is too long (maximum is 5 characters)", t.errors["title"]
+ end
+ end
+
+ def test_validates_length_of_using_within_utf8
+ with_kcode('UTF8') do
+ Topic.validates_length_of(:title, :content, :within => 3..5)
+
+ t = Topic.new("title" => "一二", "content" => "12三四五六七")
+ assert !t.valid?
+ assert_equal "is too short (minimum is 3 characters)", t.errors.on(:title)
+ assert_equal "is too long (maximum is 5 characters)", t.errors.on(:content)
+ t.title = "一二三"
+ t.content = "12三"
+ assert t.valid?
+ end
+ end
+
+ def test_optionally_validates_length_of_using_within_utf8
+ with_kcode('UTF8') do
+ Topic.validates_length_of :title, :content, :within => 3..5, :allow_nil => true
+
+ t = Topic.create('title' => '一二三', 'content' => '一二三四五')
+ assert t.valid?
+
+ t.title = nil
+ assert t.valid?
+ end
+ end
+
+ def test_optionally_validates_length_of_using_within_on_create_utf8
+ with_kcode('UTF8') do
+ Topic.validates_length_of :title, :content, :within => 5..10, :on => :create, :too_long => "長すぎます: %d"
+
+ t = Topic.create("title" => "一二三四五六七八九十A", "content" => "whatever")
+ assert !t.save
+ assert t.errors.on(:title)
+ assert_equal "長すぎます: 10", t.errors[:title]
+
+ t.title = "一二三四五六七八九"
+ assert t.save
+
+ t.title = "一二3"
+ assert t.save
+
+ t.content = "一二三四五六七八九十"
+ assert t.save
+
+ t.content = t.title = "一二三四五六"
+ assert t.save
+ end
+ end
+
+ def test_optionally_validates_length_of_using_within_on_update_utf8
+ with_kcode('UTF8') do
+ Topic.validates_length_of :title, :content, :within => 5..10, :on => :update, :too_short => "短すぎます: %d"
+
+ t = Topic.create("title" => "一二三4", "content" => "whatever")
+ assert !t.save
+ assert t.errors.on(:title)
+
+ t.title = "1二三4"
+ assert !t.save
+ assert t.errors.on(:title)
+ assert_equal "短すぎます: 5", t.errors[:title]
+
+ t.title = "valid"
+ t.content = "一二三四五六七八九十A"
+ assert !t.save
+ assert t.errors.on(:content)
+
+ t.content = "一二345"
+ assert t.save
+ end
+ end
+
+ def test_validates_length_of_using_is_utf8
+ with_kcode('UTF8') do
+ Topic.validates_length_of :title, :is => 5
+
+ t = Topic.create("title" => "一二345", "content" => "whatever")
+ assert t.valid?
+
+ t.title = "一二345六"
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "is the wrong length (should be 5 characters)", t.errors["title"]
+ end
+ end
+
+ def test_validates_size_of_association_utf8
+ with_kcode('UTF8') do
+ assert_nothing_raised { Topic.validates_size_of :replies, :minimum => 1 }
+ t = Topic.new('title' => 'あいうえお', 'content' => 'かきくけこ')
+ assert !t.save
+ assert t.errors.on(:replies)
+ t.replies.build('title' => 'あいうえお', 'content' => 'かきくけこ')
+ assert t.valid?
+ end
+ end
+
+ def test_validates_associated_many
+ Topic.validates_associated( :replies )
+ t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
+ t.replies << [r = Reply.new("title" => "A reply"), r2 = Reply.new("title" => "Another reply", "content" => "non-empty"), r3 = Reply.new("title" => "Yet another reply"), r4 = Reply.new("title" => "The last reply", "content" => "non-empty")]
+ assert !t.valid?
+ assert t.errors.on(:replies)
+ assert_equal 1, r.errors.count # make sure all associated objects have been validated
+ assert_equal 0, r2.errors.count
+ assert_equal 1, r3.errors.count
+ assert_equal 0, r4.errors.count
+ r.content = r3.content = "non-empty"
+ assert t.valid?
+ end
+
+ def test_validates_associated_one
+ Reply.validates_associated( :topic )
+ Topic.validates_presence_of( :content )
+ r = Reply.new("title" => "A reply", "content" => "with content!")
+ r.topic = Topic.create("title" => "uhohuhoh")
+ assert !r.valid?
+ assert r.errors.on(:topic)
+ r.topic.content = "non-empty"
+ assert r.valid?
+ end
+
+ def test_validate_block
+ Topic.validate { |topic| topic.errors.add("title", "will never be valid") }
+ t = Topic.create("title" => "Title", "content" => "whatever")
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "will never be valid", t.errors["title"]
+ end
+
+ def test_invalid_validator
+ Topic.validate 3
+ assert_raise(ActiveRecord::ActiveRecordError) { t = Topic.create }
+ end
+
+ def test_throw_away_typing
+ d = Developer.new("name" => "David", "salary" => "100,000")
+ assert !d.valid?
+ assert_equal 100, d.salary
+ assert_equal "100,000", d.salary_before_type_cast
+ end
+
+ def test_validates_acceptance_of_with_custom_error_using_quotes
+ Developer.validates_acceptance_of :salary, :message=> "This string contains 'single' and \"double\" quotes"
+ d = Developer.new
+ d.salary = "0"
+ assert !d.valid?
+ assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:salary).last
+ end
+
+ def test_validates_confirmation_of_with_custom_error_using_quotes
+ Developer.validates_confirmation_of :name, :message=> "confirm 'single' and \"double\" quotes"
+ d = Developer.new
+ d.name = "John"
+ d.name_confirmation = "Johnny"
+ assert !d.valid?
+ assert_equal "confirm 'single' and \"double\" quotes", d.errors.on(:name)
+ end
+
+ def test_validates_format_of_with_custom_error_using_quotes
+ Developer.validates_format_of :name, :with => /^(A-Z*)$/, :message=> "format 'single' and \"double\" quotes"
+ d = Developer.new
+ d.name = d.name_confirmation = "John 32"
+ assert !d.valid?
+ assert_equal "format 'single' and \"double\" quotes", d.errors.on(:name)
+ end
+
+ def test_validates_inclusion_of_with_custom_error_using_quotes
+ Developer.validates_inclusion_of :salary, :in => 1000..80000, :message=> "This string contains 'single' and \"double\" quotes"
+ d = Developer.new
+ d.salary = "90,000"
+ assert !d.valid?
+ assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:salary).last
+ end
+
+ def test_validates_length_of_with_custom_too_long_using_quotes
+ Developer.validates_length_of :name, :maximum => 4, :too_long=> "This string contains 'single' and \"double\" quotes"
+ d = Developer.new
+ d.name = "Jeffrey"
+ assert !d.valid?
+ assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name).last
+ end
+
+ def test_validates_length_of_with_custom_too_short_using_quotes
+ Developer.validates_length_of :name, :minimum => 4, :too_short=> "This string contains 'single' and \"double\" quotes"
+ d = Developer.new
+ d.name = "Joe"
+ assert !d.valid?
+ assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name).last
+ end
+
+ def test_validates_length_of_with_custom_message_using_quotes
+ Developer.validates_length_of :name, :minimum => 4, :message=> "This string contains 'single' and \"double\" quotes"
+ d = Developer.new
+ d.name = "Joe"
+ assert !d.valid?
+ assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name).last
+ end
+
+ def test_validates_presence_of_with_custom_message_using_quotes
+ Developer.validates_presence_of :non_existent, :message=> "This string contains 'single' and \"double\" quotes"
+ d = Developer.new
+ d.name = "Joe"
+ assert !d.valid?
+ assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:non_existent)
+ end
+
+ def test_validates_uniqueness_of_with_custom_message_using_quotes
+ Developer.validates_uniqueness_of :name, :message=> "This string contains 'single' and \"double\" quotes"
+ d = Developer.new
+ d.name = "David"
+ assert !d.valid?
+ assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name).last
+ end
+
+ def test_validates_associated_with_custom_message_using_quotes
+ Reply.validates_associated :topic, :message=> "This string contains 'single' and \"double\" quotes"
+ Topic.validates_presence_of :content
+ r = Reply.create("title" => "A reply", "content" => "with content!")
+ r.topic = Topic.create("title" => "uhohuhoh")
+ assert !r.valid?
+ assert_equal "This string contains 'single' and \"double\" quotes", r.errors.on(:topic).last
+ end
+
+ def test_if_validation_using_method_true
+ # When the method returns true
+ Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :if => :condition_is_true )
+ t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "hoo 5", t.errors["title"]
+ end
+
+ def test_unless_validation_using_method_true
+ # When the method returns true
+ Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :unless => :condition_is_true )
+ t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
+ assert t.valid?
+ assert !t.errors.on(:title)
+ end
+
+ def test_if_validation_using_method_false
+ # When the method returns false
+ Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :if => :condition_is_true_but_its_not )
+ t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
+ assert t.valid?
+ assert !t.errors.on(:title)
+ end
+
+ def test_unless_validation_using_method_false
+ # When the method returns false
+ Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :unless => :condition_is_true_but_its_not )
+ t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "hoo 5", t.errors["title"]
+ end
+
+ def test_if_validation_using_string_true
+ # When the evaluated string returns true
+ Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :if => "a = 1; a == 1" )
+ t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "hoo 5", t.errors["title"]
+ end
+
+ def test_unless_validation_using_string_true
+ # When the evaluated string returns true
+ Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :unless => "a = 1; a == 1" )
+ t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
+ assert t.valid?
+ assert !t.errors.on(:title)
+ end
+
+ def test_if_validation_using_string_false
+ # When the evaluated string returns false
+ Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :if => "false")
+ t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
+ assert t.valid?
+ assert !t.errors.on(:title)
+ end
+
+ def test_unless_validation_using_string_false
+ # When the evaluated string returns false
+ Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :unless => "false")
+ t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "hoo 5", t.errors["title"]
+ end
+
+ def test_if_validation_using_block_true
+ # When the block returns true
+ Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d",
+ :if => Proc.new { |r| r.content.size > 4 } )
+ t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "hoo 5", t.errors["title"]
+ end
+
+ def test_unless_validation_using_block_true
+ # When the block returns true
+ Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d",
+ :unless => Proc.new { |r| r.content.size > 4 } )
+ t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
+ assert t.valid?
+ assert !t.errors.on(:title)
+ end
+
+ def test_if_validation_using_block_false
+ # When the block returns false
+ Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d",
+ :if => Proc.new { |r| r.title != "uhohuhoh"} )
+ t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
+ assert t.valid?
+ assert !t.errors.on(:title)
+ end
+
+ def test_unless_validation_using_block_false
+ # When the block returns false
+ Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d",
+ :unless => Proc.new { |r| r.title != "uhohuhoh"} )
+ t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "hoo 5", t.errors["title"]
+ end
+
+ def test_validates_associated_missing
+ Reply.validates_presence_of(:topic)
+ r = Reply.create("title" => "A reply", "content" => "with content!")
+ assert !r.valid?
+ assert r.errors.on(:topic)
+
+ r.topic = Topic.find :first
+ assert r.valid?
+ end
+
+ def test_errors_to_xml
+ r = Reply.new :title => "Wrong Create"
+ assert !r.valid?
+ xml = r.errors.to_xml(:skip_instruct => true)
+ assert_equal "<errors>", xml.first(8)
+ assert xml.include?("<error>Title is Wrong Create</error>")
+ assert xml.include?("<error>Content Empty</error>")
+ end
+
+ def test_validation_order
+ Topic.validates_presence_of :title
+ Topic.validates_length_of :title, :minimum => 2
+
+ t = Topic.new("title" => "")
+ assert !t.valid?
+ assert_equal "can't be blank", t.errors.on("title").first
+ end
+
+ # previous implementation of validates_presence_of eval'd the
+ # string with the wrong binding, this regression test is to
+ # ensure that it works correctly
+ def test_validation_with_if_as_string
+ Topic.validates_presence_of(:title)
+ Topic.validates_presence_of(:author_name, :if => "title.to_s.match('important')")
+
+ t = Topic.new
+ assert !t.valid?, "A topic without a title should not be valid"
+ assert !t.errors.invalid?("author_name"), "A topic without an 'important' title should not require an author"
+
+ t.title = "Just a title"
+ assert t.valid?, "A topic with a basic title should be valid"
+
+ t.title = "A very important title"
+ assert !t.valid?, "A topic with an important title, but without an author, should not be valid"
+ assert t.errors.invalid?("author_name"), "A topic with an 'important' title should require an author"
+
+ t.author_name = "Hubert J. Farnsworth"
+ assert t.valid?, "A topic with an important title and author should be valid"
+ end
+
+ private
+ def with_kcode(kcode)
+ if RUBY_VERSION < '1.9'
+ orig_kcode, $KCODE = $KCODE, kcode
+ begin
+ yield
+ ensure
+ $KCODE = orig_kcode
+ end
+ else
+ yield
+ end
+ end
+end
+
+
+class ValidatesNumericalityTest < ActiveSupport::TestCase
+ NIL = [nil]
+ BLANK = ["", " ", " \t \r \n"]
+ BIGDECIMAL_STRINGS = %w(12345678901234567890.1234567890) # 30 significent digits
+ FLOAT_STRINGS = %w(0.0 +0.0 -0.0 10.0 10.5 -10.5 -0.0001 -090.1 90.1e1 -90.1e5 -90.1e-5 90e-5)
+ INTEGER_STRINGS = %w(0 +0 -0 10 +10 -10 0090 -090)
+ FLOATS = [0.0, 10.0, 10.5, -10.5, -0.0001] + FLOAT_STRINGS
+ INTEGERS = [0, 10, -10] + INTEGER_STRINGS
+ BIGDECIMAL = BIGDECIMAL_STRINGS.collect! { |bd| BigDecimal.new(bd) }
+ JUNK = ["not a number", "42 not a number", "0xdeadbeef", "00-1", "--3", "+-3", "+3-1", "-+019.0", "12.12.13.12", "123\nnot a number"]
+
+ def setup
+ Topic.write_inheritable_attribute(:validate, nil)
+ Topic.write_inheritable_attribute(:validate_on_create, nil)
+ Topic.write_inheritable_attribute(:validate_on_update, nil)
+ end
+
+ def test_default_validates_numericality_of
+ Topic.validates_numericality_of :approved
+
+ invalid!(NIL + BLANK + JUNK)
+ valid!(FLOATS + INTEGERS + BIGDECIMAL)
+ end
+
+ def test_validates_numericality_of_with_nil_allowed
+ Topic.validates_numericality_of :approved, :allow_nil => true
+
+ invalid!(BLANK + JUNK)
+ valid!(NIL + FLOATS + INTEGERS + BIGDECIMAL)
+ end
+
+ def test_validates_numericality_of_with_integer_only
+ Topic.validates_numericality_of :approved, :only_integer => true
+
+ invalid!(NIL + BLANK + JUNK + FLOATS + BIGDECIMAL)
+ valid!(INTEGERS)
+ end
+
+ def test_validates_numericality_of_with_integer_only_and_nil_allowed
+ Topic.validates_numericality_of :approved, :only_integer => true, :allow_nil => true
+
+ invalid!(BLANK + JUNK + FLOATS + BIGDECIMAL)
+ valid!(NIL + INTEGERS)
+ end
+
+ def test_validates_numericality_with_greater_than
+ Topic.validates_numericality_of :approved, :greater_than => 10
+
+ invalid!([-10, 10], 'must be greater than 10')
+ valid!([11])
+ end
+
+ def test_validates_numericality_with_greater_than_or_equal
+ Topic.validates_numericality_of :approved, :greater_than_or_equal_to => 10
+
+ invalid!([-9, 9], 'must be greater than or equal to 10')
+ valid!([10])
+ end
+
+ def test_validates_numericality_with_equal_to
+ Topic.validates_numericality_of :approved, :equal_to => 10
+
+ invalid!([-10, 11], 'must be equal to 10')
+ valid!([10])
+ end
+
+ def test_validates_numericality_with_less_than
+ Topic.validates_numericality_of :approved, :less_than => 10
+
+ invalid!([10], 'must be less than 10')
+ valid!([-9, 9])
+ end
+
+ def test_validates_numericality_with_less_than_or_equal_to
+ Topic.validates_numericality_of :approved, :less_than_or_equal_to => 10
+
+ invalid!([11], 'must be less than or equal to 10')
+ valid!([-10, 10])
+ end
+
+ def test_validates_numericality_with_odd
+ Topic.validates_numericality_of :approved, :odd => true
+
+ invalid!([-2, 2], 'must be odd')
+ valid!([-1, 1])
+ end
+
+ def test_validates_numericality_with_even
+ Topic.validates_numericality_of :approved, :even => true
+
+ invalid!([-1, 1], 'must be even')
+ valid!([-2, 2])
+ end
+
+ def test_validates_numericality_with_greater_than_less_than_and_even
+ Topic.validates_numericality_of :approved, :greater_than => 1, :less_than => 4, :even => true
+
+ invalid!([1, 3, 4])
+ valid!([2])
+ end
+
+ private
+ def invalid!(values, error=nil)
+ with_each_topic_approved_value(values) do |topic, value|
+ assert !topic.valid?, "#{value.inspect} not rejected as a number"
+ assert topic.errors.on(:approved)
+ assert_equal error, topic.errors.on(:approved) if error
+ end
+ end
+
+ def valid!(values)
+ with_each_topic_approved_value(values) do |topic, value|
+ assert topic.valid?, "#{value.inspect} not accepted as a number"
+ end
+ end
+
+ def with_each_topic_approved_value(values)
+ topic = Topic.new("title" => "numeric test", "content" => "whatever")
+ values.each do |value|
+ topic.approved = value
+ yield topic, value
+ end
+ end
+end
diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb
new file mode 100644
index 0000000000..2db75fad46
--- /dev/null
+++ b/activerecord/test/cases/xml_serialization_test.rb
@@ -0,0 +1,202 @@
+require 'abstract_unit'
+require 'fixtures/contact'
+require 'fixtures/post'
+require 'fixtures/author'
+require 'fixtures/tagging'
+require 'fixtures/comment'
+
+class XmlSerializationTest < ActiveSupport::TestCase
+ def test_should_serialize_default_root
+ @xml = Contact.new.to_xml
+ assert_match %r{^<contact>}, @xml
+ assert_match %r{</contact>$}, @xml
+ end
+
+ def test_should_serialize_default_root_with_namespace
+ @xml = Contact.new.to_xml :namespace=>"http://xml.rubyonrails.org/contact"
+ assert_match %r{^<contact xmlns="http://xml.rubyonrails.org/contact">}, @xml
+ assert_match %r{</contact>$}, @xml
+ end
+
+ def test_should_serialize_custom_root
+ @xml = Contact.new.to_xml :root => 'xml_contact'
+ assert_match %r{^<xml-contact>}, @xml
+ assert_match %r{</xml-contact>$}, @xml
+ end
+
+ def test_should_allow_undasherized_tags
+ @xml = Contact.new.to_xml :root => 'xml_contact', :dasherize => false
+ assert_match %r{^<xml_contact>}, @xml
+ assert_match %r{</xml_contact>$}, @xml
+ assert_match %r{<created_at}, @xml
+ end
+
+ def test_should_include_yielded_additions
+ @xml = Contact.new.to_xml do |xml|
+ xml.creator "David"
+ end
+
+ assert_match %r{<creator>David</creator>}, @xml
+ end
+end
+
+class DefaultXmlSerializationTest < ActiveSupport::TestCase
+ def setup
+ @xml = Contact.new(:name => 'aaron stack', :age => 25, :avatar => 'binarydata', :created_at => Time.utc(2006, 8, 1), :awesome => false, :preferences => { :gem => 'ruby' }).to_xml
+ end
+
+ def test_should_serialize_string
+ assert_match %r{<name>aaron stack</name>}, @xml
+ end
+
+ def test_should_serialize_integer
+ assert_match %r{<age type="integer">25</age>}, @xml
+ end
+
+ def test_should_serialize_binary
+ assert_match %r{YmluYXJ5ZGF0YQ==\n</avatar>}, @xml
+ assert_match %r{<avatar(.*)(type="binary")}, @xml
+ assert_match %r{<avatar(.*)(encoding="base64")}, @xml
+ end
+
+ def test_should_serialize_datetime
+ assert_match %r{<created-at type=\"datetime\">2006-08-01T00:00:00Z</created-at>}, @xml
+ end
+
+ def test_should_serialize_boolean
+ assert_match %r{<awesome type=\"boolean\">false</awesome>}, @xml
+ end
+
+ def test_should_serialize_yaml
+ assert_match %r{<preferences type=\"yaml\">--- \n:gem: ruby\n</preferences>}, @xml
+ end
+end
+
+class NilXmlSerializationTest < ActiveSupport::TestCase
+ def setup
+ @xml = Contact.new.to_xml(:root => 'xml_contact')
+ end
+
+ def test_should_serialize_string
+ assert_match %r{<name nil="true"></name>}, @xml
+ end
+
+ def test_should_serialize_integer
+ assert %r{<age (.*)></age>}.match(@xml)
+ attributes = $1
+ assert_match %r{nil="true"}, attributes
+ assert_match %r{type="integer"}, attributes
+ end
+
+ def test_should_serialize_binary
+ assert %r{<avatar (.*)></avatar>}.match(@xml)
+ attributes = $1
+ assert_match %r{type="binary"}, attributes
+ assert_match %r{encoding="base64"}, attributes
+ assert_match %r{nil="true"}, attributes
+ end
+
+ def test_should_serialize_datetime
+ assert %r{<created-at (.*)></created-at>}.match(@xml)
+ attributes = $1
+ assert_match %r{nil="true"}, attributes
+ assert_match %r{type="datetime"}, attributes
+ end
+
+ def test_should_serialize_boolean
+ assert %r{<awesome (.*)></awesome>}.match(@xml)
+ attributes = $1
+ assert_match %r{type="boolean"}, attributes
+ assert_match %r{nil="true"}, attributes
+ end
+
+ def test_should_serialize_yaml
+ assert %r{<preferences(.*)></preferences>}.match(@xml)
+ attributes = $1
+ assert_match %r{type="yaml"}, attributes
+ assert_match %r{nil="true"}, attributes
+ end
+end
+
+class DatabaseConnectedXmlSerializationTest < ActiveSupport::TestCase
+ fixtures :authors, :posts
+ # to_xml used to mess with the hash the user provided which
+ # caused the builder to be reused. This meant the document kept
+ # getting appended to.
+ def test_passing_hash_shouldnt_reuse_builder
+ options = {:include=>:posts}
+ david = authors(:david)
+ first_xml_size = david.to_xml(options).size
+ second_xml_size = david.to_xml(options).size
+ assert_equal first_xml_size, second_xml_size
+ end
+
+ def test_include_uses_association_name
+ xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0
+ assert_match %r{<hello-posts type="array">}, xml
+ assert_match %r{<hello-post type="Post">}, xml
+ assert_match %r{<hello-post type="StiPost">}, xml
+ end
+
+ def test_methods_are_called_on_object
+ xml = authors(:david).to_xml :methods => :label, :indent => 0
+ assert_match %r{<label>.*</label>}, xml
+ end
+
+ def test_should_not_call_methods_on_associations_that_dont_respond
+ xml = authors(:david).to_xml :include=>:hello_posts, :methods => :label, :indent => 2
+ assert !authors(:david).hello_posts.first.respond_to?(:label)
+ assert_match %r{^ <label>.*</label>}, xml
+ assert_no_match %r{^ <label>}, xml
+ end
+
+ def test_procs_are_called_on_object
+ proc = Proc.new { |options| options[:builder].tag!('nationality', 'Danish') }
+ xml = authors(:david).to_xml(:procs => [ proc ])
+ assert_match %r{<nationality>Danish</nationality>}, xml
+ end
+
+ def test_top_level_procs_arent_applied_to_associations
+ author_proc = Proc.new { |options| options[:builder].tag!('nationality', 'Danish') }
+ xml = authors(:david).to_xml(:procs => [ author_proc ], :include => :posts, :indent => 2)
+
+ assert_match %r{^ <nationality>Danish</nationality>}, xml
+ assert_no_match %r{^ {6}<nationality>Danish</nationality>}, xml
+ end
+
+ def test_procs_on_included_associations_are_called
+ posts_proc = Proc.new { |options| options[:builder].tag!('copyright', 'DHH') }
+ xml = authors(:david).to_xml(
+ :indent => 2,
+ :include => {
+ :posts => { :procs => [ posts_proc ] }
+ }
+ )
+
+ assert_no_match %r{^ <copyright>DHH</copyright>}, xml
+ assert_match %r{^ {6}<copyright>DHH</copyright>}, xml
+ end
+
+ def test_should_include_empty_has_many_as_empty_array
+ authors(:david).posts.delete_all
+ xml = authors(:david).to_xml :include=>:posts, :indent => 2
+
+ assert_equal [], Hash.from_xml(xml)['author']['posts']
+ assert_match %r{^ <posts type="array"/>}, xml
+ end
+
+ def test_should_has_many_array_elements_should_include_type_when_different_from_guessed_value
+ xml = authors(:david).to_xml :include=>:posts_with_comments, :indent => 2
+
+ assert Hash.from_xml(xml)
+ assert_match %r{^ <posts-with-comments type="array">}, xml
+ assert_match %r{^ <posts-with-comment type="Post">}, xml
+ assert_match %r{^ <posts-with-comment type="StiPost">}, xml
+
+ types = Hash.from_xml(xml)['author']['posts_with_comments'].collect {|t| t['type'] }
+ assert types.include?('SpecialPost')
+ assert types.include?('Post')
+ assert types.include?('StiPost')
+ end
+
+end \ No newline at end of file