Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-12-23 15:09:54 +00:00
parent 5c9f6c66fa
commit 9dbca64417
36 changed files with 3241 additions and 489 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"printWidth": 100,
"singleQuote": true,
"arrowParens": "avoid",
"arrowParens": "always",
"trailingComma": "all"
}

View File

@ -1,5 +1,4 @@
import { sortBy } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { ListType } from './constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@ -121,15 +120,6 @@ export function moveIssueListHelper(issue, fromList, toList) {
return updatedIssue;
}
export function getBoardsPath(endpoint, board) {
const path = `${endpoint}${board.id ? `/${board.id}` : ''}.json`;
if (board.id) {
return axios.put(path, { board });
}
return axios.post(path, { board });
}
export function isListDraggable(list) {
return list.listType !== ListType.backlog && list.listType !== ListType.closed;
}
@ -146,6 +136,5 @@ export default {
fullBoardId,
fullLabelId,
fullIterationId,
getBoardsPath,
isListDraggable,
};

View File

@ -6,36 +6,13 @@ export default {
GlFormCheckbox,
},
props: {
currentBoard: {
type: Object,
required: true,
},
board: {
type: Object,
required: true,
},
isNewForm: {
hideBacklogList: {
type: Boolean,
required: false,
default: false,
required: true,
},
},
data() {
const { hide_backlog_list: hideBacklogList, hide_closed_list: hideClosedList } = this.isNewForm
? this.board
: this.currentBoard;
return {
hideClosedList,
hideBacklogList,
};
},
methods: {
changeClosedList(checked) {
this.board.hideClosedList = !checked;
},
changeBacklogList(checked) {
this.board.hideBacklogList = !checked;
hideClosedList: {
type: Boolean,
required: true,
},
},
};
@ -52,13 +29,13 @@ export default {
<gl-form-checkbox
:checked="!hideBacklogList"
data-testid="backlog-list-checkbox"
@change="changeBacklogList"
@change="$emit('update:hideBacklogList', !hideBacklogList)"
>{{ __('Show the Open list') }}
</gl-form-checkbox>
<gl-form-checkbox
:checked="!hideClosedList"
data-testid="closed-list-checkbox"
@change="changeClosedList"
@change="$emit('update:hideClosedList', !hideClosedList)"
>{{ __('Show the Closed list') }}
</gl-form-checkbox>
</div>

View File

@ -1,14 +1,15 @@
<script>
import { GlModal } from '@gitlab/ui';
import { pick } from 'lodash';
import { __, s__ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import { visitUrl, stripFinalUrlSegment } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import boardsStore from '~/boards/stores/boards_store';
import { fullBoardId, getBoardsPath } from '../boards_util';
import { fullLabelId, fullBoardId } from '../boards_util';
import BoardConfigurationOptions from './board_configuration_options.vue';
import createBoardMutation from '../graphql/board.mutation.graphql';
import updateBoardMutation from '../graphql/board_update.mutation.graphql';
import createBoardMutation from '../graphql/board_create.mutation.graphql';
const boardDefaults = {
id: false,
@ -91,8 +92,8 @@ export default {
},
},
inject: {
endpoints: {
default: {},
fullPath: {
default: '',
},
},
data() {
@ -155,14 +156,38 @@ export default {
text: this.$options.i18n.cancelButtonText,
};
},
boardPayload() {
const { assignee, milestone, labels } = this.board;
return {
...this.board,
assignee_id: assignee?.id,
milestone_id: milestone?.id,
label_ids: labels.length ? labels.map(b => b.id) : [''],
currentMutation() {
return this.board.id ? updateBoardMutation : createBoardMutation;
},
mutationVariables() {
const { board } = this;
/* eslint-disable @gitlab/require-i18n-strings */
const baseMutationVariables = {
name: board.name,
weight: board.weight,
assigneeId: board.assignee?.id ? convertToGraphQLId('User', board.assignee.id) : null,
milestoneId:
board.milestone?.id || board.milestone?.id === 0
? convertToGraphQLId('Milestone', board.milestone.id)
: null,
labelIds: board.labels.map(fullLabelId),
hideBacklogList: board.hide_backlog_list,
hideClosedList: board.hide_closed_list,
iterationId: board.iteration_id
? convertToGraphQLId('Iteration', board.iteration_id)
: null,
};
/* eslint-enable @gitlab/require-i18n-strings */
return board.id
? {
...baseMutationVariables,
id: fullBoardId(board.id),
}
: {
...baseMutationVariables,
projectPath: this.projectId ? this.fullPath : null,
groupPath: this.groupId ? this.fullPath : null,
};
},
},
mounted() {
@ -175,55 +200,39 @@ export default {
setIteration(iterationId) {
this.board.iteration_id = iterationId;
},
callBoardMutation(id) {
return this.$apollo.mutate({
mutation: createBoardMutation,
variables: {
...pick(this.boardPayload, ['hideClosedList', 'hideBacklogList']),
id,
},
async createOrUpdateBoard() {
const response = await this.$apollo.mutate({
mutation: this.currentMutation,
variables: { input: this.mutationVariables },
});
},
async updateBoard() {
const responses = await Promise.all([
// Remove unnecessary REST API call when https://gitlab.com/gitlab-org/gitlab/-/issues/282299#note_462996301 is resolved
getBoardsPath(this.endpoints.boardsEndpoint, this.boardPayload),
this.callBoardMutation(fullBoardId(this.boardPayload.id)),
]);
return responses[0].data;
return this.board.id
? getIdFromGraphQLId(response.data.updateBoard.board.id)
: getIdFromGraphQLId(response.data.createBoard.board.id);
},
async createBoard() {
// TODO: change this to use `createBoard` mutation https://gitlab.com/gitlab-org/gitlab/-/issues/292466 is resolved
const boardData = await getBoardsPath(this.endpoints.boardsEndpoint, this.boardPayload);
this.callBoardMutation(fullBoardId(boardData.data.id));
return boardData.data || boardData;
},
submit() {
async submit() {
if (this.board.name.length === 0) return;
this.isLoading = true;
if (this.isDeleteForm) {
boardsStore
.deleteBoard(this.currentBoard)
.then(() => {
this.isLoading = false;
visitUrl(boardsStore.rootPath);
})
.catch(() => {
Flash(this.$options.i18n.deleteErrorMessage);
this.isLoading = false;
});
try {
await boardsStore.deleteBoard(this.currentBoard);
visitUrl(boardsStore.rootPath);
} catch {
Flash(this.$options.i18n.deleteErrorMessage);
} finally {
this.isLoading = false;
}
} else {
const boardAction = this.boardPayload.id ? this.updateBoard : this.createBoard;
boardAction()
.then(data => {
visitUrl(data.board_path);
})
.catch(() => {
Flash(this.$options.i18n.saveErrorMessage);
this.isLoading = false;
});
try {
const path = await this.createOrUpdateBoard();
const strippedUrl = stripFinalUrlSegment(window.location.href);
const url = strippedUrl.includes('boards') ? `${path}` : `boards/${path}`;
visitUrl(url);
} catch {
Flash(this.$options.i18n.saveErrorMessage);
} finally {
this.isLoading = false;
}
}
},
cancel() {
@ -277,9 +286,8 @@ export default {
</div>
<board-configuration-options
:is-new-form="isNewForm"
:board="board"
:current-board="currentBoard"
:hide-backlog-list.sync="board.hide_backlog_list"
:hide-closed-list.sync="board.hide_closed_list"
/>
<board-scope

View File

@ -1,11 +0,0 @@
mutation UpdateBoard($id: BoardID!, $hideClosedList: Boolean, $hideBacklogList: Boolean) {
updateBoard(
input: { id: $id, hideClosedList: $hideClosedList, hideBacklogList: $hideBacklogList }
) {
board {
id
hideClosedList
hideBacklogList
}
}
}

View File

@ -0,0 +1,7 @@
mutation createBoard($input: CreateBoardInput!) {
createBoard(input: $input) {
board {
id
}
}
}

View File

@ -0,0 +1,9 @@
mutation UpdateBoard($input: UpdateBoardInput!) {
updateBoard(input: $input) {
board {
id
hideClosedList
hideBacklogList
}
}
}

View File

@ -335,7 +335,6 @@ export default () => {
}
mountMultipleBoardsSwitcher({
boardsEndpoint: $boardApp.dataset.boardsEndpoint,
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
fullPath: $boardApp.dataset.fullPath,
});
};

View File

@ -10,7 +10,7 @@ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
export default (endpoints = {}) => {
export default (params = {}) => {
const boardsSwitcherElement = document.getElementById('js-multiple-boards-switcher');
return new Vue({
el: boardsSwitcherElement,
@ -36,7 +36,7 @@ export default (endpoints = {}) => {
return { boardsSelectorProps };
},
provide: {
endpoints,
fullPath: params.fullPath,
},
render(createElement) {
return createElement(BoardsSelector, {

View File

@ -50,7 +50,8 @@ export default {
addTooltips(elements, config) {
const newTooltips = elements
.filter(element => !this.tooltipExists(element))
.map(element => newTooltip(element, config));
.map(element => newTooltip(element, config))
.filter(tooltip => tooltip.title);
newTooltips.forEach(tooltip => this.observe(tooltip));
@ -93,6 +94,9 @@ export default {
return this.tooltips.find(tooltip => tooltip.target === element);
},
},
safeHtmlConfig: {
ADD_TAGS: ['gl-emoji'],
},
};
</script>
<template>
@ -110,7 +114,7 @@ export default {
:disabled="tooltip.disabled"
:show="tooltip.show"
>
<span v-if="tooltip.html" v-safe-html="tooltip.title"></span>
<span v-if="tooltip.html" v-safe-html:[$options.safeHtmlConfig]="tooltip.title"></span>
<span v-else>{{ tooltip.title }}</span>
</gl-tooltip>
</div>

View File

@ -68,7 +68,7 @@ const invokeBootstrapApi = (elements, method) => {
}
};
const isGlTooltipsEnabled = () => Boolean(window.gon.glTooltipsEnabled);
const isGlTooltipsEnabled = () => Boolean(window.gon.features?.glTooltips);
const tooltipApiInvoker = ({ glHandler, bsHandler }) => (elements, ...params) => {
if (isGlTooltipsEnabled()) {

View File

@ -1,13 +1,23 @@
- editing ||= false
%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "50", "spy" => "affix" } }
.sidebar-container
.block.wiki-sidebar-header.gl-mb-3.w-100
%a.gutter-toggle.float-right.d-block.d-md-none.js-sidebar-wiki-toggle{ href: "#" }
= sprite_icon('chevron-double-lg-right', css_class: 'gl-icon')
- git_access_url = wiki_path(@wiki, action: :git_access)
= link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '', data: { qa_selector: 'clone_repository_link' } do
= sprite_icon('download', css_class: 'gl-mr-2')
%span= _("Clone repository")
.gl-display-flex.gl-flex-wrap
- git_access_url = wiki_path(@wiki, action: :git_access)
= link_to git_access_url, class: 'gl-mr-5' + (active_nav_link?(path: 'wikis#git_access') ? ' active' : ''), data: { qa_selector: 'clone_repository_link' } do
= sprite_icon('download', css_class: 'gl-mr-2')
%span= _("Clone repository")
- if can?(current_user, :create_wiki, @wiki)
- edit_sidebar_url = wiki_page_path(@wiki, Wiki::SIDEBAR, action: :edit)
- link_class = (editing && @page&.slug == Wiki::SIDEBAR) ? 'active' : ''
= link_to edit_sidebar_url, class: link_class, data: { qa_selector: 'edit_sidebar_link' } do
= sprite_icon('pencil-square', css_class: 'gl-mr-2')
%span= _("Edit sidebar")
- if @sidebar_error.present?
= render 'shared/alert_info', body: s_('Wiki|The sidebar failed to load. You can reload the page to try again.')

View File

@ -23,4 +23,4 @@
= render 'shared/wikis/form', uploads_path: wiki_attachment_upload_url
= render 'shared/wikis/sidebar'
= render 'shared/wikis/sidebar', editing: true

View File

@ -0,0 +1,5 @@
---
title: Add snippet repository storage move API endpoints
merge_request: 49228
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Add button to edit custom Wiki sidebar
merge_request: 50323
author: Frank Li
type: changed

View File

@ -0,0 +1,6 @@
---
title: "[RUN-AS-IF-FOSS] Move to `createBoard` mutation instead of REST API call +
`updateBoard`"
merge_request: 50171
author:
type: changed

View File

@ -0,0 +1,8 @@
---
name: gl_tooltips
introduced_by_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/292972
milestone: '13.8'
type: development
group: group::editor
default_enabled: false

View File

@ -204,13 +204,11 @@ otherwise they will not display when pushed to GitLab:
## Customizing sidebar
On the project's Wiki page, there is a right side navigation that renders the full Wiki pages list by default, with hierarchy.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/23109) in GitLab 13.8, the sidebar can be customized by clicking the **Edit sidebar** button.
To customize the sidebar, you can create a file named `_sidebar` to fully replace the default navigation.
To customize the Wiki's navigation sidebar, you need Developer permissions to the project.
WARNING:
Unless you link the `_sidebar` file from your custom nav, to edit it you'll have to access it directly
from the browser's address bar by typing: `https://gitlab.com/<namespace>/<project_name>/-/wikis/_sidebar` (for self-managed GitLab instances, replace `gitlab.com` with your instance's URL).
On the top-right, click **Edit sidebar** and make your changes. This creates a wiki page named `_sidebar` which fully replaces the default sidebar navigation.
Example for `_sidebar` (using Markdown format):

View File

@ -251,6 +251,7 @@ module API
mount ::API::Services
mount ::API::Settings
mount ::API::SidekiqMetrics
mount ::API::SnippetRepositoryStorageMoves
mount ::API::Snippets
mount ::API::Statistics
mount ::API::Submodules

View File

@ -0,0 +1,110 @@
# frozen_string_literal: true
module API
class SnippetRepositoryStorageMoves < ::API::Base
include PaginationParams
before { authenticated_as_admin! }
feature_category :gitaly
resource :snippet_repository_storage_moves do
desc 'Get a list of all snippet repository storage moves' do
detail 'This feature was introduced in GitLab 13.8.'
success Entities::SnippetRepositoryStorageMove
end
params do
use :pagination
end
get do
storage_moves = SnippetRepositoryStorageMove.order_created_at_desc
present paginate(storage_moves), with: Entities::SnippetRepositoryStorageMove, current_user: current_user
end
desc 'Get a snippet repository storage move' do
detail 'This feature was introduced in GitLab 13.8.'
success Entities::SnippetRepositoryStorageMove
end
params do
requires :repository_storage_move_id, type: Integer, desc: 'The ID of a snippet repository storage move'
end
get ':repository_storage_move_id' do
storage_move = SnippetRepositoryStorageMove.find(params[:repository_storage_move_id])
present storage_move, with: Entities::SnippetRepositoryStorageMove, current_user: current_user
end
desc 'Schedule bulk snippet repository storage moves' do
detail 'This feature was introduced in GitLab 13.8.'
end
params do
requires :source_storage_name, type: String, desc: 'The source storage shard', values: -> { Gitlab.config.repositories.storages.keys }
optional :destination_storage_name, type: String, desc: 'The destination storage shard', values: -> { Gitlab.config.repositories.storages.keys }
end
post do
::Snippets::ScheduleBulkRepositoryShardMovesService.enqueue(
declared_params[:source_storage_name],
declared_params[:destination_storage_name]
)
accepted!
end
end
params do
requires :id, type: String, desc: 'The ID of a snippet'
end
resource :snippets do
helpers do
def user_snippet
Snippet.find_by(id: params[:id]) # rubocop: disable CodeReuse/ActiveRecord
end
end
desc 'Get a list of all snippets repository storage moves' do
detail 'This feature was introduced in GitLab 13.8.'
success Entities::SnippetRepositoryStorageMove
end
params do
use :pagination
end
get ':id/repository_storage_moves' do
storage_moves = user_snippet.repository_storage_moves.order_created_at_desc
present paginate(storage_moves), with: Entities::SnippetRepositoryStorageMove, current_user: current_user
end
desc 'Get a snippet repository storage move' do
detail 'This feature was introduced in GitLab 13.8.'
success Entities::SnippetRepositoryStorageMove
end
params do
requires :repository_storage_move_id, type: Integer, desc: 'The ID of a snippet repository storage move'
end
get ':id/repository_storage_moves/:repository_storage_move_id' do
storage_move = user_snippet.repository_storage_moves.find(params[:repository_storage_move_id])
present storage_move, with: Entities::SnippetRepositoryStorageMove, current_user: current_user
end
desc 'Schedule a snippet repository storage move' do
detail 'This feature was introduced in GitLab 13.8.'
success Entities::SnippetRepositoryStorageMove
end
params do
optional :destination_storage_name, type: String, desc: 'The destination storage shard'
end
post ':id/repository_storage_moves' do
storage_move = user_snippet.repository_storage_moves.build(
declared_params.merge(source_storage_name: user_snippet.repository_storage)
)
if storage_move.schedule
present storage_move, with: Entities::SnippetRepositoryStorageMove, current_user: current_user
else
render_validation_error!(storage_move)
end
end
end
end
end

View File

@ -48,6 +48,7 @@ module Gitlab
push_frontend_feature_flag(:snippets_binary_blob, default_enabled: false)
push_frontend_feature_flag(:usage_data_api, default_enabled: true)
push_frontend_feature_flag(:security_auto_fix, default_enabled: false)
push_frontend_feature_flag(:gl_tooltips, default_enabled: :yaml)
# Startup CSS feature is a special one as it can be enabled by means of cookies and params
gon.push({ features: { 'startupCss' => use_startup_css? } }, true)

View File

@ -4478,7 +4478,7 @@ msgstr ""
msgid "BillingPlan|Upgrade"
msgstr ""
msgid "Billing|An email address is only visible for users managed through Group Managed Accounts."
msgid "Billing|An email address is only visible for users with public emails."
msgstr ""
msgid "Billing|An error occurred while loading billable members list"
@ -8180,6 +8180,84 @@ msgstr ""
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr ""
msgid "CreateValueStreamForm|%{name} (default)"
msgstr ""
msgid "CreateValueStreamForm|Add stage"
msgstr ""
msgid "CreateValueStreamForm|All default stages are currently visible"
msgstr ""
msgid "CreateValueStreamForm|Default stages"
msgstr ""
msgid "CreateValueStreamForm|Editing stage"
msgstr ""
msgid "CreateValueStreamForm|End event"
msgstr ""
msgid "CreateValueStreamForm|End event label"
msgstr ""
msgid "CreateValueStreamForm|End event: "
msgstr ""
msgid "CreateValueStreamForm|Enter a name for the stage"
msgstr ""
msgid "CreateValueStreamForm|Enter stage name"
msgstr ""
msgid "CreateValueStreamForm|Maximum length %{maxLength} characters"
msgstr ""
msgid "CreateValueStreamForm|Name"
msgstr ""
msgid "CreateValueStreamForm|Name is required"
msgstr ""
msgid "CreateValueStreamForm|New stage"
msgstr ""
msgid "CreateValueStreamForm|Please select a start event first"
msgstr ""
msgid "CreateValueStreamForm|Recover hidden stage"
msgstr ""
msgid "CreateValueStreamForm|Restore stage"
msgstr ""
msgid "CreateValueStreamForm|Select end event"
msgstr ""
msgid "CreateValueStreamForm|Select start event"
msgstr ""
msgid "CreateValueStreamForm|Stage %{index}"
msgstr ""
msgid "CreateValueStreamForm|Stage name already exists"
msgstr ""
msgid "CreateValueStreamForm|Start event"
msgstr ""
msgid "CreateValueStreamForm|Start event changed, please select a valid end event"
msgstr ""
msgid "CreateValueStreamForm|Start event label"
msgstr ""
msgid "CreateValueStreamForm|Start event: "
msgstr ""
msgid "CreateValueStreamForm|Update stage"
msgstr ""
msgid "Created"
msgstr ""
@ -8357,57 +8435,21 @@ msgstr ""
msgid "CustomCycleAnalytics|Add stage"
msgstr ""
msgid "CustomCycleAnalytics|All default stages are currently visible"
msgstr ""
msgid "CustomCycleAnalytics|Default stages"
msgstr ""
msgid "CustomCycleAnalytics|Editing stage"
msgstr ""
msgid "CustomCycleAnalytics|End event"
msgstr ""
msgid "CustomCycleAnalytics|End event label"
msgstr ""
msgid "CustomCycleAnalytics|Enter a name for the stage"
msgstr ""
msgid "CustomCycleAnalytics|Name"
msgstr ""
msgid "CustomCycleAnalytics|New stage"
msgstr ""
msgid "CustomCycleAnalytics|Please select a start event first"
msgstr ""
msgid "CustomCycleAnalytics|Recover hidden stage"
msgstr ""
msgid "CustomCycleAnalytics|Select end event"
msgstr ""
msgid "CustomCycleAnalytics|Select start event"
msgstr ""
msgid "CustomCycleAnalytics|Stage name already exists"
msgstr ""
msgid "CustomCycleAnalytics|Start event"
msgstr ""
msgid "CustomCycleAnalytics|Start event changed, please select a valid end event"
msgstr ""
msgid "CustomCycleAnalytics|Start event label"
msgstr ""
msgid "CustomCycleAnalytics|Update stage"
msgstr ""
msgid "Customer Portal"
msgstr ""
@ -10284,6 +10326,9 @@ msgstr ""
msgid "Edit public deploy key"
msgstr ""
msgid "Edit sidebar"
msgstr ""
msgid "Edit stage"
msgstr ""

View File

@ -0,0 +1,20 @@
{
"type": "object",
"required": [
"id",
"created_at",
"state",
"source_storage_name",
"destination_storage_name",
"snippet"
],
"properties" : {
"id": { "type": "integer" },
"created_at": { "type": "date" },
"state": { "type": "string" },
"source_storage_name": { "type": "string" },
"destination_storage_name": { "type": "string" },
"snippet": { "type": "object" }
},
"additionalProperties": false
}

View File

@ -0,0 +1,6 @@
{
"type": "array",
"items": {
"$ref": "./snippet_repository_storage_move.json"
}
}

View File

@ -3,38 +3,30 @@ import BoardConfigurationOptions from '~/boards/components/board_configuration_o
describe('BoardConfigurationOptions', () => {
let wrapper;
const board = { hide_backlog_list: false, hide_closed_list: false };
const defaultProps = {
currentBoard: board,
board,
isNewForm: false,
hideBacklogList: false,
hideClosedList: false,
};
const createComponent = () => {
const createComponent = (props = {}) => {
wrapper = shallowMount(BoardConfigurationOptions, {
propsData: { ...defaultProps },
propsData: { ...defaultProps, ...props },
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
const backlogListCheckbox = el => el.find('[data-testid="backlog-list-checkbox"]');
const closedListCheckbox = el => el.find('[data-testid="closed-list-checkbox"]');
const backlogListCheckbox = () => wrapper.find('[data-testid="backlog-list-checkbox"]');
const closedListCheckbox = () => wrapper.find('[data-testid="closed-list-checkbox"]');
const checkboxAssert = (backlogCheckbox, closedCheckbox) => {
expect(backlogListCheckbox(wrapper).attributes('checked')).toEqual(
expect(backlogListCheckbox().attributes('checked')).toEqual(
backlogCheckbox ? undefined : 'true',
);
expect(closedListCheckbox(wrapper).attributes('checked')).toEqual(
closedCheckbox ? undefined : 'true',
);
expect(closedListCheckbox().attributes('checked')).toEqual(closedCheckbox ? undefined : 'true');
};
it.each`
@ -45,15 +37,28 @@ describe('BoardConfigurationOptions', () => {
${false} | ${false}
`(
'renders two checkbox when one is $backlogCheckboxValue and other is $closedCheckboxValue',
async ({ backlogCheckboxValue, closedCheckboxValue }) => {
await wrapper.setData({
({ backlogCheckboxValue, closedCheckboxValue }) => {
createComponent({
hideBacklogList: backlogCheckboxValue,
hideClosedList: closedCheckboxValue,
});
return wrapper.vm.$nextTick().then(() => {
checkboxAssert(backlogCheckboxValue, closedCheckboxValue);
});
checkboxAssert(backlogCheckboxValue, closedCheckboxValue);
},
);
it('emits a correct value on backlog checkbox change', () => {
createComponent();
backlogListCheckbox().vm.$emit('change');
expect(wrapper.emitted('update:hideBacklogList')).toEqual([[true]]);
});
it('emits a correct value on closed checkbox change', () => {
createComponent();
closedListCheckbox().vm.$emit('change');
expect(wrapper.emitted('update:hideClosedList')).toEqual([[true]]);
});
});

View File

@ -1,19 +1,18 @@
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'jest/helpers/test_constants';
import { GlModal } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import boardsStore from '~/boards/stores/boards_store';
import BoardForm from '~/boards/components/board_form.vue';
import BoardConfigurationOptions from '~/boards/components/board_configuration_options.vue';
import createBoardMutation from '~/boards/graphql/board.mutation.graphql';
import updateBoardMutation from '~/boards/graphql/board_update.mutation.graphql';
import createBoardMutation from '~/boards/graphql/board_create.mutation.graphql';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn().mockName('visitUrlMock'),
stripFinalUrlSegment: jest.requireActual('~/lib/utils/url_utility').stripFinalUrlSegment,
}));
const currentBoard = {
@ -28,18 +27,6 @@ const currentBoard = {
hide_closed_list: false,
};
const boardDefaults = {
id: false,
name: '',
labels: [],
milestone_id: undefined,
assignee: {},
assignee_id: undefined,
weight: null,
hide_backlog_list: false,
hide_closed_list: false,
};
const defaultProps = {
canAdminBoard: false,
labelsPath: `${TEST_HOST}/labels/path`,
@ -51,18 +38,21 @@ const endpoints = {
boardsEndpoint: 'test-endpoint',
};
const mutate = jest.fn().mockResolvedValue({});
const mutate = jest.fn().mockResolvedValue({
data: {
createBoard: { board: { id: 'gid://gitlab/Board/123' } },
updateBoard: { board: { id: 'gid://gitlab/Board/321' } },
},
});
describe('BoardForm', () => {
let wrapper;
let axiosMock;
const findModal = () => wrapper.find(GlModal);
const findModalActionPrimary = () => findModal().props('actionPrimary');
const findForm = () => wrapper.find('[data-testid="board-form"]');
const findFormWrapper = () => wrapper.find('[data-testid="board-form-wrapper"]');
const findDeleteConfirmation = () => wrapper.find('[data-testid="delete-confirmation-message"]');
const findConfigurationOptions = () => wrapper.find(BoardConfigurationOptions);
const findInput = () => wrapper.find('#board-new-name');
const createComponent = (props, data) => {
@ -86,13 +76,12 @@ describe('BoardForm', () => {
};
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
delete window.location;
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
axiosMock.restore();
boardsStore.state.currentPage = null;
});
@ -145,7 +134,7 @@ describe('BoardForm', () => {
});
it('clears the form', () => {
expect(findConfigurationOptions().props('board')).toEqual(boardDefaults);
expect(findInput().element.value).toBe('');
});
it('shows a correct title about creating a board', () => {
@ -164,18 +153,9 @@ describe('BoardForm', () => {
it('renders form wrapper', () => {
expect(findFormWrapper().exists()).toBe(true);
});
it('passes a true isNewForm prop to BoardConfigurationOptions component', () => {
expect(findConfigurationOptions().props('isNewForm')).toBe(true);
});
});
describe('when submitting a create event', () => {
beforeEach(() => {
const url = `${endpoints.boardsEndpoint}.json`;
axiosMock.onPost(url).reply(200, { id: '2', board_path: 'new path' });
});
it('does not call API if board name is empty', async () => {
createComponent({ canAdminBoard: true });
findInput().trigger('keyup.enter', { metaKey: true });
@ -185,7 +165,8 @@ describe('BoardForm', () => {
expect(mutate).not.toHaveBeenCalled();
});
it('calls REST and GraphQL API and redirects to correct page', async () => {
it('calls a correct GraphQL mutation and redirects to correct page from existing board', async () => {
window.location = new URL('https://test/boards/1');
createComponent({ canAdminBoard: true });
findInput().value = 'Test name';
@ -194,19 +175,40 @@ describe('BoardForm', () => {
await waitForPromises();
expect(axiosMock.history.post[0].data).toBe(
JSON.stringify({ board: { ...boardDefaults, name: 'test', label_ids: [''] } }),
);
expect(mutate).toHaveBeenCalledWith({
mutation: createBoardMutation,
variables: {
id: 'gid://gitlab/Board/2',
input: expect.objectContaining({
name: 'test',
}),
},
});
await waitForPromises();
expect(visitUrl).toHaveBeenCalledWith('new path');
expect(visitUrl).toHaveBeenCalledWith('123');
});
it('calls a correct GraphQL mutation and redirects to correct page from boards list', async () => {
window.location = new URL('https://test/boards');
createComponent({ canAdminBoard: true });
findInput().value = 'Test name';
findInput().trigger('input');
findInput().trigger('keyup.enter', { metaKey: true });
await waitForPromises();
expect(mutate).toHaveBeenCalledWith({
mutation: createBoardMutation,
variables: {
input: expect.objectContaining({
name: 'test',
}),
},
});
await waitForPromises();
expect(visitUrl).toHaveBeenCalledWith('boards/123');
});
});
});
@ -222,7 +224,7 @@ describe('BoardForm', () => {
});
it('clears the form', () => {
expect(findConfigurationOptions().props('board')).toEqual(currentBoard);
expect(findInput().element.value).toEqual(currentBoard.name);
});
it('shows a correct title about creating a board', () => {
@ -241,35 +243,28 @@ describe('BoardForm', () => {
it('renders form wrapper', () => {
expect(findFormWrapper().exists()).toBe(true);
});
it('passes a false isNewForm prop to BoardConfigurationOptions component', () => {
expect(findConfigurationOptions().props('isNewForm')).toBe(false);
});
});
describe('when submitting an update event', () => {
beforeEach(() => {
const url = endpoints.boardsEndpoint;
axiosMock.onPut(url).reply(200, { board_path: 'new path' });
});
it('calls REST and GraphQL API with correct parameters', async () => {
window.location = new URL('https://test/boards/1');
createComponent({ canAdminBoard: true });
findInput().trigger('keyup.enter', { metaKey: true });
await waitForPromises();
expect(axiosMock.history.put[0].data).toBe(
JSON.stringify({ board: { ...currentBoard, label_ids: [''] } }),
);
expect(mutate).toHaveBeenCalledWith({
mutation: createBoardMutation,
mutation: updateBoardMutation,
variables: {
id: `gid://gitlab/Board/${currentBoard.id}`,
input: expect.objectContaining({
id: `gid://gitlab/Board/${currentBoard.id}`,
}),
},
});
await waitForPromises();
expect(visitUrl).toHaveBeenCalledWith('321');
});
});
});

View File

@ -1,4 +1,5 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import { GlLoadingIcon, GlTab } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
@ -8,8 +9,7 @@ import JobsList from '~/ide/components/jobs/list.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import IDEServices from '~/ide/services';
const localVue = createLocalVue();
localVue.use(Vuex);
Vue.use(Vuex);
jest.mock('~/ide/services', () => ({
pingUsage: jest.fn(),
@ -59,9 +59,6 @@ describe('IDE pipelines list', () => {
failedStages: failedStagesGetterMock,
pipelineFailed: () => false,
},
methods: {
fetchLatestPipeline: jest.fn(),
},
},
},
});
@ -69,7 +66,6 @@ describe('IDE pipelines list', () => {
const createComponent = (state = {}, pipelinesState = {}) => {
wrapper = shallowMount(List, {
localVue,
store: createStore(state, pipelinesState),
});
};

View File

@ -51,6 +51,16 @@ describe('tooltips/components/tooltips.vue', () => {
expect(wrapper.find(GlTooltip).props('target')).toBe(target);
});
it('does not attach a tooltip to a target with empty title', async () => {
target.setAttribute('title', '');
wrapper.vm.addTooltips([target]);
await wrapper.vm.$nextTick();
expect(wrapper.find(GlTooltip).exists()).toBe(false);
});
it('does not attach a tooltip twice to the same element', async () => {
wrapper.vm.addTooltips([target]);
wrapper.vm.addTooltips([target]);

View File

@ -42,7 +42,7 @@ describe('tooltips/index.js', () => {
};
beforeEach(() => {
window.gon.glTooltipsEnabled = true;
window.gon.features = { glTooltips: true };
});
afterEach(() => {
@ -149,7 +149,7 @@ describe('tooltips/index.js', () => {
describe('when glTooltipsEnabled feature flag is disabled', () => {
beforeEach(() => {
window.gon.glTooltipsEnabled = false;
window.gon.features.glTooltips = false;
});
it.each`

View File

@ -17,7 +17,13 @@ RSpec.describe 'Getting starredProjects of the user' do
let_it_be(:user, reload: true) { create(:user) }
let(:user_fields) { 'starredProjects { nodes { id } }' }
let(:starred_projects) { graphql_data_at(:user, :starred_projects, :nodes) }
let(:current_user) { nil }
let(:starred_projects) do
post_graphql(query, current_user: current_user)
graphql_data_at(:user, :starred_projects, :nodes)
end
before do
project_b.add_reporter(user)
@ -26,11 +32,13 @@ RSpec.describe 'Getting starredProjects of the user' do
user.toggle_star(project_a)
user.toggle_star(project_b)
user.toggle_star(project_c)
post_graphql(query)
end
it_behaves_like 'a working graphql query'
it_behaves_like 'a working graphql query' do
before do
post_graphql(query)
end
end
it 'found only public project' do
expect(starred_projects).to contain_exactly(
@ -41,10 +49,6 @@ RSpec.describe 'Getting starredProjects of the user' do
context 'the current user is the user' do
let(:current_user) { user }
before do
post_graphql(query, current_user: current_user)
end
it 'found all projects' do
expect(starred_projects).to contain_exactly(
a_hash_including('id' => global_id_of(project_a)),
@ -56,11 +60,10 @@ RSpec.describe 'Getting starredProjects of the user' do
context 'the current user is a member of a private project the user starred' do
let_it_be(:other_user) { create(:user) }
let(:current_user) { other_user }
before do
project_b.add_reporter(other_user)
post_graphql(query, current_user: other_user)
end
it 'finds public and member projects' do
@ -74,7 +77,6 @@ RSpec.describe 'Getting starredProjects of the user' do
context 'the user has a private profile' do
before do
user.update!(private_profile: true)
post_graphql(query, current_user: current_user)
end
context 'the current user does not have access to view the private profile of the user' do

View File

@ -3,220 +3,10 @@
require 'spec_helper'
RSpec.describe API::ProjectRepositoryStorageMoves do
include AccessMatchersForRequest
let_it_be(:user) { create(:admin) }
let_it_be(:project) { create(:project, :repository).tap { |project| project.track_project_repository } }
let_it_be(:storage_move) { create(:project_repository_storage_move, :scheduled, container: project) }
shared_examples 'get single project repository storage move' do
let(:project_repository_storage_move_id) { storage_move.id }
def get_project_repository_storage_move
get api(url, user)
end
it 'returns a project repository storage move' do
get_project_repository_storage_move
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/project_repository_storage_move')
expect(json_response['id']).to eq(storage_move.id)
expect(json_response['state']).to eq(storage_move.human_state_name)
end
context 'non-existent project repository storage move' do
let(:project_repository_storage_move_id) { non_existing_record_id }
it 'returns not found' do
get_project_repository_storage_move
expect(response).to have_gitlab_http_status(:not_found)
end
end
describe 'permissions' do
it { expect { get_project_repository_storage_move }.to be_allowed_for(:admin) }
it { expect { get_project_repository_storage_move }.to be_denied_for(:user) }
end
end
shared_examples 'get project repository storage move list' do
def get_project_repository_storage_moves
get api(url, user)
end
it 'returns project repository storage moves' do
get_project_repository_storage_moves
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(response).to match_response_schema('public_api/v4/project_repository_storage_moves')
expect(json_response.size).to eq(1)
expect(json_response.first['id']).to eq(storage_move.id)
expect(json_response.first['state']).to eq(storage_move.human_state_name)
end
it 'avoids N+1 queries', :request_store do
# prevent `let` from polluting the control
get_project_repository_storage_moves
control = ActiveRecord::QueryRecorder.new { get_project_repository_storage_moves }
create(:project_repository_storage_move, :scheduled, container: project)
expect { get_project_repository_storage_moves }.not_to exceed_query_limit(control)
end
it 'returns the most recently created first' do
storage_move_oldest = create(:project_repository_storage_move, :scheduled, container: project, created_at: 2.days.ago)
storage_move_middle = create(:project_repository_storage_move, :scheduled, container: project, created_at: 1.day.ago)
get_project_repository_storage_moves
json_ids = json_response.map {|storage_move| storage_move['id'] }
expect(json_ids).to eq([
storage_move.id,
storage_move_middle.id,
storage_move_oldest.id
])
end
describe 'permissions' do
it { expect { get_project_repository_storage_moves }.to be_allowed_for(:admin) }
it { expect { get_project_repository_storage_moves }.to be_denied_for(:user) }
end
end
describe 'GET /project_repository_storage_moves' do
it_behaves_like 'get project repository storage move list' do
let(:url) { '/project_repository_storage_moves' }
end
end
describe 'GET /project_repository_storage_moves/:repository_storage_move_id' do
it_behaves_like 'get single project repository storage move' do
let(:url) { "/project_repository_storage_moves/#{project_repository_storage_move_id}" }
end
end
describe 'GET /projects/:id/repository_storage_moves' do
it_behaves_like 'get project repository storage move list' do
let(:url) { "/projects/#{project.id}/repository_storage_moves" }
end
end
describe 'GET /projects/:id/repository_storage_moves/:repository_storage_move_id' do
it_behaves_like 'get single project repository storage move' do
let(:url) { "/projects/#{project.id}/repository_storage_moves/#{project_repository_storage_move_id}" }
end
end
describe 'POST /projects/:id/repository_storage_moves' do
let(:url) { "/projects/#{project.id}/repository_storage_moves" }
let(:destination_storage_name) { 'test_second_storage' }
def create_project_repository_storage_move
post api(url, user), params: { destination_storage_name: destination_storage_name }
end
before do
stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' })
end
it 'schedules a project repository storage move' do
create_project_repository_storage_move
storage_move = project.repository_storage_moves.last
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('public_api/v4/project_repository_storage_move')
expect(json_response['id']).to eq(storage_move.id)
expect(json_response['state']).to eq('scheduled')
expect(json_response['source_storage_name']).to eq('default')
expect(json_response['destination_storage_name']).to eq(destination_storage_name)
end
describe 'permissions' do
it { expect { create_project_repository_storage_move }.to be_allowed_for(:admin) }
it { expect { create_project_repository_storage_move }.to be_denied_for(:user) }
end
context 'destination_storage_name is missing' do
let(:destination_storage_name) { nil }
it 'schedules a project repository storage move' do
create_project_repository_storage_move
storage_move = project.repository_storage_moves.last
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('public_api/v4/project_repository_storage_move')
expect(json_response['id']).to eq(storage_move.id)
expect(json_response['state']).to eq('scheduled')
expect(json_response['source_storage_name']).to eq('default')
expect(json_response['destination_storage_name']).to be_present
end
end
end
describe 'POST /project_repository_storage_moves' do
let(:source_storage_name) { 'default' }
let(:destination_storage_name) { 'test_second_storage' }
def create_project_repository_storage_moves
post api('/project_repository_storage_moves', user), params: {
source_storage_name: source_storage_name,
destination_storage_name: destination_storage_name
}
end
before do
stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' })
end
it 'schedules the worker' do
expect(ProjectScheduleBulkRepositoryShardMovesWorker).to receive(:perform_async).with(source_storage_name, destination_storage_name)
create_project_repository_storage_moves
expect(response).to have_gitlab_http_status(:accepted)
end
context 'source_storage_name is invalid' do
let(:destination_storage_name) { 'not-a-real-storage' }
it 'gives an error' do
create_project_repository_storage_moves
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'destination_storage_name is missing' do
let(:destination_storage_name) { nil }
it 'schedules the worker' do
expect(ProjectScheduleBulkRepositoryShardMovesWorker).to receive(:perform_async).with(source_storage_name, destination_storage_name)
create_project_repository_storage_moves
expect(response).to have_gitlab_http_status(:accepted)
end
end
context 'destination_storage_name is invalid' do
let(:destination_storage_name) { 'not-a-real-storage' }
it 'gives an error' do
create_project_repository_storage_moves
expect(response).to have_gitlab_http_status(:bad_request)
end
end
describe 'normal user' do
it { expect { create_project_repository_storage_moves }.to be_denied_for(:user) }
end
it_behaves_like 'repository_storage_moves API', 'projects' do
let_it_be(:container) { create(:project, :repository).tap { |project| project.track_project_repository } }
let_it_be(:storage_move) { create(:project_repository_storage_move, :scheduled, container: container) }
let(:repository_storage_move_factory) { :project_repository_storage_move }
let(:bulk_worker_klass) { ProjectScheduleBulkRepositoryShardMovesWorker }
end
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::SnippetRepositoryStorageMoves do
it_behaves_like 'repository_storage_moves API', 'snippets' do
let_it_be(:container) { create(:snippet, :repository).tap { |snippet| snippet.create_repository } }
let_it_be(:storage_move) { create(:snippet_repository_storage_move, :scheduled, container: container) }
let(:repository_storage_move_factory) { :snippet_repository_storage_move }
let(:bulk_worker_klass) { SnippetScheduleBulkRepositoryShardMovesWorker }
end
end

View File

@ -17,23 +17,55 @@ RSpec.shared_examples 'User views wiki sidebar' do
create(:wiki_page, wiki: wiki, title: 'another', content: 'another')
end
it 'renders a default sidebar when there is no customized sidebar' do
visit wiki_path(wiki)
expect(page).to have_content('another')
expect(page).not_to have_link('View All Pages')
end
context 'when there is a customized sidebar' do
context 'when there is no custom sidebar' do
before do
create(:wiki_page, wiki: wiki, title: '_sidebar', content: 'My customized sidebar')
visit wiki_path(wiki)
end
it 'renders my customized sidebar instead of the default one' do
visit wiki_path(wiki)
it 'renders a default sidebar' do
within('.right-sidebar') do
expect(page).to have_content('another')
expect(page).not_to have_link('View All Pages')
end
end
expect(page).to have_content('My customized sidebar')
expect(page).not_to have_content('Another')
it 'can create a custom sidebar' do
click_on 'Edit sidebar'
fill_in :wiki_content, with: 'My custom sidebar'
click_on 'Create page'
within('.right-sidebar') do
expect(page).to have_content('My custom sidebar')
expect(page).not_to have_content('another')
end
end
end
context 'when there is a custom sidebar' do
before do
create(:wiki_page, wiki: wiki, title: '_sidebar', content: 'My custom sidebar')
visit wiki_path(wiki)
end
it 'renders the custom sidebar instead of the default one' do
within('.right-sidebar') do
expect(page).to have_content('My custom sidebar')
expect(page).not_to have_content('another')
end
end
it 'can edit the custom sidebar' do
click_on 'Edit sidebar'
expect(page).to have_field(:wiki_content, with: 'My custom sidebar')
fill_in :wiki_content, with: 'My other custom sidebar'
click_on 'Save changes'
within('.right-sidebar') do
expect(page).to have_content('My other custom sidebar')
end
end
end
end

View File

@ -0,0 +1,219 @@
# frozen_string_literal: true
RSpec.shared_examples 'repository_storage_moves API' do |container_type|
include AccessMatchersForRequest
let_it_be(:user) { create(:admin) }
shared_examples 'get single container repository storage move' do
let(:repository_storage_move_id) { storage_move.id }
def get_container_repository_storage_move
get api(url, user)
end
it 'returns a container repository storage move', :aggregate_failures do
get_container_repository_storage_move
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema("public_api/v4/#{container_type.singularize}_repository_storage_move")
expect(json_response['id']).to eq(storage_move.id)
expect(json_response['state']).to eq(storage_move.human_state_name)
end
context 'non-existent container repository storage move' do
let(:repository_storage_move_id) { non_existing_record_id }
it 'returns not found' do
get_container_repository_storage_move
expect(response).to have_gitlab_http_status(:not_found)
end
end
describe 'permissions' do
it { expect { get_container_repository_storage_move }.to be_allowed_for(:admin) }
it { expect { get_container_repository_storage_move }.to be_denied_for(:user) }
end
end
shared_examples 'get container repository storage move list' do
def get_container_repository_storage_moves
get api(url, user)
end
it 'returns container repository storage moves', :aggregate_failures do
get_container_repository_storage_moves
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(response).to match_response_schema("public_api/v4/#{container_type.singularize}_repository_storage_moves")
expect(json_response.size).to eq(1)
expect(json_response.first['id']).to eq(storage_move.id)
expect(json_response.first['state']).to eq(storage_move.human_state_name)
end
it 'avoids N+1 queries', :request_store do
# prevent `let` from polluting the control
get_container_repository_storage_moves
control = ActiveRecord::QueryRecorder.new { get_container_repository_storage_moves }
create(repository_storage_move_factory, :scheduled, container: container)
expect { get_container_repository_storage_moves }.not_to exceed_query_limit(control)
end
it 'returns the most recently created first' do
storage_move_oldest = create(repository_storage_move_factory, :scheduled, container: container, created_at: 2.days.ago)
storage_move_middle = create(repository_storage_move_factory, :scheduled, container: container, created_at: 1.day.ago)
get_container_repository_storage_moves
json_ids = json_response.map {|storage_move| storage_move['id'] }
expect(json_ids).to eq([
storage_move.id,
storage_move_middle.id,
storage_move_oldest.id
])
end
describe 'permissions' do
it { expect { get_container_repository_storage_moves }.to be_allowed_for(:admin) }
it { expect { get_container_repository_storage_moves }.to be_denied_for(:user) }
end
end
describe "GET /#{container_type}/:id/repository_storage_moves" do
it_behaves_like 'get container repository storage move list' do
let(:url) { "/#{container_type}/#{container.id}/repository_storage_moves" }
end
end
describe "GET /#{container_type}/:id/repository_storage_moves/:repository_storage_move_id" do
it_behaves_like 'get single container repository storage move' do
let(:url) { "/#{container_type}/#{container.id}/repository_storage_moves/#{repository_storage_move_id}" }
end
end
describe "GET /#{container_type.singularize}_repository_storage_moves" do
it_behaves_like 'get container repository storage move list' do
let(:url) { "/#{container_type.singularize}_repository_storage_moves" }
end
end
describe "GET /#{container_type.singularize}_repository_storage_moves/:repository_storage_move_id" do
it_behaves_like 'get single container repository storage move' do
let(:url) { "/#{container_type.singularize}_repository_storage_moves/#{repository_storage_move_id}" }
end
end
describe "POST /#{container_type}/:id/repository_storage_moves" do
let(:url) { "/#{container_type}/#{container.id}/repository_storage_moves" }
let(:destination_storage_name) { 'test_second_storage' }
def create_container_repository_storage_move
post api(url, user), params: { destination_storage_name: destination_storage_name }
end
before do
stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' })
end
it 'schedules a container repository storage move', :aggregate_failures do
create_container_repository_storage_move
storage_move = container.repository_storage_moves.last
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema("public_api/v4/#{container_type.singularize}_repository_storage_move")
expect(json_response['id']).to eq(storage_move.id)
expect(json_response['state']).to eq('scheduled')
expect(json_response['source_storage_name']).to eq('default')
expect(json_response['destination_storage_name']).to eq(destination_storage_name)
end
describe 'permissions' do
it { expect { create_container_repository_storage_move }.to be_allowed_for(:admin) }
it { expect { create_container_repository_storage_move }.to be_denied_for(:user) }
end
context 'destination_storage_name is missing', :aggregate_failures do
let(:destination_storage_name) { nil }
it 'schedules a container repository storage move' do
create_container_repository_storage_move
storage_move = container.repository_storage_moves.last
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema("public_api/v4/#{container_type.singularize}_repository_storage_move")
expect(json_response['id']).to eq(storage_move.id)
expect(json_response['state']).to eq('scheduled')
expect(json_response['source_storage_name']).to eq('default')
expect(json_response['destination_storage_name']).to be_present
end
end
end
describe "POST /#{container_type.singularize}_repository_storage_moves" do
let(:url) { "/#{container_type.singularize}_repository_storage_moves" }
let(:source_storage_name) { 'default' }
let(:destination_storage_name) { 'test_second_storage' }
def create_container_repository_storage_moves
post api(url, user), params: {
source_storage_name: source_storage_name,
destination_storage_name: destination_storage_name
}
end
before do
stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' })
end
it 'schedules the worker' do
expect(bulk_worker_klass).to receive(:perform_async).with(source_storage_name, destination_storage_name)
create_container_repository_storage_moves
expect(response).to have_gitlab_http_status(:accepted)
end
context 'source_storage_name is invalid' do
let(:destination_storage_name) { 'not-a-real-storage' }
it 'gives an error' do
create_container_repository_storage_moves
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'destination_storage_name is missing' do
let(:destination_storage_name) { nil }
it 'schedules the worker' do
expect(bulk_worker_klass).to receive(:perform_async).with(source_storage_name, destination_storage_name)
create_container_repository_storage_moves
expect(response).to have_gitlab_http_status(:accepted)
end
end
context 'destination_storage_name is invalid' do
let(:destination_storage_name) { 'not-a-real-storage' }
it 'gives an error' do
create_container_repository_storage_moves
expect(response).to have_gitlab_http_status(:bad_request)
end
end
describe 'normal user' do
it { expect { create_container_repository_storage_moves }.to be_denied_for(:user) }
end
end
end

View File

@ -80,4 +80,28 @@ RSpec.describe 'shared/wikis/_sidebar.html.haml' do
end
end
end
describe 'link to edit the sidebar' do
before do
allow(view).to receive(:can?).with(anything, :create_wiki, anything).and_return(can_edit)
render
end
context 'when the user has edit permission' do
let(:can_edit) { true }
it 'renders the link' do
expect(rendered).to have_link('Edit sidebar', href: wiki_page_path(wiki, Wiki::SIDEBAR, action: :edit))
end
end
context 'when the user does not have edit permission' do
let(:can_edit) { false }
it 'does not render the link' do
expect(rendered).not_to have_link('Edit sidebar')
end
end
end
end