diff options
Diffstat (limited to 'activerecord/test/cases/associations/has_one_associations_test.rb')
-rw-r--r-- | activerecord/test/cases/associations/has_one_associations_test.rb | 786 |
1 files changed, 786 insertions, 0 deletions
diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb new file mode 100644 index 0000000000..bf574f6637 --- /dev/null +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -0,0 +1,786 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/developer" +require "models/computer" +require "models/project" +require "models/company" +require "models/ship" +require "models/pirate" +require "models/car" +require "models/bulb" +require "models/author" +require "models/image" +require "models/post" + +class HasOneAssociationsTest < ActiveRecord::TestCase + self.use_transactional_tests = false unless supports_savepoints? + fixtures :accounts, :companies, :developers, :projects, :developers_projects, :ships, :pirates, :authors, :author_addresses + + def setup + Account.destroyed_account_ids.clear + end + + def test_has_one + firm = companies(:first_firm) + first_account = Account.find(1) + assert_sql(/LIMIT|ROWNUM <=|FETCH FIRST/) do + assert_equal first_account, firm.account + assert_equal first_account.credit_limit, firm.account.credit_limit + end + end + + def test_has_one_does_not_use_order_by + ActiveRecord::SQLCounter.clear_log + companies(:first_firm).account + ensure + log_all = ActiveRecord::SQLCounter.log_all + assert log_all.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{log_all}" + end + + def test_has_one_cache_nils + firm = companies(:another_firm) + assert_queries(1) { assert_nil firm.account } + assert_no_queries { assert_nil firm.account } + + firms = Firm.includes(:account).to_a + assert_no_queries { firms.each(&:account) } + end + + def test_with_select + assert_equal Firm.find(1).account_with_select.attributes.size, 2 + assert_equal Firm.all.merge!(includes: :account_with_select).find(1).account_with_select.attributes.size, 2 + end + + def test_finding_using_primary_key + firm = companies(:first_firm) + assert_equal Account.find_by_firm_id(firm.id), firm.account + firm.firm_id = companies(:rails_core).id + assert_equal accounts(:rails_core_account), firm.account_using_primary_key + end + + def test_update_with_foreign_and_primary_keys + firm = companies(:first_firm) + account = firm.account_using_foreign_and_primary_keys + assert_equal Account.find_by_firm_name(firm.name), account + firm.save + firm.reload + assert_equal account, firm.account_using_foreign_and_primary_keys + 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_type_mismatch + assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = 1 } + assert_raise(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_raise(ActiveRecord::RecordNotFound) { Account.find(old_account_id) } + end + + def test_nullification_on_association_change + firm = companies(:rails_core) + old_account_id = firm.account.id + firm.account = Account.new(credit_limit: 5) + # account is dependent with nullify, therefore its firm_id should be nil + assert_nil Account.find(old_account_id).firm_id + end + + def test_nullification_on_destroyed_association + developer = Developer.create!(name: "Someone") + ship = Ship.create!(name: "Planet Caravan", developer: developer) + ship.destroy + assert_not_predicate ship, :persisted? + assert_not_predicate developer, :persisted? + end + + def test_natural_assignment_to_nil_after_destroy + firm = companies(:rails_core) + old_account_id = firm.account.id + firm.account.destroy + firm.account = nil + assert_nil companies(:rails_core).account + assert_raise(ActiveRecord::RecordNotFound) { Account.find(old_account_id) } + end + + def test_association_change_calls_delete + companies(:first_firm).deletable_account = Account.new(credit_limit: 5) + assert_equal [], Account.destroyed_account_ids[companies(:first_firm).id] + end + + def test_association_change_calls_destroy + companies(:first_firm).account = Account.new(credit_limit: 5) + assert_equal [companies(:first_firm).id], Account.destroyed_account_ids[companies(:first_firm).id] + end + + def test_natural_assignment_to_already_associated_record + company = companies(:first_firm) + account = accounts(:signals37) + assert_equal company.account, account + company.account = account + company.reload + account.reload + assert_equal company.account, account + end + + def test_dependence + num_accounts = Account.count + + firm = Firm.find(1) + assert_not_nil firm.account + 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_not_nil firm.account + 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_restrict_with_exception + firm = RestrictedWithExceptionFirm.create!(name: "restrict") + firm.create_account(credit_limit: 10) + + assert_not_nil firm.account + + assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy } + assert RestrictedWithExceptionFirm.exists?(name: "restrict") + assert_predicate firm.account, :present? + end + + def test_restrict_with_error + firm = RestrictedWithErrorFirm.create!(name: "restrict") + firm.create_account(credit_limit: 10) + + assert_not_nil firm.account + + firm.destroy + + assert_not_empty firm.errors + assert_equal "Cannot delete record because a dependent account exists", firm.errors[:base].first + assert RestrictedWithErrorFirm.exists?(name: "restrict") + assert_predicate firm.account, :present? + end + + def test_restrict_with_error_with_locale + I18n.backend = I18n::Backend::Simple.new + I18n.backend.store_translations "en", activerecord: { attributes: { restricted_with_error_firm: { account: "firm account" } } } + firm = RestrictedWithErrorFirm.create!(name: "restrict") + firm.create_account(credit_limit: 10) + + assert_not_nil firm.account + + firm.destroy + + assert_not_empty firm.errors + assert_equal "Cannot delete record because a dependent firm account exists", firm.errors[:base].first + assert RestrictedWithErrorFirm.exists?(name: "restrict") + assert_predicate firm.account, :present? + ensure + I18n.backend.reload! + end + + def test_successful_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_build_association_dont_create_transaction + # Load schema information so we don't query below if running just this test. + Account.define_attribute_methods + + firm = Firm.new + assert_no_queries do + firm.build_account + end + end + + def test_building_the_associated_object_with_implicit_sti_base_class + firm = DependentFirm.new + company = firm.build_company + assert_kind_of Company, company, "Expected #{company.class} to be a Company" + end + + def test_building_the_associated_object_with_explicit_sti_base_class + firm = DependentFirm.new + company = firm.build_company(type: "Company") + assert_kind_of Company, company, "Expected #{company.class} to be a Company" + end + + def test_building_the_associated_object_with_sti_subclass + firm = DependentFirm.new + company = firm.build_company(type: "Client") + assert_kind_of Client, company, "Expected #{company.class} to be a Client" + end + + def test_building_the_associated_object_with_an_invalid_type + firm = DependentFirm.new + assert_raise(ActiveRecord::SubclassNotFound) { firm.build_company(type: "Invalid") } + end + + def test_building_the_associated_object_with_an_unrelated_type + firm = DependentFirm.new + assert_raise(ActiveRecord::SubclassNotFound) { firm.build_company(type: "Account") } + end + + def test_build_and_create_should_not_happen_within_scope + pirate = pirates(:blackbeard) + scope = pirate.association(:foo_bulb).scope.where_values_hash + + bulb = pirate.build_foo_bulb + assert_not_equal scope, bulb.scope_after_initialize.where_values_hash + + bulb = pirate.create_foo_bulb + assert_not_equal scope, bulb.scope_after_initialize.where_values_hash + + bulb = pirate.create_foo_bulb! + assert_not_equal scope, bulb.scope_after_initialize.where_values_hash + 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_create_association_with_bang + firm = Firm.create(name: "GlobalMegaCorp") + account = firm.create_account!(credit_limit: 1000) + assert_equal account, firm.reload.account + end + + def test_create_association_with_bang_failing + firm = Firm.create(name: "GlobalMegaCorp") + assert_raise ActiveRecord::RecordInvalid do + firm.create_account! + end + account = firm.account + assert_not_nil account + account.credit_limit = 5 + account.save + assert_equal account, firm.reload.account + end + + def test_create_with_inexistent_foreign_key_failing + firm = Firm.create(name: "GlobalMegaCorp") + + assert_raises(ActiveRecord::UnknownAttributeError) do + firm.create_account_with_inexistent_foreign_key + end + end + + def test_create_when_parent_is_new_raises + firm = Firm.new + error = assert_raise(ActiveRecord::RecordNotSaved) do + firm.create_account + end + + assert_equal "You cannot call create unless the parent is saved", error.message + end + + def test_reload_association + odegy = companies(:odegy) + + assert_equal 53, odegy.account.credit_limit + Account.where(id: odegy.account.id).update_all(credit_limit: 80) + assert_equal 53, odegy.account.credit_limit + + assert_equal 80, odegy.reload_account.credit_limit + end + + def test_reload_association_with_query_cache + odegy_id = companies(:odegy).id + + connection = ActiveRecord::Base.connection + connection.enable_query_cache! + connection.clear_query_cache + + # Populate the cache with a query + odegy = Company.find(odegy_id) + # Populate the cache with a second query + odegy.account + + assert_equal 2, connection.query_cache.size + + # Clear the cache and fetch the account again, populating the cache with a query + assert_queries(1) { odegy.reload_account } + + # This query is not cached anymore, so it should make a real SQL query + assert_queries(1) { Company.find(odegy_id) } + ensure + ActiveRecord::Base.connection.disable_query_cache! + 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_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_nil firm.account + firm.destroy + end + + def test_dependence_with_missing_association_and_nullify + Account.destroy_all + firm = DependentFirm.first + assert_nil firm.account + firm.destroy + end + + def test_finding_with_interpolated_condition + firm = Firm.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_predicate a, :persisted? + assert_equal a, firm.account + assert_equal a, firm.account + firm.association(:account).reload + assert_equal a, firm.account + 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 + + def test_cant_save_readonly_association + assert_raise(ActiveRecord::ReadOnlyRecord) { companies(:first_firm).readonly_account.save! } + assert_predicate companies(:first_firm).readonly_account, :readonly? + end + + def test_has_one_proxy_should_not_respond_to_private_methods + assert_raise(NoMethodError) { accounts(:signals37).private_method } + assert_raise(NoMethodError) { companies(:first_firm).account.private_method } + end + + def test_has_one_proxy_should_respond_to_private_methods_via_send + accounts(:signals37).send(:private_method) + companies(:first_firm).account.send(:private_method) + end + + def test_save_of_record_with_loaded_has_one + @firm = companies(:first_firm) + assert_not_nil @firm.account + + assert_nothing_raised do + Firm.find(@firm.id).save! + Firm.all.merge!(includes: :account).find(@firm.id).save! + end + + @firm.account.destroy + + assert_nothing_raised do + Firm.find(@firm.id).save! + Firm.all.merge!(includes: :account).find(@firm.id).save! + end + end + + def test_build_respects_hash_condition + account = companies(:first_firm).build_account_limit_500_with_hash_conditions + assert account.save + assert_equal 500, account.credit_limit + end + + def test_create_respects_hash_condition + account = companies(:first_firm).create_account_limit_500_with_hash_conditions + assert_predicate account, :persisted? + assert_equal 500, account.credit_limit + end + + def test_attributes_are_being_set_when_initialized_from_has_one_association_with_where_clause + new_account = companies(:first_firm).build_account(firm_name: "Account") + assert_equal new_account.firm_name, "Account" + end + + def test_creation_failure_without_dependent_option + pirate = pirates(:blackbeard) + orig_ship = pirate.ship + + assert_equal ships(:black_pearl), orig_ship + new_ship = pirate.create_ship + assert_not_equal ships(:black_pearl), new_ship + assert_equal new_ship, pirate.ship + assert_predicate new_ship, :new_record? + assert_nil orig_ship.pirate_id + assert_not orig_ship.changed? # check it was saved + end + + def test_creation_failure_with_dependent_option + pirate = pirates(:blackbeard).becomes(DestructivePirate) + orig_ship = pirate.dependent_ship + + new_ship = pirate.create_dependent_ship + assert_predicate new_ship, :new_record? + assert_predicate orig_ship, :destroyed? + end + + def test_creation_failure_due_to_new_record_should_raise_error + pirate = pirates(:redbeard) + new_ship = Ship.new + + error = assert_raise(ActiveRecord::RecordNotSaved) do + pirate.ship = new_ship + end + + assert_equal "Failed to save the new associated ship.", error.message + assert_nil pirate.ship + assert_nil new_ship.pirate_id + end + + def test_replacement_failure_due_to_existing_record_should_raise_error + pirate = pirates(:blackbeard) + pirate.ship.name = nil + + assert_not_predicate pirate.ship, :valid? + error = assert_raise(ActiveRecord::RecordNotSaved) do + pirate.ship = ships(:interceptor) + end + + assert_equal ships(:black_pearl), pirate.ship + assert_equal pirate.id, pirate.ship.pirate_id + assert_equal "Failed to remove the existing associated ship. " \ + "The record failed to save after its foreign key was set to nil.", error.message + end + + def test_replacement_failure_due_to_new_record_should_raise_error + pirate = pirates(:blackbeard) + new_ship = Ship.new + + error = assert_raise(ActiveRecord::RecordNotSaved) do + pirate.ship = new_ship + end + + assert_equal "Failed to save the new associated ship.", error.message + assert_equal ships(:black_pearl), pirate.ship + assert_equal pirate.id, pirate.ship.pirate_id + assert_equal pirate.id, ships(:black_pearl).reload.pirate_id + assert_nil new_ship.pirate_id + end + + def test_association_keys_bypass_attribute_protection + car = Car.create(name: "honda") + + bulb = car.build_bulb + assert_equal car.id, bulb.car_id + + bulb = car.build_bulb car_id: car.id + 1 + assert_equal car.id, bulb.car_id + + bulb = car.create_bulb + assert_equal car.id, bulb.car_id + + bulb = car.create_bulb car_id: car.id + 1 + assert_equal car.id, bulb.car_id + end + + def test_association_protect_foreign_key + pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?") + + ship = pirate.build_ship + assert_equal pirate.id, ship.pirate_id + + ship = pirate.build_ship pirate_id: pirate.id + 1 + assert_equal pirate.id, ship.pirate_id + + ship = pirate.create_ship + assert_equal pirate.id, ship.pirate_id + + ship = pirate.create_ship pirate_id: pirate.id + 1 + assert_equal pirate.id, ship.pirate_id + end + + def test_build_with_block + car = Car.create(name: "honda") + + bulb = car.build_bulb { |b| b.color = "Red" } + assert_equal "RED!", bulb.color + end + + def test_create_with_block + car = Car.create(name: "honda") + + bulb = car.create_bulb { |b| b.color = "Red" } + assert_equal "RED!", bulb.color + end + + def test_create_bang_with_block + car = Car.create(name: "honda") + + bulb = car.create_bulb! { |b| b.color = "Red" } + assert_equal "RED!", bulb.color + end + + def test_association_attributes_are_available_to_after_initialize + car = Car.create(name: "honda") + bulb = car.create_bulb + + assert_equal car.id, bulb.attributes_after_initialize["car_id"] + end + + def test_has_one_transaction + company = companies(:first_firm) + account = Account.find(1) + + company.account # force loading + assert_no_queries { company.account = account } + + company.account = nil + assert_no_queries { company.account = nil } + account = Account.find(2) + assert_queries { company.account = account } + + assert_no_queries { Firm.new.account = account } + end + + def test_has_one_assignment_dont_trigger_save_on_change_of_same_object + pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?") + ship = pirate.build_ship(name: "old name") + ship.save! + + ship.name = "new name" + assert_predicate ship, :changed? + assert_queries(1) do + # One query for updating name, not triggering query for updating pirate_id + pirate.ship = ship + end + + assert_equal "new name", pirate.ship.reload.name + end + + def test_has_one_assignment_triggers_save_on_change_on_replacing_object + pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?") + ship = pirate.build_ship(name: "old name") + ship.save! + + new_ship = Ship.create(name: "new name") + assert_queries(2) do + # One query to nullify the old ship, one query to update the new ship + pirate.ship = new_ship + end + + assert_equal "new name", pirate.ship.reload.name + end + + def test_has_one_autosave_with_primary_key_manually_set + post = Post.create(id: 1234, title: "Some title", body: "Some content") + author = Author.new(id: 33, name: "Hank Moody") + + author.post = post + author.save + author.reload + + assert_not_nil author.post + assert_equal author.post, post + end + + def test_has_one_loading_for_new_record + post = Post.create!(author_id: 42, title: "foo", body: "bar") + author = Author.new(id: 42) + assert_equal post, author.post + end + + def test_has_one_relationship_cannot_have_a_counter_cache + assert_raise(ArgumentError) do + Class.new(ActiveRecord::Base) do + has_one :thing, counter_cache: true + end + end + end + + def test_with_polymorphic_has_one_with_custom_columns_name + post = Post.create! title: "foo", body: "bar" + image = Image.create! + + post.main_image = image + post.reload + + assert_equal image, post.main_image + end + + test "dangerous association name raises ArgumentError" do + [:errors, "errors", :save, "save"].each do |name| + assert_raises(ArgumentError, "Association #{name} should not be allowed") do + Class.new(ActiveRecord::Base) do + has_one name + end + end + end + end + + class SpecialBook < ActiveRecord::Base + self.table_name = "books" + belongs_to :author, class_name: "SpecialAuthor" + has_one :subscription, class_name: "SpecialSupscription", foreign_key: "subscriber_id" + + enum status: [:proposed, :written, :published] + end + + class SpecialAuthor < ActiveRecord::Base + self.table_name = "authors" + has_one :book, class_name: "SpecialBook", foreign_key: "author_id" + end + + class SpecialSupscription < ActiveRecord::Base + self.table_name = "subscriptions" + belongs_to :book, class_name: "SpecialBook" + end + + def test_association_enum_works_properly + author = SpecialAuthor.create!(name: "Test") + book = SpecialBook.create!(status: "published") + author.book = book + + assert_equal "published", book.status + assert_not_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" }).count + end + + def test_association_enum_works_properly_with_nested_join + author = SpecialAuthor.create!(name: "Test") + book = SpecialBook.create!(status: "published") + author.book = book + + where_clause = { books: { subscriptions: { subscriber_id: nil } } } + assert_nothing_raised do + SpecialAuthor.joins(book: :subscription).where.not(where_clause) + end + end + + class DestroyByParentBook < ActiveRecord::Base + self.table_name = "books" + belongs_to :author, class_name: "DestroyByParentAuthor" + before_destroy :dont, unless: :destroyed_by_association + + def dont + throw(:abort) + end + end + + class DestroyByParentAuthor < ActiveRecord::Base + self.table_name = "authors" + has_one :book, class_name: "DestroyByParentBook", foreign_key: "author_id", dependent: :destroy + end + + test "destroyed_by_association set in child destroy callback on parent destroy" do + author = DestroyByParentAuthor.create!(name: "Test") + book = DestroyByParentBook.create!(author: author) + + author.destroy + + assert_not DestroyByParentBook.exists?(book.id) + end + + test "destroyed_by_association set in child destroy callback on replace" do + author = DestroyByParentAuthor.create!(name: "Test") + book = DestroyByParentBook.create!(author: author) + + author.book = DestroyByParentBook.create! + author.save! + + assert_not DestroyByParentBook.exists?(book.id) + end + + class UndestroyableBook < ActiveRecord::Base + self.table_name = "books" + belongs_to :author, class_name: "DestroyableAuthor" + before_destroy :dont + + def dont + throw(:abort) + end + end + + class DestroyableAuthor < ActiveRecord::Base + self.table_name = "authors" + has_one :book, class_name: "UndestroyableBook", foreign_key: "author_id", dependent: :destroy + end + + def test_dependency_should_halt_parent_destruction + author = DestroyableAuthor.create!(name: "Test") + UndestroyableBook.create!(author: author) + + assert_no_difference ["DestroyableAuthor.count", "UndestroyableBook.count"] do + assert_not author.destroy + end + end +end |