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/person"
|
|
|
|
require "models/traffic_light"
|
|
|
|
require "models/post"
|
2020-10-14 05:57:31 -04:00
|
|
|
require "models/binary_field"
|
2012-08-26 01:54:46 -04:00
|
|
|
|
|
|
|
class SerializedAttributeTest < ActiveRecord::TestCase
|
2019-03-01 01:15:05 -05:00
|
|
|
fixtures :topics, :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
|
|
|
|
|
2021-02-08 06:47:19 -05:00
|
|
|
def test_serialized_attribute_on_alias_attribute
|
|
|
|
klass = Class.new(ActiveRecord::Base) do
|
|
|
|
self.table_name = Topic.table_name
|
|
|
|
alias_attribute :object, :content
|
|
|
|
serialize :object, MyObject
|
|
|
|
end
|
|
|
|
|
|
|
|
myobj = MyObject.new("value1", "value2")
|
|
|
|
topic = klass.create!(object: myobj)
|
|
|
|
assert_equal(myobj, topic.object)
|
|
|
|
|
|
|
|
topic.reload
|
|
|
|
assert_equal(myobj, topic.object)
|
|
|
|
end
|
|
|
|
|
2020-11-05 00:21:11 -05:00
|
|
|
def test_serialized_attribute_with_default
|
|
|
|
klass = Class.new(ActiveRecord::Base) do
|
|
|
|
self.table_name = Topic.table_name
|
|
|
|
serialize(:content, Hash, default: { key: "value" })
|
|
|
|
end
|
|
|
|
|
|
|
|
t = klass.new
|
|
|
|
assert_equal({ key: "value" }, t.content)
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_serialized_attribute_on_custom_attribute_with_default
|
|
|
|
klass = Class.new(ActiveRecord::Base) do
|
|
|
|
self.table_name = Topic.table_name
|
|
|
|
attribute :content, default: { key: "value" }
|
|
|
|
serialize :content, Hash
|
|
|
|
end
|
|
|
|
|
|
|
|
t = klass.new
|
|
|
|
assert_equal({ key: "value" }, t.content)
|
|
|
|
end
|
|
|
|
|
2012-08-26 01:54:46 -04:00
|
|
|
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
|
|
|
|
2020-10-14 05:57:31 -04:00
|
|
|
if current_adapter?(:Mysql2Adapter)
|
|
|
|
def test_is_not_changed_when_stored_in_mysql_blob
|
|
|
|
value = %w(Fée)
|
|
|
|
model = BinaryField.create!(normal_blob: value, normal_text: value)
|
|
|
|
model.reload
|
|
|
|
|
|
|
|
model.normal_text = value
|
|
|
|
assert_not_predicate model, :normal_text_changed?
|
|
|
|
|
|
|
|
model.normal_blob = value
|
|
|
|
assert_not_predicate model, :normal_blob_changed?
|
|
|
|
end
|
2020-10-20 03:32:21 -04:00
|
|
|
|
|
|
|
class FrozenBinaryField < BinaryField
|
|
|
|
class FrozenCoder < ActiveRecord::Coders::YAMLColumn
|
|
|
|
def dump(obj)
|
|
|
|
super&.freeze
|
|
|
|
end
|
|
|
|
end
|
|
|
|
serialize :normal_blob, FrozenCoder.new(:normal_blob, Array)
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_is_not_changed_when_stored_in_mysql_blob_frozen_payload
|
|
|
|
value = %w(Fée)
|
|
|
|
model = FrozenBinaryField.create!(normal_blob: value, normal_text: value)
|
|
|
|
model.reload
|
|
|
|
|
|
|
|
model.normal_blob = value
|
|
|
|
assert_not_predicate model, :normal_blob_changed?
|
|
|
|
end
|
2020-10-14 05:57:31 -04:00
|
|
|
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
|
|
|
|
2021-01-16 00:25:44 -05:00
|
|
|
class EncryptedType < ActiveRecord::Type::Text
|
|
|
|
include ActiveModel::Type::Helpers::Mutable
|
|
|
|
|
|
|
|
attr_reader :subtype, :encryptor
|
|
|
|
|
|
|
|
def initialize(subtype: ActiveModel::Type::String.new)
|
|
|
|
super()
|
|
|
|
|
|
|
|
@subtype = subtype
|
|
|
|
@encryptor = ActiveSupport::MessageEncryptor.new("abcd" * 8)
|
|
|
|
end
|
|
|
|
|
|
|
|
def serialize(value)
|
|
|
|
subtype.serialize(value).yield_self do |cleartext|
|
|
|
|
encryptor.encrypt_and_sign(cleartext) unless cleartext.nil?
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def deserialize(ciphertext)
|
|
|
|
encryptor.decrypt_and_verify(ciphertext)
|
|
|
|
.yield_self { |cleartext| subtype.deserialize(cleartext) } unless ciphertext.nil?
|
|
|
|
end
|
|
|
|
|
|
|
|
def changed_in_place?(old, new)
|
|
|
|
if old.nil?
|
|
|
|
!new.nil?
|
|
|
|
else
|
|
|
|
deserialize(old) != new
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_decorated_type_with_type_for_attribute
|
|
|
|
old_registry = ActiveRecord::Type.registry
|
|
|
|
ActiveRecord::Type.registry = ActiveRecord::Type.registry.dup
|
|
|
|
ActiveRecord::Type.register :encrypted, EncryptedType
|
|
|
|
|
|
|
|
klass = Class.new(ActiveRecord::Base) do
|
|
|
|
self.table_name = Topic.table_name
|
|
|
|
store :content
|
|
|
|
attribute :content, :encrypted, subtype: type_for_attribute(:content)
|
|
|
|
end
|
|
|
|
|
|
|
|
topic = klass.create!(content: { trial: true })
|
|
|
|
|
|
|
|
assert_equal({ "trial" => true }, topic.content)
|
|
|
|
ensure
|
|
|
|
ActiveRecord::Type.registry = old_registry
|
|
|
|
end
|
|
|
|
|
2021-01-16 19:12:25 -05:00
|
|
|
def test_decorated_type_with_decorator_block
|
|
|
|
klass = Class.new(ActiveRecord::Base) do
|
|
|
|
self.table_name = Topic.table_name
|
|
|
|
store :content
|
|
|
|
attribute(:content) { |subtype| EncryptedType.new(subtype: subtype) }
|
|
|
|
end
|
|
|
|
|
|
|
|
topic = klass.create!(content: { trial: true })
|
|
|
|
|
|
|
|
assert_equal({ "trial" => true }, topic.content)
|
|
|
|
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-03-01 02:12:39 -05:00
|
|
|
model = Class.new(Topic)
|
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.
|
2019-09-14 02:57:47 -04:00
|
|
|
model.define_singleton_method(:define_attribute) do |*args, **options|
|
2017-05-24 19:38:45 -04:00
|
|
|
Thread.pass
|
2019-09-14 02:57:47 -04:00
|
|
|
super(*args, **options)
|
2017-05-24 19:38:45 -04:00
|
|
|
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
|