Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
5c9f6c66fa
commit
9dbca64417
36 changed files with 3241 additions and 489 deletions
2470
.prettierignore
2470
.prettierignore
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"arrowParens": "avoid",
|
||||
"arrowParens": "always",
|
||||
"trailingComma": "all"
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
mutation UpdateBoard($id: BoardID!, $hideClosedList: Boolean, $hideBacklogList: Boolean) {
|
||||
updateBoard(
|
||||
input: { id: $id, hideClosedList: $hideClosedList, hideBacklogList: $hideBacklogList }
|
||||
) {
|
||||
board {
|
||||
id
|
||||
hideClosedList
|
||||
hideBacklogList
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
mutation createBoard($input: CreateBoardInput!) {
|
||||
createBoard(input: $input) {
|
||||
board {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
mutation UpdateBoard($input: UpdateBoardInput!) {
|
||||
updateBoard(input: $input) {
|
||||
board {
|
||||
id
|
||||
hideClosedList
|
||||
hideBacklogList
|
||||
}
|
||||
}
|
||||
}
|
|
@ -335,7 +335,6 @@ export default () => {
|
|||
}
|
||||
|
||||
mountMultipleBoardsSwitcher({
|
||||
boardsEndpoint: $boardApp.dataset.boardsEndpoint,
|
||||
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
|
||||
fullPath: $boardApp.dataset.fullPath,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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.')
|
||||
|
|
|
@ -23,4 +23,4 @@
|
|||
|
||||
= render 'shared/wikis/form', uploads_path: wiki_attachment_upload_url
|
||||
|
||||
= render 'shared/wikis/sidebar'
|
||||
= render 'shared/wikis/sidebar', editing: true
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add snippet repository storage move API endpoints
|
||||
merge_request: 49228
|
||||
author:
|
||||
type: added
|
5
changelogs/unreleased/23109-add-wiki-edit-button.yml
Normal file
5
changelogs/unreleased/23109-add-wiki-edit-button.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add button to edit custom Wiki sidebar
|
||||
merge_request: 50323
|
||||
author: Frank Li
|
||||
type: changed
|
|
@ -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
|
8
config/feature_flags/development/gl_tooltips.yml
Normal file
8
config/feature_flags/development/gl_tooltips.yml
Normal 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
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
110
lib/api/snippet_repository_storage_moves.rb
Normal file
110
lib/api/snippet_repository_storage_moves.rb
Normal 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
|
|
@ -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)
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
20
spec/fixtures/api/schemas/public_api/v4/snippet_repository_storage_move.json
vendored
Normal file
20
spec/fixtures/api/schemas/public_api/v4/snippet_repository_storage_move.json
vendored
Normal 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
|
||||
}
|
6
spec/fixtures/api/schemas/public_api/v4/snippet_repository_storage_moves.json
vendored
Normal file
6
spec/fixtures/api/schemas/public_api/v4/snippet_repository_storage_moves.json
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "./snippet_repository_storage_move.json"
|
||||
}
|
||||
}
|
|
@ -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]]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
12
spec/requests/api/snippet_repository_storage_moves_spec.rb
Normal file
12
spec/requests/api/snippet_repository_storage_moves_spec.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue