diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-09 18:09:13 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-09 18:09:13 +0300 |
commit | 9c39a0a9b81f06f6345a6b6e071b8e8cd249c064 (patch) | |
tree | 45caaf45fac61866d8cc9098487a78b17c1016a0 /spec | |
parent | 53af44b90f87cdd8d7126d64669848b0e2be5960 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
11 files changed, 433 insertions, 58 deletions
diff --git a/spec/frontend/issues_list/components/issues_list_app_spec.js b/spec/frontend/issues_list/components/issues_list_app_spec.js index 2f4803eac8c..de065af8b2b 100644 --- a/spec/frontend/issues_list/components/issues_list_app_spec.js +++ b/spec/frontend/issues_list/components/issues_list_app_spec.js @@ -18,7 +18,7 @@ import { getIssuesCountQueryResponse, } from 'jest/issues_list/mock_data'; import createFlash from '~/flash'; -import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue'; import IssuableByEmail from '~/issuable/components/issuable_by_email.vue'; import IssuableList from '~/issuable_list/components/issuable_list_root.vue'; @@ -43,6 +43,7 @@ import eventHub from '~/issues_list/eventhub'; import { getSortOptions } from '~/issues_list/utils'; import axios from '~/lib/utils/axios_utils'; import { scrollUp } from '~/lib/utils/scroll_utils'; +import { joinPaths } from '~/lib/utils/url_utility'; jest.mock('~/flash'); jest.mock('~/lib/utils/scroll_utils', () => ({ @@ -621,25 +622,25 @@ describe('IssuesListApp component', () => { const issueOne = { ...defaultQueryResponse.data.project.issues.nodes[0], id: 'gid://gitlab/Issue/1', - iid: 101, + iid: '101', title: 'Issue one', }; const issueTwo = { ...defaultQueryResponse.data.project.issues.nodes[0], id: 'gid://gitlab/Issue/2', - iid: 102, + iid: '102', title: 'Issue two', }; const issueThree = { ...defaultQueryResponse.data.project.issues.nodes[0], id: 'gid://gitlab/Issue/3', - iid: 103, + iid: '103', title: 'Issue three', }; const issueFour = { ...defaultQueryResponse.data.project.issues.nodes[0], id: 'gid://gitlab/Issue/4', - iid: 104, + iid: '104', title: 'Issue four', }; const response = { @@ -658,9 +659,36 @@ describe('IssuesListApp component', () => { jest.runOnlyPendingTimers(); }); + describe('when successful', () => { + describe.each` + description | issueToMove | oldIndex | newIndex | moveBeforeId | moveAfterId + ${'to the beginning of the list'} | ${issueThree} | ${2} | ${0} | ${null} | ${issueOne.id} + ${'down the list'} | ${issueOne} | ${0} | ${1} | ${issueTwo.id} | ${issueThree.id} + ${'up the list'} | ${issueThree} | ${2} | ${1} | ${issueOne.id} | ${issueTwo.id} + ${'to the end of the list'} | ${issueTwo} | ${1} | ${3} | ${issueFour.id} | ${null} + `( + 'when moving issue $description', + ({ issueToMove, oldIndex, newIndex, moveBeforeId, moveAfterId }) => { + it('makes API call to reorder the issue', async () => { + findIssuableList().vm.$emit('reorder', { oldIndex, newIndex }); + + await waitForPromises(); + + expect(axiosMock.history.put[0]).toMatchObject({ + url: joinPaths(defaultProvide.issuesPath, issueToMove.iid, 'reorder'), + data: JSON.stringify({ + move_before_id: getIdFromGraphQLId(moveBeforeId), + move_after_id: getIdFromGraphQLId(moveAfterId), + }), + }); + }); + }, + ); + }); + describe('when unsuccessful', () => { it('displays an error message', async () => { - axiosMock.onPut(`${defaultProvide.issuesPath}/${issueOne.iid}/reorder`).reply(500); + axiosMock.onPut(joinPaths(defaultProvide.issuesPath, issueOne.iid, 'reorder')).reply(500); findIssuableList().vm.$emit('reorder', { oldIndex: 0, newIndex: 1 }); diff --git a/spec/frontend/issues_list/utils_spec.js b/spec/frontend/issues_list/utils_spec.js index b7863068570..458776d9ec5 100644 --- a/spec/frontend/issues_list/utils_spec.js +++ b/spec/frontend/issues_list/utils_spec.js @@ -8,17 +8,36 @@ import { urlParams, urlParamsWithSpecialValues, } from 'jest/issues_list/mock_data'; -import { DUE_DATE_VALUES, urlSortParams } from '~/issues_list/constants'; +import { + defaultPageSizeParams, + DUE_DATE_VALUES, + largePageSizeParams, + RELATIVE_POSITION_ASC, + urlSortParams, +} from '~/issues_list/constants'; import { convertToApiParams, convertToSearchQuery, convertToUrlParams, getDueDateValue, getFilterTokens, + getInitialPageParams, getSortKey, getSortOptions, } from '~/issues_list/utils'; +describe('getInitialPageParams', () => { + it.each(Object.keys(urlSortParams))( + 'returns the correct page params for sort key %s', + (sortKey) => { + const expectedPageParams = + sortKey === RELATIVE_POSITION_ASC ? largePageSizeParams : defaultPageSizeParams; + + expect(getInitialPageParams(sortKey)).toBe(expectedPageParams); + }, + ); +}); + describe('getSortKey', () => { it.each(Object.keys(urlSortParams))('returns %s given the correct inputs', (sortKey) => { const sort = urlSortParams[sortKey]; diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js index f147bd67e39..e5155a8e140 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js @@ -1,5 +1,6 @@ -import { GlEmptyState, GlModal } from '@gitlab/ui'; +import { GlEmptyState } from '@gitlab/ui'; import { createLocalVue } from '@vue/test-utils'; +import { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; @@ -10,13 +11,19 @@ import createFlash from '~/flash'; import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue'; import PackagesApp from '~/packages_and_registries/package_registry/components/details/app.vue'; import InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue'; +import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue'; import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue'; import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue'; import { FETCH_PACKAGE_DETAILS_ERROR_MESSAGE, DELETE_PACKAGE_ERROR_MESSAGE, + PACKAGE_TYPE_COMPOSER, + DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, + DELETE_PACKAGE_FILE_ERROR_MESSAGE, } from '~/packages_and_registries/package_registry/constants'; + import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql'; +import destroyPackageFileMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql'; import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql'; import { packageDetailsQuery, @@ -24,6 +31,9 @@ import { emptyPackageDetailsQuery, packageDestroyMutation, packageDestroyMutationError, + packageFiles, + packageDestroyFileMutation, + packageDestroyFileMutationError, } from '../../mock_data'; jest.mock('~/flash'); @@ -50,12 +60,14 @@ describe('PackagesApp', () => { function createComponent({ resolver = jest.fn().mockResolvedValue(packageDetailsQuery()), mutationResolver = jest.fn().mockResolvedValue(packageDestroyMutation()), + fileDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFileMutation()), } = {}) { localVue.use(VueApollo); const requestHandlers = [ [getPackageDetails, resolver], [destroyPackageMutation, mutationResolver], + [destroyPackageFileMutation, fileDeleteMutationResolver], ]; apolloProvider = createMockApollo(requestHandlers); @@ -63,7 +75,15 @@ describe('PackagesApp', () => { localVue, apolloProvider, provide, - stubs: { PackageTitle }, + stubs: { + PackageTitle, + GlModal: { + template: '<div></div>', + methods: { + show: jest.fn(), + }, + }, + }, }); } @@ -72,8 +92,10 @@ describe('PackagesApp', () => { const findPackageHistory = () => wrapper.findComponent(PackageHistory); const findAdditionalMetadata = () => wrapper.findComponent(AdditionalMetadata); const findInstallationCommands = () => wrapper.findComponent(InstallationCommands); - const findDeleteModal = () => wrapper.findComponent(GlModal); + const findDeleteModal = () => wrapper.findByTestId('delete-modal'); const findDeleteButton = () => wrapper.findByTestId('delete-package'); + const findPackageFiles = () => wrapper.findComponent(PackageFiles); + const findDeleteFileModal = () => wrapper.findByTestId('delete-file-modal'); afterEach(() => { wrapper.destroy(); @@ -240,4 +262,104 @@ describe('PackagesApp', () => { }); }); }); + + describe('package files', () => { + it('renders the package files component and has the right props', async () => { + const expectedFile = { ...packageFiles()[0] }; + // eslint-disable-next-line no-underscore-dangle + delete expectedFile.__typename; + createComponent(); + + await waitForPromises(); + + expect(findPackageFiles().exists()).toBe(true); + + expect(findPackageFiles().props('packageFiles')[0]).toMatchObject(expectedFile); + }); + + it('does not render the package files table when the package is composer', async () => { + createComponent({ + resolver: jest + .fn() + .mockResolvedValue(packageDetailsQuery({ packageType: PACKAGE_TYPE_COMPOSER })), + }); + + await waitForPromises(); + + expect(findPackageFiles().exists()).toBe(false); + }); + + describe('deleting a file', () => { + const [fileToDelete] = packageFiles(); + + const doDeleteFile = () => { + findPackageFiles().vm.$emit('delete-file', fileToDelete); + + findDeleteFileModal().vm.$emit('primary'); + + return waitForPromises(); + }; + + it('opens a confirmation modal', async () => { + createComponent(); + + await waitForPromises(); + + findPackageFiles().vm.$emit('delete-file', fileToDelete); + + await nextTick(); + + expect(findDeleteFileModal().exists()).toBe(true); + }); + + it('confirming on the modal deletes the file and shows a success message', async () => { + const resolver = jest.fn().mockResolvedValue(packageDetailsQuery()); + createComponent({ resolver }); + + await waitForPromises(); + + await doDeleteFile(); + + expect(createFlash).toHaveBeenCalledWith( + expect.objectContaining({ + message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, + }), + ); + // we are re-fetching the package details, so we expect the resolver to have been called twice + expect(resolver).toHaveBeenCalledTimes(2); + }); + + describe('errors', () => { + it('shows an error when the mutation request fails', async () => { + createComponent({ fileDeleteMutationResolver: jest.fn().mockRejectedValue() }); + await waitForPromises(); + + await doDeleteFile(); + + expect(createFlash).toHaveBeenCalledWith( + expect.objectContaining({ + message: DELETE_PACKAGE_FILE_ERROR_MESSAGE, + }), + ); + }); + + it('shows an error when the mutation request returns an error payload', async () => { + createComponent({ + fileDeleteMutationResolver: jest + .fn() + .mockResolvedValue(packageDestroyFileMutationError()), + }); + await waitForPromises(); + + await doDeleteFile(); + + expect(createFlash).toHaveBeenCalledWith( + expect.objectContaining({ + message: DELETE_PACKAGE_FILE_ERROR_MESSAGE, + }), + ); + }); + }); + }); + }); }); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js index 75fd5b163fc..042b2026199 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js @@ -1,36 +1,39 @@ import { GlDropdown, GlButton } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; import stubChildren from 'helpers/stub_children'; -import { npmFiles, mavenFiles } from 'jest/packages/mock_data'; -import component from '~/packages_and_registries/package_registry/components/details/package_files.vue'; +import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper'; +import { packageFiles as packageFilesMock } from 'jest/packages_and_registries/package_registry/mock_data'; +import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; describe('Package Files', () => { let wrapper; - const findAllRows = () => wrapper.findAll('[data-testid="file-row"'); - const findFirstRow = () => findAllRows().at(0); - const findSecondRow = () => findAllRows().at(1); - const findFirstRowDownloadLink = () => findFirstRow().find('[data-testid="download-link"]'); - const findFirstRowCommitLink = () => findFirstRow().find('[data-testid="commit-link"]'); - const findSecondRowCommitLink = () => findSecondRow().find('[data-testid="commit-link"]'); - const findFirstRowFileIcon = () => findFirstRow().find(FileIcon); - const findFirstRowCreatedAt = () => findFirstRow().find(TimeAgoTooltip); - const findFirstActionMenu = () => findFirstRow().findComponent(GlDropdown); - const findActionMenuDelete = () => findFirstActionMenu().find('[data-testid="delete-file"]'); + const findAllRows = () => wrapper.findAllByTestId('file-row'); + const findFirstRow = () => extendedWrapper(findAllRows().at(0)); + const findSecondRow = () => extendedWrapper(findAllRows().at(1)); + const findFirstRowDownloadLink = () => findFirstRow().findByTestId('download-link'); + const findFirstRowCommitLink = () => findFirstRow().findByTestId('commit-link'); + const findSecondRowCommitLink = () => findSecondRow().findByTestId('commit-link'); + const findFirstRowFileIcon = () => findFirstRow().findComponent(FileIcon); + const findFirstRowCreatedAt = () => findFirstRow().findComponent(TimeAgoTooltip); + const findFirstActionMenu = () => extendedWrapper(findFirstRow().findComponent(GlDropdown)); + const findActionMenuDelete = () => findFirstActionMenu().findByTestId('delete-file'); const findFirstToggleDetailsButton = () => findFirstRow().findComponent(GlButton); - const findFirstRowShaComponent = (id) => wrapper.find(`[data-testid="${id}"]`); + const findFirstRowShaComponent = (id) => wrapper.findByTestId(id); - const createComponent = ({ packageFiles = npmFiles, canDelete = true } = {}) => { - wrapper = mount(component, { + const files = packageFilesMock(); + const [file] = files; + + const createComponent = ({ packageFiles = [file], canDelete = true } = {}) => { + wrapper = mountExtended(PackageFiles, { + provide: { canDelete }, propsData: { packageFiles, - canDelete, }, stubs: { - ...stubChildren(component), + ...stubChildren(PackageFiles), GlTable: false, }, }); @@ -38,7 +41,6 @@ describe('Package Files', () => { afterEach(() => { wrapper.destroy(); - wrapper = null; }); describe('rows', () => { @@ -49,7 +51,7 @@ describe('Package Files', () => { }); it('renders multiple files for a package that contains more than one file', () => { - createComponent({ packageFiles: mavenFiles }); + createComponent({ packageFiles: files }); expect(findAllRows()).toHaveLength(2); }); @@ -65,7 +67,7 @@ describe('Package Files', () => { it('has the correct attrs bound', () => { createComponent(); - expect(findFirstRowDownloadLink().attributes('href')).toBe(npmFiles[0].download_path); + expect(findFirstRowDownloadLink().attributes('href')).toBe(file.downloadPath); }); it('emits "download-file" event on click', () => { @@ -87,7 +89,7 @@ describe('Package Files', () => { it('has the correct props bound', () => { createComponent(); - expect(findFirstRowFileIcon().props('fileName')).toBe(npmFiles[0].file_name); + expect(findFirstRowFileIcon().props('fileName')).toBe(file.fileName); }); }); @@ -101,35 +103,47 @@ describe('Package Files', () => { it('has the correct props bound', () => { createComponent(); - expect(findFirstRowCreatedAt().props('time')).toBe(npmFiles[0].created_at); + expect(findFirstRowCreatedAt().props('time')).toBe(file.createdAt); }); }); describe('commit', () => { + const withPipeline = { + ...file, + pipelines: [ + { + sha: 'sha', + id: 1, + commitPath: 'commitPath', + }, + ], + }; + describe('when package file has a pipeline associated', () => { it('exists', () => { - createComponent(); + createComponent({ packageFiles: [withPipeline] }); expect(findFirstRowCommitLink().exists()).toBe(true); }); - it('the link points to the commit url', () => { - createComponent(); + it('the link points to the commit path', () => { + createComponent({ packageFiles: [withPipeline] }); expect(findFirstRowCommitLink().attributes('href')).toBe( - npmFiles[0].pipelines[0].project.commit_url, + withPipeline.pipelines[0].commitPath, ); }); - it('the text is git_commit_message', () => { - createComponent(); + it('the text is the pipeline sha', () => { + createComponent({ packageFiles: [withPipeline] }); - expect(findFirstRowCommitLink().text()).toBe(npmFiles[0].pipelines[0].git_commit_message); + expect(findFirstRowCommitLink().text()).toBe(withPipeline.pipelines[0].sha); }); }); + describe('when package file has no pipeline associated', () => { it('does not exist', () => { - createComponent({ packageFiles: mavenFiles }); + createComponent(); expect(findFirstRowCommitLink().exists()).toBe(false); }); @@ -137,7 +151,7 @@ describe('Package Files', () => { describe('when only one file lacks an associated pipeline', () => { it('renders the commit when it exists and not otherwise', () => { - createComponent({ packageFiles: [npmFiles[0], mavenFiles[0]] }); + createComponent({ packageFiles: [withPipeline, file] }); expect(findFirstRowCommitLink().exists()).toBe(true); expect(findSecondRowCommitLink().exists()).toBe(false); @@ -166,7 +180,7 @@ describe('Package Files', () => { findActionMenuDelete().vm.$emit('click'); const [[{ id }]] = wrapper.emitted('delete-file'); - expect(id).toBe(npmFiles[0].id); + expect(id).toBe(file.id); }); }); }); @@ -193,10 +207,10 @@ describe('Package Files', () => { }); it('is hidden when no details is present', () => { - const [{ ...noShaFile }] = npmFiles; - noShaFile.file_sha256 = null; - noShaFile.file_md5 = null; - noShaFile.file_sha1 = null; + const { ...noShaFile } = file; + noShaFile.fileSha256 = null; + noShaFile.fileMd5 = null; + noShaFile.fileSha1 = null; createComponent({ packageFiles: [noShaFile] }); expect(findFirstToggleDetailsButton().exists()).toBe(false); @@ -229,9 +243,9 @@ describe('Package Files', () => { it.each` selector | title | sha - ${'sha-256'} | ${'SHA-256'} | ${'file_sha256'} - ${'md5'} | ${'MD5'} | ${'file_md5'} - ${'sha-1'} | ${'SHA-1'} | ${'file_sha1'} + ${'sha-256'} | ${'SHA-256'} | ${'fileSha256'} + ${'md5'} | ${'MD5'} | ${'fileMd5'} + ${'sha-1'} | ${'SHA-1'} | ${'be93151dc23ac34a82752444556fe79b32c7a1ad'} `('has a $title row', async ({ selector, title, sha }) => { createComponent(); @@ -244,8 +258,8 @@ describe('Package Files', () => { }); it('does not display a row when the data is missing', async () => { - const [{ ...missingMd5 }] = npmFiles; - missingMd5.file_md5 = null; + const { ...missingMd5 } = file; + missingMd5.fileMd5 = null; createComponent({ packageFiles: [missingMd5] }); diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js index 110cb5a3798..5cf81e5cd09 100644 --- a/spec/frontend/packages_and_registries/package_registry/mock_data.js +++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js @@ -29,11 +29,13 @@ export const packagePipelines = (extend) => [ export const packageFiles = () => [ { id: 'gid://gitlab/Packages::PackageFile/118', - fileMd5: null, + fileMd5: 'fileMd5', fileName: 'foo-1.0.1.tgz', fileSha1: 'be93151dc23ac34a82752444556fe79b32c7a1ad', - fileSha256: null, + fileSha256: 'fileSha256', size: '409600', + createdAt: '2020-08-17T14:23:32Z', + downloadPath: 'downloadPath', __typename: 'PackageFile', }, { @@ -43,6 +45,8 @@ export const packageFiles = () => [ fileSha1: 'be93151dc23ac34a82752444556fe79b32c7a1ss', fileSha256: null, size: '409600', + createdAt: '2020-08-17T14:23:32Z', + downloadPath: 'downloadPath', __typename: 'PackageFile', }, ]; @@ -90,7 +94,7 @@ export const nugetMetadata = () => ({ projectUrl: 'projectUrl', }); -export const packageDetailsQuery = () => ({ +export const packageDetailsQuery = (extendPackage) => ({ data: { package: { ...packageData(), @@ -114,6 +118,7 @@ export const packageDetailsQuery = () => ({ __typename: 'PackageFileConnection', }, __typename: 'PackageDetailsType', + ...extendPackage, }, }, }); @@ -133,6 +138,7 @@ export const packageDestroyMutation = () => ({ }, }, }); + export const packageDestroyMutationError = () => ({ data: { destroyPackage: null, @@ -151,3 +157,29 @@ export const packageDestroyMutationError = () => ({ }, ], }); + +export const packageDestroyFileMutation = () => ({ + data: { + destroyPackageFile: { + errors: [], + }, + }, +}); +export const packageDestroyFileMutationError = () => ({ + data: { + destroyPackageFile: null, + }, + errors: [ + { + message: + "The resource that you are attempting to access does not exist or you don't have permission to perform this action", + locations: [ + { + line: 2, + column: 3, + }, + ], + path: ['destroyPackageFile'], + }, + ], +}); diff --git a/spec/lib/gitlab/data_builder/deployment_spec.rb b/spec/lib/gitlab/data_builder/deployment_spec.rb index d64dfc957ca..75741c52579 100644 --- a/spec/lib/gitlab/data_builder/deployment_spec.rb +++ b/spec/lib/gitlab/data_builder/deployment_spec.rb @@ -27,6 +27,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do expect(data[:status]).to eq('failed') expect(data[:status_changed_at]).to eq(status_changed_at) + expect(data[:deployment_id]).to eq(deployment.id) expect(data[:deployable_id]).to eq(deployable.id) expect(data[:deployable_url]).to eq(expected_deployable_url) expect(data[:environment]).to eq("somewhere") diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index 4c59bda856f..4158205bbd7 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -139,4 +139,161 @@ RSpec.describe ProjectMember do end end end + + context 'refreshing project_authorizations' do + let_it_be_with_refind(:project) { create(:project) } + let_it_be_with_refind(:user) { create(:user) } + let_it_be(:project_member) { create(:project_member, :guest, project: project, user: user) } + + context 'when the source project of the project member is destroyed' do + it 'refreshes the authorization of user to the project in the group' do + expect { project.destroy! }.to change { user.can?(:guest_access, project) }.from(true).to(false) + end + + it 'refreshes the authorization without calling AuthorizedProjectUpdate::ProjectRecalculatePerUserService' do + expect(AuthorizedProjectUpdate::ProjectRecalculatePerUserService).not_to receive(:new) + + project.destroy! + end + end + + context 'when the user of the project member is destroyed' do + it 'refreshes the authorization of user to the project in the group' do + expect(project.authorized_users).to include(user) + + user.destroy! + + expect(project.authorized_users).not_to include(user) + end + + it 'refreshes the authorization without calling UserProjectAccessChangedService' do + expect(UserProjectAccessChangedService).not_to receive(:new) + + user.destroy! + end + end + end + + context 'authorization refresh on addition/updation/deletion' do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + shared_examples_for 'calls AuthorizedProjectUpdate::ProjectRecalculatePerUserService to recalculate authorizations' do + it 'calls AuthorizedProjectUpdate::ProjectRecalculatePerUserService' do + expect_next_instance_of(AuthorizedProjectUpdate::ProjectRecalculatePerUserService, project, user) do |service| + expect(service).to receive(:execute) + end + + action + end + end + + shared_examples_for 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do + it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker' do + expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to( + receive(:bulk_perform_in) + .with(1.hour, + [[user.id]], + batch_delay: 30.seconds, batch_size: 100) + ) + + action + end + end + + context 'on create' do + let(:action) { project.add_user(user, Gitlab::Access::GUEST) } + + it 'changes access level' do + expect { action }.to change { user.can?(:guest_access, project) }.from(false).to(true) + end + + it_behaves_like 'calls AuthorizedProjectUpdate::ProjectRecalculatePerUserService to recalculate authorizations' + it_behaves_like 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' + end + + context 'on update' do + let(:action) { project.members.find_by(user: user).update!(access_level: Gitlab::Access::DEVELOPER) } + + before do + project.add_user(user, Gitlab::Access::GUEST) + end + + it 'changes access level' do + expect { action }.to change { user.can?(:developer_access, project) }.from(false).to(true) + end + + it_behaves_like 'calls AuthorizedProjectUpdate::ProjectRecalculatePerUserService to recalculate authorizations' + it_behaves_like 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' + end + + context 'on destroy' do + let(:action) { project.members.find_by(user: user).destroy! } + + before do + project.add_user(user, Gitlab::Access::GUEST) + end + + it 'changes access level' do + expect { action }.to change { user.can?(:guest_access, project) }.from(true).to(false) + end + + it_behaves_like 'calls AuthorizedProjectUpdate::ProjectRecalculatePerUserService to recalculate authorizations' + it_behaves_like 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' + end + + context 'when the feature flag `specialized_service_for_project_member_auth_refresh` is disabled' do + before do + stub_feature_flags(specialized_service_for_project_member_auth_refresh: false) + end + + shared_examples_for 'calls UserProjectAccessChangedService to recalculate authorizations' do + it 'calls UserProjectAccessChangedService' do + expect_next_instance_of(UserProjectAccessChangedService, user.id) do |service| + expect(service).to receive(:execute) + end + + action + end + end + + context 'on create' do + let(:action) { project.add_user(user, Gitlab::Access::GUEST) } + + it 'changes access level' do + expect { action }.to change { user.can?(:guest_access, project) }.from(false).to(true) + end + + it_behaves_like 'calls UserProjectAccessChangedService to recalculate authorizations' + end + + context 'on update' do + let(:action) { project.members.find_by(user: user).update!(access_level: Gitlab::Access::DEVELOPER) } + + before do + project.add_user(user, Gitlab::Access::GUEST) + end + + it 'changes access level' do + expect { action }.to change { user.can?(:developer_access, project) }.from(false).to(true) + end + + it_behaves_like 'calls UserProjectAccessChangedService to recalculate authorizations' + end + + context 'on destroy' do + let(:action) { project.members.find_by(user: user).destroy! } + + before do + project.add_user(user, Gitlab::Access::GUEST) + end + + it 'changes access level' do + expect { action }.to change { user.can?(:guest_access, project) }.from(true).to(false) + end + + it_behaves_like 'calls UserProjectAccessChangedService to recalculate authorizations' + end + end + end end diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb index 606279ec20a..8c9a93cf9fa 100644 --- a/spec/requests/api/project_milestones_spec.rb +++ b/spec/requests/api/project_milestones_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe API::ProjectMilestones do let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project, namespace: user.namespace ) } + let_it_be_with_reload(:project) { create(:project, namespace: user.namespace ) } let_it_be(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') } let_it_be(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') } let_it_be(:route) { "/projects/#{project.id}/milestones" } diff --git a/spec/services/ci/drop_pipeline_service_spec.rb b/spec/services/ci/drop_pipeline_service_spec.rb index 4adbb99b9e2..c6a118c6083 100644 --- a/spec/services/ci/drop_pipeline_service_spec.rb +++ b/spec/services/ci/drop_pipeline_service_spec.rb @@ -9,8 +9,10 @@ RSpec.describe Ci::DropPipelineService do let!(:cancelable_pipeline) { create(:ci_pipeline, :running, user: user) } let!(:running_build) { create(:ci_build, :running, pipeline: cancelable_pipeline) } + let!(:commit_status_running) { create(:commit_status, :running, pipeline: cancelable_pipeline) } let!(:success_pipeline) { create(:ci_pipeline, :success, user: user) } let!(:success_build) { create(:ci_build, :success, pipeline: success_pipeline) } + let!(:commit_status_success) { create(:commit_status, :success, pipeline: cancelable_pipeline) } describe '#execute_async_for_all' do subject { described_class.new.execute_async_for_all(user.pipelines, failure_reason, user) } diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 4e2a056a641..3c4d7d50002 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -7,7 +7,7 @@ RSpec.describe NotificationService, :mailer do include ExternalAuthorizationServiceHelpers include NotificationHelpers - let_it_be(:project, reload: true) { create(:project, :public) } + let_it_be_with_refind(:project) { create(:project, :public) } let_it_be_with_refind(:assignee) { create(:user) } let(:notification) { described_class.new } diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 92f9c2356cd..c3928563125 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -135,7 +135,7 @@ RSpec.describe Projects::CreateService, '#execute' do end it_behaves_like 'storing arguments in the application context' do - let(:expected_params) { { project: subject.full_path, related_class: described_class.to_s } } + let(:expected_params) { { project: subject.full_path } } subject { create_project(user, opts) } end |