1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00
rails--rails/activerecord/test/cases/attribute_methods_test.rb
Marcelo Lauxen 5fdc7d385f Fix read_attribute_before_type_cast to consider attribute aliases
Numericality validations for aliased attributes are not able
to get the value of the attribute before type cast because
activerecord was trying to get the value of the attribute based
on attribute alias name and not the original attribute name.

Example of validation which would pass even if a invalid value
would be provided

    class MyModel < ActiveRecord::Base
      validates :aliased_balance, numericality: { greater_than_or_equal_to: 0 }
    end

If we instantiate MyModel like bellow it will be valid because
when numericality validation runs it will not be able to get the
value before type cast, so it uses the type casted value
which will be `0.0` and the validation will match.

    subject = MyModel.new(aliased_balance: "abcd")
    subject.valid?

But if we declare MyModel like this

    class MyModel < ActiveRecord::Base
      validates :balance, numericality: { greater_than_or_equal_to: 0 }
    end

and assign "abcd" value to `balance` when the validations
run the model will be invalid because activerecord will be able
to get the value before type cast.

With this change `read_attribute_before_type_cast` will be able to
get the value before type cast even when the attr_name is an
attribute_alias.
2020-10-18 20:32:47 -03:00

1123 lines
36 KiB
Ruby

# frozen_string_literal: true
require "cases/helper"
require "models/minimalistic"
require "models/developer"
require "models/auto_id"
require "models/boolean"
require "models/computer"
require "models/topic"
require "models/company"
require "models/category"
require "models/reply"
require "models/contact"
require "models/keyboard"
require "models/numeric_data"
class AttributeMethodsTest < ActiveRecord::TestCase
include InTimeZone
fixtures :topics, :developers, :companies, :computers
def setup
@old_matchers = ActiveRecord::Base.send(:attribute_method_matchers).dup
@target = Class.new(ActiveRecord::Base)
@target.table_name = "topics"
end
teardown do
ActiveRecord::Base.send(:attribute_method_matchers).clear
ActiveRecord::Base.send(:attribute_method_matchers).concat(@old_matchers)
end
test "attribute_for_inspect with a string" do
t = topics(:first)
t.title = "The First Topic Now Has A Title With\nNewlines And More Than 50 Characters"
assert_equal '"The First Topic Now Has A Title With\nNewlines And ..."', t.attribute_for_inspect(:title)
assert_equal '"The First Topic Now Has A Title With\nNewlines And ..."', t.attribute_for_inspect(:heading)
end
test "attribute_for_inspect with a date" do
t = topics(:first)
assert_equal %("#{t.written_on.to_s(:inspect)}"), t.attribute_for_inspect(:written_on)
end
test "attribute_for_inspect with an array" do
t = topics(:first)
t.content = [Object.new]
assert_match %r(\[#<Object:0x[0-9a-f]+>\]), t.attribute_for_inspect(:content)
end
test "attribute_for_inspect with a long array" do
t = topics(:first)
t.content = (1..11).to_a
assert_equal "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]", t.attribute_for_inspect(:content)
end
test "attribute_for_inspect with a non-primary key id attribute" do
t = topics(:first).becomes(TitlePrimaryKeyTopic)
t.title = "The First Topic Now Has A Title With\nNewlines And More Than 50 Characters"
assert_equal "1", t.attribute_for_inspect(:id)
end
test "attribute_present" do
t = Topic.new
t.title = "hello there!"
t.written_on = Time.now
t.author_name = ""
assert t.attribute_present?("title")
assert t.attribute_present?("heading")
assert t.attribute_present?("written_on")
assert_not t.attribute_present?("content")
assert_not t.attribute_present?("author_name")
end
test "attribute_present with booleans" do
b1 = Boolean.new
b1.value = false
assert b1.attribute_present?(:value)
b2 = Boolean.new
b2.value = true
assert b2.attribute_present?(:value)
b3 = Boolean.new
assert_not b3.attribute_present?(:value)
b4 = Boolean.new
b4.value = false
b4.save!
assert Boolean.find(b4.id).attribute_present?(:value)
end
test "caching a nil primary key" do
klass = Class.new(Minimalistic)
assert_called(klass, :reset_primary_key, returns: nil) do
2.times { klass.primary_key }
end
end
test "attribute keys on a new instance" do
t = Topic.new
assert_nil t.title, "The topics table has a title column, so it should be nil"
assert_raise(NoMethodError) { t.title2 }
end
test "boolean attributes" do
assert_not_predicate Topic.find(1), :approved?
assert_predicate Topic.find(2), :approved?
end
test "set attributes" do
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
test "set attributes without a hash" do
topic = Topic.new
assert_raise(ArgumentError) { topic.attributes = "" }
end
test "integers as nil" do
test = AutoId.create(value: "")
assert_nil AutoId.find(test.id).value
end
test "set attributes with a block" do
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
test "respond_to?" do
topic = Topic.find(1)
assert_respond_to topic, "title"
assert_respond_to topic, "title?"
assert_respond_to topic, "title="
assert_respond_to topic, :title
assert_respond_to topic, :title?
assert_respond_to topic, :title=
assert_respond_to topic, "author_name"
assert_respond_to topic, "attribute_names"
assert_not_respond_to topic, "nothingness"
assert_not_respond_to topic, :nothingness
end
test "respond_to? with a custom primary key" do
keyboard = Keyboard.create
assert_not_nil keyboard.key_number
assert_equal keyboard.key_number, keyboard.id
assert_respond_to keyboard, "key_number"
assert_respond_to keyboard, "id"
end
test "id_before_type_cast with a custom primary key" do
keyboard = Keyboard.create
keyboard.key_number = "10"
assert_equal "10", keyboard.id_before_type_cast
assert_nil keyboard.read_attribute_before_type_cast("id")
assert_equal "10", keyboard.read_attribute_before_type_cast("key_number")
assert_equal "10", keyboard.read_attribute_before_type_cast(:key_number)
end
# IRB inspects the return value of MyModel.allocate.
test "allocated objects can be inspected" do
topic = Topic.allocate
assert_equal "#<Topic not initialized>", topic.inspect
end
test "array content" do
content = %w( one two three )
topic = Topic.new
topic.content = content
topic.save
assert_equal content, Topic.find(topic.id).content
end
test "read attributes_before_type_cast" do
category = Category.new(name: "Test category", type: nil)
category_attrs = { "name" => "Test category", "id" => nil, "type" => nil, "categorizations_count" => nil }
assert_equal category_attrs, category.attributes_before_type_cast
end
if current_adapter?(:Mysql2Adapter)
test "read attributes_before_type_cast on a boolean" do
bool = Boolean.create!("value" => false)
assert_equal 0, bool.reload.attributes_before_type_cast["value"]
end
end
test "read attributes_before_type_cast on a datetime" do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
record.written_on = "345643456"
assert_equal "345643456", record.written_on_before_type_cast
assert_nil record.written_on
record.written_on = "2009-10-11 12:13:14"
assert_equal "2009-10-11 12:13:14", record.written_on_before_type_cast
assert_equal Time.zone.parse("2009-10-11 12:13:14"), record.written_on
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
end
end
test "read attributes_after_type_cast on a date" do
tz = "Pacific Time (US & Canada)"
in_time_zone tz do
record = @target.new
date_string = "2011-03-24"
time = Time.zone.parse date_string
record.written_on = date_string
assert_equal date_string, record.written_on_before_type_cast
assert_equal time, record.written_on
assert_equal ActiveSupport::TimeZone[tz], record.written_on.time_zone
record.save
record.reload
assert_equal time, record.written_on
end
end
test "hash content" do
topic = Topic.new
topic.content = { "one" => 1, "two" => 2 }
topic.save
assert_equal 2, Topic.find(topic.id).content["two"]
topic.content_will_change!
topic.content["three"] = 3
topic.save
assert_equal 3, Topic.find(topic.id).content["three"]
end
test "update array content" do
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
test "case-sensitive attributes hash" do
assert_equal @loaded_fixtures["computers"]["workstation"].to_hash, Computer.first.attributes
end
test "attributes without primary key" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = "developers_projects"
end
assert_equal klass.column_names, klass.new.attributes.keys
assert_not klass.new.has_attribute?("id")
end
test "hashes are not mangled" do
new_topic = { title: "New Topic", content: { key: "First value" } }
new_topic_values = { title: "AnotherTopic", content: { key: "Second value" } }
topic = Topic.new(new_topic)
assert_equal new_topic[:title], topic.title
assert_equal new_topic[:content], topic.content
topic.attributes = new_topic_values
assert_equal new_topic_values[:title], topic.title
assert_equal new_topic_values[:content], topic.content
end
test "create through factory" do
topic = Topic.create(title: "New Topic")
topicReloaded = Topic.find(topic.id)
assert_equal(topic, topicReloaded)
end
test "write_attribute" do
topic = Topic.new
topic.write_attribute :title, "Still another topic"
assert_equal "Still another topic", topic.title
topic[:title] = "Still another topic: part 2"
assert_equal "Still another topic: part 2", topic.title
topic.write_attribute "title", "Still another topic: part 3"
assert_equal "Still another topic: part 3", topic.title
topic["title"] = "Still another topic: part 4"
assert_equal "Still another topic: part 4", topic.title
end
test "write_attribute can write aliased attributes as well" do
topic = Topic.new(title: "Don't change the topic")
topic.write_attribute :heading, "New topic"
assert_equal "New topic", topic.title
end
test "write_attribute raises ActiveModel::MissingAttributeError when the attribute does not exist" do
topic = Topic.first
assert_raises(ActiveModel::MissingAttributeError) { topic.update_columns(no_column_exists: "Hello!") }
assert_raises(ActiveModel::UnknownAttributeError) { topic.update(no_column_exists: "Hello!") }
end
test "write_attribute allows writing to aliased attributes" do
topic = Topic.first
assert_nothing_raised { topic.update_columns(heading: "Hello!") }
assert_nothing_raised { topic.update(heading: "Hello!") }
end
test "read_attribute" do
topic = Topic.new
topic.title = "Don't change the topic"
assert_equal "Don't change the topic", topic.read_attribute("title")
assert_equal "Don't change the topic", topic["title"]
assert_equal "Don't change the topic", topic.read_attribute(:title)
assert_equal "Don't change the topic", topic[:title]
end
test "read_attribute can read aliased attributes as well" do
topic = Topic.new(title: "Don't change the topic")
assert_equal "Don't change the topic", topic.read_attribute("heading")
assert_equal "Don't change the topic", topic["heading"]
assert_equal "Don't change the topic", topic.read_attribute(:heading)
assert_equal "Don't change the topic", topic[:heading]
end
test "read_attribute raises ActiveModel::MissingAttributeError when the attribute does not exist" do
computer = Computer.select("id").first
assert_raises(ActiveModel::MissingAttributeError) { computer[:developer] }
assert_raises(ActiveModel::MissingAttributeError) { computer[:extendedWarranty] }
assert_raises(ActiveModel::MissingAttributeError) { computer[:no_column_exists] = "Hello!" }
assert_nothing_raised { computer[:developer] = "Hello!" }
end
test "read_attribute when false" do
topic = topics(:first)
topic.approved = false
assert_not topic.approved?, "approved should be false"
topic.approved = "false"
assert_not topic.approved?, "approved should be false"
end
test "read_attribute when true" do
topic = topics(:first)
topic.approved = true
assert topic.approved?, "approved should be true"
topic.approved = "true"
assert topic.approved?, "approved should be true"
end
test "boolean attributes writing and reading" do
topic = Topic.new
topic.approved = "false"
assert_not topic.approved?, "approved should be false"
topic.approved = "false"
assert_not topic.approved?, "approved should be false"
topic.approved = "true"
assert topic.approved?, "approved should be true"
topic.approved = "true"
assert topic.approved?, "approved should be true"
end
test "overridden write_attribute" do
topic = Topic.new
def topic.write_attribute(attr_name, value)
super(attr_name, value.downcase)
end
topic.write_attribute :title, "Yet another topic"
assert_equal "yet another topic", topic.title
topic[:title] = "Yet another topic: part 2"
assert_equal "yet another topic: part 2", topic.title
topic.write_attribute "title", "Yet another topic: part 3"
assert_equal "yet another topic: part 3", topic.title
topic["title"] = "Yet another topic: part 4"
assert_equal "yet another topic: part 4", topic.title
end
test "overridden read_attribute" do
topic = Topic.new
topic.title = "Stop changing the topic"
def topic.read_attribute(attr_name)
super(attr_name).upcase
end
assert_equal "STOP CHANGING THE TOPIC", topic.read_attribute("title")
assert_equal "STOP CHANGING THE TOPIC", topic["title"]
assert_equal "STOP CHANGING THE TOPIC", topic.read_attribute(:title)
assert_equal "STOP CHANGING THE TOPIC", topic[:title]
end
test "read overridden attribute" do
topic = Topic.new(title: "a")
def topic.title() "b" end
assert_equal "a", topic[:title]
end
test "string attribute predicate" do
[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?
ActiveModel::Type::Boolean::FALSE_VALUES.each do |value|
assert_predicate Topic.new(author_name: value), :author_name?
end
end
test "number attribute predicate" do
[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
test "boolean attribute predicate" do
[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
test "user-defined text attribute predicate" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = Topic.table_name
attribute :user_defined_text, :text
end
topic = klass.new(user_defined_text: "text")
assert_predicate topic, :user_defined_text?
ActiveModel::Type::Boolean::FALSE_VALUES.each do |value|
topic = klass.new(user_defined_text: value)
assert_predicate topic, :user_defined_text?
end
end
test "user-defined date attribute predicate" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = Topic.table_name
attribute :user_defined_date, :date
end
topic = klass.new(user_defined_date: Date.current)
assert_predicate topic, :user_defined_date?
end
test "user-defined datetime attribute predicate" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = Topic.table_name
attribute :user_defined_datetime, :datetime
end
topic = klass.new(user_defined_datetime: Time.current)
assert_predicate topic, :user_defined_datetime?
end
test "user-defined time attribute predicate" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = Topic.table_name
attribute :user_defined_time, :time
end
topic = klass.new(user_defined_time: Time.current)
assert_predicate topic, :user_defined_time?
end
test "user-defined json attribute predicate" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = Topic.table_name
attribute :user_defined_json, :json
end
topic = klass.new(user_defined_json: { key: "value" })
assert_predicate topic, :user_defined_json?
topic = klass.new(user_defined_json: {})
assert_not_predicate topic, :user_defined_json?
end
test "custom field attribute predicate" do
object = Company.find_by_sql(<<~SQL).first
SELECT c1.*, c2.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_predicate object, :string_value?
object.string_value = " "
assert_not_predicate object, :string_value?
assert_equal 1, object.int_value.to_i
assert_predicate object, :int_value?
object.int_value = "0"
assert_not_predicate object, :int_value?
end
test "non-attribute read and write" do
topic = Topic.new
assert_not_respond_to topic, "mumbo"
assert_raise(NoMethodError) { topic.mumbo }
assert_raise(NoMethodError) { topic.mumbo = 5 }
end
test "undeclared attribute method does not affect respond_to? and method_missing" do
topic = @target.new(title: "Budget")
assert_respond_to topic, "title"
assert_equal "Budget", topic.title
assert_not_respond_to topic, "title_hello_world"
assert_raise(NoMethodError) { topic.title_hello_world }
end
test "declared prefixed attribute method affects respond_to? and method_missing" do
topic = @target.new(title: "Budget")
%w(default_ title_).each do |prefix|
@target.class_eval "def #{prefix}attribute(*args) args end"
@target.attribute_method_prefix prefix
meth = "#{prefix}title"
assert_respond_to topic, meth
assert_equal ["title"], topic.public_send(meth)
assert_equal ["title", "a"], topic.public_send(meth, "a")
assert_equal ["title", 1, 2, 3], topic.public_send(meth, 1, 2, 3)
end
end
test "declared suffixed attribute method affects respond_to? and method_missing" do
%w(_default _title_default _it! _candidate= able?).each do |suffix|
@target.class_eval "def attribute#{suffix}(*args) args end"
@target.attribute_method_suffix suffix
topic = @target.new(title: "Budget")
meth = "title#{suffix}"
assert_respond_to topic, meth
assert_equal ["title"], topic.public_send(meth)
assert_equal ["title", "a"], topic.public_send(meth, "a")
assert_equal ["title", 1, 2, 3], topic.public_send(meth, 1, 2, 3)
end
end
test "declared affixed attribute method affects respond_to? and method_missing" do
[["mark_", "_for_update"], ["reset_", "!"], ["default_", "_value?"]].each do |prefix, suffix|
@target.class_eval "def #{prefix}attribute#{suffix}(*args) args end"
@target.attribute_method_affix(prefix: prefix, suffix: suffix)
topic = @target.new(title: "Budget")
meth = "#{prefix}title#{suffix}"
assert_respond_to topic, meth
assert_equal ["title"], topic.public_send(meth)
assert_equal ["title", "a"], topic.public_send(meth, "a")
assert_equal ["title", 1, 2, 3], topic.public_send(meth, 1, 2, 3)
end
end
test "should unserialize attributes for frozen records" do
myobj = { value1: :value2 }
topic = Topic.create(content: myobj)
topic.freeze
assert_equal myobj, topic.content
end
test "typecast attribute from select to false" do
Topic.create(title: "Budget")
# Oracle does not support boolean expressions in SELECT.
if current_adapter?(:OracleAdapter)
topic = Topic.all.merge!(select: "topics.*, 0 as is_test").first
else
topic = Topic.all.merge!(select: "topics.*, 1=2 as is_test").first
end
assert_not_predicate topic, :is_test?
end
test "typecast attribute from select to true" do
Topic.create(title: "Budget")
# Oracle does not support boolean expressions in SELECT.
if current_adapter?(:OracleAdapter)
topic = Topic.all.merge!(select: "topics.*, 1 as is_test").first
else
topic = Topic.all.merge!(select: "topics.*, 2=2 as is_test").first
end
assert_predicate topic, :is_test?
end
test "raises ActiveRecord::DangerousAttributeError when defining an AR method in a model" do
%w(save create_or_update).each do |method|
klass = Class.new(ActiveRecord::Base)
klass.class_eval "def #{method}() 'defined #{method}' end"
assert_raise ActiveRecord::DangerousAttributeError do
klass.instance_method_already_implemented?(method)
end
end
end
test "converted values are returned after assignment" do
developer = Developer.new(name: 1337, salary: "50000")
assert_equal "50000", developer.salary_before_type_cast
assert_equal 1337, developer.name_before_type_cast
assert_equal 50000, developer.salary
assert_equal "1337", developer.name
developer.save!
assert_equal 50000, developer.salary
assert_equal "1337", developer.name
end
test "write nil to time attribute" do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
record.written_on = nil
assert_nil record.written_on
end
end
test "write time to date attribute" do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
record.last_read = Time.utc(2010, 1, 1, 10)
assert_equal Date.civil(2010, 1, 1), record.last_read
end
end
test "time attributes are retrieved in the current time zone" do
in_time_zone "Pacific Time (US & Canada)" do
utc_time = Time.utc(2008, 1, 1)
record = @target.new
record[:written_on] = utc_time
assert_equal utc_time, record.written_on # record.written on is equal to (i.e., simultaneous with) utc_time
assert_kind_of ActiveSupport::TimeWithZone, record.written_on # but is a TimeWithZone
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone # and is in the current Time.zone
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time # and represents time values adjusted accordingly
end
end
test "setting a time zone-aware attribute to UTC" do
in_time_zone "Pacific Time (US & Canada)" do
utc_time = Time.utc(2008, 1, 1)
record = @target.new
record.written_on = utc_time
assert_equal utc_time, record.written_on
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time
end
end
test "setting time zone-aware attribute in other time zone" do
utc_time = Time.utc(2008, 1, 1)
cst_time = utc_time.in_time_zone("Central Time (US & Canada)")
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
record.written_on = cst_time
assert_equal utc_time, record.written_on
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time
end
end
test "setting time zone-aware read attribute" do
utc_time = Time.utc(2008, 1, 1)
cst_time = utc_time.in_time_zone("Central Time (US & Canada)")
in_time_zone "Pacific Time (US & Canada)" do
record = @target.create(written_on: cst_time).reload
assert_equal utc_time, record[:written_on]
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record[:written_on].time_zone
assert_equal Time.utc(2007, 12, 31, 16), record[:written_on].time
end
end
test "setting time zone-aware attribute with a string" do
utc_time = Time.utc(2008, 1, 1)
(-11..13).each do |timezone_offset|
time_string = utc_time.in_time_zone(timezone_offset).to_s
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
record.written_on = time_string
assert_equal Time.zone.parse(time_string), record.written_on
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time
end
end
end
test "time zone-aware attribute saved" do
in_time_zone 1 do
record = @target.create(written_on: "2012-02-20 10:00")
record.written_on = "2012-02-20 09:00"
record.save
assert_equal Time.zone.local(2012, 02, 20, 9), record.reload.written_on
end
end
test "setting a time zone-aware attribute to a blank string returns nil" do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
record.written_on = " "
assert_nil record.written_on
assert_nil record[:written_on]
end
end
test "setting a time zone-aware attribute interprets time zone-unaware string in time zone" do
time_string = "Tue Jan 01 00:00:00 2008"
(-11..13).each do |timezone_offset|
in_time_zone timezone_offset do
record = @target.new
record.written_on = time_string
assert_equal Time.zone.parse(time_string), record.written_on
assert_equal ActiveSupport::TimeZone[timezone_offset], record.written_on.time_zone
assert_equal Time.utc(2008, 1, 1), record.written_on.time
end
end
end
test "setting a time zone-aware datetime in the current time zone" do
utc_time = Time.utc(2008, 1, 1)
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
record.written_on = utc_time.in_time_zone
assert_equal utc_time, record.written_on
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time
end
end
test "YAML dumping a record with time zone-aware attribute" do
in_time_zone "Pacific Time (US & Canada)" do
record = Topic.new(id: 1)
record.written_on = "Jan 01 00:00:00 2014"
assert_equal record, YAML.load(YAML.dump(record))
end
ensure
# NOTE: Reset column info because global topics
# don't have tz-aware attributes by default.
Topic.reset_column_information
end
test "setting a time zone-aware time in the current time zone" do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
time_string = "10:00:00"
expected_time = Time.zone.parse("2000-01-01 #{time_string}")
record.bonus_time = time_string
assert_equal expected_time, record.bonus_time
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.bonus_time.time_zone
record.bonus_time = ""
assert_nil record.bonus_time
end
end
test "setting a time zone-aware time with DST" do
in_time_zone "Pacific Time (US & Canada)" do
current_time = Time.zone.local(2014, 06, 15, 10)
record = @target.new(bonus_time: current_time)
time_before_save = record.bonus_time
record.save
record.reload
assert_equal time_before_save, record.bonus_time
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.bonus_time.time_zone
end
end
test "setting invalid string to a zone-aware time attribute" do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
time_string = "ABC"
record.bonus_time = time_string
assert_nil record.bonus_time
end
end
test "removing time zone-aware types" do
with_time_zone_aware_types(:datetime) do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new(bonus_time: "10:00:00")
expected_time = Time.utc(2000, 01, 01, 10)
assert_equal expected_time, record.bonus_time
assert_predicate record.bonus_time, :utc?
end
end
end
test "time zone-aware attributes do not recurse infinitely on invalid values" do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new(bonus_time: [])
assert_nil record.bonus_time
end
end
test "setting a time_zone_conversion_for_attributes should write the value on a class variable" do
Topic.skip_time_zone_conversion_for_attributes = [:field_a]
Minimalistic.skip_time_zone_conversion_for_attributes = [:field_b]
assert_equal [:field_a], Topic.skip_time_zone_conversion_for_attributes
assert_equal [:field_b], Minimalistic.skip_time_zone_conversion_for_attributes
end
test "attribute readers respect access control" do
privatize("title")
topic = @target.new(title: "The pros and cons of programming naked.")
assert_not_respond_to topic, :title
exception = assert_raise(NoMethodError) { topic.title }
assert_includes exception.message, "private method"
assert_equal "I'm private", topic.send(:title)
end
test "attribute writers respect access control" do
privatize("title=(value)")
topic = @target.new
assert_not_respond_to topic, :title=
exception = assert_raise(NoMethodError) { topic.title = "Pants" }
assert_includes exception.message, "private method"
topic.send(:title=, "Very large pants")
end
test "attribute predicates respect access control" do
privatize("title?")
topic = @target.new(title: "Isaac Newton's pants")
assert_not_respond_to topic, :title?
exception = assert_raise(NoMethodError) { topic.title? }
assert_includes exception.message, "private method"
assert topic.send(:title?)
end
test "bulk updates respect access control" do
privatize("title=(value)")
assert_raise(ActiveRecord::UnknownAttributeError) { @target.new(title: "Rants about pants") }
assert_raise(ActiveRecord::UnknownAttributeError) { @target.new.attributes = { title: "Ants in pants" } }
end
test "bulk update raises ActiveRecord::UnknownAttributeError" do
error = assert_raises(ActiveRecord::UnknownAttributeError) {
Topic.new(hello: "world")
}
assert_instance_of Topic, error.record
assert_equal "hello", error.attribute
assert_equal "unknown attribute 'hello' for Topic.", error.message
end
test "method overrides in multi-level subclasses" do
klass = Class.new(Developer) do
def name
"dev:#{read_attribute(:name)}"
end
end
2.times { klass = Class.new(klass) }
dev = klass.new(name: "arthurnn")
dev.save!
assert_equal "dev:arthurnn", dev.reload.name
end
test "global methods are overwritten" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = "computers"
end
assert_not klass.instance_method_already_implemented?(:system)
computer = klass.new
assert_nil computer.system
end
test "global methods are overwritten when subclassing" do
klass = Class.new(ActiveRecord::Base) do
self.abstract_class = true
end
subklass = Class.new(klass) do
self.table_name = "computers"
end
assert_not klass.instance_method_already_implemented?(:system)
assert_not subklass.instance_method_already_implemented?(:system)
computer = subklass.new
assert_nil computer.system
end
test "instance methods should be defined on the base class" do
subklass = Class.new(Topic)
Topic.define_attribute_methods
instance = subklass.new
instance.id = 5
assert_equal 5, instance.id
assert subklass.method_defined?(:id), "subklass is missing id method"
Topic.undefine_attribute_methods
assert_equal 5, instance.id
assert subklass.method_defined?(:id), "subklass is missing id method"
end
test "define_attribute_method works with both symbol and string" do
klass = Class.new(ActiveRecord::Base)
assert_nothing_raised { klass.define_attribute_method(:foo) }
assert_nothing_raised { klass.define_attribute_method("bar") }
end
test "read_attribute with nil should not asplode" do
assert_nil Topic.new.read_attribute(nil)
end
# If B < A, and A defines an accessor for 'foo', we don't want to override
# that by defining a 'foo' method in the generated methods module for B.
# (That module will be inserted between the two, e.g. [B, <GeneratedAttributes>, A].)
test "inherited custom accessors" do
klass = new_topic_like_ar_class do
self.abstract_class = true
def title; "omg"; end
def title=(val); self.author_name = val; end
end
subklass = Class.new(klass)
[klass, subklass].each(&:define_attribute_methods)
topic = subklass.find(1)
assert_equal "omg", topic.title
topic.title = "lol"
assert_equal "lol", topic.author_name
end
test "inherited custom accessors with reserved names" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = "computers"
self.abstract_class = true
def system; "omg"; end
def system=(val); self.developer = val; end
end
subklass = Class.new(klass)
[klass, subklass].each(&:define_attribute_methods)
computer = subklass.find(1)
assert_equal "omg", computer.system
computer.developer = 99
assert_equal 99, computer.developer
end
test "on_the_fly_super_invokable_generated_attribute_methods_via_method_missing" do
klass = new_topic_like_ar_class do
def title
super + "!"
end
end
real_topic = topics(:first)
assert_equal real_topic.title + "!", klass.find(real_topic.id).title
end
test "on-the-fly super-invokable generated attribute predicates via method_missing" do
klass = new_topic_like_ar_class do
def title?
!super
end
end
real_topic = topics(:first)
assert_equal !real_topic.title?, klass.find(real_topic.id).title?
end
test "calling super when the parent does not define method raises NoMethodError" do
klass = new_topic_like_ar_class do
def some_method_that_is_not_on_super
super
end
end
assert_raise(NoMethodError) do
klass.new.some_method_that_is_not_on_super
end
end
test "attribute_method?" do
assert @target.attribute_method?(:title)
assert @target.attribute_method?(:title=)
assert_not @target.attribute_method?(:wibble)
end
test "attribute_method? returns false if the table does not exist" do
@target.table_name = "wibble"
assert_not @target.attribute_method?(:title)
end
test "attribute_names on a new record" do
model = @target.new
assert_equal @target.column_names, model.attribute_names
end
test "attribute_names on a queried record" do
model = @target.last!
assert_equal @target.column_names, model.attribute_names
end
test "attribute_names with a custom select" do
model = @target.select("id").last!
assert_equal ["id"], model.attribute_names
# Sanity check, make sure other columns exist.
assert_not_equal ["id"], @target.column_names
end
test "came_from_user?" do
model = @target.first
assert_not_predicate model, :id_came_from_user?
model.id = "omg"
assert_predicate model, :id_came_from_user?
end
test "accessed_fields" do
model = @target.first
assert_equal [], model.accessed_fields
model.title
assert_equal ["title"], model.accessed_fields
end
test "generated attribute methods ancestors have correct module" do
mod = Topic.send(:generated_attribute_methods)
assert_equal "Topic::GeneratedAttributeMethods", mod.inspect
end
test "read_attribute_before_type_cast with aliased attribute" do
model = NumericData.new(new_bank_balance: "abcd")
assert_equal "abcd", model.read_attribute_before_type_cast("new_bank_balance")
end
private
def new_topic_like_ar_class(&block)
klass = Class.new(ActiveRecord::Base) do
self.table_name = "topics"
class_eval(&block)
end
assert_empty klass.send(:generated_attribute_methods).instance_methods(false)
klass
end
def with_time_zone_aware_types(*types)
old_types = ActiveRecord::Base.time_zone_aware_types
ActiveRecord::Base.time_zone_aware_types = types
yield
ensure
ActiveRecord::Base.time_zone_aware_types = old_types
end
def privatize(method_signature)
@target.class_eval(<<-private_method, __FILE__, __LINE__ + 1)
private
def #{method_signature}
"I'm private"
end
private_method
end
end