123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- require 'rails_helper'
- RSpec.describe ObjectManager::Attribute, type: :model do
- describe 'callbacks' do
- context 'for setting default values on local data options' do
- subject(:attr) { described_class.new }
- context ':null' do
- it 'sets nil values to true' do
- expect { attr.validate }
- .to change { attr.data_option[:null] }.to(true)
- end
- it 'does not overwrite false values' do
- attr.data_option[:null] = false
- expect { attr.validate }
- .not_to change { attr.data_option[:null] }
- end
- end
- context ':maxlength' do
- context 'for data_type: select / tree_select / checkbox' do
- subject(:attr) { described_class.new(data_type: 'select') }
- it 'sets nil values to 255' do
- expect { attr.validate }
- .to change { attr.data_option[:maxlength] }.to(255)
- end
- end
- end
- context ':nulloption' do
- context 'for data_type: select / tree_select / checkbox' do
- subject(:attr) { described_class.new(data_type: 'select') }
- it 'sets nil values to true' do
- expect { attr.validate }
- .to change { attr.data_option[:nulloption] }.to(true)
- end
- it 'does not overwrite false values' do
- attr.data_option[:nulloption] = false
- expect { attr.validate }
- .not_to change { attr.data_option[:nulloption] }
- end
- end
- end
- end
- end
- describe 'check name' do
- it 'rejects ActiveRecord reserved word "attribute"' do
- expect do
- described_class.add attributes_for :object_manager_attribute_text, name: 'attribute'
- end.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Name attribute is a reserved word')
- end
- %w[destroy true false integer select drop create alter index table varchar blob date datetime timestamp url icon initials avatar permission validate subscribe unsubscribe translate search _type _doc _id id action].each do |reserved_word|
- it "rejects Zammad reserved word '#{reserved_word}'" do
- expect do
- described_class.add attributes_for :object_manager_attribute_text, name: reserved_word
- end.to raise_error(ActiveRecord::RecordInvalid, %r{is a reserved word})
- end
- end
- %w[someting_id something_ids].each do |reserved_word|
- it "rejects word '#{reserved_word}' which is used for database references" do
- expect do
- described_class.add attributes_for :object_manager_attribute_text, name: reserved_word
- end.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Name can't be used because *_id and *_ids are not allowed")
- end
- end
- %w[title tags number].each do |not_editable_attribute|
- it "rejects '#{not_editable_attribute}' which is used" do
- expect do
- described_class.add attributes_for :object_manager_attribute_text, name: not_editable_attribute
- end.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Name attribute is not editable')
- end
- end
- %w[priority state note].each do |existing_attribute|
- it "rejects '#{existing_attribute}' which is used" do
- expect do
- described_class.add attributes_for :object_manager_attribute_text, name: existing_attribute
- end.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Name #{existing_attribute} already exists")
- end
- end
- it 'rejects duplicate attribute name of conflicting types' do
- attribute = attributes_for(:object_manager_attribute_text)
- described_class.add attribute
- attribute[:data_type] = 'boolean'
- expect do
- described_class.add attribute
- end.to raise_error ActiveRecord::RecordInvalid
- end
- it 'accepts duplicate attribute name on the same types (editing an existing attribute)' do
- attribute = attributes_for(:object_manager_attribute_text)
- described_class.add attribute
- expect do
- described_class.add attribute
- end.not_to raise_error
- end
- it 'accepts duplicate attribute name on compatible types (editing the type of an existing attribute)' do
- attribute = attributes_for(:object_manager_attribute_text)
- described_class.add attribute
- attribute[:data_type] = 'select'
- attribute[:data_option_new] = { default: '', options: { 'a' => 'a' } }
- expect do
- described_class.add attribute
- end.not_to raise_error
- end
- it 'accepts valid attribute names' do
- expect do
- described_class.add attributes_for :object_manager_attribute_text
- end.not_to raise_error
- end
- end
- describe 'validate that referenced attributes are not set as inactive' do
- subject(:attr) { create(:object_manager_attribute_text) }
- before do
- allow(described_class)
- .to receive(:attribute_used_by_references?)
- .with(attr.object_lookup.name, attr.name)
- .and_return(is_referenced)
- attr.active = active
- end
- context 'when is used and changing to inactive' do
- let(:active) { false }
- let(:is_referenced) { true }
- it { is_expected.not_to be_valid }
- it do
- attr.valid?
- expect(attr.errors).not_to be_blank
- end
- end
- context 'when is not used and changing to inactive' do
- let(:active) { false }
- let(:is_referenced) { false }
- it { is_expected.to be_valid }
- end
- context 'when is used and staying active and chan' do
- let(:active) { true }
- let(:is_referenced) { true }
- it { is_expected.to be_valid }
- end
- end
- describe 'Class methods:' do
- describe '.pending_migration?', db_strategy: :reset do
- it 'returns false if there are no pending migrations' do
- expect(described_class.pending_migration?).to be false
- end
- it 'returns true if there are pending migrations' do
- create(:object_manager_attribute_text)
- expect(described_class.pending_migration?).to be true
- end
- it 'returns false if migration was executed' do
- create(:object_manager_attribute_text)
- described_class.migration_execute
- expect(described_class.pending_migration?).to be false
- end
- end
- describe '.attribute_to_references_hash_objects' do
- it 'returns classes with conditions' do
- expect(described_class.attribute_to_references_hash_objects).to contain_exactly(Trigger, Overview, Job, Sla, Report::Profile)
- end
- end
- describe '.data_options_hash' do
- context 'when hash' do
- let(:check) do
- {
- 'a' => 'A',
- 'b' => 'B',
- 'c' => 'c',
- }
- end
- it 'does return the options as hash' do
- expect(described_class.data_options_hash(check)).to eq({
- 'a' => 'A',
- 'b' => 'B',
- 'c' => 'c',
- })
- end
- end
- context 'when array' do
- let(:check) do
- [
- {
- value: 'a',
- name: 'A',
- },
- {
- value: 'b',
- name: 'B',
- },
- {
- value: 'c',
- name: 'c',
- },
- ]
- end
- it 'does return the options as hash' do
- expect(described_class.data_options_hash(check)).to eq({
- 'a' => 'A',
- 'b' => 'B',
- 'c' => 'c',
- })
- end
- end
- context 'when tree array' do
- let(:check) do
- [
- {
- value: 'a',
- name: 'A',
- },
- {
- value: 'b',
- name: 'B',
- },
- {
- value: 'c',
- name: 'c',
- children: [
- {
- value: 'c::a',
- name: 'c sub a',
- },
- {
- value: 'c::b',
- name: 'c sub b',
- },
- {
- value: 'c::c',
- name: 'c sub c',
- },
- ],
- },
- ]
- end
- it 'does return the options as hash' do
- expect(described_class.data_options_hash(check)).to eq({
- 'a' => 'A',
- 'b' => 'B',
- 'c' => 'c',
- 'c::a' => 'c sub a',
- 'c::b' => 'c sub b',
- 'c::c' => 'c sub c',
- })
- end
- end
- end
- end
- describe '#data_option_validations' do
- context 'when maxlength is checked for non-integers' do
- shared_examples 'tests the exception on invalid maxlength values' do |type|
- context "when type '#{type}'" do
- subject(:attr) { described_class.new(data_type: type, data_option: { maxlength: 'brbrbr' }) }
- it 'does throw an exception' do
- expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{Data option must have integer for :maxlength})
- end
- end
- end
- include_examples 'tests the exception on invalid maxlength values', 'input'
- include_examples 'tests the exception on invalid maxlength values', 'textarea'
- include_examples 'tests the exception on invalid maxlength values', 'richtext'
- end
- context 'when type is checked' do
- shared_examples 'tests the exception on invalid types' do |type|
- context "when type '#{type}'" do
- subject(:attr) { described_class.new(data_type: type, data_option: { type: 'brbrbr' }) }
- it 'does throw an exception' do
- expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{must have one of text/password/tel/fax/email/url for :type})
- end
- end
- end
- include_examples 'tests the exception on invalid types', 'input'
- end
- context 'when min max values are checked' do
- shared_examples 'tests the exception on invalid min max values' do |type|
- context "when type '#{type}'" do
- context 'when no integer for min' do
- subject(:attr) { described_class.new(data_type: type, data_option: { min: 'brbrbr' }) }
- it 'does throw an exception' do
- expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{must have integer for :min})
- end
- end
- context 'when no integer for max' do
- subject(:attr) { described_class.new(data_type: type, data_option: { max: 'brbrbr' }) }
- it 'does throw an exception' do
- expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{must have integer for :max})
- end
- end
- context 'when high integer for min' do
- subject(:attr) { described_class.new(data_type: type, data_option: { min: 999_999_999_999 }) }
- it 'does throw an exception' do
- expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{min must be lower than 2147483648})
- end
- end
- context 'when high integer for max' do
- subject(:attr) { described_class.new(data_type: type, data_option: { max: 999_999_999_999 }) }
- it 'does throw an exception' do
- expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{max must be lower than 2147483648})
- end
- end
- context 'when negative high integer for min' do
- subject(:attr) { described_class.new(data_type: type, data_option: { min: -999_999_999_999 }) }
- it 'does throw an exception' do
- expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{min must be higher than -2147483648})
- end
- end
- context 'when negative high integer for max' do
- subject(:attr) { described_class.new(data_type: type, data_option: { max: -999_999_999_999 }) }
- it 'does throw an exception' do
- expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{max must be higher than -2147483648})
- end
- end
- context 'when min is greater than max' do
- subject(:attr) { described_class.new(data_type: type, data_option: { min: 5, max: 2 }) }
- it 'does throw an exception' do
- expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{min must be lower than max})
- end
- end
- end
- end
- include_examples 'tests the exception on invalid min max values', 'integer'
- end
- context 'when default is checked' do
- shared_examples 'tests the exception on missing default' do |type|
- context "when type '#{type}'" do
- subject(:attr) { described_class.new(data_type: type, data_option: {}) }
- it 'does throw an exception' do
- expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{must have value for :default})
- end
- end
- end
- include_examples 'tests the exception on missing default', 'select'
- include_examples 'tests the exception on missing default', 'tree_select'
- include_examples 'tests the exception on missing default', 'checkbox'
- include_examples 'tests the exception on missing default', 'boolean'
- end
- context 'when relation is checked' do
- shared_examples 'tests the exception on missing relation' do |type|
- context "when type '#{type}'" do
- subject(:attr) { described_class.new(data_type: type, data_option: {}) }
- it 'does throw an exception' do
- expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{must have non-nil value for either :options or :relation})
- end
- end
- end
- include_examples 'tests the exception on missing relation', 'select'
- include_examples 'tests the exception on missing relation', 'tree_select'
- include_examples 'tests the exception on missing relation', 'checkbox'
- end
- context 'when nil options are checked' do
- shared_examples 'tests the exception on missing nil options' do |type|
- context "when type '#{type}'" do
- subject(:attr) { described_class.new(data_type: type, data_option: {}) }
- it 'does throw an exception' do
- expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{must have non-nil value for :options})
- end
- end
- end
- include_examples 'tests the exception on missing nil options', 'boolean'
- end
- context 'when future is checked' do
- shared_examples 'tests the exception on missing future' do |type|
- context "when type '#{type}'" do
- subject(:attr) { described_class.new(data_type: type, data_option: {}) }
- it 'does throw an exception' do
- expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{must have boolean value for :future})
- end
- end
- end
- include_examples 'tests the exception on missing future', 'datetime'
- end
- context 'when past is checked' do
- shared_examples 'tests the exception on missing past' do |type|
- context "when type '#{type}'" do
- subject(:attr) { described_class.new(data_type: type, data_option: {}) }
- it 'does throw an exception' do
- expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{must have boolean value for :past})
- end
- end
- end
- include_examples 'tests the exception on missing past', 'datetime'
- end
- end
- describe 'undefined method `to_hash` on editing select fields in the admin interface after migration to 5.1 #4027', db_strategy: :reset do
- let(:select_field) { create(:object_manager_attribute_select) }
- before do
- described_class.migration_execute
- end
- it 'does save the attribute with sorted options' do
- add = select_field.attributes.deep_symbolize_keys
- add[:data_option_new] = add[:data_option]
- add[:data_option_new][:options] = [
- {
- name: 'a',
- value: 'a',
- },
- {
- name: 'b',
- value: 'b',
- },
- {
- name: 'c',
- value: 'c',
- },
- ]
- described_class.add(add)
- described_class.migration_execute
- expect_result = {
- 'key_1' => 'value_1',
- 'key_2' => 'value_2',
- 'key_3' => 'value_3',
- 'a' => 'a',
- 'b' => 'b',
- 'c' => 'c'
- }
- expect(select_field.reload.data_option[:historical_options]).to eq(expect_result)
- end
- end
- describe '#add' do
- context 'when data is valid' do
- let(:attribute) do
- {
- object: 'Ticket',
- name: 'test1',
- display: 'Test 1',
- data_type: 'input',
- data_option: {
- maxlength: 200,
- type: 'text',
- null: false,
- },
- active: true,
- screens: {},
- position: 20,
- created_by_id: 1,
- updated_by_id: 1,
- editable: false,
- to_migrate: false,
- }
- end
- it 'is successful' do
- expect { described_class.add(attribute) }.to change(described_class, :count)
- expect(described_class.get(object: 'Ticket', name: 'test1')).to have_attributes(attribute)
- end
- end
- context 'when data is invalid' do
- let(:attribute) do
- {
- object: 'Ticket',
- name: 'test2_id',
- display: 'Test 2 with id',
- data_type: 'input',
- data_option: {
- maxlength: 200,
- type: 'text',
- null: false,
- },
- active: true,
- screens: {},
- position: 20,
- created_by_id: 1,
- updated_by_id: 1,
- }
- end
- it 'raises an error' do
- expect { described_class.add(attribute) }.to raise_error(ActiveRecord::RecordInvalid)
- end
- end
- context 'when adding a json field' do
- let(:expected_attributes) do
- {
- data_type: 'autocompletion_ajax_external_data_source',
- active: true,
- }
- end
- let(:attribute) { create(:object_manager_attribute_autocompletion_ajax_external_data_source) }
- it 'works on postgresql', db_adapter: :postgresql do
- expect { attribute }.to change(described_class, :count)
- expect(attribute).to have_attributes(expected_attributes)
- end
- it 'fails on mysql', db_adapter: :mysql do
- expect { attribute }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Data type can only be created on postgresql databases')
- end
- end
- end
- describe '#get' do
- context 'when attribute exists' do
- before do
- create(:object_manager_attribute_text, name: 'test3')
- end
- it 'returns the attribute' do
- expect(described_class.get(object: 'Ticket', name: 'test3')).to have_attributes(name: 'test3', editable: true)
- end
- end
- context 'when attribute does not exist' do
- it 'returns nil' do
- expect(described_class.get(object: 'Ticket', name: 'test4')).to be_nil
- end
- end
- end
- describe '#remove' do
- context 'when attribute exists' do
- before do
- create(:object_manager_attribute_text, name: 'test3')
- end
- it 'is successful' do
- expect { described_class.remove(object: 'Ticket', name: 'test3') }.to change(described_class, :count)
- expect(described_class.get(object: 'Ticket', name: 'test3')).to be_nil
- end
- end
- context 'when attribute does not exist' do
- it 'raises an error' do
- expect { described_class.remove(object: 'Ticket', name: 'test4') }.to raise_error(RuntimeError)
- end
- end
- end
- end
|