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-10-24 21:11:45 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-10-24 21:11:45 +0300
commit4bb797f25563205cf495f4dd5366e037e88831ab (patch)
treea345ddbd0e2464067323d3c6fd34960607ef4f44 /spec
parent40a4f37126bb1a1dd6b6f4b3c0ebb414a3e3908a (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/projects/group_links_controller_spec.rb56
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_noteable.json6
-rw-r--r--spec/frontend/batch_comments/components/submit_dropdown_spec.js48
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/graph_component_spec.js91
-rw-r--r--spec/frontend/diffs/components/app_spec.js51
-rw-r--r--spec/frontend/diffs/components/diff_file_spec.js4
-rw-r--r--spec/frontend/search/sidebar/components/blobs_filters_spec.js39
-rw-r--r--spec/frontend/vue_merge_request_widget/components/checks/unresolved_discussions_spec.js39
-rw-r--r--spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js88
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js19
-rw-r--r--spec/helpers/visibility_level_helper_spec.rb30
-rw-r--r--spec/models/integration_spec.rb12
-rw-r--r--spec/models/integrations/base_chat_notification_spec.rb6
-rw-r--r--spec/requests/api/projects_spec.rb10
-rw-r--r--spec/services/projects/group_links/create_service_spec.rb95
-rw-r--r--spec/services/projects/group_links/destroy_service_spec.rb143
-rw-r--r--spec/services/projects/group_links/update_service_spec.rb121
17 files changed, 620 insertions, 238 deletions
diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb
index 2075dd3e7a7..4510e9e646e 100644
--- a/spec/controllers/projects/group_links_controller_spec.rb
+++ b/spec/controllers/projects/group_links_controller_spec.rb
@@ -30,24 +30,29 @@ RSpec.describe Projects::GroupLinksController, feature_category: :system_access
end
let(:expiry_date) { 1.month.from_now.to_date }
+ let(:group_access) { Gitlab::Access::GUEST }
- before do
- travel_to Time.now.utc.beginning_of_day
-
+ subject(:update_link) do
put(
:update,
params: {
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: link.id,
- group_link: { group_access: Gitlab::Access::GUEST, expires_at: expiry_date }
+ group_link: { group_access: group_access, expires_at: expiry_date }
},
format: :json
)
end
+ before do
+ travel_to Time.now.utc.beginning_of_day
+ end
+
context 'when `expires_at` is set' do
it 'returns correct json response' do
+ update_link
+
expect(json_response).to eq({ "expires_in" => controller.helpers.time_ago_with_tooltip(expiry_date), "expires_soon" => false })
end
end
@@ -56,27 +61,41 @@ RSpec.describe Projects::GroupLinksController, feature_category: :system_access
let(:expiry_date) { nil }
it 'returns empty json response' do
+ update_link
+
expect(json_response).to be_empty
end
end
+
+ it "returns an error when link is not updated" do
+ allow(::Projects::GroupLinks::UpdateService).to receive_message_chain(:new, :execute)
+ .and_return(ServiceResponse.error(message: '404 Not Found', reason: :not_found))
+
+ update_link
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Not Found')
+ end
end
describe '#destroy' do
let(:group_owner) { create(:user) }
+ let(:group_access) { Gitlab::Access::DEVELOPER }
+ let(:format) { :html }
- let(:link) do
- create(:project_group_link, project: project, group: group, group_access: Gitlab::Access::DEVELOPER)
+ let!(:link) do
+ create(:project_group_link, project: project, group: group, group_access: group_access)
end
subject(:destroy_link) do
post(:destroy, params: { namespace_id: project.namespace.to_param,
project_id: project.to_param,
- id: link.id })
+ id: link.id }, format: format)
end
shared_examples 'success response' do
it 'deletes the project group link' do
- destroy_link
+ expect { destroy_link }.to change { project.reload.project_group_links.count }
expect(response).to redirect_to(project_project_members_path(project))
expect(response).to have_gitlab_http_status(:found)
@@ -119,6 +138,27 @@ RSpec.describe Projects::GroupLinksController, feature_category: :system_access
end
it_behaves_like 'success response'
+
+ it "returns an error when link is not destroyed" do
+ allow(::Projects::GroupLinks::DestroyService).to receive_message_chain(:new, :execute)
+ .and_return(ServiceResponse.error(message: 'The error message'))
+
+ expect { destroy_link }.not_to change { project.reload.project_group_links.count }
+ expect(flash[:alert]).to eq('The project-group link could not be removed.')
+ end
+
+ context 'when format is js' do
+ let(:format) { :js }
+
+ it "returns an error when link is not destroyed" do
+ allow(::Projects::GroupLinks::DestroyService).to receive_message_chain(:new, :execute)
+ .and_return(ServiceResponse.error(message: '404 Not Found', reason: :not_found))
+
+ expect { destroy_link }.not_to change { project.reload.project_group_links.count }
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Not Found')
+ end
+ end
end
context 'when user is not a project maintainer' do
diff --git a/spec/fixtures/api/schemas/entities/merge_request_noteable.json b/spec/fixtures/api/schemas/entities/merge_request_noteable.json
index 4b790a2c34b..6f3c29b16e9 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_noteable.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_noteable.json
@@ -24,13 +24,11 @@
"type": "object",
"required": [
"can_create_note",
- "can_update",
- "can_approve"
+ "can_update"
],
"properties": {
"can_create_note": { "type": "boolean" },
- "can_update": { "type": "boolean" },
- "can_approve": { "type": "boolean" }
+ "can_update": { "type": "boolean" }
},
"additionalProperties": false
},
diff --git a/spec/frontend/batch_comments/components/submit_dropdown_spec.js b/spec/frontend/batch_comments/components/submit_dropdown_spec.js
index a2591635cf9..2f057af8a7d 100644
--- a/spec/frontend/batch_comments/components/submit_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/submit_dropdown_spec.js
@@ -1,13 +1,18 @@
import { GlDisclosureDropdown } from '@gitlab/ui';
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import SubmitDropdown from '~/batch_comments/components/submit_dropdown.vue';
import { mockTracking } from 'helpers/tracking_helper';
+import userCanApproveQuery from '~/batch_comments/queries/can_approve.query.graphql';
jest.mock('~/autosave');
+Vue.use(VueApollo);
Vue.use(Vuex);
let wrapper;
@@ -17,6 +22,26 @@ let trackingSpy;
function factory({ canApprove = true, shouldAnimateReviewButton = false } = {}) {
publishReview = jest.fn();
trackingSpy = mockTracking(undefined, null, jest.spyOn);
+ const requestHandlers = [
+ [
+ userCanApproveQuery,
+ () =>
+ Promise.resolve({
+ data: {
+ project: {
+ id: 1,
+ mergeRequest: {
+ id: 1,
+ userPermissions: {
+ canApprove,
+ },
+ },
+ },
+ },
+ }),
+ ],
+ ];
+ const apolloProvider = createMockApollo(requestHandlers);
const store = new Vuex.Store({
getters: {
@@ -27,12 +52,17 @@ function factory({ canApprove = true, shouldAnimateReviewButton = false } = {})
getNoteableData: () => ({
id: 1,
preview_note_path: '/preview',
- current_user: { can_approve: canApprove },
}),
noteableType: () => 'merge_request',
getCurrentUserLastNote: () => ({ id: 1 }),
},
modules: {
+ diffs: {
+ namespaced: true,
+ state: {
+ projectPath: 'gitlab-org/gitlab',
+ },
+ },
batchComments: {
namespaced: true,
state: { shouldAnimateReviewButton },
@@ -44,6 +74,7 @@ function factory({ canApprove = true, shouldAnimateReviewButton = false } = {})
});
wrapper = mountExtended(SubmitDropdown, {
store,
+ apolloProvider,
});
}
@@ -113,11 +144,18 @@ describe('Batch comments submit dropdown', () => {
canApprove | exists | existsText
${true} | ${true} | ${'shows'}
${false} | ${false} | ${'hides'}
- `('$existsText approve checkbox if can_approve is $canApprove', ({ canApprove, exists }) => {
- factory({ canApprove });
+ `(
+ '$existsText approve checkbox if can_approve is $canApprove',
+ async ({ canApprove, exists }) => {
+ factory({ canApprove });
- expect(wrapper.findByTestId('approve_merge_request').exists()).toBe(exists);
- });
+ wrapper.findComponent(GlDisclosureDropdown).vm.$emit('shown');
+
+ await waitForPromises();
+
+ expect(wrapper.findByTestId('approve_merge_request').exists()).toBe(exists);
+ },
+ );
it.each`
shouldAnimateReviewButton | animationClassApplied | classText
diff --git a/spec/frontend/ci/pipeline_details/graph/components/graph_component_spec.js b/spec/frontend/ci/pipeline_details/graph/components/graph_component_spec.js
index a98e79c69fe..c3f22749978 100644
--- a/spec/frontend/ci/pipeline_details/graph/components/graph_component_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/graph_component_spec.js
@@ -19,6 +19,10 @@ describe('graph component', () => {
const findLinksLayer = () => wrapper.findComponent(LinksLayer);
const findStageColumns = () => wrapper.findAllComponents(StageColumnComponent);
const findStageNameInJob = () => wrapper.findByTestId('stage-name-in-job');
+ const findPipelineContainer = () => wrapper.findByTestId('pipeline-container');
+ const findRootGraphLayout = () => wrapper.findByTestId('stage-column');
+ const findStageColumnTitle = () => wrapper.findByTestId('stage-column-title');
+ const findJobItem = () => wrapper.findComponent(JobItem);
const defaultProps = {
pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'),
@@ -42,6 +46,9 @@ describe('graph component', () => {
mountFn = shallowMount,
props = {},
stubOverride = {},
+ glFeatures = {
+ newPipelineGraph: false,
+ },
} = {}) => {
wrapper = mountFn(PipelineGraph, {
propsData: {
@@ -61,6 +68,9 @@ describe('graph component', () => {
'job-group-dropdown': true,
...stubOverride,
},
+ provide: {
+ glFeatures,
+ },
});
};
@@ -112,9 +122,8 @@ describe('graph component', () => {
});
it('dims unrelated jobs', () => {
- const unrelatedJob = wrapper.findComponent(JobItem);
expect(findLinksLayer().emitted().highlightedJobsChange).toHaveLength(1);
- expect(unrelatedJob.classes('gl-opacity-3')).toBe(true);
+ expect(findJobItem().classes('gl-opacity-3')).toBe(true);
});
});
});
@@ -179,4 +188,82 @@ describe('graph component', () => {
expect(findDownstreamColumn().props().linkedPipelines).toHaveLength(1);
});
});
+
+ describe.each`
+ name | value | state
+ ${'disabled'} | ${false} | ${'should not'}
+ ${'enabled'} | ${true} | ${'should'}
+ `('With feature flag newPipelineGraph $name', ({ value, state }) => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ stubOverride: { 'job-item': false, StageColumnComponent },
+ glFeatures: {
+ newPipelineGraph: value,
+ },
+ stubs: {
+ StageColumnComponent,
+ },
+ });
+ });
+
+ it(`${state} add class pipeline-graph-container on wrapper`, () => {
+ expect(findPipelineContainer().classes('pipeline-graph-container')).toBe(value);
+ });
+
+ it(`${state} add class is-stage-view on rootGraphLayout`, () => {
+ expect(findRootGraphLayout().classes('is-stage-view')).toBe(value);
+ });
+
+ it(`${state} add titleClasses on stageColumnTitle`, () => {
+ const titleClasses = [
+ 'gl-font-weight-bold',
+ 'gl-pipeline-job-width',
+ 'gl-text-truncate',
+ 'gl-line-height-36',
+ 'gl-pl-4',
+ 'gl-mb-n2',
+ ];
+ const legacyTitleClasses = [
+ 'gl-font-weight-bold',
+ 'gl-pipeline-job-width',
+ 'gl-text-truncate',
+ 'gl-line-height-36',
+ 'gl-pl-3',
+ ];
+ const checkClasses = value ? titleClasses : legacyTitleClasses;
+
+ expect(findStageColumnTitle().classes()).toEqual(expect.arrayContaining(checkClasses));
+ });
+
+ it(`${state} add jobClasses on findJobItem`, () => {
+ const jobClasses = [
+ 'gl-p-3',
+ 'gl-border-0',
+ 'gl-bg-transparent',
+ 'gl-rounded-base',
+ 'gl-hover-bg-gray-50',
+ 'gl-focus-bg-gray-50',
+ 'gl-hover-text-gray-900',
+ 'gl-focus-text-gray-900',
+ ];
+ const legacyJobClasses = [
+ 'gl-p-3',
+ 'gl-border-gray-100',
+ 'gl-border-solid',
+ 'gl-border-1',
+ 'gl-bg-white',
+ 'gl-rounded-7',
+ 'gl-hover-bg-gray-50',
+ 'gl-focus-bg-gray-50',
+ 'gl-hover-text-gray-900',
+ 'gl-focus-text-gray-900',
+ 'gl-hover-border-gray-200',
+ 'gl-focus-border-gray-200',
+ ];
+ const checkClasses = value ? jobClasses : legacyJobClasses;
+
+ expect(findJobItem().props('cssClassJobName')).toEqual(expect.arrayContaining(checkClasses));
+ });
+ });
});
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 47fd96ff625..5299361a493 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -22,11 +22,14 @@ import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vu
import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
import eventHub from '~/diffs/event_hub';
+import { EVT_DISCUSSIONS_ASSIGNED } from '~/diffs/constants';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { Mousetrap } from '~/lib/mousetrap';
import * as urlUtils from '~/lib/utils/url_utility';
+import * as commonUtils from '~/lib/utils/common_utils';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { stubPerformanceWebAPI } from 'helpers/performance';
import createDiffsStore from '../create_diffs_store';
import diffsMockData from '../mock_data/merge_request_diffs';
@@ -662,6 +665,12 @@ describe('diffs/components/app', () => {
});
describe('file-by-file', () => {
+ let hashSpy;
+
+ beforeEach(() => {
+ hashSpy = jest.spyOn(commonUtils, 'handleLocationHash');
+ });
+
it('renders a single diff', async () => {
createComponent(
undefined,
@@ -681,6 +690,48 @@ describe('diffs/components/app', () => {
expect(wrapper.findAllComponents(DiffFile).length).toBe(1);
});
+ describe('rechecking the url hash for scrolling', () => {
+ const advanceAndCheckCalls = (count = 0) => {
+ jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ expect(hashSpy).toHaveBeenCalledTimes(count);
+ };
+
+ it('re-checks one time after the file finishes loading', () => {
+ createComponent(
+ undefined,
+ ({ state }) => {
+ state.diffs.diffFiles = [{ isLoadingFullFile: true }];
+ },
+ undefined,
+ { viewDiffsFileByFile: true },
+ );
+
+ // The hash check is not called if the file is still marked as loading
+ expect(hashSpy).toHaveBeenCalledTimes(0);
+ eventHub.$emit(EVT_DISCUSSIONS_ASSIGNED);
+ advanceAndCheckCalls();
+ eventHub.$emit(EVT_DISCUSSIONS_ASSIGNED);
+ advanceAndCheckCalls();
+ // Once the file has finished loading, it calls through to check the hash
+ store.state.diffs.diffFiles[0].isLoadingFullFile = false;
+ eventHub.$emit(EVT_DISCUSSIONS_ASSIGNED);
+ advanceAndCheckCalls(1);
+ // No further scrolls happen after one hash check / scroll
+ eventHub.$emit(EVT_DISCUSSIONS_ASSIGNED);
+ advanceAndCheckCalls(1);
+ eventHub.$emit(EVT_DISCUSSIONS_ASSIGNED);
+ advanceAndCheckCalls(1);
+ });
+
+ it('does not re-check when not in single-file mode', () => {
+ createComponent();
+
+ eventHub.$emit(EVT_DISCUSSIONS_ASSIGNED);
+
+ expect(hashSpy).not.toHaveBeenCalled();
+ });
+ });
+
describe('pagination', () => {
const fileByFileNav = () => wrapper.find('[data-testid="file-by-file-navigation"]');
const paginator = () => fileByFileNav().findComponent(GlPagination);
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js
index 13efd3584b4..34af3d72b04 100644
--- a/spec/frontend/diffs/components/diff_file_spec.js
+++ b/spec/frontend/diffs/components/diff_file_spec.js
@@ -13,6 +13,7 @@ import DiffFileComponent from '~/diffs/components/diff_file.vue';
import DiffFileHeaderComponent from '~/diffs/components/diff_file_header.vue';
import {
+ EVT_DISCUSSIONS_ASSIGNED,
EVT_EXPAND_ALL_FILES,
EVT_PERF_MARK_DIFF_FILES_END,
EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN,
@@ -271,9 +272,10 @@ describe('DiffFile', () => {
await nextTick(); // Wait for the idleCallback
await nextTick(); // Wait for nextTick inside postRender
- expect(eventHub.$emit).toHaveBeenCalledTimes(2);
+ expect(eventHub.$emit).toHaveBeenCalledTimes(3);
expect(eventHub.$emit).toHaveBeenCalledWith(EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN);
expect(eventHub.$emit).toHaveBeenCalledWith(EVT_PERF_MARK_DIFF_FILES_END);
+ expect(eventHub.$emit).toHaveBeenCalledWith(EVT_DISCUSSIONS_ASSIGNED);
});
});
});
diff --git a/spec/frontend/search/sidebar/components/blobs_filters_spec.js b/spec/frontend/search/sidebar/components/blobs_filters_spec.js
index 729fae44c19..245ddb8f8bb 100644
--- a/spec/frontend/search/sidebar/components/blobs_filters_spec.js
+++ b/spec/frontend/search/sidebar/components/blobs_filters_spec.js
@@ -17,7 +17,7 @@ describe('GlobalSearch BlobsFilters', () => {
currentScope: () => 'blobs',
};
- const createComponent = ({ initialState = {}, searchBlobsHideArchivedProjects = true } = {}) => {
+ const createComponent = ({ initialState = {} } = {}) => {
const store = new Vuex.Store({
state: {
urlQuery: MOCK_QUERY,
@@ -30,11 +30,6 @@ describe('GlobalSearch BlobsFilters', () => {
wrapper = shallowMount(BlobsFilters, {
store,
- provide: {
- glFeatures: {
- searchBlobsHideArchivedProjects,
- },
- },
});
};
@@ -42,29 +37,20 @@ describe('GlobalSearch BlobsFilters', () => {
const findArchivedFilter = () => wrapper.findComponent(ArchivedFilter);
const findDividers = () => wrapper.findAll('hr');
- describe.each`
- description | searchBlobsHideArchivedProjects
- ${'Renders correctly with Archived Filter enabled'} | ${true}
- ${'Renders correctly with Archived Filter disabled'} | ${false}
- `('$description', ({ searchBlobsHideArchivedProjects }) => {
- beforeEach(() => {
- createComponent({
- searchBlobsHideArchivedProjects,
- });
- });
+ beforeEach(() => {
+ createComponent({});
+ });
- it('renders LanguageFilter', () => {
- expect(findLanguageFilter().exists()).toBe(true);
- });
+ it('renders LanguageFilter', () => {
+ expect(findLanguageFilter().exists()).toBe(true);
+ });
- it(`renders correctly ArchivedFilter when searchBlobsHideArchivedProjects is ${searchBlobsHideArchivedProjects}`, () => {
- expect(findArchivedFilter().exists()).toBe(searchBlobsHideArchivedProjects);
- });
+ it('renders ArchivedFilter', () => {
+ expect(findArchivedFilter().exists()).toBe(true);
+ });
- it('renders divider correctly', () => {
- const dividersCount = searchBlobsHideArchivedProjects ? 1 : 0;
- expect(findDividers()).toHaveLength(dividersCount);
- });
+ it('renders divider correctly', () => {
+ expect(findDividers()).toHaveLength(1);
});
describe('Renders correctly in new nav', () => {
@@ -74,7 +60,6 @@ describe('GlobalSearch BlobsFilters', () => {
searchType: SEARCH_TYPE_ADVANCED,
useSidebarNavigation: true,
},
- searchBlobsHideArchivedProjects: true,
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/checks/unresolved_discussions_spec.js b/spec/frontend/vue_merge_request_widget/components/checks/unresolved_discussions_spec.js
new file mode 100644
index 00000000000..3ec791d421f
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/checks/unresolved_discussions_spec.js
@@ -0,0 +1,39 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import notesEventHub from '~/notes/event_hub';
+import MergeChecksUnresolvedDiscussions from '~/vue_merge_request_widget/components/checks/unresolved_discussions.vue';
+import MergeChecksMessage from '~/vue_merge_request_widget/components/checks/message.vue';
+
+describe('MergeChecksUnresolvedDiscussions component', () => {
+ let wrapper;
+
+ function createComponent(
+ propsData = { check: { result: 'failed', failureReason: 'Failed message' } },
+ ) {
+ wrapper = mountExtended(MergeChecksUnresolvedDiscussions, {
+ propsData,
+ });
+ }
+
+ it('passes check down to the MergeChecksMessage', () => {
+ const check = { result: 'failed', failureReason: 'Unresolved discussions' };
+ createComponent({ check });
+
+ expect(wrapper.findComponent(MergeChecksMessage).props('check')).toEqual(check);
+ });
+
+ it('does not show go to first unresolved discussion button with passed state', () => {
+ createComponent({ check: { result: 'passed' } });
+ const button = wrapper.findByRole('button', { name: 'Go to first unresolved thread' });
+ expect(button.exists()).toBe(false);
+ });
+
+ it('triggers go to first discussion action', () => {
+ const callback = jest.fn();
+ notesEventHub.$on('jumpToFirstUnresolvedDiscussion', callback);
+ createComponent();
+
+ wrapper.findByRole('button', { name: 'Go to first unresolved thread' }).trigger('click');
+
+ expect(callback).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js b/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js
index 6224d6e42ee..f8a09391e5d 100644
--- a/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js
@@ -1,18 +1,22 @@
import VueApollo from 'vue-apollo';
import Vue from 'vue';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import MergeChecksComponent from '~/vue_merge_request_widget/components/merge_checks.vue';
import mergeChecksQuery from '~/vue_merge_request_widget/queries/merge_checks.query.graphql';
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
+import StateContainer from '~/vue_merge_request_widget/components/state_container.vue';
+import { COMPONENTS } from '~/vue_merge_request_widget/components/checks/constants';
+import conflictsStateQuery from '~/vue_merge_request_widget/queries/states/conflicts.query.graphql';
+import rebaseStateQuery from '~/vue_merge_request_widget/queries/states/rebase.query.graphql';
Vue.use(VueApollo);
let wrapper;
let apolloProvider;
-function factory({ canMerge = true, mergeChecks = [] } = {}) {
+function factory(mountFn, { canMerge = true, mergeChecks = [] } = {}) {
apolloProvider = createMockApollo([
[
mergeChecksQuery,
@@ -25,9 +29,56 @@ function factory({ canMerge = true, mergeChecks = [] } = {}) {
},
}),
],
+ [
+ conflictsStateQuery,
+ () =>
+ Promise.resolve({
+ data: {
+ project: {
+ id: 1,
+ mergeRequest: {
+ id: 1,
+ shouldBeRebased: false,
+ sourceBranchProtected: false,
+ userPermissions: { pushToSourceBranch: true },
+ },
+ },
+ },
+ }),
+ ],
+ [
+ rebaseStateQuery,
+ () =>
+ Promise.resolve({
+ data: {
+ project: {
+ id: '1',
+ mergeRequest: {
+ id: '2',
+ rebaseInProgress: false,
+ targetBranch: 'main',
+ userPermissions: {
+ pushToSourceBranch: true,
+ },
+ pipelines: {
+ nodes: [
+ {
+ id: '1',
+ project: {
+ id: '2',
+ fullPath: 'gitlab/gitlab',
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ }),
+ ],
]);
- wrapper = mountExtended(MergeChecksComponent, {
+ wrapper = mountFn(MergeChecksComponent, {
apolloProvider,
propsData: {
mr: {},
@@ -36,13 +87,16 @@ function factory({ canMerge = true, mergeChecks = [] } = {}) {
});
}
+const mountComponent = factory.bind(null, mountExtended);
+const shallowMountComponent = factory.bind(null, shallowMountExtended);
+
describe('Merge request merge checks component', () => {
afterEach(() => {
apolloProvider = null;
});
it('renders ready to merge text if user can merge', async () => {
- factory({ canMerge: true });
+ mountComponent({ canMerge: true });
await waitForPromises();
@@ -50,7 +104,7 @@ describe('Merge request merge checks component', () => {
});
it('renders ready to merge by members text if user can not merge', async () => {
- factory({ canMerge: false });
+ mountComponent({ canMerge: false });
await waitForPromises();
@@ -62,7 +116,7 @@ describe('Merge request merge checks component', () => {
${[{ identifier: 'discussions', result: 'failed' }]} | ${'Merge blocked: 1 check failed'}
${[{ identifier: 'discussions', result: 'failed' }, { identifier: 'rebase', result: 'failed' }]} | ${'Merge blocked: 2 checks failed'}
`('renders $text for $mergeChecks', async ({ mergeChecks, text }) => {
- factory({ mergeChecks });
+ mountComponent({ mergeChecks });
await waitForPromises();
@@ -74,15 +128,33 @@ describe('Merge request merge checks component', () => {
${'failed'} | ${'failed'}
${'passed'} | ${'success'}
`('renders $statusIcon for $result result', async ({ result, statusIcon }) => {
- factory({ mergeChecks: [{ result, identifier: 'discussions' }] });
+ mountComponent({ mergeChecks: [{ result, identifier: 'discussions' }] });
await waitForPromises();
expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe(statusIcon);
});
+ it.each`
+ identifier
+ ${'conflicts'}
+ ${'unresolved_discussions'}
+ ${'rebase'}
+ ${'default'}
+ `('renders $identifier merge check', async ({ identifier }) => {
+ shallowMountComponent({ mergeChecks: [{ result: 'failed', identifier }] });
+
+ wrapper.findComponent(StateContainer).vm.$emit('toggle');
+
+ await waitForPromises();
+
+ const { default: component } = await COMPONENTS[identifier]();
+
+ expect(wrapper.findComponent(component).exists()).toBe(true);
+ });
+
it('expands collapsed area', async () => {
- factory();
+ mountComponent();
await waitForPromises();
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js
index 16751bcc0f0..213959fe4e2 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js
@@ -2,6 +2,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
import DynamicContent from '~/vue_merge_request_widget/components/widget/dynamic_content.vue';
import ContentRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
describe('~/vue_merge_request_widget/components/widget/dynamic_content.vue', () => {
let wrapper;
@@ -16,10 +17,13 @@ describe('~/vue_merge_request_widget/components/widget/dynamic_content.vue', ()
DynamicContent,
ContentRow,
},
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
});
};
- it('renders given data', () => {
+ beforeEach(() => {
createComponent({
propsData: {
data: {
@@ -49,10 +53,23 @@ describe('~/vue_merge_request_widget/components/widget/dynamic_content.vue', ()
text: 'This is recursive. It will be listed in level 3.',
},
],
+ tooltipText: 'Tooltip text',
},
},
});
+ });
+ it('renders given data', () => {
expect(wrapper.html()).toMatchSnapshot();
});
+
+ it('has a tooltip on the row text', () => {
+ const text = wrapper.findByText('Main text for the row');
+ const tooltip = getBinding(text.element, 'gl-tooltip');
+
+ expect(tooltip.value).toMatchObject({
+ title: 'Tooltip text',
+ boundary: 'viewport',
+ });
+ });
});
diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb
index 8f37bf29a4b..4af7fae400e 100644
--- a/spec/helpers/visibility_level_helper_spec.rb
+++ b/spec/helpers/visibility_level_helper_spec.rb
@@ -323,34 +323,4 @@ RSpec.describe VisibilityLevelHelper, feature_category: :system_access do
it { is_expected.to eq(expected) }
end
end
-
- describe '#visibility_level_options' do
- let(:user) { build(:user) }
-
- before do
- allow(helper).to receive(:current_user).and_return(user)
- end
-
- it 'returns the desired mapping' do
- expected_options = [
- {
- level: 0,
- label: 'Private',
- description: 'The group and its projects can only be viewed by members.'
- },
- {
- level: 10,
- label: 'Internal',
- description: 'The group and any internal projects can be viewed by any logged in user except external users.'
- },
- {
- level: 20,
- label: 'Public',
- description: 'The group and any public projects can be viewed without any authentication.'
- }
- ]
-
- expect(helper.visibility_level_options(group)).to eq expected_options
- end
- end
end
diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb
index d7b69546de6..8396d5469ad 100644
--- a/spec/models/integration_spec.rb
+++ b/spec/models/integration_spec.rb
@@ -157,6 +157,18 @@ RSpec.describe Integration, feature_category: :integrations do
include_examples 'hook scope', 'incident'
end
+ describe '.title' do
+ it 'raises an error' do
+ expect { described_class.title }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '.description' do
+ it 'raises an error' do
+ expect { described_class.description }.to raise_error(NotImplementedError)
+ end
+ end
+
describe '#operating?' do
it 'is false when the integration is not active' do
expect(build(:integration).operating?).to eq(false)
diff --git a/spec/models/integrations/base_chat_notification_spec.rb b/spec/models/integrations/base_chat_notification_spec.rb
index 497f2f1e7c9..9ad37f40fbc 100644
--- a/spec/models/integrations/base_chat_notification_spec.rb
+++ b/spec/models/integrations/base_chat_notification_spec.rb
@@ -347,12 +347,6 @@ RSpec.describe Integrations::BaseChatNotification, feature_category: :integratio
end
end
- describe '#help' do
- it 'raises an error' do
- expect { subject.help }.to raise_error(NotImplementedError)
- end
- end
-
describe '#event_channel_name' do
it 'returns the channel field name for the given event' do
expect(subject.event_channel_name(:event)).to eq('event_channel')
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 64e010aa50f..ba3103fd52f 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -3688,6 +3688,16 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
it_behaves_like '412 response' do
subject(:request) { api("/projects/#{project.id}/share/#{group.id}", user) }
end
+
+ it "returns an error when link is not destroyed" do
+ allow(::Projects::GroupLinks::DestroyService).to receive_message_chain(:new, :execute)
+ .and_return(ServiceResponse.error(message: '404 Not Found', reason: :not_found))
+
+ delete api("/projects/#{project.id}/share/#{group.id}", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq '404 Not Found'
+ end
end
it 'returns a 400 when group id is not an integer' do
diff --git a/spec/services/projects/group_links/create_service_spec.rb b/spec/services/projects/group_links/create_service_spec.rb
index ca2902af472..e3f170ef3fe 100644
--- a/spec/services/projects/group_links/create_service_spec.rb
+++ b/spec/services/projects/group_links/create_service_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe Projects::GroupLinks::CreateService, '#execute', feature_category
let_it_be(:user) { create :user }
let_it_be(:group) { create :group }
let_it_be(:project) { create(:project, namespace: create(:namespace, :with_namespace_settings)) }
+ let_it_be(:group_user) { create(:user).tap { |user| group.add_guest(user) } }
let(:opts) do
{
@@ -37,67 +38,75 @@ RSpec.describe Projects::GroupLinks::CreateService, '#execute', feature_category
end
end
- context 'when user has proper membership to share a group' do
+ context 'when user has proper permissions to share a project with a group' do
before do
group.add_guest(user)
end
- it_behaves_like 'shareable'
-
- it 'updates authorization', :sidekiq_inline do
- expect { subject.execute }.to(
- change { Ability.allowed?(user, :read_project, project) }
- .from(false).to(true))
- end
-
- context 'with specialized project_authorization workers' do
- let_it_be(:other_user) { create(:user) }
-
+ context 'when the user is a MAINTAINER in the project' do
before do
- group.add_developer(other_user)
+ project.add_maintainer(user)
end
- it 'schedules authorization update for users with access to group' do
- stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
-
- expect(AuthorizedProjectsWorker).not_to(
- receive(:bulk_perform_async)
- )
- expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to(
- receive(:perform_async)
- .with(project.id)
- .and_call_original
- )
- expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
- receive(:bulk_perform_in).with(
- 1.hour,
- array_including([user.id], [other_user.id]),
- batch_delay: 30.seconds, batch_size: 100
- ).and_call_original
- )
-
- subject.execute
+ it_behaves_like 'shareable'
+
+ it 'updates authorization', :sidekiq_inline do
+ expect { subject.execute }.to(
+ change { Ability.allowed?(group_user, :read_project, project) }
+ .from(false).to(true))
end
- end
- context 'when sharing outside the hierarchy is disabled' do
- let_it_be(:shared_group_parent) do
- create(:group, namespace_settings: create(:namespace_settings, prevent_sharing_groups_outside_hierarchy: true))
+ context 'with specialized project_authorization workers' do
+ let_it_be(:other_user) { create(:user) }
+
+ before do
+ group.add_developer(other_user)
+ end
+
+ it 'schedules authorization update for users with access to group' do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+
+ expect(AuthorizedProjectsWorker).not_to(
+ receive(:bulk_perform_async)
+ )
+ expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to(
+ receive(:perform_async)
+ .with(project.id)
+ .and_call_original
+ )
+ expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
+ receive(:bulk_perform_in).with(
+ 1.hour,
+ array_including([user.id], [other_user.id]),
+ batch_delay: 30.seconds, batch_size: 100
+ ).and_call_original
+ )
+
+ subject.execute
+ end
end
- let_it_be(:project, reload: true) { create(:project, group: shared_group_parent) }
+ context 'when sharing outside the hierarchy is disabled' do
+ let_it_be(:shared_group_parent) do
+ create(:group,
+ namespace_settings: create(:namespace_settings, prevent_sharing_groups_outside_hierarchy: true)
+ )
+ end
+
+ let_it_be(:project, reload: true) { create(:project, group: shared_group_parent) }
- it_behaves_like 'not shareable'
+ it_behaves_like 'not shareable'
- context 'when group is inside hierarchy' do
- let(:group) { create(:group, :private, parent: shared_group_parent) }
+ context 'when group is inside hierarchy' do
+ let(:group) { create(:group, :private, parent: shared_group_parent) }
- it_behaves_like 'shareable'
+ it_behaves_like 'shareable'
+ end
end
end
end
- context 'when user does not have permissions for the group' do
+ context 'when user does not have permissions to share the project with a group' do
it_behaves_like 'not shareable'
end
diff --git a/spec/services/projects/group_links/destroy_service_spec.rb b/spec/services/projects/group_links/destroy_service_spec.rb
index 103aff8c659..0cd003f6142 100644
--- a/spec/services/projects/group_links/destroy_service_spec.rb
+++ b/spec/services/projects/group_links/destroy_service_spec.rb
@@ -6,83 +6,120 @@ RSpec.describe Projects::GroupLinks::DestroyService, '#execute', feature_categor
let_it_be(:user) { create :user }
let_it_be(:project) { create(:project, :private) }
let_it_be(:group) { create(:group) }
+ let_it_be(:group_user) { create(:user).tap { |user| group.add_guest(user) } }
- let!(:group_link) { create(:project_group_link, project: project, group: group) }
+ let(:group_access) { Gitlab::Access::DEVELOPER }
+ let!(:group_link) { create(:project_group_link, project: project, group: group, group_access: group_access) }
subject { described_class.new(project, user) }
- it 'removes group from project' do
- expect { subject.execute(group_link) }.to change { project.project_group_links.count }.from(1).to(0)
- end
-
- context 'project authorizations refresh' do
- before do
- group.add_maintainer(user)
+ shared_examples_for 'removes group from project' do
+ it 'removes group from project' do
+ expect { subject.execute(group_link) }.to change { project.reload.project_group_links.count }.from(1).to(0)
end
+ end
- it 'calls AuthorizedProjectUpdate::ProjectRecalculateWorker to update project authorizations' do
- expect(AuthorizedProjectUpdate::ProjectRecalculateWorker)
- .to receive(:perform_async).with(group_link.project.id)
+ shared_examples_for 'returns not_found' do
+ it do
+ expect do
+ result = subject.execute(group_link)
- subject.execute(group_link)
+ expect(result[:status]).to eq(:error)
+ expect(result[:reason]).to eq(:not_found)
+ end.not_to change { project.reload.project_group_links.count }
end
+ end
- it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do
- stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
-
- expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
- receive(:bulk_perform_in).with(
- 1.hour,
- [[user.id]],
- batch_delay: 30.seconds, batch_size: 100
- )
- )
-
- subject.execute(group_link)
- end
+ context 'if group_link is blank' do
+ let!(:group_link) { nil }
- it 'updates project authorizations of users who had access to the project via the group share', :sidekiq_inline do
- expect { subject.execute(group_link) }.to(
- change { Ability.allowed?(user, :read_project, project) }
- .from(true).to(false))
- end
+ it_behaves_like 'returns not_found'
end
- it 'returns false if group_link is blank' do
- expect { subject.execute(nil) }.not_to change { project.project_group_links.count }
+ context 'if the user does not have access to destroy the link' do
+ it_behaves_like 'returns not_found'
end
- describe 'todos cleanup' do
- context 'when project is private' do
- it 'triggers todos cleanup' do
- expect(TodosDestroyer::ProjectPrivateWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, project.id)
- expect(project.private?).to be true
-
- subject.execute(group_link)
+ context 'when the user has proper permissions to remove a group-link from a project' do
+ context 'when the user is a MAINTAINER in the project' do
+ before do
+ project.add_maintainer(user)
end
- end
- context 'when project is public or internal' do
- shared_examples_for 'removes confidential todos' do
- it 'does not trigger todos cleanup' do
- expect(TodosDestroyer::ProjectPrivateWorker).not_to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, project.id)
- expect(TodosDestroyer::ConfidentialIssueWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, nil, project.id)
- expect(project.private?).to be false
+ it_behaves_like 'removes group from project'
+
+ context 'project authorizations refresh' do
+ it 'calls AuthorizedProjectUpdate::ProjectRecalculateWorker to update project authorizations' do
+ expect(AuthorizedProjectUpdate::ProjectRecalculateWorker)
+ .to receive(:perform_async).with(group_link.project.id)
subject.execute(group_link)
end
- end
- context 'when project is public' do
- let(:project) { create(:project, :public) }
+ it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
- it_behaves_like 'removes confidential todos'
+ expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
+ receive(:bulk_perform_in).with(
+ 1.hour,
+ [[group_user.id]],
+ batch_delay: 30.seconds, batch_size: 100
+ )
+ )
+
+ subject.execute(group_link)
+ end
+
+ it 'updates project authorizations of users who had access to the project via the group share', :sidekiq_inline do
+ expect { subject.execute(group_link) }.to(
+ change { Ability.allowed?(group_user, :read_project, project) }
+ .from(true).to(false))
+ end
end
- context 'when project is internal' do
- let(:project) { create(:project, :public) }
+ describe 'todos cleanup' do
+ context 'when project is private' do
+ it 'triggers todos cleanup' do
+ expect(TodosDestroyer::ProjectPrivateWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, project.id)
+ expect(project.private?).to be true
+
+ subject.execute(group_link)
+ end
+ end
+
+ context 'when project is public or internal' do
+ shared_examples_for 'removes confidential todos' do
+ it 'does not trigger todos cleanup' do
+ expect(TodosDestroyer::ProjectPrivateWorker).not_to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, project.id)
+ expect(TodosDestroyer::ConfidentialIssueWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, nil, project.id)
+ expect(project.private?).to be false
+
+ subject.execute(group_link)
+ end
+ end
+
+ context 'when project is public' do
+ let(:project) { create(:project, :public) }
+
+ it_behaves_like 'removes confidential todos'
+ end
+
+ context 'when project is internal' do
+ let(:project) { create(:project, :public) }
+
+ it_behaves_like 'removes confidential todos'
+ end
+ end
+ end
+ end
+ end
- it_behaves_like 'removes confidential todos'
+ context 'when skipping authorization' do
+ context 'without providing a user' do
+ it 'destroys the link' do
+ expect do
+ described_class.new(project, nil).execute(group_link, skip_authorization: true)
+ end.to change { project.reload.project_group_links.count }.by(-1)
end
end
end
diff --git a/spec/services/projects/group_links/update_service_spec.rb b/spec/services/projects/group_links/update_service_spec.rb
index f7607deef04..b02614fa062 100644
--- a/spec/services/projects/group_links/update_service_spec.rb
+++ b/spec/services/projects/group_links/update_service_spec.rb
@@ -6,8 +6,11 @@ RSpec.describe Projects::GroupLinks::UpdateService, '#execute', feature_category
let_it_be(:user) { create :user }
let_it_be(:group) { create :group }
let_it_be(:project) { create :project }
+ let_it_be(:group_user) { create(:user).tap { |user| group.add_developer(user) } }
- let!(:link) { create(:project_group_link, project: project, group: group) }
+ let(:group_access) { Gitlab::Access::DEVELOPER }
+
+ let!(:link) { create(:project_group_link, project: project, group: group, group_access: group_access) }
let(:expiry_date) { 1.month.from_now.to_date }
let(:group_link_params) do
@@ -17,60 +20,78 @@ RSpec.describe Projects::GroupLinks::UpdateService, '#execute', feature_category
subject { described_class.new(link, user).execute(group_link_params) }
- before do
- group.add_developer(user)
- end
-
- it 'updates existing link' do
- expect(link.group_access).to eq(Gitlab::Access::DEVELOPER)
- expect(link.expires_at).to be_nil
-
- subject
-
- link.reload
+ shared_examples_for 'returns not_found' do
+ it do
+ result = subject
- expect(link.group_access).to eq(Gitlab::Access::GUEST)
- expect(link.expires_at).to eq(expiry_date)
- end
-
- context 'project authorizations update' do
- it 'calls AuthorizedProjectUpdate::ProjectRecalculateWorker to update project authorizations' do
- expect(AuthorizedProjectUpdate::ProjectRecalculateWorker)
- .to receive(:perform_async).with(link.project.id)
-
- subject
- end
-
- it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do
- stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
-
- expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
- receive(:bulk_perform_in).with(
- 1.hour,
- [[user.id]],
- batch_delay: 30.seconds, batch_size: 100
- )
- )
-
- subject
- end
-
- it 'updates project authorizations of users who had access to the project via the group share', :sidekiq_inline do
- group.add_maintainer(user)
-
- expect { subject }.to(
- change { Ability.allowed?(user, :create_release, project) }
- .from(true).to(false))
+ expect(result[:status]).to eq(:error)
+ expect(result[:reason]).to eq(:not_found)
end
end
- context 'with only param not requiring authorization refresh' do
- let(:group_link_params) { { expires_at: Date.tomorrow } }
-
- it 'does not perform any project authorizations update using `AuthorizedProjectUpdate::ProjectRecalculateWorker`' do
- expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).not_to receive(:perform_async)
+ context 'when the user does not have proper permissions to update a project group link' do
+ it_behaves_like 'returns not_found'
+ end
- subject
+ context 'when user has proper permissions to update a project group link' do
+ context 'when the user is a MAINTAINER in the project' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'updates existing link' do
+ expect(link.group_access).to eq(Gitlab::Access::DEVELOPER)
+ expect(link.expires_at).to be_nil
+
+ subject
+
+ link.reload
+
+ expect(link.group_access).to eq(Gitlab::Access::GUEST)
+ expect(link.expires_at).to eq(expiry_date)
+ end
+
+ context 'project authorizations update' do
+ it 'calls AuthorizedProjectUpdate::ProjectRecalculateWorker to update project authorizations' do
+ expect(AuthorizedProjectUpdate::ProjectRecalculateWorker)
+ .to receive(:perform_async).with(link.project.id)
+
+ subject
+ end
+
+ it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker ' \
+ 'with a delay to update project authorizations' do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+
+ expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
+ receive(:bulk_perform_in).with(
+ 1.hour,
+ [[group_user.id]],
+ batch_delay: 30.seconds, batch_size: 100
+ )
+ )
+
+ subject
+ end
+
+ it 'updates project authorizations of users who had access to the project via the group share',
+ :sidekiq_inline do
+ expect { subject }.to(
+ change { Ability.allowed?(group_user, :developer_access, project) }
+ .from(true).to(false))
+ end
+ end
+
+ context 'with only param not requiring authorization refresh' do
+ let(:group_link_params) { { expires_at: Date.tomorrow } }
+
+ it 'does not perform any project authorizations update using ' \
+ '`AuthorizedProjectUpdate::ProjectRecalculateWorker`' do
+ expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).not_to receive(:perform_async)
+
+ subject
+ end
+ end
end
end
end