diff --git a/app/models/miq_product_feature.rb b/app/models/miq_product_feature.rb index 6b96c2d68b3..597480e3697 100644 --- a/app/models/miq_product_feature.rb +++ b/app/models/miq_product_feature.rb @@ -6,6 +6,8 @@ class MiqProductFeature < ApplicationRecord ALL_TASKS_FEATURE = "miq_task_all_ui".freeze TENANT_ADMIN_FEATURE = "rbac_tenant".freeze + include_concern "Seeding" + acts_as_tree has_and_belongs_to_many :miq_user_roles, :join_table => :miq_roles_features @@ -18,8 +20,6 @@ class MiqProductFeature < ApplicationRecord validates_presence_of :identifier validates_uniqueness_of :identifier - FIXTURE_PATH = Rails.root.join(*["db", "fixtures", table_name]) - DETAIL_ATTRS = [ :name, :description, @@ -62,10 +62,6 @@ def self.current_tenant_identifier(identifier) tenant_identifier(identifier, User.current_tenant.id) if identifier && feature_details(identifier) && root_tenant_identifier?(identifier) end - def self.feature_yaml(path = FIXTURE_PATH) - "#{path}.yml".freeze - end - def self.feature_root features.keys.detect { |k| feature_parent(k).nil? } end @@ -169,21 +165,30 @@ def self.seed_tenant_miq_product_features Tenant.in_my_region.all.flat_map { |t| seed_single_tenant_miq_product_features(t) } end - def self.seed_features(path = FIXTURE_PATH) - fixture_yaml = feature_yaml(path) + def self.seed_features + transaction do + features = all.index_by(&:identifier) + + root_file, other_files = seed_files - features = all.to_a.index_by(&:identifier) - seen = seed_from_hash(YAML.load_file(fixture_yaml), seen, nil, features) + seen = seed_from_hash(YAML.load_file(root_file), seen, nil, features) + root_feature = find_by(:identifier => SUPER_ADMIN_FEATURE) - root_feature = MiqProductFeature.find_by(:identifier => SUPER_ADMIN_FEATURE) - Dir.glob(path.join("*.yml")).each do |fixture| - seed_from_hash(YAML.load_file(fixture), seen, root_feature) + other_files.each do |file| + seed_from_array(YAML.load_file(file), seen, root_feature) + end + + tenant_identifiers = seed_tenant_miq_product_features + deletes = where.not(:identifier => seen.values.flatten + tenant_identifiers).destroy_all + _log.info("Deleting product features: #{deletes.collect(&:identifier).inspect}") unless deletes.empty? + seen end + end - tenant_identifiers = seed_tenant_miq_product_features - deletes = where.not(:identifier => seen.values.flatten + tenant_identifiers).destroy_all - _log.info("Deleting product features: #{deletes.collect(&:identifier).inspect}") unless deletes.empty? - seen + def self.seed_from_array(array, seen = nil, parent = nil, features = nil) + Array.wrap(array).each do |hash| + seed_from_hash(hash, seen, parent, features) + end end def self.seed_from_hash(hash, seen = nil, parent = nil, features = nil) diff --git a/app/models/miq_product_feature/seeding.rb b/app/models/miq_product_feature/seeding.rb new file mode 100644 index 00000000000..488fb0ddd65 --- /dev/null +++ b/app/models/miq_product_feature/seeding.rb @@ -0,0 +1,30 @@ +class MiqProductFeature < ApplicationRecord + module Seeding + extend ActiveSupport::Concern + + RELATIVE_FIXTURE_PATH = "db/fixtures/miq_product_features".freeze + FIXTURE_PATH = Rails.root.join(RELATIVE_FIXTURE_PATH).freeze + + module ClassMethods + def seed_files + return seed_root_filename, seed_core_files + seed_plugin_files + end + + private + + def seed_root_filename + "#{FIXTURE_PATH}.yml" + end + + def seed_core_files + Dir.glob("#{FIXTURE_PATH}/*.y{,a}ml").sort + end + + def seed_plugin_files + Vmdb::Plugins.flat_map do |plugin| + Dir.glob("#{plugin.root.join(RELATIVE_FIXTURE_PATH)}{.yml,.yaml,/*.yml,/*.yaml}").sort + end + end + end + end +end diff --git a/spec/models/miq_product_feature_spec.rb b/spec/models/miq_product_feature_spec.rb index 789b8de86b3..9b76f9d85ec 100644 --- a/spec/models/miq_product_feature_spec.rb +++ b/spec/models/miq_product_feature_spec.rb @@ -1,6 +1,3 @@ -require 'tmpdir' -require 'pathname' - describe MiqProductFeature do let(:miq_product_feature_class) do Class.new(described_class) do @@ -26,40 +23,51 @@ def self.tenant_features_in_hash ) end - it "is properly configured" do - everything = YAML.load_file(described_class.feature_yaml) - traverse_product_features(everything) do |pf| - expect(pf).to include(*described_class::REQUIRED_ATTRIBUTES) - expect(pf.keys - described_class::ALLOWED_ATTRIBUTES).to be_empty - expect(pf[:children]).not_to be_empty if pf.key?(:children) + def assert_product_feature_attributes(pf) + expect(pf).to include(*described_class::REQUIRED_ATTRIBUTES) + expect(pf.keys - described_class::ALLOWED_ATTRIBUTES).to be_empty + expect(pf[:children]).not_to be_empty if pf.key?(:children) + end + + def traverse_product_feature_children(pfs, &block) + pfs.each do |pf| + yield pf + traverse_product_feature_children(pf[:children], &block) if pf.key?(:children) end end - def traverse_product_features(product_feature, &block) - block.call(product_feature) - if product_feature.key?(:children) - product_feature[:children].each { |child| traverse_product_features(child, &block) } + it "is properly configured" do + root_file, other_files = described_class.seed_files + + hash = YAML.load_file(root_file) + assert_product_feature_attributes(hash) + + traverse_product_feature_children(hash[:children]) do |pf| + assert_product_feature_attributes(pf) + end + + other_files.each do |f| + hash = YAML.load_file(f) + traverse_product_feature_children(Array.wrap(hash)) do |pf| + assert_product_feature_attributes(pf) + end end end context ".seed" do it "creates feature identifiers once on first seed, changes nothing on second seed" do - status_seed1 = nil - expect { status_seed1 = MiqProductFeature.seed }.to change(MiqProductFeature, :count) - expect(status_seed1[:created]).to match_array status_seed1[:created].uniq - expect(status_seed1[:updated]).to match_array [] - expect(status_seed1[:unchanged]).to match_array [] - - status_seed2 = nil - expect { status_seed2 = MiqProductFeature.seed }.not_to change(MiqProductFeature, :count) - expect(status_seed2[:created]).to match_array [] - expect(status_seed2[:updated]).to match_array [] - expect(status_seed2[:unchanged]).to match_array status_seed1[:created] + expect { MiqProductFeature.seed }.to change(MiqProductFeature, :count) + orig_ids = MiqProductFeature.pluck(:id) + expect { MiqProductFeature.seed }.not_to change(MiqProductFeature, :count) + expect(MiqProductFeature.pluck(:id)).to match_array orig_ids end end context ".seed_features" do - let(:feature_path) { Pathname.new(@tempfile.path.sub(/\.yml/, '')) } + let(:tempdir) { Dir.mktmpdir } + let(:feature_path) { File.join(tempdir, "miq_product_features") } + let(:root_file) { File.join(tempdir, "miq_product_features.yml") } + let(:base) do { :feature_type => "node", @@ -76,47 +84,56 @@ def traverse_product_features(product_feature, &block) end before do - @tempdir = Dir.mktmpdir - @tempfile = Tempfile.new(['feature', '.yml'], @tempdir) - @tempfile.write(YAML.dump(base)) - @tempfile.close + File.write(root_file, base.to_yaml) + + expect(described_class).to receive(:seed_files).at_least(:once) do + [root_file, Dir.glob(File.join(feature_path, "*.yml"))] + end end after do - @tempfile.unlink - Dir.rmdir(@tempdir) + FileUtils.rm_rf(tempdir) end - it "existing records" do + it "creates/updates/deletes records" do deleted = FactoryBot.create(:miq_product_feature, :identifier => "xxx") changed = FactoryBot.create(:miq_product_feature, :identifier => "dialog_new_editor", :name => "XXX") unchanged = FactoryBot.create(:miq_product_feature_everything) unchanged_orig_updated_at = unchanged.updated_at - MiqProductFeature.seed_features(feature_path) + MiqProductFeature.seed_features + expect { deleted.reload }.to raise_error(ActiveRecord::RecordNotFound) expect(changed.reload.name).to eq("One") expect(unchanged.reload.updated_at).to be_within(0.1).of unchanged_orig_updated_at end - it "additional yaml feature" do - additional = { - :feature_type => "node", - :identifier => "dialog_edit_editor", - :children => [] - } + context "additional yaml feature" do + before do + additional_hash = { + :feature_type => "node", + :identifier => "dialog_edit_editor", + :children => [] + } + + additional_array = [ + { + :feature_type => "node", + :identifier => "policy_edit_editor", + :children => [] + } + ] - Dir.mkdir(feature_path) - additional_file = Tempfile.new(['additional', '.yml'], feature_path) - additional_file.write(YAML.dump(additional)) - additional_file.close + FileUtils.mkdir_p(feature_path) + File.write(File.join(feature_path, "additional_hash.yml"), additional_hash.to_yaml) + File.write(File.join(feature_path, "additional_array.yml"), additional_array.to_yaml) + end - status_seed = MiqProductFeature.seed_features(feature_path) - expect(MiqProductFeature.count).to eq(3) - expect(status_seed[:created]).to match_array %w(everything dialog_new_editor dialog_edit_editor) + it "creates/updates/deletes records" do + MiqProductFeature.seed_features - additional_file.unlink - Dir.rmdir(feature_path) + expect(MiqProductFeature.pluck(:identifier)).to match_array %w(everything dialog_new_editor dialog_edit_editor policy_edit_editor) + end end context 'dynamic product features' do @@ -151,7 +168,7 @@ def traverse_product_features(product_feature, &block) let!(:tenant) { FactoryBot.create(:tenant, :parent => root_tenant) } before do - MiqProductFeature.seed_features(feature_path) + MiqProductFeature.seed_features end it "creates tenant features" do diff --git a/spec/support/evm_spec_helper.rb b/spec/support/evm_spec_helper.rb index b71abfcadec..37d8781f7cf 100644 --- a/spec/support/evm_spec_helper.rb +++ b/spec/support/evm_spec_helper.rb @@ -96,7 +96,14 @@ def self.specific_product_features(*features) def self.seed_specific_product_features(*features) features.flatten! - hashes = YAML.load_file(MiqProductFeature.feature_yaml) + + root_file, other_files = MiqProductFeature.seed_files + + hashes = YAML.load_file(root_file) + other_files.each do |f| + hashes[:children] += Array.wrap(YAML.load_file(f)) + end + filtered = filter_specific_features([hashes], features).first MiqProductFeature.seed_from_hash(filtered) MiqProductFeature.seed_tenant_miq_product_features