diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-20 21:42:06 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-20 21:42:06 +0300 |
commit | 6e4e1050d9dba2b7b2523fdd1768823ab85feef4 (patch) | |
tree | 78be5963ec075d80116a932011d695dd33910b4e /spec/lib/gitlab/metrics/dashboard | |
parent | 1ce776de4ae122aba3f349c02c17cebeaa8ecf07 (diff) |
Add latest changes from gitlab-org/gitlab@13-3-stable-ee
Diffstat (limited to 'spec/lib/gitlab/metrics/dashboard')
13 files changed, 705 insertions, 28 deletions
diff --git a/spec/lib/gitlab/metrics/dashboard/cache_spec.rb b/spec/lib/gitlab/metrics/dashboard/cache_spec.rb new file mode 100644 index 00000000000..9467d441ae1 --- /dev/null +++ b/spec/lib/gitlab/metrics/dashboard/cache_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Metrics::Dashboard::Cache, :use_clean_rails_memory_store_caching do + let_it_be(:project1) { build_stubbed(:project) } + let_it_be(:project2) { build_stubbed(:project) } + + let(:project1_key1) { "#{project1.id}_key1" } + let(:project1_key2) { "#{project1.id}_key2" } + let(:project2_key1) { "#{project2.id}_key1" } + + let(:cache1) { described_class.for(project1) } + let(:cache2) { described_class.for(project2) } + + before do + cache1.fetch(project1_key1) { 'data1' } + cache1.fetch(project1_key2) { 'data2' } + cache2.fetch(project2_key1) { 'data3' } + end + + describe '.fetch' do + it 'stores data correctly' do + described_class.fetch('key1') { 'data1' } + described_class.fetch('key2') { 'data2' } + + expect(described_class.fetch('key1')).to eq('data1') + expect(described_class.fetch('key2')).to eq('data2') + end + end + + describe '.for' do + it 'returns a new instance' do + expect(described_class.for(project1)).to be_instance_of(described_class) + end + end + + describe '#fetch' do + it 'stores data correctly' do + expect(cache1.fetch(project1_key1)).to eq('data1') + expect(cache1.fetch(project1_key2)).to eq('data2') + expect(cache2.fetch(project2_key1)).to eq('data3') + end + end + + describe '#delete_all!' do + it 'deletes keys of the given project', :aggregate_failures do + cache1.delete_all! + + expect(Rails.cache.exist?(project1_key1)).to be(false) + expect(Rails.cache.exist?(project1_key2)).to be(false) + expect(cache2.fetch(project2_key1)).to eq('data3') + + cache2.delete_all! + + expect(Rails.cache.exist?(project2_key1)).to be(false) + end + + it 'does not fail when nothing to delete' do + project3 = build_stubbed(:project) + cache3 = described_class.for(project3) + + expect { cache3.delete_all! }.not_to raise_error + end + end + + context 'multiple fetches and deletes' do + specify :aggregate_failures do + cache1.delete_all! + + expect(Rails.cache.exist?(project1_key1)).to be(false) + expect(Rails.cache.exist?(project1_key2)).to be(false) + + cache1.fetch("#{project1.id}_key3") { 'data1' } + cache1.fetch("#{project1.id}_key4") { 'data2' } + + expect(cache1.fetch("#{project1.id}_key3")).to eq('data1') + expect(cache1.fetch("#{project1.id}_key4")).to eq('data2') + + cache1.delete_all! + + expect(Rails.cache.exist?("#{project1.id}_key3")).to be(false) + expect(Rails.cache.exist?("#{project1.id}_key4")).to be(false) + end + end +end diff --git a/spec/lib/gitlab/metrics/dashboard/defaults_spec.rb b/spec/lib/gitlab/metrics/dashboard/defaults_spec.rb index dd61f8ebc4d..1f306753c39 100644 --- a/spec/lib/gitlab/metrics/dashboard/defaults_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/defaults_spec.rb @@ -4,5 +4,4 @@ require 'spec_helper' RSpec.describe Gitlab::Metrics::Dashboard::Defaults do it { is_expected.to be_const_defined(:DEFAULT_PANEL_TYPE) } - it { is_expected.to be_const_defined(:DEFAULT_PANEL_WEIGHT) } end diff --git a/spec/lib/gitlab/metrics/dashboard/finder_spec.rb b/spec/lib/gitlab/metrics/dashboard/finder_spec.rb index 60e1e29d4c5..730a31346d7 100644 --- a/spec/lib/gitlab/metrics/dashboard/finder_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/finder_spec.rb @@ -142,20 +142,42 @@ RSpec.describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store describe '.find_all_paths' do let(:all_dashboard_paths) { described_class.find_all_paths(project) } - let(:system_dashboard) { { path: system_dashboard_path, display_name: 'Default dashboard', default: true, system_dashboard: true, out_of_the_box_dashboard: true } } + let(:system_dashboard) { { path: system_dashboard_path, display_name: 'Overview', default: true, system_dashboard: true, out_of_the_box_dashboard: true } } + let(:k8s_pod_health_dashboard) { { path: pod_dashboard_path, display_name: 'K8s pod health', default: false, system_dashboard: false, out_of_the_box_dashboard: true } } - it 'includes only the system dashboard by default' do - expect(all_dashboard_paths).to eq([system_dashboard]) + it 'includes OOTB dashboards by default' do + expect(all_dashboard_paths).to eq([k8s_pod_health_dashboard, system_dashboard]) end context 'when the project contains dashboards' do - let(:dashboard_path) { '.gitlab/dashboards/test.yml' } - let(:project) { project_with_dashboard(dashboard_path) } + let(:dashboard_content) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') } + let(:project) { project_with_dashboards(dashboards) } - it 'includes system and project dashboards' do - project_dashboard = { path: dashboard_path, display_name: 'test.yml', default: false, system_dashboard: false, out_of_the_box_dashboard: false } + let(:dashboards) do + { + '.gitlab/dashboards/metrics.yml' => dashboard_content, + '.gitlab/dashboards/better_metrics.yml' => dashboard_content + } + end - expect(all_dashboard_paths).to contain_exactly(system_dashboard, project_dashboard) + it 'includes OOTB and project dashboards' do + project_dashboard1 = { + path: '.gitlab/dashboards/metrics.yml', + display_name: 'metrics.yml', + default: false, + system_dashboard: false, + out_of_the_box_dashboard: false + } + + project_dashboard2 = { + path: '.gitlab/dashboards/better_metrics.yml', + display_name: 'better_metrics.yml', + default: false, + system_dashboard: false, + out_of_the_box_dashboard: false + } + + expect(all_dashboard_paths).to eq([project_dashboard2, k8s_pod_health_dashboard, project_dashboard1, system_dashboard]) end end @@ -163,12 +185,13 @@ RSpec.describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store let(:self_monitoring_dashboard) do { path: self_monitoring_dashboard_path, - display_name: 'Default dashboard', + display_name: 'Overview', default: true, - system_dashboard: false, + system_dashboard: true, out_of_the_box_dashboard: true } end + let(:dashboard_path) { '.gitlab/dashboards/test.yml' } let(:project) { project_with_dashboard(dashboard_path) } @@ -185,7 +208,7 @@ RSpec.describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store out_of_the_box_dashboard: false } - expect(all_dashboard_paths).to contain_exactly(self_monitoring_dashboard, project_dashboard) + expect(all_dashboard_paths).to eq([self_monitoring_dashboard, project_dashboard]) end end end diff --git a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb index 7f7070dfafb..14a4c01fce3 100644 --- a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb @@ -16,7 +16,6 @@ RSpec.describe Gitlab::Metrics::Dashboard::Processor do Gitlab::Metrics::Dashboard::Stages::CustomMetricsInserter, Gitlab::Metrics::Dashboard::Stages::CustomMetricsDetailsInserter, Gitlab::Metrics::Dashboard::Stages::MetricEndpointInserter, - Gitlab::Metrics::Dashboard::Stages::Sorter, Gitlab::Metrics::Dashboard::Stages::AlertsInserter, Gitlab::Metrics::Dashboard::Stages::PanelIdsInserter, Gitlab::Metrics::Dashboard::Stages::UrlValidator @@ -26,12 +25,6 @@ RSpec.describe Gitlab::Metrics::Dashboard::Processor do let(:process_params) { [project, dashboard_yml, sequence, { environment: environment }] } let(:dashboard) { described_class.new(*process_params).process } - it 'includes a path for the prometheus endpoint with each metric' do - expect(all_metrics).to satisfy_all do |metric| - metric[:prometheus_endpoint_path] == prometheus_path(metric[:query_range]) - end - end - it 'includes an id for each dashboard panel' do expect(all_panels).to satisfy_all do |panel| panel[:id].present? @@ -72,14 +65,14 @@ RSpec.describe Gitlab::Metrics::Dashboard::Processor do expect(all_metrics).to include get_metric_details(project_business_metric) end - it 'orders groups by priority and panels by weight' do + it 'display groups and panels in the order they are defined' do expected_metrics_order = [ - 'metric_b', # group priority 10, panel weight 1 - 'metric_a2', # group priority 1, panel weight 2 - 'metric_a1', # group priority 1, panel weight 1 - project_business_metric.id, # group priority 0, panel weight nil (0) - project_response_metric.id, # group priority -5, panel weight nil (0) - project_system_metric.id # group priority -10, panel weight nil (0) + 'metric_b', + 'metric_a2', + 'metric_a1', + project_business_metric.id, + project_response_metric.id, + project_system_metric.id ] actual_metrics_order = all_metrics.map { |m| m[:id] || m[:metric_id] } @@ -100,10 +93,10 @@ RSpec.describe Gitlab::Metrics::Dashboard::Processor do let(:sequence) do [ Gitlab::Metrics::Dashboard::Stages::CommonMetricsInserter, - Gitlab::Metrics::Dashboard::Stages::MetricEndpointInserter, - Gitlab::Metrics::Dashboard::Stages::Sorter + Gitlab::Metrics::Dashboard::Stages::MetricEndpointInserter ] end + let(:dashboard) { described_class.new(*process_params).process } it 'includes only dashboard metrics' do diff --git a/spec/lib/gitlab/metrics/dashboard/repo_dashboard_finder_spec.rb b/spec/lib/gitlab/metrics/dashboard/repo_dashboard_finder_spec.rb new file mode 100644 index 00000000000..a2c9906c0e9 --- /dev/null +++ b/spec/lib/gitlab/metrics/dashboard/repo_dashboard_finder_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Metrics::Dashboard::RepoDashboardFinder do + include MetricsDashboardHelpers + + let_it_be(:project) { create(:project) } + + describe '.list_dashboards' do + it 'deletes dashboard cache entries' do + cache = instance_double(Gitlab::Metrics::Dashboard::Cache) + allow(Gitlab::Metrics::Dashboard::Cache).to receive(:for).and_return(cache) + + expect(cache).to receive(:delete_all!) + + described_class.list_dashboards(project) + end + + it 'returns empty array when there are no dashboards' do + expect(described_class.list_dashboards(project)).to eq([]) + end + + context 'when there are project dashboards available' do + let_it_be(:dashboard_path) { '.gitlab/dashboards/test.yml' } + let_it_be(:project) { project_with_dashboard(dashboard_path) } + + it 'returns the dashboard list' do + expect(described_class.list_dashboards(project)).to contain_exactly(dashboard_path) + end + end + end + + describe '.read_dashboard' do + it 'raises error when dashboard does not exist' do + dashboard_path = '.gitlab/dashboards/test.yml' + + expect { described_class.read_dashboard(project, dashboard_path) }.to raise_error( + Gitlab::Metrics::Dashboard::Errors::NOT_FOUND_ERROR + ) + end + + context 'when there are project dashboards available' do + let_it_be(:dashboard_path) { '.gitlab/dashboards/test.yml' } + let_it_be(:project) { project_with_dashboard(dashboard_path) } + + it 'reads dashboard' do + expect(described_class.read_dashboard(project, dashboard_path)).to eq( + fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') + ) + end + end + end +end diff --git a/spec/lib/gitlab/metrics/dashboard/stages/metric_endpoint_inserter_spec.rb b/spec/lib/gitlab/metrics/dashboard/stages/metric_endpoint_inserter_spec.rb new file mode 100644 index 00000000000..bb3c8626d32 --- /dev/null +++ b/spec/lib/gitlab/metrics/dashboard/stages/metric_endpoint_inserter_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Metrics::Dashboard::Stages::MetricEndpointInserter do + include MetricsDashboardHelpers + + let(:project) { build_stubbed(:project) } + let(:environment) { build_stubbed(:environment, project: project) } + + describe '#transform!' do + subject(:transform!) { described_class.new(project, dashboard, environment: environment).transform! } + + let(:dashboard) { load_sample_dashboard.deep_symbolize_keys } + + it 'generates prometheus_endpoint_path without newlines' do + query = 'avg( sum( container_memory_usage_bytes{ container_name!="POD", '\ + 'pod_name=~"^{{ci_environment_slug}}-(.*)", namespace="{{kube_namespace}}" } ) '\ + 'by (job) ) without (job) /1024/1024/1024' + + transform! + + expect(all_metrics[2][:prometheus_endpoint_path]).to eq(prometheus_path(query)) + end + + it 'includes a path for the prometheus endpoint with each metric' do + transform! + + expect(all_metrics).to satisfy_all do |metric| + metric[:prometheus_endpoint_path].present? && !metric[:prometheus_endpoint_path].include?("\n") + end + end + + it 'works when query/query_range is a number' do + query = 2000 + + transform! + + expect(all_metrics[1][:prometheus_endpoint_path]).to eq(prometheus_path(query)) + end + end + + private + + def all_metrics + dashboard[:panel_groups].flat_map do |group| + group[:panels].flat_map { |panel| panel[:metrics] } + end + end + + def prometheus_path(query) + Gitlab::Routing.url_helpers.prometheus_api_project_environment_path( + project, + environment, + proxy_path: :query_range, + query: query + ) + end +end diff --git a/spec/lib/gitlab/metrics/dashboard/stages/track_panel_type_spec.rb b/spec/lib/gitlab/metrics/dashboard/stages/track_panel_type_spec.rb new file mode 100644 index 00000000000..d9987b67127 --- /dev/null +++ b/spec/lib/gitlab/metrics/dashboard/stages/track_panel_type_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Metrics::Dashboard::Stages::TrackPanelType do + include MetricsDashboardHelpers + + let(:project) { build_stubbed(:project) } + let(:environment) { build_stubbed(:environment, project: project) } + + describe '#transform!' do + subject { described_class.new(project, dashboard, environment: environment) } + + let(:dashboard) { load_sample_dashboard.deep_symbolize_keys } + + it 'creates tracking event' do + stub_application_setting(snowplow_enabled: true, snowplow_collector_hostname: 'localhost') + allow(Gitlab::Tracking).to receive(:event).and_call_original + + subject.transform! + + expect(Gitlab::Tracking).to have_received(:event) + .with('MetricsDashboard::Chart', 'chart_rendered', { label: 'area-chart' }) + .at_least(:once) + end + end +end diff --git a/spec/lib/gitlab/metrics/dashboard/url_spec.rb b/spec/lib/gitlab/metrics/dashboard/url_spec.rb index 56556423b05..205e1000376 100644 --- a/spec/lib/gitlab/metrics/dashboard/url_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/url_spec.rb @@ -102,6 +102,34 @@ RSpec.describe Gitlab::Metrics::Dashboard::Url do it_behaves_like 'regex which matches url when expected' end + describe '#alert_regex' do + let(:url) do + Gitlab::Routing.url_helpers.metrics_dashboard_namespace_project_prometheus_alert_url( + 'foo', + 'bar', + '1', + start: '2020-02-10T12:59:49.938Z', + end: '2020-02-10T20:59:49.938Z', + anchor: "anchor" + ) + end + + let(:expected_params) do + { + 'url' => url, + 'namespace' => 'foo', + 'project' => 'bar', + 'alert' => '1', + 'query' => "?end=2020-02-10T20%3A59%3A49.938Z&start=2020-02-10T12%3A59%3A49.938Z", + 'anchor' => '#anchor' + } + end + + subject { described_class.alert_regex } + + it_behaves_like 'regex which matches url when expected' + end + describe '#build_dashboard_url' do it 'builds the url for the dashboard endpoint' do url = described_class.build_dashboard_url('foo', 'bar', 1) diff --git a/spec/lib/gitlab/metrics/dashboard/validator/client_spec.rb b/spec/lib/gitlab/metrics/dashboard/validator/client_spec.rb new file mode 100644 index 00000000000..4b07f9dbbab --- /dev/null +++ b/spec/lib/gitlab/metrics/dashboard/validator/client_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Metrics::Dashboard::Validator::Client do + include MetricsDashboardHelpers + + let_it_be(:schema_path) { 'lib/gitlab/metrics/dashboard/validator/schemas/dashboard.json' } + + subject { described_class.new(dashboard, schema_path) } + + describe '#execute' do + context 'with no validation errors' do + let(:dashboard) { load_sample_dashboard } + + it 'returns empty array' do + expect(subject.execute).to eq([]) + end + end + + context 'with validation errors' do + let(:dashboard) { load_dashboard_yaml(fixture_file('lib/gitlab/metrics/dashboard/invalid_dashboard.yml')) } + + it 'returns array of error objects' do + expect(subject.execute).to include(Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError) + end + end + end +end diff --git a/spec/lib/gitlab/metrics/dashboard/validator/custom_formats_spec.rb b/spec/lib/gitlab/metrics/dashboard/validator/custom_formats_spec.rb new file mode 100644 index 00000000000..129fb631f3e --- /dev/null +++ b/spec/lib/gitlab/metrics/dashboard/validator/custom_formats_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Metrics::Dashboard::Validator::CustomFormats do + describe '#format_handlers' do + describe 'add_to_metric_id_cache' do + it 'adds data to metric id cache' do + subject.format_handlers['add_to_metric_id_cache'].call('metric_id', '_schema') + + expect(subject.metric_ids_cache).to eq(["metric_id"]) + end + end + end +end diff --git a/spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb b/spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb new file mode 100644 index 00000000000..f0db1bd0d33 --- /dev/null +++ b/spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors do + describe Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError do + context 'empty error hash' do + let(:error_hash) { {} } + + it 'uses default error message' do + expect(described_class.new(error_hash).message).to eq('Dashboard failed schema validation') + end + end + + context 'formatted message' do + subject { described_class.new(error_hash).message } + + let(:error_hash) do + { + 'data' => 'property_name', + 'data_pointer' => pointer, + 'type' => type, + 'schema' => 'schema', + 'details' => details + } + end + + context 'for root object' do + let(:pointer) { '' } + + context 'when required keys are missing' do + let(:type) { 'required' } + let(:details) { { 'missing_keys' => ['one'] } } + + it { is_expected.to eq 'root is missing required keys: one' } + end + end + + context 'for nested object' do + let(:pointer) { '/nested_objects/0' } + + context 'when required keys are missing' do + let(:type) { 'required' } + let(:details) { { 'missing_keys' => ['two'] } } + + it { is_expected.to eq '/nested_objects/0 is missing required keys: two' } + end + + context 'when there is type mismatch' do + %w(null string boolean integer number array object).each do |expected_type| + context "on type: #{expected_type}" do + let(:type) { expected_type } + let(:details) { nil } + + subject { described_class.new(error_hash).message } + + it { is_expected.to eq "'property_name' at /nested_objects/0 is not of type: #{expected_type}" } + end + end + end + + context 'when data does not match pattern' do + let(:type) { 'pattern' } + let(:error_hash) do + { + 'data' => 'property_name', + 'data_pointer' => pointer, + 'type' => type, + 'schema' => { 'pattern' => 'aa.*' } + } + end + + it { is_expected.to eq "'property_name' at /nested_objects/0 does not match pattern: aa.*" } + end + + context 'when data does not match format' do + let(:type) { 'format' } + let(:error_hash) do + { + 'data' => 'property_name', + 'data_pointer' => pointer, + 'type' => type, + 'schema' => { 'format' => 'date-time' } + } + end + + it { is_expected.to eq "'property_name' at /nested_objects/0 does not match format: date-time" } + end + + context 'when data is not const' do + let(:type) { 'const' } + let(:error_hash) do + { + 'data' => 'property_name', + 'data_pointer' => pointer, + 'type' => type, + 'schema' => { 'const' => 'one' } + } + end + + it { is_expected.to eq "'property_name' at /nested_objects/0 is not: \"one\"" } + end + + context 'when data is not included in enum' do + let(:type) { 'enum' } + let(:error_hash) do + { + 'data' => 'property_name', + 'data_pointer' => pointer, + 'type' => type, + 'schema' => { 'enum' => %w(one two) } + } + end + + it { is_expected.to eq "'property_name' at /nested_objects/0 is not one of: [\"one\", \"two\"]" } + end + + context 'when data is not included in enum' do + let(:type) { 'unknown' } + let(:error_hash) do + { + 'data' => 'property_name', + 'data_pointer' => pointer, + 'type' => type, + 'schema' => 'schema' + } + end + + it { is_expected.to eq "'property_name' at /nested_objects/0 is invalid: error_type=unknown" } + end + end + end + end + + describe Gitlab::Metrics::Dashboard::Validator::Errors::DuplicateMetricIds do + it 'has custom error message' do + expect(described_class.new.message).to eq('metric_id must be unique across a project') + end + end +end diff --git a/spec/lib/gitlab/metrics/dashboard/validator/post_schema_validator_spec.rb b/spec/lib/gitlab/metrics/dashboard/validator/post_schema_validator_spec.rb new file mode 100644 index 00000000000..e7cb1429ca9 --- /dev/null +++ b/spec/lib/gitlab/metrics/dashboard/validator/post_schema_validator_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Metrics::Dashboard::Validator::PostSchemaValidator do + describe '#validate' do + context 'with no project and dashboard_path provided' do + context 'unique local metric_ids' do + it 'returns empty array' do + expect(described_class.new(metric_ids: [1, 2, 3]).validate).to eq([]) + end + end + + context 'duplicate local metrics_ids' do + it 'returns error' do + expect(described_class.new(metric_ids: [1, 1]).validate) + .to eq([Gitlab::Metrics::Dashboard::Validator::Errors::DuplicateMetricIds]) + end + end + end + + context 'with project and dashboard_path' do + let(:project) { create(:project) } + + subject do + described_class.new( + project: project, + metric_ids: ['some_identifier'], + dashboard_path: 'test/path.yml' + ).validate + end + + context 'with unique metric identifiers' do + before do + create(:prometheus_metric, + project: project, + identifier: 'some_other_identifier', + dashboard_path: 'test/path.yml' + ) + end + + it 'returns empty array' do + expect(subject).to eq([]) + end + end + + context 'duplicate metric identifiers in database' do + context 'with different dashboard_path' do + before do + create(:prometheus_metric, + project: project, + identifier: 'some_identifier', + dashboard_path: 'some/other/path.yml' + ) + end + + it 'returns error' do + expect(subject).to include(Gitlab::Metrics::Dashboard::Validator::Errors::DuplicateMetricIds) + end + end + + context 'with same dashboard_path' do + before do + create(:prometheus_metric, + project: project, + identifier: 'some_identifier', + dashboard_path: 'test/path.yml' + ) + end + + it 'returns empty array' do + expect(subject).to eq([]) + end + end + end + end + end +end diff --git a/spec/lib/gitlab/metrics/dashboard/validator_spec.rb b/spec/lib/gitlab/metrics/dashboard/validator_spec.rb new file mode 100644 index 00000000000..c4cda271408 --- /dev/null +++ b/spec/lib/gitlab/metrics/dashboard/validator_spec.rb @@ -0,0 +1,146 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Metrics::Dashboard::Validator do + include MetricsDashboardHelpers + + let_it_be(:valid_dashboard) { load_sample_dashboard } + let_it_be(:invalid_dashboard) { load_dashboard_yaml(fixture_file('lib/gitlab/metrics/dashboard/invalid_dashboard.yml')) } + let_it_be(:duplicate_id_dashboard) { load_dashboard_yaml(fixture_file('lib/gitlab/metrics/dashboard/duplicate_id_dashboard.yml')) } + + let_it_be(:project) { create(:project) } + + describe '#validate' do + context 'valid dashboard schema' do + it 'returns true' do + expect(described_class.validate(valid_dashboard)).to be true + end + + context 'with duplicate metric_ids' do + it 'returns false' do + expect(described_class.validate(duplicate_id_dashboard)).to be false + end + end + + context 'with dashboard_path and project' do + subject { described_class.validate(valid_dashboard, dashboard_path: 'test/path.yml', project: project) } + + context 'with no conflicting metric identifiers in db' do + it { is_expected.to be true } + end + + context 'with metric identifier present in current dashboard' do + before do + create(:prometheus_metric, + identifier: 'metric_a1', + dashboard_path: 'test/path.yml', + project: project + ) + end + + it { is_expected.to be true } + end + + context 'with metric identifier present in another dashboard' do + before do + create(:prometheus_metric, + identifier: 'metric_a1', + dashboard_path: 'some/other/dashboard/path.yml', + project: project + ) + end + + it { is_expected.to be false } + end + end + end + + context 'invalid dashboard schema' do + it 'returns false' do + expect(described_class.validate(invalid_dashboard)).to be false + end + end + end + + describe '#validate!' do + shared_examples 'validation failed' do |errors_message| + it 'raises error with corresponding messages', :aggregate_failures do + expect { subject }.to raise_error do |error| + expect(error).to be_kind_of(Gitlab::Metrics::Dashboard::Validator::Errors::InvalidDashboardError) + expect(error.message).to eq(errors_message) + end + end + end + + context 'valid dashboard schema' do + it 'returns true' do + expect(described_class.validate!(valid_dashboard)).to be true + end + + context 'with duplicate metric_ids' do + subject { described_class.validate!(duplicate_id_dashboard) } + + it_behaves_like 'validation failed', 'metric_id must be unique across a project' + end + + context 'with dashboard_path and project' do + subject { described_class.validate!(valid_dashboard, dashboard_path: 'test/path.yml', project: project) } + + context 'with no conflicting metric identifiers in db' do + it { is_expected.to be true } + end + + context 'with metric identifier present in current dashboard' do + before do + create(:prometheus_metric, + identifier: 'metric_a1', + dashboard_path: 'test/path.yml', + project: project + ) + end + + it { is_expected.to be true } + end + + context 'with metric identifier present in another dashboard' do + before do + create(:prometheus_metric, + identifier: 'metric_a1', + dashboard_path: 'some/other/dashboard/path.yml', + project: project + ) + end + + it_behaves_like 'validation failed', 'metric_id must be unique across a project' + end + end + end + + context 'invalid dashboard schema' do + subject { described_class.validate!(invalid_dashboard) } + + context 'wrong property type' do + it_behaves_like 'validation failed', "'this_should_be_a_int' at /panel_groups/0/panels/0/weight is not of type: number" + end + + context 'panel groups missing' do + let_it_be(:invalid_dashboard) { load_dashboard_yaml(fixture_file('lib/gitlab/metrics/dashboard/dashboard_missing_panel_groups.yml')) } + + it_behaves_like 'validation failed', 'root is missing required keys: panel_groups' + end + + context 'groups are missing panels and group keys' do + let_it_be(:invalid_dashboard) { load_dashboard_yaml(fixture_file('lib/gitlab/metrics/dashboard/dashboard_groups_missing_panels_and_group.yml')) } + + it_behaves_like 'validation failed', '/panel_groups/0 is missing required keys: group' + end + + context 'panel is missing metrics key' do + let_it_be(:invalid_dashboard) { load_dashboard_yaml(fixture_file('lib/gitlab/metrics/dashboard/dashboard_panel_is_missing_metrics.yml')) } + + it_behaves_like 'validation failed', '/panel_groups/0/panels/0 is missing required keys: metrics' + end + end + end +end |