rails/activerecord/test/cases/enum_test.rb
Ryuta Kamizono 71f0df943d Fix pluck to correctly type cast same column names and association columns
That issues are caused by using only the model's cast types on the
relation.
To fix that issues, use the attribute's type caster takes precedence
over the model's cast types on the relation.

Fixes #35232.
Fixes #36042.
Fixes #37484.
2020-05-13 20:24:42 +09:00

618 lines
19 KiB
Ruby

# frozen_string_literal: true
require "cases/helper"
require "models/author"
require "models/book"
require "active_support/log_subscriber/test_helper"
class EnumTest < ActiveRecord::TestCase
fixtures :books, :authors, :author_addresses
setup do
@book = books(:awdr)
end
test "query state by predicate" do
assert_predicate @book, :published?
assert_not_predicate @book, :written?
assert_not_predicate @book, :proposed?
assert_predicate @book, :read?
assert_predicate @book, :in_english?
assert_predicate @book, :author_visibility_visible?
assert_predicate @book, :illustrator_visibility_visible?
assert_predicate @book, :with_medium_font_size?
assert_predicate @book, :medium_to_read?
end
test "query state with strings" do
assert_equal "published", @book.status
assert_equal "read", @book.last_read
assert_equal "english", @book.language
assert_equal "visible", @book.author_visibility
assert_equal "visible", @book.illustrator_visibility
assert_equal "medium", @book.difficulty
end
test "find via scope" do
assert_equal @book, Book.published.first
assert_equal @book, Book.read.first
assert_equal @book, Book.in_english.first
assert_equal @book, Book.author_visibility_visible.first
assert_equal @book, Book.illustrator_visibility_visible.first
assert_equal @book, Book.medium_to_read.first
assert_equal books(:ddd), Book.forgotten.first
assert_equal books(:rfr), authors(:david).unpublished_books.first
end
test "find via negative scope" do
assert Book.not_published.exclude?(@book)
assert Book.not_proposed.include?(@book)
end
test "find via where with values" do
published, written = Book.statuses[:published], Book.statuses[:written]
assert_equal @book, Book.where(status: published).first
assert_not_equal @book, Book.where(status: written).first
assert_equal @book, Book.where(status: [published]).first
assert_not_equal @book, Book.where(status: [written]).first
assert_not_equal @book, Book.where("status <> ?", published).first
assert_equal @book, Book.where("status <> ?", written).first
end
test "find via where with symbols" do
assert_equal @book, Book.where(status: :published).first
assert_not_equal @book, Book.where(status: :written).first
assert_equal @book, Book.where(status: [:published]).first
assert_not_equal @book, Book.where(status: [:written]).first
assert_not_equal @book, Book.where.not(status: :published).first
assert_equal @book, Book.where.not(status: :written).first
assert_equal books(:ddd), Book.where(last_read: :forgotten).first
end
test "find via where with strings" do
assert_equal @book, Book.where(status: "published").first
assert_not_equal @book, Book.where(status: "written").first
assert_equal @book, Book.where(status: ["published"]).first
assert_not_equal @book, Book.where(status: ["written"]).first
assert_not_equal @book, Book.where.not(status: "published").first
assert_equal @book, Book.where.not(status: "written").first
assert_equal books(:ddd), Book.where(last_read: "forgotten").first
end
test "build from scope" do
assert_predicate Book.written.build, :written?
assert_not_predicate Book.written.build, :proposed?
end
test "build from where" do
assert_predicate Book.where(status: Book.statuses[:written]).build, :written?
assert_not_predicate Book.where(status: Book.statuses[:written]).build, :proposed?
assert_predicate Book.where(status: :written).build, :written?
assert_not_predicate Book.where(status: :written).build, :proposed?
assert_predicate Book.where(status: "written").build, :written?
assert_not_predicate Book.where(status: "written").build, :proposed?
end
test "update by declaration" do
@book.written!
assert_predicate @book, :written?
@book.in_english!
assert_predicate @book, :in_english?
@book.author_visibility_visible!
assert_predicate @book, :author_visibility_visible?
end
test "update by setter" do
@book.update! status: :written
assert_predicate @book, :written?
end
test "enum methods are overwritable" do
assert_equal "do publish work...", @book.published!
assert_predicate @book, :published?
end
test "direct assignment" do
@book.status = :written
assert_predicate @book, :written?
end
test "assign string value" do
@book.status = "written"
assert_predicate @book, :written?
end
test "enum changed attributes" do
old_status = @book.status
old_language = @book.language
@book.status = :proposed
@book.language = :spanish
assert_equal old_status, @book.changed_attributes[:status]
assert_equal old_language, @book.changed_attributes[:language]
end
test "enum value after write symbol" do
@book.status = :proposed
assert_equal "proposed", @book.status
end
test "enum value after write string" do
@book.status = "proposed"
assert_equal "proposed", @book.status
end
test "enum changes" do
old_status = @book.status
old_language = @book.language
@book.status = :proposed
@book.language = :spanish
assert_equal [old_status, "proposed"], @book.changes[:status]
assert_equal [old_language, "spanish"], @book.changes[:language]
end
test "enum attribute was" do
old_status = @book.status
old_language = @book.language
@book.status = :published
@book.language = :spanish
assert_equal old_status, @book.attribute_was(:status)
assert_equal old_language, @book.attribute_was(:language)
end
test "enum attribute changed" do
@book.status = :proposed
@book.language = :french
assert @book.attribute_changed?(:status)
assert @book.attribute_changed?(:language)
end
test "enum attribute changed to" do
@book.status = :proposed
@book.language = :french
assert @book.attribute_changed?(:status, to: "proposed")
assert @book.attribute_changed?(:language, to: "french")
end
test "enum attribute changed from" do
old_status = @book.status
old_language = @book.language
@book.status = :proposed
@book.language = :french
assert @book.attribute_changed?(:status, from: old_status)
assert @book.attribute_changed?(:language, from: old_language)
end
test "enum attribute changed from old status to new status" do
old_status = @book.status
old_language = @book.language
@book.status = :proposed
@book.language = :french
assert @book.attribute_changed?(:status, from: old_status, to: "proposed")
assert @book.attribute_changed?(:language, from: old_language, to: "french")
end
test "enum didn't change" do
old_status = @book.status
@book.status = old_status
assert_not @book.attribute_changed?(:status)
end
test "persist changes that are dirty" do
@book.status = :proposed
assert @book.attribute_changed?(:status)
@book.status = :written
assert @book.attribute_changed?(:status)
end
test "reverted changes that are not dirty" do
old_status = @book.status
@book.status = :proposed
assert @book.attribute_changed?(:status)
@book.status = old_status
assert_not @book.attribute_changed?(:status)
end
test "reverted changes are not dirty going from nil to value and back" do
book = Book.create!(nullable_status: nil)
book.nullable_status = :married
assert book.attribute_changed?(:nullable_status)
book.nullable_status = nil
assert_not book.attribute_changed?(:nullable_status)
end
test "assign non existing value raises an error" do
e = assert_raises(ArgumentError) do
@book.status = :unknown
end
assert_equal "'unknown' is not a valid status", e.message
end
test "NULL values from database should be casted to nil" do
Book.where(id: @book.id).update_all("status = NULL")
assert_nil @book.reload.status
end
test "assign nil value" do
@book.status = nil
assert_nil @book.status
end
test "assign nil value to enum which defines nil value to hash" do
@book.last_read = nil
assert_equal "forgotten", @book.last_read
end
test "assign empty string value" do
@book.status = ""
assert_nil @book.status
end
test "assign false value to a field defined as not boolean" do
@book.status = false
assert_nil @book.status
end
test "assign false value to a field defined as boolean" do
@book.boolean_status = false
assert_equal "disabled", @book.boolean_status
end
test "assign long empty string value" do
@book.status = " "
assert_nil @book.status
end
test "constant to access the mapping" do
assert_equal 0, Book.statuses[:proposed]
assert_equal 1, Book.statuses["written"]
assert_equal 2, Book.statuses[:published]
end
test "building new objects with enum scopes" do
assert_predicate Book.written.build, :written?
assert_predicate Book.read.build, :read?
assert_predicate Book.in_spanish.build, :in_spanish?
assert_predicate Book.illustrator_visibility_invisible.build, :illustrator_visibility_invisible?
end
test "creating new objects with enum scopes" do
assert_predicate Book.written.create, :written?
assert_predicate Book.read.create, :read?
assert_predicate Book.in_spanish.create, :in_spanish?
assert_predicate Book.illustrator_visibility_invisible.create, :illustrator_visibility_invisible?
end
test "_before_type_cast" do
assert_equal 2, @book.status_before_type_cast
assert_equal "published", @book.status
@book.status = "published"
assert_equal "published", @book.status_before_type_cast
assert_equal "published", @book.status
end
test "invalid definition values raise an ArgumentError" do
e = assert_raises(ArgumentError) do
Class.new(ActiveRecord::Base) do
self.table_name = "books"
enum status: [proposed: 1, written: 2, published: 3]
end
end
assert_match(/must be either a hash, an array of symbols, or an array of strings./, e.message)
e = assert_raises(ArgumentError) do
Class.new(ActiveRecord::Base) do
self.table_name = "books"
enum status: { "" => 1, "active" => 2 }
end
end
assert_match(/Enum label name must not be blank/, e.message)
e = assert_raises(ArgumentError) do
Class.new(ActiveRecord::Base) do
self.table_name = "books"
enum status: ["active", ""]
end
end
assert_match(/Enum label name must not be blank/, e.message)
end
test "reserved enum names" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = "books"
enum status: [:proposed, :written, :published]
end
conflicts = [
:column, # generates class method .columns, which conflicts with an AR method
:logger, # generates #logger, which conflicts with an AR method
:attributes, # generates #attributes=, which conflicts with an AR method
]
conflicts.each_with_index do |name, i|
e = assert_raises(ArgumentError) do
klass.class_eval { enum name => ["value_#{i}"] }
end
assert_match(/You tried to define an enum named \"#{name}\" on the model/, e.message)
end
end
test "reserved enum values" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = "books"
enum status: [:proposed, :written, :published]
end
conflicts = [
:new, # generates a scope that conflicts with an AR class method
:valid, # generates #valid?, which conflicts with an AR method
:save, # generates #save!, which conflicts with an AR method
:proposed, # same value as an existing enum
:public, :private, :protected, # some important methods on Module and Class
:name, :parent, :superclass
]
conflicts.each_with_index do |value, i|
e = assert_raises(ArgumentError, "enum value `#{value}` should not be allowed") do
klass.class_eval { enum "status_#{i}" => [value] }
end
assert_match(/You tried to define an enum named .* on the model/, e.message)
end
end
test "reserved enum values for relation" do
relation_method_samples = [
:records,
:to_ary,
:scope_for_create
]
relation_method_samples.each do |value|
e = assert_raises(ArgumentError, "enum value `#{value}` should not be allowed") do
Class.new(ActiveRecord::Base) do
self.table_name = "books"
enum category: [:other, value]
end
end
assert_match(/You tried to define an enum named .* on the model/, e.message)
end
end
test "overriding enum method should not raise" do
assert_nothing_raised do
Class.new(ActiveRecord::Base) do
self.table_name = "books"
def published!
super
"do publish work..."
end
enum status: [:proposed, :written, :published]
def written!
super
"do written work..."
end
end
end
end
test "validate uniqueness" do
klass = Class.new(ActiveRecord::Base) do
def self.name; "Book"; end
enum status: [:proposed, :written]
validates_uniqueness_of :status
end
klass.delete_all
klass.create!(status: "proposed")
book = klass.new(status: "written")
assert_predicate book, :valid?
book.status = "proposed"
assert_not_predicate book, :valid?
end
test "validate inclusion of value in array" do
klass = Class.new(ActiveRecord::Base) do
def self.name; "Book"; end
enum status: [:proposed, :written]
validates_inclusion_of :status, in: ["written"]
end
klass.delete_all
invalid_book = klass.new(status: "proposed")
assert_not_predicate invalid_book, :valid?
valid_book = klass.new(status: "written")
assert_predicate valid_book, :valid?
end
test "enums are distinct per class" do
klass1 = Class.new(ActiveRecord::Base) do
self.table_name = "books"
enum status: [:proposed, :written]
end
klass2 = Class.new(ActiveRecord::Base) do
self.table_name = "books"
enum status: [:drafted, :uploaded]
end
book1 = klass1.proposed.create!
book1.status = :written
assert_equal ["proposed", "written"], book1.status_change
book2 = klass2.drafted.create!
book2.status = :uploaded
assert_equal ["drafted", "uploaded"], book2.status_change
end
test "enums are inheritable" do
subklass1 = Class.new(Book)
subklass2 = Class.new(Book) do
enum status: [:drafted, :uploaded]
end
book1 = subklass1.proposed.create!
book1.status = :written
assert_equal ["proposed", "written"], book1.status_change
book2 = subklass2.drafted.create!
book2.status = :uploaded
assert_equal ["drafted", "uploaded"], book2.status_change
end
test "attempting to modify enum raises error" do
e = assert_raises(RuntimeError) do
Book.statuses["bad_enum"] = 40
end
assert_match(/can't modify frozen/, e.message)
e = assert_raises(RuntimeError) do
Book.statuses.delete("published")
end
assert_match(/can't modify frozen/, e.message)
end
test "declare multiple enums at a time" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = "books"
enum status: [:proposed, :written, :published],
nullable_status: [:single, :married]
end
book1 = klass.proposed.create!
assert_predicate book1, :proposed?
book2 = klass.single.create!
assert_predicate book2, :single?
end
test "enum with alias_attribute" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = "books"
alias_attribute :aliased_status, :status
enum aliased_status: [:proposed, :written, :published]
end
book = klass.proposed.create!
assert_predicate book, :proposed?
assert_equal "proposed", book.aliased_status
book = klass.find(book.id)
assert_predicate book, :proposed?
assert_equal "proposed", book.aliased_status
end
test "query state by predicate with prefix" do
assert_predicate @book, :author_visibility_visible?
assert_not_predicate @book, :author_visibility_invisible?
assert_predicate @book, :illustrator_visibility_visible?
assert_not_predicate @book, :illustrator_visibility_invisible?
end
test "query state by predicate with custom prefix" do
assert_predicate @book, :in_english?
assert_not_predicate @book, :in_spanish?
assert_not_predicate @book, :in_french?
end
test "query state by predicate with custom suffix" do
assert_predicate @book, :medium_to_read?
assert_not_predicate @book, :easy_to_read?
assert_not_predicate @book, :hard_to_read?
end
test "enum methods with custom suffix defined" do
assert_respond_to @book.class, :easy_to_read
assert_respond_to @book.class, :medium_to_read
assert_respond_to @book.class, :hard_to_read
assert_respond_to @book, :easy_to_read?
assert_respond_to @book, :medium_to_read?
assert_respond_to @book, :hard_to_read?
assert_respond_to @book, :easy_to_read!
assert_respond_to @book, :medium_to_read!
assert_respond_to @book, :hard_to_read!
end
test "update enum attributes with custom suffix" do
@book.medium_to_read!
assert_not_predicate @book, :easy_to_read?
assert_predicate @book, :medium_to_read?
assert_not_predicate @book, :hard_to_read?
@book.easy_to_read!
assert_predicate @book, :easy_to_read?
assert_not_predicate @book, :medium_to_read?
assert_not_predicate @book, :hard_to_read?
@book.hard_to_read!
assert_not_predicate @book, :easy_to_read?
assert_not_predicate @book, :medium_to_read?
assert_predicate @book, :hard_to_read?
end
test "uses default status when no status is provided in fixtures" do
book = books(:tlg)
assert book.proposed?, "expected fixture to default to proposed status"
assert book.in_english?, "expected fixture to default to english language"
end
test "uses default value from database on initialization" do
book = Book.new
assert_predicate book, :proposed?
end
test "uses default value from database on initialization when using custom mapping" do
book = Book.new
assert_predicate book, :hard?
end
test "data type of Enum type" do
assert_equal :integer, Book.type_for_attribute("status").type
end
test "scopes can be disabled" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = "books"
enum status: [:proposed, :written], _scopes: false
end
assert_raises(NoMethodError) { klass.proposed }
end
test "enums with a negative condition log a warning" do
old_logger = ActiveRecord::Base.logger
logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
ActiveRecord::Base.logger = logger
expected_message = "An enum element in Book uses the prefix 'not_'."\
" This will cause a conflict with auto generated negative scopes."
Class.new(ActiveRecord::Base) do
def self.name
"Book"
end
silence_warnings do
enum status: [:sent, :not_sent]
end
end
assert_match(expected_message, logger.logged(:warn).first)
ensure
ActiveRecord::Base.logger = old_logger
end
end