# frozen_string_literal: true require 'spec_helper' RSpec.shared_examples 'validate dictionary' do |objects, directory_path, required_fields| context 'for each object' do let(:directory_path) { directory_path } let(:metadata_allowed_fields) do required_fields + %i[ feature_categories classes description introduced_by_url milestone gitlab_schema ] end let(:metadata) do objects.each_with_object({}) do |object_name, hash| next unless File.exist?(object_metadata_file_path(object_name)) hash[object_name] ||= load_object_metadata(required_fields, object_name) end end # This list is used to provide temporary exceptions for feature categories # that are transitioning and not yet in the feature_categories.yml file # any additions here should be accompanied by a link to an issue link let(:valid_feature_categories) do [ 'jihu' # https://gitlab.com/gitlab-org/database-team/team-tasks/-/issues/192 ] end let(:all_feature_categories) do YAML.load_file(Rails.root.join('config/feature_categories.yml')) + valid_feature_categories end let(:objects_without_metadata) do objects.reject { |t| metadata.has_key?(t) } end let(:objects_without_valid_metadata) do metadata.select { |_, t| t.has_key?(:error) }.keys end let(:objects_with_disallowed_fields) do metadata.select { |_, t| t.has_key?(:disallowed_fields) }.keys end let(:objects_with_missing_required_fields) do metadata.select { |_, t| t.has_key?(:missing_required_fields) }.keys end let(:objects_with_invalid_feature_category) do metadata.select { |_, t| t.has_key?(:invalid_feature_category) }.keys end it 'has a metadata file' do expect(objects_without_metadata).to be_empty, multiline_error( 'Missing metadata files', objects_without_metadata.map { |t| " #{object_metadata_file(t)}" } ) end it 'has a valid metadata file' do expect(objects_without_valid_metadata).to be_empty, object_metadata_errors( 'Table metadata files with errors', :error, objects_without_valid_metadata ) end it 'has a valid feature category' do message = <<~TEXT.chomp Please use a category from https://about.gitlab.com/handbook/product/categories/#categories-a-z Table metadata files with an invalid feature category TEXT expect(objects_with_invalid_feature_category).to be_empty, object_metadata_errors( message, :invalid_feature_category, objects_with_invalid_feature_category ) end it 'has a valid metadata file with allowed fields' do expect(objects_with_disallowed_fields).to be_empty, object_metadata_errors( 'Table metadata files with disallowed fields', :disallowed_fields, objects_with_disallowed_fields ) end it 'has a valid metadata file without missing fields' do expect(objects_with_missing_required_fields).to be_empty, object_metadata_errors( 'Table metadata files with missing fields', :missing_required_fields, objects_with_missing_required_fields ) end end private def object_metadata_file(object_name) File.join(directory_path, "#{object_name}.yml") end def object_metadata_file_path(object_name) Rails.root.join(object_metadata_file(object_name)) end def invalid_feature_categories(object_feature_categories) return false unless object_feature_categories.present? object_feature_categories - all_feature_categories end def load_object_metadata(required_fields, object_name) result = {} begin result[:metadata] = YAML.safe_load(File.read(object_metadata_file_path(object_name))).deep_symbolize_keys disallowed_fields = (result[:metadata].keys - metadata_allowed_fields) result[:disallowed_fields] = "fields not allowed: #{disallowed_fields.join(', ')}" unless disallowed_fields.empty? missing_required_fields = (required_fields - result[:metadata].reject { |_, v| v.blank? }.keys) unless missing_required_fields.empty? result[:missing_required_fields] = "missing required fields: #{missing_required_fields.join(', ')}" end if required_fields.include?(:feature_categories) object_feature_categories = result.dig(:metadata, :feature_categories) if (invalid = invalid_feature_categories(object_feature_categories)).any? result[:invalid_feature_category] = "invalid feature category: #{invalid.join(', ')}" end end rescue Psych::SyntaxError => ex result[:error] = ex.message end result end # rubocop:disable Naming/HeredocDelimiterNaming def object_metadata_errors(title, field, objects) lines = objects.map do |object_name| <<~EOM #{object_metadata_file(object_name)} #{metadata[object_name][field]} EOM end multiline_error(title, lines) end def multiline_error(title, lines) <<~EOM #{title}: #{lines.join("\n")} EOM end # rubocop:enable Naming/HeredocDelimiterNaming end RSpec.describe 'Views documentation', feature_category: :database do database_base_models = Gitlab::Database.database_base_models.select { |k, _| k != 'geo' } views = database_base_models.flat_map { |_, m| m.connection.views }.sort.uniq directory_path = File.join('db', 'docs', 'views') required_fields = %i[feature_categories view_name gitlab_schema] include_examples 'validate dictionary', views, directory_path, required_fields end RSpec.describe 'Tables documentation', feature_category: :database do database_base_models = Gitlab::Database.database_base_models.select { |k, _| k != 'geo' } tables = database_base_models.flat_map { |_, m| m.connection.tables }.sort.uniq directory_path = File.join('db', 'docs') required_fields = %i[feature_categories table_name gitlab_schema] include_examples 'validate dictionary', tables, directory_path, required_fields end RSpec.describe 'Deleted tables documentation', feature_category: :database do directory_path = File.join('db', 'docs', 'deleted_tables') tables = Dir.glob(File.join(directory_path, '*.yml')).map { |f| File.basename(f, '.yml') }.sort.uniq required_fields = %i[table_name gitlab_schema removed_by_url removed_in_milestone] include_examples 'validate dictionary', tables, directory_path, required_fields end RSpec.describe 'Deleted views documentation', feature_category: :database do directory_path = File.join('db', 'docs', 'deleted_views') views = Dir.glob(File.join(directory_path, '*.yml')).map { |f| File.basename(f, '.yml') }.sort.uniq required_fields = %i[view_name gitlab_schema removed_by_url removed_in_milestone] include_examples 'validate dictionary', views, directory_path, required_fields end