Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1c0289261b
commit
9c8d620e48
2
Gemfile
2
Gemfile
|
@ -345,7 +345,7 @@ group :development do
|
|||
gem 'better_errors', '~> 2.7.1'
|
||||
|
||||
# thin instead webrick
|
||||
gem 'thin', '~> 1.7.0'
|
||||
gem 'thin', '~> 1.8.0'
|
||||
end
|
||||
|
||||
group :development, :test do
|
||||
|
|
|
@ -207,7 +207,7 @@ GEM
|
|||
git
|
||||
css_parser (1.7.0)
|
||||
addressable
|
||||
daemons (1.2.6)
|
||||
daemons (1.3.1)
|
||||
danger (8.0.6)
|
||||
claide (~> 1.0)
|
||||
claide-plugins (>= 0.9.2)
|
||||
|
@ -1180,7 +1180,7 @@ GEM
|
|||
execjs (>= 0.3.0, < 3)
|
||||
test-prof (0.12.0)
|
||||
text (1.3.1)
|
||||
thin (1.7.2)
|
||||
thin (1.8.0)
|
||||
daemons (~> 1.0, >= 1.0.9)
|
||||
eventmachine (~> 1.0, >= 1.0.4)
|
||||
rack (>= 1, < 3)
|
||||
|
@ -1534,7 +1534,7 @@ DEPENDENCIES
|
|||
sys-filesystem (~> 1.1.6)
|
||||
terser (= 1.0.2)
|
||||
test-prof (~> 0.12.0)
|
||||
thin (~> 1.7.0)
|
||||
thin (~> 1.8.0)
|
||||
timecop (~> 0.9.1)
|
||||
toml-rb (~> 1.0.0)
|
||||
truncato (~> 0.7.11)
|
||||
|
|
|
@ -5,8 +5,8 @@ import { deprecatedCreateFlash as Flash } from '~/flash';
|
|||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import { getParameterByName } from '~/lib/utils/common_utils';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import boardsStore from '~/boards/stores/boards_store';
|
||||
import { fullLabelId, fullBoardId } from '../boards_util';
|
||||
import { formType } from '../constants';
|
||||
|
||||
import updateBoardMutation from '../graphql/board_update.mutation.graphql';
|
||||
import createBoardMutation from '../graphql/board_create.mutation.graphql';
|
||||
|
@ -26,12 +26,6 @@ const boardDefaults = {
|
|||
hide_closed_list: false,
|
||||
};
|
||||
|
||||
const formType = {
|
||||
new: 'new',
|
||||
delete: 'delete',
|
||||
edit: 'edit',
|
||||
};
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
[formType.new]: { title: s__('Board|Create new board'), btnText: s__('Board|Create board') },
|
||||
|
@ -100,11 +94,14 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
currentPage: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
board: { ...boardDefaults, ...this.currentBoard },
|
||||
currentPage: boardsStore.state.currentPage,
|
||||
isLoading: false,
|
||||
};
|
||||
},
|
||||
|
@ -256,7 +253,7 @@ export default {
|
|||
}
|
||||
},
|
||||
cancel() {
|
||||
boardsStore.showPage('');
|
||||
this.$emit('cancel');
|
||||
},
|
||||
resetFormState() {
|
||||
if (this.isNewForm) {
|
||||
|
|
|
@ -12,11 +12,12 @@ import {
|
|||
|
||||
import httpStatusCodes from '~/lib/utils/http_status';
|
||||
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import projectQuery from '../graphql/project_boards.query.graphql';
|
||||
import groupQuery from '../graphql/group_boards.query.graphql';
|
||||
|
||||
import boardsStore from '../stores/boards_store';
|
||||
import eventHub from '../eventhub';
|
||||
import BoardForm from './board_form.vue';
|
||||
|
||||
const MIN_BOARDS_TO_VIEW_RECENT = 10;
|
||||
|
@ -35,6 +36,7 @@ export default {
|
|||
directives: {
|
||||
GlModalDirective,
|
||||
},
|
||||
inject: ['fullPath', 'recentBoardsEndpoint'],
|
||||
props: {
|
||||
currentBoard: {
|
||||
type: Object,
|
||||
|
@ -99,12 +101,11 @@ export default {
|
|||
scrollFadeInitialized: false,
|
||||
boards: [],
|
||||
recentBoards: [],
|
||||
state: boardsStore.state,
|
||||
throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration),
|
||||
contentClientHeight: 0,
|
||||
maxPosition: 0,
|
||||
store: boardsStore,
|
||||
filterTerm: '',
|
||||
currentPage: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -114,16 +115,13 @@ export default {
|
|||
loading() {
|
||||
return this.loadingRecentBoards || Boolean(this.loadingBoards);
|
||||
},
|
||||
currentPage() {
|
||||
return this.state.currentPage;
|
||||
},
|
||||
filteredBoards() {
|
||||
return this.boards.filter((board) =>
|
||||
board.name.toLowerCase().includes(this.filterTerm.toLowerCase()),
|
||||
);
|
||||
},
|
||||
board() {
|
||||
return this.state.currentBoard;
|
||||
return this.currentBoard;
|
||||
},
|
||||
showDelete() {
|
||||
return this.boards.length > 1;
|
||||
|
@ -148,11 +146,17 @@ export default {
|
|||
},
|
||||
},
|
||||
created() {
|
||||
boardsStore.setCurrentBoard(this.currentBoard);
|
||||
eventHub.$on('showBoardModal', this.showPage);
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('showBoardModal', this.showPage);
|
||||
},
|
||||
methods: {
|
||||
showPage(page) {
|
||||
boardsStore.showPage(page);
|
||||
this.currentPage = page;
|
||||
},
|
||||
cancel() {
|
||||
this.showPage('');
|
||||
},
|
||||
loadBoards(toggleDropdown = true) {
|
||||
if (toggleDropdown && this.boards.length > 0) {
|
||||
|
@ -161,7 +165,7 @@ export default {
|
|||
|
||||
this.$apollo.addSmartQuery('boards', {
|
||||
variables() {
|
||||
return { fullPath: this.state.endpoints.fullPath };
|
||||
return { fullPath: this.fullPath };
|
||||
},
|
||||
query() {
|
||||
return this.groupId ? groupQuery : projectQuery;
|
||||
|
@ -179,8 +183,10 @@ export default {
|
|||
});
|
||||
|
||||
this.loadingRecentBoards = true;
|
||||
boardsStore
|
||||
.recentBoards()
|
||||
// Follow up to fetch recent boards using GraphQL
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/300985
|
||||
axios
|
||||
.get(this.recentBoardsEndpoint)
|
||||
.then((res) => {
|
||||
this.recentBoards = res.data;
|
||||
})
|
||||
|
@ -346,6 +352,8 @@ export default {
|
|||
:weights="weights"
|
||||
:enable-scoped-labels="enabledScopedLabels"
|
||||
:current-board="currentBoard"
|
||||
:current-page="currentPage"
|
||||
@cancel="cancel"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,357 @@
|
|||
<script>
|
||||
import { throttle } from 'lodash';
|
||||
import {
|
||||
GlLoadingIcon,
|
||||
GlSearchBoxByType,
|
||||
GlDropdown,
|
||||
GlDropdownDivider,
|
||||
GlDropdownSectionHeader,
|
||||
GlDropdownItem,
|
||||
GlModalDirective,
|
||||
} from '@gitlab/ui';
|
||||
|
||||
import httpStatusCodes from '~/lib/utils/http_status';
|
||||
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import projectQuery from '../graphql/project_boards.query.graphql';
|
||||
import groupQuery from '../graphql/group_boards.query.graphql';
|
||||
|
||||
import boardsStore from '../stores/boards_store';
|
||||
import BoardForm from './board_form.vue';
|
||||
|
||||
const MIN_BOARDS_TO_VIEW_RECENT = 10;
|
||||
|
||||
export default {
|
||||
name: 'BoardsSelector',
|
||||
components: {
|
||||
BoardForm,
|
||||
GlLoadingIcon,
|
||||
GlSearchBoxByType,
|
||||
GlDropdown,
|
||||
GlDropdownDivider,
|
||||
GlDropdownSectionHeader,
|
||||
GlDropdownItem,
|
||||
},
|
||||
directives: {
|
||||
GlModalDirective,
|
||||
},
|
||||
props: {
|
||||
currentBoard: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
throttleDuration: {
|
||||
type: Number,
|
||||
default: 200,
|
||||
required: false,
|
||||
},
|
||||
boardBaseUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
hasMissingBoards: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
canAdminBoard: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
multipleIssueBoardsAvailable: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
labelsPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
labelsWebUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
groupId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
scopedIssueBoardFeatureEnabled: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
weights: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
enabledScopedLabels: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasScrollFade: false,
|
||||
loadingBoards: 0,
|
||||
loadingRecentBoards: false,
|
||||
scrollFadeInitialized: false,
|
||||
boards: [],
|
||||
recentBoards: [],
|
||||
state: boardsStore.state,
|
||||
throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration),
|
||||
contentClientHeight: 0,
|
||||
maxPosition: 0,
|
||||
store: boardsStore,
|
||||
filterTerm: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
parentType() {
|
||||
return this.groupId ? 'group' : 'project';
|
||||
},
|
||||
loading() {
|
||||
return this.loadingRecentBoards || Boolean(this.loadingBoards);
|
||||
},
|
||||
currentPage() {
|
||||
return this.state.currentPage;
|
||||
},
|
||||
filteredBoards() {
|
||||
return this.boards.filter((board) =>
|
||||
board.name.toLowerCase().includes(this.filterTerm.toLowerCase()),
|
||||
);
|
||||
},
|
||||
board() {
|
||||
return this.state.currentBoard;
|
||||
},
|
||||
showDelete() {
|
||||
return this.boards.length > 1;
|
||||
},
|
||||
scrollFadeClass() {
|
||||
return {
|
||||
'fade-out': !this.hasScrollFade,
|
||||
};
|
||||
},
|
||||
showRecentSection() {
|
||||
return (
|
||||
this.recentBoards.length &&
|
||||
this.boards.length > MIN_BOARDS_TO_VIEW_RECENT &&
|
||||
!this.filterTerm.length
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
filteredBoards() {
|
||||
this.scrollFadeInitialized = false;
|
||||
this.$nextTick(this.setScrollFade);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
boardsStore.setCurrentBoard(this.currentBoard);
|
||||
},
|
||||
methods: {
|
||||
showPage(page) {
|
||||
boardsStore.showPage(page);
|
||||
},
|
||||
cancel() {
|
||||
this.showPage('');
|
||||
},
|
||||
loadBoards(toggleDropdown = true) {
|
||||
if (toggleDropdown && this.boards.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$apollo.addSmartQuery('boards', {
|
||||
variables() {
|
||||
return { fullPath: this.state.endpoints.fullPath };
|
||||
},
|
||||
query() {
|
||||
return this.groupId ? groupQuery : projectQuery;
|
||||
},
|
||||
loadingKey: 'loadingBoards',
|
||||
update(data) {
|
||||
if (!data?.[this.parentType]) {
|
||||
return [];
|
||||
}
|
||||
return data[this.parentType].boards.edges.map(({ node }) => ({
|
||||
id: getIdFromGraphQLId(node.id),
|
||||
name: node.name,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
this.loadingRecentBoards = true;
|
||||
boardsStore
|
||||
.recentBoards()
|
||||
.then((res) => {
|
||||
this.recentBoards = res.data;
|
||||
})
|
||||
.catch((err) => {
|
||||
/**
|
||||
* If user is unauthorized we'd still want to resolve the
|
||||
* request to display all boards.
|
||||
*/
|
||||
if (err?.response?.status === httpStatusCodes.UNAUTHORIZED) {
|
||||
this.recentBoards = []; // recent boards are empty
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
})
|
||||
.then(() => this.$nextTick()) // Wait for boards list in DOM
|
||||
.then(() => {
|
||||
this.setScrollFade();
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
this.loadingRecentBoards = false;
|
||||
});
|
||||
},
|
||||
isScrolledUp() {
|
||||
const { content } = this.$refs;
|
||||
|
||||
if (!content) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentPosition = this.contentClientHeight + content.scrollTop;
|
||||
|
||||
return currentPosition < this.maxPosition;
|
||||
},
|
||||
initScrollFade() {
|
||||
const { content } = this.$refs;
|
||||
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.scrollFadeInitialized = true;
|
||||
|
||||
this.contentClientHeight = content.clientHeight;
|
||||
this.maxPosition = content.scrollHeight;
|
||||
},
|
||||
setScrollFade() {
|
||||
if (!this.scrollFadeInitialized) this.initScrollFade();
|
||||
|
||||
this.hasScrollFade = this.isScrolledUp();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="boards-switcher js-boards-selector gl-mr-3">
|
||||
<span class="boards-selector-wrapper js-boards-selector-wrapper">
|
||||
<gl-dropdown
|
||||
data-qa-selector="boards_dropdown"
|
||||
toggle-class="dropdown-menu-toggle js-dropdown-toggle"
|
||||
menu-class="flex-column dropdown-extended-height"
|
||||
:text="board.name"
|
||||
@show="loadBoards"
|
||||
>
|
||||
<p class="gl-new-dropdown-header-top" @mousedown.prevent>
|
||||
{{ s__('IssueBoards|Switch board') }}
|
||||
</p>
|
||||
<gl-search-box-by-type ref="searchBox" v-model="filterTerm" class="m-2" />
|
||||
|
||||
<div
|
||||
v-if="!loading"
|
||||
ref="content"
|
||||
data-qa-selector="boards_dropdown_content"
|
||||
class="dropdown-content flex-fill"
|
||||
@scroll.passive="throttledSetScrollFade"
|
||||
>
|
||||
<gl-dropdown-item
|
||||
v-show="filteredBoards.length === 0"
|
||||
class="gl-pointer-events-none text-secondary"
|
||||
>
|
||||
{{ s__('IssueBoards|No matching boards found') }}
|
||||
</gl-dropdown-item>
|
||||
|
||||
<gl-dropdown-section-header v-if="showRecentSection">
|
||||
{{ __('Recent') }}
|
||||
</gl-dropdown-section-header>
|
||||
|
||||
<template v-if="showRecentSection">
|
||||
<gl-dropdown-item
|
||||
v-for="recentBoard in recentBoards"
|
||||
:key="`recent-${recentBoard.id}`"
|
||||
class="js-dropdown-item"
|
||||
:href="`${boardBaseUrl}/${recentBoard.id}`"
|
||||
>
|
||||
{{ recentBoard.name }}
|
||||
</gl-dropdown-item>
|
||||
</template>
|
||||
|
||||
<gl-dropdown-divider v-if="showRecentSection" />
|
||||
|
||||
<gl-dropdown-section-header v-if="showRecentSection">
|
||||
{{ __('All') }}
|
||||
</gl-dropdown-section-header>
|
||||
|
||||
<gl-dropdown-item
|
||||
v-for="otherBoard in filteredBoards"
|
||||
:key="otherBoard.id"
|
||||
class="js-dropdown-item"
|
||||
:href="`${boardBaseUrl}/${otherBoard.id}`"
|
||||
>
|
||||
{{ otherBoard.name }}
|
||||
</gl-dropdown-item>
|
||||
|
||||
<gl-dropdown-item v-if="hasMissingBoards" class="no-pointer-events">
|
||||
{{
|
||||
s__(
|
||||
'IssueBoards|Some of your boards are hidden, activate a license to see them again.',
|
||||
)
|
||||
}}
|
||||
</gl-dropdown-item>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="filteredBoards.length > 0"
|
||||
class="dropdown-content-faded-mask"
|
||||
:class="scrollFadeClass"
|
||||
></div>
|
||||
|
||||
<gl-loading-icon v-if="loading" />
|
||||
|
||||
<div v-if="canAdminBoard">
|
||||
<gl-dropdown-divider />
|
||||
|
||||
<gl-dropdown-item
|
||||
v-if="multipleIssueBoardsAvailable"
|
||||
v-gl-modal-directive="'board-config-modal'"
|
||||
data-qa-selector="create_new_board_button"
|
||||
@click.prevent="showPage('new')"
|
||||
>
|
||||
{{ s__('IssueBoards|Create new board') }}
|
||||
</gl-dropdown-item>
|
||||
|
||||
<gl-dropdown-item
|
||||
v-if="showDelete"
|
||||
v-gl-modal-directive="'board-config-modal'"
|
||||
class="text-danger js-delete-board"
|
||||
@click.prevent="showPage('delete')"
|
||||
>
|
||||
{{ s__('IssueBoards|Delete board') }}
|
||||
</gl-dropdown-item>
|
||||
</div>
|
||||
</gl-dropdown>
|
||||
|
||||
<board-form
|
||||
v-if="currentPage"
|
||||
:labels-path="labelsPath"
|
||||
:labels-web-url="labelsWebUrl"
|
||||
:project-id="projectId"
|
||||
:group-id="groupId"
|
||||
:can-admin-board="canAdminBoard"
|
||||
:scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled"
|
||||
:weights="weights"
|
||||
:enable-scoped-labels="enabledScopedLabels"
|
||||
:current-board="currentBoard"
|
||||
:current-page="state.currentPage"
|
||||
@cancel="cancel"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
|
@ -21,6 +21,12 @@ export const ListTypeTitles = {
|
|||
label: __('Label'),
|
||||
};
|
||||
|
||||
export const formType = {
|
||||
new: 'new',
|
||||
delete: 'delete',
|
||||
edit: 'edit',
|
||||
};
|
||||
|
||||
export const inactiveId = 0;
|
||||
|
||||
export const ISSUABLE = 'issuable';
|
||||
|
|
|
@ -358,5 +358,6 @@ export default () => {
|
|||
mountMultipleBoardsSwitcher({
|
||||
fullPath: $boardApp.dataset.fullPath,
|
||||
rootPath: $boardApp.dataset.boardsEndpoint,
|
||||
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { mapGetters } from 'vuex';
|
||||
import store from '~/boards/stores';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import BoardsSelector from '~/boards/components/boards_selector.vue';
|
||||
import BoardsSelectorDeprecated from '~/boards/components/boards_selector_deprecated.vue';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
|
@ -16,11 +20,15 @@ export default (params = {}) => {
|
|||
el: boardsSwitcherElement,
|
||||
components: {
|
||||
BoardsSelector,
|
||||
BoardsSelectorDeprecated,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
apolloProvider,
|
||||
store,
|
||||
provide: {
|
||||
fullPath: params.fullPath,
|
||||
rootPath: params.rootPath,
|
||||
recentBoardsEndpoint: params.recentBoardsEndpoint,
|
||||
},
|
||||
data() {
|
||||
const { dataset } = boardsSwitcherElement;
|
||||
|
@ -39,8 +47,16 @@ export default (params = {}) => {
|
|||
|
||||
return { boardsSelectorProps };
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['shouldUseGraphQL']),
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(BoardsSelector, {
|
||||
if (this.shouldUseGraphQL) {
|
||||
return createElement(BoardsSelector, {
|
||||
props: this.boardsSelectorProps,
|
||||
});
|
||||
}
|
||||
return createElement(BoardsSelectorDeprecated, {
|
||||
props: this.boardsSelectorProps,
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module Boards
|
||||
module Lists
|
||||
class Base < BaseMutation
|
||||
include Mutations::ResolvesIssuable
|
||||
|
||||
argument :board_id, ::Types::GlobalIDType[::Board],
|
||||
required: true,
|
||||
description: 'Global ID of the issue board to mutate.'
|
||||
|
||||
field :list,
|
||||
Types::BoardListType,
|
||||
null: true,
|
||||
description: 'List of the issue board.'
|
||||
|
||||
authorize :admin_list
|
||||
|
||||
private
|
||||
|
||||
def find_object(id:)
|
||||
GitlabSchema.object_from_id(id, expected_type: ::Board)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module Boards
|
||||
module Lists
|
||||
class BaseCreate < BaseMutation
|
||||
argument :backlog, GraphQL::BOOLEAN_TYPE,
|
||||
required: false,
|
||||
description: 'Create the backlog list.'
|
||||
|
||||
argument :label_id, ::Types::GlobalIDType[::Label],
|
||||
required: false,
|
||||
description: 'Global ID of an existing label.'
|
||||
|
||||
def ready?(**args)
|
||||
if args.slice(*mutually_exclusive_args).size != 1
|
||||
arg_str = mutually_exclusive_args.map { |x| x.to_s.camelize(:lower) }.join(' or ')
|
||||
raise Gitlab::Graphql::Errors::ArgumentError, "one and only one of #{arg_str} is required"
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def resolve(**args)
|
||||
board = authorized_find!(id: args[:board_id])
|
||||
params = create_list_params(args)
|
||||
|
||||
response = create_list(board, params)
|
||||
|
||||
{
|
||||
list: response.success? ? response.payload[:list] : nil,
|
||||
errors: response.errors
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_list(board, params)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def create_list_params(args)
|
||||
params = args.slice(*mutually_exclusive_args).with_indifferent_access
|
||||
params[:label_id] &&= ::GitlabSchema.parse_gid(params[:label_id], expected_type: ::Label).model_id
|
||||
|
||||
params
|
||||
end
|
||||
|
||||
def mutually_exclusive_args
|
||||
[:backlog, :label_id]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,59 +3,32 @@
|
|||
module Mutations
|
||||
module Boards
|
||||
module Lists
|
||||
class Create < Base
|
||||
class Create < BaseCreate
|
||||
graphql_name 'BoardListCreate'
|
||||
|
||||
argument :backlog, GraphQL::BOOLEAN_TYPE,
|
||||
required: false,
|
||||
description: 'Create the backlog list.'
|
||||
argument :board_id, ::Types::GlobalIDType[::Board],
|
||||
required: true,
|
||||
description: 'Global ID of the issue board to mutate.'
|
||||
|
||||
argument :label_id, ::Types::GlobalIDType[::Label],
|
||||
required: false,
|
||||
description: 'Global ID of an existing label.'
|
||||
field :list,
|
||||
Types::BoardListType,
|
||||
null: true,
|
||||
description: 'Issue list in the issue board.'
|
||||
|
||||
def ready?(**args)
|
||||
if args.slice(*mutually_exclusive_args).size != 1
|
||||
arg_str = mutually_exclusive_args.map { |x| x.to_s.camelize(:lower) }.join(' or ')
|
||||
raise Gitlab::Graphql::Errors::ArgumentError, "one and only one of #{arg_str} is required"
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def resolve(**args)
|
||||
board = authorized_find!(id: args[:board_id])
|
||||
params = create_list_params(args)
|
||||
|
||||
response = create_list(board, params)
|
||||
|
||||
{
|
||||
list: response.success? ? response.payload[:list] : nil,
|
||||
errors: response.errors
|
||||
}
|
||||
end
|
||||
authorize :admin_list
|
||||
|
||||
private
|
||||
|
||||
def find_object(id:)
|
||||
GitlabSchema.object_from_id(id, expected_type: ::Board)
|
||||
end
|
||||
|
||||
def create_list(board, params)
|
||||
create_list_service =
|
||||
::Boards::Lists::CreateService.new(board.resource_parent, current_user, params)
|
||||
|
||||
create_list_service.execute(board)
|
||||
end
|
||||
|
||||
# Overridden in EE
|
||||
def create_list_params(args)
|
||||
params = args.slice(*mutually_exclusive_args).with_indifferent_access
|
||||
params[:label_id] &&= ::GitlabSchema.parse_gid(params[:label_id], expected_type: ::Label).model_id
|
||||
|
||||
params
|
||||
end
|
||||
|
||||
# Overridden in EE
|
||||
def mutually_exclusive_args
|
||||
[:backlog, :label_id]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,6 +7,12 @@ module Packages
|
|||
|
||||
included do
|
||||
belongs_to :distribution, class_name: "Packages::Debian::#{container_type.capitalize}Distribution", inverse_of: :architectures
|
||||
# files must be destroyed by ruby code in order to properly remove carrierwave uploads
|
||||
has_many :files,
|
||||
class_name: "Packages::Debian::#{container_type.capitalize}ComponentFile",
|
||||
foreign_key: :architecture_id,
|
||||
inverse_of: :architecture,
|
||||
dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
|
||||
validates :distribution,
|
||||
presence: true
|
||||
|
|
|
@ -7,6 +7,12 @@ module Packages
|
|||
|
||||
included do
|
||||
belongs_to :distribution, class_name: "Packages::Debian::#{container_type.capitalize}Distribution", inverse_of: :components
|
||||
# files must be destroyed by ruby code in order to properly remove carrierwave uploads
|
||||
has_many :files,
|
||||
class_name: "Packages::Debian::#{container_type.capitalize}ComponentFile",
|
||||
foreign_key: :component_id,
|
||||
inverse_of: :component,
|
||||
dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
|
||||
validates :distribution,
|
||||
presence: true
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Packages
|
||||
module Debian
|
||||
module ComponentFile
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
include Sortable
|
||||
include FileStoreMounter
|
||||
|
||||
def self.container_foreign_key
|
||||
"#{container_type}_id".to_sym
|
||||
end
|
||||
|
||||
def self.distribution_class
|
||||
"::Packages::Debian::#{container_type.capitalize}Distribution".constantize
|
||||
end
|
||||
|
||||
belongs_to :component, class_name: "Packages::Debian::#{container_type.capitalize}Component", inverse_of: :files
|
||||
belongs_to :architecture, class_name: "Packages::Debian::#{container_type.capitalize}Architecture", inverse_of: :files, optional: true
|
||||
|
||||
enum file_type: { packages: 1, source: 2, di_packages: 3 }
|
||||
enum compression_type: { gz: 1, bz2: 2, xz: 3 }
|
||||
|
||||
validates :component, presence: true
|
||||
validates :file_type, presence: true
|
||||
validates :architecture, presence: true, unless: :source?
|
||||
validates :architecture, absence: true, if: :source?
|
||||
validates :file, length: { minimum: 0, allow_nil: false }
|
||||
validates :size, presence: true
|
||||
validates :file_store, presence: true
|
||||
validates :file_md5, presence: true
|
||||
validates :file_sha256, presence: true
|
||||
|
||||
scope :with_container, ->(container) do
|
||||
joins(component: :distribution)
|
||||
.where("packages_debian_#{container_type}_distributions" => { container_foreign_key => container.id })
|
||||
end
|
||||
|
||||
scope :with_codename_or_suite, ->(codename_or_suite) do
|
||||
joins(component: :distribution)
|
||||
.merge(distribution_class.with_codename_or_suite(codename_or_suite))
|
||||
end
|
||||
|
||||
scope :with_component_name, ->(component_name) do
|
||||
joins(:component)
|
||||
.where("packages_debian_#{container_type}_components" => { name: component_name })
|
||||
end
|
||||
|
||||
scope :with_file_type, ->(file_type) { where(file_type: file_type) }
|
||||
|
||||
scope :with_architecture_name, ->(architecture_name) do
|
||||
left_outer_joins(:architecture)
|
||||
.where("packages_debian_#{container_type}_architectures" => { name: architecture_name })
|
||||
end
|
||||
|
||||
scope :with_compression_type, ->(compression_type) { where(compression_type: compression_type) }
|
||||
scope :with_file_sha256, ->(file_sha256) { where(file_sha256: file_sha256) }
|
||||
|
||||
scope :preload_distribution, -> { includes(component: :distribution) }
|
||||
|
||||
mount_file_store_uploader Packages::Debian::ComponentFileUploader
|
||||
|
||||
before_validation :update_size_from_file
|
||||
|
||||
def file_name
|
||||
case file_type
|
||||
when 'di_packages'
|
||||
'Packages'
|
||||
else
|
||||
file_type.capitalize
|
||||
end
|
||||
end
|
||||
|
||||
def relative_path
|
||||
case file_type
|
||||
when 'packages'
|
||||
"#{component.name}/binary-#{architecture.name}/#{file_name}#{extension}"
|
||||
when 'source'
|
||||
"#{component.name}/source/#{file_name}#{extension}"
|
||||
when 'di_packages'
|
||||
"#{component.name}/debian-installer/binary-#{architecture.name}/#{file_name}#{extension}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def extension
|
||||
return '' unless compression_type
|
||||
|
||||
".#{compression_type}"
|
||||
end
|
||||
|
||||
def update_size_from_file
|
||||
self.size ||= file.size
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -18,10 +18,16 @@ module Packages
|
|||
belongs_to container_type
|
||||
belongs_to :creator, class_name: 'User'
|
||||
|
||||
# component_files must be destroyed by ruby code in order to properly remove carrierwave uploads
|
||||
has_many :components,
|
||||
class_name: "Packages::Debian::#{container_type.capitalize}Component",
|
||||
foreign_key: :distribution_id,
|
||||
inverse_of: :distribution
|
||||
inverse_of: :distribution,
|
||||
dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :component_files,
|
||||
through: :components,
|
||||
source: :files,
|
||||
class_name: "Packages::Debian::#{container_type.capitalize}ComponentFile"
|
||||
has_many :architectures,
|
||||
class_name: "Packages::Debian::#{container_type.capitalize}Architecture",
|
||||
foreign_key: :distribution_id,
|
||||
|
|
|
@ -75,7 +75,7 @@ class Group < Namespace
|
|||
has_many :dependency_proxy_blobs, class_name: 'DependencyProxy::Blob'
|
||||
has_many :dependency_proxy_manifests, class_name: 'DependencyProxy::Manifest'
|
||||
|
||||
# debian_distributions must be destroyed by ruby code in order to properly remove carrierwave uploads
|
||||
# debian_distributions and associated component_files must be destroyed by ruby code in order to properly remove carrierwave uploads
|
||||
has_many :debian_distributions, class_name: 'Packages::Debian::GroupDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
|
||||
accepts_nested_attributes_for :variables, allow_destroy: true
|
||||
|
|
|
@ -6,6 +6,7 @@ module Operations
|
|||
include AtomicInternalId
|
||||
include IidRoutes
|
||||
include Limitable
|
||||
include Referable
|
||||
|
||||
self.table_name = 'operations_feature_flags'
|
||||
self.limit_scope = :project
|
||||
|
@ -65,6 +66,31 @@ module Operations
|
|||
.reorder(:id)
|
||||
.references(:operations_scopes)
|
||||
end
|
||||
|
||||
def reference_prefix
|
||||
'[feature_flag:'
|
||||
end
|
||||
|
||||
def reference_pattern
|
||||
@reference_pattern ||= %r{
|
||||
#{Regexp.escape(reference_prefix)}(#{::Project.reference_pattern}\/)?(?<feature_flag>\d+)#{Regexp.escape(reference_postfix)}
|
||||
}x
|
||||
end
|
||||
|
||||
def link_reference_pattern
|
||||
@link_reference_pattern ||= super("feature_flags", /(?<feature_flag>\d+)\/edit/)
|
||||
end
|
||||
|
||||
def reference_postfix
|
||||
']'
|
||||
end
|
||||
end
|
||||
|
||||
def to_reference(from = nil, full: false)
|
||||
project
|
||||
.to_reference_base(from, full: full)
|
||||
.then { |reference_base| reference_base.present? ? "#{reference_base}/" : nil }
|
||||
.then { |reference_base| "#{self.class.reference_prefix}#{reference_base}#{iid}#{self.class.reference_postfix}" }
|
||||
end
|
||||
|
||||
def related_issues(current_user, preload:)
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Packages::Debian::GroupComponentFile < ApplicationRecord
|
||||
def self.container_type
|
||||
:group
|
||||
end
|
||||
|
||||
include Packages::Debian::ComponentFile
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Packages::Debian::ProjectComponentFile < ApplicationRecord
|
||||
def self.container_type
|
||||
:project
|
||||
end
|
||||
|
||||
include Packages::Debian::ComponentFile
|
||||
end
|
|
@ -200,7 +200,7 @@ class Project < ApplicationRecord
|
|||
# Packages
|
||||
has_many :packages, class_name: 'Packages::Package'
|
||||
has_many :package_files, through: :packages, class_name: 'Packages::PackageFile'
|
||||
# debian_distributions must be destroyed by ruby code in order to properly remove carrierwave uploads
|
||||
# debian_distributions and associated component_files must be destroyed by ruby code in order to properly remove carrierwave uploads
|
||||
has_many :debian_distributions, class_name: 'Packages::Debian::ProjectDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
|
||||
has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Boards
|
||||
module Lists
|
||||
# This class is used by issue and epic board lists
|
||||
# for creating new list
|
||||
class BaseCreateService < Boards::BaseService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def execute(board)
|
||||
list = case type
|
||||
when :backlog
|
||||
create_backlog(board)
|
||||
else
|
||||
target = target(board)
|
||||
position = next_position(board)
|
||||
|
||||
return ServiceResponse.error(message: _('%{board_target} not found') % { board_target: type.to_s.capitalize }) if target.blank?
|
||||
|
||||
create_list(board, type, target, position)
|
||||
end
|
||||
|
||||
return ServiceResponse.error(message: list.errors.full_messages) unless list.persisted?
|
||||
|
||||
ServiceResponse.success(payload: { list: list })
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def type
|
||||
# We don't ever expect to have more than one list
|
||||
# type param at once.
|
||||
if params.key?('backlog')
|
||||
:backlog
|
||||
else
|
||||
:label
|
||||
end
|
||||
end
|
||||
|
||||
def target(board)
|
||||
strong_memoize(:target) do
|
||||
available_labels.find_by(id: params[:label_id]) # rubocop: disable CodeReuse/ActiveRecord
|
||||
end
|
||||
end
|
||||
|
||||
def available_labels
|
||||
::Labels::AvailableLabelsService.new(current_user, parent, {})
|
||||
.available_labels
|
||||
end
|
||||
|
||||
def next_position(board)
|
||||
max_position = board.lists.movable.maximum(:position)
|
||||
max_position.nil? ? 0 : max_position.succ
|
||||
end
|
||||
|
||||
def create_list(board, type, target, position)
|
||||
board.lists.create(create_list_attributes(type, target, position))
|
||||
end
|
||||
|
||||
def create_list_attributes(type, target, position)
|
||||
{ type => target, list_type: type, position: position }
|
||||
end
|
||||
|
||||
def create_backlog(board)
|
||||
return board.lists.backlog.first if board.lists.backlog.exists?
|
||||
|
||||
board.lists.create(list_type: :backlog, position: nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,68 +2,7 @@
|
|||
|
||||
module Boards
|
||||
module Lists
|
||||
class CreateService < Boards::BaseService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def execute(board)
|
||||
list = case type
|
||||
when :backlog
|
||||
create_backlog(board)
|
||||
else
|
||||
target = target(board)
|
||||
position = next_position(board)
|
||||
|
||||
return ServiceResponse.error(message: _('%{board_target} not found') % { board_target: type.to_s.capitalize }) if target.blank?
|
||||
|
||||
create_list(board, type, target, position)
|
||||
end
|
||||
|
||||
return ServiceResponse.error(message: list.errors.full_messages) unless list.persisted?
|
||||
|
||||
ServiceResponse.success(payload: { list: list })
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def type
|
||||
# We don't ever expect to have more than one list
|
||||
# type param at once.
|
||||
if params.key?('backlog')
|
||||
:backlog
|
||||
else
|
||||
:label
|
||||
end
|
||||
end
|
||||
|
||||
def target(board)
|
||||
strong_memoize(:target) do
|
||||
available_labels.find_by(id: params[:label_id]) # rubocop: disable CodeReuse/ActiveRecord
|
||||
end
|
||||
end
|
||||
|
||||
def available_labels
|
||||
::Labels::AvailableLabelsService.new(current_user, parent, {})
|
||||
.available_labels
|
||||
end
|
||||
|
||||
def next_position(board)
|
||||
max_position = board.lists.movable.maximum(:position)
|
||||
max_position.nil? ? 0 : max_position.succ
|
||||
end
|
||||
|
||||
def create_list(board, type, target, position)
|
||||
board.lists.create(create_list_attributes(type, target, position))
|
||||
end
|
||||
|
||||
def create_list_attributes(type, target, position)
|
||||
{ type => target, list_type: type, position: position }
|
||||
end
|
||||
|
||||
def create_backlog(board)
|
||||
return board.lists.backlog.first if board.lists.backlog.exists?
|
||||
|
||||
board.lists.create(list_type: :backlog, position: nil)
|
||||
end
|
||||
class CreateService < Boards::Lists::BaseCreateService
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
class Packages::Debian::ComponentFileUploader < GitlabUploader
|
||||
extend Workhorse::UploadPath
|
||||
include ObjectStorage::Concern
|
||||
|
||||
storage_options Gitlab.config.packages
|
||||
|
||||
after :store, :schedule_background_upload
|
||||
|
||||
alias_method :upload, :model
|
||||
|
||||
def filename
|
||||
model.file_name
|
||||
end
|
||||
|
||||
def store_dir
|
||||
dynamic_segment
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def dynamic_segment
|
||||
raise ObjectNotReadyError, 'Package model not ready' unless model.id && model.component.distribution.container_id
|
||||
|
||||
Gitlab::HashedPath.new("debian_#{model.class.container_type}_component_file", model.id, root_hash: model.component.distribution.container_id)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix Net::HTTP proxy encoding username and password
|
||||
merge_request: 52368
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Debian Group and Project Distribution Component Files
|
||||
merge_request: 52885
|
||||
author: Mathieu Parent
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add GFM reference format for feature flags
|
||||
merge_request: 53021
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: feature_flag_contextual_issue
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53021
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/320741
|
||||
milestone: '13.9'
|
||||
type: development
|
||||
group: group::release
|
||||
default_enabled: false
|
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Monkey patch Net::HTTP to fix missing URL decoding for username and password in proxy settings
|
||||
#
|
||||
# See proposed upstream fix https://github.com/ruby/net-http/pull/5
|
||||
# See Ruby-lang issue https://bugs.ruby-lang.org/issues/17542
|
||||
# See issue on GitLab https://gitlab.com/gitlab-org/gitlab/-/issues/289836
|
||||
|
||||
module Net
|
||||
class HTTP < Protocol
|
||||
def proxy_user
|
||||
if environment_variable_is_multiuser_safe? && @proxy_from_env
|
||||
user = proxy_uri&.user
|
||||
CGI.unescape(user) unless user.nil?
|
||||
else
|
||||
@proxy_user
|
||||
end
|
||||
end
|
||||
|
||||
def proxy_pass
|
||||
if environment_variable_is_multiuser_safe? && @proxy_from_env
|
||||
pass = proxy_uri&.password
|
||||
CGI.unescape(pass) unless pass.nil?
|
||||
else
|
||||
@proxy_pass
|
||||
end
|
||||
end
|
||||
|
||||
def environment_variable_is_multiuser_safe?
|
||||
ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreatePackagesDebianProjectComponentFiles < ActiveRecord::Migration[5.2]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
INDEX_ARCHITECTURE = 'idx_packages_debian_project_component_files_on_architecture_id'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
unless table_exists?(:packages_debian_project_component_files)
|
||||
create_table :packages_debian_project_component_files do |t|
|
||||
t.timestamps_with_timezone
|
||||
t.references :component,
|
||||
foreign_key: { to_table: :packages_debian_project_components, on_delete: :restrict },
|
||||
null: false,
|
||||
index: true
|
||||
t.references :architecture,
|
||||
foreign_key: { to_table: :packages_debian_project_architectures, on_delete: :restrict },
|
||||
index: { name: INDEX_ARCHITECTURE }
|
||||
t.integer :size, null: false
|
||||
t.integer :file_type, limit: 2, null: false
|
||||
t.integer :compression_type, limit: 2
|
||||
t.integer :file_store, limit: 2, default: 1, null: false
|
||||
t.text :file, null: false
|
||||
t.binary :file_md5, null: false
|
||||
t.binary :file_sha256, null: false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
add_text_limit :packages_debian_project_component_files, :file, 255
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :packages_debian_project_component_files
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreatePackagesDebianGroupComponentFiles < ActiveRecord::Migration[5.2]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
INDEX_ARCHITECTURE = 'idx_packages_debian_group_component_files_on_architecture_id'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
unless table_exists?(:packages_debian_group_component_files)
|
||||
create_table :packages_debian_group_component_files do |t|
|
||||
t.timestamps_with_timezone
|
||||
t.references :component,
|
||||
foreign_key: { to_table: :packages_debian_group_components, on_delete: :restrict },
|
||||
null: false,
|
||||
index: true
|
||||
t.references :architecture,
|
||||
foreign_key: { to_table: :packages_debian_group_architectures, on_delete: :restrict },
|
||||
index: { name: INDEX_ARCHITECTURE }
|
||||
t.integer :size, null: false
|
||||
t.integer :file_type, limit: 2, null: false
|
||||
t.integer :compression_type, limit: 2
|
||||
t.integer :file_store, limit: 2, default: 1, null: false
|
||||
t.text :file, null: false
|
||||
t.binary :file_md5, null: false
|
||||
t.binary :file_sha256, null: false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
add_text_limit :packages_debian_group_component_files, :file, 255
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :packages_debian_group_component_files
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
6fcaa4184ae69fabd6f2668cad19c38a8ae7c187053d60cdf4fcbdbc0443aa42
|
|
@ -0,0 +1 @@
|
|||
3f422a916b50cafd46b4a7486b6c3cc0a9992831a7dbc40c51323c835d845a0a
|
|
@ -14894,6 +14894,31 @@ CREATE SEQUENCE packages_debian_group_architectures_id_seq
|
|||
|
||||
ALTER SEQUENCE packages_debian_group_architectures_id_seq OWNED BY packages_debian_group_architectures.id;
|
||||
|
||||
CREATE TABLE packages_debian_group_component_files (
|
||||
id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
component_id bigint NOT NULL,
|
||||
architecture_id bigint,
|
||||
size integer NOT NULL,
|
||||
file_type smallint NOT NULL,
|
||||
compression_type smallint,
|
||||
file_store smallint DEFAULT 1 NOT NULL,
|
||||
file text NOT NULL,
|
||||
file_md5 bytea NOT NULL,
|
||||
file_sha256 bytea NOT NULL,
|
||||
CONSTRAINT check_839e1685bc CHECK ((char_length(file) <= 255))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE packages_debian_group_component_files_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE packages_debian_group_component_files_id_seq OWNED BY packages_debian_group_component_files.id;
|
||||
|
||||
CREATE TABLE packages_debian_group_components (
|
||||
id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
|
@ -14971,6 +14996,31 @@ CREATE SEQUENCE packages_debian_project_architectures_id_seq
|
|||
|
||||
ALTER SEQUENCE packages_debian_project_architectures_id_seq OWNED BY packages_debian_project_architectures.id;
|
||||
|
||||
CREATE TABLE packages_debian_project_component_files (
|
||||
id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
component_id bigint NOT NULL,
|
||||
architecture_id bigint,
|
||||
size integer NOT NULL,
|
||||
file_type smallint NOT NULL,
|
||||
compression_type smallint,
|
||||
file_store smallint DEFAULT 1 NOT NULL,
|
||||
file text NOT NULL,
|
||||
file_md5 bytea NOT NULL,
|
||||
file_sha256 bytea NOT NULL,
|
||||
CONSTRAINT check_e5af03fa2d CHECK ((char_length(file) <= 255))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE packages_debian_project_component_files_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE packages_debian_project_component_files_id_seq OWNED BY packages_debian_project_component_files.id;
|
||||
|
||||
CREATE TABLE packages_debian_project_components (
|
||||
id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
|
@ -19021,12 +19071,16 @@ ALTER TABLE ONLY packages_conan_metadata ALTER COLUMN id SET DEFAULT nextval('pa
|
|||
|
||||
ALTER TABLE ONLY packages_debian_group_architectures ALTER COLUMN id SET DEFAULT nextval('packages_debian_group_architectures_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY packages_debian_group_component_files ALTER COLUMN id SET DEFAULT nextval('packages_debian_group_component_files_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY packages_debian_group_components ALTER COLUMN id SET DEFAULT nextval('packages_debian_group_components_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY packages_debian_group_distributions ALTER COLUMN id SET DEFAULT nextval('packages_debian_group_distributions_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY packages_debian_project_architectures ALTER COLUMN id SET DEFAULT nextval('packages_debian_project_architectures_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY packages_debian_project_component_files ALTER COLUMN id SET DEFAULT nextval('packages_debian_project_component_files_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY packages_debian_project_components ALTER COLUMN id SET DEFAULT nextval('packages_debian_project_components_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY packages_debian_project_distributions ALTER COLUMN id SET DEFAULT nextval('packages_debian_project_distributions_id_seq'::regclass);
|
||||
|
@ -20401,6 +20455,9 @@ ALTER TABLE ONLY packages_debian_file_metadata
|
|||
ALTER TABLE ONLY packages_debian_group_architectures
|
||||
ADD CONSTRAINT packages_debian_group_architectures_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY packages_debian_group_component_files
|
||||
ADD CONSTRAINT packages_debian_group_component_files_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY packages_debian_group_components
|
||||
ADD CONSTRAINT packages_debian_group_components_pkey PRIMARY KEY (id);
|
||||
|
||||
|
@ -20410,6 +20467,9 @@ ALTER TABLE ONLY packages_debian_group_distributions
|
|||
ALTER TABLE ONLY packages_debian_project_architectures
|
||||
ADD CONSTRAINT packages_debian_project_architectures_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY packages_debian_project_component_files
|
||||
ADD CONSTRAINT packages_debian_project_component_files_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY packages_debian_project_components
|
||||
ADD CONSTRAINT packages_debian_project_components_pkey PRIMARY KEY (id);
|
||||
|
||||
|
@ -21134,6 +21194,10 @@ CREATE UNIQUE INDEX idx_on_compliance_management_frameworks_namespace_id_name ON
|
|||
|
||||
CREATE INDEX idx_packages_build_infos_on_package_id ON packages_build_infos USING btree (package_id);
|
||||
|
||||
CREATE INDEX idx_packages_debian_group_component_files_on_architecture_id ON packages_debian_group_component_files USING btree (architecture_id);
|
||||
|
||||
CREATE INDEX idx_packages_debian_project_component_files_on_architecture_id ON packages_debian_project_component_files USING btree (architecture_id);
|
||||
|
||||
CREATE INDEX idx_packages_packages_on_project_id_name_version_package_type ON packages_packages USING btree (project_id, name, version, package_type);
|
||||
|
||||
CREATE INDEX idx_pkgs_deb_grp_architectures_on_distribution_id ON packages_debian_group_architectures USING btree (distribution_id);
|
||||
|
@ -22636,10 +22700,14 @@ CREATE UNIQUE INDEX index_packages_conan_file_metadata_on_package_file_id ON pac
|
|||
|
||||
CREATE UNIQUE INDEX index_packages_conan_metadata_on_package_id_username_channel ON packages_conan_metadata USING btree (package_id, package_username, package_channel);
|
||||
|
||||
CREATE INDEX index_packages_debian_group_component_files_on_component_id ON packages_debian_group_component_files USING btree (component_id);
|
||||
|
||||
CREATE INDEX index_packages_debian_group_distributions_on_creator_id ON packages_debian_group_distributions USING btree (creator_id);
|
||||
|
||||
CREATE INDEX index_packages_debian_group_distributions_on_group_id ON packages_debian_group_distributions USING btree (group_id);
|
||||
|
||||
CREATE INDEX index_packages_debian_project_component_files_on_component_id ON packages_debian_project_component_files USING btree (component_id);
|
||||
|
||||
CREATE INDEX index_packages_debian_project_distributions_on_creator_id ON packages_debian_project_distributions USING btree (creator_id);
|
||||
|
||||
CREATE INDEX index_packages_debian_project_distributions_on_project_id ON packages_debian_project_distributions USING btree (project_id);
|
||||
|
@ -24850,6 +24918,9 @@ ALTER TABLE ONLY group_group_links
|
|||
ALTER TABLE ONLY geo_repository_updated_events
|
||||
ADD CONSTRAINT fk_rails_2b70854c08 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY packages_debian_group_component_files
|
||||
ADD CONSTRAINT fk_rails_2b8992dd83 FOREIGN KEY (architecture_id) REFERENCES packages_debian_group_architectures(id) ON DELETE RESTRICT;
|
||||
|
||||
ALTER TABLE ONLY boards_epic_board_labels
|
||||
ADD CONSTRAINT fk_rails_2bedeb8799 FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE;
|
||||
|
||||
|
@ -25621,6 +25692,9 @@ ALTER TABLE ONLY merge_trains
|
|||
ALTER TABLE ONLY application_settings
|
||||
ADD CONSTRAINT fk_rails_b53e481273 FOREIGN KEY (custom_project_templates_group_id) REFERENCES namespaces(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY packages_debian_project_component_files
|
||||
ADD CONSTRAINT fk_rails_b543a9622b FOREIGN KEY (architecture_id) REFERENCES packages_debian_project_architectures(id) ON DELETE RESTRICT;
|
||||
|
||||
ALTER TABLE ONLY namespace_aggregation_schedules
|
||||
ADD CONSTRAINT fk_rails_b565c8d16c FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
|
@ -25645,6 +25719,9 @@ ALTER TABLE ONLY lists
|
|||
ALTER TABLE ONLY security_findings
|
||||
ADD CONSTRAINT fk_rails_bb63863cf1 FOREIGN KEY (scan_id) REFERENCES security_scans(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY packages_debian_project_component_files
|
||||
ADD CONSTRAINT fk_rails_bbe9ebfbd9 FOREIGN KEY (component_id) REFERENCES packages_debian_project_components(id) ON DELETE RESTRICT;
|
||||
|
||||
ALTER TABLE ONLY approval_merge_request_rules_users
|
||||
ADD CONSTRAINT fk_rails_bc8972fa55 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
|
@ -25813,6 +25890,9 @@ ALTER TABLE ONLY vulnerability_occurrence_pipelines
|
|||
ALTER TABLE ONLY deployment_merge_requests
|
||||
ADD CONSTRAINT fk_rails_dcbce9f4df FOREIGN KEY (deployment_id) REFERENCES deployments(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY packages_debian_group_component_files
|
||||
ADD CONSTRAINT fk_rails_dd262386e9 FOREIGN KEY (component_id) REFERENCES packages_debian_group_components(id) ON DELETE RESTRICT;
|
||||
|
||||
ALTER TABLE ONLY user_callouts
|
||||
ADD CONSTRAINT fk_rails_ddfdd80f3d FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -122,7 +122,7 @@ Do not run `git prune` or `git gc` in pool repositories! This can
|
|||
cause data loss in "real" repositories that depend on the pool in
|
||||
question.
|
||||
|
||||
Forks of public projects are deduplicated by creating a third repository, the
|
||||
Forks of public and internal projects are deduplicated by creating a third repository, the
|
||||
object pool, containing the objects from the source project. Using
|
||||
`objects/info/alternates`, the source project and forks use the object pool for
|
||||
shared objects. Objects are moved from the source project to the object pool
|
||||
|
|
|
@ -2361,7 +2361,7 @@ type BoardListCreatePayload {
|
|||
errors: [String!]!
|
||||
|
||||
"""
|
||||
List of the issue board.
|
||||
Issue list in the issue board.
|
||||
"""
|
||||
list: BoardList
|
||||
}
|
||||
|
@ -9155,6 +9155,51 @@ type EpicBoardEdge {
|
|||
node: EpicBoard
|
||||
}
|
||||
|
||||
"""
|
||||
Autogenerated input type of EpicBoardListCreate
|
||||
"""
|
||||
input EpicBoardListCreateInput {
|
||||
"""
|
||||
Create the backlog list.
|
||||
"""
|
||||
backlog: Boolean
|
||||
|
||||
"""
|
||||
Global ID of the issue board to mutate.
|
||||
"""
|
||||
boardId: BoardsEpicBoardID!
|
||||
|
||||
"""
|
||||
A unique identifier for the client performing the mutation.
|
||||
"""
|
||||
clientMutationId: String
|
||||
|
||||
"""
|
||||
Global ID of an existing label.
|
||||
"""
|
||||
labelId: LabelID
|
||||
}
|
||||
|
||||
"""
|
||||
Autogenerated return type of EpicBoardListCreate
|
||||
"""
|
||||
type EpicBoardListCreatePayload {
|
||||
"""
|
||||
A unique identifier for the client performing the mutation.
|
||||
"""
|
||||
clientMutationId: String
|
||||
|
||||
"""
|
||||
Errors encountered during execution of the mutation.
|
||||
"""
|
||||
errors: [String!]!
|
||||
|
||||
"""
|
||||
Epic list in the epic board.
|
||||
"""
|
||||
list: EpicList
|
||||
}
|
||||
|
||||
"""
|
||||
The connection type for Epic.
|
||||
"""
|
||||
|
@ -16235,6 +16280,7 @@ type Mutation {
|
|||
environmentsCanaryIngressUpdate(input: EnvironmentsCanaryIngressUpdateInput!): EnvironmentsCanaryIngressUpdatePayload
|
||||
epicAddIssue(input: EpicAddIssueInput!): EpicAddIssuePayload
|
||||
epicBoardCreate(input: EpicBoardCreateInput!): EpicBoardCreatePayload
|
||||
epicBoardListCreate(input: EpicBoardListCreateInput!): EpicBoardListCreatePayload
|
||||
epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload
|
||||
epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload
|
||||
exportRequirements(input: ExportRequirementsInput!): ExportRequirementsPayload
|
||||
|
|
|
@ -6045,20 +6045,6 @@
|
|||
"description": "Autogenerated input type of BoardListCreate",
|
||||
"fields": null,
|
||||
"inputFields": [
|
||||
{
|
||||
"name": "boardId",
|
||||
"description": "Global ID of the issue board to mutate.",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "BoardID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "backlog",
|
||||
"description": "Create the backlog list.",
|
||||
|
@ -6079,6 +6065,20 @@
|
|||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "boardId",
|
||||
"description": "Global ID of the issue board to mutate.",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "BoardID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "milestoneId",
|
||||
"description": "Global ID of an existing milestone.",
|
||||
|
@ -6171,7 +6171,7 @@
|
|||
},
|
||||
{
|
||||
"name": "list",
|
||||
"description": "List of the issue board.",
|
||||
"description": "Issue list in the issue board.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
|
@ -25287,6 +25287,128 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "EpicBoardListCreateInput",
|
||||
"description": "Autogenerated input type of EpicBoardListCreate",
|
||||
"fields": null,
|
||||
"inputFields": [
|
||||
{
|
||||
"name": "backlog",
|
||||
"description": "Create the backlog list.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "labelId",
|
||||
"description": "Global ID of an existing label.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "LabelID",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "boardId",
|
||||
"description": "Global ID of the issue board to mutate.",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "BoardsEpicBoardID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "clientMutationId",
|
||||
"description": "A unique identifier for the client performing the mutation.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "EpicBoardListCreatePayload",
|
||||
"description": "Autogenerated return type of EpicBoardListCreate",
|
||||
"fields": [
|
||||
{
|
||||
"name": "clientMutationId",
|
||||
"description": "A unique identifier for the client performing the mutation.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "errors",
|
||||
"description": "Errors encountered during execution of the mutation.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "list",
|
||||
"description": "Epic list in the epic board.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "EpicList",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "EpicConnection",
|
||||
|
@ -46077,6 +46199,33 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "epicBoardListCreate",
|
||||
"description": null,
|
||||
"args": [
|
||||
{
|
||||
"name": "input",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "EpicBoardListCreateInput",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "EpicBoardListCreatePayload",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "epicSetSubscription",
|
||||
"description": null,
|
||||
|
|
|
@ -357,7 +357,7 @@ Autogenerated return type of BoardListCreate.
|
|||
| ----- | ---- | ----------- |
|
||||
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
|
||||
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
|
||||
| `list` | BoardList | List of the issue board. |
|
||||
| `list` | BoardList | Issue list in the issue board. |
|
||||
|
||||
### BoardListUpdateLimitMetricsPayload
|
||||
|
||||
|
@ -1482,6 +1482,16 @@ Autogenerated return type of EpicBoardCreate.
|
|||
| `epicBoard` | EpicBoard | The created epic board. |
|
||||
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
|
||||
|
||||
### EpicBoardListCreatePayload
|
||||
|
||||
Autogenerated return type of EpicBoardListCreate.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
|
||||
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
|
||||
| `list` | EpicList | Epic list in the epic board. |
|
||||
|
||||
### EpicDescendantCount
|
||||
|
||||
Counts of descendent epics.
|
||||
|
|
|
@ -444,6 +444,7 @@ GFM recognizes the following:
|
|||
| snippet | `$123` | `namespace/project$123` | `project$123` |
|
||||
| epic **(ULTIMATE)** | `&123` | `group1/subgroup&123` | |
|
||||
| vulnerability **(ULTIMATE)** (1)| `[vulnerability:123]` | `[vulnerability:namespace/project/123]` | `[vulnerability:project/123]` |
|
||||
| feature flag | `[feature_flag:123]` | `[feature_flag:namespace/project/123]` | `[feature_flag:project/123]` |
|
||||
| label by ID | `~123` | `namespace/project~123` | `project~123` |
|
||||
| one-word label by name | `~bug` | `namespace/project~bug` | `project~bug` |
|
||||
| multi-word label by name | `~"feature request"` | `namespace/project~"feature request"` | `project~"feature request"` |
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Banzai
|
||||
module Filter
|
||||
class FeatureFlagReferenceFilter < IssuableReferenceFilter
|
||||
self.reference_type = :feature_flag
|
||||
|
||||
def self.object_class
|
||||
Operations::FeatureFlag
|
||||
end
|
||||
|
||||
def self.object_sym
|
||||
:feature_flag
|
||||
end
|
||||
|
||||
def parent_records(parent, ids)
|
||||
return self.class.object_class.none unless Feature.enabled?(:feature_flag_contextual_issue, parent)
|
||||
|
||||
parent.operations_feature_flags.where(iid: ids.to_a)
|
||||
end
|
||||
|
||||
def url_for_object(feature_flag, project)
|
||||
::Gitlab::Routing.url_helpers.edit_project_feature_flag_url(
|
||||
project,
|
||||
feature_flag.iid,
|
||||
only_path: context[:only_path]
|
||||
)
|
||||
end
|
||||
|
||||
def object_link_title(object, matches)
|
||||
object.name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -62,7 +62,8 @@ module Banzai
|
|||
Filter::CommitReferenceFilter,
|
||||
Filter::LabelReferenceFilter,
|
||||
Filter::MilestoneReferenceFilter,
|
||||
Filter::AlertReferenceFilter
|
||||
Filter::AlertReferenceFilter,
|
||||
Filter::FeatureFlagReferenceFilter
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -24,7 +24,8 @@ module Banzai
|
|||
Filter::SnippetReferenceFilter,
|
||||
Filter::CommitRangeReferenceFilter,
|
||||
Filter::CommitReferenceFilter,
|
||||
Filter::AlertReferenceFilter
|
||||
Filter::AlertReferenceFilter,
|
||||
Filter::FeatureFlagReferenceFilter
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Banzai
|
||||
module ReferenceParser
|
||||
class FeatureFlagParser < BaseParser
|
||||
self.reference_type = :feature_flag
|
||||
|
||||
def references_relation
|
||||
Operations::FeatureFlag
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def can_read_reference?(user, feature_flag, node)
|
||||
can?(user, :read_feature_flag, feature_flag)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,7 +3,7 @@
|
|||
module QA
|
||||
module Resource
|
||||
class CiVariable < Base
|
||||
attr_accessor :key, :value, :masked
|
||||
attr_accessor :key, :value, :masked, :protected
|
||||
|
||||
attribute :project do
|
||||
Project.fabricate! do |resource|
|
||||
|
@ -12,6 +12,11 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
def initialize
|
||||
@masked = false
|
||||
@protected = false
|
||||
end
|
||||
|
||||
def fabricate!
|
||||
project.visit!
|
||||
|
||||
|
@ -49,7 +54,8 @@ module QA
|
|||
{
|
||||
key: key,
|
||||
value: value,
|
||||
masked: masked
|
||||
masked: masked,
|
||||
protected: protected
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,10 +25,6 @@ module QA
|
|||
attribute :template_name
|
||||
attribute :import
|
||||
|
||||
attribute :default_branch do
|
||||
api_response[:default_branch] || Runtime::Env.default_branch
|
||||
end
|
||||
|
||||
attribute :group do
|
||||
Group.fabricate!
|
||||
end
|
||||
|
@ -224,6 +220,10 @@ module QA
|
|||
parse_body(get(Runtime::API::Request.new(api_client, api_commits_path).url))
|
||||
end
|
||||
|
||||
def default_branch
|
||||
reload!.api_response[:default_branch] || Runtime::Env.default_branch
|
||||
end
|
||||
|
||||
def import_status
|
||||
response = get Runtime::API::Request.new(api_client, "/projects/#{id}/import").url
|
||||
|
||||
|
|
|
@ -12,14 +12,11 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
def branch_name
|
||||
@branch_name ||= wiki.project.default_branch
|
||||
end
|
||||
|
||||
def initialize
|
||||
@file_name = 'Home.md'
|
||||
@file_content = 'This line was created using git push'
|
||||
@commit_message = 'Updating using git push'
|
||||
@branch_name = 'master'
|
||||
@new_branch = false
|
||||
end
|
||||
|
||||
|
|
|
@ -14,10 +14,12 @@ module QA
|
|||
Git::Repository.perform do |repository|
|
||||
repository.uri = project.repository_http_location.uri
|
||||
repository.use_default_credentials
|
||||
repository.default_branch = project.default_branch
|
||||
|
||||
repository.act do
|
||||
clone
|
||||
configure_identity('GitLab QA', 'root@gitlab.com')
|
||||
checkout(default_branch, new_branch: true)
|
||||
commit_file('test.rb', 'class Test; end', 'Add Test class')
|
||||
commit_file('README.md', '# Test', 'Add Readme')
|
||||
push_changes
|
||||
|
|
|
@ -26,6 +26,8 @@ module QA
|
|||
repository.use_default_credentials
|
||||
repository.clone
|
||||
repository.configure_identity(username, email)
|
||||
repository.default_branch = project.default_branch
|
||||
repository.checkout(project.default_branch, new_branch: true)
|
||||
|
||||
git_protocol_reported = repository.push_with_git_protocol(
|
||||
git_protocol,
|
||||
|
|
|
@ -49,6 +49,8 @@ module QA
|
|||
repository.use_ssh_key(ssh_key)
|
||||
repository.clone
|
||||
repository.configure_identity(username, email)
|
||||
repository.default_branch = project.default_branch
|
||||
repository.checkout(project.default_branch, new_branch: true)
|
||||
|
||||
git_protocol_reported = repository.push_with_git_protocol(
|
||||
git_protocol,
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'faker'
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Verify', :runner do
|
||||
describe 'Pipeline with protected variable' do
|
||||
let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(8)}" }
|
||||
let(:protected_value) { Faker::Alphanumeric.alphanumeric(8) }
|
||||
|
||||
let(:project) do
|
||||
Resource::Project.fabricate_via_api! do |project|
|
||||
project.name = 'project-with-ci-variables'
|
||||
project.description = 'project with CI variables'
|
||||
end
|
||||
end
|
||||
|
||||
let!(:runner) do
|
||||
Resource::Runner.fabricate! do |runner|
|
||||
runner.project = project
|
||||
runner.name = executor
|
||||
runner.tags = [executor]
|
||||
end
|
||||
end
|
||||
|
||||
let!(:ci_file) do
|
||||
Resource::Repository::Commit.fabricate_via_api! do |commit|
|
||||
commit.project = project
|
||||
commit.commit_message = 'Add .gitlab-ci.yml'
|
||||
commit.add_files(
|
||||
[
|
||||
{
|
||||
file_path: '.gitlab-ci.yml',
|
||||
content: <<~YAML
|
||||
job:
|
||||
tags:
|
||||
- #{executor}
|
||||
script: echo $PROTECTED_VARIABLE
|
||||
YAML
|
||||
}
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
let(:developer) do
|
||||
Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1)
|
||||
end
|
||||
|
||||
let(:maintainer) do
|
||||
Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_2, Runtime::Env.gitlab_qa_password_2)
|
||||
end
|
||||
|
||||
before do
|
||||
Flow::Login.sign_in
|
||||
project.visit!
|
||||
project.add_member(developer)
|
||||
project.add_member(maintainer, Resource::Members::AccessLevel::MAINTAINER)
|
||||
add_ci_variable
|
||||
end
|
||||
|
||||
after do
|
||||
runner.remove_via_api!
|
||||
end
|
||||
|
||||
it 'exposes variable on protected branch', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/156' do
|
||||
create_protected_branch
|
||||
|
||||
[developer, maintainer].each do |user|
|
||||
user_commit_to_protected_branch(Runtime::API::Client.new(:gitlab, user: user))
|
||||
go_to_pipeline_job(user)
|
||||
|
||||
Page::Project::Job::Show.perform do |show|
|
||||
expect(show.output).to have_content(protected_value), 'Expect protected variable to be in job log.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not expose variable on unprotected branch', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/156' do
|
||||
[developer, maintainer].each do |user|
|
||||
create_merge_request(Runtime::API::Client.new(:gitlab, user: user))
|
||||
go_to_pipeline_job(user)
|
||||
|
||||
Page::Project::Job::Show.perform do |show|
|
||||
expect(show.output).to have_no_content(protected_value), 'Expect protected variable to NOT be in job log.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_ci_variable
|
||||
Resource::CiVariable.fabricate_via_api! do |ci_variable|
|
||||
ci_variable.project = project
|
||||
ci_variable.key = 'PROTECTED_VARIABLE'
|
||||
ci_variable.value = protected_value
|
||||
ci_variable.protected = true
|
||||
end
|
||||
end
|
||||
|
||||
def create_protected_branch
|
||||
# Using default setups, which allows access for developer and maintainer
|
||||
Resource::ProtectedBranch.fabricate_via_api! do |resource|
|
||||
resource.branch_name = 'protected-branch'
|
||||
resource.project = project
|
||||
end
|
||||
end
|
||||
|
||||
def user_commit_to_protected_branch(api_client)
|
||||
Resource::Repository::Commit.fabricate_via_api! do |commit|
|
||||
commit.api_client = api_client
|
||||
commit.project = project
|
||||
commit.branch = 'protected-branch'
|
||||
commit.commit_message = Faker::Lorem.sentence
|
||||
commit.add_files(
|
||||
[
|
||||
{
|
||||
file_path: "#{Faker::Lorem.word}.txt",
|
||||
content: Faker::Lorem.sentence
|
||||
}
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def create_merge_request(api_client)
|
||||
Resource::MergeRequest.fabricate_via_api! do |merge_request|
|
||||
merge_request.api_client = api_client
|
||||
merge_request.project = project
|
||||
merge_request.description = Faker::Lorem.sentence
|
||||
merge_request.target_new_branch = false
|
||||
merge_request.file_name = "#{Faker::Lorem.word}.txt"
|
||||
merge_request.file_content = Faker::Lorem.sentence
|
||||
end
|
||||
end
|
||||
|
||||
def go_to_pipeline_job(user)
|
||||
Flow::Login.sign_in(as: user)
|
||||
project.visit!
|
||||
Flow::Pipeline.visit_latest_pipeline(pipeline_condition: 'completed')
|
||||
|
||||
Page::Project::Pipeline::Show.perform do |pipeline|
|
||||
pipeline.click_job('job')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :debian_project_component_file, class: 'Packages::Debian::ProjectComponentFile' do
|
||||
component { association(:debian_project_component) }
|
||||
architecture { association(:debian_project_architecture, distribution: component.distribution) }
|
||||
|
||||
factory :debian_group_component_file, class: 'Packages::Debian::GroupComponentFile' do
|
||||
component { association(:debian_group_component) }
|
||||
architecture { association(:debian_group_architecture, distribution: component.distribution) }
|
||||
end
|
||||
|
||||
file_type { :packages }
|
||||
|
||||
after(:build) do |component_file, evaluator|
|
||||
component_file.file = fixture_file_upload('spec/fixtures/packages/debian/distribution/Packages')
|
||||
end
|
||||
|
||||
file_md5 { '12345abcde' }
|
||||
file_sha256 { 'be93151dc23ac34a82752444556fe79b32c7a1ad' }
|
||||
|
||||
trait(:packages) do
|
||||
file_type { :packages }
|
||||
end
|
||||
|
||||
trait(:source) do
|
||||
file_type { :source }
|
||||
architecture { nil }
|
||||
end
|
||||
|
||||
trait(:di_packages) do
|
||||
file_type { :di_packages }
|
||||
end
|
||||
|
||||
trait(:object_storage) do
|
||||
file_store { Packages::PackageFileUploader::Store::REMOTE }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -14,7 +14,7 @@ FactoryBot.define do
|
|||
|
||||
trait(:with_file) do
|
||||
after(:build) do |distribution, evaluator|
|
||||
distribution.file = fixture_file_upload('spec/fixtures/packages/debian/README.md')
|
||||
distribution.file = fixture_file_upload('spec/fixtures/packages/debian/distribution/Release')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
Package: example-package
|
||||
Description: This is an incomplete Packages file
|
|
@ -0,0 +1 @@
|
|||
Codename: fixture-distribution
|
|
@ -6,7 +6,7 @@ import waitForPromises from 'helpers/wait_for_promises';
|
|||
|
||||
import { deprecatedCreateFlash as createFlash } from '~/flash';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import boardsStore from '~/boards/stores/boards_store';
|
||||
import { formType } from '~/boards/constants';
|
||||
import BoardForm from '~/boards/components/board_form.vue';
|
||||
import updateBoardMutation from '~/boards/graphql/board_update.mutation.graphql';
|
||||
import createBoardMutation from '~/boards/graphql/board_create.mutation.graphql';
|
||||
|
@ -35,6 +35,7 @@ const defaultProps = {
|
|||
labelsPath: `${TEST_HOST}/labels/path`,
|
||||
labelsWebUrl: `${TEST_HOST}/-/labels`,
|
||||
currentBoard,
|
||||
currentPage: '',
|
||||
};
|
||||
|
||||
describe('BoardForm', () => {
|
||||
|
@ -75,14 +76,12 @@ describe('BoardForm', () => {
|
|||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
boardsStore.state.currentPage = null;
|
||||
mutate = null;
|
||||
});
|
||||
|
||||
describe('when user can not admin the board', () => {
|
||||
beforeEach(() => {
|
||||
boardsStore.state.currentPage = 'new';
|
||||
createComponent();
|
||||
createComponent({ currentPage: formType.new });
|
||||
});
|
||||
|
||||
it('hides modal footer when user is not a board admin', () => {
|
||||
|
@ -100,8 +99,7 @@ describe('BoardForm', () => {
|
|||
|
||||
describe('when user can admin the board', () => {
|
||||
beforeEach(() => {
|
||||
boardsStore.state.currentPage = 'new';
|
||||
createComponent({ canAdminBoard: true });
|
||||
createComponent({ canAdminBoard: true, currentPage: formType.new });
|
||||
});
|
||||
|
||||
it('shows modal footer when user is a board admin', () => {
|
||||
|
@ -118,13 +116,9 @@ describe('BoardForm', () => {
|
|||
});
|
||||
|
||||
describe('when creating a new board', () => {
|
||||
beforeEach(() => {
|
||||
boardsStore.state.currentPage = 'new';
|
||||
});
|
||||
|
||||
describe('on non-scoped-board', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ canAdminBoard: true });
|
||||
createComponent({ canAdminBoard: true, currentPage: formType.new });
|
||||
});
|
||||
|
||||
it('clears the form', () => {
|
||||
|
@ -165,7 +159,7 @@ describe('BoardForm', () => {
|
|||
});
|
||||
|
||||
it('does not call API if board name is empty', async () => {
|
||||
createComponent({ canAdminBoard: true });
|
||||
createComponent({ canAdminBoard: true, currentPage: formType.new });
|
||||
findInput().trigger('keyup.enter', { metaKey: true });
|
||||
|
||||
await waitForPromises();
|
||||
|
@ -174,7 +168,7 @@ describe('BoardForm', () => {
|
|||
});
|
||||
|
||||
it('calls a correct GraphQL mutation and redirects to correct page from existing board', async () => {
|
||||
createComponent({ canAdminBoard: true });
|
||||
createComponent({ canAdminBoard: true, currentPage: formType.new });
|
||||
fillForm();
|
||||
|
||||
await waitForPromises();
|
||||
|
@ -194,7 +188,7 @@ describe('BoardForm', () => {
|
|||
|
||||
it('shows an error flash if GraphQL mutation fails', async () => {
|
||||
mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
|
||||
createComponent({ canAdminBoard: true });
|
||||
createComponent({ canAdminBoard: true, currentPage: formType.new });
|
||||
fillForm();
|
||||
|
||||
await waitForPromises();
|
||||
|
@ -209,13 +203,9 @@ describe('BoardForm', () => {
|
|||
});
|
||||
|
||||
describe('when editing a board', () => {
|
||||
beforeEach(() => {
|
||||
boardsStore.state.currentPage = 'edit';
|
||||
});
|
||||
|
||||
describe('on non-scoped-board', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ canAdminBoard: true });
|
||||
createComponent({ canAdminBoard: true, currentPage: formType.edit });
|
||||
});
|
||||
|
||||
it('clears the form', () => {
|
||||
|
@ -247,7 +237,7 @@ describe('BoardForm', () => {
|
|||
},
|
||||
});
|
||||
window.location = new URL('https://test/boards/1');
|
||||
createComponent({ canAdminBoard: true });
|
||||
createComponent({ canAdminBoard: true, currentPage: formType.edit });
|
||||
|
||||
findInput().trigger('keyup.enter', { metaKey: true });
|
||||
|
||||
|
@ -273,7 +263,7 @@ describe('BoardForm', () => {
|
|||
},
|
||||
});
|
||||
window.location = new URL('https://test/boards/1?group_by=epic');
|
||||
createComponent({ canAdminBoard: true });
|
||||
createComponent({ canAdminBoard: true, currentPage: formType.edit });
|
||||
|
||||
findInput().trigger('keyup.enter', { metaKey: true });
|
||||
|
||||
|
@ -294,7 +284,7 @@ describe('BoardForm', () => {
|
|||
|
||||
it('shows an error flash if GraphQL mutation fails', async () => {
|
||||
mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
|
||||
createComponent({ canAdminBoard: true });
|
||||
createComponent({ canAdminBoard: true, currentPage: formType.edit });
|
||||
findInput().trigger('keyup.enter', { metaKey: true });
|
||||
|
||||
await waitForPromises();
|
||||
|
@ -308,24 +298,20 @@ describe('BoardForm', () => {
|
|||
});
|
||||
|
||||
describe('when deleting a board', () => {
|
||||
beforeEach(() => {
|
||||
boardsStore.state.currentPage = 'delete';
|
||||
});
|
||||
|
||||
it('passes correct primary action text and variant', () => {
|
||||
createComponent({ canAdminBoard: true });
|
||||
createComponent({ canAdminBoard: true, currentPage: formType.delete });
|
||||
expect(findModalActionPrimary().text).toBe('Delete');
|
||||
expect(findModalActionPrimary().attributes[0].variant).toBe('danger');
|
||||
});
|
||||
|
||||
it('renders delete confirmation message', () => {
|
||||
createComponent({ canAdminBoard: true });
|
||||
createComponent({ canAdminBoard: true, currentPage: formType.delete });
|
||||
expect(findDeleteConfirmation().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('calls a correct GraphQL mutation and redirects to correct page after deleting board', async () => {
|
||||
mutate = jest.fn().mockResolvedValue({});
|
||||
createComponent({ canAdminBoard: true });
|
||||
createComponent({ canAdminBoard: true, currentPage: formType.delete });
|
||||
findModal().vm.$emit('primary');
|
||||
|
||||
await waitForPromises();
|
||||
|
@ -343,7 +329,7 @@ describe('BoardForm', () => {
|
|||
|
||||
it('shows an error flash if GraphQL mutation fails', async () => {
|
||||
mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
|
||||
createComponent({ canAdminBoard: true });
|
||||
createComponent({ canAdminBoard: true, currentPage: formType.delete });
|
||||
findModal().vm.$emit('primary');
|
||||
|
||||
await waitForPromises();
|
||||
|
|
|
@ -0,0 +1,214 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui';
|
||||
import { TEST_HOST } from 'spec/test_constants';
|
||||
import BoardsSelector from '~/boards/components/boards_selector_deprecated.vue';
|
||||
import boardsStore from '~/boards/stores/boards_store';
|
||||
|
||||
const throttleDuration = 1;
|
||||
|
||||
function boardGenerator(n) {
|
||||
return new Array(n).fill().map((board, index) => {
|
||||
const id = `${index}`;
|
||||
const name = `board${id}`;
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
describe('BoardsSelector', () => {
|
||||
let wrapper;
|
||||
let allBoardsResponse;
|
||||
let recentBoardsResponse;
|
||||
const boards = boardGenerator(20);
|
||||
const recentBoards = boardGenerator(5);
|
||||
|
||||
const fillSearchBox = (filterTerm) => {
|
||||
const searchBox = wrapper.find({ ref: 'searchBox' });
|
||||
const searchBoxInput = searchBox.find('input');
|
||||
searchBoxInput.setValue(filterTerm);
|
||||
searchBoxInput.trigger('input');
|
||||
};
|
||||
|
||||
const getDropdownItems = () => wrapper.findAll('.js-dropdown-item');
|
||||
const getDropdownHeaders = () => wrapper.findAll(GlDropdownSectionHeader);
|
||||
const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
|
||||
const findDropdown = () => wrapper.find(GlDropdown);
|
||||
|
||||
beforeEach(() => {
|
||||
const $apollo = {
|
||||
queries: {
|
||||
boards: {
|
||||
loading: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
boardsStore.setEndpoints({
|
||||
boardsEndpoint: '',
|
||||
recentBoardsEndpoint: '',
|
||||
listsEndpoint: '',
|
||||
bulkUpdatePath: '',
|
||||
boardId: '',
|
||||
});
|
||||
|
||||
allBoardsResponse = Promise.resolve({
|
||||
data: {
|
||||
group: {
|
||||
boards: {
|
||||
edges: boards.map((board) => ({ node: board })),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
recentBoardsResponse = Promise.resolve({
|
||||
data: recentBoards,
|
||||
});
|
||||
|
||||
boardsStore.allBoards = jest.fn(() => allBoardsResponse);
|
||||
boardsStore.recentBoards = jest.fn(() => recentBoardsResponse);
|
||||
|
||||
wrapper = mount(BoardsSelector, {
|
||||
propsData: {
|
||||
throttleDuration,
|
||||
currentBoard: {
|
||||
id: 1,
|
||||
name: 'Development',
|
||||
milestone_id: null,
|
||||
weight: null,
|
||||
assignee_id: null,
|
||||
labels: [],
|
||||
},
|
||||
boardBaseUrl: `${TEST_HOST}/board/base/url`,
|
||||
hasMissingBoards: false,
|
||||
canAdminBoard: true,
|
||||
multipleIssueBoardsAvailable: true,
|
||||
labelsPath: `${TEST_HOST}/labels/path`,
|
||||
labelsWebUrl: `${TEST_HOST}/labels`,
|
||||
projectId: 42,
|
||||
groupId: 19,
|
||||
scopedIssueBoardFeatureEnabled: true,
|
||||
weights: [],
|
||||
},
|
||||
mocks: { $apollo },
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
||||
wrapper.vm.$apollo.addSmartQuery = jest.fn((_, options) => {
|
||||
wrapper.setData({
|
||||
[options.loadingKey]: true,
|
||||
});
|
||||
});
|
||||
|
||||
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
|
||||
findDropdown().vm.$emit('show');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
describe('loading', () => {
|
||||
// we are testing loading state, so don't resolve responses until after the tests
|
||||
afterEach(() => {
|
||||
return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick());
|
||||
});
|
||||
|
||||
it('shows loading spinner', () => {
|
||||
expect(getDropdownHeaders()).toHaveLength(0);
|
||||
expect(getDropdownItems()).toHaveLength(0);
|
||||
expect(getLoadingIcon().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loaded', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.setData({
|
||||
loadingBoards: false,
|
||||
});
|
||||
return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick());
|
||||
});
|
||||
|
||||
it('hides loading spinner', () => {
|
||||
expect(getLoadingIcon().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('filtering', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setData({
|
||||
boards,
|
||||
});
|
||||
|
||||
return nextTick();
|
||||
});
|
||||
|
||||
it('shows all boards without filtering', () => {
|
||||
expect(getDropdownItems()).toHaveLength(boards.length + recentBoards.length);
|
||||
});
|
||||
|
||||
it('shows only matching boards when filtering', () => {
|
||||
const filterTerm = 'board1';
|
||||
const expectedCount = boards.filter((board) => board.name.includes(filterTerm)).length;
|
||||
|
||||
fillSearchBox(filterTerm);
|
||||
|
||||
return nextTick().then(() => {
|
||||
expect(getDropdownItems()).toHaveLength(expectedCount);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows message if there are no matching boards', () => {
|
||||
fillSearchBox('does not exist');
|
||||
|
||||
return nextTick().then(() => {
|
||||
expect(getDropdownItems()).toHaveLength(0);
|
||||
expect(wrapper.text().includes('No matching boards found')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('recent boards section', () => {
|
||||
it('shows only when boards are greater than 10', () => {
|
||||
wrapper.setData({
|
||||
boards,
|
||||
});
|
||||
|
||||
return nextTick().then(() => {
|
||||
expect(getDropdownHeaders()).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show when boards are less than 10', () => {
|
||||
wrapper.setData({
|
||||
boards: boards.slice(0, 5),
|
||||
});
|
||||
|
||||
return nextTick().then(() => {
|
||||
expect(getDropdownHeaders()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show when recentBoards api returns empty array', () => {
|
||||
wrapper.setData({
|
||||
recentBoards: [],
|
||||
});
|
||||
|
||||
return nextTick().then(() => {
|
||||
expect(getDropdownHeaders()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show when search is active', () => {
|
||||
fillSearchBox('Random string');
|
||||
|
||||
return nextTick().then(() => {
|
||||
expect(getDropdownHeaders()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,9 +1,10 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { TEST_HOST } from 'spec/test_constants';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import BoardsSelector from '~/boards/components/boards_selector.vue';
|
||||
import boardsStore from '~/boards/stores/boards_store';
|
||||
|
||||
const throttleDuration = 1;
|
||||
|
||||
|
@ -23,6 +24,7 @@ describe('BoardsSelector', () => {
|
|||
let wrapper;
|
||||
let allBoardsResponse;
|
||||
let recentBoardsResponse;
|
||||
let mock;
|
||||
const boards = boardGenerator(20);
|
||||
const recentBoards = boardGenerator(5);
|
||||
|
||||
|
@ -39,6 +41,7 @@ describe('BoardsSelector', () => {
|
|||
const findDropdown = () => wrapper.find(GlDropdown);
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
const $apollo = {
|
||||
queries: {
|
||||
boards: {
|
||||
|
@ -47,14 +50,6 @@ describe('BoardsSelector', () => {
|
|||
},
|
||||
};
|
||||
|
||||
boardsStore.setEndpoints({
|
||||
boardsEndpoint: '',
|
||||
recentBoardsEndpoint: '',
|
||||
listsEndpoint: '',
|
||||
bulkUpdatePath: '',
|
||||
boardId: '',
|
||||
});
|
||||
|
||||
allBoardsResponse = Promise.resolve({
|
||||
data: {
|
||||
group: {
|
||||
|
@ -68,9 +63,6 @@ describe('BoardsSelector', () => {
|
|||
data: recentBoards,
|
||||
});
|
||||
|
||||
boardsStore.allBoards = jest.fn(() => allBoardsResponse);
|
||||
boardsStore.recentBoards = jest.fn(() => recentBoardsResponse);
|
||||
|
||||
wrapper = mount(BoardsSelector, {
|
||||
propsData: {
|
||||
throttleDuration,
|
||||
|
@ -95,6 +87,10 @@ describe('BoardsSelector', () => {
|
|||
},
|
||||
mocks: { $apollo },
|
||||
attachTo: document.body,
|
||||
provide: {
|
||||
fullPath: '',
|
||||
recentBoardsEndpoint: `${TEST_HOST}/recent`,
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.vm.$apollo.addSmartQuery = jest.fn((_, options) => {
|
||||
|
@ -103,6 +99,8 @@ describe('BoardsSelector', () => {
|
|||
});
|
||||
});
|
||||
|
||||
mock.onGet(`${TEST_HOST}/recent`).replyOnce(200, recentBoards);
|
||||
|
||||
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
|
||||
findDropdown().vm.$emit('show');
|
||||
});
|
||||
|
@ -110,6 +108,7 @@ describe('BoardsSelector', () => {
|
|||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('loading', () => {
|
||||
|
@ -133,7 +132,8 @@ describe('BoardsSelector', () => {
|
|||
return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick());
|
||||
});
|
||||
|
||||
it('hides loading spinner', () => {
|
||||
it('hides loading spinner', async () => {
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(getLoadingIcon().exists()).toBe(false);
|
||||
});
|
||||
|
||||
|
|
|
@ -3,84 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::Boards::Lists::Create do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:group) { create(:group, :private) }
|
||||
let_it_be(:board) { create(:board, group: group) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:guest) { create(:user) }
|
||||
|
||||
let(:current_user) { user }
|
||||
let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
|
||||
let(:list_create_params) { {} }
|
||||
|
||||
before_all do
|
||||
group.add_reporter(user)
|
||||
group.add_guest(guest)
|
||||
end
|
||||
|
||||
subject { mutation.resolve(board_id: board.to_global_id.to_s, **list_create_params) }
|
||||
|
||||
describe '#ready?' do
|
||||
it 'raises an error if required arguments are missing' do
|
||||
expect { mutation.ready?(board_id: 'some id') }
|
||||
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/)
|
||||
end
|
||||
|
||||
it 'raises an error if too many required arguments are specified' do
|
||||
expect { mutation.ready?(board_id: 'some id', backlog: true, label_id: 'some label') }
|
||||
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#resolve' do
|
||||
context 'with proper permissions' do
|
||||
describe 'backlog list' do
|
||||
let(:list_create_params) { { backlog: true } }
|
||||
|
||||
it 'creates one and only one backlog' do
|
||||
expect { subject }.to change { board.lists.backlog.count }.from(0).to(1)
|
||||
expect(board.lists.backlog.first.list_type).to eq 'backlog'
|
||||
|
||||
backlog_id = board.lists.backlog.first.id
|
||||
|
||||
expect { subject }.not_to change { board.lists.backlog.count }
|
||||
expect(board.lists.backlog.last.id).to eq backlog_id
|
||||
end
|
||||
end
|
||||
|
||||
describe 'label list' do
|
||||
let_it_be(:dev_label) do
|
||||
create(:group_label, title: 'Development', color: '#FFAABB', group: group)
|
||||
end
|
||||
|
||||
let(:list_create_params) { { label_id: dev_label.to_global_id.to_s } }
|
||||
|
||||
it 'creates a new issue board list for labels' do
|
||||
expect { subject }.to change { board.lists.count }.from(1).to(2)
|
||||
|
||||
new_list = subject[:list]
|
||||
|
||||
expect(new_list.title).to eq dev_label.title
|
||||
expect(new_list.position).to eq 0
|
||||
end
|
||||
|
||||
context 'when label not found' do
|
||||
let(:list_create_params) { { label_id: "gid://gitlab/Label/#{non_existing_record_id}" } }
|
||||
|
||||
it 'returns an error' do
|
||||
expect(subject[:errors]).to include 'Label not found'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without proper permissions' do
|
||||
let(:current_user) { guest }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||
end
|
||||
end
|
||||
end
|
||||
it_behaves_like 'board lists create mutation'
|
||||
end
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
# frozen_string_literal: true
|
||||
require 'fast_spec_helper'
|
||||
|
||||
RSpec.describe 'Net::HTTP patch proxy user and password encoding' do
|
||||
let(:net_http) { Net::HTTP.new('hostname.example') }
|
||||
|
||||
describe '#proxy_user' do
|
||||
subject { net_http.proxy_user }
|
||||
|
||||
it { is_expected.to eq(nil) }
|
||||
|
||||
context 'with http_proxy env' do
|
||||
let(:http_proxy) { 'http://proxy.example:8000' }
|
||||
|
||||
before do
|
||||
allow(ENV).to receive(:[]).and_call_original
|
||||
allow(ENV).to receive(:[]).with('http_proxy').and_return(http_proxy)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(nil) }
|
||||
|
||||
context 'and user:password authentication' do
|
||||
let(:http_proxy) { 'http://Y%5CX:R%25S%5D%20%3FX@proxy.example:8000' }
|
||||
|
||||
context 'when on multiuser safe platform' do
|
||||
# linux, freebsd, darwin are considered multi user safe platforms
|
||||
# See https://github.com/ruby/net-http/blob/v0.1.1/lib/net/http.rb#L1174-L1178
|
||||
|
||||
before do
|
||||
allow(net_http).to receive(:environment_variable_is_multiuser_safe?).and_return(true)
|
||||
end
|
||||
|
||||
it { is_expected.to eq 'Y\\X' }
|
||||
end
|
||||
|
||||
context 'when not on multiuser safe platform' do
|
||||
before do
|
||||
allow(net_http).to receive(:environment_variable_is_multiuser_safe?).and_return(false)
|
||||
end
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#proxy_pass' do
|
||||
subject { net_http.proxy_pass }
|
||||
|
||||
it { is_expected.to eq(nil) }
|
||||
|
||||
context 'with http_proxy env' do
|
||||
let(:http_proxy) { 'http://proxy.example:8000' }
|
||||
|
||||
before do
|
||||
allow(ENV).to receive(:[]).and_call_original
|
||||
allow(ENV).to receive(:[]).with('http_proxy').and_return(http_proxy)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(nil) }
|
||||
|
||||
context 'and user:password authentication' do
|
||||
let(:http_proxy) { 'http://Y%5CX:R%25S%5D%20%3FX@proxy.example:8000' }
|
||||
|
||||
context 'when on multiuser safe platform' do
|
||||
# linux, freebsd, darwin are considered multi user safe platforms
|
||||
# See https://github.com/ruby/net-http/blob/v0.1.1/lib/net/http.rb#L1174-L1178
|
||||
|
||||
before do
|
||||
allow(net_http).to receive(:environment_variable_is_multiuser_safe?).and_return(true)
|
||||
end
|
||||
|
||||
it { is_expected.to eq 'R%S] ?X' }
|
||||
end
|
||||
|
||||
context 'when not on multiuser safe platform' do
|
||||
before do
|
||||
allow(net_http).to receive(:environment_variable_is_multiuser_safe?).and_return(false)
|
||||
end
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,235 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Banzai::Filter::FeatureFlagReferenceFilter do
|
||||
include FilterSpecHelper
|
||||
|
||||
let_it_be(:project) { create(:project, :public) }
|
||||
let_it_be(:feature_flag) { create(:operations_feature_flag, project: project) }
|
||||
let_it_be(:reference) { feature_flag.to_reference }
|
||||
|
||||
it 'requires project context' do
|
||||
expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
|
||||
end
|
||||
|
||||
%w(pre code a style).each do |elem|
|
||||
it "ignores valid references contained inside '#{elem}' element" do
|
||||
exp = act = "<#{elem}>Feature Flag #{reference}</#{elem}>"
|
||||
|
||||
expect(reference_filter(act).to_html).to eq exp
|
||||
end
|
||||
end
|
||||
|
||||
context 'with internal reference' do
|
||||
it 'links to a valid reference' do
|
||||
doc = reference_filter("See #{reference}")
|
||||
|
||||
expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project, feature_flag)
|
||||
end
|
||||
|
||||
it 'links with adjacent text' do
|
||||
doc = reference_filter("Feature Flag (#{reference}.)")
|
||||
|
||||
expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(reference)}</a>\.\)})
|
||||
end
|
||||
|
||||
it 'ignores invalid feature flag IIDs' do
|
||||
exp = act = "Check [feature_flag:#{non_existing_record_id}]"
|
||||
|
||||
expect(reference_filter(act).to_html).to eq exp
|
||||
end
|
||||
|
||||
it 'includes a title attribute' do
|
||||
doc = reference_filter("Feature Flag #{reference}")
|
||||
|
||||
expect(doc.css('a').first.attr('title')).to eq feature_flag.name
|
||||
end
|
||||
|
||||
it 'escapes the title attribute' do
|
||||
allow(feature_flag).to receive(:name).and_return(%{"></a>whatever<a title="})
|
||||
doc = reference_filter("Feature Flag #{reference}")
|
||||
|
||||
expect(doc.text).to eq "Feature Flag #{reference}"
|
||||
end
|
||||
|
||||
it 'includes default classes' do
|
||||
doc = reference_filter("Feature Flag #{reference}")
|
||||
|
||||
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-feature_flag has-tooltip'
|
||||
end
|
||||
|
||||
it 'includes a data-project attribute' do
|
||||
doc = reference_filter("Feature Flag #{reference}")
|
||||
link = doc.css('a').first
|
||||
|
||||
expect(link).to have_attribute('data-project')
|
||||
expect(link.attr('data-project')).to eq project.id.to_s
|
||||
end
|
||||
|
||||
it 'includes a data-feature-flag attribute' do
|
||||
doc = reference_filter("See #{reference}")
|
||||
link = doc.css('a').first
|
||||
|
||||
expect(link).to have_attribute('data-feature-flag')
|
||||
expect(link.attr('data-feature-flag')).to eq feature_flag.id.to_s
|
||||
end
|
||||
|
||||
it 'supports an :only_path context' do
|
||||
doc = reference_filter("Feature Flag #{reference}", only_path: true)
|
||||
link = doc.css('a').first.attr('href')
|
||||
|
||||
expect(link).not_to match %r(https?://)
|
||||
expect(link).to eq urls.edit_project_feature_flag_url(project, feature_flag.iid, only_path: true)
|
||||
end
|
||||
|
||||
context 'when feature_flag_contextual_issue feture flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(feature_flag_contextual_issue: false)
|
||||
end
|
||||
|
||||
it 'does not link the reference' do
|
||||
doc = reference_filter("See #{reference}")
|
||||
|
||||
expect(doc.css('a').first).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with cross-project / cross-namespace complete reference' do
|
||||
let_it_be(:namespace) { create(:namespace) }
|
||||
let_it_be(:project2) { create(:project, :public, namespace: namespace) }
|
||||
let_it_be(:feature_flag) { create(:operations_feature_flag, project: project2) }
|
||||
let_it_be(:reference) { "[feature_flag:#{project2.full_path}/#{feature_flag.iid}]" }
|
||||
|
||||
it 'links to a valid reference' do
|
||||
doc = reference_filter("See #{reference}")
|
||||
|
||||
expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project2, feature_flag)
|
||||
end
|
||||
|
||||
it 'produces a valid text in a link' do
|
||||
doc = reference_filter("See (#{reference}.)")
|
||||
|
||||
expect(doc.css('a').first.text).to eql(reference)
|
||||
end
|
||||
|
||||
it 'produces a valid text' do
|
||||
doc = reference_filter("See (#{reference}.)")
|
||||
|
||||
expect(doc.text).to eql("See (#{reference}.)")
|
||||
end
|
||||
|
||||
it 'ignores invalid feature flag IIDs on the referenced project' do
|
||||
exp = act = "Check [feature_flag:#{non_existing_record_id}]"
|
||||
|
||||
expect(reference_filter(act).to_html).to eq exp
|
||||
end
|
||||
end
|
||||
|
||||
context 'with cross-project / same-namespace complete reference' do
|
||||
let_it_be(:namespace) { create(:namespace) }
|
||||
let_it_be(:project) { create(:project, :public, namespace: namespace) }
|
||||
let_it_be(:project2) { create(:project, :public, namespace: namespace) }
|
||||
let_it_be(:feature_flag) { create(:operations_feature_flag, project: project2) }
|
||||
let_it_be(:reference) { "[feature_flag:#{project2.full_path}/#{feature_flag.iid}]" }
|
||||
|
||||
it 'links to a valid reference' do
|
||||
doc = reference_filter("See #{reference}")
|
||||
|
||||
expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project2, feature_flag)
|
||||
end
|
||||
|
||||
it 'produces a valid text in a link' do
|
||||
doc = reference_filter("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)")
|
||||
|
||||
expect(doc.css('a').first.text).to eql("[feature_flag:#{project2.path}/#{feature_flag.iid}]")
|
||||
end
|
||||
|
||||
it 'produces a valid text' do
|
||||
doc = reference_filter("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)")
|
||||
|
||||
expect(doc.text).to eql("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)")
|
||||
end
|
||||
|
||||
it 'ignores invalid feature flag IIDs on the referenced project' do
|
||||
exp = act = "Check [feature_flag:#{non_existing_record_id}]"
|
||||
|
||||
expect(reference_filter(act).to_html).to eq exp
|
||||
end
|
||||
end
|
||||
|
||||
context 'with cross-project shorthand reference' do
|
||||
let_it_be(:namespace) { create(:namespace) }
|
||||
let_it_be(:project) { create(:project, :public, namespace: namespace) }
|
||||
let_it_be(:project2) { create(:project, :public, namespace: namespace) }
|
||||
let_it_be(:feature_flag) { create(:operations_feature_flag, project: project2) }
|
||||
let_it_be(:reference) { "[feature_flag:#{project2.path}/#{feature_flag.iid}]" }
|
||||
|
||||
it 'links to a valid reference' do
|
||||
doc = reference_filter("See #{reference}")
|
||||
|
||||
expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project2, feature_flag)
|
||||
end
|
||||
|
||||
it 'produces a valid text in a link' do
|
||||
doc = reference_filter("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)")
|
||||
|
||||
expect(doc.css('a').first.text).to eql("[feature_flag:#{project2.path}/#{feature_flag.iid}]")
|
||||
end
|
||||
|
||||
it 'produces a valid text' do
|
||||
doc = reference_filter("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)")
|
||||
|
||||
expect(doc.text).to eql("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)")
|
||||
end
|
||||
|
||||
it 'ignores invalid feature flag IDs on the referenced project' do
|
||||
exp = act = "Check [feature_flag:#{non_existing_record_id}]"
|
||||
|
||||
expect(reference_filter(act).to_html).to eq exp
|
||||
end
|
||||
end
|
||||
|
||||
context 'with cross-project URL reference' do
|
||||
let_it_be(:namespace) { create(:namespace, name: 'cross-reference') }
|
||||
let_it_be(:project2) { create(:project, :public, namespace: namespace) }
|
||||
let_it_be(:feature_flag) { create(:operations_feature_flag, project: project2) }
|
||||
let_it_be(:reference) { urls.edit_project_feature_flag_url(project2, feature_flag) }
|
||||
|
||||
it 'links to a valid reference' do
|
||||
doc = reference_filter("See #{reference}")
|
||||
|
||||
expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project2, feature_flag)
|
||||
end
|
||||
|
||||
it 'links with adjacent text' do
|
||||
doc = reference_filter("See (#{reference}.)")
|
||||
|
||||
expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(feature_flag.to_reference(project))}</a>\.\)})
|
||||
end
|
||||
|
||||
it 'ignores invalid feature flag IIDs on the referenced project' do
|
||||
act = "See #{invalidate_reference(reference)}"
|
||||
|
||||
expect(reference_filter(act).to_html).to match(%r{<a.+>#{Regexp.escape(invalidate_reference(reference))}</a>})
|
||||
end
|
||||
end
|
||||
|
||||
context 'with group context' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
it 'links to a valid reference' do
|
||||
reference = "[feature_flag:#{project.full_path}/#{feature_flag.iid}]"
|
||||
result = reference_filter("See #{reference}", { project: nil, group: group } )
|
||||
|
||||
expect(result.css('a').first.attr('href')).to eq(urls.edit_project_feature_flag_url(project, feature_flag))
|
||||
end
|
||||
|
||||
it 'ignores internal references' do
|
||||
exp = act = "See [feature_flag:#{feature_flag.iid}]"
|
||||
|
||||
expect(reference_filter(act, project: nil, group: group).to_html).to eq exp
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,53 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Banzai::ReferenceParser::FeatureFlagParser do
|
||||
include ReferenceParserHelpers
|
||||
|
||||
subject { described_class.new(Banzai::RenderContext.new(project, user)) }
|
||||
|
||||
let(:link) { empty_html_link }
|
||||
|
||||
describe '#nodes_visible_to_user' do
|
||||
let(:project) { create(:project, :public) }
|
||||
let(:user) { create(:user) }
|
||||
let(:feature_flag) { create(:operations_feature_flag, project: project) }
|
||||
|
||||
context 'when the link has a data-issue attribute' do
|
||||
before do
|
||||
link['data-feature-flag'] = feature_flag.id.to_s
|
||||
end
|
||||
|
||||
it_behaves_like "referenced feature visibility", "issues", "merge_requests" do
|
||||
before do
|
||||
project.add_developer(user) if enable_user?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#referenced_by' do
|
||||
let_it_be(:project) { create(:project, :public) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:feature_flag) { create(:operations_feature_flag, project: project) }
|
||||
|
||||
describe 'when the link has a data-feature-flag attribute' do
|
||||
context 'using an existing feature flag ID' do
|
||||
it 'returns an Array of feature flags' do
|
||||
link['data-feature-flag'] = feature_flag.id.to_s
|
||||
|
||||
expect(subject.referenced_by([link])).to eq([feature_flag])
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a non-existing feature flag ID' do
|
||||
it 'returns an empty Array' do
|
||||
link['data-feature-flag'] = ''
|
||||
|
||||
expect(subject.referenced_by([link])).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1778,19 +1778,6 @@ RSpec.describe Group do
|
|||
describe 'with Debian Distributions' do
|
||||
subject { create(:group) }
|
||||
|
||||
let!(:distributions) { create_list(:debian_group_distribution, 2, :with_file, container: subject) }
|
||||
|
||||
it 'removes distribution files on removal' do
|
||||
distribution_file_paths = distributions.map do |distribution|
|
||||
distribution.file.path
|
||||
end
|
||||
|
||||
expect { subject.destroy }
|
||||
.to change {
|
||||
distribution_file_paths.select do |path|
|
||||
File.exist? path
|
||||
end.length
|
||||
}.from(distribution_file_paths.length).to(0)
|
||||
end
|
||||
it_behaves_like 'model with Debian distributions'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,6 +16,35 @@ RSpec.describe Operations::FeatureFlag do
|
|||
it { is_expected.to have_many(:scopes) }
|
||||
end
|
||||
|
||||
describe '.reference_pattern' do
|
||||
subject { described_class.reference_pattern }
|
||||
|
||||
it { is_expected.to match('[feature_flag:123]') }
|
||||
it { is_expected.to match('[feature_flag:gitlab-org/gitlab/123]') }
|
||||
end
|
||||
|
||||
describe '.link_reference_pattern' do
|
||||
subject { described_class.link_reference_pattern }
|
||||
|
||||
it { is_expected.to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab/-/feature_flags/123/edit") }
|
||||
it { is_expected.not_to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab/issues/123/edit") }
|
||||
it { is_expected.not_to match("gitlab-org/gitlab/-/feature_flags/123/edit") }
|
||||
end
|
||||
|
||||
describe '#to_reference' do
|
||||
let(:namespace) { build(:namespace, path: 'sample-namespace') }
|
||||
let(:project) { build(:project, name: 'sample-project', namespace: namespace) }
|
||||
let(:feature_flag) { build(:operations_feature_flag, iid: 1, project: project) }
|
||||
|
||||
it 'returns feature flag id' do
|
||||
expect(feature_flag.to_reference).to eq '[feature_flag:1]'
|
||||
end
|
||||
|
||||
it 'returns complete path to the feature flag with full: true' do
|
||||
expect(feature_flag.to_reference(full: true)).to eq '[feature_flag:sample-namespace/sample-project/1]'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
it { is_expected.to validate_presence_of(:project) }
|
||||
it { is_expected.to validate_presence_of(:name) }
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Packages::Debian::GroupComponentFile do
|
||||
it_behaves_like 'Debian Component File', :group, false
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Packages::Debian::ProjectComponentFile do
|
||||
it_behaves_like 'Debian Component File', :project, true
|
||||
end
|
|
@ -6376,20 +6376,7 @@ RSpec.describe Project, factory_default: :keep do
|
|||
describe 'with Debian Distributions' do
|
||||
subject { create(:project) }
|
||||
|
||||
let!(:distributions) { create_list(:debian_project_distribution, 2, :with_file, container: subject) }
|
||||
|
||||
it 'removes distribution files on removal' do
|
||||
distribution_file_paths = distributions.map do |distribution|
|
||||
distribution.file.path
|
||||
end
|
||||
|
||||
expect { subject.destroy }
|
||||
.to change {
|
||||
distribution_file_paths.select do |path|
|
||||
File.exist? path
|
||||
end.length
|
||||
}.from(distribution_file_paths.length).to(0)
|
||||
end
|
||||
it_behaves_like 'model with Debian distributions'
|
||||
end
|
||||
|
||||
describe '#environments_for_scope' do
|
||||
|
|
|
@ -3,52 +3,10 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Create a label or backlog board list' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:group) { create(:group, :private) }
|
||||
let_it_be(:board) { create(:board, group: group) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:dev_label) do
|
||||
create(:group_label, title: 'Development', color: '#FFAABB', group: group)
|
||||
end
|
||||
|
||||
let(:current_user) { user }
|
||||
let(:mutation) { graphql_mutation(:board_list_create, input) }
|
||||
let(:mutation_response) { graphql_mutation_response(:board_list_create) }
|
||||
|
||||
context 'the user is not allowed to read board lists' do
|
||||
let(:input) { { board_id: board.to_global_id.to_s, backlog: true } }
|
||||
|
||||
it_behaves_like 'a mutation that returns a top-level access error'
|
||||
end
|
||||
|
||||
context 'when user has permissions to admin board lists' do
|
||||
before do
|
||||
group.add_reporter(current_user)
|
||||
end
|
||||
|
||||
describe 'backlog list' do
|
||||
let(:input) { { board_id: board.to_global_id.to_s, backlog: true } }
|
||||
|
||||
it 'creates the list' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response['list'])
|
||||
.to include('position' => nil, 'listType' => 'backlog')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'label list' do
|
||||
let(:input) { { board_id: board.to_global_id.to_s, label_id: dev_label.to_global_id.to_s } }
|
||||
|
||||
it 'creates the list' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response['list'])
|
||||
.to include('position' => 0, 'listType' => 'label', 'label' => include('title' => 'Development'))
|
||||
end
|
||||
end
|
||||
it_behaves_like 'board lists create request' do
|
||||
let(:mutation_name) { :board_list_create }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,99 +3,23 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Boards::Lists::CreateService do
|
||||
describe '#execute' do
|
||||
shared_examples 'creating board lists' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
context 'when board parent is a project' do
|
||||
let_it_be(:parent) { create(:project) }
|
||||
let_it_be(:board) { create(:board, project: parent) }
|
||||
let_it_be(:label) { create(:label, project: parent, name: 'in-progress') }
|
||||
|
||||
before_all do
|
||||
parent.add_developer(user)
|
||||
end
|
||||
it_behaves_like 'board lists create service'
|
||||
end
|
||||
|
||||
subject(:service) { described_class.new(parent, user, label_id: label.id) }
|
||||
context 'when board parent is a group' do
|
||||
let_it_be(:parent) { create(:group) }
|
||||
let_it_be(:board) { create(:board, group: parent) }
|
||||
let_it_be(:label) { create(:group_label, group: parent, name: 'in-progress') }
|
||||
|
||||
context 'when board lists is empty' do
|
||||
it 'creates a new list at beginning of the list' do
|
||||
response = service.execute(board)
|
||||
it_behaves_like 'board lists create service'
|
||||
end
|
||||
|
||||
expect(response.success?).to eq(true)
|
||||
expect(response.payload[:list].position).to eq 0
|
||||
end
|
||||
end
|
||||
|
||||
context 'when board lists has the done list' do
|
||||
it 'creates a new list at beginning of the list' do
|
||||
response = service.execute(board)
|
||||
|
||||
expect(response.success?).to eq(true)
|
||||
expect(response.payload[:list].position).to eq 0
|
||||
end
|
||||
end
|
||||
|
||||
context 'when board lists has labels lists' do
|
||||
it 'creates a new list at end of the lists' do
|
||||
create(:list, board: board, position: 0)
|
||||
create(:list, board: board, position: 1)
|
||||
|
||||
response = service.execute(board)
|
||||
|
||||
expect(response.success?).to eq(true)
|
||||
expect(response.payload[:list].position).to eq 2
|
||||
end
|
||||
end
|
||||
|
||||
context 'when board lists has label and done lists' do
|
||||
it 'creates a new list at end of the label lists' do
|
||||
list1 = create(:list, board: board, position: 0)
|
||||
|
||||
list2 = service.execute(board).payload[:list]
|
||||
|
||||
expect(list1.reload.position).to eq 0
|
||||
expect(list2.reload.position).to eq 1
|
||||
end
|
||||
end
|
||||
|
||||
context 'when provided label does not belong to the parent' do
|
||||
it 'returns an error' do
|
||||
label = create(:label, name: 'in-development')
|
||||
service = described_class.new(parent, user, label_id: label.id)
|
||||
|
||||
response = service.execute(board)
|
||||
|
||||
expect(response.success?).to eq(false)
|
||||
expect(response.errors).to include('Label not found')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when backlog param is sent' do
|
||||
it 'creates one and only one backlog list' do
|
||||
service = described_class.new(parent, user, 'backlog' => true)
|
||||
list = service.execute(board).payload[:list]
|
||||
|
||||
expect(list.list_type).to eq('backlog')
|
||||
expect(list.position).to be_nil
|
||||
expect(list).to be_valid
|
||||
|
||||
another_backlog = service.execute(board).payload[:list]
|
||||
|
||||
expect(another_backlog).to eq list
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when board parent is a project' do
|
||||
let_it_be(:parent) { create(:project) }
|
||||
let_it_be(:board) { create(:board, project: parent) }
|
||||
let_it_be(:label) { create(:label, project: parent, name: 'in-progress') }
|
||||
|
||||
it_behaves_like 'creating board lists'
|
||||
end
|
||||
|
||||
context 'when board parent is a group' do
|
||||
let_it_be(:parent) { create(:group) }
|
||||
let_it_be(:board) { create(:board, group: parent) }
|
||||
let_it_be(:label) { create(:group_label, group: parent, name: 'in-progress') }
|
||||
|
||||
it_behaves_like 'creating board lists'
|
||||
end
|
||||
def create_list(params)
|
||||
create(:list, params.merge(board: board))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,6 +15,8 @@ RSpec.describe Packages::Debian::CreateDistributionService do
|
|||
.from(nil).to(expected_components.count)
|
||||
.and change { container.debian_distributions.first&.architectures&.count }
|
||||
.from(nil).to(expected_architectures.count)
|
||||
.and not_change { Packages::Debian::ProjectComponentFile.count }
|
||||
.and not_change { Packages::Debian::GroupComponentFile.count }
|
||||
else
|
||||
expect { response }
|
||||
.to not_change { container.debian_distributions.klass.all.count }
|
||||
|
@ -23,6 +25,8 @@ RSpec.describe Packages::Debian::CreateDistributionService do
|
|||
.and not_change { Packages::Debian::GroupComponent.count }
|
||||
.and not_change { Packages::Debian::ProjectArchitecture.count }
|
||||
.and not_change { Packages::Debian::GroupArchitecture.count }
|
||||
.and not_change { Packages::Debian::ProjectComponentFile.count }
|
||||
.and not_change { Packages::Debian::GroupComponentFile.count }
|
||||
end
|
||||
|
||||
expect(response).to be_a(ServiceResponse)
|
||||
|
|
|
@ -15,12 +15,15 @@ RSpec.describe Packages::Debian::DestroyDistributionService do
|
|||
.from(2).to(0)
|
||||
.and change { architecture1.class.all.count }
|
||||
.from(3).to(0)
|
||||
.and change { component_file1.class.all.count }
|
||||
.from(4).to(0)
|
||||
else
|
||||
expect { response }
|
||||
.to not_change { container.debian_distributions.klass.all.count }
|
||||
.and not_change { container.debian_distributions.count }
|
||||
.and not_change { component1.class.all.count }
|
||||
.and not_change { architecture1.class.all.count }
|
||||
.and not_change { component_file1.class.all.count }
|
||||
end
|
||||
|
||||
expect(response).to be_a(ServiceResponse)
|
||||
|
@ -45,6 +48,10 @@ RSpec.describe Packages::Debian::DestroyDistributionService do
|
|||
let_it_be(:architecture0, freeze: true) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'all') }
|
||||
let_it_be(:architecture1, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'architecture1') }
|
||||
let_it_be(:architecture2, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'architecture2') }
|
||||
let_it_be(:component_file1, freeze: can_freeze) { create("debian_#{container_type}_component_file", :source, component: component1) }
|
||||
let_it_be(:component_file2, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1, architecture: architecture1) }
|
||||
let_it_be(:component_file3, freeze: can_freeze) { create("debian_#{container_type}_component_file", :source, component: component2) }
|
||||
let_it_be(:component_file4, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component2, architecture: architecture2) }
|
||||
|
||||
subject { described_class.new(distribution) }
|
||||
|
||||
|
|
|
@ -3,15 +3,26 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Packages::Debian::UpdateDistributionService do
|
||||
RSpec.shared_examples 'Update Debian Distribution' do |expected_message, expected_components, expected_architectures|
|
||||
RSpec.shared_examples 'Update Debian Distribution' do |expected_message, expected_components, expected_architectures, component_file_delta = 0|
|
||||
it 'returns ServiceResponse', :aggregate_failures do
|
||||
expect(distribution).to receive(:update).with(simple_params).and_call_original if expected_message.nil?
|
||||
|
||||
expect { response }
|
||||
.to not_change { container.debian_distributions.klass.all.count }
|
||||
.and not_change { container.debian_distributions.count }
|
||||
.and not_change { component1.class.all.count }
|
||||
.and not_change { architecture1.class.all.count }
|
||||
if component_file_delta.zero?
|
||||
expect { response }
|
||||
.to not_change { container.debian_distributions.klass.all.count }
|
||||
.and not_change { container.debian_distributions.count }
|
||||
.and not_change { component1.class.all.count }
|
||||
.and not_change { architecture1.class.all.count }
|
||||
.and not_change { component_file1.class.all.count }
|
||||
else
|
||||
expect { response }
|
||||
.to not_change { container.debian_distributions.klass.all.count }
|
||||
.and not_change { container.debian_distributions.count }
|
||||
.and not_change { component1.class.all.count }
|
||||
.and not_change { architecture1.class.all.count }
|
||||
.and change { component_file1.class.all.count }
|
||||
.from(4).to(4 + component_file_delta)
|
||||
end
|
||||
|
||||
expect(response).to be_a(ServiceResponse)
|
||||
expect(response.success?).to eq(expected_message.nil?)
|
||||
|
@ -48,6 +59,10 @@ RSpec.describe Packages::Debian::UpdateDistributionService do
|
|||
let_it_be(:architecture0) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'all') }
|
||||
let_it_be(:architecture1) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'architecture1') }
|
||||
let_it_be(:architecture2) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'architecture2') }
|
||||
let_it_be(:component_file1) { create("debian_#{container_type}_component_file", :source, component: component1) }
|
||||
let_it_be(:component_file2) { create("debian_#{container_type}_component_file", component: component1, architecture: architecture1) }
|
||||
let_it_be(:component_file3) { create("debian_#{container_type}_component_file", :source, component: component2) }
|
||||
let_it_be(:component_file4) { create("debian_#{container_type}_component_file", component: component2, architecture: architecture2) }
|
||||
|
||||
let(:original_params) do
|
||||
{
|
||||
|
@ -110,7 +125,7 @@ RSpec.describe Packages::Debian::UpdateDistributionService do
|
|||
}
|
||||
end
|
||||
|
||||
it_behaves_like 'Update Debian Distribution', nil, %w[component2 component3], %w[all architecture2 architecture3]
|
||||
it_behaves_like 'Update Debian Distribution', nil, %w[component2 component3], %w[all architecture2 architecture3], -2
|
||||
end
|
||||
|
||||
context 'with invalid components' do
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.shared_examples 'board lists create mutation' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
|
||||
let(:list_create_params) { {} }
|
||||
|
||||
subject { mutation.resolve(board_id: board.to_global_id.to_s, **list_create_params) }
|
||||
|
||||
describe '#ready?' do
|
||||
it 'raises an error if required arguments are missing' do
|
||||
expect { mutation.ready?(board_id: 'some id') }
|
||||
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/)
|
||||
end
|
||||
|
||||
it 'raises an error if too many required arguments are specified' do
|
||||
expect { mutation.ready?(board_id: 'some id', backlog: true, label_id: 'some label') }
|
||||
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#resolve' do
|
||||
context 'with proper permissions' do
|
||||
before_all do
|
||||
group.add_reporter(user)
|
||||
end
|
||||
|
||||
describe 'backlog list' do
|
||||
let(:list_create_params) { { backlog: true } }
|
||||
|
||||
it 'creates one and only one backlog' do
|
||||
expect { subject }.to change { board.lists.backlog.count }.by(1)
|
||||
expect(board.lists.backlog.first.list_type).to eq 'backlog'
|
||||
|
||||
backlog_id = board.lists.backlog.first.id
|
||||
|
||||
expect { subject }.not_to change { board.lists.backlog.count }
|
||||
expect(board.lists.backlog.last.id).to eq backlog_id
|
||||
end
|
||||
end
|
||||
|
||||
describe 'label list' do
|
||||
let_it_be(:dev_label) do
|
||||
create(:group_label, title: 'Development', color: '#FFAABB', group: group)
|
||||
end
|
||||
|
||||
let(:list_create_params) { { label_id: dev_label.to_global_id.to_s } }
|
||||
|
||||
it 'creates a new label board list' do
|
||||
expect { subject }.to change { board.lists.count }.by(1)
|
||||
|
||||
new_list = subject[:list]
|
||||
|
||||
expect(new_list.title).to eq dev_label.title
|
||||
expect(new_list.position).to eq 0
|
||||
end
|
||||
|
||||
context 'when label not found' do
|
||||
let(:list_create_params) { { label_id: "gid://gitlab/Label/#{non_existing_record_id}" } }
|
||||
|
||||
it 'returns an error' do
|
||||
expect(subject[:errors]).to include 'Label not found'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without proper permissions' do
|
||||
before_all do
|
||||
group.add_guest(user)
|
||||
end
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -11,6 +11,7 @@ RSpec.shared_examples 'Debian Distribution Architecture' do |factory, container,
|
|||
|
||||
describe 'relationships' do
|
||||
it { is_expected.to belong_to(:distribution).class_name("Packages::Debian::#{container.capitalize}Distribution").inverse_of(:architectures) }
|
||||
it { is_expected.to have_many(:files).class_name("Packages::Debian::#{container.capitalize}ComponentFile").inverse_of(:architecture) }
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
|
|
|
@ -0,0 +1,245 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze|
|
||||
let_it_be(:container1, freeze: can_freeze) { create(container_type) } # rubocop:disable Rails/SaveBang
|
||||
let_it_be(:container2, freeze: can_freeze) { create(container_type) } # rubocop:disable Rails/SaveBang
|
||||
let_it_be(:distribution1, freeze: can_freeze) { create("debian_#{container_type}_distribution", container: container1) }
|
||||
let_it_be(:distribution2, freeze: can_freeze) { create("debian_#{container_type}_distribution", container: container2) }
|
||||
let_it_be(:architecture1_1, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution1) }
|
||||
let_it_be(:architecture1_2, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution1) }
|
||||
let_it_be(:architecture2_1, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution2) }
|
||||
let_it_be(:architecture2_2, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution2) }
|
||||
let_it_be(:component1_1, freeze: can_freeze) { create("debian_#{container_type}_component", distribution: distribution1) }
|
||||
let_it_be(:component1_2, freeze: can_freeze) { create("debian_#{container_type}_component", distribution: distribution1) }
|
||||
let_it_be(:component2_1, freeze: can_freeze) { create("debian_#{container_type}_component", distribution: distribution2) }
|
||||
let_it_be(:component2_2, freeze: can_freeze) { create("debian_#{container_type}_component", distribution: distribution2) }
|
||||
|
||||
let_it_be_with_refind(:component_file_with_architecture) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1) }
|
||||
let_it_be(:component_file_other_architecture, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_2) }
|
||||
let_it_be(:component_file_other_component, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_2, architecture: architecture1_1) }
|
||||
let_it_be(:component_file_other_compression_type, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, compression_type: :xz) }
|
||||
let_it_be(:component_file_other_file_md5, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, file_md5: 'other_md5') }
|
||||
let_it_be(:component_file_other_file_sha256, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, file_sha256: 'other_sha256') }
|
||||
let_it_be(:component_file_other_container, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component2_1, architecture: architecture2_1) }
|
||||
let_it_be_with_refind(:component_file_with_file_type_source) { create("debian_#{container_type}_component_file", :source, component: component1_1) }
|
||||
let_it_be(:component_file_with_file_type_di_packages, freeze: can_freeze) { create("debian_#{container_type}_component_file", :di_packages, component: component1_1, architecture: architecture1_1) }
|
||||
|
||||
subject { component_file_with_architecture }
|
||||
|
||||
describe 'relationships' do
|
||||
context 'with stubbed uploader' do
|
||||
before do
|
||||
allow_next_instance_of(Packages::Debian::ComponentFileUploader) do |uploader|
|
||||
allow(uploader).to receive(:dynamic_segment).and_return('stubbed')
|
||||
end
|
||||
end
|
||||
|
||||
it { is_expected.to belong_to(:component).class_name("Packages::Debian::#{container_type.capitalize}Component").inverse_of(:files) }
|
||||
end
|
||||
|
||||
context 'with packages file_type' do
|
||||
it { is_expected.to belong_to(:architecture).class_name("Packages::Debian::#{container_type.capitalize}Architecture").inverse_of(:files) }
|
||||
end
|
||||
|
||||
context 'with :source file_type' do
|
||||
subject { component_file_with_file_type_source }
|
||||
|
||||
it { is_expected.to belong_to(:architecture).class_name("Packages::Debian::#{container_type.capitalize}Architecture").inverse_of(:files).optional }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
describe "#component" do
|
||||
before do
|
||||
allow_next_instance_of(Packages::Debian::ComponentFileUploader) do |uploader|
|
||||
allow(uploader).to receive(:dynamic_segment).and_return('stubbed')
|
||||
end
|
||||
end
|
||||
|
||||
it { is_expected.to validate_presence_of(:component) }
|
||||
end
|
||||
|
||||
describe "#architecture" do
|
||||
context 'with packages file_type' do
|
||||
it { is_expected.to validate_presence_of(:architecture) }
|
||||
end
|
||||
|
||||
context 'with :source file_type' do
|
||||
subject { component_file_with_file_type_source }
|
||||
|
||||
it { is_expected.to validate_absence_of(:architecture) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#file_type' do
|
||||
it { is_expected.to validate_presence_of(:file_type) }
|
||||
|
||||
it { is_expected.to allow_value(:packages).for(:file_type) }
|
||||
end
|
||||
|
||||
describe '#compression_type' do
|
||||
it { is_expected.not_to validate_presence_of(:compression_type) }
|
||||
|
||||
it { is_expected.to allow_value(nil).for(:compression_type) }
|
||||
it { is_expected.to allow_value(:gz).for(:compression_type) }
|
||||
end
|
||||
|
||||
describe '#file' do
|
||||
subject { component_file_with_architecture.file }
|
||||
|
||||
context 'the uploader api' do
|
||||
it { is_expected.to respond_to(:store_dir) }
|
||||
it { is_expected.to respond_to(:cache_dir) }
|
||||
it { is_expected.to respond_to(:work_dir) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#file_store' do
|
||||
it { is_expected.to validate_presence_of(:file_store) }
|
||||
end
|
||||
|
||||
describe '#file_md5' do
|
||||
it { is_expected.to validate_presence_of(:file_md5) }
|
||||
end
|
||||
|
||||
describe '#file_sha256' do
|
||||
it { is_expected.to validate_presence_of(:file_sha256) }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'scopes' do
|
||||
describe '.with_container' do
|
||||
subject { described_class.with_container(container2) }
|
||||
|
||||
it do
|
||||
queries = ActiveRecord::QueryRecorder.new do
|
||||
expect(subject.to_a).to contain_exactly(component_file_other_container)
|
||||
end
|
||||
|
||||
expect(queries.count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.with_codename_or_suite' do
|
||||
subject { described_class.with_codename_or_suite(distribution2.codename) }
|
||||
|
||||
it do
|
||||
queries = ActiveRecord::QueryRecorder.new do
|
||||
expect(subject.to_a).to contain_exactly(component_file_other_container)
|
||||
end
|
||||
|
||||
expect(queries.count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.with_component_name' do
|
||||
subject { described_class.with_component_name(component1_2.name) }
|
||||
|
||||
it do
|
||||
queries = ActiveRecord::QueryRecorder.new do
|
||||
expect(subject.to_a).to contain_exactly(component_file_other_component)
|
||||
end
|
||||
|
||||
expect(queries.count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.with_file_type' do
|
||||
subject { described_class.with_file_type(:source) }
|
||||
|
||||
it do
|
||||
# let_it_be_with_refind triggers a query
|
||||
component_file_with_file_type_source
|
||||
|
||||
queries = ActiveRecord::QueryRecorder.new do
|
||||
expect(subject.to_a).to contain_exactly(component_file_with_file_type_source)
|
||||
end
|
||||
|
||||
expect(queries.count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.with_architecture_name' do
|
||||
subject { described_class.with_architecture_name(architecture1_2.name) }
|
||||
|
||||
it do
|
||||
queries = ActiveRecord::QueryRecorder.new do
|
||||
expect(subject.to_a).to contain_exactly(component_file_other_architecture)
|
||||
end
|
||||
|
||||
expect(queries.count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.with_compression_type' do
|
||||
subject { described_class.with_compression_type(:xz) }
|
||||
|
||||
it do
|
||||
queries = ActiveRecord::QueryRecorder.new do
|
||||
expect(subject.to_a).to contain_exactly(component_file_other_compression_type)
|
||||
end
|
||||
|
||||
expect(queries.count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.with_file_sha256' do
|
||||
subject { described_class.with_file_sha256('other_sha256') }
|
||||
|
||||
it do
|
||||
queries = ActiveRecord::QueryRecorder.new do
|
||||
expect(subject.to_a).to contain_exactly(component_file_other_file_sha256)
|
||||
end
|
||||
|
||||
expect(queries.count).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'callbacks' do
|
||||
let(:component_file) { build("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, size: nil) }
|
||||
|
||||
subject { component_file.save! }
|
||||
|
||||
it 'updates metadata columns' do
|
||||
expect(component_file)
|
||||
.to receive(:update_file_store)
|
||||
.and_call_original
|
||||
|
||||
expect(component_file)
|
||||
.to receive(:update_column)
|
||||
.with(:file_store, ::Packages::PackageFileUploader::Store::LOCAL)
|
||||
.and_call_original
|
||||
|
||||
expect { subject }.to change { component_file.size }.from(nil).to(74)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#relative_path' do
|
||||
context 'with a Packages file_type' do
|
||||
subject { component_file_with_architecture.relative_path }
|
||||
|
||||
it { is_expected.to eq("#{component1_1.name}/binary-#{architecture1_1.name}/Packages") }
|
||||
end
|
||||
|
||||
context 'with a Source file_type' do
|
||||
subject { component_file_with_file_type_source.relative_path }
|
||||
|
||||
it { is_expected.to eq("#{component1_1.name}/source/Source") }
|
||||
end
|
||||
|
||||
context 'with a DI Packages file_type' do
|
||||
subject { component_file_with_file_type_di_packages.relative_path }
|
||||
|
||||
it { is_expected.to eq("#{component1_1.name}/debian-installer/binary-#{architecture1_1.name}/Packages") }
|
||||
end
|
||||
|
||||
context 'with an xz compression_type' do
|
||||
subject { component_file_other_compression_type.relative_path }
|
||||
|
||||
it { is_expected.to eq("#{component1_1.name}/binary-#{architecture1_1.name}/Packages.xz") }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -11,6 +11,7 @@ RSpec.shared_examples 'Debian Distribution Component' do |factory, container, ca
|
|||
|
||||
describe 'relationships' do
|
||||
it { is_expected.to belong_to(:distribution).class_name("Packages::Debian::#{container.capitalize}Distribution").inverse_of(:components) }
|
||||
it { is_expected.to have_many(:files).class_name("Packages::Debian::#{container.capitalize}ComponentFile").inverse_of(:component) }
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'model with Debian distributions' do
|
||||
let(:container_type) { subject.class.name.downcase }
|
||||
let!(:distributions) { create_list("debian_#{container_type}_distribution", 2, :with_file, container: subject) }
|
||||
let!(:components) { create_list("debian_#{container_type}_component", 5, distribution: distributions[0]) }
|
||||
let!(:component_files) { create_list("debian_#{container_type}_component_file", 3, component: components[0]) }
|
||||
|
||||
it 'removes distribution files on removal' do
|
||||
distribution_file_paths = distributions.map do |distribution|
|
||||
[distribution.file.path] +
|
||||
distribution.component_files.map do |component_file|
|
||||
component_file.file.path
|
||||
end
|
||||
end.flatten
|
||||
|
||||
expect { subject.destroy! }
|
||||
.to change {
|
||||
distribution_file_paths.select do |path|
|
||||
File.exist? path
|
||||
end.length
|
||||
}.from(distribution_file_paths.length).to(0)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,51 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.shared_examples 'board lists create request' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:dev_label) do
|
||||
create(:group_label, title: 'Development', color: '#FFAABB', group: group)
|
||||
end
|
||||
|
||||
let(:mutation) { graphql_mutation(mutation_name, input) }
|
||||
let(:mutation_response) { graphql_mutation_response(mutation_name) }
|
||||
|
||||
context 'the user is not allowed to read board lists' do
|
||||
let(:input) { { board_id: board.to_global_id.to_s, backlog: true } }
|
||||
|
||||
it_behaves_like 'a mutation that returns a top-level access error'
|
||||
end
|
||||
|
||||
context 'when user has permissions to admin board lists' do
|
||||
before do
|
||||
group.add_reporter(current_user)
|
||||
end
|
||||
|
||||
describe 'backlog list' do
|
||||
let(:input) { { board_id: board.to_global_id.to_s, backlog: true } }
|
||||
|
||||
it 'creates the list' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response['list'])
|
||||
.to include('position' => nil, 'listType' => 'backlog')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'label list' do
|
||||
let(:input) { { board_id: board.to_global_id.to_s, label_id: dev_label.to_global_id.to_s } }
|
||||
|
||||
it 'creates the list' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response['list'])
|
||||
.to include('position' => 0, 'listType' => 'label', 'label' => include('title' => 'Development'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,81 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'board lists create service' do
|
||||
describe '#execute' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
before_all do
|
||||
parent.add_developer(user)
|
||||
end
|
||||
|
||||
subject(:service) { described_class.new(parent, user, label_id: label.id) }
|
||||
|
||||
context 'when board lists is empty' do
|
||||
it 'creates a new list at beginning of the list' do
|
||||
response = service.execute(board)
|
||||
|
||||
expect(response.success?).to eq(true)
|
||||
expect(response.payload[:list].position).to eq 0
|
||||
end
|
||||
end
|
||||
|
||||
context 'when board lists has the done list' do
|
||||
it 'creates a new list at beginning of the list' do
|
||||
response = service.execute(board)
|
||||
|
||||
expect(response.success?).to eq(true)
|
||||
expect(response.payload[:list].position).to eq 0
|
||||
end
|
||||
end
|
||||
|
||||
context 'when board lists has labels lists' do
|
||||
it 'creates a new list at end of the lists' do
|
||||
create_list(position: 0)
|
||||
create_list(position: 1)
|
||||
|
||||
response = service.execute(board)
|
||||
|
||||
expect(response.success?).to eq(true)
|
||||
expect(response.payload[:list].position).to eq 2
|
||||
end
|
||||
end
|
||||
|
||||
context 'when board lists has label and done lists' do
|
||||
it 'creates a new list at end of the label lists' do
|
||||
list1 = create_list(position: 0)
|
||||
|
||||
list2 = service.execute(board).payload[:list]
|
||||
|
||||
expect(list1.reload.position).to eq 0
|
||||
expect(list2.reload.position).to eq 1
|
||||
end
|
||||
end
|
||||
|
||||
context 'when provided label does not belong to the parent' do
|
||||
it 'returns an error' do
|
||||
label = create(:label, name: 'in-development')
|
||||
service = described_class.new(parent, user, label_id: label.id)
|
||||
|
||||
response = service.execute(board)
|
||||
|
||||
expect(response.success?).to eq(false)
|
||||
expect(response.errors).to include('Label not found')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when backlog param is sent' do
|
||||
it 'creates one and only one backlog list' do
|
||||
service = described_class.new(parent, user, 'backlog' => true)
|
||||
list = service.execute(board).payload[:list]
|
||||
|
||||
expect(list.list_type).to eq('backlog')
|
||||
expect(list.position).to be_nil
|
||||
expect(list).to be_valid
|
||||
|
||||
another_backlog = service.execute(board).payload[:list]
|
||||
|
||||
expect(another_backlog).to eq list
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Packages::Debian::ComponentFileUploader do
|
||||
[:project, :group].each do |container_type|
|
||||
context "Packages::Debian::#{container_type.capitalize}ComponentFile" do
|
||||
let(:factory) { "debian_#{container_type}_component_file" }
|
||||
let(:component_file) { create(factory) } # rubocop:disable Rails/SaveBang
|
||||
let(:uploader) { described_class.new(component_file, :file) }
|
||||
let(:path) { Gitlab.config.packages.storage_path }
|
||||
|
||||
subject { uploader }
|
||||
|
||||
it_behaves_like "builds correct paths",
|
||||
store_dir: %r[^\h{2}/\h{2}/\h{64}/debian_#{container_type}_component_file/\d+$],
|
||||
cache_dir: %r[/packages/tmp/cache$],
|
||||
work_dir: %r[/packages/tmp/work$]
|
||||
|
||||
context 'object store is remote' do
|
||||
before do
|
||||
stub_package_file_object_storage
|
||||
end
|
||||
|
||||
include_context 'with storage', described_class::Store::REMOTE
|
||||
|
||||
it_behaves_like "builds correct paths",
|
||||
store_dir: %r[^\h{2}/\h{2}/\h{64}/debian_#{container_type}_component_file/\d+$],
|
||||
cache_dir: %r[/packages/tmp/cache$],
|
||||
work_dir: %r[/packages/tmp/work$]
|
||||
end
|
||||
|
||||
describe 'remote file' do
|
||||
let(:component_file) { create(factory, :object_storage) }
|
||||
|
||||
context 'with object storage enabled' do
|
||||
before do
|
||||
stub_package_file_object_storage
|
||||
end
|
||||
|
||||
it 'can store file remotely' do
|
||||
allow(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async)
|
||||
|
||||
component_file
|
||||
|
||||
expect(component_file.file_store).to eq(described_class::Store::REMOTE)
|
||||
expect(component_file.file.path).not_to be_blank
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue