Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-02-02 18:09:54 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-02-02 18:09:54 +0300
commitd944f09d3212ca8aad09b92dbbae7323ee634237 (patch)
treebd2af8e867fb1baf2c2687fd12adab26515f3230 /spec
parentae9f43a2c4bda0ee7dae59ea9a7d412068f6f7ff (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/initializers/google_api_client_spec.rb3
-rw-r--r--spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb148
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_ci_internal_pipelines_metric_spec.rb28
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_issues_created_manually_from_alerts_metric_spec.rb28
-rw-r--r--spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb8
-rw-r--r--spec/lib/gitlab/usage/service_ping_report_spec.rb19
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb18
-rw-r--r--spec/mailers/emails/profile_spec.rb6
-rw-r--r--spec/models/concerns/exportable_spec.rb236
-rw-r--r--spec/models/note_spec.rb30
-rw-r--r--spec/support/helpers/usage_data_helpers.rb1
-rw-r--r--spec/support/shared_examples/models/chat_integration_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/exportable_shared_examples.rb73
13 files changed, 536 insertions, 64 deletions
diff --git a/spec/initializers/google_api_client_spec.rb b/spec/initializers/google_api_client_spec.rb
index 0ed82d7debe..b3c4ac5e23b 100644
--- a/spec/initializers/google_api_client_spec.rb
+++ b/spec/initializers/google_api_client_spec.rb
@@ -26,8 +26,9 @@ RSpec.describe Google::Apis::Core::HttpCommand do # rubocop:disable RSpec/FilePa
it 'retries with max elapsed_time and retries' do
expect(Retriable).to receive(:retriable).with(
tries: Google::Apis::RequestOptions.default.retries + 1,
- max_elapsed_time: 3600,
+ max_elapsed_time: 900,
base_interval: 1,
+ max_interval: 60,
multiplier: 2,
on: described_class::RETRIABLE_ERRORS).and_call_original
allow(Retriable).to receive(:retriable).and_call_original
diff --git a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb
index 02ac8065c9f..d8441a7aa30 100644
--- a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb
+++ b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer do
+RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer, feature_category: :importers do
let_it_be(:user) { create(:user) }
let_it_be(:release) { create(:release) }
let_it_be(:group) { create(:group) }
@@ -213,59 +213,143 @@ RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer do
end
end
- describe 'conditional export of included associations' do
+ describe 'with inaccessible associations' do
+ let_it_be(:milestone) { create(:milestone, project: exportable) }
+ let_it_be(:issue) { create(:issue, assignees: [user], project: exportable, milestone: milestone) }
+ let_it_be(:label1) { create(:label, project: exportable) }
+ let_it_be(:label2) { create(:label, project: exportable) }
+ let_it_be(:link1) { create(:label_link, label: label1, target: issue) }
+ let_it_be(:link2) { create(:label_link, label: label2, target: issue) }
+
+ let(:options) { { include: [{ label_links: { include: [:label] } }, { milestone: { include: [] } }] } }
+
let(:include) do
- [{ issues: { include: [{ label_links: { include: [:label] } }] } }]
+ [{ issues: options }]
end
- let(:include_if_exportable) do
- { issues: [:label_links] }
+ shared_examples 'record with exportable associations' do
+ it 'includes exportable association' do
+ expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, array_including(expected_issue))
+
+ subject.execute
+ end
end
- let_it_be(:label) { create(:label, project: exportable) }
- let_it_be(:link) { create(:label_link, label: label, target: issue) }
+ context 'conditional export of included associations' do
+ let(:include_if_exportable) do
+ { issues: [:label_links, :milestone] }
+ end
- context 'when association is exportable' do
- before do
- allow_next_found_instance_of(Issue) do |issue|
- allow(issue).to receive(:exportable_association?).with(:label_links, current_user: user).and_return(true)
+ context 'when association is exportable' do
+ before do
+ allow_next_found_instance_of(Issue) do |issue|
+ allow(issue).to receive(:exportable_association?).with(:label_links, current_user: user).and_return(true)
+ allow(issue).to receive(:exportable_association?).with(:milestone, current_user: user).and_return(true)
+ end
+ end
+
+ it_behaves_like 'record with exportable associations' do
+ let(:expected_issue) { issue.to_json(options) }
end
end
- it 'includes exportable association' do
- expected_issue = issue.to_json(include: [{ label_links: { include: [:label] } }])
+ context 'when an association is not exportable' do
+ before do
+ allow_next_found_instance_of(Issue) do |issue|
+ allow(issue).to receive(:exportable_association?).with(:label_links, current_user: user).and_return(true)
+ allow(issue).to receive(:exportable_association?).with(:milestone, current_user: user).and_return(false)
+ end
+ end
- expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, array_including(expected_issue))
+ it_behaves_like 'record with exportable associations' do
+ let(:expected_issue) { issue.to_json(include: [{ label_links: { include: [:label] } }]) }
+ end
+ end
- subject.execute
+ context 'when association does not respond to exportable_association?' do
+ before do
+ allow_next_found_instance_of(Issue) do |issue|
+ allow(issue).to receive(:respond_to?).and_call_original
+ allow(issue).to receive(:respond_to?).with(:exportable_association?).and_return(false)
+ end
+ end
+
+ it_behaves_like 'record with exportable associations' do
+ let(:expected_issue) { issue.to_json }
+ end
end
end
- context 'when association is not exportable' do
- before do
- allow_next_found_instance_of(Issue) do |issue|
- allow(issue).to receive(:exportable_association?).with(:label_links, current_user: user).and_return(false)
+ context 'export of included restricted associations' do
+ let(:many_relation) { :label_links }
+ let(:single_relation) { :milestone }
+ let(:issue_hash) { issue.as_json(options).with_indifferent_access }
+ let(:expected_issue) { issue.to_json(options) }
+
+ context 'when the association is restricted' do
+ context 'when some association records are exportable' do
+ before do
+ allow_next_found_instance_of(Issue) do |issue|
+ allow(issue).to receive(:restricted_associations).with([many_relation, single_relation]).and_return([many_relation])
+ allow(issue).to receive(:readable_records).with(many_relation, current_user: user).and_return([link1])
+ end
+ end
+
+ it_behaves_like 'record with exportable associations' do
+ let(:expected_issue) do
+ issue_hash[many_relation].delete_at(1)
+ issue_hash.to_json(options)
+ end
+ end
end
- end
- it 'filters out not exportable association' do
- expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, array_including(issue.to_json))
+ context 'when all association records are exportable' do
+ before do
+ allow_next_found_instance_of(Issue) do |issue|
+ allow(issue).to receive(:restricted_associations).with([many_relation, single_relation]).and_return([many_relation])
+ allow(issue).to receive(:readable_records).with(many_relation, current_user: user).and_return([link1, link2])
+ end
+ end
- subject.execute
- end
- end
+ it_behaves_like 'record with exportable associations'
+ end
- context 'when association does not respond to exportable_association?' do
- before do
- allow_next_found_instance_of(Issue) do |issue|
- allow(issue).to receive(:respond_to?).with(:exportable_association?).and_return(false)
+ context 'when the single association record is exportable' do
+ before do
+ allow_next_found_instance_of(Issue) do |issue|
+ allow(issue).to receive(:restricted_associations).with([many_relation, single_relation]).and_return([single_relation])
+ allow(issue).to receive(:readable_records).with(single_relation, current_user: user).and_return(milestone)
+ end
+ end
+
+ it_behaves_like 'record with exportable associations'
+ end
+
+ context 'when the single association record is not exportable' do
+ before do
+ allow_next_found_instance_of(Issue) do |issue|
+ allow(issue).to receive(:restricted_associations).with([many_relation, single_relation]).and_return([single_relation])
+ allow(issue).to receive(:readable_records).with(single_relation, current_user: user).and_return(nil)
+ end
+ end
+
+ it_behaves_like 'record with exportable associations' do
+ let(:expected_issue) do
+ issue_hash[single_relation] = nil
+ issue_hash.to_json(options)
+ end
+ end
end
end
- it 'filters out not exportable association' do
- expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, array_including(issue.to_json))
+ context 'when the associations are not restricted' do
+ before do
+ allow_next_found_instance_of(Issue) do |issue|
+ allow(issue).to receive(:restricted_associations).with([many_relation, single_relation]).and_return([])
+ end
+ end
- subject.execute
+ it_behaves_like 'record with exportable associations'
end
end
end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_internal_pipelines_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_internal_pipelines_metric_spec.rb
new file mode 100644
index 00000000000..afd8fccd56c
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_internal_pipelines_metric_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountCiInternalPipelinesMetric,
+feature_category: :service_ping do
+ let_it_be(:ci_pipeline_1) { create(:ci_pipeline, source: :external) }
+ let_it_be(:ci_pipeline_2) { create(:ci_pipeline, source: :push) }
+
+ let(:expected_value) { 1 }
+ let(:expected_query) do
+ 'SELECT COUNT("ci_pipelines"."id") FROM "ci_pipelines" ' \
+ 'WHERE ("ci_pipelines"."source" IN (1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15) ' \
+ 'OR "ci_pipelines"."source" IS NULL)'
+ end
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
+
+ context 'on Gitlab.com' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ let(:expected_value) { -1 }
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_issues_created_manually_from_alerts_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_issues_created_manually_from_alerts_metric_spec.rb
new file mode 100644
index 00000000000..86f54c48666
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_issues_created_manually_from_alerts_metric_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountIssuesCreatedManuallyFromAlertsMetric,
+feature_category: :service_ping do
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:issue_with_alert) { create(:issue, :with_alert) }
+
+ let(:expected_value) { 1 }
+ let(:expected_query) do
+ 'SELECT COUNT("issues"."id") FROM "issues" ' \
+ 'INNER JOIN "alert_management_alerts" ON "alert_management_alerts"."issue_id" = "issues"."id" ' \
+ 'WHERE "issues"."author_id" != 99'
+ end
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
+
+ context 'on Gitlab.com' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ let(:expected_value) { -1 }
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb b/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb
index 83a4ea8e948..4f647c2700a 100644
--- a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do
+RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator, feature_category: :service_ping do
include UsageDataHelpers
before do
@@ -43,9 +43,9 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do
context 'joined relations' do
context 'counted attribute comes from source relation' do
it_behaves_like 'name suggestion' do
- # corresponding metric is collected with count(Issue.with_alert_management_alerts.not_authored_by(::User.alert_bot), start: issue_minimum_id, finish: issue_maximum_id)
- let(:key_path) { 'counts.issues_created_manually_from_alerts' }
- let(:name_suggestion) { /count_<adjective describing: '\(issues\.author_id != \d+\)'>_issues_<with>_alert_management_alerts/ }
+ # corresponding metric is collected with distinct_count(Release.with_milestones, :author_id)
+ let(:key_path) { 'usage_activity_by_stage.release.releases_with_milestones' }
+ let(:name_suggestion) { /count_distinct_author_id_from_releases_<with>_milestone_releases/ }
end
end
end
diff --git a/spec/lib/gitlab/usage/service_ping_report_spec.rb b/spec/lib/gitlab/usage/service_ping_report_spec.rb
index ee2469ea463..730c05b7dcb 100644
--- a/spec/lib/gitlab/usage/service_ping_report_spec.rb
+++ b/spec/lib/gitlab/usage/service_ping_report_spec.rb
@@ -171,14 +171,25 @@ RSpec.describe Gitlab::Usage::ServicePingReport, :use_clean_rails_memory_store_c
let(:metric_definitions) { ::Gitlab::Usage::MetricDefinition.definitions }
it 'generates queries that match collected data', :aggregate_failures do
- message = "Expected %{query} result to match %{value} for %{key_path} metric"
+ message = "Expected %{query} result to match %{value} for %{key_path} metric (got %{payload_value} instead)"
metrics_queries_with_values.each do |key_path, query, value|
- value = type_cast_to_defined_type(value, metric_definitions[key_path.join('.')])
+ metric_definition = metric_definitions[key_path.join('.')]
+
+ # Skip broken metrics since they are usually overriden to return -1
+ next if metric_definition&.attributes&.fetch(:status) == 'broken'
+
+ value = type_cast_to_defined_type(value, metric_definition)
+ payload_value = service_ping_payload.dig(*key_path)
expect(value).to(
- eq(service_ping_payload.dig(*key_path)),
- message % { query: query, value: (value || 'NULL'), key_path: key_path.join('.') }
+ eq(payload_value),
+ message % {
+ query: query,
+ value: (value || 'NULL'),
+ payload_value: payload_value,
+ key_path: key_path.join('.')
+ }
)
end
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 0b2c0f9170e..ffa34acef5c 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -557,9 +557,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
expect(count_data[:issues_using_zoom_quick_actions]).to eq(3)
expect(count_data[:issues_with_embedded_grafana_charts_approx]).to eq(2)
expect(count_data[:incident_issues]).to eq(4)
- expect(count_data[:issues_created_gitlab_alerts]).to eq(1)
expect(count_data[:issues_created_from_alerts]).to eq(3)
- expect(count_data[:issues_created_manually_from_alerts]).to eq(1)
expect(count_data[:alert_bot_incident_issues]).to eq(4)
expect(count_data[:clusters_enabled]).to eq(6)
expect(count_data[:project_clusters_enabled]).to eq(4)
@@ -1137,20 +1135,4 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
expect(result).to be_nil
end
end
-
- context 'on Gitlab.com' do
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
-
- describe '.system_usage_data' do
- subject { described_class.system_usage_data }
-
- it 'returns fallback value for disabled metrics' do
- expect(subject[:counts][:ci_internal_pipelines]).to eq(Gitlab::Utils::UsageData::FALLBACK)
- expect(subject[:counts][:issues_created_gitlab_alerts]).to eq(Gitlab::Utils::UsageData::FALLBACK)
- expect(subject[:counts][:issues_created_manually_from_alerts]).to eq(Gitlab::Utils::UsageData::FALLBACK)
- end
- end
- end
end
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index 1fd2a92866d..f5fce559778 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -438,7 +438,7 @@ RSpec.describe Emails::Profile do
end
it 'includes a link to the change password documentation' do
- is_expected.to have_body_text 'https://docs.gitlab.com/ee/user/profile/user_passwords.html#change-your-password'
+ is_expected.to have_body_text help_page_url('user/profile/user_passwords', anchor: 'change-your-password')
end
it 'mentions two factor authentication when two factor is not enabled' do
@@ -446,7 +446,7 @@ RSpec.describe Emails::Profile do
end
it 'includes a link to two-factor authentication documentation' do
- is_expected.to have_body_text 'https://docs.gitlab.com/ee/user/profile/account/two_factor_authentication.html'
+ is_expected.to have_body_text help_page_url('user/profile/account/two_factor_authentication')
end
context 'when two factor authentication is enabled' do
@@ -488,7 +488,7 @@ RSpec.describe Emails::Profile do
end
it 'includes a link to the change password documentation' do
- is_expected.to have_body_text 'https://docs.gitlab.com/ee/user/profile/user_passwords.html#change-your-password'
+ is_expected.to have_body_text help_page_url('user/profile/user_passwords', anchor: 'change-your-password')
end
end
diff --git a/spec/models/concerns/exportable_spec.rb b/spec/models/concerns/exportable_spec.rb
new file mode 100644
index 00000000000..74709b06403
--- /dev/null
+++ b/spec/models/concerns/exportable_spec.rb
@@ -0,0 +1,236 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Exportable, feature_category: :importers do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:milestone) { create(:milestone, project: project) }
+ let_it_be(:issue) { create(:issue, project: project, milestone: milestone) }
+ let_it_be(:note1) { create(:system_note, project: project, noteable: issue) }
+ let_it_be(:note2) { create(:system_note, project: project, noteable: issue) }
+
+ let_it_be(:model_klass) do
+ Class.new(ApplicationRecord) do
+ include Exportable
+
+ belongs_to :project
+ has_one :milestone
+ has_many :notes
+
+ self.table_name = 'issues'
+
+ def self.name
+ 'Issue'
+ end
+ end
+ end
+
+ subject { model_klass.new }
+
+ describe '.readable_records' do
+ let_it_be(:model_record) { model_klass.new }
+
+ context 'when model does not respond to association name' do
+ it 'returns nil' do
+ expect(subject.readable_records(:foo, current_user: user)).to be_nil
+ end
+ end
+
+ context 'when model does respond to association name' do
+ context 'when there are no records' do
+ it 'returns nil' do
+ expect(model_record.readable_records(:notes, current_user: user)).to be_nil
+ end
+ end
+
+ context 'when association has #exportable_record? defined' do
+ before do
+ allow(model_record).to receive(:try).with(:notes).and_return(issue.notes)
+ end
+
+ context 'when user can read all records' do
+ before do
+ allow_next_found_instance_of(Note) do |note|
+ allow(note).to receive(:respond_to?).with(:exportable_record?).and_return(true)
+ allow(note).to receive(:exportable_record?).with(user).and_return(true)
+ end
+ end
+
+ it 'returns collection of readable records' do
+ expect(model_record.readable_records(:notes, current_user: user)).to contain_exactly(note1, note2)
+ end
+ end
+
+ context 'when user can not read records' do
+ before do
+ allow_next_instance_of(Note) do |note|
+ allow(note).to receive(:respond_to?).with(:exportable_record?).and_return(true)
+ allow(note).to receive(:exportable_record?).with(user).and_return(false)
+ end
+ end
+
+ it 'returns collection of readable records' do
+ expect(model_record.readable_records(:notes, current_user: user)).to eq([])
+ end
+ end
+ end
+
+ context 'when association does not have #exportable_record? defined' do
+ before do
+ allow(model_record).to receive(:try).with(:notes).and_return([note1])
+
+ allow(note1).to receive(:respond_to?).and_call_original
+ allow(note1).to receive(:respond_to?).with(:exportable_record?).and_return(false)
+ end
+
+ it 'calls #readable_by?' do
+ expect(note1).to receive(:readable_by?).with(user)
+
+ model_record.readable_records(:notes, current_user: user)
+ end
+ end
+
+ context 'with single relation' do
+ before do
+ allow(model_record).to receive(:try).with(:milestone).and_return(issue.milestone)
+ end
+
+ context 'when user can read the record' do
+ before do
+ allow(milestone).to receive(:readable_by?).with(user).and_return(true)
+ end
+
+ it 'returns collection of readable records' do
+ expect(model_record.readable_records(:milestone, current_user: user)).to eq(milestone)
+ end
+ end
+
+ context 'when user can not read the record' do
+ before do
+ allow(milestone).to receive(:readable_by?).with(user).and_return(false)
+ end
+
+ it 'returns collection of readable records' do
+ expect(model_record.readable_records(:milestone, current_user: user)).to be_nil
+ end
+ end
+ end
+ end
+ end
+
+ describe '.exportable_association?' do
+ context 'when model does not respond to association name' do
+ it 'returns false' do
+ expect(subject.exportable_association?(:tests)).to eq(false)
+
+ allow(issue).to receive(:respond_to?).with(:tests).and_return(false)
+ end
+ end
+
+ context 'when model responds to association name' do
+ let_it_be(:model_record) { model_klass.new }
+
+ context 'when association contains records' do
+ before do
+ allow(model_record).to receive(:try).with(:milestone).and_return(milestone)
+ end
+
+ context 'when current_user is not present' do
+ it 'returns false' do
+ expect(model_record.exportable_association?(:milestone)).to eq(false)
+ end
+ end
+
+ context 'when current_user can read association' do
+ before do
+ allow(milestone).to receive(:readable_by?).with(user).and_return(true)
+ end
+
+ it 'returns true' do
+ expect(model_record.exportable_association?(:milestone, current_user: user)).to eq(true)
+ end
+ end
+
+ context 'when current_user can not read association' do
+ before do
+ allow(milestone).to receive(:readable_by?).with(user).and_return(false)
+ end
+
+ it 'returns false' do
+ expect(model_record.exportable_association?(:milestone, current_user: user)).to eq(false)
+ end
+ end
+ end
+
+ context 'when association is empty' do
+ before do
+ allow(model_record).to receive(:try).with(:milestone).and_return(nil)
+ allow(milestone).to receive(:readable_by?).with(user).and_return(true)
+ end
+
+ it 'returns true' do
+ expect(model_record.exportable_association?(:milestone, current_user: user)).to eq(true)
+ end
+ end
+
+ context 'when association type is has_many' do
+ it 'returns true' do
+ expect(subject.exportable_association?(:notes)).to eq(true)
+ end
+ end
+ end
+ end
+
+ describe '.restricted_associations' do
+ let(:model_associations) { [:notes, :labels] }
+
+ context 'when `exportable_restricted_associations` is not defined in inheriting class' do
+ it 'returns empty array' do
+ expect(subject.restricted_associations(model_associations)).to eq([])
+ end
+ end
+
+ context 'when `exportable_restricted_associations` is defined in inheriting class' do
+ before do
+ stub_const('DummyModel', model_klass)
+
+ DummyModel.class_eval do
+ def exportable_restricted_associations
+ super + [:notes]
+ end
+ end
+ end
+
+ it 'returns empty array if provided key are not restricted' do
+ expect(subject.restricted_associations([:labels])).to eq([])
+ end
+
+ it 'returns array with restricted keys' do
+ expect(subject.restricted_associations(model_associations)).to contain_exactly(:notes)
+ end
+ end
+ end
+
+ describe '.has_many_association?' do
+ let(:model_associations) { [:notes, :labels] }
+
+ context 'when association type is `has_many`' do
+ it 'returns true' do
+ expect(subject.has_many_association?(:notes)).to eq(true)
+ end
+ end
+
+ context 'when association type is `has_one`' do
+ it 'returns true' do
+ expect(subject.has_many_association?(:milestone)).to eq(false)
+ end
+ end
+
+ context 'when association type is `belongs_to`' do
+ it 'returns true' do
+ expect(subject.has_many_association?(:project)).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 27a4132c27e..aa284f34c2f 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -1878,4 +1878,34 @@ RSpec.describe Note do
it { is_expected.to eq :read_internal_note }
end
end
+
+ describe '#exportable_record?' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:noteable) { create(:issue, project: project) }
+
+ subject { note.exportable_record?(user) }
+
+ context 'when not a system note' do
+ let(:note) { build(:note, noteable: noteable) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'with system note' do
+ let(:note) { build(:system_note, project: project, noteable: noteable) }
+
+ it 'returns `false` when the user cannot read the note' do
+ is_expected.to be_falsey
+ end
+
+ context 'when user can read the note' do
+ before do
+ project.add_developer(user)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
end
diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb
index aadc7dfb69d..2bec945fbc8 100644
--- a/spec/support/helpers/usage_data_helpers.rb
+++ b/spec/support/helpers/usage_data_helpers.rb
@@ -4,7 +4,6 @@ module UsageDataHelpers
COUNTS_KEYS = %i(
assignee_lists
ci_builds
- ci_internal_pipelines
ci_external_pipelines
ci_pipeline_config_auto_devops
ci_pipeline_config_repository
diff --git a/spec/support/shared_examples/models/chat_integration_shared_examples.rb b/spec/support/shared_examples/models/chat_integration_shared_examples.rb
index 6d0462a9ee8..fe880e4b31b 100644
--- a/spec/support/shared_examples/models/chat_integration_shared_examples.rb
+++ b/spec/support/shared_examples/models/chat_integration_shared_examples.rb
@@ -33,7 +33,7 @@ RSpec.shared_examples "chat integration" do |integration_name|
describe "#execute" do
let_it_be(:user) { create(:user) }
- let_it_be_with_reload(:project) { create(:project, :repository) }
+ let_it_be_with_refind(:project) { create(:project, :repository) }
let(:webhook_url) { "https://example.gitlab.com/" }
let(:webhook_url_regex) { /\A#{webhook_url}.*/ }
diff --git a/spec/support/shared_examples/models/exportable_shared_examples.rb b/spec/support/shared_examples/models/exportable_shared_examples.rb
new file mode 100644
index 00000000000..37c3e68fd5f
--- /dev/null
+++ b/spec/support/shared_examples/models/exportable_shared_examples.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'resource with exportable associations' do
+ before do
+ stub_licensed_features(stubbed_features) if stubbed_features.any?
+ end
+
+ describe '#exportable_association?' do
+ let(:association) { single_association }
+
+ subject { resource.exportable_association?(association, current_user: user) }
+
+ it { is_expected.to be_falsey }
+
+ context 'when user can read resource' do
+ before do
+ group.add_developer(user)
+ end
+
+ it { is_expected.to be_falsey }
+
+ context "when user can read resource's association" do
+ before do
+ other_group.add_developer(user)
+ end
+
+ it { is_expected.to be_truthy }
+
+ context 'for an unknown association' do
+ let(:association) { :foo }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'for an unauthenticated user' do
+ let(:user) { nil }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+ end
+
+ describe '#readable_records' do
+ subject { resource.readable_records(association, current_user: user) }
+
+ before do
+ group.add_developer(user)
+ end
+
+ context 'when association not supported' do
+ let(:association) { :foo }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when association is `:notes`' do
+ let(:association) { :notes }
+
+ it { is_expected.to match_array([readable_note]) }
+
+ context 'when user have access' do
+ before do
+ other_group.add_developer(user)
+ end
+
+ it 'returns all records' do
+ is_expected.to match_array([readable_note, restricted_note])
+ end
+ end
+ end
+ end
+end