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>2022-02-10 21:18:16 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-02-10 21:18:16 +0300
commite0277d5393d958865fdec470176ac5874edded06 (patch)
treef867094e393909ef822e354b1c72997ec5102f6f /spec
parent74d9798736a89f07e047698e5e32964829bf8859 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb14
-rw-r--r--spec/frontend/pages/dashboard/todos/index/todos_spec.js5
-rw-r--r--spec/frontend/projects/project_find_file_spec.js5
-rw-r--r--spec/frontend/prometheus_metrics/prometheus_metrics_spec.js30
-rw-r--r--spec/frontend/sidebar/components/severity/sidebar_severity_spec.js8
-rw-r--r--spec/frontend/sidebar/sidebar_move_issue_spec.js54
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js22
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js39
-rw-r--r--spec/lib/gitlab/background_migration/populate_topics_non_private_projects_count_spec.rb50
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb2
-rw-r--r--spec/models/merge_request_spec.rb36
-rw-r--r--spec/models/project_spec.rb61
-rw-r--r--spec/requests/api/merge_requests_spec.rb12
-rw-r--r--spec/services/merge_requests/bulk_remove_attention_requested_service_spec.rb4
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb97
-rw-r--r--spec/support/shared_examples/quick_actions/merge_request/rebase_quick_action_shared_examples.rb10
-rw-r--r--spec/workers/container_registry/migration/observer_worker_spec.rb57
-rw-r--r--spec/workers/projects/git_garbage_collect_worker_spec.rb15
18 files changed, 434 insertions, 87 deletions
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 88e60a5352a..2390687c3ea 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -2078,6 +2078,20 @@ RSpec.describe Projects::MergeRequestsController do
end
end
+ context 'when source branch is protected from force push' do
+ before do
+ create(:protected_branch, project: project, name: merge_request.source_branch, allow_force_push: false)
+ end
+
+ it 'returns 404' do
+ expect_rebase_worker_for(user).never
+
+ post_rebase
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
context 'with a forked project' do
let(:forked_project) { fork_project(project, fork_owner, repository: true) }
let(:fork_owner) { create(:user) }
diff --git a/spec/frontend/pages/dashboard/todos/index/todos_spec.js b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
index 6a7ce80ec5a..ef295e7d1ba 100644
--- a/spec/frontend/pages/dashboard/todos/index/todos_spec.js
+++ b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
+import waitForPromises from 'helpers/wait_for_promises';
import '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
import { addDelimiter } from '~/lib/utils/text_utility';
@@ -71,7 +72,7 @@ describe('Todos', () => {
describe('on done todo click', () => {
let onToggleSpy;
- beforeEach((done) => {
+ beforeEach(() => {
const el = document.querySelector('.js-done-todo');
const path = el.dataset.href;
@@ -86,7 +87,7 @@ describe('Todos', () => {
el.click();
// Wait for axios and HTML to udpate
- setImmediate(done);
+ return waitForPromises();
});
it('dispatches todo:toggle', () => {
diff --git a/spec/frontend/projects/project_find_file_spec.js b/spec/frontend/projects/project_find_file_spec.js
index 9c1000039b1..eec54dd04bc 100644
--- a/spec/frontend/projects/project_find_file_spec.js
+++ b/spec/frontend/projects/project_find_file_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { TEST_HOST } from 'helpers/test_constants';
+import waitForPromises from 'helpers/wait_for_promises';
import { sanitize } from '~/lib/dompurify';
import axios from '~/lib/utils/axios_utils';
import ProjectFindFile from '~/projects/project_find_file';
@@ -53,7 +54,7 @@ describe('ProjectFindFile', () => {
{ path: 'folde?rC/fil#F.txt', escaped: 'folde%3FrC/fil%23F.txt' },
];
- beforeEach((done) => {
+ beforeEach(() => {
// Create a mock adapter for stubbing axios API requests
mock = new MockAdapter(axios);
@@ -64,7 +65,7 @@ describe('ProjectFindFile', () => {
);
getProjectFindFileInstance(); // This triggers a load / axios call + subsequent render in the constructor
- setImmediate(done);
+ return waitForPromises();
});
afterEach(() => {
diff --git a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
index a703dc0a66f..ee74e28ba23 100644
--- a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
@@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import PANEL_STATE from '~/prometheus_metrics/constants';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
@@ -132,7 +133,7 @@ describe('PrometheusMetrics', () => {
mock.restore();
});
- it('should show loader animation while response is being loaded and hide it when request is complete', (done) => {
+ it('should show loader animation while response is being loaded and hide it when request is complete', async () => {
mockSuccess();
prometheusMetrics.loadActiveMetrics();
@@ -140,34 +141,31 @@ describe('PrometheusMetrics', () => {
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy();
expect(axios.get).toHaveBeenCalledWith(prometheusMetrics.activeMetricsEndpoint);
- setImmediate(() => {
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
- done();
- });
+ await waitForPromises();
+
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
});
- it('should show empty state if response failed to load', (done) => {
+ it('should show empty state if response failed to load', async () => {
mockError();
prometheusMetrics.loadActiveMetrics();
- setImmediate(() => {
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
- expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy();
- done();
- });
+ await waitForPromises();
+
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
+ expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy();
});
- it('should populate metrics list once response is loaded', (done) => {
+ it('should populate metrics list once response is loaded', async () => {
jest.spyOn(prometheusMetrics, 'populateActiveMetrics').mockImplementation();
mockSuccess();
prometheusMetrics.loadActiveMetrics();
- setImmediate(() => {
- expect(prometheusMetrics.populateActiveMetrics).toHaveBeenCalledWith(metrics);
- done();
- });
+ await waitForPromises();
+
+ expect(prometheusMetrics.populateActiveMetrics).toHaveBeenCalledWith(metrics);
});
});
});
diff --git a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
index 6116bc68927..5d80a221d8e 100644
--- a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
+++ b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
@@ -97,14 +97,14 @@ describe('SidebarSeverity', () => {
});
});
- it('shows error alert when severity update fails ', () => {
+ it('shows error alert when severity update fails ', async () => {
const errorMsg = 'Something went wrong';
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValueOnce(errorMsg);
findCriticalSeverityDropdownItem().vm.$emit('click');
- setImmediate(() => {
- expect(createFlash).toHaveBeenCalled();
- });
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalled();
});
it('shows loading icon while updating', async () => {
diff --git a/spec/frontend/sidebar/sidebar_move_issue_spec.js b/spec/frontend/sidebar/sidebar_move_issue_spec.js
index d9972ae75c3..7bb7b18adf8 100644
--- a/spec/frontend/sidebar/sidebar_move_issue_spec.js
+++ b/spec/frontend/sidebar/sidebar_move_issue_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
+import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue';
@@ -77,15 +78,14 @@ describe('SidebarMoveIssue', () => {
expect(test.sidebarMoveIssue.$dropdownToggle.data('deprecatedJQueryDropdown')).toBeTruthy();
});
- it('escapes html from project name', (done) => {
+ it('escapes html from project name', async () => {
test.$toggleButton.dropdown('toggle');
- setImmediate(() => {
- expect(test.$content.find('.js-move-issue-dropdown-item')[1].innerHTML.trim()).toEqual(
- '&lt;img src=x onerror=alert(document.domain)&gt; foo / bar',
- );
- done();
- });
+ await waitForPromises();
+
+ expect(test.$content.find('.js-move-issue-dropdown-item')[1].innerHTML.trim()).toEqual(
+ '&lt;img src=x onerror=alert(document.domain)&gt; foo / bar',
+ );
});
});
@@ -101,20 +101,20 @@ describe('SidebarMoveIssue', () => {
expect(test.$confirmButton.hasClass('is-loading')).toBe(true);
});
- it('should remove loading state from confirm button on failure', (done) => {
+ it('should remove loading state from confirm button on failure', async () => {
jest.spyOn(test.mediator, 'moveIssue').mockReturnValue(Promise.reject());
test.mediator.setMoveToProjectId(7);
test.sidebarMoveIssue.onConfirmClicked();
expect(test.mediator.moveIssue).toHaveBeenCalled();
+
// Wait for the move issue request to fail
- setImmediate(() => {
- expect(createFlash).toHaveBeenCalled();
- expect(test.$confirmButton.prop('disabled')).toBeFalsy();
- expect(test.$confirmButton.hasClass('is-loading')).toBe(false);
- done();
- });
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalled();
+ expect(test.$confirmButton.prop('disabled')).toBeFalsy();
+ expect(test.$confirmButton.hasClass('is-loading')).toBe(false);
});
it('should not move the issue with id=0', () => {
@@ -127,35 +127,33 @@ describe('SidebarMoveIssue', () => {
});
});
- it('should set moveToProjectId on dropdown item "No project" click', (done) => {
+ it('should set moveToProjectId on dropdown item "No project" click', async () => {
jest.spyOn(test.mediator, 'setMoveToProjectId').mockImplementation(() => {});
// Open the dropdown
test.$toggleButton.dropdown('toggle');
// Wait for the autocomplete request to finish
- setImmediate(() => {
- test.$content.find('.js-move-issue-dropdown-item').eq(0).trigger('click');
+ await waitForPromises();
- expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(0);
- expect(test.$confirmButton.prop('disabled')).toBeTruthy();
- done();
- });
+ test.$content.find('.js-move-issue-dropdown-item').eq(0).trigger('click');
+
+ expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(0);
+ expect(test.$confirmButton.prop('disabled')).toBeTruthy();
});
- it('should set moveToProjectId on dropdown item click', (done) => {
+ it('should set moveToProjectId on dropdown item click', async () => {
jest.spyOn(test.mediator, 'setMoveToProjectId').mockImplementation(() => {});
// Open the dropdown
test.$toggleButton.dropdown('toggle');
// Wait for the autocomplete request to finish
- setImmediate(() => {
- test.$content.find('.js-move-issue-dropdown-item').eq(1).trigger('click');
+ await waitForPromises();
- expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(20);
- expect(test.$confirmButton.attr('disabled')).toBe(undefined);
- done();
- });
+ test.$content.find('.js-move-issue-dropdown-item').eq(1).trigger('click');
+
+ expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(20);
+ expect(test.$confirmButton.attr('disabled')).toBe(undefined);
});
});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js
index 45e9335abbe..c0a30a5093d 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js
@@ -1,6 +1,7 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
+import waitForPromises from 'helpers/wait_for_promises';
import MemoryUsage from '~/vue_merge_request_widget/components/deployment/memory_usage.vue';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
@@ -152,23 +153,18 @@ describe('MemoryUsage', () => {
});
describe('loadMetrics', () => {
- const returnServicePromise = () =>
- new Promise((resolve) => {
- resolve({
- data: metricsMockData,
- });
+ it('should load metrics data using MRWidgetService', async () => {
+ jest.spyOn(MRWidgetService, 'fetchMetrics').mockResolvedValue({
+ data: metricsMockData,
});
-
- it('should load metrics data using MRWidgetService', (done) => {
- jest.spyOn(MRWidgetService, 'fetchMetrics').mockReturnValue(returnServicePromise(true));
jest.spyOn(vm, 'computeGraphData').mockImplementation(() => {});
vm.loadMetrics();
- setImmediate(() => {
- expect(MRWidgetService.fetchMetrics).toHaveBeenCalledWith(url);
- expect(vm.computeGraphData).toHaveBeenCalledWith(metrics, deployment_time);
- done();
- });
+
+ await waitForPromises();
+
+ expect(MRWidgetService.fetchMetrics).toHaveBeenCalledWith(url);
+ expect(vm.computeGraphData).toHaveBeenCalledWith(metrics, deployment_time);
});
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
index 52a56af454f..7387ed2d5e9 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { trimText } from 'helpers/text_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import autoMergeEnabledComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue';
import { MWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants';
import eventHub from '~/vue_merge_request_widget/event_hub';
@@ -185,7 +186,7 @@ describe('MRWidgetAutoMergeEnabled', () => {
describe('methods', () => {
describe('cancelAutomaticMerge', () => {
- it('should set flag and call service then tell main component to update the widget with data', (done) => {
+ it('should set flag and call service then tell main component to update the widget with data', async () => {
factory({
...defaultMrProps(),
});
@@ -201,20 +202,20 @@ describe('MRWidgetAutoMergeEnabled', () => {
);
wrapper.vm.cancelAutomaticMerge();
- setImmediate(() => {
- expect(wrapper.vm.isCancellingAutoMerge).toBeTruthy();
- if (mergeRequestWidgetGraphql) {
- expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
- } else {
- expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
- }
- done();
- });
+
+ await waitForPromises();
+
+ expect(wrapper.vm.isCancellingAutoMerge).toBeTruthy();
+ if (mergeRequestWidgetGraphql) {
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
+ } else {
+ expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
+ }
});
});
describe('removeSourceBranch', () => {
- it('should set flag and call service then request main component to update the widget', (done) => {
+ it('should set flag and call service then request main component to update the widget', async () => {
factory({
...defaultMrProps(),
});
@@ -227,14 +228,14 @@ describe('MRWidgetAutoMergeEnabled', () => {
);
wrapper.vm.removeSourceBranch();
- setImmediate(() => {
- expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
- expect(wrapper.vm.service.merge).toHaveBeenCalledWith({
- sha,
- auto_merge_strategy: MWPS_MERGE_STRATEGY,
- should_remove_source_branch: true,
- });
- done();
+
+ await waitForPromises();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
+ expect(wrapper.vm.service.merge).toHaveBeenCalledWith({
+ sha,
+ auto_merge_strategy: MWPS_MERGE_STRATEGY,
+ should_remove_source_branch: true,
});
});
});
diff --git a/spec/lib/gitlab/background_migration/populate_topics_non_private_projects_count_spec.rb b/spec/lib/gitlab/background_migration/populate_topics_non_private_projects_count_spec.rb
new file mode 100644
index 00000000000..e72e3392210
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/populate_topics_non_private_projects_count_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::PopulateTopicsNonPrivateProjectsCount, schema: 20220125122640 do
+ it 'correctly populates the non private projects counters' do
+ namespaces = table(:namespaces)
+ projects = table(:projects)
+ topics = table(:topics)
+ project_topics = table(:project_topics)
+
+ group = namespaces.create!(name: 'group', path: 'group')
+ project_public = projects.create!(namespace_id: group.id, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ project_internal = projects.create!(namespace_id: group.id, visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ project_private = projects.create!(namespace_id: group.id, visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ topic_1 = topics.create!(name: 'Topic1')
+ topic_2 = topics.create!(name: 'Topic2')
+ topic_3 = topics.create!(name: 'Topic3')
+ topic_4 = topics.create!(name: 'Topic4')
+ topic_5 = topics.create!(name: 'Topic5')
+ topic_6 = topics.create!(name: 'Topic6')
+ topic_7 = topics.create!(name: 'Topic7')
+ topic_8 = topics.create!(name: 'Topic8')
+
+ project_topics.create!(topic_id: topic_1.id, project_id: project_public.id)
+ project_topics.create!(topic_id: topic_2.id, project_id: project_internal.id)
+ project_topics.create!(topic_id: topic_3.id, project_id: project_private.id)
+ project_topics.create!(topic_id: topic_4.id, project_id: project_public.id)
+ project_topics.create!(topic_id: topic_4.id, project_id: project_internal.id)
+ project_topics.create!(topic_id: topic_5.id, project_id: project_public.id)
+ project_topics.create!(topic_id: topic_5.id, project_id: project_private.id)
+ project_topics.create!(topic_id: topic_6.id, project_id: project_internal.id)
+ project_topics.create!(topic_id: topic_6.id, project_id: project_private.id)
+ project_topics.create!(topic_id: topic_7.id, project_id: project_public.id)
+ project_topics.create!(topic_id: topic_7.id, project_id: project_internal.id)
+ project_topics.create!(topic_id: topic_7.id, project_id: project_private.id)
+ project_topics.create!(topic_id: topic_8.id, project_id: project_public.id)
+
+ subject.perform(topic_1.id, topic_7.id)
+
+ expect(topic_1.reload.non_private_projects_count).to eq(1)
+ expect(topic_2.reload.non_private_projects_count).to eq(1)
+ expect(topic_3.reload.non_private_projects_count).to eq(0)
+ expect(topic_4.reload.non_private_projects_count).to eq(2)
+ expect(topic_5.reload.non_private_projects_count).to eq(1)
+ expect(topic_6.reload.non_private_projects_count).to eq(1)
+ expect(topic_7.reload.non_private_projects_count).to eq(2)
+ expect(topic_8.reload.non_private_projects_count).to eq(0)
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
index 067b8b09516..a70ff0bd82d 100644
--- a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
@@ -164,7 +164,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsImporter do
expect(project.repository)
.to receive(:fetch_remote)
- .with(url, forced: false, refmap: Gitlab::GithubImport.refmap)
+ .with(url, forced: true, refmap: Gitlab::GithubImport.refmap)
freeze_time do
importer.update_repository
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index e1f3b36a2ba..e9a38755692 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1538,6 +1538,42 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
end
+ describe '#permits_force_push?' do
+ let_it_be(:merge_request) { build_stubbed(:merge_request) }
+
+ subject { merge_request.permits_force_push? }
+
+ context 'when source branch is not protected' do
+ before do
+ allow(ProtectedBranch).to receive(:protected?).and_return(false)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when source branch is protected' do
+ before do
+ allow(ProtectedBranch).to receive(:protected?).and_return(true)
+ end
+
+ context 'when force push is not allowed' do
+ before do
+ allow(ProtectedBranch).to receive(:allow_force_push?) { false }
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when force push is allowed' do
+ before do
+ allow(ProtectedBranch).to receive(:allow_force_push?) { true }
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+
describe '#can_remove_source_branch?' do
let_it_be(:user) { create(:user) }
let_it_be(:merge_request, reload: true) { create(:merge_request, :simple) }
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 35d0566d9b4..f343e164d9b 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -7418,6 +7418,67 @@ RSpec.describe Project, factory_default: :keep do
expect(project.reload.topics.map(&:name)).to eq(%w[topic1 topic2 topic3])
end
end
+
+ context 'public topics counter' do
+ let_it_be(:topic_1) { create(:topic, name: 't1') }
+ let_it_be(:topic_2) { create(:topic, name: 't2') }
+ let_it_be(:topic_3) { create(:topic, name: 't3') }
+
+ let(:private) { Gitlab::VisibilityLevel::PRIVATE }
+ let(:internal) { Gitlab::VisibilityLevel::INTERNAL }
+ let(:public) { Gitlab::VisibilityLevel::PUBLIC }
+
+ subject do
+ project_updates = {
+ visibility_level: new_visibility,
+ topic_list: new_topic_list
+ }.compact
+
+ project.update!(project_updates)
+ end
+
+ using RSpec::Parameterized::TableSyntax
+
+ # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
+ where(:initial_visibility, :new_visibility, :new_topic_list, :expected_count_changes) do
+ ref(:private) | nil | 't2, t3' | [0, 0, 0]
+ ref(:internal) | nil | 't2, t3' | [-1, 0, 1]
+ ref(:public) | nil | 't2, t3' | [-1, 0, 1]
+ ref(:private) | ref(:public) | nil | [1, 1, 0]
+ ref(:private) | ref(:internal) | nil | [1, 1, 0]
+ ref(:private) | ref(:private) | nil | [0, 0, 0]
+ ref(:internal) | ref(:public) | nil | [0, 0, 0]
+ ref(:internal) | ref(:internal) | nil | [0, 0, 0]
+ ref(:internal) | ref(:private) | nil | [-1, -1, 0]
+ ref(:public) | ref(:public) | nil | [0, 0, 0]
+ ref(:public) | ref(:internal) | nil | [0, 0, 0]
+ ref(:public) | ref(:private) | nil | [-1, -1, 0]
+ ref(:private) | ref(:public) | 't2, t3' | [0, 1, 1]
+ ref(:private) | ref(:internal) | 't2, t3' | [0, 1, 1]
+ ref(:private) | ref(:private) | 't2, t3' | [0, 0, 0]
+ ref(:internal) | ref(:public) | 't2, t3' | [-1, 0, 1]
+ ref(:internal) | ref(:internal) | 't2, t3' | [-1, 0, 1]
+ ref(:internal) | ref(:private) | 't2, t3' | [-1, -1, 0]
+ ref(:public) | ref(:public) | 't2, t3' | [-1, 0, 1]
+ ref(:public) | ref(:internal) | 't2, t3' | [-1, 0, 1]
+ ref(:public) | ref(:private) | 't2, t3' | [-1, -1, 0]
+ end
+ # rubocop:enable Lint/BinaryOperatorWithIdenticalOperands
+
+ with_them do
+ it 'increments or decrements counters of topics' do
+ project.reload.update!(
+ visibility_level: initial_visibility,
+ topic_list: [topic_1.name, topic_2.name]
+ )
+
+ expect { subject }
+ .to change { topic_1.reload.non_private_projects_count }.by(expected_count_changes[0])
+ .and change { topic_2.reload.non_private_projects_count }.by(expected_count_changes[1])
+ .and change { topic_3.reload.non_private_projects_count }.by(expected_count_changes[2])
+ end
+ end
+ end
end
shared_examples 'all_runners' do
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 2a266ff5d19..dcf5c0151eb 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -3344,6 +3344,18 @@ RSpec.describe API::MergeRequests do
end
end
+ context 'when merge request branch does not allow force push' do
+ before do
+ create(:protected_branch, project: project, name: merge_request.source_branch, allow_force_push: false)
+ end
+
+ it 'returns 403' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/rebase", user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
it 'returns 403 if the user cannot push to the branch' do
guest = create(:user)
project.add_guest(guest)
diff --git a/spec/services/merge_requests/bulk_remove_attention_requested_service_spec.rb b/spec/services/merge_requests/bulk_remove_attention_requested_service_spec.rb
index fe4ce0dab5e..ae8846974ce 100644
--- a/spec/services/merge_requests/bulk_remove_attention_requested_service_spec.rb
+++ b/spec/services/merge_requests/bulk_remove_attention_requested_service_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe MergeRequests::BulkRemoveAttentionRequestedService do
let(:reviewer) { merge_request.find_reviewer(user) }
let(:assignee) { merge_request.find_assignee(assignee_user) }
let(:project) { merge_request.project }
- let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request) }
+ let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, users: [user, assignee_user]) }
let(:result) { service.execute }
before do
@@ -20,7 +20,7 @@ RSpec.describe MergeRequests::BulkRemoveAttentionRequestedService do
describe '#execute' do
context 'invalid permissions' do
- let(:service) { described_class.new(project: project, current_user: create(:user), merge_request: merge_request) }
+ let(:service) { described_class.new(project: project, current_user: create(:user), merge_request: merge_request, users: [user]) }
it 'returns an error' do
expect(result[:status]).to eq :error
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index e3a6a60393d..bd0896bf0a8 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -701,6 +701,27 @@ RSpec.describe QuickActions::InterpretService do
end
end
+ shared_examples 'attention command' do
+ it 'updates reviewers attention status' do
+ _, _, message = service.execute(content, issuable)
+
+ expect(message).to eq("Requested attention from #{developer.to_reference}.")
+
+ reviewer.reload
+
+ expect(reviewer).to be_attention_requested
+ end
+ end
+
+ shared_examples 'remove attention command' do
+ it 'updates reviewers attention status' do
+ _, _, message = service.execute(content, issuable)
+
+ expect(message).to eq("Removed attention from #{developer.to_reference}.")
+ expect(reviewer).not_to be_attention_requested
+ end
+ end
+
it_behaves_like 'reopen command' do
let(:content) { '/reopen' }
let(:issuable) { issue }
@@ -2283,6 +2304,82 @@ RSpec.describe QuickActions::InterpretService do
expect(message).to eq('One or more contacts were successfully removed.')
end
end
+
+ describe 'attention command' do
+ let(:issuable) { create(:merge_request, reviewers: [developer], source_project: project) }
+ let(:reviewer) { issuable.merge_request_reviewers.find_by(user_id: developer.id) }
+ let(:content) { "/attention @#{developer.username}" }
+
+ context 'with one user' do
+ before do
+ reviewer.update!(state: :reviewed)
+ end
+
+ it_behaves_like 'attention command'
+ end
+
+ context 'with no user' do
+ let(:content) { "/attention" }
+
+ it_behaves_like 'failed command', 'Failed to request attention because no user was found.'
+ end
+
+ context 'with incorrect permissions' do
+ let(:service) { described_class.new(project, create(:user)) }
+
+ it_behaves_like 'failed command', 'Could not apply attention command.'
+ end
+
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(mr_attention_requests: false)
+ end
+
+ it_behaves_like 'failed command', 'Could not apply attention command.'
+ end
+
+ context 'with an issue instead of a merge request' do
+ let(:issuable) { issue }
+
+ it_behaves_like 'failed command', 'Could not apply attention command.'
+ end
+ end
+
+ describe 'remove attention command' do
+ let(:issuable) { create(:merge_request, reviewers: [developer], source_project: project) }
+ let(:reviewer) { issuable.merge_request_reviewers.find_by(user_id: developer.id) }
+ let(:content) { "/remove_attention @#{developer.username}" }
+
+ context 'with one user' do
+ it_behaves_like 'remove attention command'
+ end
+
+ context 'with no user' do
+ let(:content) { "/remove_attention" }
+
+ it_behaves_like 'failed command', 'Failed to remove attention because no user was found.'
+ end
+
+ context 'with incorrect permissions' do
+ let(:service) { described_class.new(project, create(:user)) }
+
+ it_behaves_like 'failed command', 'Could not apply remove_attention command.'
+ end
+
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(mr_attention_requests: false)
+ end
+
+ it_behaves_like 'failed command', 'Could not apply remove_attention command.'
+ end
+
+ context 'with an issue instead of a merge request' do
+ let(:issuable) { issue }
+
+ it_behaves_like 'failed command', 'Could not apply remove_attention command.'
+ end
+ end
end
describe '#explain' do
diff --git a/spec/support/shared_examples/quick_actions/merge_request/rebase_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/rebase_quick_action_shared_examples.rb
index 28decb4011d..2258bdd2c79 100644
--- a/spec/support/shared_examples/quick_actions/merge_request/rebase_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/merge_request/rebase_quick_action_shared_examples.rb
@@ -73,6 +73,16 @@ RSpec.shared_examples 'rebase quick action' do
expect(page).to have_content 'This merge request cannot be rebased while there are conflicts.'
end
end
+
+ context 'when the merge request branch is protected from force push' do
+ let!(:protected_branch) { create(:protected_branch, project: project, name: merge_request.source_branch, allow_force_push: false) }
+
+ it 'does not rebase the MR' do
+ add_note("/rebase")
+
+ expect(page).to have_content 'This merge request branch is protected from force push.'
+ end
+ end
end
context 'when the current user cannot rebase the MR' do
diff --git a/spec/workers/container_registry/migration/observer_worker_spec.rb b/spec/workers/container_registry/migration/observer_worker_spec.rb
new file mode 100644
index 00000000000..fec6640d7ec
--- /dev/null
+++ b/spec/workers/container_registry/migration/observer_worker_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ContainerRegistry::Migration::ObserverWorker, :aggregate_failures do
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ subject { worker.perform }
+
+ context 'when the migration feature flag is disabled' do
+ before do
+ stub_feature_flags(container_registry_migration_phase2_enabled: false)
+ end
+
+ it 'does nothing' do
+ expect(worker).not_to receive(:log_extra_metadata_on_done)
+
+ subject
+ end
+ end
+
+ context 'when the migration is enabled' do
+ before do
+ create_list(:container_repository, 3)
+ create(:container_repository, :pre_importing)
+ create(:container_repository, :pre_import_done)
+ create_list(:container_repository, 2, :importing)
+ create(:container_repository, :import_aborted)
+ # batch_count is not allowed within a transaction but
+ # all rspec tests run inside of a transaction.
+ # This mocks the false positive.
+ allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false) # rubocop:disable Database/MultipleDatabases
+ end
+
+ it 'logs all the counts' do
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:default_count, 3)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:pre_importing_count, 1)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:pre_import_done_count, 1)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:importing_count, 2)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:import_done_count, 0)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:import_aborted_count, 1)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:import_skipped_count, 0)
+
+ subject
+ end
+
+ context 'with load balancing enabled', :db_load_balancing do
+ it 'uses the replica' do
+ expect(Gitlab::Database::LoadBalancing::Session.current).to receive(:use_replicas_for_read_queries).and_call_original
+
+ subject
+ end
+ end
+ end
+ end
+end
diff --git a/spec/workers/projects/git_garbage_collect_worker_spec.rb b/spec/workers/projects/git_garbage_collect_worker_spec.rb
index 7b54d7df4b2..ae567107443 100644
--- a/spec/workers/projects/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/projects/git_garbage_collect_worker_spec.rb
@@ -32,6 +32,21 @@ RSpec.describe Projects::GitGarbageCollectWorker do
subject.perform(*params)
end
+
+ context 'when deduplication service runs into a GRPC internal error' do
+ before do
+ allow_next_instance_of(::Projects::GitDeduplicationService) do |instance|
+ expect(instance).to receive(:execute).and_raise(GRPC::Internal)
+ end
+ end
+
+ it_behaves_like 'can collect git garbage' do
+ let(:resource) { project }
+ let(:statistics_service_klass) { Projects::UpdateStatisticsService }
+ let(:statistics_keys) { [:repository_size, :lfs_objects_size] }
+ let(:expected_default_lease) { "projects:#{resource.id}" }
+ end
+ end
end
context 'LFS object garbage collection' do