discourse/spec/lib/concern/has_custom_fields_spec.rb
David Taylor 922fbe82da
DEV: Ensure custom_fields_clean? returns false when values change (#16552)
We were calling `dup` on the hash and using that to check for changes. However, we were not duplicating the values, so changes to arrays or nested hashes would not be detected.
2022-04-25 17:19:39 +01:00

434 lines
15 KiB
Ruby

# frozen_string_literal: true
describe HasCustomFields do
context "custom_fields" do
before do
DB.exec("create temporary table custom_fields_test_items(id SERIAL primary key)")
DB.exec("create temporary table custom_fields_test_item_custom_fields(id SERIAL primary key, custom_fields_test_item_id int, name varchar(256) not null, value text, created_at TIMESTAMP, updated_at TIMESTAMP)")
DB.exec(<<~SQL)
CREATE UNIQUE INDEX ON custom_fields_test_item_custom_fields (custom_fields_test_item_id)
WHERE NAME = 'rare'
SQL
class CustomFieldsTestItem < ActiveRecord::Base
include HasCustomFields
end
class CustomFieldsTestItemCustomField < ActiveRecord::Base
belongs_to :custom_fields_test_item
end
end
after do
DB.exec("drop table custom_fields_test_items")
DB.exec("drop table custom_fields_test_item_custom_fields")
# this weakref in the descendant tracker should clean up the two tests
# if this becomes an issue we can revisit (watch out for erratic tests)
Object.send(:remove_const, :CustomFieldsTestItem)
Object.send(:remove_const, :CustomFieldsTestItemCustomField)
end
it "allows preloading of custom fields" do
test_item = CustomFieldsTestItem.new
CustomFieldsTestItem.preload_custom_fields([test_item], ["test_field"])
expect(test_item.preloaded_custom_fields).to eq({ "test_field" => nil })
end
it "errors if a custom field is not preloaded" do
test_item = CustomFieldsTestItem.new
CustomFieldsTestItem.preload_custom_fields([test_item], ["test_field"])
expect { test_item.custom_fields["other_field"] }.to raise_error(HasCustomFields::NotPreloadedError)
end
it "resets the preloaded_custom_fields if preload_custom_fields is called twice" do
test_item = CustomFieldsTestItem.new
CustomFieldsTestItem.preload_custom_fields([test_item], ["test_field"])
CustomFieldsTestItem.preload_custom_fields([test_item], ["other_field"])
expect(test_item.preloaded_custom_fields).to eq({ "other_field" => nil })
end
it "does not error with NotPreloadedError if preload_custom_fields is called twice" do
test_item = CustomFieldsTestItem.new
CustomFieldsTestItem.preload_custom_fields([test_item], ["test_field"])
expect { test_item.custom_fields["test_field"] }.not_to raise_error
CustomFieldsTestItem.preload_custom_fields([test_item], ["other_field"])
expect { test_item.custom_fields["other_field"] }.not_to raise_error
end
it "allows simple modification of custom fields" do
test_item = CustomFieldsTestItem.new
expect(test_item.custom_fields["a"]).to eq(nil)
test_item.custom_fields["bob"] = "marley"
test_item.custom_fields["jack"] = "black"
test_item.save
test_item = CustomFieldsTestItem.find(test_item.id)
expect(test_item.custom_fields["bob"]).to eq("marley")
expect(test_item.custom_fields["jack"]).to eq("black")
test_item.custom_fields.delete("bob")
test_item.custom_fields["jack"] = "jill"
test_item.save
test_item = CustomFieldsTestItem.find(test_item.id)
expect(test_item.custom_fields).to eq("jack" => "jill")
end
it "casts integers to string without error" do
test_item = CustomFieldsTestItem.new
expect(test_item.custom_fields["a"]).to eq(nil)
test_item.custom_fields["a"] = 0
expect(test_item.custom_fields["a"]).to eq(0)
test_item.save
# should be casted right after saving
expect(test_item.custom_fields["a"]).to eq("0")
test_item = CustomFieldsTestItem.find(test_item.id)
expect(test_item.custom_fields["a"]).to eq("0")
end
it "reloads from the database" do
test_item = CustomFieldsTestItem.new
test_item.custom_fields["a"] = 0
expect(test_item.custom_fields["a"]).to eq(0)
test_item.save
# should be casted right after saving
expect(test_item.custom_fields["a"]).to eq("0")
DB.exec("UPDATE custom_fields_test_item_custom_fields SET value='1' WHERE custom_fields_test_item_id=? AND name='a'", test_item.id)
# still the same, did not load
expect(test_item.custom_fields["a"]).to eq("0")
# refresh loads from database
expect(test_item.reload.custom_fields["a"]).to eq("1")
expect(test_item.custom_fields["a"]).to eq("1")
end
it "actually saves on double save" do
test_item = CustomFieldsTestItem.new
test_item.custom_fields = { "a" => "b" }
test_item.save
test_item.custom_fields["c"] = "d"
test_item.save
db_item = CustomFieldsTestItem.find(test_item.id)
expect(db_item.custom_fields).to eq("a" => "b", "c" => "d")
end
it "handles arrays properly" do
CustomFieldsTestItem.register_custom_field_type "array", [:integer]
test_item = CustomFieldsTestItem.new
test_item.custom_fields = { "array" => ["1"] }
test_item.save
db_item = CustomFieldsTestItem.find(test_item.id)
expect(db_item.custom_fields).to eq("array" => [1])
test_item = CustomFieldsTestItem.new
test_item.custom_fields = { "a" => ["b", "c", "d"] }
test_item.save
db_item = CustomFieldsTestItem.find(test_item.id)
expect(db_item.custom_fields).to eq("a" => ["b", "c", "d"])
db_item.custom_fields.update('a' => ['c', 'd'])
db_item.save
expect(db_item.custom_fields).to eq("a" => ["c", "d"])
# It can be updated to the exact same value
db_item.custom_fields.update('a' => ['c'])
db_item.save
expect(db_item.custom_fields).to eq("a" => "c")
db_item.custom_fields.update('a' => ['c'])
db_item.save
expect(db_item.custom_fields).to eq("a" => "c")
db_item.custom_fields.delete('a')
expect(db_item.custom_fields).to eq({})
end
it "deletes nil-filled arrays" do
test_item = CustomFieldsTestItem.create!
db_item = CustomFieldsTestItem.find(test_item.id)
db_item.custom_fields.update("a" => [nil, nil])
db_item.save_custom_fields
db_item.custom_fields.delete("a")
expect(db_item.custom_fields).to eq({})
db_item.save_custom_fields
expect(db_item.custom_fields).to eq({})
end
it "casts integers in arrays properly without error" do
test_item = CustomFieldsTestItem.new
test_item.custom_fields = { "a" => ["b", 10, "d"] }
test_item.save
expect(test_item.custom_fields).to eq("a" => ["b", "10", "d"])
db_item = CustomFieldsTestItem.find(test_item.id)
expect(db_item.custom_fields).to eq("a" => ["b", "10", "d"])
end
it "supports type coercion" do
test_item = CustomFieldsTestItem.new
CustomFieldsTestItem.register_custom_field_type("bool", :boolean)
CustomFieldsTestItem.register_custom_field_type("int", :integer)
CustomFieldsTestItem.register_custom_field_type("json", :json)
test_item.custom_fields = { "bool" => true, "int" => 1, "json" => { "foo" => "bar" } }
test_item.save
test_item.reload
expect(test_item.custom_fields).to eq("bool" => true, "int" => 1, "json" => { "foo" => "bar" })
before_ids = CustomFieldsTestItemCustomField.where(custom_fields_test_item_id: test_item.id).pluck(:id)
test_item.custom_fields["bool"] = false
test_item.save
after_ids = CustomFieldsTestItemCustomField.where(custom_fields_test_item_id: test_item.id).pluck(:id)
# we updated only 1 custom field, so there should be only 1 different id
expect((before_ids - after_ids).size).to eq(1)
end
it "doesn't allow simple modifications to interfere" do
test_item = CustomFieldsTestItem.new
expect(test_item.custom_fields["a"]).to eq(nil)
test_item.custom_fields["bob"] = "marley"
test_item.custom_fields["jack"] = "black"
test_item.save
test_item2 = CustomFieldsTestItem.new
expect(test_item2.custom_fields["x"]).to eq(nil)
test_item2.custom_fields["sixto"] = "rodriguez"
test_item2.custom_fields["de"] = "la playa"
test_item2.save
test_item = CustomFieldsTestItem.find(test_item.id)
test_item2 = CustomFieldsTestItem.find(test_item2.id)
expect(test_item.custom_fields).to eq("jack" => "black", "bob" => "marley")
expect(test_item2.custom_fields).to eq("sixto" => "rodriguez", "de" => "la playa")
end
it "supports arrays in json fields" do
field_type = "json_array"
CustomFieldsTestItem.register_custom_field_type(field_type, :json)
item = CustomFieldsTestItem.new
item.custom_fields = {
"json_array" => [{ a: "test" }, { b: "another" }]
}
item.save
item.reload
expect(item.custom_fields[field_type]).to eq(
[{ "a" => "test" }, { "b" => "another" }]
)
item.custom_fields["json_array"] = ['a', 'b']
item.save
item.reload
expect(item.custom_fields[field_type]).to eq(["a", "b"])
end
it "will not fail to load custom fields if json is corrupt" do
field_type = "bad_json"
CustomFieldsTestItem.register_custom_field_type(field_type, :json)
item = CustomFieldsTestItem.create!
CustomFieldsTestItemCustomField.create!(
custom_fields_test_item_id: item.id,
name: field_type,
value: "{test"
)
item = item.reload
expect(item.custom_fields[field_type]).to eq({})
end
it "supports bulk retrieval with a list of ids" do
item1 = CustomFieldsTestItem.new
item1.custom_fields = { "a" => ["b", "c", "d"], 'not_allowlisted' => 'secret' }
item1.save
item2 = CustomFieldsTestItem.new
item2.custom_fields = { "e" => 'hallo' }
item2.save
fields = CustomFieldsTestItem.custom_fields_for_ids([item1.id, item2.id], ['a', 'e'])
expect(fields).to be_present
expect(fields[item1.id]['a']).to match_array(['b', 'c', 'd'])
expect(fields[item1.id]['not_allowlisted']).to be_blank
expect(fields[item2.id]['e']).to eq('hallo')
end
it "handles interleaving saving properly" do
field_type = 'deep-nest-test'
CustomFieldsTestItem.register_custom_field_type(field_type, :json)
test_item = CustomFieldsTestItem.create!
test_item.custom_fields[field_type] ||= {}
test_item.custom_fields[field_type]['b'] ||= {}
test_item.custom_fields[field_type]['b']['c'] = 'd'
test_item.save_custom_fields(true)
db_item = CustomFieldsTestItem.find(test_item.id)
db_item.custom_fields[field_type]['b']['e'] = 'f'
test_item.custom_fields[field_type]['b']['e'] = 'f'
expected = { field_type => { 'b' => { 'c' => 'd', 'e' => 'f' } } }
db_item.save_custom_fields(true)
expect(db_item.reload.custom_fields).to eq(expected)
test_item.save_custom_fields(true)
expect(test_item.reload.custom_fields).to eq(expected)
end
it 'determines clean state correctly for mutable fields' do
json_field = "json_field"
array_field = "array_field"
CustomFieldsTestItem.register_custom_field_type(json_field, :json)
CustomFieldsTestItem.register_custom_field_type(array_field, :array)
item_with_array = CustomFieldsTestItem.new
expect(item_with_array.custom_fields_clean?).to eq(true)
item_with_array.custom_fields[array_field] = [1]
expect(item_with_array.custom_fields_clean?).to eq(false)
item_with_array.save!
expect(item_with_array.custom_fields_clean?).to eq(true)
item_with_array.custom_fields[array_field] << 2
expect(item_with_array.custom_fields_clean?).to eq(false)
item_with_array.save!
expect(item_with_array.custom_fields_clean?).to eq(true)
item_with_json = CustomFieldsTestItem.new
expect(item_with_json.custom_fields_clean?).to eq(true)
item_with_json.custom_fields[json_field] = { "hello" => "world" }
expect(item_with_json.custom_fields_clean?).to eq(false)
item_with_json.save!
expect(item_with_json.custom_fields_clean?).to eq(true)
item_with_json.custom_fields[json_field]["hello"] = "world2"
expect(item_with_json.custom_fields_clean?).to eq(false)
item_with_json.save!
expect(item_with_json.custom_fields_clean?).to eq(true)
end
describe "create_singular" do
it "creates new records" do
item = CustomFieldsTestItem.create!
item.create_singular('hello', 'world')
expect(item.reload.custom_fields['hello']).to eq('world')
end
it "upserts on a database constraint error" do
item0 = CustomFieldsTestItem.new
item0.custom_fields = { "rare" => "gem" }
item0.save
expect(item0.reload.custom_fields['rare']).to eq("gem")
item0.create_singular('rare', "diamond")
expect(item0.reload.custom_fields['rare']).to eq("diamond")
end
end
describe "upsert_custom_fields" do
it 'upserts records' do
test_item = CustomFieldsTestItem.create
test_item.upsert_custom_fields('hello' => 'world', 'abc' => 'def')
# In memory
expect(test_item.custom_fields['hello']).to eq('world')
expect(test_item.custom_fields['abc']).to eq('def')
# Persisted
test_item.reload
expect(test_item.custom_fields['hello']).to eq('world')
expect(test_item.custom_fields['abc']).to eq('def')
# In memory
test_item.upsert_custom_fields('abc' => 'ghi')
expect(test_item.custom_fields['hello']).to eq('world')
expect(test_item.custom_fields['abc']).to eq('ghi')
# Persisted
test_item.reload
expect(test_item.custom_fields['hello']).to eq('world')
expect(test_item.custom_fields['abc']).to eq('ghi')
end
it 'allows upsert to use keywords' do
test_item = CustomFieldsTestItem.create
test_item.upsert_custom_fields(hello: 'world', abc: 'def')
# In memory
expect(test_item.custom_fields['hello']).to eq('world')
expect(test_item.custom_fields['abc']).to eq('def')
# Persisted
test_item.reload
expect(test_item.custom_fields['hello']).to eq('world')
expect(test_item.custom_fields['abc']).to eq('def')
# In memory
test_item.upsert_custom_fields('abc' => 'ghi')
expect(test_item.custom_fields['hello']).to eq('world')
expect(test_item.custom_fields['abc']).to eq('ghi')
# Persisted
test_item.reload
expect(test_item.custom_fields['hello']).to eq('world')
expect(test_item.custom_fields['abc']).to eq('ghi')
end
it 'allows using string and symbol indices interchangeably' do
test_item = CustomFieldsTestItem.new
test_item.custom_fields["bob"] = "marley"
test_item.custom_fields["jack"] = "black"
# In memory
expect(test_item.custom_fields[:bob]).to eq('marley')
expect(test_item.custom_fields[:jack]).to eq('black')
# Persisted
test_item.save
test_item.reload
expect(test_item.custom_fields[:bob]).to eq('marley')
expect(test_item.custom_fields[:jack]).to eq('black')
# Update via string index again
test_item.custom_fields['bob'] = 'the builder'
expect(test_item.custom_fields[:bob]).to eq('the builder')
test_item.save
test_item.reload
expect(test_item.custom_fields[:bob]).to eq('the builder')
end
end
end
end