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:
Diffstat (limited to 'spec')
-rw-r--r--spec/frontend/pipeline_new/components/pipeline_new_form_spec.js242
-rw-r--r--spec/frontend/pipeline_new/mock_data.js59
-rw-r--r--spec/frontend/pipeline_schedules/components/pipeline_schedules_spec.js102
-rw-r--r--spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js15
-rw-r--r--spec/frontend/pipeline_schedules/mock_data.js11
-rw-r--r--spec/helpers/markup_helper_spec.rb60
-rw-r--r--spec/lib/banzai/filter/truncate_visible_filter_spec.rb128
-rw-r--r--spec/migrations/adjust_task_note_rename_background_migration_values_spec.rb143
-rw-r--r--spec/models/member_spec.rb8
-rw-r--r--spec/models/members/member_role_spec.rb34
-rw-r--r--spec/policies/project_policy_spec.rb44
-rw-r--r--spec/requests/api/graphql/ci/ci_cd_setting_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb47
13 files changed, 700 insertions, 195 deletions
diff --git a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
index 5ce29bd6c5d..3e699b93fd3 100644
--- a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
+++ b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
@@ -1,72 +1,101 @@
-import { GlForm, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlForm, GlDropdownItem, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
import CreditCardValidationRequiredAlert from 'ee_component/billings/components/cc_validation_required_alert.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import { redirectTo } from '~/lib/utils/url_utility';
import PipelineNewForm from '~/pipeline_new/components/pipeline_new_form.vue';
+import ciConfigVariablesQuery from '~/pipeline_new/graphql/queries/ci_config_variables.graphql';
+import { resolvers } from '~/pipeline_new/graphql/resolvers';
import RefsDropdown from '~/pipeline_new/components/refs_dropdown.vue';
import {
+ mockCreditCardValidationRequiredError,
+ mockCiConfigVariablesResponse,
+ mockCiConfigVariablesResponseWithoutDesc,
+ mockEmptyCiConfigVariablesResponse,
+ mockError,
mockQueryParams,
mockPostParams,
mockProjectId,
- mockError,
mockRefs,
- mockCreditCardValidationRequiredError,
+ mockYamlVariables,
} from '../mock_data';
+Vue.use(VueApollo);
+
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
}));
const projectRefsEndpoint = '/root/project/refs';
const pipelinesPath = '/root/project/-/pipelines';
-const configVariablesPath = '/root/project/-/pipelines/config_variables';
+const projectPath = '/root/project/-/pipelines/config_variables';
const newPipelinePostResponse = { id: 1 };
const defaultBranch = 'main';
describe('Pipeline New Form', () => {
let wrapper;
let mock;
+ let mockApollo;
+ let mockCiConfigVariables;
let dummySubmitEvent;
const findForm = () => wrapper.findComponent(GlForm);
const findRefsDropdown = () => wrapper.findComponent(RefsDropdown);
- const findSubmitButton = () => wrapper.find('[data-testid="run_pipeline_button"]');
- const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]');
- const findRemoveIcons = () => wrapper.findAll('[data-testid="remove-ci-variable-row"]');
- const findDropdowns = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-type"]');
- const findKeyInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-key"]');
- const findValueInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-value"]');
- const findErrorAlert = () => wrapper.find('[data-testid="run-pipeline-error-alert"]');
- const findWarningAlert = () => wrapper.find('[data-testid="run-pipeline-warning-alert"]');
+ const findSubmitButton = () => wrapper.findByTestId('run_pipeline_button');
+ const findVariableRows = () => wrapper.findAllByTestId('ci-variable-row');
+ const findRemoveIcons = () => wrapper.findAllByTestId('remove-ci-variable-row');
+ const findVariableTypes = () => wrapper.findAllByTestId('pipeline-form-ci-variable-type');
+ const findKeyInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-key');
+ const findValueInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-value');
+ const findValueDropdowns = () =>
+ wrapper.findAllByTestId('pipeline-form-ci-variable-value-dropdown');
+ const findValueDropdownItems = (dropdown) => dropdown.findAllComponents(GlDropdownItem);
+ const findErrorAlert = () => wrapper.findByTestId('run-pipeline-error-alert');
+ const findWarningAlert = () => wrapper.findByTestId('run-pipeline-warning-alert');
const findWarningAlertSummary = () => findWarningAlert().findComponent(GlSprintf);
- const findWarnings = () => wrapper.findAll('[data-testid="run-pipeline-warning"]');
+ const findWarnings = () => wrapper.findAllByTestId('run-pipeline-warning');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findCCAlert = () => wrapper.findComponent(CreditCardValidationRequiredAlert);
const getFormPostParams = () => JSON.parse(mock.history.post[0].data);
- const selectBranch = (branch) => {
+ const selectBranch = async (branch) => {
// Select a branch in the dropdown
findRefsDropdown().vm.$emit('input', {
shortName: branch,
fullName: `refs/heads/${branch}`,
});
+
+ await waitForPromises();
+ };
+
+ const changeKeyInputValue = async (keyInputIndex, value) => {
+ const input = findKeyInputs().at(keyInputIndex);
+ input.element.value = value;
+ input.trigger('change');
+
+ await nextTick();
};
- const createComponent = (props = {}, method = shallowMount) => {
+ const createComponentWithApollo = ({ method = shallowMountExtended, props = {} } = {}) => {
+ const handlers = [[ciConfigVariablesQuery, mockCiConfigVariables]];
+ mockApollo = createMockApollo(handlers, resolvers);
+
wrapper = method(PipelineNewForm, {
+ apolloProvider: mockApollo,
provide: {
projectRefsEndpoint,
},
propsData: {
projectId: mockProjectId,
pipelinesPath,
- configVariablesPath,
+ projectPath,
defaultBranch,
refParam: defaultBranch,
settingsLink: '',
@@ -78,7 +107,7 @@ describe('Pipeline New Form', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {});
+ mockCiConfigVariables = jest.fn();
mock.onGet(projectRefsEndpoint).reply(httpStatusCodes.OK, mockRefs);
dummySubmitEvent = {
@@ -87,24 +116,20 @@ describe('Pipeline New Form', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
-
mock.restore();
+ wrapper.destroy();
});
describe('Form', () => {
beforeEach(async () => {
- createComponent(mockQueryParams, mount);
-
- mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse);
-
+ mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
+ createComponentWithApollo({ props: mockQueryParams, method: mountExtended });
await waitForPromises();
});
it('displays the correct values for the provided query params', async () => {
- expect(findDropdowns().at(0).props('text')).toBe('Variable');
- expect(findDropdowns().at(1).props('text')).toBe('File');
+ expect(findVariableTypes().at(0).props('text')).toBe('Variable');
+ expect(findVariableTypes().at(1).props('text')).toBe('File');
expect(findRefsDropdown().props('value')).toEqual({ shortName: 'tag-1' });
expect(findVariableRows()).toHaveLength(3);
});
@@ -117,7 +142,7 @@ describe('Pipeline New Form', () => {
it('displays an empty variable for the user to fill out', async () => {
expect(findKeyInputs().at(2).element.value).toBe('');
expect(findValueInputs().at(2).element.value).toBe('');
- expect(findDropdowns().at(2).props('text')).toBe('Variable');
+ expect(findVariableTypes().at(2).props('text')).toBe('Variable');
});
it('does not display remove icon for last row', () => {
@@ -147,13 +172,12 @@ describe('Pipeline New Form', () => {
describe('Pipeline creation', () => {
beforeEach(async () => {
+ mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse);
-
- await waitForPromises();
});
it('does not submit the native HTML form', async () => {
- createComponent();
+ createComponentWithApollo();
findForm().vm.$emit('submit', dummySubmitEvent);
@@ -161,7 +185,7 @@ describe('Pipeline New Form', () => {
});
it('disables the submit button immediately after submitting', async () => {
- createComponent();
+ createComponentWithApollo();
expect(findSubmitButton().props('disabled')).toBe(false);
@@ -172,7 +196,7 @@ describe('Pipeline New Form', () => {
});
it('creates pipeline with full ref and variables', async () => {
- createComponent();
+ createComponentWithApollo();
findForm().vm.$emit('submit', dummySubmitEvent);
await waitForPromises();
@@ -182,7 +206,7 @@ describe('Pipeline New Form', () => {
});
it('creates a pipeline with short ref and variables from the query params', async () => {
- createComponent(mockQueryParams);
+ createComponentWithApollo({ props: mockQueryParams });
await waitForPromises();
@@ -197,64 +221,51 @@ describe('Pipeline New Form', () => {
describe('When the ref has been changed', () => {
beforeEach(async () => {
- createComponent({}, mount);
+ mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
+ createComponentWithApollo({ method: mountExtended });
await waitForPromises();
});
- it('variables persist between ref changes', async () => {
- selectBranch('main');
-
- await waitForPromises();
- const mainInput = findKeyInputs().at(0);
- mainInput.element.value = 'build_var';
- mainInput.trigger('change');
+ it('variables persist between ref changes', async () => {
+ await selectBranch('main');
+ await changeKeyInputValue(0, 'build_var');
- await nextTick();
+ await selectBranch('branch-1');
+ await changeKeyInputValue(0, 'deploy_var');
- selectBranch('branch-1');
+ await selectBranch('main');
- await waitForPromises();
+ expect(findKeyInputs().at(0).element.value).toBe('build_var');
+ expect(findVariableRows().length).toBe(2);
- const branchOneInput = findKeyInputs().at(0);
- branchOneInput.element.value = 'deploy_var';
- branchOneInput.trigger('change');
+ await selectBranch('branch-1');
- await nextTick();
+ expect(findKeyInputs().at(0).element.value).toBe('deploy_var');
+ expect(findVariableRows().length).toBe(2);
+ });
- selectBranch('main');
+ it('skips query call when form variables are already cached', async () => {
+ await selectBranch('main');
+ await changeKeyInputValue(0, 'build_var');
- await waitForPromises();
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(1);
- expect(findKeyInputs().at(0).element.value).toBe('build_var');
- expect(findVariableRows().length).toBe(2);
+ await selectBranch('branch-1');
- selectBranch('branch-1');
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(2);
- await waitForPromises();
+ // no additional call since `main` form values have been cached
+ await selectBranch('main');
- expect(findKeyInputs().at(0).element.value).toBe('deploy_var');
- expect(findVariableRows().length).toBe(2);
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(2);
});
});
describe('when yml defines a variable', () => {
- const mockYmlKey = 'yml_var';
- const mockYmlValue = 'yml_var_val';
- const mockYmlMultiLineValue = `A value
- with multiple
- lines`;
- const mockYmlDesc = 'A var from yml.';
-
it('loading icon is shown when content is requested and hidden when received', async () => {
- createComponent(mockQueryParams, mount);
-
- mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
- [mockYmlKey]: {
- value: mockYmlValue,
- description: mockYmlDesc,
- },
- });
+ mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
+ createComponentWithApollo({ props: mockQueryParams, method: mountExtended });
expect(findLoadingIcon().exists()).toBe(true);
@@ -263,51 +274,62 @@ describe('Pipeline New Form', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
- it('multi-line strings are added to the value field without removing line breaks', async () => {
- createComponent(mockQueryParams, mount);
+ describe('with different predefined values', () => {
+ beforeEach(async () => {
+ mockCiConfigVariables.mockResolvedValue(mockCiConfigVariablesResponse);
+ createComponentWithApollo({ method: mountExtended });
+ await waitForPromises();
+ });
+
+ it('multi-line strings are added to the value field without removing line breaks', () => {
+ expect(findValueInputs().at(1).element.value).toBe(mockYamlVariables[1].value);
+ });
- mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
- [mockYmlKey]: {
- value: mockYmlMultiLineValue,
- description: mockYmlDesc,
- },
+ it('multiple predefined values are rendered as a dropdown', () => {
+ const dropdown = findValueDropdowns().at(0);
+ const dropdownItems = findValueDropdownItems(dropdown);
+ const { valueOptions } = mockYamlVariables[2];
+
+ expect(dropdownItems.at(0).text()).toBe(valueOptions[0]);
+ expect(dropdownItems.at(1).text()).toBe(valueOptions[1]);
+ expect(dropdownItems.at(2).text()).toBe(valueOptions[2]);
});
- await waitForPromises();
+ it('variables with multiple predefined values sets the first option as the default', () => {
+ const dropdown = findValueDropdowns().at(0);
+ const { valueOptions } = mockYamlVariables[2];
- expect(findValueInputs().at(0).element.value).toBe(mockYmlMultiLineValue);
+ expect(dropdown.props('text')).toBe(valueOptions[0]);
+ });
});
describe('with description', () => {
beforeEach(async () => {
- createComponent(mockQueryParams, mount);
-
- mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
- [mockYmlKey]: {
- value: mockYmlValue,
- description: mockYmlDesc,
- },
- });
-
+ mockCiConfigVariables.mockResolvedValue(mockCiConfigVariablesResponse);
+ createComponentWithApollo({ props: mockQueryParams, method: mountExtended });
await waitForPromises();
});
it('displays all the variables', async () => {
- expect(findVariableRows()).toHaveLength(4);
+ expect(findVariableRows()).toHaveLength(6);
});
it('displays a variable from yml', () => {
- expect(findKeyInputs().at(0).element.value).toBe(mockYmlKey);
- expect(findValueInputs().at(0).element.value).toBe(mockYmlValue);
+ expect(findKeyInputs().at(0).element.value).toBe(mockYamlVariables[0].key);
+ expect(findValueInputs().at(0).element.value).toBe(mockYamlVariables[0].value);
});
it('displays a variable from provided query params', () => {
- expect(findKeyInputs().at(1).element.value).toBe('test_var');
- expect(findValueInputs().at(1).element.value).toBe('test_var_val');
+ expect(findKeyInputs().at(3).element.value).toBe(
+ Object.keys(mockQueryParams.variableParams)[0],
+ );
+ expect(findValueInputs().at(3).element.value).toBe(
+ Object.values(mockQueryParams.fileParams)[0],
+ );
});
it('adds a description to the first variable from yml', () => {
- expect(findVariableRows().at(0).text()).toContain(mockYmlDesc);
+ expect(findVariableRows().at(0).text()).toContain(mockYamlVariables[0].description);
});
it('removes the description when a variable key changes', async () => {
@@ -316,39 +338,27 @@ describe('Pipeline New Form', () => {
await nextTick();
- expect(findVariableRows().at(0).text()).not.toContain(mockYmlDesc);
+ expect(findVariableRows().at(0).text()).not.toContain(mockYamlVariables[0].description);
});
});
describe('without description', () => {
beforeEach(async () => {
- createComponent(mockQueryParams, mount);
-
- mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
- [mockYmlKey]: {
- value: mockYmlValue,
- description: null,
- },
- yml_var2: {
- value: 'yml_var2_val',
- },
- yml_var3: {
- description: '',
- },
- });
-
+ mockCiConfigVariables.mockResolvedValue(mockCiConfigVariablesResponseWithoutDesc);
+ createComponentWithApollo({ method: mountExtended });
await waitForPromises();
});
- it('displays all the variables', async () => {
- expect(findVariableRows()).toHaveLength(3);
+ it('displays variables with description only', async () => {
+ expect(findVariableRows()).toHaveLength(2); // extra empty variable is added at the end
});
});
});
describe('Form errors and warnings', () => {
beforeEach(() => {
- createComponent();
+ mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
+ createComponentWithApollo();
});
describe('when the refs cannot be loaded', () => {
diff --git a/spec/frontend/pipeline_new/mock_data.js b/spec/frontend/pipeline_new/mock_data.js
index e99684ff417..e95a65171fc 100644
--- a/spec/frontend/pipeline_new/mock_data.js
+++ b/spec/frontend/pipeline_new/mock_data.js
@@ -65,3 +65,62 @@ export const mockVariables = [
},
{ uniqueId: 'var-refs/heads/main4', variable_type: 'env_var', key: '', value: '' },
];
+
+export const mockYamlVariables = [
+ {
+ description: 'This is a variable with a value.',
+ key: 'VAR_WITH_VALUE',
+ value: 'test_value',
+ valueOptions: null,
+ },
+ {
+ description: 'This is a variable with a multi-line value.',
+ key: 'VAR_WITH_MULTILINE',
+ value: `this is
+ a multiline value`,
+ valueOptions: null,
+ },
+ {
+ description: 'This is a variable with predefined values.',
+ key: 'VAR_WITH_OPTIONS',
+ value: 'development',
+ valueOptions: ['development', 'staging', 'production'],
+ },
+];
+
+export const mockYamlVariablesWithoutDesc = [
+ {
+ description: 'This is a variable with a value.',
+ key: 'VAR_WITH_VALUE',
+ value: 'test_value',
+ valueOptions: null,
+ },
+ {
+ description: null,
+ key: 'VAR_WITH_MULTILINE',
+ value: `this is
+ a multiline value`,
+ valueOptions: null,
+ },
+ {
+ description: null,
+ key: 'VAR_WITH_OPTIONS',
+ value: 'development',
+ valueOptions: ['development', 'staging', 'production'],
+ },
+];
+
+export const mockCiConfigVariablesQueryResponse = (ciConfigVariables) => ({
+ data: {
+ project: {
+ id: 1,
+ ciConfigVariables,
+ },
+ },
+});
+
+export const mockCiConfigVariablesResponse = mockCiConfigVariablesQueryResponse(mockYamlVariables);
+export const mockEmptyCiConfigVariablesResponse = mockCiConfigVariablesQueryResponse([]);
+export const mockCiConfigVariablesResponseWithoutDesc = mockCiConfigVariablesQueryResponse(
+ mockYamlVariablesWithoutDesc,
+);
diff --git a/spec/frontend/pipeline_schedules/components/pipeline_schedules_spec.js b/spec/frontend/pipeline_schedules/components/pipeline_schedules_spec.js
index d0292c65bcd..cce8f480928 100644
--- a/spec/frontend/pipeline_schedules/components/pipeline_schedules_spec.js
+++ b/spec/frontend/pipeline_schedules/components/pipeline_schedules_spec.js
@@ -1,13 +1,18 @@
-import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PipelineSchedules from '~/pipeline_schedules/components/pipeline_schedules.vue';
import PipelineSchedulesTable from '~/pipeline_schedules/components/table/pipeline_schedules_table.vue';
+import deletePipelineScheduleMutation from '~/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql';
import getPipelineSchedulesQuery from '~/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql';
-import { mockGetPipelineSchedulesGraphQLResponse, mockPipelineScheduleNodes } from '../mock_data';
+import {
+ mockGetPipelineSchedulesGraphQLResponse,
+ mockPipelineScheduleNodes,
+ deleteMutationResponse,
+} from '../mock_data';
Vue.use(VueApollo);
@@ -17,24 +22,28 @@ describe('Pipeline schedules app', () => {
const successHandler = jest.fn().mockResolvedValue(mockGetPipelineSchedulesGraphQLResponse);
const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
- const createMockApolloProvider = (handler) => {
- const requestHandlers = [[getPipelineSchedulesQuery, handler]];
+ const deleteMutationHandlerSuccess = jest.fn().mockResolvedValue(deleteMutationResponse);
+ const deleteMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+ const createMockApolloProvider = (
+ requestHandlers = [[getPipelineSchedulesQuery, successHandler]],
+ ) => {
return createMockApollo(requestHandlers);
};
- const createComponent = (handler = successHandler) => {
+ const createComponent = (requestHandlers) => {
wrapper = shallowMount(PipelineSchedules, {
provide: {
fullPath: 'gitlab-org/gitlab',
},
- apolloProvider: createMockApolloProvider(handler),
+ apolloProvider: createMockApolloProvider(requestHandlers),
});
};
const findTable = () => wrapper.findComponent(PipelineSchedulesTable);
const findAlert = () => wrapper.findComponent(GlAlert);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findModal = () => wrapper.findComponent(GlModal);
afterEach(() => {
wrapper.destroy();
@@ -69,11 +78,84 @@ describe('Pipeline schedules app', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
- it('shows error alert', async () => {
- createComponent(failedHandler);
+ it('shows query error alert', async () => {
+ createComponent([[getPipelineSchedulesQuery, failedHandler]]);
await waitForPromises();
- expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe('There was a problem fetching pipeline schedules.');
+ });
+
+ it('shows delete mutation error alert', async () => {
+ createComponent([
+ [getPipelineSchedulesQuery, successHandler],
+ [deletePipelineScheduleMutation, deleteMutationHandlerFailed],
+ ]);
+
+ await waitForPromises();
+
+ findModal().vm.$emit('primary');
+
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe('There was a problem deleting the pipeline schedule.');
+ });
+
+ it('deletes pipeline schedule and refetches query', async () => {
+ createComponent([
+ [getPipelineSchedulesQuery, successHandler],
+ [deletePipelineScheduleMutation, deleteMutationHandlerSuccess],
+ ]);
+
+ jest.spyOn(wrapper.vm.$apollo.queries.schedules, 'refetch');
+
+ await waitForPromises();
+
+ const scheduleId = mockPipelineScheduleNodes[0].id;
+
+ findTable().vm.$emit('showDeleteModal', scheduleId);
+
+ expect(wrapper.vm.$apollo.queries.schedules.refetch).not.toHaveBeenCalled();
+
+ findModal().vm.$emit('primary');
+
+ await waitForPromises();
+
+ expect(deleteMutationHandlerSuccess).toHaveBeenCalledWith({
+ id: scheduleId,
+ });
+ expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalled();
+ });
+
+ it('modal should be visible after event', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findModal().props('visible')).toBe(false);
+
+ findTable().vm.$emit('showDeleteModal', mockPipelineScheduleNodes[0].id);
+
+ await nextTick();
+
+ expect(findModal().props('visible')).toBe(true);
+ });
+
+ it('modal should be hidden', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ findTable().vm.$emit('showDeleteModal', mockPipelineScheduleNodes[0].id);
+
+ await nextTick();
+
+ expect(findModal().props('visible')).toBe(true);
+
+ findModal().vm.$emit('hide');
+
+ await nextTick();
+
+ expect(findModal().props('visible')).toBe(false);
});
});
diff --git a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js b/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
index 8f51269f8ab..ecc1bdeb679 100644
--- a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
+++ b/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
@@ -1,5 +1,5 @@
import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PipelineScheduleActions from '~/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue';
import { mockPipelineScheduleNodes, mockPipelineScheduleAsGuestNodes } from '../../../mock_data';
@@ -11,7 +11,7 @@ describe('Pipeline schedule actions', () => {
};
const createComponent = (props = defaultProps) => {
- wrapper = shallowMount(PipelineScheduleActions, {
+ wrapper = shallowMountExtended(PipelineScheduleActions, {
propsData: {
...props,
},
@@ -19,6 +19,7 @@ describe('Pipeline schedule actions', () => {
};
const findAllButtons = () => wrapper.findAllComponents(GlButton);
+ const findDeleteBtn = () => wrapper.findByTestId('delete-pipeline-schedule-btn');
afterEach(() => {
wrapper.destroy();
@@ -35,4 +36,14 @@ describe('Pipeline schedule actions', () => {
expect(findAllButtons()).toHaveLength(0);
});
+
+ it('delete button emits showDeleteModal event and schedule id', () => {
+ createComponent();
+
+ findDeleteBtn().vm.$emit('click');
+
+ expect(wrapper.emitted()).toEqual({
+ showDeleteModal: [[mockPipelineScheduleNodes[0].id]],
+ });
+ });
});
diff --git a/spec/frontend/pipeline_schedules/mock_data.js b/spec/frontend/pipeline_schedules/mock_data.js
index b551b4c529d..0a60998d8fb 100644
--- a/spec/frontend/pipeline_schedules/mock_data.js
+++ b/spec/frontend/pipeline_schedules/mock_data.js
@@ -1,3 +1,4 @@
+// Fixture located at spec/frontend/fixtures/pipeline_schedules.rb
import mockGetPipelineSchedulesGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.json';
import mockGetPipelineSchedulesAsGuestGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.as_guest.json';
@@ -21,4 +22,14 @@ export const mockPipelineScheduleNodes = nodes;
export const mockPipelineScheduleAsGuestNodes = guestNodes;
+export const deleteMutationResponse = {
+ data: {
+ pipelineScheduleDelete: {
+ clientMutationId: null,
+ errors: [],
+ __typename: 'PipelineScheduleDeletePayload',
+ },
+ },
+};
+
export { mockGetPipelineSchedulesGraphQLResponse };
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index 5b16a8fbb40..a2e34471324 100644
--- a/spec/helpers/markup_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -562,20 +562,6 @@ FooBar
shared_examples_for 'common markdown examples' do
let(:project_base) { build(:project, :repository) }
- it 'displays inline code' do
- object = create_object('Text with `inline code`')
- expected = 'Text with <code>inline code</code>'
-
- expect(first_line_in_markdown(object, attribute, 100, project: project)).to match(expected)
- end
-
- it 'truncates the text with multiple paragraphs' do
- object = create_object("Paragraph 1\n\nParagraph 2")
- expected = 'Paragraph 1...'
-
- expect(first_line_in_markdown(object, attribute, 100, project: project)).to match(expected)
- end
-
it 'displays the first line of a code block' do
object = create_object("```\nCode block\nwith two lines\n```")
expected = %r{<pre.+><code><span class="line">Code block\.\.\.</span>\n</code></pre>}
@@ -591,18 +577,6 @@ FooBar
expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(expected)
end
- it 'preserves a link href when link text is truncated' do
- text = 'The quick brown fox jumped over the lazy dog' # 44 chars
- link_url = 'http://example.com/foo/bar/baz' # 30 chars
- input = "#{text}#{text}#{text} #{link_url}" # 163 chars
- expected_link_text = 'http://example...</a>'
-
- object = create_object(input)
-
- expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(link_url)
- expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(expected_link_text)
- end
-
it 'preserves code color scheme' do
object = create_object("```ruby\ndef test\n 'hello world'\nend\n```")
expected = "\n<pre class=\"code highlight js-syntax-highlight language-ruby\">" \
@@ -669,40 +643,6 @@ FooBar
expect(result).to include(html)
end
- it 'truncates Markdown properly' do
- object = create_object("@#{user.username}, can you look at this?\nHello world\n")
- actual = first_line_in_markdown(object, attribute, 100, project: project)
-
- doc = Nokogiri::HTML.parse(actual)
-
- # Make sure we didn't create invalid markup
- expect(doc.errors).to be_empty
-
- # Leading user link
- expect(doc.css('a').length).to eq(1)
- expect(doc.css('a')[0].attr('href')).to eq user_path(user)
- expect(doc.css('a')[0].text).to eq "@#{user.username}"
-
- expect(doc.content).to eq "@#{user.username}, can you look at this?..."
- end
-
- it 'truncates Markdown with emoji properly' do
- object = create_object("foo :wink:\nbar :grinning:")
- actual = first_line_in_markdown(object, attribute, 100, project: project)
-
- doc = Nokogiri::HTML.parse(actual)
-
- # Make sure we didn't create invalid markup
- # But also account for the 2 errors caused by the unknown `gl-emoji` elements
- expect(doc.errors.length).to eq(2)
-
- expect(doc.css('gl-emoji').length).to eq(2)
- expect(doc.css('gl-emoji')[0].attr('data-name')).to eq 'wink'
- expect(doc.css('gl-emoji')[1].attr('data-name')).to eq 'grinning'
-
- expect(doc.content).to eq "foo 😉\nbar 😀"
- end
-
it 'does not post-process truncated text', :request_store do
object = create_object("hello \n\n [Test](README.md)")
diff --git a/spec/lib/banzai/filter/truncate_visible_filter_spec.rb b/spec/lib/banzai/filter/truncate_visible_filter_spec.rb
new file mode 100644
index 00000000000..8daaed05264
--- /dev/null
+++ b/spec/lib/banzai/filter/truncate_visible_filter_spec.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Banzai::Filter::TruncateVisibleFilter do
+ include FilterSpecHelper
+
+ let_it_be(:project) { build(:project, :repository) }
+ let_it_be(:max_chars) { 100 }
+ let_it_be(:user) do
+ user = create(:user, username: 'gfm')
+ project.add_maintainer(user)
+ user
+ end
+
+ # Since we're truncating nodes of an html document, actually use the
+ # full pipeline to generate full documents.
+ def convert_markdown(text, context = {})
+ Banzai::Pipeline::FullPipeline.to_html(text, { project: project }.merge(context))
+ end
+
+ shared_examples_for 'truncates text' do
+ specify do
+ html = convert_markdown(markdown)
+ doc = filter(html, { truncate_visible_max_chars: max_chars })
+
+ expect(doc.to_html).to match(expected)
+ end
+ end
+
+ describe 'displays inline code' do
+ let(:markdown) { 'Text with `inline code`' }
+ let(:expected) { 'Text with <code>inline code</code>' }
+
+ it_behaves_like 'truncates text'
+ end
+
+ describe 'truncates the text with multiple paragraphs' do
+ let(:markdown) { "Paragraph 1\n\nParagraph 2" }
+ let(:expected) { 'Paragraph 1...' }
+
+ it_behaves_like 'truncates text'
+ end
+
+ describe 'truncates the first line of a code block' do
+ let(:markdown) { "```\nCode block\nwith two lines\n```" }
+ let(:expected) { "Code block...</span>\n</code>" }
+
+ it_behaves_like 'truncates text'
+ end
+
+ describe 'preserves code color scheme' do
+ let(:max_chars) { 150 }
+ let(:markdown) { "```ruby\ndef test\n 'hello world'\nend\n```" }
+ let(:expected) do
+ '<code><span id="LC1" class="line" lang="ruby">' \
+ '<span class="k">def</span> <span class="nf">test</span>...</span>'
+ end
+
+ it_behaves_like 'truncates text'
+ end
+
+ describe 'truncates a single long line of text' do
+ let(:max_chars) { 150 }
+ let(:text) { 'The quick brown fox jumped over the lazy dog twice' } # 50 chars
+ let(:markdown) { text * 4 }
+ let(:expected) { (text * 2).sub(/.{3}/, '...') }
+
+ it_behaves_like 'truncates text'
+ end
+
+ it 'preserves a link href when link text is truncated' do
+ max_chars = 150
+ text = 'The quick brown fox jumped over the lazy dog' # 44 chars
+ link_url = 'http://example.com/foo/bar/baz' # 30 chars
+ markdown = "#{text}#{text}#{text} #{link_url}" # 163 chars
+ expected_link_text = 'http://example...</a>'
+
+ html = convert_markdown(markdown)
+ doc = filter(html, { truncate_visible_max_chars: max_chars })
+
+ expect(doc.to_html).to match(link_url)
+ expect(doc.to_html).to match(expected_link_text)
+ end
+
+ it 'truncates HTML properly' do
+ markdown = "@#{user.username}, can you look at this?\nHello world\n"
+
+ html = convert_markdown(markdown)
+ doc = filter(html, { truncate_visible_max_chars: max_chars })
+
+ # Make sure we didn't create invalid markup
+ expect(doc.errors).to be_empty
+
+ # Leading user link
+ expect(doc.css('a').length).to eq(1)
+ expect(doc.css('a')[0].attr('href')).to eq urls.user_path(user)
+ expect(doc.css('a')[0].text).to eq "@#{user.username}"
+ expect(doc.content).to eq "@#{user.username}, can you look at this?..."
+ end
+
+ it 'truncates HTML with emoji properly' do
+ markdown = "foo :wink:\nbar :grinning:"
+ # actual = first_line_in_markdown(object, attribute, 100, project: project)
+
+ html = convert_markdown(markdown)
+ doc = filter(html, { truncate_visible_max_chars: max_chars })
+
+ # Make sure we didn't create invalid markup
+ # But also account for the 2 errors caused by the unknown `gl-emoji` elements
+ expect(doc.errors.length).to eq(2)
+
+ expect(doc.css('gl-emoji').length).to eq(2)
+ expect(doc.css('gl-emoji')[0].attr('data-name')).to eq 'wink'
+ expect(doc.css('gl-emoji')[1].attr('data-name')).to eq 'grinning'
+
+ expect(doc.content).to eq "foo 😉\nbar 😀"
+ end
+
+ it 'does not truncate if truncate_visible_max_chars not specified' do
+ markdown = "@#{user.username}, can you look at this?\nHello world"
+
+ html = convert_markdown(markdown)
+ doc = filter(html)
+
+ expect(doc.content).to eq markdown
+ end
+end
diff --git a/spec/migrations/adjust_task_note_rename_background_migration_values_spec.rb b/spec/migrations/adjust_task_note_rename_background_migration_values_spec.rb
new file mode 100644
index 00000000000..422d0655e36
--- /dev/null
+++ b/spec/migrations/adjust_task_note_rename_background_migration_values_spec.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe AdjustTaskNoteRenameBackgroundMigrationValues, :migration do
+ let(:finished_status) { 3 }
+ let(:failed_status) { described_class::MIGRATION_FAILED_STATUS }
+ let(:active_status) { described_class::MIGRATION_ACTIVE_STATUS }
+
+ shared_examples 'task note migration with failing batches' do
+ it 'updates batch sizes and resets failed batches' do
+ migration = create_background_migration(status: initial_status)
+ batches = []
+
+ batches << create_failed_batched_job(migration)
+ batches << create_failed_batched_job(migration)
+
+ migrate!
+
+ expect(described_class::JOB_CLASS_NAME).to have_scheduled_batched_migration(
+ table_name: :system_note_metadata,
+ column_name: :id,
+ interval: 2.minutes,
+ batch_size: described_class::NEW_BATCH_SIZE,
+ max_batch_size: 20_000,
+ sub_batch_size: described_class::NEW_SUB_BATCH_SIZE
+ )
+ expect(migration.reload.status).to eq(active_status)
+
+ updated_batches = batches.map { |b| b.reload.attributes.slice('attempts', 'sub_batch_size') }
+ expect(updated_batches).to all(eq("attempts" => 0, "sub_batch_size" => 10))
+ end
+ end
+
+ describe '#up' do
+ context 'when migration was already finished' do
+ it 'does not update batch sizes' do
+ create_background_migration(status: finished_status)
+
+ migrate!
+
+ expect(described_class::JOB_CLASS_NAME).to have_scheduled_batched_migration(
+ table_name: :system_note_metadata,
+ column_name: :id,
+ interval: 2.minutes,
+ batch_size: described_class::OLD_BATCH_SIZE,
+ max_batch_size: 20_000,
+ sub_batch_size: described_class::OLD_SUB_BATCH_SIZE
+ )
+ end
+ end
+
+ context 'when the migration had failing batches' do
+ context 'when migration had a failed status' do
+ it_behaves_like 'task note migration with failing batches' do
+ let(:initial_status) { failed_status }
+ end
+
+ it 'updates started_at timestamp' do
+ migration = create_background_migration(status: failed_status)
+ now = Time.zone.now
+
+ travel_to now do
+ migrate!
+ migration.reload
+ end
+
+ expect(migration.started_at).to be_like_time(now)
+ end
+ end
+
+ context 'when migration had an active status' do
+ it_behaves_like 'task note migration with failing batches' do
+ let(:initial_status) { active_status }
+ end
+
+ it 'does not update started_at timestamp' do
+ migration = create_background_migration(status: active_status)
+ original_time = migration.started_at
+
+ migrate!
+ migration.reload
+
+ expect(migration.started_at).to be_like_time(original_time)
+ end
+ end
+ end
+ end
+
+ describe '#down' do
+ it 'reverts to old batch sizes' do
+ create_background_migration(status: finished_status)
+
+ migrate!
+ schema_migrate_down!
+
+ expect(described_class::JOB_CLASS_NAME).to have_scheduled_batched_migration(
+ table_name: :system_note_metadata,
+ column_name: :id,
+ interval: 2.minutes,
+ batch_size: described_class::OLD_BATCH_SIZE,
+ max_batch_size: 20_000,
+ sub_batch_size: described_class::OLD_SUB_BATCH_SIZE
+ )
+ end
+ end
+
+ def create_failed_batched_job(migration)
+ table(:batched_background_migration_jobs).create!(
+ batched_background_migration_id: migration.id,
+ status: described_class::JOB_FAILED_STATUS,
+ min_value: 1,
+ max_value: 10,
+ attempts: 3,
+ batch_size: described_class::OLD_BATCH_SIZE,
+ sub_batch_size: described_class::OLD_SUB_BATCH_SIZE
+ )
+ end
+
+ def create_background_migration(status:)
+ migrations_table = table(:batched_background_migrations)
+ # make sure we only have on migration with that job class name in the specs
+ migrations_table.where(job_class_name: described_class::JOB_CLASS_NAME).delete_all
+
+ migrations_table.create!(
+ job_class_name: described_class::JOB_CLASS_NAME,
+ status: status,
+ max_value: 10,
+ max_batch_size: 20_000,
+ batch_size: described_class::OLD_BATCH_SIZE,
+ sub_batch_size: described_class::OLD_SUB_BATCH_SIZE,
+ interval: 2.minutes,
+ table_name: :system_note_metadata,
+ column_name: :id,
+ total_tuple_count: 100_000,
+ pause_ms: 100,
+ gitlab_schema: :gitlab_main,
+ job_arguments: [],
+ started_at: 2.days.ago
+ )
+ end
+end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 414a201aa34..04df8ecc882 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -178,8 +178,8 @@ RSpec.describe Member do
end
context 'member role is associated' do
- let_it_be(:member_role) do
- create(:member_role, members: [member])
+ let!(:member_role) do
+ create(:member_role, members: [member], base_access_level: Gitlab::Access::DEVELOPER)
end
context 'member role matches access level' do
@@ -201,7 +201,9 @@ RSpec.describe Member do
member.access_level = Gitlab::Access::MAINTAINER
expect(member).not_to be_valid
- expect(member.errors.full_messages).to include( "Access level cannot be changed since member is associated with a custom role")
+ expect(member.errors.full_messages).to include(
+ "Access level cannot be changed since member is associated with a custom role"
+ )
end
end
end
diff --git a/spec/models/members/member_role_spec.rb b/spec/models/members/member_role_spec.rb
index e8993491918..e2691e2e78c 100644
--- a/spec/models/members/member_role_spec.rb
+++ b/spec/models/members/member_role_spec.rb
@@ -11,7 +11,39 @@ RSpec.describe MemberRole do
describe 'validation' do
subject { described_class.new }
- it { is_expected.to validate_presence_of(:namespace_id) }
+ it { is_expected.to validate_presence_of(:namespace) }
it { is_expected.to validate_presence_of(:base_access_level) }
+
+ context 'for namespace' do
+ subject { build(:member_role) }
+
+ let_it_be(:root_group) { create(:group) }
+
+ context 'when namespace is a subgroup' do
+ it 'is invalid' do
+ subgroup = create(:group, parent: root_group)
+ subject.namespace = subgroup
+
+ expect(subject).to be_invalid
+ end
+ end
+
+ context 'when namespace is a root group' do
+ it 'is valid' do
+ subject.namespace = root_group
+
+ expect(subject).to be_valid
+ end
+ end
+
+ context 'when namespace is not present' do
+ it 'is invalid with a different error message' do
+ subject.namespace = nil
+
+ expect(subject).to be_invalid
+ expect(subject.errors.full_messages).to eq(["Namespace can't be blank"])
+ end
+ end
+ end
end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 847379af91a..49709d47645 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -2777,6 +2777,50 @@ RSpec.describe ProjectPolicy do
end
end
+ describe 'role_enables_download_code' do
+ using RSpec::Parameterized::TableSyntax
+
+ context 'default roles' do
+ let(:current_user) { public_send(role) }
+
+ context 'public project' do
+ let(:project) { public_project }
+
+ where(:role, :allowed) do
+ :owner | true
+ :maintainer | true
+ :developer | true
+ :reporter | true
+ :guest | true
+
+ with_them do
+ it do
+ expect(subject.can?(:download_code)).to be(allowed)
+ end
+ end
+ end
+ end
+
+ context 'private project' do
+ let(:project) { private_project }
+
+ where(:role, :allowed) do
+ :owner | true
+ :maintainer | true
+ :developer | true
+ :reporter | true
+ :guest | false
+ end
+
+ with_them do
+ it do
+ expect(subject.can?(:download_code)).to be(allowed)
+ end
+ end
+ end
+ end
+ end
+
private
def project_subject(project_type)
diff --git a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
index c19defa37e8..2dc7b9764fe 100644
--- a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
+++ b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
@@ -48,6 +48,8 @@ RSpec.describe 'Getting Ci Cd Setting' do
expect(settings_data['mergeTrainsEnabled']).to eql project.ci_cd_settings.merge_trains_enabled?
expect(settings_data['keepLatestArtifact']).to eql project.keep_latest_artifacts_available?
expect(settings_data['jobTokenScopeEnabled']).to eql project.ci_cd_settings.job_token_scope_enabled?
+ expect(settings_data['inboundJobTokenScopeEnabled']).to eql(
+ project.ci_cd_settings.inbound_job_token_scope_enabled?)
end
end
end
diff --git a/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb b/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
index 394d9ff53d1..6cca618726b 100644
--- a/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
@@ -6,15 +6,19 @@ RSpec.describe 'ProjectCiCdSettingsUpdate' do
include GraphqlHelpers
let_it_be(:project) do
- create(:project, keep_latest_artifact: true, ci_job_token_scope_enabled: true)
- .tap(&:save!)
+ create(:project,
+ keep_latest_artifact: true,
+ ci_job_token_scope_enabled: true,
+ ci_inbound_job_token_scope_enabled: true
+ ).tap(&:save!)
end
let(:variables) do
{
full_path: project.full_path,
keep_latest_artifact: false,
- job_token_scope_enabled: false
+ job_token_scope_enabled: false,
+ inbound_job_token_scope_enabled: false
}
end
@@ -76,6 +80,43 @@ RSpec.describe 'ProjectCiCdSettingsUpdate' do
expect(project.ci_job_token_scope_enabled).to eq(true)
end
+ describe 'inbound_job_token_scope_enabled' do
+ it 'updates inbound_job_token_scope_enabled' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ project.reload
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(project.ci_inbound_job_token_scope_enabled).to eq(false)
+ end
+
+ it 'does not update inbound_job_token_scope_enabled if not specified' do
+ variables.except!(:inbound_job_token_scope_enabled)
+
+ post_graphql_mutation(mutation, current_user: user)
+
+ project.reload
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(project.ci_inbound_job_token_scope_enabled).to eq(true)
+ end
+
+ context 'when ci_inbound_job_token_scope disabled' do
+ before do
+ stub_feature_flags(ci_inbound_job_token_scope: false)
+ end
+
+ it 'does not update inbound_job_token_scope_enabled' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ project.reload
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(project.ci_inbound_job_token_scope_enabled).to eq(true)
+ end
+ end
+ end
+
context 'when bad arguments are provided' do
let(:variables) { { full_path: '', keep_latest_artifact: false } }