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>2021-08-09 18:09:13 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-08-09 18:09:13 +0300
commit9c39a0a9b81f06f6345a6b6e071b8e8cd249c064 (patch)
tree45caaf45fac61866d8cc9098487a78b17c1016a0 /spec
parent53af44b90f87cdd8d7126d64669848b0e2be5960 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/frontend/issues_list/components/issues_list_app_spec.js40
-rw-r--r--spec/frontend/issues_list/utils_spec.js21
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js128
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js98
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js38
-rw-r--r--spec/lib/gitlab/data_builder/deployment_spec.rb1
-rw-r--r--spec/models/members/project_member_spec.rb157
-rw-r--r--spec/requests/api/project_milestones_spec.rb2
-rw-r--r--spec/services/ci/drop_pipeline_service_spec.rb2
-rw-r--r--spec/services/notification_service_spec.rb2
-rw-r--r--spec/services/projects/create_service_spec.rb2
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