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-04-25 15:18:56 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-04-25 15:18:56 +0300
commitd2d913b606702ecefa01f03362602fde256e3f75 (patch)
tree07643306ee63f789188a9133823aac3c92c94dfb /spec
parentaf69e63b6655a450849a8fa2640ae6ce5a8db681 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/factories/bulk_import/export_batches.rb1
-rw-r--r--spec/factories/bulk_import/exports.rb4
-rw-r--r--spec/factories/ci/processable.rb20
-rw-r--r--spec/factories/draft_note.rb4
-rw-r--r--spec/factories/environments.rb27
-rw-r--r--spec/factories/group_members.rb10
-rw-r--r--spec/factories/ml/candidates.rb10
-rw-r--r--spec/factories/notes.rb40
-rw-r--r--spec/features/dashboard/issuables_counter_spec.rb2
-rw-r--r--spec/features/dashboard/issues_filter_spec.rb73
-rw-r--r--spec/features/dashboard/issues_spec.rb43
-rw-r--r--spec/features/dashboard/label_filter_spec.rb34
-rw-r--r--spec/features/groups/labels/index_spec.rb3
-rw-r--r--spec/features/merge_requests/filters_generic_behavior_spec.rb2
-rw-r--r--spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap1
-rw-r--r--spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap4
-rw-r--r--spec/frontend/issuable/components/related_issuable_item_spec.js43
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_block_spec.js157
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_list_spec.js93
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_root_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js28
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_actions_spec.js29
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_spec.js18
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js30
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js34
-rw-r--r--spec/frontend/work_items/components/work_item_notes_spec.js3
-rw-r--r--spec/frontend/work_items/router_spec.js1
-rw-r--r--spec/helpers/sidebars_helper_spec.rb2
-rw-r--r--spec/helpers/work_items_helper_spec.rb24
-rw-r--r--spec/initializers/active_record_transaction_observer_spec.rb49
-rw-r--r--spec/lib/feature_spec.rb26
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb21
-rw-r--r--spec/models/bulk_imports/file_transfer/group_config_spec.rb49
-rw-r--r--spec/models/bulk_imports/file_transfer/project_config_spec.rb29
-rw-r--r--spec/services/bulk_imports/batched_relation_export_service_spec.rb104
-rw-r--r--spec/services/bulk_imports/export_service_spec.rb51
-rw-r--r--spec/services/bulk_imports/file_export_service_spec.rb62
-rw-r--r--spec/services/bulk_imports/lfs_objects_export_service_spec.rb23
-rw-r--r--spec/services/bulk_imports/relation_batch_export_service_spec.rb67
-rw-r--r--spec/services/bulk_imports/relation_export_service_spec.rb22
-rw-r--r--spec/services/bulk_imports/tree_export_service_spec.rb10
-rw-r--r--spec/services/bulk_imports/uploads_export_service_spec.rb33
-rw-r--r--spec/services/system_notes/time_tracking_service_spec.rb6
-rw-r--r--spec/spec_helper.rb4
-rw-r--r--spec/support/helpers/filtered_search_helpers.rb6
-rw-r--r--spec/views/projects/issues/_related_issues.html.haml_spec.rb37
-rw-r--r--spec/workers/bulk_imports/finish_batched_relation_export_worker_spec.rb80
-rw-r--r--spec/workers/bulk_imports/relation_batch_export_worker_spec.rb26
-rw-r--r--spec/workers/bulk_imports/relation_export_worker_spec.rb47
-rw-r--r--spec/workers/pipeline_process_worker_spec.rb42
59 files changed, 1186 insertions, 360 deletions
diff --git a/spec/factories/bulk_import/export_batches.rb b/spec/factories/bulk_import/export_batches.rb
index 4339b02d27e..f5f12696f5f 100644
--- a/spec/factories/bulk_import/export_batches.rb
+++ b/spec/factories/bulk_import/export_batches.rb
@@ -7,6 +7,7 @@ FactoryBot.define do
upload { association(:bulk_import_export_upload) }
status { 0 }
+ batch_number { 1 }
trait :started do
status { 0 }
diff --git a/spec/factories/bulk_import/exports.rb b/spec/factories/bulk_import/exports.rb
index dd8831ce33a..795a9bbfe20 100644
--- a/spec/factories/bulk_import/exports.rb
+++ b/spec/factories/bulk_import/exports.rb
@@ -20,5 +20,9 @@ FactoryBot.define do
trait :failed do
status { -1 }
end
+
+ trait :batched do
+ batched { true }
+ end
end
end
diff --git a/spec/factories/ci/processable.rb b/spec/factories/ci/processable.rb
index 49e66368f94..49756433713 100644
--- a/spec/factories/ci/processable.rb
+++ b/spec/factories/ci/processable.rb
@@ -26,13 +26,19 @@ FactoryBot.define do
before(:create) do |processable, evaluator|
next if processable.ci_stage
- if ci_stage = processable.pipeline.stages.find_by(name: evaluator.stage)
- processable.ci_stage = ci_stage
- else
- processable.ci_stage = create(:ci_stage, pipeline: processable.pipeline,
- project: processable.project || evaluator.project,
- name: evaluator.stage, position: evaluator.stage_idx, status: 'created')
- end
+ processable.ci_stage =
+ if ci_stage = processable.pipeline.stages.find_by(name: evaluator.stage)
+ ci_stage
+ else
+ create(
+ :ci_stage,
+ pipeline: processable.pipeline,
+ project: processable.project || evaluator.project,
+ name: evaluator.stage,
+ position: evaluator.stage_idx,
+ status: 'created'
+ )
+ end
end
trait :waiting_for_resource do
diff --git a/spec/factories/draft_note.rb b/spec/factories/draft_note.rb
index cde8831f169..8433271a3c5 100644
--- a/spec/factories/draft_note.rb
+++ b/spec/factories/draft_note.rb
@@ -28,9 +28,7 @@ FactoryBot.define do
end
position do
- association(:image_diff_position,
- file: path,
- diff_refs: diff_refs)
+ association(:image_diff_position, file: path, diff_refs: diff_refs)
end
end
end
diff --git a/spec/factories/environments.rb b/spec/factories/environments.rb
index 34843dab0fe..2df9f482bb9 100644
--- a/spec/factories/environments.rb
+++ b/spec/factories/environments.rb
@@ -46,20 +46,19 @@ FactoryBot.define do
after(:create) do |environment, evaluator|
pipeline = create(:ci_pipeline, project: environment.project)
- deployable = create(:ci_build, :success, name: "#{environment.name}:deploy",
- pipeline: pipeline)
-
- deployment = create(:deployment,
- :success,
- environment: environment,
- project: environment.project,
- deployable: deployable,
- ref: evaluator.ref,
- sha: environment.project.commit(evaluator.ref).id)
-
- teardown_build = create(:ci_build, :manual,
- name: "#{environment.name}:teardown",
- pipeline: pipeline)
+ deployable = create(:ci_build, :success, name: "#{environment.name}:deploy", pipeline: pipeline)
+
+ deployment = create(
+ :deployment,
+ :success,
+ environment: environment,
+ project: environment.project,
+ deployable: deployable,
+ ref: evaluator.ref,
+ sha: environment.project.commit(evaluator.ref).id
+ )
+
+ teardown_build = create(:ci_build, :manual, name: "#{environment.name}:teardown", pipeline: pipeline)
deployment.update_column(:on_stop, teardown_build.name)
environment.update_attribute(:deployments, [deployment])
diff --git a/spec/factories/group_members.rb b/spec/factories/group_members.rb
index c8ee52019a4..e1841745cb4 100644
--- a/spec/factories/group_members.rb
+++ b/spec/factories/group_members.rb
@@ -60,10 +60,12 @@ FactoryBot.define do
after(:build) do |group_member, evaluator|
if evaluator.tasks_to_be_done.present?
- build(:member_task,
- member: group_member,
- project: build(:project, namespace: group_member.source),
- tasks_to_be_done: evaluator.tasks_to_be_done)
+ build(
+ :member_task,
+ member: group_member,
+ project: build(:project, namespace: group_member.source),
+ tasks_to_be_done: evaluator.tasks_to_be_done
+ )
end
end
end
diff --git a/spec/factories/ml/candidates.rb b/spec/factories/ml/candidates.rb
index 9d049987cfd..b9a2320138a 100644
--- a/spec/factories/ml/candidates.rb
+++ b/spec/factories/ml/candidates.rb
@@ -21,10 +21,12 @@ FactoryBot.define do
trait :with_artifact do
after(:create) do |candidate|
- candidate.package = FactoryBot.create(:generic_package,
- name: candidate.package_name,
- version: candidate.package_version,
- project: candidate.project)
+ candidate.package = FactoryBot.create(
+ :generic_package,
+ name: candidate.package_name,
+ version: candidate.package_version,
+ project: candidate.project
+ )
end
end
end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index c58e7bb2e79..b1e7866f9ce 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -55,28 +55,34 @@ FactoryBot.define do
end
position do
- association(:text_diff_position,
- file: "files/ruby/popen.rb",
- old_line: nil,
- new_line: line_number,
- diff_refs: diff_refs)
+ association(
+ :text_diff_position,
+ file: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: line_number,
+ diff_refs: diff_refs
+ )
end
trait :folded_position do
position do
- association(:text_diff_position,
- file: "files/ruby/popen.rb",
- old_line: 1,
- new_line: 1,
- diff_refs: diff_refs)
+ association(
+ :text_diff_position,
+ file: "files/ruby/popen.rb",
+ old_line: 1,
+ new_line: 1,
+ diff_refs: diff_refs
+ )
end
end
factory :image_diff_note_on_merge_request do
position do
- association(:image_diff_position,
- file: "files/images/any_image.png",
- diff_refs: diff_refs)
+ association(
+ :image_diff_position,
+ file: "files/images/any_image.png",
+ diff_refs: diff_refs
+ )
end
end
end
@@ -101,9 +107,11 @@ FactoryBot.define do
factory :diff_note_on_design, parent: :note, traits: [:on_design], class: 'DiffNote' do
position do
- association(:image_diff_position,
- file: noteable.full_path,
- diff_refs: noteable.diff_refs)
+ association(
+ :image_diff_position,
+ file: noteable.full_path,
+ diff_refs: noteable.diff_refs
+ )
end
end
diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb
index 5dc59cfa841..5e6ec007569 100644
--- a/spec/features/dashboard/issuables_counter_spec.rb
+++ b/spec/features/dashboard/issuables_counter_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe 'Navigation bar counter', :use_clean_rails_memory_store_caching,
sign_in(user)
end
- it 'reflects dashboard issues count' do
+ it 'reflects dashboard issues count', :js do
visit issues_path
expect_counters('issues', '1', n_("%d assigned issue", "%d assigned issues", 1) % 1)
diff --git a/spec/features/dashboard/issues_filter_spec.rb b/spec/features/dashboard/issues_filter_spec.rb
index ee1e704c6c4..e67e04ee0b0 100644
--- a/spec/features/dashboard/issues_filter_spec.rb
+++ b/spec/features/dashboard/issues_filter_spec.rb
@@ -6,43 +6,55 @@ RSpec.describe 'Dashboard Issues filtering', :js, feature_category: :team_planni
include Features::SortingHelpers
include FilteredSearchHelpers
- let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:milestone) { create(:milestone, project: project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:milestone) { create(:milestone, project: project) }
- let!(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
- let!(:issue2) { create(:issue, project: project, author: user, assignees: [user], milestone: milestone) }
+ let_it_be(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
+ let_it_be(:issue2) { create(:issue, project: project, author: user, assignees: [user], milestone: milestone) }
+ let_it_be(:label) { create(:label, project: project, title: 'bug') }
+ let_it_be(:label_link) { create(:label_link, label: label, target: issue) }
+
+ let_it_be(:project2) { create(:project, namespace: user.namespace) }
+ let_it_be(:label2) { create(:label, title: 'bug') }
before do
+ project.labels << label
+ project2.labels << label2
project.add_maintainer(user)
sign_in(user)
-
- visit_issues
end
context 'without any filter' do
it 'shows error message' do
+ visit issues_dashboard_path
+
expect(page).to have_content 'Please select at least one filter to see results'
end
end
context 'filtering by milestone' do
it 'shows all issues with no milestone' do
- input_filtered_search("milestone:=none")
+ visit issues_dashboard_path
+
+ select_tokens 'Milestone', '=', 'None', submit: true
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_selector('.issue', count: 1)
end
it 'shows all issues with the selected milestone' do
- input_filtered_search("milestone:=%\"#{milestone.title}\"")
+ visit issues_dashboard_path
+
+ select_tokens 'Milestone', '=', milestone.title, submit: true
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_selector('.issue', count: 1)
end
it 'updates atom feed link' do
- visit_issues(milestone_title: '', assignee_username: user.username)
+ visit issues_dashboard_path(milestone_title: '', assignee_username: user.username)
+ click_button 'Actions'
link = find_link('Subscribe to RSS feed')
params = CGI.parse(URI.parse(link[:href]).query)
@@ -59,40 +71,47 @@ RSpec.describe 'Dashboard Issues filtering', :js, feature_category: :team_planni
end
context 'filtering by label' do
- let(:label) { create(:label, project: project) }
- let!(:label_link) { create(:label_link, label: label, target: issue) }
+ before do
+ visit issues_dashboard_path
+ end
it 'shows all issues with the selected label' do
- input_filtered_search("label:=~#{label.title}")
+ select_tokens 'Label', '=', label.title, submit: true
- page.within 'ul.content-list' do
- expect(page).to have_content issue.title
- expect(page).not_to have_content issue2.title
- end
+ expect(page).to have_content issue.title
+ expect(page).not_to have_content issue2.title
+ end
+
+ it 'removes duplicate labels' do
+ select_tokens 'Label', '='
+ send_keys 'bu'
+
+ expect_suggestion('bug')
+ expect_suggestion_count(3) # Expect None, Any, and bug
end
end
context 'sorting' do
before do
- visit_issues(assignee_username: user.username)
+ visit issues_dashboard_path(assignee_username: user.username)
end
it 'remembers last sorting value' do
- pajamas_sort_by(s_('SortOptions|Created date'))
- visit_issues(assignee_username: user.username)
+ click_button 'Created date'
+ click_button 'Updated date'
+
+ visit issues_dashboard_path(assignee_username: user.username)
- expect(page).to have_button('Created date')
+ expect(page).to have_button('Updated date')
end
it 'keeps sorting issues after visiting Projects Issues page' do
- pajamas_sort_by(s_('SortOptions|Created date'))
+ click_button 'Created date'
+ click_button 'Due date'
+
visit project_issues_path(project)
- expect(page).to have_button('Created date')
+ expect(page).to have_button('Due date')
end
end
-
- def visit_issues(...)
- visit issues_dashboard_path(...)
- end
end
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index 4499aa021ff..70d9f7e5137 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -5,15 +5,15 @@ require 'spec_helper'
RSpec.describe 'Dashboard Issues', feature_category: :team_planning do
include FilteredSearchHelpers
- let(:current_user) { create :user }
- let(:user) { current_user } # Shared examples depend on this being available
- let!(:public_project) { create(:project, :public) }
- let(:project) { create(:project) }
- let(:project_with_issues_disabled) { create(:project, :issues_disabled) }
- let!(:authored_issue) { create :issue, author: current_user, project: project }
- let!(:authored_issue_on_public_project) { create :issue, author: current_user, project: public_project }
- let!(:assigned_issue) { create :issue, assignees: [current_user], project: project }
- let!(:other_issue) { create :issue, project: project }
+ let_it_be(:current_user) { create :user }
+ let_it_be(:user) { current_user } # Shared examples depend on this being available
+ let_it_be(:public_project) { create(:project, :public) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:project_with_issues_disabled) { create(:project, :issues_disabled) }
+ let_it_be(:authored_issue) { create :issue, author: current_user, project: project }
+ let_it_be(:authored_issue_on_public_project) { create :issue, author: current_user, project: public_project }
+ let_it_be(:assigned_issue) { create :issue, assignees: [current_user], project: project }
+ let_it_be(:other_issue) { create :issue, project: project }
before do
[project, project_with_issues_disabled].each { |project| project.add_maintainer(current_user) }
@@ -23,16 +23,16 @@ RSpec.describe 'Dashboard Issues', feature_category: :team_planning do
it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :issues_dashboard_path, :issues
- describe 'issues' do
+ describe 'issues', :js do
it 'shows issues assigned to current user' do
expect(page).to have_content(assigned_issue.title)
expect(page).not_to have_content(authored_issue.title)
expect(page).not_to have_content(other_issue.title)
end
- it 'shows issues when current user is author', :js do
- reset_filters
- input_filtered_search("author:=#{current_user.to_reference}")
+ it 'shows issues when current user is author' do
+ click_button 'Clear'
+ select_tokens 'Author', '=', current_user.to_reference, submit: true
expect(page).to have_content(authored_issue.title)
expect(page).to have_content(authored_issue_on_public_project.title)
@@ -41,12 +41,21 @@ RSpec.describe 'Dashboard Issues', feature_category: :team_planning do
end
it 'state filter tabs work' do
- find('#state-closed').click
- expect(page).to have_current_path(issues_dashboard_url(assignee_username: current_user.username, state: 'closed'), url: true)
+ click_link 'Closed'
+
+ expect(page).not_to have_content(assigned_issue.title)
+ expect(page).not_to have_content(authored_issue.title)
+ expect(page).not_to have_content(other_issue.title)
end
- it_behaves_like "it has an RSS button with current_user's feed token"
- it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
+ describe 'RSS link' do
+ before do
+ click_button 'Actions'
+ end
+
+ it_behaves_like "it has an RSS link with current_user's feed token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
+ end
end
describe 'new issue dropdown' do
diff --git a/spec/features/dashboard/label_filter_spec.rb b/spec/features/dashboard/label_filter_spec.rb
deleted file mode 100644
index f116c84ff40..00000000000
--- a/spec/features/dashboard/label_filter_spec.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Dashboard > label filter', :js, feature_category: :team_planning do
- include FilteredSearchHelpers
-
- let(:filtered_search) { find('.filtered-search') }
- let(:filter_dropdown) { find("#js-dropdown-label .filter-dropdown") }
-
- let(:user) { create(:user) }
- let(:project) { create(:project, name: 'test', namespace: user.namespace) }
- let(:project2) { create(:project, name: 'test2', path: 'test2', namespace: user.namespace) }
- let(:label) { create(:label, title: 'bug', color: '#ff0000') }
- let(:label2) { create(:label, title: 'bug') }
-
- before do
- project.labels << label
- project2.labels << label2
-
- sign_in(user)
- visit issues_dashboard_path
-
- init_label_search
- end
-
- context 'duplicate labels' do
- it 'removes duplicate labels' do
- filtered_search.send_keys('bu')
-
- expect(filter_dropdown).to have_selector('.filter-dropdown-item', text: 'bug', count: 1)
- end
- end
-end
diff --git a/spec/features/groups/labels/index_spec.rb b/spec/features/groups/labels/index_spec.rb
index ea27fa2c5d9..7b0a38a83db 100644
--- a/spec/features/groups/labels/index_spec.rb
+++ b/spec/features/groups/labels/index_spec.rb
@@ -24,6 +24,7 @@ RSpec.describe 'Group labels', feature_category: :team_planning do
end
it 'shows an edit label button', :js do
- expect(page).to have_selector('.edit')
+ click_button 'Label actions dropdown'
+ expect(page).to have_link('Edit')
end
end
diff --git a/spec/features/merge_requests/filters_generic_behavior_spec.rb b/spec/features/merge_requests/filters_generic_behavior_spec.rb
index 197b9fa770d..4dbbde5168b 100644
--- a/spec/features/merge_requests/filters_generic_behavior_spec.rb
+++ b/spec/features/merge_requests/filters_generic_behavior_spec.rb
@@ -71,7 +71,7 @@ RSpec.describe 'Merge Requests > Filters generic behavior', :js, feature_categor
context 'filter dropdown' do
it 'filters by label name' do
- init_label_search
+ filtered_search.set('label:=')
filtered_search.send_keys('~bug')
page.within '.filter-dropdown' do
diff --git a/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap b/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap
index e379aba094c..ddeab3e3b62 100644
--- a/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap
+++ b/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap
@@ -35,6 +35,7 @@ exports[`AddContextCommitsModal renders modal with 2 tabs 1`] = `
placeholder="Search or filter commits"
searchbuttonattributes="[object Object]"
searchinputattributes="[object Object]"
+ searchtextoptionlabel="Search for this text"
value=""
/>
diff --git a/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap b/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap
index c979ee5a1d2..788e80de3f6 100644
--- a/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap
+++ b/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap
@@ -21,7 +21,7 @@ exports[`Comment templates list item component renders list item 1`] = `
class="gl-new-dropdown gl-disclosure-dropdown"
>
<button
- aria-controls="base-dropdown-5"
+ aria-controls="base-dropdown-7"
aria-labelledby="actions-toggle-3"
class="btn btn-default btn-md gl-button btn-default-tertiary gl-new-dropdown-toggle gl-new-dropdown-icon-only gl-new-dropdown-toggle-no-caret"
data-testid="base-dropdown-toggle"
@@ -60,7 +60,7 @@ exports[`Comment templates list item component renders list item 1`] = `
<div
class="gl-new-dropdown-panel"
data-testid="base-dropdown-menu"
- id="base-dropdown-5"
+ id="base-dropdown-7"
>
<div
class="gl-new-dropdown-inner"
diff --git a/spec/frontend/issuable/components/related_issuable_item_spec.js b/spec/frontend/issuable/components/related_issuable_item_spec.js
index 3e23558ceb4..68b41de4730 100644
--- a/spec/frontend/issuable/components/related_issuable_item_spec.js
+++ b/spec/frontend/issuable/components/related_issuable_item_spec.js
@@ -1,14 +1,18 @@
import { GlIcon, GlLink, GlButton } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import { formatDate } from '~/lib/utils/datetime_utility';
import { updateHistory } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
+import { stubComponent } from 'helpers/stub_component';
import RelatedIssuableItem from '~/issuable/components/related_issuable_item.vue';
import IssueMilestone from '~/issuable/components/issue_milestone.vue';
import IssueAssignees from '~/issuable/components/issue_assignees.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
+import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
+import { mockWorkItemCommentNote } from 'jest/work_items/mock_data';
import { defaultAssignees, defaultMilestone } from './related_issuable_mock_data';
jest.mock('~/lib/utils/url_utility', () => ({
@@ -18,6 +22,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
describe('RelatedIssuableItem', () => {
let wrapper;
+ let showModalSpy;
const defaultProps = {
idKey: 1,
@@ -40,13 +45,25 @@ describe('RelatedIssuableItem', () => {
const findRemoveButton = () => wrapper.findComponent(GlButton);
const findTitleLink = () => wrapper.findComponent(GlLink);
const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
+ const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
function mountComponent({ data = {}, props = {} } = {}) {
+ showModalSpy = jest.fn();
wrapper = shallowMount(RelatedIssuableItem, {
propsData: {
...defaultProps,
...props,
},
+ provide: {
+ reportAbusePath: '/report/abuse/path',
+ },
+ stubs: {
+ WorkItemDetailModal: stubComponent(WorkItemDetailModal, {
+ methods: {
+ show: showModalSpy,
+ },
+ }),
+ },
data() {
return data;
},
@@ -265,4 +282,30 @@ describe('RelatedIssuableItem', () => {
});
});
});
+
+ describe('abuse category selector', () => {
+ beforeEach(() => {
+ mountComponent({ props: { workItemType: 'TASK' } });
+ findTitleLink().vm.$emit('click', { preventDefault: () => {} });
+ });
+
+ it('should not be visible by default', () => {
+ expect(showModalSpy).toHaveBeenCalled();
+ expect(findAbuseCategorySelector().exists()).toBe(false);
+ });
+
+ it('should be visible when the work item modal emits `openReportAbuse` event', async () => {
+ findWorkItemDetailModal().vm.$emit('openReportAbuse', mockWorkItemCommentNote);
+
+ await nextTick();
+
+ expect(findAbuseCategorySelector().exists()).toBe(true);
+
+ findAbuseCategorySelector().vm.$emit('close-drawer');
+
+ await nextTick();
+
+ expect(findAbuseCategorySelector().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
index b9580b90c12..8807bc311f0 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
@@ -22,6 +22,41 @@ describe('RelatedIssuesBlock', () => {
const findRelatedIssuesBody = () => wrapper.findByTestId('related-issues-body');
const findIssueCountBadgeAddButton = () => wrapper.findByTestId('related-issues-plus-button');
+ const createComponent = ({
+ mountFn = mountExtended,
+ pathIdSeparator = PathIdSeparator.Issue,
+ issuableType = TYPE_ISSUE,
+ canAdmin = false,
+ helpPath = '',
+ isFetching = false,
+ isFormVisible = false,
+ relatedIssues = [],
+ showCategorizedIssues = false,
+ autoCompleteEpics = true,
+ slots = '',
+ } = {}) => {
+ wrapper = mountFn(RelatedIssuesBlock, {
+ propsData: {
+ pathIdSeparator,
+ issuableType,
+ canAdmin,
+ helpPath,
+ isFetching,
+ isFormVisible,
+ relatedIssues,
+ showCategorizedIssues,
+ autoCompleteEpics,
+ },
+ provide: {
+ reportAbusePath: '/report/abuse/path',
+ },
+ stubs: {
+ GlCard,
+ },
+ slots,
+ });
+ };
+
afterEach(() => {
if (wrapper) {
wrapper.destroy();
@@ -31,12 +66,7 @@ describe('RelatedIssuesBlock', () => {
describe('with defaults', () => {
beforeEach(() => {
- wrapper = mountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- issuableType: TYPE_ISSUE,
- },
- });
+ createComponent();
});
it.each`
@@ -46,13 +76,11 @@ describe('RelatedIssuesBlock', () => {
`(
'displays "$titleText" in the header and "$addButtonText" aria-label for add button when issuableType is set to "$issuableType"',
({ issuableType, pathIdSeparator, titleText, addButtonText }) => {
- wrapper = mountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator,
- issuableType,
- canAdmin: true,
- helpPath: '/help/user/project/issues/related_issues',
- },
+ createComponent({
+ pathIdSeparator,
+ issuableType,
+ canAdmin: true,
+ helpPath: '/help/user/project/issues/related_issues',
});
expect(wrapper.find('.card-title').text()).toContain(titleText);
@@ -73,14 +101,8 @@ describe('RelatedIssuesBlock', () => {
it('displays header text slot data', () => {
const headerText = '<div>custom header text</div>';
- wrapper = shallowMountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- issuableType: 'issue',
- },
- stubs: {
- GlCard,
- },
+ createComponent({
+ mountFn: shallowMountExtended,
slots: { 'header-text': headerText },
});
@@ -92,14 +114,8 @@ describe('RelatedIssuesBlock', () => {
it('displays header actions slot data', () => {
const headerActions = '<button data-testid="custom-button">custom button</button>';
- wrapper = shallowMountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- issuableType: 'issue',
- },
- stubs: {
- GlCard,
- },
+ createComponent({
+ mountFn: shallowMountExtended,
slots: { 'header-actions': headerActions },
});
@@ -109,12 +125,8 @@ describe('RelatedIssuesBlock', () => {
describe('with isFetching=true', () => {
beforeEach(() => {
- wrapper = mountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- isFetching: true,
- issuableType: 'issue',
- },
+ createComponent({
+ isFetching: true,
});
});
@@ -125,13 +137,7 @@ describe('RelatedIssuesBlock', () => {
describe('with canAddRelatedIssues=true', () => {
beforeEach(() => {
- wrapper = mountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- canAdmin: true,
- issuableType: 'issue',
- },
- });
+ createComponent({ canAdmin: true });
});
it('can add new related issues', () => {
@@ -141,14 +147,7 @@ describe('RelatedIssuesBlock', () => {
describe('with isFormVisible=true', () => {
beforeEach(() => {
- wrapper = mountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- isFormVisible: true,
- issuableType: 'issue',
- autoCompleteEpics: false,
- },
- });
+ createComponent({ isFormVisible: true, autoCompleteEpics: false });
});
it('shows add related issues form', () => {
@@ -164,19 +163,14 @@ describe('RelatedIssuesBlock', () => {
const issueList = () => wrapper.findAll('.js-related-issues-token-list-item');
const categorizedHeadings = () => wrapper.findAll('h4');
const headingTextAt = (index) => categorizedHeadings().at(index).text();
- const mountComponent = (showCategorizedIssues) => {
- wrapper = mountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- relatedIssues: [issuable1, issuable2, issuable3],
- issuableType: 'issue',
- showCategorizedIssues,
- },
- });
- };
describe('when showCategorizedIssues=true', () => {
- beforeEach(() => mountComponent(true));
+ beforeEach(() =>
+ createComponent({
+ showCategorizedIssues: true,
+ relatedIssues: [issuable1, issuable2, issuable3],
+ }),
+ );
it('should render issue tokens items', () => {
expect(issueList()).toHaveLength(3);
@@ -203,8 +197,10 @@ describe('RelatedIssuesBlock', () => {
describe('when showCategorizedIssues=false', () => {
it('should render issues as a flat list with no header', () => {
- mountComponent(false);
-
+ createComponent({
+ showCategorizedIssues: false,
+ relatedIssues: [issuable1, issuable2, issuable3],
+ });
expect(issueList()).toHaveLength(3);
expect(categorizedHeadings()).toHaveLength(0);
});
@@ -223,14 +219,8 @@ describe('RelatedIssuesBlock', () => {
},
].forEach(({ issuableType, icon }) => {
it(`issuableType=${issuableType} is passed`, () => {
- wrapper = shallowMountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- issuableType,
- },
- stubs: {
- GlCard,
- },
+ createComponent({
+ issuableType,
});
const iconComponent = wrapper.findComponent(GlIcon);
@@ -242,15 +232,8 @@ describe('RelatedIssuesBlock', () => {
describe('toggle', () => {
beforeEach(() => {
- wrapper = shallowMountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- relatedIssues: [issuable1, issuable2, issuable3],
- issuableType: TYPE_ISSUE,
- },
- stubs: {
- GlCard,
- },
+ createComponent({
+ relatedIssues: [issuable1, issuable2, issuable3],
});
});
@@ -280,14 +263,12 @@ describe('RelatedIssuesBlock', () => {
`(
'displays "$emptyText" in the body and "$helpLinkText" aria-label for help link',
({ issuableType, pathIdSeparator, showCategorizedIssues, emptyText, helpLinkText }) => {
- wrapper = mountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator,
- issuableType,
- canAdmin: true,
- helpPath: '/help/user/project/issues/related_issues',
- showCategorizedIssues,
- },
+ createComponent({
+ pathIdSeparator,
+ issuableType,
+ canAdmin: true,
+ helpPath: '/help/user/project/issues/related_issues',
+ showCategorizedIssues,
});
expect(wrapper.findByTestId('related-issues-body').text()).toContain(emptyText);
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_list_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_list_spec.js
index 9bb71ec3dcb..0a6a0a90d44 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_list_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_list_spec.js
@@ -13,6 +13,30 @@ import { PathIdSeparator } from '~/related_issues/constants';
describe('RelatedIssuesList', () => {
let wrapper;
+ const createComponent = ({
+ mountFn = shallowMount,
+ pathIdSeparator = PathIdSeparator.Issue,
+ issuableType = 'issue',
+ listLinkType = 'relates_to',
+ heading = '',
+ isFetching = false,
+ relatedIssues = [],
+ } = {}) => {
+ wrapper = mountFn(RelatedIssuesList, {
+ propsData: {
+ pathIdSeparator,
+ issuableType,
+ listLinkType,
+ heading,
+ isFetching,
+ relatedIssues,
+ },
+ provide: {
+ reportAbusePath: '/report/abuse/path',
+ },
+ });
+ };
+
afterEach(() => {
if (wrapper) {
wrapper.destroy();
@@ -24,14 +48,7 @@ describe('RelatedIssuesList', () => {
const heading = 'Related to';
beforeEach(() => {
- wrapper = shallowMount(RelatedIssuesList, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- issuableType: 'issue',
- listLinkType: 'relates_to',
- heading,
- },
- });
+ createComponent({ heading });
});
it('assigns value of listLinkType prop to data attribute', () => {
@@ -49,13 +66,7 @@ describe('RelatedIssuesList', () => {
describe('with isFetching=true', () => {
beforeEach(() => {
- wrapper = shallowMount(RelatedIssuesList, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- isFetching: true,
- issuableType: 'issue',
- },
- });
+ createComponent({ isFetching: true });
});
it('should show loading icon', () => {
@@ -65,13 +76,7 @@ describe('RelatedIssuesList', () => {
describe('methods', () => {
beforeEach(() => {
- wrapper = shallowMount(RelatedIssuesList, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- relatedIssues: [issuable1, issuable2, issuable3, issuable4, issuable5],
- issuableType: 'issue',
- },
- });
+ createComponent({ relatedIssues: [issuable1, issuable2, issuable3, issuable4, issuable5] });
});
it('updates the order correctly when an item is moved to the top', () => {
@@ -112,23 +117,17 @@ describe('RelatedIssuesList', () => {
});
describe('issuableOrderingId returns correct issuable order id when', () => {
- it('issuableType is epic', () => {
- wrapper = shallowMount(RelatedIssuesList, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- issuableType: 'issue',
- },
+ it('issuableType is issue', () => {
+ createComponent({
+ issuableType: 'issue',
});
expect(wrapper.vm.issuableOrderingId(issuable1)).toBe(issuable1.epicIssueId);
});
- it('issuableType is issue', () => {
- wrapper = shallowMount(RelatedIssuesList, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- issuableType: 'epic',
- },
+ it('issuableType is epic', () => {
+ createComponent({
+ issuableType: 'epic',
});
expect(wrapper.vm.issuableOrderingId(issuable1)).toBe(issuable1.id);
@@ -143,12 +142,9 @@ describe('RelatedIssuesList', () => {
});
it('issuableType is epic', () => {
- wrapper = shallowMount(RelatedIssuesList, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- issuableType: 'epic',
- relatedIssues,
- },
+ createComponent({
+ issuableType: 'epic',
+ relatedIssues,
});
const listItems = wrapper.vm.$el.querySelectorAll('.list-item');
@@ -159,12 +155,9 @@ describe('RelatedIssuesList', () => {
});
it('issuableType is issue', () => {
- wrapper = shallowMount(RelatedIssuesList, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- issuableType: 'issue',
- relatedIssues,
- },
+ createComponent({
+ issuableType: 'issue',
+ relatedIssues,
});
const listItems = wrapper.vm.$el.querySelectorAll('.list-item');
@@ -177,13 +170,7 @@ describe('RelatedIssuesList', () => {
describe('related item contents', () => {
beforeAll(() => {
- wrapper = mount(RelatedIssuesList, {
- propsData: {
- issuableType: 'issue',
- pathIdSeparator: PathIdSeparator.Issue,
- relatedIssues: [issuable1],
- },
- });
+ createComponent({ mountFn: mount, relatedIssues: [issuable1] });
});
it('shows due date', () => {
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
index 1383013aedb..b119c836411 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
@@ -42,6 +42,9 @@ describe('RelatedIssuesRoot', () => {
...defaultProps,
...props,
},
+ provide: {
+ reportAbusePath: '/report/abuse/path',
+ },
data() {
return data;
},
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
index f115ec2d6ca..d87aa3194d2 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
@@ -99,6 +99,7 @@ function createComponent({
portalName: 'fake target',
alignSuggestions: jest.fn(),
suggestionsListClass: () => 'custom-class',
+ termsAsTokens: () => false,
filteredSearchSuggestionListInstance: {
register: jest.fn(),
unregister: jest.fn(),
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
index a6bb32736db..6bbbfd838a0 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
@@ -46,6 +46,7 @@ function createComponent(options = {}) {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: () => 'custom-class',
+ termsAsTokens: () => false,
},
stubs,
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
index ce134f7d24e..fb8cea09a9b 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
@@ -71,6 +71,7 @@ describe('CrmContactToken', () => {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: () => 'custom-class',
+ termsAsTokens: () => false,
},
stubs,
listeners,
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
index f41c5b5d432..20369342220 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
@@ -70,6 +70,7 @@ describe('CrmOrganizationToken', () => {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: () => 'custom-class',
+ termsAsTokens: () => false,
},
stubs,
listeners,
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
index 0dddae50c4e..5e675c10038 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
@@ -52,6 +52,7 @@ function createComponent(options = {}) {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: () => 'custom-class',
+ termsAsTokens: () => false,
},
stubs,
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
index 696483df8ef..c55721fe032 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
@@ -51,6 +51,7 @@ function createComponent(options = {}) {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: () => 'custom-class',
+ termsAsTokens: () => false,
},
stubs,
listeners,
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
index c758e550ba2..db51b4a05b1 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
@@ -49,6 +49,7 @@ function createComponent(options = {}) {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: () => 'custom-class',
+ termsAsTokens: () => false,
},
stubs,
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
index 5190ab919b1..79fd527cbe3 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
@@ -24,6 +24,7 @@ describe('ReleaseToken', () => {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: () => 'custom-class',
+ termsAsTokens: () => false,
},
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js
index d0a6519f16d..e4ca7dcb19a 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js
@@ -57,6 +57,7 @@ function createComponent(options = {}) {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: () => 'custom-class',
+ termsAsTokens: () => false,
},
data() {
return { ...data };
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
index cd4ebe334c0..015e08ed760 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
@@ -1,6 +1,6 @@
import { GlAlert, GlModal, GlButton, GlSkeletonLoader } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, ErrorWrapper } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -41,7 +41,12 @@ describe('RunnerInstructionsModal component', () => {
let runnerPlatformsHandler;
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
- const findAlert = () => wrapper.findComponent(GlAlert);
+ const findAlert = (variant = 'danger') => {
+ const { wrappers } = wrapper
+ .findAllComponents(GlAlert)
+ .filter((w) => w.props('variant') === variant);
+ return wrappers[0] || new ErrorWrapper();
+ };
const findModal = () => wrapper.findComponent(GlModal);
const findPlatformButtonGroup = () => wrapper.findByTestId('platform-buttons');
const findPlatformButtons = () => findPlatformButtonGroup().findAllComponents(GlButton);
@@ -84,6 +89,10 @@ describe('RunnerInstructionsModal component', () => {
expect(findAlert().exists()).toBe(false);
});
+ it('should not show deprecation alert', () => {
+ expect(findAlert('warning').exists()).toBe(false);
+ });
+
it('should contain a number of platforms buttons', () => {
expect(runnerPlatformsHandler).toHaveBeenCalledWith({});
@@ -100,6 +109,21 @@ describe('RunnerInstructionsModal component', () => {
);
});
+ describe.each`
+ glFeatures | deprecationAlertExists
+ ${{}} | ${false}
+ ${{ createRunnerWorkflowForAdmin: true }} | ${true}
+ ${{ createRunnerWorkflowForNamespace: true }} | ${true}
+ `('with features $glFeatures', ({ glFeatures, deprecationAlertExists }) => {
+ beforeEach(() => {
+ createComponent({ provide: { glFeatures } });
+ });
+
+ it(`alert is ${deprecationAlertExists ? 'shown' : 'not shown'}`, () => {
+ expect(findAlert('warning').exists()).toBe(deprecationAlertExists);
+ });
+ });
+
describe('when the modal resizes', () => {
it('to an xs viewport', async () => {
MockResizeObserver.mockResize('xs');
diff --git a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
index b406c9d843a..99bf391e261 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
@@ -22,6 +22,7 @@ describe('Work Item Note Actions', () => {
const findDeleteNoteButton = () => wrapper.find('[data-testid="delete-note-action"]');
const findCopyLinkButton = () => wrapper.find('[data-testid="copy-link-action"]');
const findAssignUnassignButton = () => wrapper.find('[data-testid="assign-note-action"]');
+ const findReportAbuseToAdminButton = () => wrapper.find('[data-testid="abuse-note-action"]');
const addEmojiMutationResolver = jest.fn().mockResolvedValue({
data: {
@@ -39,6 +40,7 @@ describe('Work Item Note Actions', () => {
showEdit = true,
showAwardEmoji = true,
showAssignUnassign = false,
+ canReportAbuse = false,
} = {}) => {
wrapper = shallowMount(WorkItemNoteActions, {
propsData: {
@@ -47,6 +49,7 @@ describe('Work Item Note Actions', () => {
noteId,
showAwardEmoji,
showAssignUnassign,
+ canReportAbuse,
},
provide: {
glFeatures: {
@@ -195,4 +198,30 @@ describe('Work Item Note Actions', () => {
expect(wrapper.emitted('assignUser')).toEqual([[]]);
});
});
+
+ describe('report abuse to admin', () => {
+ it('should not report abuse to admin by default', () => {
+ createComponent();
+
+ expect(findReportAbuseToAdminButton().exists()).toBe(false);
+ });
+
+ it('should display assign/unassign when the props is true', () => {
+ createComponent({
+ canReportAbuse: true,
+ });
+
+ expect(findReportAbuseToAdminButton().exists()).toBe(true);
+ });
+
+ it('should emit `reportAbuse` event when report abuse action is clicked', () => {
+ createComponent({
+ canReportAbuse: true,
+ });
+
+ findReportAbuseToAdminButton().vm.$emit('click');
+
+ expect(wrapper.emitted('reportAbuse')).toEqual([[]]);
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_spec.js b/spec/frontend/work_items/components/notes/work_item_note_spec.js
index 69b7c7b0828..f8be2f5667b 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_spec.js
@@ -300,5 +300,23 @@ describe('Work Item Note', () => {
});
});
});
+
+ describe('report abuse props', () => {
+ it.each`
+ currentUserId | canReportAbuse | sameAsAuthor
+ ${1} | ${false} | ${'same as'}
+ ${4} | ${true} | ${'not same as'}
+ `(
+ 'should be $canReportAbuse when the author is $sameAsAuthor as the author of the note',
+ ({ currentUserId, canReportAbuse }) => {
+ window.gon = {
+ current_user_id: currentUserId,
+ };
+ createComponent();
+
+ expect(findNoteActions().props('canReportAbuse')).toBe(canReportAbuse);
+ },
+ );
+ });
});
});
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index 46189850e09..1d164648e27 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -26,6 +26,7 @@ import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
+import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import { i18n } from '~/work_items/constants';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import workItemDatesSubscription from '~/graphql_shared/subscriptions/work_item_dates.subscription.graphql';
@@ -43,6 +44,7 @@ import {
workItemAssigneesSubscriptionResponse,
workItemMilestoneSubscriptionResponse,
objectiveType,
+ mockWorkItemCommentNote,
} from '../mock_data';
describe('WorkItemDetail component', () => {
@@ -88,6 +90,7 @@ describe('WorkItemDetail component', () => {
const findHierarchyTree = () => wrapper.findComponent(WorkItemTree);
const findNotesWidget = () => wrapper.findComponent(WorkItemNotes);
const findModal = () => wrapper.findComponent(WorkItemDetailModal);
+ const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
const createComponent = ({
isModal = false,
@@ -128,6 +131,7 @@ describe('WorkItemDetail component', () => {
hasIssuableHealthStatusFeature: true,
projectNamespace: 'namespace',
fullPath: 'group/project',
+ reportAbusePath: '/report/abuse/path',
},
stubs: {
WorkItemWeight: true,
@@ -725,4 +729,30 @@ describe('WorkItemDetail component', () => {
expect(findCreatedUpdated().exists()).toBe(true);
});
+
+ describe('abuse category selector', () => {
+ beforeEach(async () => {
+ setWindowLocation('?work_item_id=2');
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('should not be visible by default', () => {
+ expect(findAbuseCategorySelector().exists()).toBe(false);
+ });
+
+ it('should be visible when the work item modal emits `openReportAbuse` event', async () => {
+ findModal().vm.$emit('openReportAbuse', mockWorkItemCommentNote);
+
+ await nextTick();
+
+ expect(findAbuseCategorySelector().exists()).toBe(true);
+
+ findAbuseCategorySelector().vm.$emit('close-drawer');
+
+ await nextTick();
+
+ expect(findAbuseCategorySelector().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
index efa08ced3ad..4bf7d0c57a3 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
@@ -12,6 +12,7 @@ import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
+import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import { FORM_TYPES } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import changeWorkItemParentMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
@@ -25,6 +26,7 @@ import {
changeWorkItemParentMutationResponse,
workItemQueryResponse,
projectWorkItemResponse,
+ mockWorkItemCommentNote,
} from '../../mock_data';
Vue.use(VueApollo);
@@ -76,6 +78,7 @@ describe('WorkItemLinks', () => {
provide: {
projectPath: 'project/path',
hasIterationsFeature,
+ reportAbusePath: '/report/abuse/path',
},
propsData: { issuableId: 1 },
apolloProvider: mockApollo,
@@ -105,6 +108,8 @@ describe('WorkItemLinks', () => {
const findFirstWorkItemLinkChild = () => findWorkItemLinkChildItems().at(0);
const findAddLinksForm = () => wrapper.findByTestId('add-links-form');
const findChildrenCount = () => wrapper.findByTestId('children-count');
+ const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
+ const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
afterEach(() => {
mockApollo = null;
@@ -328,7 +333,7 @@ describe('WorkItemLinks', () => {
await createComponent();
expect(showModal).not.toHaveBeenCalled();
- expect(wrapper.findComponent(WorkItemDetailModal).props('workItemIid')).toBe(null);
+ expect(findWorkItemDetailModal().props('workItemIid')).toBe(null);
});
it('opens the modal if work item iid URL parameter is found in child items', async () => {
@@ -336,6 +341,31 @@ describe('WorkItemLinks', () => {
await createComponent();
expect(showModal).toHaveBeenCalled();
- expect(wrapper.findComponent(WorkItemDetailModal).props('workItemIid')).toBe('2');
+ expect(findWorkItemDetailModal().props('workItemIid')).toBe('2');
+ });
+
+ describe('abuse category selector', () => {
+ beforeEach(async () => {
+ setWindowLocation('?work_item_id=2');
+ await createComponent();
+ });
+
+ it('should not be visible by default', () => {
+ expect(findAbuseCategorySelector().exists()).toBe(false);
+ });
+
+ it('should be visible when the work item modal emits `openReportAbuse` event', async () => {
+ findWorkItemDetailModal().vm.$emit('openReportAbuse', mockWorkItemCommentNote);
+
+ await nextTick();
+
+ expect(findAbuseCategorySelector().exists()).toBe(true);
+
+ findAbuseCategorySelector().vm.$emit('close-drawer');
+
+ await nextTick();
+
+ expect(findAbuseCategorySelector().exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/work_items/components/work_item_notes_spec.js b/spec/frontend/work_items/components/work_item_notes_spec.js
index 3cc6a9813fc..7dbf828c44a 100644
--- a/spec/frontend/work_items/components/work_item_notes_spec.js
+++ b/spec/frontend/work_items/components/work_item_notes_spec.js
@@ -97,6 +97,7 @@ describe('WorkItemNotes component', () => {
workItemIid = mockWorkItemIid,
defaultWorkItemNotesQueryHandler = workItemNotesQueryHandler,
deleteWINoteMutationHandler = deleteWorkItemNoteMutationSuccessHandler,
+ isModal = false,
} = {}) => {
wrapper = shallowMount(WorkItemNotes, {
apolloProvider: createMockApollo([
@@ -116,6 +117,8 @@ describe('WorkItemNotes component', () => {
fullPath: 'test-path',
fetchByIid,
workItemType: 'task',
+ reportAbusePath: '/report/abuse/path',
+ isModal,
},
stubs: {
GlModal: stubComponent(GlModal, { methods: { show: showModal } }),
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
index 988fdc301de..86e890ea809 100644
--- a/spec/frontend/work_items/router_spec.js
+++ b/spec/frontend/work_items/router_spec.js
@@ -74,6 +74,7 @@ describe('Work items router', () => {
hasIterationsFeature: false,
hasOkrsFeature: false,
hasIssuableHealthStatusFeature: false,
+ reportAbusePath: '/report/abuse/path',
},
stubs: {
WorkItemWeight: true,
diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb
index 6c0ac024944..5fbda3d77b0 100644
--- a/spec/helpers/sidebars_helper_spec.rb
+++ b/spec/helpers/sidebars_helper_spec.rb
@@ -401,7 +401,7 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
it 'returns public links and admin area link' do
expect(subject[:context_switcher_links]).to eq([
*public_link,
- { title: s_('Navigation|Admin'), link: '/admin', icon: 'admin' }
+ { title: s_('Navigation|Admin Area'), link: '/admin', icon: 'admin' }
])
end
end
diff --git a/spec/helpers/work_items_helper_spec.rb b/spec/helpers/work_items_helper_spec.rb
new file mode 100644
index 00000000000..4e1eca3d411
--- /dev/null
+++ b/spec/helpers/work_items_helper_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe WorkItemsHelper, feature_category: :team_planning do
+ describe '#work_items_index_data' do
+ subject(:work_items_index_data) { helper.work_items_index_data(project) }
+
+ let_it_be(:project) { build(:project) }
+
+ it 'returns the expected data properties' do
+ expect(work_items_index_data).to include(
+ {
+ full_path: project.full_path,
+ issues_list_path: project_issues_path(project),
+ register_path: new_user_registration_path(redirect_to_referer: 'yes'),
+ sign_in_path: user_session_path(redirect_to_referer: 'yes'),
+ new_comment_template_path: profile_comment_templates_path,
+ report_abuse_path: add_category_abuse_reports_path
+ }
+ )
+ end
+ end
+end
diff --git a/spec/initializers/active_record_transaction_observer_spec.rb b/spec/initializers/active_record_transaction_observer_spec.rb
new file mode 100644
index 00000000000..a834037dce5
--- /dev/null
+++ b/spec/initializers/active_record_transaction_observer_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'ActiveRecord Transaction Observer', feature_category: :application_performance do
+ def load_initializer
+ load Rails.root.join('config/initializers/active_record_transaction_observer.rb')
+ end
+
+ context 'when DBMS is available' do
+ before do
+ allow_next_instance_of(ActiveRecord::Base.connection) do |connection| # rubocop:disable Database/MultipleDatabases
+ allow(connection).to receive(:active?).and_return(true)
+ end
+ end
+
+ it 'calls Gitlab::Database::Transaction::Observer' do
+ allow(Feature::FlipperFeature).to receive(:table_exists?).and_return(true)
+
+ expect(Gitlab::Database::Transaction::Observer).to receive(:register!)
+
+ load_initializer
+ end
+
+ context 'when flipper table does not exist' do
+ before do
+ allow(Feature::FlipperFeature).to receive(:table_exists?).and_raise(ActiveRecord::NoDatabaseError)
+ end
+
+ it 'does not calls Gitlab::Database::Transaction::Observer' do
+ expect(Gitlab::Database::Transaction::Observer).not_to receive(:register!)
+
+ load_initializer
+ end
+ end
+ end
+
+ context 'when DBMS is not available' do
+ before do
+ allow(ActiveRecord::Base).to receive(:connection).and_raise(PG::ConnectionBad)
+ end
+
+ it 'does not calls Gitlab::Database::Transaction::Observer' do
+ expect(Gitlab::Database::Transaction::Observer).not_to receive(:register!)
+
+ load_initializer
+ end
+ end
+end
diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb
index 51f21e7f46e..f0a017897c1 100644
--- a/spec/lib/feature_spec.rb
+++ b/spec/lib/feature_spec.rb
@@ -11,32 +11,6 @@ RSpec.describe Feature, stub_feature_flags: false, feature_category: :shared do
skip_feature_flags_yaml_validation
end
- describe '.feature_flags_available?' do
- it 'returns false on connection error' do
- expect(ActiveRecord::Base.connection).to receive(:active?).and_raise(PG::ConnectionBad) # rubocop:disable Database/MultipleDatabases
-
- expect(described_class.feature_flags_available?).to eq(false)
- end
-
- it 'returns false when connection is not active' do
- expect(ActiveRecord::Base.connection).to receive(:active?).and_return(false) # rubocop:disable Database/MultipleDatabases
-
- expect(described_class.feature_flags_available?).to eq(false)
- end
-
- it 'returns false when the flipper table does not exist' do
- expect(Feature::FlipperFeature).to receive(:table_exists?).and_return(false)
-
- expect(described_class.feature_flags_available?).to eq(false)
- end
-
- it 'returns false on NoDatabaseError' do
- expect(Feature::FlipperFeature).to receive(:table_exists?).and_raise(ActiveRecord::NoDatabaseError)
-
- expect(described_class.feature_flags_available?).to eq(false)
- end
- end
-
describe '.get' do
let(:feature) { double(:feature) }
let(:key) { 'my_feature' }
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
index 31258c42b5f..8e6aea96c58 100644
--- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gitlab_redis_queues, :clean_gitlab_redis_shared_state do
+RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gitlab_redis_queues, :clean_gitlab_redis_shared_state,
+ feature_category: :shared do
using RSpec::Parameterized::TableSyntax
subject(:duplicate_job) do
@@ -63,6 +64,15 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
it_behaves_like 'scheduling with deduplication class', 'None'
end
end
+
+ # This context is to be removed when FF `ci_pipeline_process_worker_dedup_until_executed` is removed
+ context 'when deduplication strategy is provided in the job options' do
+ before do
+ job['deduplicate'] = { 'strategy' => 'until_executed' }
+ end
+
+ it_behaves_like 'scheduling with deduplication class', 'UntilExecuted'
+ end
end
describe '#perform' do
@@ -480,6 +490,15 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
expect(duplicate_job.options).to eq(worker_options)
end
+
+ # This context is to be removed when FF `ci_pipeline_process_worker_dedup_until_executed` is removed
+ context 'when deduplication options are provided in the job options' do
+ it "returns the job's deduplication options" do
+ job['deduplicate'] = { 'options' => { 'if_deduplicated' => 'reschedule_once', 'ttl' => '60' } }
+
+ expect(duplicate_job.options).to eq({ if_deduplicated: :reschedule_once, ttl: 60 })
+ end
+ end
end
describe '#idempotent?' do
diff --git a/spec/models/bulk_imports/file_transfer/group_config_spec.rb b/spec/models/bulk_imports/file_transfer/group_config_spec.rb
index 8660114b719..e50f52c728f 100644
--- a/spec/models/bulk_imports/file_transfer/group_config_spec.rb
+++ b/spec/models/bulk_imports/file_transfer/group_config_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::FileTransfer::GroupConfig do
+RSpec.describe BulkImports::FileTransfer::GroupConfig, feature_category: :importers do
let_it_be(:exportable) { create(:group) }
let_it_be(:hex) { '123' }
@@ -49,4 +49,51 @@ RSpec.describe BulkImports::FileTransfer::GroupConfig do
expect(subject.relation_excluded_keys('group')).to include('owner_id')
end
end
+
+ describe '#batchable_relation?' do
+ context 'when relation is batchable' do
+ it 'returns true' do
+ expect(subject.batchable_relation?('labels')).to eq(true)
+ end
+ end
+
+ context 'when relation is not batchable' do
+ it 'returns false' do
+ expect(subject.batchable_relation?('namespace_settings')).to eq(false)
+ end
+ end
+
+ context 'when relation is not listed as portable' do
+ it 'returns false' do
+ expect(subject.batchable_relation?('foo')).to eq(false)
+ end
+ end
+ end
+
+ describe '#batchable_relations' do
+ it 'returns a list of collection associations for a group' do
+ expect(subject.batchable_relations).to include('labels', 'boards', 'milestones')
+ expect(subject.batchable_relations).not_to include('namespace_settings')
+ end
+ end
+
+ describe '#export_service_for' do
+ context 'when relation is a tree' do
+ it 'returns TreeExportService' do
+ expect(subject.export_service_for('labels')).to eq(BulkImports::TreeExportService)
+ end
+ end
+
+ context 'when relation is a file' do
+ it 'returns FileExportService' do
+ expect(subject.export_service_for('uploads')).to eq(BulkImports::FileExportService)
+ end
+ end
+
+ context 'when relation is unknown' do
+ it 'raises' do
+ expect { subject.export_service_for('foo') }.to raise_error(BulkImports::Error, 'Unsupported export relation')
+ end
+ end
+ end
end
diff --git a/spec/models/bulk_imports/file_transfer/project_config_spec.rb b/spec/models/bulk_imports/file_transfer/project_config_spec.rb
index 21fe6cfb3fa..014f624165c 100644
--- a/spec/models/bulk_imports/file_transfer/project_config_spec.rb
+++ b/spec/models/bulk_imports/file_transfer/project_config_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::FileTransfer::ProjectConfig do
+RSpec.describe BulkImports::FileTransfer::ProjectConfig, feature_category: :importers do
let_it_be(:exportable) { create(:project) }
let_it_be(:hex) { '123' }
@@ -109,4 +109,31 @@ RSpec.describe BulkImports::FileTransfer::ProjectConfig do
expect(subject.file_relations).to contain_exactly('uploads', 'lfs_objects', 'repository', 'design')
end
end
+
+ describe '#batchable_relation?' do
+ context 'when relation is batchable' do
+ it 'returns true' do
+ expect(subject.batchable_relation?('issues')).to eq(true)
+ end
+ end
+
+ context 'when relation is not batchable' do
+ it 'returns false' do
+ expect(subject.batchable_relation?('project_feature')).to eq(false)
+ end
+ end
+
+ context 'when relation is not listed as portable' do
+ it 'returns false' do
+ expect(subject.batchable_relation?('foo')).to eq(false)
+ end
+ end
+ end
+
+ describe '#batchable_relations' do
+ it 'returns a list of collection associations for a project' do
+ expect(subject.batchable_relations).to include('issues', 'merge_requests', 'milestones')
+ expect(subject.batchable_relations).not_to include('project_feature', 'ci_cd_settings')
+ end
+ end
end
diff --git a/spec/services/bulk_imports/batched_relation_export_service_spec.rb b/spec/services/bulk_imports/batched_relation_export_service_spec.rb
new file mode 100644
index 00000000000..c361dfe5052
--- /dev/null
+++ b/spec/services/bulk_imports/batched_relation_export_service_spec.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::BatchedRelationExportService, feature_category: :importers do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:portable) { create(:group) }
+
+ let(:relation) { 'labels' }
+ let(:jid) { '123' }
+
+ subject(:service) { described_class.new(user, portable, relation, jid) }
+
+ describe '#execute' do
+ context 'when there are batches to export' do
+ let_it_be(:label) { create(:group_label, group: portable) }
+
+ it 'marks export as started' do
+ service.execute
+
+ export = portable.bulk_import_exports.first
+
+ expect(export.reload.started?).to eq(true)
+ end
+
+ it 'removes existing batches' do
+ expect_next_instance_of(BulkImports::Export) do |export|
+ expect(export.batches).to receive(:destroy_all)
+ end
+
+ service.execute
+ end
+
+ it 'enqueues export jobs for each batch & caches batch record ids' do
+ expect(BulkImports::RelationBatchExportWorker).to receive(:perform_async)
+ expect(Gitlab::Cache::Import::Caching).to receive(:set_add)
+
+ service.execute
+ end
+
+ it 'enqueues FinishBatchedRelationExportWorker' do
+ expect(BulkImports::FinishBatchedRelationExportWorker).to receive(:perform_async)
+
+ service.execute
+ end
+
+ context 'when there are multiple batches' do
+ it 'creates a batch record for each batch of records' do
+ stub_const("#{described_class.name}::BATCH_SIZE", 1)
+
+ create_list(:group_label, 10, group: portable)
+
+ service.execute
+
+ export = portable.bulk_import_exports.first
+
+ expect(export.batches.count).to eq(11)
+ end
+ end
+ end
+
+ context 'when there are no batches to export' do
+ let(:relation) { 'milestones' }
+
+ it 'marks export as finished' do
+ service.execute
+
+ export = portable.bulk_import_exports.first
+
+ expect(export.finished?).to eq(true)
+ expect(export.batches.count).to eq(0)
+ end
+ end
+
+ context 'when exception occurs' do
+ it 'tracks exception and marks export as failed' do
+ allow_next_instance_of(BulkImports::Export) do |export|
+ allow(export).to receive(:update!).and_call_original
+
+ allow(export)
+ .to receive(:update!)
+ .with(status_event: 'finish', total_objects_count: 0, batched: true, batches_count: 0, jid: jid, error: nil)
+ .and_raise(StandardError, 'Error!')
+ end
+
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(StandardError, portable_id: portable.id, portable_type: portable.class.name)
+
+ service.execute
+
+ export = portable.bulk_import_exports.first
+
+ expect(export.reload.failed?).to eq(true)
+ end
+ end
+ end
+
+ describe '.cache_key' do
+ it 'returns cache key given export and batch ids' do
+ expect(described_class.cache_key(1, 1)).to eq('bulk_imports/batched_relation_export/1/1')
+ end
+ end
+end
diff --git a/spec/services/bulk_imports/export_service_spec.rb b/spec/services/bulk_imports/export_service_spec.rb
index ac7514fde5b..25a4547477c 100644
--- a/spec/services/bulk_imports/export_service_spec.rb
+++ b/spec/services/bulk_imports/export_service_spec.rb
@@ -13,17 +13,36 @@ RSpec.describe BulkImports::ExportService, feature_category: :importers do
subject { described_class.new(portable: group, user: user) }
describe '#execute' do
- it 'schedules RelationExportWorker for each top level relation' do
- expect(subject).to receive(:execute).and_return(ServiceResponse.success).and_call_original
- top_level_relations = BulkImports::FileTransfer.config_for(group).portable_relations
-
- top_level_relations.each do |relation|
- expect(BulkImports::RelationExportWorker)
- .to receive(:perform_async)
- .with(user.id, group.id, group.class.name, relation)
+ let_it_be(:top_level_relations) { BulkImports::FileTransfer.config_for(group).portable_relations }
+
+ before do
+ allow(subject).to receive(:execute).and_return(ServiceResponse.success).and_call_original
+ end
+
+ context 'when export is not batched' do
+ it 'schedules RelationExportWorker for each top level relation' do
+ top_level_relations.each do |relation|
+ expect(BulkImports::RelationExportWorker)
+ .to receive(:perform_async)
+ .with(user.id, group.id, group.class.name, relation, false)
+ end
+
+ subject.execute
end
+ end
+
+ context 'when export is batched' do
+ subject { described_class.new(portable: group, user: user, batched: true) }
- subject.execute
+ it 'schedules RelationExportWorker with a `batched: true` flag' do
+ top_level_relations.each do |relation|
+ expect(BulkImports::RelationExportWorker)
+ .to receive(:perform_async)
+ .with(user.id, group.id, group.class.name, relation, true)
+ end
+
+ subject.execute
+ end
end
context 'when exception occurs' do
@@ -38,6 +57,20 @@ RSpec.describe BulkImports::ExportService, feature_category: :importers do
service.execute
end
+
+ context 'when user is not allowed to perform export' do
+ let(:another_user) { create(:user) }
+
+ it 'does not schedule RelationExportWorker' do
+ another_user = create(:user)
+ service = described_class.new(portable: group, user: another_user)
+ response = service.execute
+
+ expect(response.status).to eq(:error)
+ expect(response.message).to eq(Gitlab::ImportExport::Error)
+ expect(response.http_status).to eq(:unprocessable_entity)
+ end
+ end
end
end
end
diff --git a/spec/services/bulk_imports/file_export_service_spec.rb b/spec/services/bulk_imports/file_export_service_spec.rb
index 3c23b86ad5c..001fccb2054 100644
--- a/spec/services/bulk_imports/file_export_service_spec.rb
+++ b/spec/services/bulk_imports/file_export_service_spec.rb
@@ -5,18 +5,20 @@ require 'spec_helper'
RSpec.describe BulkImports::FileExportService, feature_category: :importers do
let_it_be(:project) { create(:project) }
+ let(:relations) do
+ {
+ 'uploads' => BulkImports::UploadsExportService,
+ 'lfs_objects' => BulkImports::LfsObjectsExportService,
+ 'repository' => BulkImports::RepositoryBundleExportService,
+ 'design' => BulkImports::RepositoryBundleExportService
+ }
+ end
+
describe '#execute' do
it 'executes export service and archives exported data for each file relation' do
- relations = {
- 'uploads' => BulkImports::UploadsExportService,
- 'lfs_objects' => BulkImports::LfsObjectsExportService,
- 'repository' => BulkImports::RepositoryBundleExportService,
- 'design' => BulkImports::RepositoryBundleExportService
- }
-
relations.each do |relation, klass|
Dir.mktmpdir do |export_path|
- service = described_class.new(project, export_path, relation)
+ service = described_class.new(project, export_path, relation, nil)
expect_next_instance_of(klass) do |service|
expect(service).to receive(:execute)
@@ -31,18 +33,58 @@ RSpec.describe BulkImports::FileExportService, feature_category: :importers do
context 'when unsupported relation is passed' do
it 'raises an error' do
- service = described_class.new(project, nil, 'unsupported')
+ service = described_class.new(project, nil, 'unsupported', nil)
expect { service.execute }.to raise_error(BulkImports::Error, 'Unsupported relation export type')
end
end
end
+ describe '#execute_batch' do
+ it 'calls execute with provided array of record ids' do
+ relations.each do |relation, klass|
+ Dir.mktmpdir do |export_path|
+ service = described_class.new(project, export_path, relation, nil)
+
+ expect_next_instance_of(klass) do |service|
+ expect(service).to receive(:execute).with({ batch_ids: [1, 2, 3] })
+ end
+
+ service.export_batch([1, 2, 3])
+ end
+ end
+ end
+ end
+
describe '#exported_filename' do
it 'returns filename of the exported file' do
- service = described_class.new(project, nil, 'uploads')
+ service = described_class.new(project, nil, 'uploads', nil)
expect(service.exported_filename).to eq('uploads.tar')
end
end
+
+ describe '#exported_objects_count' do
+ context 'when relation is a collection' do
+ it 'returns a number of exported relations' do
+ %w[uploads lfs_objects].each do |relation|
+ service = described_class.new(project, nil, relation, nil)
+
+ allow(service).to receive_message_chain(:export_service, :exported_objects_count).and_return(10)
+
+ expect(service.exported_objects_count).to eq(10)
+ end
+ end
+ end
+
+ context 'when relation is a repository' do
+ it 'returns 1' do
+ %w[repository design].each do |relation|
+ service = described_class.new(project, nil, relation, nil)
+
+ expect(service.exported_objects_count).to eq(1)
+ end
+ end
+ end
+ end
end
diff --git a/spec/services/bulk_imports/lfs_objects_export_service_spec.rb b/spec/services/bulk_imports/lfs_objects_export_service_spec.rb
index 4f721a3a259..587c99d9897 100644
--- a/spec/services/bulk_imports/lfs_objects_export_service_spec.rb
+++ b/spec/services/bulk_imports/lfs_objects_export_service_spec.rb
@@ -53,6 +53,19 @@ RSpec.describe BulkImports::LfsObjectsExportService, feature_category: :importer
)
end
+ context 'when export is batched' do
+ it 'exports only specified lfs objects' do
+ new_lfs_object = create(:lfs_object, :with_file)
+
+ project.lfs_objects << new_lfs_object
+
+ service.execute(batch_ids: [new_lfs_object.id])
+
+ expect(File).to exist(File.join(export_path, new_lfs_object.oid))
+ expect(File).not_to exist(File.join(export_path, lfs_object.oid))
+ end
+ end
+
context 'when lfs object has file on disk missing' do
it 'does not attempt to copy non-existent file' do
FileUtils.rm(lfs_object.file.path)
@@ -79,4 +92,14 @@ RSpec.describe BulkImports::LfsObjectsExportService, feature_category: :importer
end
end
end
+
+ describe '#exported_objects_count' do
+ it 'return the number of exported lfs objects' do
+ project.lfs_objects << create(:lfs_object, :with_file)
+
+ service.execute
+
+ expect(service.exported_objects_count).to eq(2)
+ end
+ end
end
diff --git a/spec/services/bulk_imports/relation_batch_export_service_spec.rb b/spec/services/bulk_imports/relation_batch_export_service_spec.rb
new file mode 100644
index 00000000000..c3abd02aff8
--- /dev/null
+++ b/spec/services/bulk_imports/relation_batch_export_service_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::RelationBatchExportService, feature_category: :importers do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:label) { create(:label, project: project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:export) { create(:bulk_import_export, :batched, project: project) }
+ let_it_be(:batch) { create(:bulk_import_export_batch, export: export) }
+ let_it_be(:cache_key) { BulkImports::BatchedRelationExportService.cache_key(export.id, batch.id) }
+
+ subject(:service) { described_class.new(user.id, batch.id) }
+
+ before(:all) do
+ Gitlab::Cache::Import::Caching.set_add(cache_key, label.id)
+ end
+
+ after(:all) do
+ Gitlab::Cache::Import::Caching.expire(cache_key, 0)
+ end
+
+ describe '#execute' do
+ it 'exports relation batch' do
+ expect(Gitlab::Cache::Import::Caching).to receive(:values_from_set).with(cache_key).and_call_original
+
+ service.execute
+ batch.reload
+
+ expect(batch.finished?).to eq(true)
+ expect(batch.objects_count).to eq(1)
+ expect(batch.error).to be_nil
+ expect(export.upload.export_file).to be_present
+ end
+
+ it 'removes exported contents after export' do
+ double = instance_double(BulkImports::FileTransfer::ProjectConfig, export_path: 'foo')
+
+ allow(BulkImports::FileTransfer).to receive(:config_for).and_return(double)
+ allow(double).to receive(:export_service_for).and_raise(StandardError, 'Error!')
+ allow(FileUtils).to receive(:remove_entry)
+
+ expect(FileUtils).to receive(:remove_entry).with('foo')
+
+ service.execute
+ end
+
+ context 'when exception occurs' do
+ before do
+ allow(service).to receive(:gzip).and_raise(StandardError, 'Error!')
+ end
+
+ it 'marks batch as failed' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(StandardError, portable_id: project.id, portable_type: 'Project')
+
+ service.execute
+ batch.reload
+
+ expect(batch.failed?).to eq(true)
+ expect(batch.objects_count).to eq(0)
+ expect(batch.error).to eq('Error!')
+ end
+ end
+ end
+end
diff --git a/spec/services/bulk_imports/relation_export_service_spec.rb b/spec/services/bulk_imports/relation_export_service_spec.rb
index bc999b0b9b3..1c050fe4143 100644
--- a/spec/services/bulk_imports/relation_export_service_spec.rb
+++ b/spec/services/bulk_imports/relation_export_service_spec.rb
@@ -35,6 +35,10 @@ RSpec.describe BulkImports::RelationExportService, feature_category: :importers
expect(export.reload.upload.export_file).to be_present
expect(export.finished?).to eq(true)
+ expect(export.batched?).to eq(false)
+ expect(export.batches_count).to eq(0)
+ expect(export.batches.count).to eq(0)
+ expect(export.total_objects_count).to eq(0)
end
it 'removes temp export files' do
@@ -133,13 +137,23 @@ RSpec.describe BulkImports::RelationExportService, feature_category: :importers
include_examples 'tracks exception', ActiveRecord::RecordInvalid
end
+ end
+
+ context 'when export was batched' do
+ let(:relation) { 'milestones' }
+ let(:export) { create(:bulk_import_export, group: group, relation: relation, batched: true, batches_count: 2) }
- context 'when user is not allowed to perform export' do
- let(:another_user) { create(:user) }
+ it 'removes existing batches and marks export as not batched' do
+ create(:bulk_import_export_batch, batch_number: 1, export: export)
+ create(:bulk_import_export_batch, batch_number: 2, export: export)
- subject { described_class.new(another_user, group, relation, jid) }
+ expect { described_class.new(user, group, relation, jid).execute }
+ .to change { export.reload.batches.count }
+ .from(2)
+ .to(0)
- include_examples 'tracks exception', Gitlab::ImportExport::Error
+ expect(export.batched?).to eq(false)
+ expect(export.batches_count).to eq(0)
end
end
end
diff --git a/spec/services/bulk_imports/tree_export_service_spec.rb b/spec/services/bulk_imports/tree_export_service_spec.rb
index fa96641f1c1..ae78858976f 100644
--- a/spec/services/bulk_imports/tree_export_service_spec.rb
+++ b/spec/services/bulk_imports/tree_export_service_spec.rb
@@ -53,4 +53,14 @@ RSpec.describe BulkImports::TreeExportService, feature_category: :importers do
end
end
end
+
+ describe '#export_batch' do
+ it 'serializes relation with specified ids' do
+ expect_next_instance_of(Gitlab::ImportExport::Json::StreamingSerializer) do |serializer|
+ expect(serializer).to receive(:serialize_relation).with(anything, batch_ids: [1, 2, 3])
+ end
+
+ subject.export_batch([1, 2, 3])
+ end
+ end
end
diff --git a/spec/services/bulk_imports/uploads_export_service_spec.rb b/spec/services/bulk_imports/uploads_export_service_spec.rb
index 8dc67b28d12..709ade4a504 100644
--- a/spec/services/bulk_imports/uploads_export_service_spec.rb
+++ b/spec/services/bulk_imports/uploads_export_service_spec.rb
@@ -3,9 +3,8 @@
require 'spec_helper'
RSpec.describe BulkImports::UploadsExportService, feature_category: :importers do
- let_it_be(:export_path) { Dir.mktmpdir }
- let_it_be(:project) { create(:project, avatar: fixture_file_upload('spec/fixtures/rails_sample.png', 'image/png')) }
-
+ let(:export_path) { Dir.mktmpdir }
+ let(:project) { create(:project, avatar: fixture_file_upload('spec/fixtures/rails_sample.png', 'image/png')) }
let!(:upload) { create(:upload, :with_file, :issuable_upload, uploader: FileUploader, model: project) }
let(:exported_filepath) { File.join(export_path, upload.secret, upload.retrieve_uploader.filename) }
@@ -23,6 +22,16 @@ RSpec.describe BulkImports::UploadsExportService, feature_category: :importers d
expect(File).to exist(exported_filepath)
end
+ context 'when export is batched' do
+ it 'exports only specified uploads' do
+ service.execute(batch_ids: [upload.id])
+
+ expect(service.exported_objects_count).to eq(1)
+ expect(File).not_to exist(File.join(export_path, 'avatar', 'rails_sample.png'))
+ expect(File).to exist(exported_filepath)
+ end
+ end
+
context 'when upload has underlying file missing' do
context 'with an upload missing its file' do
it 'does not cause errors' do
@@ -53,6 +62,16 @@ RSpec.describe BulkImports::UploadsExportService, feature_category: :importers d
}
)
+ expect(Gitlab::ErrorTracking)
+ .to receive(:log_exception)
+ .with(
+ instance_of(exception), {
+ portable_id: project.id,
+ portable_class: 'Project',
+ upload_id: project.avatar.upload.id
+ }
+ )
+
service.execute
expect(File).not_to exist(exported_filepath)
@@ -73,4 +92,12 @@ RSpec.describe BulkImports::UploadsExportService, feature_category: :importers d
end
end
end
+
+ describe '#exported_objects_count' do
+ it 'return the number of exported uploads' do
+ service.execute
+
+ expect(service.exported_objects_count).to eq(2)
+ end
+ end
end
diff --git a/spec/services/system_notes/time_tracking_service_spec.rb b/spec/services/system_notes/time_tracking_service_spec.rb
index 5cc17f55012..71228050085 100644
--- a/spec/services/system_notes/time_tracking_service_spec.rb
+++ b/spec/services/system_notes/time_tracking_service_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe ::SystemNotes::TimeTrackingService, feature_category: :team_plann
end
it 'sets the correct note message' do
- expect(note.note).to eq('removed start date and removed due date')
+ expect(note.note).to eq("removed start date #{start_date.to_s(:long)} and removed due date #{due_date.to_s(:long)}")
end
end
@@ -52,7 +52,7 @@ RSpec.describe ::SystemNotes::TimeTrackingService, feature_category: :team_plann
let(:changed_dates) { { 'due_date' => [nil, due_date], 'start_date' => [start_date, nil] } }
it 'sets the correct note message' do
- expect(note.note).to eq("removed start date and changed due date to #{due_date.to_s(:long)}")
+ expect(note.note).to eq("removed start date #{start_date.to_s(:long)} and changed due date to #{due_date.to_s(:long)}")
end
end
end
@@ -80,7 +80,7 @@ RSpec.describe ::SystemNotes::TimeTrackingService, feature_category: :team_plann
let(:changed_dates) { { 'due_date' => [due_date, nil], 'start_date' => [nil, start_date] } }
it 'sets the correct note message' do
- expect(note.note).to eq("changed start date to #{start_date.to_s(:long)} and removed due date")
+ expect(note.note).to eq("changed start date to #{start_date.to_s(:long)} and removed due date #{due_date.to_s(:long)}")
end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index c3bddf1a6ae..334c709dcf8 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -311,10 +311,6 @@ RSpec.configure do |config|
# See https://docs.gitlab.com/ee/development/feature_flags/#selectively-disable-by-actor
stub_feature_flags(legacy_merge_request_state_check_for_merged_result_pipelines: false)
- # Disable the `vue_issues_dashboard` feature flag in specs as we migrate the issues
- # dashboard page to Vue. https://gitlab.com/gitlab-org/gitlab/-/issues/379025
- stub_feature_flags(vue_issues_dashboard: false)
-
allow(Gitlab::GitalyClient).to receive(:can_use_disk?).and_return(enable_rugged)
else
unstub_all_feature_flags
diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb
index b07f5dcf2e1..ecc749b1e45 100644
--- a/spec/support/helpers/filtered_search_helpers.rb
+++ b/spec/support/helpers/filtered_search_helpers.rb
@@ -69,12 +69,6 @@ module FilteredSearchHelpers
filtered_search.send_keys(:enter)
end
- def init_label_search
- filtered_search.set('label:=')
- # This ensures the dropdown is shown
- expect(find('#js-dropdown-label')).not_to have_css('.filter-dropdown-loading')
- end
-
def expect_filtered_search_input_empty
expect(find('.filtered-search').value).to eq('')
end
diff --git a/spec/views/projects/issues/_related_issues.html.haml_spec.rb b/spec/views/projects/issues/_related_issues.html.haml_spec.rb
new file mode 100644
index 00000000000..0dbca032c4b
--- /dev/null
+++ b/spec/views/projects/issues/_related_issues.html.haml_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'projects/issues/_related_issues.html.haml', feature_category: :team_planning do
+ let_it_be(:project) { build_stubbed(:project) }
+ let_it_be(:issue) { build_stubbed(:issue, project: project) }
+
+ context 'when current user cannot read issue link for the project' do
+ before do
+ allow(view).to receive(:can?).and_return(false)
+ end
+
+ it 'does not render the related issues root node' do
+ render
+
+ expect(rendered).not_to have_selector(".js-related-issues-root")
+ end
+ end
+
+ context 'when current user can read issue link for the project' do
+ before do
+ allow(view).to receive(:can?).and_return(true)
+
+ assign(:project, project)
+ assign(:issue, issue)
+ end
+
+ it 'adds the report abuse path as a data attribute' do
+ render
+
+ expect(rendered).to have_selector(
+ ".js-related-issues-root[data-report-abuse-path=\"#{add_category_abuse_reports_path}\"]"
+ )
+ end
+ end
+end
diff --git a/spec/workers/bulk_imports/finish_batched_relation_export_worker_spec.rb b/spec/workers/bulk_imports/finish_batched_relation_export_worker_spec.rb
new file mode 100644
index 00000000000..6fbcb267c0a
--- /dev/null
+++ b/spec/workers/bulk_imports/finish_batched_relation_export_worker_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::FinishBatchedRelationExportWorker, feature_category: :importers do
+ let(:export) { create(:bulk_import_export, :started) }
+ let(:batch) { create(:bulk_import_export_batch, :finished, export: export) }
+ let(:export_id) { export.id }
+ let(:job_args) { [export_id] }
+
+ describe '#perform' do
+ it_behaves_like 'an idempotent worker' do
+ it 'marks export as finished and expires batches cache' do
+ cache_key = BulkImports::BatchedRelationExportService.cache_key(export.id, batch.id)
+
+ expect(Gitlab::Cache::Import::Caching).to receive(:expire).with(cache_key, 0)
+
+ perform_multiple(job_args)
+
+ expect(export.reload.finished?).to eq(true)
+ end
+
+ context 'when export is finished' do
+ let(:export) { create(:bulk_import_export, :finished) }
+
+ it 'returns without updating export' do
+ perform_multiple(job_args)
+
+ expect(export.reload.finished?).to eq(true)
+ end
+ end
+
+ context 'when export is failed' do
+ let(:export) { create(:bulk_import_export, :failed) }
+
+ it 'returns without updating export' do
+ perform_multiple(job_args)
+
+ expect(export.reload.failed?).to eq(true)
+ end
+ end
+
+ context 'when export is in progress' do
+ it 'reenqueues itself' do
+ create(:bulk_import_export_batch, :started, export: export)
+
+ expect(described_class).to receive(:perform_in).twice
+
+ perform_multiple(job_args)
+
+ expect(export.reload.started?).to eq(true)
+ end
+ end
+
+ context 'when export timed out' do
+ it 'marks export as failed' do
+ expect(export.reload.failed?).to eq(false)
+ expect(batch.reload.failed?).to eq(false)
+
+ export.update!(updated_at: 1.day.ago)
+
+ perform_multiple(job_args)
+
+ expect(export.reload.failed?).to eq(true)
+ expect(batch.reload.failed?).to eq(true)
+ end
+ end
+
+ context 'when export is missing' do
+ let(:export_id) { nil }
+
+ it 'returns' do
+ expect(described_class).not_to receive(:perform_in)
+
+ perform_multiple(job_args)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/workers/bulk_imports/relation_batch_export_worker_spec.rb b/spec/workers/bulk_imports/relation_batch_export_worker_spec.rb
new file mode 100644
index 00000000000..4a2c8d48742
--- /dev/null
+++ b/spec/workers/bulk_imports/relation_batch_export_worker_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::RelationBatchExportWorker, feature_category: :importers do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:batch) { create(:bulk_import_export_batch) }
+
+ let(:job_args) { [user.id, batch.id] }
+
+ describe '#perform' do
+ include_examples 'an idempotent worker' do
+ it 'executes RelationBatchExportService' do
+ service = instance_double(BulkImports::RelationBatchExportService)
+
+ expect(BulkImports::RelationBatchExportService)
+ .to receive(:new)
+ .with(user.id, batch.id)
+ .twice.and_return(service)
+ expect(service).to receive(:execute).twice
+
+ perform_multiple(job_args)
+ end
+ end
+ end
+end
diff --git a/spec/workers/bulk_imports/relation_export_worker_spec.rb b/spec/workers/bulk_imports/relation_export_worker_spec.rb
index c2f7831896b..38ef4df263e 100644
--- a/spec/workers/bulk_imports/relation_export_worker_spec.rb
+++ b/spec/workers/bulk_imports/relation_export_worker_spec.rb
@@ -4,17 +4,18 @@ require 'spec_helper'
RSpec.describe BulkImports::RelationExportWorker, feature_category: :importers do
let_it_be(:jid) { 'jid' }
- let_it_be(:relation) { 'labels' }
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
- let(:job_args) { [user.id, group.id, group.class.name, relation] }
+ let(:batched) { false }
+ let(:relation) { 'labels' }
+ let(:job_args) { [user.id, group.id, group.class.name, relation, batched] }
describe '#perform' do
include_examples 'an idempotent worker' do
context 'when export record does not exist' do
let(:another_group) { create(:group) }
- let(:job_args) { [user.id, another_group.id, another_group.class.name, relation] }
+ let(:job_args) { [user.id, another_group.id, another_group.class.name, relation, batched] }
it 'creates export record' do
another_group.add_owner(user)
@@ -26,21 +27,37 @@ RSpec.describe BulkImports::RelationExportWorker, feature_category: :importers d
end
end
- it 'executes RelationExportService' do
- group.add_owner(user)
+ shared_examples 'export service' do |export_service|
+ it 'executes export service' do
+ group.add_owner(user)
- service = instance_double(BulkImports::RelationExportService)
+ service = instance_double(export_service)
- expect(BulkImports::RelationExportService)
- .to receive(:new)
- .with(user, group, relation, anything)
- .twice
- .and_return(service)
- expect(service)
- .to receive(:execute)
- .twice
+ expect(export_service)
+ .to receive(:new)
+ .with(user, group, relation, anything)
+ .twice
+ .and_return(service)
+ expect(service).to receive(:execute).twice
- perform_multiple(job_args)
+ perform_multiple(job_args)
+ end
+ end
+
+ context 'when export is batched' do
+ let(:batched) { true }
+
+ include_examples 'export service', BulkImports::BatchedRelationExportService
+
+ context 'when relation is not batchable' do
+ let(:relation) { 'namespace_settings' }
+
+ include_examples 'export service', BulkImports::RelationExportService
+ end
+ end
+
+ context 'when export is not batched' do
+ include_examples 'export service', BulkImports::RelationExportService
end
end
end
diff --git a/spec/workers/pipeline_process_worker_spec.rb b/spec/workers/pipeline_process_worker_spec.rb
index 6c6851c51ce..1c76cdca347 100644
--- a/spec/workers/pipeline_process_worker_spec.rb
+++ b/spec/workers/pipeline_process_worker_spec.rb
@@ -5,6 +5,48 @@ require 'spec_helper'
RSpec.describe PipelineProcessWorker, feature_category: :continuous_integration do
let_it_be(:pipeline) { create(:ci_pipeline) }
+ # The two examples below are to be added when FF `ci_pipeline_process_worker_dedup_until_executed` is removed
+ # it 'has the `until_executed` deduplicate strategy' do
+ # expect(described_class.get_deduplicate_strategy).to eq(:until_executed)
+ # end
+
+ # it 'has the option to reschedule once if deduplicated and a TTL of 1 minute' do
+ # expect(described_class.get_deduplication_options).to include({ if_deduplicated: :reschedule_once, ttl: 1.minute })
+ # end
+
+ # This context is to be removed when FF `ci_pipeline_process_worker_dedup_until_executed` is removed
+ describe '#perform_async', :sidekiq_inline do
+ around do |example|
+ Sidekiq::Testing.fake! { example.run }
+ end
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ subject { described_class.perform_async(pipeline.id) }
+
+ it 'sets the deduplication settings in the job options' do
+ subject
+
+ job = described_class.jobs.last
+ expect(job['deduplicate']).to eq({ 'strategy' => 'until_executed',
+ 'options' => { 'if_deduplicated' => 'reschedule_once', 'ttl' => '60' } })
+ end
+
+ context 'when FF `ci_pipeline_process_worker_dedup_until_executed` is disabled' do
+ before do
+ stub_feature_flags(ci_pipeline_process_worker_dedup_until_executed: false)
+ end
+
+ it 'does not set the deduplication settings in the job options' do
+ subject
+
+ job = described_class.jobs.last
+ expect(job['deduplicate']).to be_nil
+ end
+ end
+ end
+
include_examples 'an idempotent worker' do
let(:pipeline) { create(:ci_pipeline, :created) }
let(:job_args) { [pipeline.id] }