diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index 5e8b80cd959..8b44ccfd276 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -10,6 +10,11 @@ import {
} from '@gitlab/ui';
import httpStatusCodes from '~/lib/utils/http_status';
+
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import projectQuery from '../queries/project_boards.query.graphql';
+import groupQuery from '../queries/group_boards.query.graphql';
+
import boardsStore from '../stores/boards_store';
import BoardForm from './board_form.vue';
@@ -88,8 +93,9 @@ export default {
},
data() {
return {
- loading: true,
hasScrollFade: false,
+ loadingBoards: 0,
+ loadingRecentBoards: false,
scrollFadeInitialized: false,
boards: [],
recentBoards: [],
@@ -102,6 +108,12 @@ export default {
};
},
computed: {
+ parentType() {
+ return this.groupId ? 'group' : 'project';
+ },
+ loading() {
+ return this.loadingRecentBoards && this.loadingBoards;
+ },
currentPage() {
return this.state.currentPage;
},
@@ -147,49 +159,71 @@ export default {
return;
}
- const recentBoardsPromise = new Promise((resolve, reject) =>
- boardsStore
- .recentBoards()
- .then(resolve)
- .catch(err => {
- /**
- * If user is unauthorized we'd still want to resolve the
- * request to display all boards.
- */
- if (err.response.status === httpStatusCodes.UNAUTHORIZED) {
- resolve({ data: [] }); // recent boards are empty
- return;
- }
- reject(err);
- }),
- );
+ this.$apollo.addSmartQuery('boards', {
+ variables() {
+ return { fullPath: this.state.endpoints.fullPath };
+ },
+ query() {
+ return this.groupId ? groupQuery : projectQuery;
+ },
+ loadingKey: 'loadingBoards',
+ update(data) {
+ if (!data?.[this.parentType]) {
+ return [];
+ }
+ return data[this.parentType].boards.edges.map(({ node }) => ({
+ id: getIdFromGraphQLId(node.id),
+ name: node.name,
+ }));
+ },
+ });
- Promise.all([boardsStore.allBoards(), recentBoardsPromise])
- .then(([allBoards, recentBoards]) => [allBoards.data, recentBoards.data])
- .then(([allBoardsJson, recentBoardsJson]) => {
- this.loading = false;
- this.boards = allBoardsJson;
- this.recentBoards = recentBoardsJson;
+ this.loadingRecentBoards = true;
+ boardsStore
+ .recentBoards()
+ .then(res => {
+ this.recentBoards = res.data;
+ })
+ .catch(err => {
+ /**
+ * If user is unauthorized we'd still want to resolve the
+ * request to display all boards.
+ */
+ if (err?.response?.status === httpStatusCodes.UNAUTHORIZED) {
+ this.recentBoards = []; // recent boards are empty
+ return;
+ }
+ throw err;
})
.then(() => this.$nextTick()) // Wait for boards list in DOM
.then(() => {
this.setScrollFade();
})
- .catch(() => {
- this.loading = false;
+ .catch(() => {})
+ .finally(() => {
+ this.loadingRecentBoards = false;
});
},
isScrolledUp() {
const { content } = this.$refs;
+
+ if (!content) {
+ return false;
+ }
+
const currentPosition = this.contentClientHeight + content.scrollTop;
- return content && currentPosition < this.maxPosition;
+ return currentPosition < this.maxPosition;
},
initScrollFade() {
- this.scrollFadeInitialized = true;
-
const { content } = this.$refs;
+ if (!content) {
+ return;
+ }
+
+ this.scrollFadeInitialized = true;
+
this.contentClientHeight = content.clientHeight;
this.maxPosition = content.scrollHeight;
},
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index f1b481fc386..f72fc8d54b3 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -98,6 +98,7 @@ export default () => {
listsEndpoint: this.listsEndpoint,
bulkUpdatePath: this.bulkUpdatePath,
boardId: this.boardId,
+ fullPath: $boardApp.dataset.fullPath,
});
boardsStore.rootPath = this.boardsEndpoint;
diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
index 8d22f009784..73d37459bfe 100644
--- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
+++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
@@ -1,7 +1,15 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import BoardsSelector from '~/boards/components/boards_selector.vue';
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
export default () => {
const boardsSwitcherElement = document.getElementById('js-multiple-boards-switcher');
return new Vue({
@@ -9,6 +17,7 @@ export default () => {
components: {
BoardsSelector,
},
+ apolloProvider,
data() {
const { dataset } = boardsSwitcherElement;
diff --git a/app/assets/javascripts/boards/queries/board.fragment.graphql b/app/assets/javascripts/boards/queries/board.fragment.graphql
new file mode 100644
index 00000000000..48f55e899bf
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/board.fragment.graphql
@@ -0,0 +1,4 @@
+fragment BoardFragment on Board {
+ id,
+ name
+}
diff --git a/app/assets/javascripts/boards/queries/group_boards.query.graphql b/app/assets/javascripts/boards/queries/group_boards.query.graphql
new file mode 100644
index 00000000000..74c224add7d
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/group_boards.query.graphql
@@ -0,0 +1,13 @@
+#import "ee_else_ce/boards/queries/board.fragment.graphql"
+
+query group_boards($fullPath: ID!) {
+ group(fullPath: $fullPath) {
+ boards {
+ edges {
+ node {
+ ...BoardFragment
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/queries/project_boards.query.graphql b/app/assets/javascripts/boards/queries/project_boards.query.graphql
new file mode 100644
index 00000000000..a1326bd5eff
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/project_boards.query.graphql
@@ -0,0 +1,13 @@
+#import "ee_else_ce/boards/queries/board.fragment.graphql"
+
+query project_boards($fullPath: ID!) {
+ project(fullPath: $fullPath) {
+ boards {
+ edges {
+ node {
+ ...BoardFragment
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 2a5571543fb..2a2cff3d07d 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -45,7 +45,14 @@ const boardsStore = {
},
multiSelect: { list: [] },
- setEndpoints({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, recentBoardsEndpoint }) {
+ setEndpoints({
+ boardsEndpoint,
+ listsEndpoint,
+ bulkUpdatePath,
+ boardId,
+ recentBoardsEndpoint,
+ fullPath,
+ }) {
const listsEndpointGenerate = `${listsEndpoint}/generate.json`;
this.state.endpoints = {
boardsEndpoint,
@@ -53,6 +60,7 @@ const boardsStore = {
listsEndpoint,
listsEndpointGenerate,
bulkUpdatePath,
+ fullPath,
recentBoardsEndpoint: `${recentBoardsEndpoint}.json`,
};
},
@@ -542,10 +550,6 @@ const boardsStore = {
return axios.post(endpoint);
},
- allBoards() {
- return axios.get(this.generateBoardsPath());
- },
-
recentBoards() {
return axios.get(this.state.endpoints.recentBoardsEndpoint);
},
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index 6994f83bce0..faaa65b1a16 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -165,6 +165,16 @@ export default {
showContainerRegistryPublicNote() {
return this.visibilityLevel === visibilityOptions.PUBLIC;
},
+
+ repositoryHelpText() {
+ if (this.visibilityLevel === visibilityOptions.PRIVATE) {
+ return s__('ProjectSettings|View and edit files in this project');
+ }
+
+ return s__(
+ 'ProjectSettings|View and edit files in this project. Non-project members will only have read access',
+ );
+ },
},
watch: {
@@ -225,6 +235,7 @@ export default {
@@ -270,6 +281,7 @@ export default {
@@ -280,8 +292,9 @@ export default {
/>
@@ -302,6 +316,7 @@ export default {
/>
@@ -327,6 +343,7 @@ export default {
@@ -386,6 +406,7 @@ export default {
/>
@@ -397,6 +418,7 @@ export default {
+
`;
- balsamiqViewer = jasmine.createSpyObj('balsamiqViewer', ['getResource']);
+ balsamiqViewer = {
+ getResource: jest.fn(),
+ };
- spyOn(BalsamiqViewer, 'parseTitle').and.returnValue(name);
- balsamiqViewer.getResource.and.returnValue(resource);
+ jest.spyOn(BalsamiqViewer, 'parseTitle').mockReturnValue(name);
+ balsamiqViewer.getResource.mockReturnValue(resource);
renderTemplate = BalsamiqViewer.prototype.renderTemplate.call(balsamiqViewer, preview);
});
@@ -306,7 +316,7 @@ describe('BalsamiqViewer', () => {
expect(BalsamiqViewer.parseTitle).toHaveBeenCalledWith(resource);
});
- it('should return the template string', function() {
+ it('should return the template string', () => {
expect(renderTemplate.replace(/\s/g, '')).toEqual(template.replace(/\s/g, ''));
});
});
@@ -318,7 +328,7 @@ describe('BalsamiqViewer', () => {
beforeEach(() => {
preview = ['{}', '{ "id": 1 }'];
- spyOn(JSON, 'parse').and.callThrough();
+ jest.spyOn(JSON, 'parse');
parsePreview = BalsamiqViewer.parsePreview(preview);
});
@@ -337,7 +347,7 @@ describe('BalsamiqViewer', () => {
beforeEach(() => {
title = { values: [['{}', '{}', '{"name":"name"}']] };
- spyOn(JSON, 'parse').and.callThrough();
+ jest.spyOn(JSON, 'parse');
parseTitle = BalsamiqViewer.parseTitle(title);
});
diff --git a/spec/frontend/boards/boards_store_spec.js b/spec/frontend/boards/boards_store_spec.js
index 2dc9039bc9d..5c5315fd465 100644
--- a/spec/frontend/boards/boards_store_spec.js
+++ b/spec/frontend/boards/boards_store_spec.js
@@ -440,23 +440,6 @@ describe('boardsStore', () => {
});
});
- describe('allBoards', () => {
- const url = `${endpoints.boardsEndpoint}.json`;
-
- it('makes a request to fetch all boards', () => {
- axiosMock.onGet(url).replyOnce(200, dummyResponse);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.allBoards()).resolves.toEqual(expectedResponse);
- });
-
- it('fails for error response', () => {
- axiosMock.onGet(url).replyOnce(500);
-
- return expect(boardsStore.allBoards()).rejects.toThrow();
- });
- });
-
describe('recentBoards', () => {
const url = `${endpoints.recentBoardsEndpoint}.json`;
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index 7723af07d8c..b1ae86c2d3f 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -1,6 +1,6 @@
-import Vue from 'vue';
+import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
-import { GlDropdown } from '@gitlab/ui';
+import { GlDropdown, GlLoadingIcon } from '@gitlab/ui';
import { TEST_HOST } from 'spec/test_constants';
import BoardsSelector from '~/boards/components/boards_selector.vue';
import boardsStore from '~/boards/stores/boards_store';
@@ -8,7 +8,8 @@ import boardsStore from '~/boards/stores/boards_store';
const throttleDuration = 1;
function boardGenerator(n) {
- return new Array(n).fill().map((board, id) => {
+ return new Array(n).fill().map((board, index) => {
+ const id = `${index}`;
const name = `board${id}`;
return {
@@ -34,8 +35,17 @@ describe('BoardsSelector', () => {
const getDropdownItems = () => wrapper.findAll('.js-dropdown-item');
const getDropdownHeaders = () => wrapper.findAll('.dropdown-bold-header');
+ const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
beforeEach(() => {
+ const $apollo = {
+ queries: {
+ boards: {
+ loading: false,
+ },
+ },
+ };
+
boardsStore.setEndpoints({
boardsEndpoint: '',
recentBoardsEndpoint: '',
@@ -45,7 +55,13 @@ describe('BoardsSelector', () => {
});
allBoardsResponse = Promise.resolve({
- data: boards,
+ data: {
+ group: {
+ boards: {
+ edges: boards.map(board => ({ node: board })),
+ },
+ },
+ },
});
recentBoardsResponse = Promise.resolve({
data: recentBoards,
@@ -54,8 +70,7 @@ describe('BoardsSelector', () => {
boardsStore.allBoards = jest.fn(() => allBoardsResponse);
boardsStore.recentBoards = jest.fn(() => recentBoardsResponse);
- const Component = Vue.extend(BoardsSelector);
- wrapper = mount(Component, {
+ wrapper = mount(BoardsSelector, {
propsData: {
throttleDuration,
currentBoard: {
@@ -77,13 +92,18 @@ describe('BoardsSelector', () => {
scopedIssueBoardFeatureEnabled: true,
weights: [],
},
+ mocks: { $apollo },
attachToDocument: true,
});
+ wrapper.vm.$apollo.addSmartQuery = jest.fn((_, options) => {
+ wrapper.setData({
+ [options.loadingKey]: true,
+ });
+ });
+
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
wrapper.find(GlDropdown).vm.$emit('show');
-
- return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => Vue.nextTick());
});
afterEach(() => {
@@ -91,64 +111,99 @@ describe('BoardsSelector', () => {
wrapper = null;
});
- describe('filtering', () => {
- it('shows all boards without filtering', () => {
- expect(getDropdownItems().length).toBe(boards.length + recentBoards.length);
+ describe('loading', () => {
+ // we are testing loading state, so don't resolve responses until after the tests
+ afterEach(() => {
+ return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick());
});
- it('shows only matching boards when filtering', () => {
- const filterTerm = 'board1';
- const expectedCount = boards.filter(board => board.name.includes(filterTerm)).length;
-
- fillSearchBox(filterTerm);
-
- return Vue.nextTick().then(() => {
- expect(getDropdownItems().length).toBe(expectedCount);
- });
- });
-
- it('shows message if there are no matching boards', () => {
- fillSearchBox('does not exist');
-
- return Vue.nextTick().then(() => {
- expect(getDropdownItems().length).toBe(0);
- expect(wrapper.text().includes('No matching boards found')).toBe(true);
- });
+ it('shows loading spinner', () => {
+ expect(getDropdownHeaders()).toHaveLength(0);
+ expect(getDropdownItems()).toHaveLength(0);
+ expect(getLoadingIcon().exists()).toBe(true);
});
});
- describe('recent boards section', () => {
- it('shows only when boards are greater than 10', () => {
- const expectedCount = 2; // Recent + All
-
- expect(getDropdownHeaders().length).toBe(expectedCount);
+ describe('loaded', () => {
+ beforeEach(() => {
+ return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick());
});
- it('does not show when boards are less than 10', () => {
- wrapper.setData({
- boards: boards.slice(0, 5),
+ it('hides loading spinner', () => {
+ expect(getLoadingIcon().exists()).toBe(false);
+ });
+
+ describe('filtering', () => {
+ beforeEach(() => {
+ wrapper.setData({
+ boards,
+ });
+
+ return nextTick();
});
- return Vue.nextTick().then(() => {
- expect(getDropdownHeaders().length).toBe(0);
+ it('shows all boards without filtering', () => {
+ expect(getDropdownItems()).toHaveLength(boards.length + recentBoards.length);
+ });
+
+ it('shows only matching boards when filtering', () => {
+ const filterTerm = 'board1';
+ const expectedCount = boards.filter(board => board.name.includes(filterTerm)).length;
+
+ fillSearchBox(filterTerm);
+
+ return nextTick().then(() => {
+ expect(getDropdownItems()).toHaveLength(expectedCount);
+ });
+ });
+
+ it('shows message if there are no matching boards', () => {
+ fillSearchBox('does not exist');
+
+ return nextTick().then(() => {
+ expect(getDropdownItems()).toHaveLength(0);
+ expect(wrapper.text().includes('No matching boards found')).toBe(true);
+ });
});
});
- it('does not show when recentBoards api returns empty array', () => {
- wrapper.setData({
- recentBoards: [],
+ describe('recent boards section', () => {
+ it('shows only when boards are greater than 10', () => {
+ wrapper.setData({
+ boards,
+ });
+
+ return nextTick().then(() => {
+ expect(getDropdownHeaders()).toHaveLength(2);
+ });
});
- return Vue.nextTick().then(() => {
- expect(getDropdownHeaders().length).toBe(0);
+ it('does not show when boards are less than 10', () => {
+ wrapper.setData({
+ boards: boards.slice(0, 5),
+ });
+
+ return nextTick().then(() => {
+ expect(getDropdownHeaders()).toHaveLength(0);
+ });
});
- });
- it('does not show when search is active', () => {
- fillSearchBox('Random string');
+ it('does not show when recentBoards api returns empty array', () => {
+ wrapper.setData({
+ recentBoards: [],
+ });
- return Vue.nextTick().then(() => {
- expect(getDropdownHeaders().length).toBe(0);
+ return nextTick().then(() => {
+ expect(getDropdownHeaders()).toHaveLength(0);
+ });
+ });
+
+ it('does not show when search is active', () => {
+ fillSearchBox('Random string');
+
+ return nextTick().then(() => {
+ expect(getDropdownHeaders()).toHaveLength(0);
+ });
});
});
});
diff --git a/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js b/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js
new file mode 100644
index 00000000000..8ab5426a005
--- /dev/null
+++ b/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js
@@ -0,0 +1,124 @@
+import { mount, shallowMount } from '@vue/test-utils';
+
+import projectFeatureSetting from '~/pages/projects/shared/permissions/components/project_feature_setting.vue';
+import projectFeatureToggle from '~/vue_shared/components/toggle_button.vue';
+
+describe('Project Feature Settings', () => {
+ const defaultProps = {
+ name: 'Test',
+ options: [[1, 1], [2, 2], [3, 3], [4, 4], [5, 5]],
+ value: 1,
+ disabledInput: false,
+ };
+ let wrapper;
+
+ const mountComponent = customProps => {
+ const propsData = { ...defaultProps, ...customProps };
+ return shallowMount(projectFeatureSetting, { propsData });
+ };
+
+ beforeEach(() => {
+ wrapper = mountComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('Hidden name input', () => {
+ it('should set the hidden name input if the name exists', () => {
+ expect(wrapper.find({ name: 'Test' }).props().value).toBe(1);
+ });
+
+ it('should not set the hidden name input if the name does not exist', () => {
+ wrapper.setProps({ name: null });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ name: 'Test' }).exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('Feature toggle', () => {
+ it('should enable the feature toggle if the value is not 0', () => {
+ expect(wrapper.find(projectFeatureToggle).props().value).toBe(true);
+ });
+
+ it('should enable the feature toggle if the value is less than 0', () => {
+ wrapper.setProps({ value: -1 });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find(projectFeatureToggle).props().value).toBe(true);
+ });
+ });
+
+ it('should disable the feature toggle if the value is 0', () => {
+ wrapper.setProps({ value: 0 });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find(projectFeatureToggle).props().value).toBe(false);
+ });
+ });
+
+ it('should disable the feature toggle if disabledInput is set', () => {
+ wrapper.setProps({ disabledInput: true });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find(projectFeatureToggle).props().disabledInput).toBe(true);
+ });
+ });
+
+ it('should emit a change event when the feature toggle changes', () => {
+ // Needs to be fully mounted to be able to trigger the click event on the internal button
+ wrapper = mount(projectFeatureSetting, { propsData: defaultProps });
+
+ expect(wrapper.emitted().change).toBeUndefined();
+ wrapper
+ .find(projectFeatureToggle)
+ .find('button')
+ .trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.emitted().change.length).toBe(1);
+ expect(wrapper.emitted().change[0]).toEqual([0]);
+ });
+ });
+ });
+
+ describe('Project repo select', () => {
+ it.each`
+ disabledInput | value | options | isDisabled
+ ${true} | ${0} | ${[[1, 1]]} | ${true}
+ ${true} | ${1} | ${[[1, 1], [2, 2], [3, 3]]} | ${true}
+ ${false} | ${0} | ${[[1, 1], [2, 2], [3, 3]]} | ${true}
+ ${false} | ${1} | ${[[1, 1]]} | ${true}
+ ${false} | ${1} | ${[[1, 1], [2, 2], [3, 3]]} | ${false}
+ `(
+ 'should set disabled to $isDisabled when disabledInput is $disabledInput, the value is $value and options are $options',
+ ({ disabledInput, value, options, isDisabled }) => {
+ wrapper.setProps({ disabledInput, value, options });
+
+ return wrapper.vm.$nextTick(() => {
+ if (isDisabled) {
+ expect(wrapper.find('select').attributes().disabled).toEqual('disabled');
+ } else {
+ expect(wrapper.find('select').attributes().disabled).toBeUndefined();
+ }
+ });
+ },
+ );
+
+ it('should emit the change when a new option is selected', () => {
+ expect(wrapper.emitted().change).toBeUndefined();
+ wrapper
+ .findAll('option')
+ .at(1)
+ .trigger('change');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.emitted().change.length).toBe(1);
+ expect(wrapper.emitted().change[0]).toEqual([2]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js b/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js
new file mode 100644
index 00000000000..7cbcbdcdd1f
--- /dev/null
+++ b/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js
@@ -0,0 +1,63 @@
+import { shallowMount } from '@vue/test-utils';
+
+import projectSettingRow from '~/pages/projects/shared/permissions/components/project_setting_row.vue';
+
+describe('Project Setting Row', () => {
+ let wrapper;
+
+ const mountComponent = (customProps = {}) => {
+ const propsData = { ...customProps };
+ return shallowMount(projectSettingRow, { propsData });
+ };
+
+ beforeEach(() => {
+ wrapper = mountComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should show the label if it is set', () => {
+ wrapper.setProps({ label: 'Test label' });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find('label').text()).toEqual('Test label');
+ });
+ });
+
+ it('should hide the label if it is not set', () => {
+ expect(wrapper.find('label').exists()).toBe(false);
+ });
+
+ it('should show the help icon with the correct help path if it is set', () => {
+ wrapper.setProps({ label: 'Test label', helpPath: '/123' });
+
+ return wrapper.vm.$nextTick(() => {
+ const link = wrapper.find('a');
+
+ expect(link.exists()).toBe(true);
+ expect(link.attributes().href).toEqual('/123');
+ });
+ });
+
+ it('should hide the help icon if no help path is set', () => {
+ wrapper.setProps({ label: 'Test label' });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find('a').exists()).toBe(false);
+ });
+ });
+
+ it('should show the help text if it is set', () => {
+ wrapper.setProps({ helpText: 'Test text' });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find('span').text()).toEqual('Test text');
+ });
+ });
+
+ it('should hide the help text if it is set', () => {
+ expect(wrapper.find('span').exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
new file mode 100644
index 00000000000..c304dfd2048
--- /dev/null
+++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
@@ -0,0 +1,434 @@
+import { shallowMount } from '@vue/test-utils';
+
+import settingsPanel from '~/pages/projects/shared/permissions/components/settings_panel.vue';
+import {
+ featureAccessLevel,
+ visibilityLevelDescriptions,
+ visibilityOptions,
+} from '~/pages/projects/shared/permissions/constants';
+
+const defaultProps = {
+ currentSettings: {
+ visibilityLevel: 10,
+ requestAccessEnabled: true,
+ issuesAccessLevel: 20,
+ repositoryAccessLevel: 20,
+ forkingAccessLevel: 20,
+ mergeRequestsAccessLevel: 20,
+ buildsAccessLevel: 20,
+ wikiAccessLevel: 20,
+ snippetsAccessLevel: 20,
+ pagesAccessLevel: 10,
+ containerRegistryEnabled: true,
+ lfsEnabled: true,
+ emailsDisabled: false,
+ packagesEnabled: true,
+ },
+ canDisableEmails: true,
+ canChangeVisibilityLevel: true,
+ allowedVisibilityOptions: [0, 10, 20],
+ visibilityHelpPath: '/help/public_access/public_access',
+ registryAvailable: false,
+ registryHelpPath: '/help/user/packages/container_registry/index',
+ lfsAvailable: true,
+ lfsHelpPath: '/help/workflow/lfs/manage_large_binaries_with_git_lfs',
+ pagesAvailable: true,
+ pagesAccessControlEnabled: false,
+ pagesAccessControlForced: false,
+ pagesHelpPath: '/help/user/project/pages/introduction#gitlab-pages-access-control-core',
+ packagesAvailable: false,
+ packagesHelpPath: '/help/user/packages/index',
+};
+
+describe('Settings Panel', () => {
+ let wrapper;
+
+ const mountComponent = customProps => {
+ const propsData = { ...defaultProps, ...customProps };
+ return shallowMount(settingsPanel, { propsData });
+ };
+
+ const overrideCurrentSettings = (currentSettingsProps, extraProps = {}) => {
+ return mountComponent({
+ ...extraProps,
+ currentSettings: {
+ ...defaultProps.currentSettings,
+ ...currentSettingsProps,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ wrapper = mountComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('Project Visibility', () => {
+ it('should set the project visibility help path', () => {
+ expect(wrapper.find({ ref: 'project-visibility-settings' }).props().helpPath).toBe(
+ defaultProps.visibilityHelpPath,
+ );
+ });
+
+ it('should not disable the visibility level dropdown', () => {
+ wrapper.setProps({ canChangeVisibilityLevel: true });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(
+ wrapper.find('[name="project[visibility_level]"]').attributes().disabled,
+ ).toBeUndefined();
+ });
+ });
+
+ it('should disable the visibility level dropdown', () => {
+ wrapper.setProps({ canChangeVisibilityLevel: false });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find('[name="project[visibility_level]"]').attributes().disabled).toBe(
+ 'disabled',
+ );
+ });
+ });
+
+ it.each`
+ option | allowedOptions | disabled
+ ${visibilityOptions.PRIVATE} | ${[visibilityOptions.PRIVATE, visibilityOptions.INTERNAL, visibilityOptions.PUBLIC]} | ${false}
+ ${visibilityOptions.PRIVATE} | ${[visibilityOptions.INTERNAL, visibilityOptions.PUBLIC]} | ${true}
+ ${visibilityOptions.INTERNAL} | ${[visibilityOptions.PRIVATE, visibilityOptions.INTERNAL, visibilityOptions.PUBLIC]} | ${false}
+ ${visibilityOptions.INTERNAL} | ${[visibilityOptions.PRIVATE, visibilityOptions.PUBLIC]} | ${true}
+ ${visibilityOptions.PUBLIC} | ${[visibilityOptions.PRIVATE, visibilityOptions.INTERNAL, visibilityOptions.PUBLIC]} | ${false}
+ ${visibilityOptions.PUBLIC} | ${[visibilityOptions.PRIVATE, visibilityOptions.INTERNAL]} | ${true}
+ `(
+ 'sets disabled to $disabled for the visibility option $option when given $allowedOptions',
+ ({ option, allowedOptions, disabled }) => {
+ wrapper.setProps({ allowedVisibilityOptions: allowedOptions });
+
+ return wrapper.vm.$nextTick(() => {
+ const attributeValue = wrapper
+ .find(`[name="project[visibility_level]"] option[value="${option}"]`)
+ .attributes().disabled;
+
+ if (disabled) {
+ expect(attributeValue).toBe('disabled');
+ } else {
+ expect(attributeValue).toBeUndefined();
+ }
+ });
+ },
+ );
+
+ it('should set the visibility level description based upon the selected visibility level', () => {
+ wrapper.find('[name="project[visibility_level]"]').setValue(visibilityOptions.INTERNAL);
+
+ expect(wrapper.find({ ref: 'project-visibility-settings' }).text()).toContain(
+ visibilityLevelDescriptions[visibilityOptions.INTERNAL],
+ );
+ });
+
+ it('should show the request access checkbox if the visibility level is not private', () => {
+ wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.INTERNAL });
+
+ expect(wrapper.find('[name="project[request_access_enabled]"]').exists()).toBe(true);
+ });
+
+ it('should not show the request access checkbox if the visibility level is private', () => {
+ wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.PRIVATE });
+
+ expect(wrapper.find('[name="project[request_access_enabled]"]').exists()).toBe(false);
+ });
+ });
+
+ describe('Repository', () => {
+ it('should set the repository help text when the visibility level is set to private', () => {
+ wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.PRIVATE });
+
+ expect(wrapper.find({ ref: 'repository-settings' }).props().helpText).toEqual(
+ 'View and edit files in this project',
+ );
+ });
+
+ it('should set the repository help text with a read access warning when the visibility level is set to non-private', () => {
+ wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.PUBLIC });
+
+ expect(wrapper.find({ ref: 'repository-settings' }).props().helpText).toEqual(
+ 'View and edit files in this project. Non-project members will only have read access',
+ );
+ });
+ });
+
+ describe('Merge requests', () => {
+ it('should enable the merge requests access level input when the repository is enabled', () => {
+ wrapper = overrideCurrentSettings({ repositoryAccessLevel: featureAccessLevel.EVERYONE });
+
+ expect(
+ wrapper
+ .find('[name="project[project_feature_attributes][merge_requests_access_level]"]')
+ .props().disabledInput,
+ ).toEqual(false);
+ });
+
+ it('should disable the merge requests access level input when the repository is disabled', () => {
+ wrapper = overrideCurrentSettings({ repositoryAccessLevel: featureAccessLevel.NOT_ENABLED });
+
+ expect(
+ wrapper
+ .find('[name="project[project_feature_attributes][merge_requests_access_level]"]')
+ .props().disabledInput,
+ ).toEqual(true);
+ });
+ });
+
+ describe('Forks', () => {
+ it('should enable the forking access level input when the repository is enabled', () => {
+ wrapper = overrideCurrentSettings({ repositoryAccessLevel: featureAccessLevel.EVERYONE });
+
+ expect(
+ wrapper.find('[name="project[project_feature_attributes][forking_access_level]"]').props()
+ .disabledInput,
+ ).toEqual(false);
+ });
+
+ it('should disable the forking access level input when the repository is disabled', () => {
+ wrapper = overrideCurrentSettings({ repositoryAccessLevel: featureAccessLevel.NOT_ENABLED });
+
+ expect(
+ wrapper.find('[name="project[project_feature_attributes][forking_access_level]"]').props()
+ .disabledInput,
+ ).toEqual(true);
+ });
+ });
+
+ describe('Pipelines', () => {
+ it('should enable the builds access level input when the repository is enabled', () => {
+ wrapper = overrideCurrentSettings({ repositoryAccessLevel: featureAccessLevel.EVERYONE });
+
+ expect(
+ wrapper.find('[name="project[project_feature_attributes][builds_access_level]"]').props()
+ .disabledInput,
+ ).toEqual(false);
+ });
+
+ it('should disable the builds access level input when the repository is disabled', () => {
+ wrapper = overrideCurrentSettings({ repositoryAccessLevel: featureAccessLevel.NOT_ENABLED });
+
+ expect(
+ wrapper.find('[name="project[project_feature_attributes][builds_access_level]"]').props()
+ .disabledInput,
+ ).toEqual(true);
+ });
+ });
+
+ describe('Container registry', () => {
+ it('should show the container registry settings if the registry is available', () => {
+ wrapper.setProps({ registryAvailable: true });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'container-registry-settings' }).exists()).toBe(true);
+ });
+ });
+
+ it('should hide the container registry settings if the registry is not available', () => {
+ wrapper.setProps({ registryAvailable: false });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'container-registry-settings' }).exists()).toBe(false);
+ });
+ });
+
+ it('should set the container registry settings help path', () => {
+ wrapper.setProps({ registryAvailable: true });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'container-registry-settings' }).props().helpPath).toBe(
+ defaultProps.registryHelpPath,
+ );
+ });
+ });
+
+ it('should show the container registry public note if the visibility level is public and the registry is available', () => {
+ wrapper = overrideCurrentSettings(
+ { visibilityLevel: visibilityOptions.PUBLIC },
+ { registryAvailable: true },
+ );
+
+ expect(wrapper.find({ ref: 'container-registry-settings' }).text()).toContain(
+ 'Note: the container registry is always visible when a project is public',
+ );
+ });
+
+ it('should hide the container registry public note if the visibility level is private and the registry is available', () => {
+ wrapper = overrideCurrentSettings(
+ { visibilityLevel: visibilityOptions.PRIVATE },
+ { registryAvailable: true },
+ );
+
+ expect(wrapper.find({ ref: 'container-registry-settings' }).text()).not.toContain(
+ 'Note: the container registry is always visible when a project is public',
+ );
+ });
+
+ it('should enable the container registry input when the repository is enabled', () => {
+ wrapper = overrideCurrentSettings(
+ { repositoryAccessLevel: featureAccessLevel.EVERYONE },
+ { registryAvailable: true },
+ );
+
+ expect(
+ wrapper.find('[name="project[container_registry_enabled]"]').props().disabledInput,
+ ).toEqual(false);
+ });
+
+ it('should disable the container registry input when the repository is disabled', () => {
+ wrapper = overrideCurrentSettings(
+ { repositoryAccessLevel: featureAccessLevel.NOT_ENABLED },
+ { registryAvailable: true },
+ );
+
+ expect(
+ wrapper.find('[name="project[container_registry_enabled]"]').props().disabledInput,
+ ).toEqual(true);
+ });
+ });
+
+ describe('Git Large File Storage', () => {
+ it('should show the LFS settings if LFS is available', () => {
+ wrapper.setProps({ lfsAvailable: true });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'git-lfs-settings' }).exists()).toEqual(true);
+ });
+ });
+
+ it('should hide the LFS settings if LFS is not available', () => {
+ wrapper.setProps({ lfsAvailable: false });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'git-lfs-settings' }).exists()).toEqual(false);
+ });
+ });
+
+ it('should set the LFS settings help path', () => {
+ expect(wrapper.find({ ref: 'git-lfs-settings' }).props().helpPath).toBe(
+ defaultProps.lfsHelpPath,
+ );
+ });
+
+ it('should enable the LFS input when the repository is enabled', () => {
+ wrapper = overrideCurrentSettings(
+ { repositoryAccessLevel: featureAccessLevel.EVERYONE },
+ { lfsAvailable: true },
+ );
+
+ expect(wrapper.find('[name="project[lfs_enabled]"]').props().disabledInput).toEqual(false);
+ });
+
+ it('should disable the LFS input when the repository is disabled', () => {
+ wrapper = overrideCurrentSettings(
+ { repositoryAccessLevel: featureAccessLevel.NOT_ENABLED },
+ { lfsAvailable: true },
+ );
+
+ expect(wrapper.find('[name="project[lfs_enabled]"]').props().disabledInput).toEqual(true);
+ });
+ });
+
+ describe('Packages', () => {
+ it('should show the packages settings if packages are available', () => {
+ wrapper.setProps({ packagesAvailable: true });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'package-settings' }).exists()).toEqual(true);
+ });
+ });
+
+ it('should hide the packages settings if packages are not available', () => {
+ wrapper.setProps({ packagesAvailable: false });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'package-settings' }).exists()).toEqual(false);
+ });
+ });
+
+ it('should set the package settings help path', () => {
+ wrapper.setProps({ packagesAvailable: true });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'package-settings' }).props().helpPath).toBe(
+ defaultProps.packagesHelpPath,
+ );
+ });
+ });
+
+ it('should enable the packages input when the repository is enabled', () => {
+ wrapper = overrideCurrentSettings(
+ { repositoryAccessLevel: featureAccessLevel.EVERYONE },
+ { packagesAvailable: true },
+ );
+
+ expect(wrapper.find('[name="project[packages_enabled]"]').props().disabledInput).toEqual(
+ false,
+ );
+ });
+
+ it('should disable the packages input when the repository is disabled', () => {
+ wrapper = overrideCurrentSettings(
+ { repositoryAccessLevel: featureAccessLevel.NOT_ENABLED },
+ { packagesAvailable: true },
+ );
+
+ expect(wrapper.find('[name="project[packages_enabled]"]').props().disabledInput).toEqual(
+ true,
+ );
+ });
+ });
+
+ describe('Pages', () => {
+ it.each`
+ pagesAvailable | pagesAccessControlEnabled | visibility
+ ${true} | ${true} | ${'show'}
+ ${true} | ${false} | ${'hide'}
+ ${false} | ${true} | ${'hide'}
+ ${false} | ${false} | ${'hide'}
+ `(
+ 'should $visibility the page settings if pagesAvailable is $pagesAvailable and pagesAccessControlEnabled is $pagesAccessControlEnabled',
+ ({ pagesAvailable, pagesAccessControlEnabled, visibility }) => {
+ wrapper.setProps({ pagesAvailable, pagesAccessControlEnabled });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'pages-settings' }).exists()).toBe(visibility === 'show');
+ });
+ },
+ );
+
+ it('should set the pages settings help path', () => {
+ wrapper.setProps({ pagesAvailable: true, pagesAccessControlEnabled: true });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'pages-settings' }).props().helpPath).toBe(
+ defaultProps.pagesHelpPath,
+ );
+ });
+ });
+ });
+
+ describe('Email notifications', () => {
+ it('should show the disable email notifications input if emails an be disabled', () => {
+ wrapper.setProps({ canDisableEmails: true });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'email-settings' }).exists()).toBe(true);
+ });
+ });
+
+ it('should hide the disable email notifications input if emails cannot be disabled', () => {
+ wrapper.setProps({ canDisableEmails: false });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'email-settings' }).exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
new file mode 100644
index 00000000000..3c3f9764f64
--- /dev/null
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
@@ -0,0 +1,48 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Snippet Description Edit component rendering matches the snapshot 1`] = `
+
+`;
diff --git a/spec/frontend/snippets/components/snippet_description_edit_spec.js b/spec/frontend/snippets/components/snippet_description_edit_spec.js
new file mode 100644
index 00000000000..167489dc004
--- /dev/null
+++ b/spec/frontend/snippets/components/snippet_description_edit_spec.js
@@ -0,0 +1,52 @@
+import SnippetDescriptionEdit from '~/snippets/components/snippet_description_edit.vue';
+import { shallowMount } from '@vue/test-utils';
+
+describe('Snippet Description Edit component', () => {
+ let wrapper;
+ const defaultDescription = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
+ const markdownPreviewPath = 'foo/';
+ const markdownDocsPath = 'help/';
+
+ function createComponent(description = defaultDescription) {
+ wrapper = shallowMount(SnippetDescriptionEdit, {
+ propsData: {
+ description,
+ markdownPreviewPath,
+ markdownDocsPath,
+ },
+ });
+ }
+
+ function isHidden(sel) {
+ return wrapper.find(sel).classes('d-none');
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('rendering', () => {
+ it('matches the snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('renders the field expanded when description exists', () => {
+ expect(wrapper.find('.js-collapsed').classes('d-none')).toBe(true);
+ expect(wrapper.find('.js-expanded').classes('d-none')).toBe(false);
+
+ expect(isHidden('.js-collapsed')).toBe(true);
+ expect(isHidden('.js-expanded')).toBe(false);
+ });
+
+ it('renders the field collapsed if there is no description yet', () => {
+ createComponent('');
+
+ expect(isHidden('.js-collapsed')).toBe(false);
+ expect(isHidden('.js-expanded')).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
index 5edf41b1ec6..ef95cb1b8f2 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -259,16 +259,40 @@ describe('mrWidgetOptions', () => {
describe('methods', () => {
describe('checkStatus', () => {
- it('should tell service to check status', () => {
+ let cb;
+ let isCbExecuted;
+
+ beforeEach(() => {
jest.spyOn(vm.service, 'checkStatus').mockReturnValue(returnPromise(mockData));
jest.spyOn(vm.mr, 'setData').mockImplementation(() => {});
jest.spyOn(vm, 'handleNotification').mockImplementation(() => {});
- let isCbExecuted = false;
- const cb = () => {
+ isCbExecuted = false;
+ cb = () => {
isCbExecuted = true;
};
+ });
+ it('should not tell service to check status if document is not visible', () => {
+ Object.defineProperty(document, 'visibilityState', {
+ value: 'hidden',
+ configurable: true,
+ });
+ vm.checkStatus(cb);
+
+ return vm.$nextTick().then(() => {
+ expect(vm.service.checkStatus).not.toHaveBeenCalled();
+ expect(vm.mr.setData).not.toHaveBeenCalled();
+ expect(vm.handleNotification).not.toHaveBeenCalled();
+ expect(isCbExecuted).toBeFalsy();
+ Object.defineProperty(document, 'visibilityState', {
+ value: 'visible',
+ configurable: true,
+ });
+ });
+ });
+
+ it('should tell service to check status if document is visible', () => {
vm.checkStatus(cb);
return vm.$nextTick().then(() => {
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 635349955b1..5f22208a3ac 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -52,7 +52,7 @@ describe ProjectPolicy do
admin_snippet admin_project_member admin_note admin_wiki admin_project
admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster
- daily_statistics read_deploy_token
+ daily_statistics read_deploy_token create_deploy_token
]
end
diff --git a/spec/requests/api/deploy_tokens_spec.rb b/spec/requests/api/deploy_tokens_spec.rb
index 14153fae42f..8076b0958a4 100644
--- a/spec/requests/api/deploy_tokens_spec.rb
+++ b/spec/requests/api/deploy_tokens_spec.rb
@@ -133,4 +133,57 @@ describe API::DeployTokens do
end
end
end
+
+ describe 'POST /projects/:id/deploy_tokens' do
+ let(:params) do
+ {
+ name: 'Foo',
+ expires_at: 1.year.from_now,
+ scopes: [
+ 'read_repository'
+ ],
+ username: 'Bar'
+ }
+ end
+
+ subject do
+ post api("/projects/#{project.id}/deploy_tokens", user), params: params
+ response
+ end
+
+ context 'when unauthenticated' do
+ let(:user) { nil }
+
+ it { is_expected.to have_gitlab_http_status(:not_found) }
+ end
+
+ context 'when authenticated as non-admin user' do
+ before do
+ project.add_developer(user)
+ end
+
+ it { is_expected.to have_gitlab_http_status(:forbidden) }
+ end
+
+ context 'when authenticated as maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'creates the deploy token' do
+ expect { subject }.to change { DeployToken.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(response).to match_response_schema('public_api/v4/deploy_token')
+ end
+
+ context 'with an invalid scope' do
+ before do
+ params[:scopes] = %w[read_repository all_access]
+ end
+
+ it { is_expected.to have_gitlab_http_status(:bad_request) }
+ end
+ end
+ end
end