2017-07-09 13:41:28 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
require "cases/helper"
|
|
|
|
require "models/topic"
|
|
|
|
require "models/person"
|
|
|
|
require "models/traffic_light"
|
|
|
|
require "models/post"
|
|
|
|
require "bcrypt"
|
2012-08-26 01:54:46 -04:00
|
|
|
|
|
|
|
class SerializedAttributeTest < ActiveRecord::TestCase
|
2019-02-28 04:17:33 -05:00
|
|
|
fixtures :posts
|
2012-08-26 01:54:46 -04:00
|
|
|
|
|
|
|
MyObject = Struct.new :attribute1, :attribute2
|
|
|
|
|
2019-02-28 04:17:33 -05:00
|
|
|
class Topic < ActiveRecord::Base
|
|
|
|
serialize :content
|
|
|
|
end
|
|
|
|
|
|
|
|
class ImportantTopic < Topic
|
|
|
|
serialize :important, Hash
|
|
|
|
end
|
2018-11-28 14:34:19 -05:00
|
|
|
|
2014-03-14 00:35:58 -04:00
|
|
|
teardown do
|
2012-08-26 01:58:12 -04:00
|
|
|
Topic.serialize("content")
|
|
|
|
end
|
|
|
|
|
2014-06-06 11:43:09 -04:00
|
|
|
def test_serialize_does_not_eagerly_load_columns
|
2019-02-28 04:17:33 -05:00
|
|
|
Topic.reset_column_information
|
2014-06-06 11:43:09 -04:00
|
|
|
assert_no_queries do
|
|
|
|
Topic.serialize(:content)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-08-26 01:54:46 -04:00
|
|
|
def test_serialized_attribute
|
|
|
|
Topic.serialize("content", MyObject)
|
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
myobj = MyObject.new("value1", "value2")
|
2012-08-26 01:54:46 -04:00
|
|
|
topic = Topic.create("content" => myobj)
|
|
|
|
assert_equal(myobj, topic.content)
|
|
|
|
|
|
|
|
topic.reload
|
|
|
|
assert_equal(myobj, topic.content)
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_serialized_attribute_in_base_class
|
|
|
|
Topic.serialize("content", Hash)
|
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
hash = { "content1" => "value1", "content2" => "value2" }
|
2012-08-26 01:54:46 -04:00
|
|
|
important_topic = ImportantTopic.create("content" => hash)
|
|
|
|
assert_equal(hash, important_topic.content)
|
|
|
|
|
|
|
|
important_topic.reload
|
|
|
|
assert_equal(hash, important_topic.content)
|
|
|
|
end
|
|
|
|
|
2014-05-30 15:52:58 -04:00
|
|
|
def test_serialized_attributes_from_database_on_subclass
|
2012-08-26 02:03:12 -04:00
|
|
|
Topic.serialize :content, Hash
|
2012-08-26 01:54:46 -04:00
|
|
|
|
2019-02-28 04:17:33 -05:00
|
|
|
t = ImportantTopic.new(content: { foo: :bar })
|
2014-05-30 15:52:58 -04:00
|
|
|
assert_equal({ foo: :bar }, t.content)
|
2012-08-26 01:54:46 -04:00
|
|
|
t.save!
|
2019-02-28 04:17:33 -05:00
|
|
|
t = ImportantTopic.last
|
2014-05-30 15:52:58 -04:00
|
|
|
assert_equal({ foo: :bar }, t.content)
|
2012-08-26 01:54:46 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def test_serialized_attribute_calling_dup_method
|
2012-08-26 02:03:12 -04:00
|
|
|
Topic.serialize :content, JSON
|
2012-08-26 01:54:46 -04:00
|
|
|
|
2014-05-30 12:29:22 -04:00
|
|
|
orig = Topic.new(content: { foo: :bar })
|
|
|
|
clone = orig.dup
|
|
|
|
assert_equal(orig.content, clone.content)
|
2012-08-26 01:54:46 -04:00
|
|
|
end
|
|
|
|
|
2014-07-15 11:43:18 -04:00
|
|
|
def test_serialized_json_attribute_returns_unserialized_value
|
|
|
|
Topic.serialize :content, JSON
|
|
|
|
my_post = posts(:welcome)
|
|
|
|
|
|
|
|
t = Topic.new(content: my_post)
|
|
|
|
t.save!
|
|
|
|
t.reload
|
|
|
|
|
|
|
|
assert_instance_of(Hash, t.content)
|
|
|
|
assert_equal(my_post.id, t.content["id"])
|
|
|
|
assert_equal(my_post.title, t.content["title"])
|
|
|
|
end
|
|
|
|
|
2014-07-15 12:08:31 -04:00
|
|
|
def test_json_read_legacy_null
|
2014-07-14 13:38:14 -04:00
|
|
|
Topic.serialize :content, JSON
|
|
|
|
|
2014-07-15 12:08:31 -04:00
|
|
|
# Force a row to have a JSON "null" instead of a database NULL (this is how
|
|
|
|
# null values are saved on 4.1 and before)
|
|
|
|
id = Topic.connection.insert "INSERT INTO topics (content) VALUES('null')"
|
2014-07-14 13:38:14 -04:00
|
|
|
t = Topic.find(id)
|
|
|
|
|
|
|
|
assert_nil t.content
|
2014-07-15 12:08:31 -04:00
|
|
|
end
|
2014-07-14 13:38:14 -04:00
|
|
|
|
2014-07-15 12:08:31 -04:00
|
|
|
def test_json_read_db_null
|
|
|
|
Topic.serialize :content, JSON
|
2014-07-14 13:38:14 -04:00
|
|
|
|
2014-07-15 12:08:31 -04:00
|
|
|
# Force a row to have a database NULL instead of a JSON "null"
|
|
|
|
id = Topic.connection.insert "INSERT INTO topics (content) VALUES(NULL)"
|
|
|
|
t = Topic.find(id)
|
|
|
|
|
|
|
|
assert_nil t.content
|
2014-07-14 13:38:14 -04:00
|
|
|
end
|
|
|
|
|
2012-08-26 01:54:46 -04:00
|
|
|
def test_serialized_attribute_declared_in_subclass
|
2016-08-06 12:26:20 -04:00
|
|
|
hash = { "important1" => "value1", "important2" => "value2" }
|
2012-08-26 01:54:46 -04:00
|
|
|
important_topic = ImportantTopic.create("important" => hash)
|
|
|
|
assert_equal(hash, important_topic.important)
|
|
|
|
|
|
|
|
important_topic.reload
|
|
|
|
assert_equal(hash, important_topic.important)
|
|
|
|
assert_equal(hash, important_topic.read_attribute(:important))
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_serialized_time_attribute
|
2016-10-28 23:05:58 -04:00
|
|
|
myobj = Time.local(2008, 1, 1, 1, 0)
|
2012-08-26 01:54:46 -04:00
|
|
|
topic = Topic.create("content" => myobj).reload
|
|
|
|
assert_equal(myobj, topic.content)
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_serialized_string_attribute
|
|
|
|
myobj = "Yes"
|
|
|
|
topic = Topic.create("content" => myobj).reload
|
|
|
|
assert_equal(myobj, topic.content)
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_nil_serialized_attribute_without_class_constraint
|
|
|
|
topic = Topic.new
|
|
|
|
assert_nil topic.content
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_nil_not_serialized_without_class_constraint
|
2016-08-06 13:37:57 -04:00
|
|
|
assert Topic.new(content: nil).save
|
|
|
|
assert_equal 1, Topic.where(content: nil).count
|
2012-08-26 01:54:46 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def test_nil_not_serialized_with_class_constraint
|
|
|
|
Topic.serialize :content, Hash
|
2016-08-06 13:37:57 -04:00
|
|
|
assert Topic.new(content: nil).save
|
|
|
|
assert_equal 1, Topic.where(content: nil).count
|
2012-08-26 01:54:46 -04:00
|
|
|
end
|
|
|
|
|
2014-10-16 05:48:07 -04:00
|
|
|
def test_serialized_attribute_should_raise_exception_on_assignment_with_wrong_type
|
2012-08-26 02:09:31 -04:00
|
|
|
Topic.serialize(:content, Hash)
|
2014-05-30 12:29:22 -04:00
|
|
|
assert_raise(ActiveRecord::SerializationTypeMismatch) do
|
2016-08-06 12:26:20 -04:00
|
|
|
Topic.new(content: "string")
|
2014-05-30 12:29:22 -04:00
|
|
|
end
|
2012-08-26 02:09:31 -04:00
|
|
|
end
|
|
|
|
|
2012-08-26 01:54:46 -04:00
|
|
|
def test_should_raise_exception_on_serialized_attribute_with_type_mismatch
|
2016-08-06 12:26:20 -04:00
|
|
|
myobj = MyObject.new("value1", "value2")
|
2016-08-06 13:37:57 -04:00
|
|
|
topic = Topic.new(content: myobj)
|
2012-08-26 01:54:46 -04:00
|
|
|
assert topic.save
|
|
|
|
Topic.serialize(:content, Hash)
|
2012-08-26 02:03:53 -04:00
|
|
|
assert_raise(ActiveRecord::SerializationTypeMismatch) { Topic.find(topic.id).content }
|
2012-08-26 01:54:46 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def test_serialized_attribute_with_class_constraint
|
|
|
|
settings = { "color" => "blue" }
|
|
|
|
Topic.serialize(:content, Hash)
|
2016-08-06 13:37:57 -04:00
|
|
|
topic = Topic.new(content: settings)
|
2012-08-26 01:54:46 -04:00
|
|
|
assert topic.save
|
|
|
|
assert_equal(settings, Topic.find(topic.id).content)
|
|
|
|
end
|
|
|
|
|
2018-06-06 06:20:13 -04:00
|
|
|
def test_where_by_serialized_attribute_with_array
|
|
|
|
settings = [ "color" => "green" ]
|
|
|
|
Topic.serialize(:content, Array)
|
|
|
|
topic = Topic.create!(content: settings)
|
|
|
|
assert_equal topic, Topic.where(content: settings).take
|
|
|
|
end
|
|
|
|
|
2016-07-03 17:18:37 -04:00
|
|
|
def test_where_by_serialized_attribute_with_hash
|
|
|
|
settings = { "color" => "green" }
|
|
|
|
Topic.serialize(:content, Hash)
|
|
|
|
topic = Topic.create!(content: settings)
|
|
|
|
assert_equal topic, Topic.where(content: settings).take
|
|
|
|
end
|
|
|
|
|
2018-05-19 07:39:52 -04:00
|
|
|
def test_where_by_serialized_attribute_with_hash_in_array
|
|
|
|
settings = { "color" => "green" }
|
|
|
|
Topic.serialize(:content, Hash)
|
|
|
|
topic = Topic.create!(content: settings)
|
|
|
|
assert_equal topic, Topic.where(content: [settings]).take
|
|
|
|
end
|
|
|
|
|
2012-08-26 01:54:46 -04:00
|
|
|
def test_serialized_default_class
|
|
|
|
Topic.serialize(:content, Hash)
|
|
|
|
topic = Topic.new
|
|
|
|
assert_equal Hash, topic.content.class
|
|
|
|
assert_equal Hash, topic.read_attribute(:content).class
|
|
|
|
topic.content["beer"] = "MadridRb"
|
|
|
|
assert topic.save
|
|
|
|
topic.reload
|
|
|
|
assert_equal Hash, topic.content.class
|
|
|
|
assert_equal "MadridRb", topic.content["beer"]
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_serialized_no_default_class_for_object
|
|
|
|
topic = Topic.new
|
|
|
|
assert_nil topic.content
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_serialized_boolean_value_true
|
2016-08-06 13:37:57 -04:00
|
|
|
topic = Topic.new(content: true)
|
2012-08-26 01:54:46 -04:00
|
|
|
assert topic.save
|
|
|
|
topic = topic.reload
|
2016-12-25 21:04:41 -05:00
|
|
|
assert_equal true, topic.content
|
2012-08-26 01:54:46 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def test_serialized_boolean_value_false
|
2016-08-06 13:37:57 -04:00
|
|
|
topic = Topic.new(content: false)
|
2012-08-26 01:54:46 -04:00
|
|
|
assert topic.save
|
|
|
|
topic = topic.reload
|
2016-12-25 21:04:41 -05:00
|
|
|
assert_equal false, topic.content
|
2012-08-26 01:54:46 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def test_serialize_with_coder
|
2014-05-30 15:52:58 -04:00
|
|
|
some_class = Struct.new(:foo) do
|
|
|
|
def self.dump(value)
|
|
|
|
value.foo
|
2012-08-26 01:54:46 -04:00
|
|
|
end
|
|
|
|
|
2014-05-30 15:52:58 -04:00
|
|
|
def self.load(value)
|
|
|
|
new(value)
|
2012-08-26 01:54:46 -04:00
|
|
|
end
|
2014-05-30 15:52:58 -04:00
|
|
|
end
|
2012-08-26 01:54:46 -04:00
|
|
|
|
2014-05-30 15:52:58 -04:00
|
|
|
Topic.serialize(:content, some_class)
|
2016-08-06 13:37:57 -04:00
|
|
|
topic = Topic.new(content: some_class.new("my value"))
|
2014-05-30 15:52:58 -04:00
|
|
|
topic.save!
|
|
|
|
topic.reload
|
|
|
|
assert_kind_of some_class, topic.content
|
2016-12-25 21:04:41 -05:00
|
|
|
assert_equal some_class.new("my value"), topic.content
|
2012-08-26 01:54:46 -04:00
|
|
|
end
|
2012-12-06 14:30:00 -05:00
|
|
|
|
|
|
|
def test_serialize_attribute_via_select_method_when_time_zone_available
|
2013-10-24 15:26:23 -04:00
|
|
|
with_timezone_config aware_attributes: true do
|
|
|
|
Topic.serialize(:content, MyObject)
|
2012-12-06 14:30:00 -05:00
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
myobj = MyObject.new("value1", "value2")
|
2013-10-24 15:26:23 -04:00
|
|
|
topic = Topic.create(content: myobj)
|
2012-12-06 14:30:00 -05:00
|
|
|
|
2013-10-24 15:26:23 -04:00
|
|
|
assert_equal(myobj, Topic.select(:content).find(topic.id).content)
|
|
|
|
assert_raise(ActiveModel::MissingAttributeError) { Topic.select(:id).find(topic.id).content }
|
|
|
|
end
|
2012-12-06 14:30:00 -05:00
|
|
|
end
|
2012-12-21 12:15:33 -05:00
|
|
|
|
|
|
|
def test_serialize_attribute_can_be_serialized_in_an_integer_column
|
2016-08-06 12:26:20 -04:00
|
|
|
insures = ["life"]
|
|
|
|
person = SerializedPerson.new(first_name: "David", insures: insures)
|
2012-12-21 12:15:33 -05:00
|
|
|
assert person.save
|
|
|
|
person = person.reload
|
|
|
|
assert_equal(insures, person.insures)
|
|
|
|
end
|
2013-02-04 03:48:21 -05:00
|
|
|
|
|
|
|
def test_regression_serialized_default_on_text_column_with_null_false
|
|
|
|
light = TrafficLight.new
|
|
|
|
assert_equal [], light.state
|
|
|
|
assert_equal [], light.long_state
|
|
|
|
end
|
2013-04-24 13:04:07 -04:00
|
|
|
|
2017-01-29 17:24:44 -05:00
|
|
|
def test_unexpected_serialized_type
|
|
|
|
Topic.serialize :content, Hash
|
|
|
|
topic = Topic.create!(content: { zomg: true })
|
|
|
|
|
|
|
|
Topic.serialize :content, Array
|
|
|
|
|
|
|
|
topic.reload
|
|
|
|
error = assert_raise(ActiveRecord::SerializationTypeMismatch) do
|
|
|
|
topic.content
|
|
|
|
end
|
2017-02-04 06:55:02 -05:00
|
|
|
expected = "can't load `content`: was supposed to be a Array, but was a Hash. -- {:zomg=>true}"
|
2017-01-29 17:24:44 -05:00
|
|
|
assert_equal expected, error.to_s
|
|
|
|
end
|
|
|
|
|
2014-04-05 13:19:18 -04:00
|
|
|
def test_serialized_column_should_unserialize_after_update_column
|
|
|
|
t = Topic.create(content: "first")
|
|
|
|
assert_equal("first", t.content)
|
|
|
|
|
`update_column` take ruby-land input, not database-land input
In the case of serialized columns, we would expect the unserialized
value as input, not the serialized value. The original issue which made
this distinction, #14163, introduced a bug. If you passed serialized
input to the method, it would double serialize when it was sent to the
database. You would see the wrong input upon reloading, or get an error
if you had a specific type on the serialized column.
To put it another way, `update_column` is a special case of
`update_all`, which would take `['a']` and not `['a'].to_yaml`, but you
would not pass data from `params` to it.
Fixes #18037
2014-12-16 17:19:47 -05:00
|
|
|
t.update_column(:content, ["second"])
|
|
|
|
assert_equal(["second"], t.content)
|
|
|
|
assert_equal(["second"], t.reload.content)
|
2014-04-05 13:19:18 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def test_serialized_column_should_unserialize_after_update_attribute
|
|
|
|
t = Topic.create(content: "first")
|
|
|
|
assert_equal("first", t.content)
|
|
|
|
|
|
|
|
t.update_attribute(:content, "second")
|
|
|
|
assert_equal("second", t.content)
|
`update_column` take ruby-land input, not database-land input
In the case of serialized columns, we would expect the unserialized
value as input, not the serialized value. The original issue which made
this distinction, #14163, introduced a bug. If you passed serialized
input to the method, it would double serialize when it was sent to the
database. You would see the wrong input upon reloading, or get an error
if you had a specific type on the serialized column.
To put it another way, `update_column` is a special case of
`update_all`, which would take `['a']` and not `['a'].to_yaml`, but you
would not pass data from `params` to it.
Fixes #18037
2014-12-16 17:19:47 -05:00
|
|
|
assert_equal("second", t.reload.content)
|
2014-04-05 13:19:18 -04:00
|
|
|
end
|
2014-12-23 11:38:48 -05:00
|
|
|
|
|
|
|
def test_nil_is_not_changed_when_serialized_with_a_class
|
|
|
|
Topic.serialize(:content, Array)
|
|
|
|
|
|
|
|
topic = Topic.new(content: nil)
|
|
|
|
|
2018-01-25 18:14:09 -05:00
|
|
|
assert_not_predicate topic, :content_changed?
|
2014-12-23 11:38:48 -05:00
|
|
|
end
|
2014-12-27 21:46:36 -05:00
|
|
|
|
|
|
|
def test_classes_without_no_arg_constructors_are_not_supported
|
|
|
|
assert_raises(ArgumentError) do
|
|
|
|
Topic.serialize(:content, Regexp)
|
|
|
|
end
|
|
|
|
end
|
2015-03-23 00:17:24 -04:00
|
|
|
|
|
|
|
def test_newly_emptied_serialized_hash_is_changed
|
|
|
|
Topic.serialize(:content, Hash)
|
|
|
|
topic = Topic.create(content: { "things" => "stuff" })
|
|
|
|
topic.content.delete("things")
|
|
|
|
topic.save!
|
|
|
|
topic.reload
|
|
|
|
|
|
|
|
assert_equal({}, topic.content)
|
|
|
|
end
|
2015-06-11 18:56:33 -04:00
|
|
|
|
|
|
|
def test_values_cast_from_nil_are_persisted_as_nil
|
|
|
|
# This is required to fulfil the following contract, which must be universally
|
|
|
|
# true in Active Record:
|
|
|
|
#
|
|
|
|
# model.attribute = value
|
|
|
|
# assert_equal model.attribute, model.tap(&:save).reload.attribute
|
|
|
|
Topic.serialize(:content, Hash)
|
|
|
|
topic = Topic.create!(content: {})
|
|
|
|
topic2 = Topic.create!(content: nil)
|
|
|
|
|
2018-08-18 23:22:46 -04:00
|
|
|
assert_equal [topic, topic2], Topic.where(content: nil).sort_by(&:id)
|
2015-06-11 18:56:33 -04:00
|
|
|
end
|
2015-06-12 15:30:18 -04:00
|
|
|
|
|
|
|
def test_nil_is_always_persisted_as_null
|
|
|
|
Topic.serialize(:content, Hash)
|
|
|
|
|
|
|
|
topic = Topic.create!(content: { foo: "bar" })
|
|
|
|
topic.update_attribute :content, nil
|
|
|
|
assert_equal [topic], Topic.where(content: nil)
|
|
|
|
end
|
2016-05-12 09:06:29 -04:00
|
|
|
|
|
|
|
def test_mutation_detection_does_not_double_serialize
|
|
|
|
coder = Object.new
|
|
|
|
def coder.dump(value)
|
|
|
|
return if value.nil?
|
|
|
|
value + " encoded"
|
|
|
|
end
|
|
|
|
def coder.load(value)
|
|
|
|
return if value.nil?
|
|
|
|
value.gsub(" encoded", "")
|
|
|
|
end
|
2016-10-23 02:19:54 -04:00
|
|
|
type = Class.new(ActiveModel::Type::Value) do
|
|
|
|
include ActiveModel::Type::Helpers::Mutable
|
2016-05-12 09:06:29 -04:00
|
|
|
|
|
|
|
def serialize(value)
|
|
|
|
return if value.nil?
|
|
|
|
value + " serialized"
|
|
|
|
end
|
|
|
|
|
|
|
|
def deserialize(value)
|
|
|
|
return if value.nil?
|
|
|
|
value.gsub(" serialized", "")
|
|
|
|
end
|
|
|
|
end.new
|
|
|
|
model = Class.new(Topic) do
|
|
|
|
attribute :foo, type
|
|
|
|
serialize :foo, coder
|
|
|
|
end
|
|
|
|
|
|
|
|
topic = model.create!(foo: "bar")
|
|
|
|
topic.foo
|
2018-01-25 18:14:09 -05:00
|
|
|
assert_not_predicate topic, :changed?
|
2016-05-12 09:06:29 -04:00
|
|
|
end
|
2017-05-24 19:38:45 -04:00
|
|
|
|
|
|
|
def test_serialized_attribute_works_under_concurrent_initial_access
|
2019-02-28 04:17:33 -05:00
|
|
|
model = Topic.dup
|
2017-05-24 19:38:45 -04:00
|
|
|
|
2019-02-28 04:17:33 -05:00
|
|
|
topic = model.create!
|
2017-05-24 19:38:45 -04:00
|
|
|
topic.update group: "1"
|
|
|
|
|
|
|
|
model.serialize :group, JSON
|
2019-02-28 04:17:33 -05:00
|
|
|
model.reset_column_information
|
2017-05-24 19:38:45 -04:00
|
|
|
|
|
|
|
# This isn't strictly necessary for the test, but a little bit of
|
|
|
|
# knowledge of internals allows us to make failures far more likely.
|
|
|
|
model.define_singleton_method(:define_attribute) do |*args|
|
|
|
|
Thread.pass
|
|
|
|
super(*args)
|
|
|
|
end
|
|
|
|
|
|
|
|
threads = 4.times.map do
|
|
|
|
Thread.new do
|
|
|
|
topic.reload.group
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# All the threads should retrieve the value knowing it is JSON, and
|
|
|
|
# thus decode it. If this fails, some threads will instead see the
|
|
|
|
# raw string ("1"), or raise an exception.
|
|
|
|
assert_equal [1] * threads.size, threads.map(&:value)
|
|
|
|
end
|
2012-08-26 01:54:46 -04:00
|
|
|
end
|