Commit 476ee015 authored by Mikolaj Wawrzyniak's avatar Mikolaj Wawrzyniak

Expand dashboard schema validation to full list

In order to provide user with complete information we need to expand
dashboard validation to return exhaustive list of errors
parent 498292b1
......@@ -14,7 +14,8 @@ module Resolvers
def resolve(**args)
return unless environment
::PerformanceMonitoring::PrometheusDashboard.find_for(project: environment.project, user: context[:current_user], path: args[:path], options: { environment: environment })
::PerformanceMonitoring::PrometheusDashboard
.find_for(project: environment.project, user: context[:current_user], path: args[:path], options: { environment: environment })
end
end
end
......
......@@ -16,6 +16,13 @@ module Types
field :annotations, Types::Metrics::Dashboards::AnnotationType.connection_type, null: true,
description: 'Annotations added to the dashboard',
resolver: Resolvers::Metrics::Dashboards::AnnotationResolver
# In order to maintain backward compatibility we need to return NULL when there are no warnings
# and dashboard validation returns an empty array when there are no issues.
def schema_validation_warnings
warnings = object.schema_validation_warnings
warnings unless warnings.empty?
end
end
# rubocop: enable Graphql/AuthorizeTypes
end
......
......@@ -25,20 +25,30 @@ module BlobViewer
private
def parse_blob_data
yaml = ::Gitlab::Config::Loader::Yaml.new(blob.data).load_raw!
if Feature.enabled?(:metrics_dashboard_exhaustive_validations, project)
exhaustive_metrics_dashboard_validation
else
old_metrics_dashboard_validation
end
end
def old_metrics_dashboard_validation
yaml = ::Gitlab::Config::Loader::Yaml.new(blob.data).load_raw!
::PerformanceMonitoring::PrometheusDashboard.from_json(yaml)
nil
[]
rescue Gitlab::Config::Loader::FormatError => error
wrap_yml_syntax_error(error)
["YAML syntax: #{error.message}"]
rescue ActiveModel::ValidationError => invalid
invalid.model.errors
invalid.model.errors.messages.map { |messages| messages.join(': ') }
end
def wrap_yml_syntax_error(error)
::PerformanceMonitoring::PrometheusDashboard.new.errors.tap do |errors|
errors.add(:'YAML syntax', error.message)
end
def exhaustive_metrics_dashboard_validation
yaml = ::Gitlab::Config::Loader::Yaml.new(blob.data).load_raw!
Gitlab::Metrics::Dashboard::Validator
.errors(yaml, dashboard_path: blob.path, project: project)
.map(&:message)
rescue Gitlab::Config::Loader::FormatError => error
[error.message]
end
end
end
......@@ -53,14 +53,23 @@ module PerformanceMonitoring
# This method is planned to be refactored as a part of https://gitlab.com/gitlab-org/gitlab/-/issues/219398
# implementation. For new existing logic was reused to faster deliver MVC
def schema_validation_warnings
return run_custom_validation.map(&:message) if Feature.enabled?(:metrics_dashboard_exhaustive_validations, environment&.project)
self.class.from_json(reload_schema)
nil
[]
rescue Gitlab::Metrics::Dashboard::Errors::LayoutError => error
[error.message]
rescue ActiveModel::ValidationError => exception
exception.model.errors.map { |attr, error| "#{attr}: #{error}" }
end
private
def run_custom_validation
Gitlab::Metrics::Dashboard::Validator
.errors(reload_schema, dashboard_path: path, project: environment&.project)
end
# dashboard finder methods are somehow limited, #find includes checking if
# user is authorised to view selected dashboard, but modifies schema, which in some cases may
# cause false positives returned from validation, and #find_raw does not authorise users
......
......@@ -5,7 +5,7 @@
= icon('warning fw')
= _('Metrics Dashboard YAML definition is invalid:')
%ul
- viewer.errors.messages.each do |error|
%li= error.join(': ')
- viewer.errors.each do |error|
%li= error
= link_to _('Learn more'), help_page_path('operations/metrics/dashboards/index.md')
---
name: metrics_dashboard_exhaustive_validations
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40103
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/241697
group: group::apm
type: development
default_enabled: false
\ No newline at end of file
......@@ -4,24 +4,22 @@ module Gitlab
module Metrics
module Dashboard
module Validator
DASHBOARD_SCHEMA_PATH = 'lib/gitlab/metrics/dashboard/validator/schemas/dashboard.json'.freeze
DASHBOARD_SCHEMA_PATH = Rails.root.join(*%w[lib gitlab metrics dashboard validator schemas dashboard.json]).freeze
class << self
def validate(content, schema_path = DASHBOARD_SCHEMA_PATH, dashboard_path: nil, project: nil)
errors = _validate(content, schema_path, dashboard_path: dashboard_path, project: project)
errors.empty?
errors(content, schema_path, dashboard_path: dashboard_path, project: project).empty?
end
def validate!(content, schema_path = DASHBOARD_SCHEMA_PATH, dashboard_path: nil, project: nil)
errors = _validate(content, schema_path, dashboard_path: dashboard_path, project: project)
errors = errors(content, schema_path, dashboard_path: dashboard_path, project: project)
errors.empty? || raise(errors.first)
end
private
def _validate(content, schema_path, dashboard_path: nil, project: nil)
client = Validator::Client.new(content, schema_path, dashboard_path: dashboard_path, project: project)
client.execute
def errors(content, schema_path = DASHBOARD_SCHEMA_PATH, dashboard_path: nil, project: nil)
Validator::Client
.new(content, schema_path, dashboard_path: dashboard_path, project: project)
.execute
end
end
end
......
......@@ -46,7 +46,7 @@ module Gitlab
def validate_against_schema
schemer.validate(content).map do |error|
Errors::SchemaValidationError.new(error)
::Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError.new(error)
end
end
end
......
......@@ -4,7 +4,7 @@
"properties": {
"type": {
"type": "string",
"enum": ["area-chart", "anomaly-chart", "bar", "column", "stacked-column", "single-stat", "heatmap"],
"enum": ["area-chart", "line-chart", "anomaly-chart", "bar", "column", "stacked-column", "single-stat", "heatmap", "gauge"],
"default": "area-chart"
},
"title": { "type": "string" },
......
......@@ -564,7 +564,11 @@ RSpec.describe 'File blob', :js do
file_path: '.gitlab/dashboards/custom-dashboard.yml',
file_content: file_content
).execute
end
context 'with metrics_dashboard_exhaustive_validations feature flag off' do
before do
stub_feature_flags(metrics_dashboard_exhaustive_validations: false)
visit_blob('.gitlab/dashboards/custom-dashboard.yml')
end
......@@ -598,6 +602,43 @@ RSpec.describe 'File blob', :js do
end
end
context 'with metrics_dashboard_exhaustive_validations feature flag on' do
before do
stub_feature_flags(metrics_dashboard_exhaustive_validations: true)
visit_blob('.gitlab/dashboards/custom-dashboard.yml')
end
context 'valid dashboard file' do
let(:file_content) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) }
it 'displays an auxiliary viewer' do
aggregate_failures do
# shows that dashboard yaml is valid
expect(page).to have_content('Metrics Dashboard YAML definition is valid.')
# shows a learn more link
expect(page).to have_link('Learn more')
end
end
end
context 'invalid dashboard file' do
let(:file_content) { "dashboard: 'invalid'" }
it 'displays an auxiliary viewer' do
aggregate_failures do
# shows that dashboard yaml is invalid
expect(page).to have_content('Metrics Dashboard YAML definition is invalid:')
expect(page).to have_content("root is missing required keys: panel_groups")
# shows a learn more link
expect(page).to have_link('Learn more')
end
end
end
end
end
context 'LICENSE' do
before do
visit_blob('LICENSE')
......
dashboard: 'Test Dashboard'
panel_groups:
- group: Group B
panels:
- title: "Super Chart B"
type: "area-chart"
y_label: "y_label"
metrics:
- id: metric_b
query_range: 'query'
unit: unit
label: Legend Label
- group: Group A
......@@ -34,6 +34,17 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors do
it { is_expected.to eq 'root is missing required keys: one' }
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 }
it { is_expected.to eq "'property_name' at root is not of type: #{expected_type}" }
end
end
end
end
context 'for nested object' do
......@@ -52,8 +63,6 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors 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
......
......@@ -143,4 +143,56 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator do
end
end
end
describe '#errors' do
context 'valid dashboard schema' do
it 'returns no errors' do
expect(described_class.errors(valid_dashboard)).to eq []
end
context 'with duplicate metric_ids' do
it 'returns errors' do
expect(described_class.errors(duplicate_id_dashboard)).to eq [Gitlab::Metrics::Dashboard::Validator::Errors::DuplicateMetricIds.new]
end
end
context 'with dashboard_path and project' do
subject { described_class.errors(valid_dashboard, dashboard_path: 'test/path.yml', project: project) }
context 'with no conflicting metric identifiers in db' do
it { is_expected.to eq [] }
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 eq [] }
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 eq [Gitlab::Metrics::Dashboard::Validator::Errors::DuplicateMetricIds.new] }
end
end
end
context 'invalid dashboard schema' do
it 'returns collection of validation errors' do
expect(described_class.errors(invalid_dashboard)).to all be_kind_of(Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError)
end
end
end
end
......@@ -9,22 +9,137 @@ RSpec.describe BlobViewer::MetricsDashboardYml do
let_it_be(:project) { create(:project, :repository) }
let(:blob) { fake_blob(path: '.gitlab/dashboards/custom-dashboard.yml', data: data) }
let(:sha) { sample_commit.id }
let(:data) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
subject(:viewer) { described_class.new(blob) }
context 'with metrics_dashboard_exhaustive_validations feature flag on' do
before do
stub_feature_flags(metrics_dashboard_exhaustive_validations: true)
end
context 'when the definition is valid' do
let(:data) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) }
before do
allow(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).and_return([])
end
describe '#valid?' do
it 'calls prepare! on the viewer' do
expect(viewer).to receive(:prepare!)
viewer.valid?
end
it 'processes dashboard yaml and returns true', :aggregate_failures do
yml = ::Gitlab::Config::Loader::Yaml.new(data).load_raw!
expect_next_instance_of(::Gitlab::Config::Loader::Yaml, data) do |loader|
expect(loader).to receive(:load_raw!).and_call_original
end
expect(Gitlab::Metrics::Dashboard::Validator)
.to receive(:errors)
.with(yml, dashboard_path: '.gitlab/dashboards/custom-dashboard.yml', project: project)
.and_call_original
expect(viewer.valid?).to be true
end
end
describe '#errors' do
it 'returns empty array' do
expect(viewer.errors).to eq []
end
end
end
context 'when definition is invalid' do
let(:error) { ::Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError.new }
let(:data) do
<<~YAML
dashboard:
YAML
end
before do
allow(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).and_return([error])
end
describe '#valid?' do
it 'returns false' do
expect(viewer.valid?).to be false
end
end
describe '#errors' do
it 'returns validation errors' do
expect(viewer.errors).to eq ["Dashboard failed schema validation"]
end
end
end
context 'when YAML syntax is invalid' do
let(:data) do
<<~YAML
dashboard: 'empty metrics'
panel_groups:
- group: 'Group Title'
YAML
end
describe '#valid?' do
it 'returns false' do
expect(Gitlab::Metrics::Dashboard::Validator).not_to receive(:errors)
expect(viewer.valid?).to be false
end
end
describe '#errors' do
it 'returns validation errors' do
expect(viewer.errors).to all be_kind_of String
end
end
end
context 'when YAML loader raises error' do
let(:data) do
<<~YAML
large yaml file
YAML
end
before do
allow(::Gitlab::Config::Loader::Yaml).to receive(:new)
.and_raise(::Gitlab::Config::Loader::Yaml::DataTooLargeError, 'The parsed YAML is too big')
end
it 'is invalid' do
expect(Gitlab::Metrics::Dashboard::Validator).not_to receive(:errors)
expect(viewer.valid?).to be false
end
it 'returns validation errors' do
expect(viewer.errors).to eq ['The parsed YAML is too big']
end
end
end
context 'with metrics_dashboard_exhaustive_validations feature flag off' do
before do
stub_feature_flags(metrics_dashboard_exhaustive_validations: false)
end
context 'when the definition is valid' do
describe '#valid?' do
before do
allow(PerformanceMonitoring::PrometheusDashboard).to receive(:from_json)
end
it 'calls prepare! on the viewer' do
expect(viewer).to receive(:prepare!)
viewer.valid?
end
it 'returns true', :aggregate_failures do
it 'processes dashboard yaml and returns true', :aggregate_failures do
yml = ::Gitlab::Config::Loader::Yaml.new(data).load_raw!
expect_next_instance_of(::Gitlab::Config::Loader::Yaml, data) do |loader|
......@@ -34,15 +149,13 @@ RSpec.describe BlobViewer::MetricsDashboardYml do
.to receive(:from_json)
.with(yml)
.and_call_original
expect(viewer.valid?).to be_truthy
expect(viewer.valid?).to be true
end
end
describe '#errors' do
it 'returns nil' do
allow(PerformanceMonitoring::PrometheusDashboard).to receive(:from_json)
expect(viewer.errors).to be nil
it 'returns empty array' do
expect(viewer.errors).to eq []
end
end
end
......@@ -60,7 +173,7 @@ RSpec.describe BlobViewer::MetricsDashboardYml do
expect(PerformanceMonitoring::PrometheusDashboard)
.to receive(:from_json).and_raise(error)
expect(viewer.valid?).to be_falsey
expect(viewer.valid?).to be false
end
end
......@@ -69,7 +182,7 @@ RSpec.describe BlobViewer::MetricsDashboardYml do
allow(PerformanceMonitoring::PrometheusDashboard)
.to receive(:from_json).and_raise(error)
expect(viewer.errors).to be error.model.errors
expect(viewer.errors).to eq error.model.errors.messages.map { |messages| messages.join(': ') }
end
end
end
......@@ -86,16 +199,13 @@ RSpec.describe BlobViewer::MetricsDashboardYml do
describe '#valid?' do
it 'returns false' do
expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:from_json)
expect(viewer.valid?).to be_falsey
expect(viewer.valid?).to be false
end
end
describe '#errors' do
it 'returns validation errors' do
yaml_wrapped_errors = { 'YAML syntax': ["(<unknown>): did not find expected key while parsing a block mapping at line 1 column 1"] }
expect(viewer.errors).to be_kind_of ActiveModel::Errors
expect(viewer.errors.messages).to eql(yaml_wrapped_errors)
expect(viewer.errors).to eq ["YAML syntax: (<unknown>): did not find expected key while parsing a block mapping at line 1 column 1"]
end
end
end
......@@ -108,20 +218,19 @@ RSpec.describe BlobViewer::MetricsDashboardYml do
end
before do
allow(::Gitlab::Config::Loader::Yaml).to receive(:new)
.and_raise(::Gitlab::Config::Loader::Yaml::DataTooLargeError, 'The parsed YAML is too big')
allow(::Gitlab::Config::Loader::Yaml).to(
receive(:new).and_raise(::Gitlab::Config::Loader::Yaml::DataTooLargeError, 'The parsed YAML is too big')
)
end
it 'is invalid' do
expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:from_json)
expect(viewer.valid?).to be(false)
expect(viewer.valid?).to be false
end
it 'returns validation errors' do
yaml_wrapped_errors = { 'YAML syntax': ["The parsed YAML is too big"] }
expect(viewer.errors).to be_kind_of(ActiveModel::Errors)
expect(viewer.errors.messages).to eq(yaml_wrapped_errors)
expect(viewer.errors).to eq ["YAML syntax: The parsed YAML is too big"]
end
end
end
end
......@@ -219,14 +219,74 @@ RSpec.describe PerformanceMonitoring::PrometheusDashboard do
end
describe '#schema_validation_warnings' do
let(:environment) { create(:environment, project: project) }
let(:path) { '.gitlab/dashboards/test.yml' }
let(:project) { create(:project, :repository, :custom_repo, files: { path => dashboard_schema.to_yaml }) }
subject(:schema_validation_warnings) { described_class.new(dashboard_schema.merge(path: path, environment: environment)).schema_validation_warnings }
before do
allow(Gitlab::Metrics::Dashboard::Finder).to receive(:find_raw).with(project, dashboard_path: path).and_call_original
end
context 'metrics_dashboard_exhaustive_validations is on' do
before do
stub_feature_flags(metrics_dashboard_exhaustive_validations: true)
end
context 'when schema is valid' do
it 'returns nil' do
expect(described_class).to receive(:from_json)
expect(described_class.new.schema_validation_warnings).to be_nil
let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml')) }
it 'returns empty array' do
expect(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).with(dashboard_schema, dashboard_path: path, project: project).and_return([])
expect(schema_validation_warnings).to eq []
end
end
context 'when schema is invalid' do
let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/dashboard_missing_panel_groups.yml')) }
it 'returns array with errors messages' do
error = ::Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError.new
expect(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).with(dashboard_schema, dashboard_path: path, project: project).and_return([error])
expect(schema_validation_warnings).to eq [error.message]
end
end
context 'when YAML has wrong syntax' do
let(:project) { create(:project, :repository, :custom_repo, files: { path => fixture_file('lib/gitlab/metrics/dashboard/broken_yml_syntax.yml') }) }
subject(:schema_validation_warnings) { described_class.new(path: path, environment: environment).schema_validation_warnings }
it 'returns array with errors messages' do
expect(Gitlab::Metrics::Dashboard::Validator).not_to receive(:errors)
expect(schema_validation_warnings).to eq ['Invalid yaml']
end
end
end
context 'metrics_dashboard_exhaustive_validations is off' do
before do
stub_feature_flags(metrics_dashboard_exhaustive_validations: false)
end
context 'when schema is valid' do
let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml')) }
it 'returns empty array' do
expect(described_class).to receive(:from_json).with(dashboard_schema)
expect(schema_validation_warnings).to eq []
end
end
context 'when schema is invalid' do
let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/dashboard_missing_panel_groups.yml')) }
it 'returns array with errors messages' do
instance = described_class.new
instance.errors.add(:test, 'test error')
......@@ -235,6 +295,19 @@ RSpec.describe PerformanceMonitoring::PrometheusDashboard do
expect(described_class.new.schema_validation_warnings).to eq ['test: test error']
end
end
context 'when YAML has wrong syntax' do
let(:project) { create(:project, :repository, :custom_repo, files: { path => fixture_file('lib/gitlab/metrics/dashboard/broken_yml_syntax.yml') }) }
subject(:schema_validation_warnings) { described_class.new(path: path, environment: environment).schema_validation_warnings }
it 'returns array with errors messages' do
expect(described_class).not_to receive(:from_json)
expect(schema_validation_warnings).to eq ['Invalid yaml']
end
end
end
end
describe '#to_yaml' do
......
......@@ -7,7 +7,7 @@ RSpec.describe 'Getting Metrics Dashboard' do
let_it_be(:current_user) { create(:user) }
let(:project) { create(:project) }
let!(:environment) { create(:environment, project: project) }
let(:environment) { create(:environment, project: project) }
let(:query) do
graphql_query_for(
......@@ -25,6 +25,11 @@ RSpec.describe 'Getting Metrics Dashboard' do
)
end
context 'with metrics_dashboard_exhaustive_validations feature flag off' do
before do
stub_feature_flags(metrics_dashboard_exhaustive_validations: false)
end
context 'for anonymous user' do
before do
post_graphql(query, current_user: current_user)
......@@ -55,7 +60,7 @@ RSpec.describe 'Getting Metrics Dashboard' do
it_behaves_like 'a working graphql query'
it 'returns metrics dashboard' do
dashboard = graphql_data.dig('project', 'environments', 'nodes')[0]['metricsDashboard']
dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => nil)
end
......@@ -89,10 +94,88 @@ RSpec.describe 'Getting Metrics Dashboard' do
it_behaves_like 'a working graphql query'
it 'returns nil' do
dashboard = graphql_data.dig('project', 'environments', 'nodes')[0]['metricsDashboard']
dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
expect(dashboard).to be_nil
end
end
end
end
context 'with metrics_dashboard_exhaustive_validations feature flag on' do
before do
stub_feature_flags(metrics_dashboard_exhaustive_validations: true)
end
context 'for anonymous user' do
before do
post_graphql(query, current_user: current_user)
end
context 'requested dashboard is available' do
let(:path) { 'config/prometheus/common_metrics.yml' }
it_behaves_like 'a working graphql query'
it 'returns nil' do
dashboard = graphql_data.dig('project', 'environments', 'nodes')
expect(dashboard).to be_nil
end
end
end
context 'for user with developer access' do
before do
project.add_developer(current_user)
post_graphql(query, current_user: current_user)
end
context 'requested dashboard is available' do
let(:path) { 'config/prometheus/common_metrics.yml' }
it_behaves_like 'a working graphql query'
it 'returns metrics dashboard' do
dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => nil)
end
context 'invalid dashboard' do
let(:path) { '.gitlab/dashboards/metrics.yml' }
let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "---\ndashboard: 'test'" }) }
it 'returns metrics dashboard' do
dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["root is missing required keys: panel_groups"])
end
end
context 'empty dashboard' do
let(:path) { '.gitlab/dashboards/metrics.yml' }
let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "" }) }
it 'returns metrics dashboard' do
dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["root is missing required keys: dashboard, panel_groups"])
end
end
end
context 'requested dashboard can not be found' do
let(:path) { 'config/prometheus/i_am_not_here.yml' }
it_behaves_like 'a working graphql query'
it 'returns nil' do
dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
expect(dashboard).to be_nil
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment