Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
6a4cea0a8f
commit
d58a3afef8
|
|
@ -51,8 +51,6 @@ text_color_for_bg:
|
|||
file: app/helpers/labels_helper.rb
|
||||
manage_labels_title:
|
||||
file: app/helpers/labels_helper.rb
|
||||
show_projects?:
|
||||
file: app/helpers/projects_helper.rb
|
||||
request_access_group_members_path:
|
||||
file: app/helpers/routing/groups/members_helper.rb
|
||||
approve_access_request_group_member_path:
|
||||
|
|
|
|||
|
|
@ -73,8 +73,12 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
composedCreateButtonLabel() {
|
||||
return sprintf(__('Create wildcard: %{searchTerm}'), { searchTerm: this.searchTerm });
|
||||
composedCreateScopeButtonLabel() {
|
||||
const label = this.searchTerm?.includes('*')
|
||||
? __('Create wildcard: %{searchTerm}')
|
||||
: s__('CiVariable|Create environment scope: %{searchTerm}');
|
||||
|
||||
return sprintf(label, { searchTerm: this.searchTerm });
|
||||
},
|
||||
environmentScopeLabel() {
|
||||
return convertEnvironmentScope(this.selectedEnvironmentScope);
|
||||
|
|
@ -108,15 +112,12 @@ export default {
|
|||
text: environment,
|
||||
}));
|
||||
},
|
||||
shouldRenderCreateButton() {
|
||||
if (!this.canCreateWildcard) {
|
||||
shouldRenderCreateScopeButton() {
|
||||
if (!this.canCreateWildcard || !this.searchTerm) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
this.searchTerm?.includes('*') &&
|
||||
![...this.environments, this.customEnvScope].includes(this.searchTerm)
|
||||
);
|
||||
return ![...this.environments, this.customEnvScope].includes(this.searchTerm);
|
||||
},
|
||||
shouldRenderDivider() {
|
||||
return !this.areEnvironmentsLoading;
|
||||
|
|
@ -174,14 +175,14 @@ export default {
|
|||
<gl-dropdown-item class="gl-list-none" disabled data-testid="search-query-note">
|
||||
{{ $options.i18n.searchQueryNote }}
|
||||
</gl-dropdown-item>
|
||||
<div v-if="shouldRenderCreateButton">
|
||||
<div v-if="shouldRenderCreateScopeButton">
|
||||
<!-- TODO: Rethink create wildcard button. https://gitlab.com/gitlab-org/gitlab/-/issues/396928 -->
|
||||
<gl-dropdown-item
|
||||
class="gl-list-none"
|
||||
data-testid="create-wildcard-button"
|
||||
data-testid="create-scope-button"
|
||||
@click="createEnvironmentScope"
|
||||
>
|
||||
{{ composedCreateButtonLabel }}
|
||||
{{ composedCreateScopeButtonLabel }}
|
||||
</gl-dropdown-item>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -7,9 +7,8 @@ export default {
|
|||
},
|
||||
props: {
|
||||
staticBreadcrumbs: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({ items: [] }),
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -37,7 +36,7 @@ export default {
|
|||
if (!this.isLoaded) {
|
||||
return [];
|
||||
}
|
||||
const breadCrumbs = [...this.staticBreadcrumbs.items, this.rootRoute];
|
||||
const breadCrumbs = [...this.staticBreadcrumbs, this.rootRoute];
|
||||
|
||||
if (!this.isRootRoute) {
|
||||
breadCrumbs.push(this.logsRoute);
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ export const initPage = async () => {
|
|||
},
|
||||
});
|
||||
|
||||
injectVueAppBreadcrumbs(router, EnvironmentBreadcrumbs, null, {}, { singleNavOptIn: true });
|
||||
injectVueAppBreadcrumbs(router, EnvironmentBreadcrumbs);
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
|
|
|
|||
|
|
@ -7,11 +7,6 @@ export const injectVueAppBreadcrumbs = (
|
|||
BreadcrumbsComponent,
|
||||
apolloProvider = null,
|
||||
provide = {},
|
||||
// this is intended to be a temporary option. Once all uses of
|
||||
// injectVueAppBreadcrumbs use it, the option should be removed and its
|
||||
// behavior should be the default.
|
||||
// Cf. https://gitlab.com/gitlab-org/gitlab/-/merge_requests/186906
|
||||
{ singleNavOptIn = false } = {},
|
||||
// eslint-disable-next-line max-params
|
||||
) => {
|
||||
const injectBreadcrumbEl = document.querySelector('#js-injected-page-breadcrumbs');
|
||||
|
|
@ -20,22 +15,9 @@ export const injectVueAppBreadcrumbs = (
|
|||
return false;
|
||||
}
|
||||
|
||||
if (singleNavOptIn) {
|
||||
destroySuperSidebarBreadcrumbs();
|
||||
// After singleNavOptIn is turned on for all Vue apps, we can stop
|
||||
// changing the content of staticBreadcrumbs and instead pass a mutated
|
||||
// copy of it to the CustomBreadcrumbsRoot component. For now, we need
|
||||
// to conditionally mutate the staticBreadcrumbs object so that the last
|
||||
// breadcrumb is hidden for Vue apps that have not opted in to the
|
||||
// singleNavOptIn.
|
||||
// Cf. https://gitlab.com/gitlab-org/gitlab/-/merge_requests/186906
|
||||
staticBreadcrumbs.items = staticBreadcrumbs.items.slice(0, -1);
|
||||
} else {
|
||||
// Hide the last of the static breadcrumbs by nulling its values.
|
||||
// This way, the separator "/" stays visible and also the new "last" static item isn't displayed in bold font.
|
||||
staticBreadcrumbs.items[staticBreadcrumbs.items.length - 1].text = '';
|
||||
staticBreadcrumbs.items[staticBreadcrumbs.items.length - 1].href = '';
|
||||
}
|
||||
destroySuperSidebarBreadcrumbs();
|
||||
|
||||
const { items } = staticBreadcrumbs;
|
||||
|
||||
return new Vue({
|
||||
el: injectBreadcrumbEl,
|
||||
|
|
@ -47,7 +29,9 @@ export const injectVueAppBreadcrumbs = (
|
|||
return createElement(BreadcrumbsComponent, {
|
||||
class: injectBreadcrumbEl.className,
|
||||
props: {
|
||||
staticBreadcrumbs,
|
||||
// The last item from the static breadcrumb set is replaced by the
|
||||
// root of the vue app, so the last item should be removed
|
||||
staticBreadcrumbs: items.slice(0, -1),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -92,14 +92,7 @@ export default () => {
|
|||
});
|
||||
|
||||
return {
|
||||
attachBreadcrumb: () =>
|
||||
injectVueAppBreadcrumbs(
|
||||
router,
|
||||
RegistryBreadcrumb,
|
||||
apolloProvider,
|
||||
{},
|
||||
{ singleNavOptIn: true },
|
||||
),
|
||||
attachBreadcrumb: () => injectVueAppBreadcrumbs(router, RegistryBreadcrumb, apolloProvider),
|
||||
attachMainComponent,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,9 +11,8 @@ export default {
|
|||
},
|
||||
props: {
|
||||
staticBreadcrumbs: {
|
||||
type: Object,
|
||||
default: () => ({ items: [] }),
|
||||
required: false,
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -42,9 +41,7 @@ export default {
|
|||
});
|
||||
}
|
||||
|
||||
const staticCrumbs = this.staticBreadcrumbs.items;
|
||||
|
||||
return [...staticCrumbs, ...routeInfoList];
|
||||
return [...this.staticBreadcrumbs, ...routeInfoList];
|
||||
},
|
||||
isLoaded() {
|
||||
return this.isRootRoute || last(this.currentRoute).text;
|
||||
|
|
@ -59,9 +56,8 @@ export default {
|
|||
if (!this.isRootRoute) {
|
||||
crumbs = crumbs.concat(this.currentRoute);
|
||||
}
|
||||
const staticCrumbs = this.staticBreadcrumbs.items;
|
||||
|
||||
return [...staticCrumbs, ...crumbs];
|
||||
return [...this.staticBreadcrumbs, ...crumbs];
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -77,17 +77,7 @@ export default (id) => {
|
|||
};
|
||||
|
||||
return {
|
||||
attachBreadcrumb: () =>
|
||||
injectVueAppBreadcrumbs(
|
||||
router,
|
||||
RegistryBreadcrumb,
|
||||
null,
|
||||
{},
|
||||
{
|
||||
// cf. https://gitlab.com/gitlab-org/gitlab/-/merge_requests/186906
|
||||
singleNavOptIn: true,
|
||||
},
|
||||
),
|
||||
attachBreadcrumb: () => injectVueAppBreadcrumbs(router, RegistryBreadcrumb),
|
||||
attachMainComponent,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -51,14 +51,7 @@ export default () => {
|
|||
});
|
||||
|
||||
return {
|
||||
attachBreadcrumb: () =>
|
||||
injectVueAppBreadcrumbs(
|
||||
router,
|
||||
RegistryBreadcrumb,
|
||||
apolloProvider,
|
||||
{},
|
||||
{ singleNavOptIn: true },
|
||||
),
|
||||
attachBreadcrumb: () => injectVueAppBreadcrumbs(router, RegistryBreadcrumb, apolloProvider),
|
||||
attachMainComponent,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,9 +11,8 @@ export default {
|
|||
},
|
||||
props: {
|
||||
staticBreadcrumbs: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({ items: [] }),
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -34,7 +33,7 @@ export default {
|
|||
},
|
||||
allCrumbs() {
|
||||
const crumbs = [
|
||||
...this.staticBreadcrumbs.items,
|
||||
...this.staticBreadcrumbs,
|
||||
{
|
||||
text: this.rootRoute.meta.nameGenerator(),
|
||||
to: this.rootRoute.path,
|
||||
|
|
|
|||
|
|
@ -142,8 +142,10 @@ export default {
|
|||
groups = this.mapAllowlistNodes(allowlist?.groupsAllowlist);
|
||||
projects = this.mapAllowlistNodes(allowlist?.projectsAllowlist);
|
||||
// Add a dummy entry for the current project. The new ciJobTokenScopeAllowlist endpoint doesn't have an entry
|
||||
// for the current project like the old ciJobTokenScope endpoint did, so we have to add it in manually.
|
||||
projects.push({ ...project, defaultPermissions: true, jobTokenPolicies: [] });
|
||||
// for the current project like the old ciJobTokenScope endpoint did, so we have to add it in manually, if it
|
||||
// doesn't exist yet.
|
||||
if (!projects.some(({ id }) => id === project.id))
|
||||
projects.push({ ...project, defaultPermissions: true, jobTokenPolicies: [] });
|
||||
} else {
|
||||
projects = project?.ciJobTokenScope?.inboundAllowlist?.nodes ?? [];
|
||||
groups = project?.ciJobTokenScope?.groupsAllowlist?.nodes ?? [];
|
||||
|
|
@ -211,7 +213,7 @@ export default {
|
|||
},
|
||||
allowlist() {
|
||||
const { groups, projects } = this.groupsAndProjectsWithAccess;
|
||||
return [...groups, ...projects];
|
||||
return [...groups, ...projects].sort((a, b) => a.fullPath.localeCompare(b.fullPath));
|
||||
},
|
||||
disclosureDropdownOptions() {
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import {
|
||||
GlBadge,
|
||||
GlButton,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
|
|
@ -15,6 +16,7 @@ import { JOB_TOKEN_POLICIES } from '../constants';
|
|||
|
||||
export default {
|
||||
components: {
|
||||
GlBadge,
|
||||
GlButton,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
|
|
@ -89,9 +91,6 @@ export default {
|
|||
isCurrentProject(item) {
|
||||
return item.fullPath === this.fullPath;
|
||||
},
|
||||
shouldShowEditButton(item) {
|
||||
return this.showPolicies && !this.isCurrentProject(item);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -136,6 +135,9 @@ export default {
|
|||
class="gl-ml-3 gl-shrink-0"
|
||||
data-testid="autopopulated-icon"
|
||||
/>
|
||||
<gl-badge v-if="isCurrentProject(item)" class="gl-ml-3">{{
|
||||
__('Current project')
|
||||
}}</gl-badge>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -159,7 +161,7 @@ export default {
|
|||
<template #cell(actions)="{ item }">
|
||||
<div class="gl-flex gl-gap-2">
|
||||
<gl-button
|
||||
v-if="shouldShowEditButton(item)"
|
||||
v-if="showPolicies"
|
||||
icon="pencil"
|
||||
:aria-label="__('Edit')"
|
||||
data-testid="token-access-table-edit-button"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import CustomizableDashboard from './customizable_dashboard.vue';
|
|||
|
||||
export default {
|
||||
component: CustomizableDashboard,
|
||||
title: 'vue_shared/components/customizable_dashboard',
|
||||
title: 'vue_shared/components/customizable_dashboard/customizable_dashboard',
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import DashboardLayout from './dashboard_layout.vue';
|
|||
|
||||
export default {
|
||||
component: DashboardLayout,
|
||||
title: 'vue_shared/components/dashboard_layout',
|
||||
title: 'vue_shared/components/customizable_dashboard/dashboard_layout',
|
||||
};
|
||||
|
||||
const dashboardConfig = {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,12 @@
|
|||
import GridstackWrapper from './gridstack_wrapper.vue';
|
||||
import { dashboardConfigValidator } from './utils';
|
||||
|
||||
/**
|
||||
* The `DashboardLayout` component provides an easy way to render dashboards using a configuration, aligning with our [Pajamas guidelines](https://design.gitlab.com/patterns/dashboards).
|
||||
*
|
||||
* Please refer to the [documentation](https://docs.gitlab.com/development/fe_guide/dashboard_layout_framework.md) for more information.
|
||||
*
|
||||
*/
|
||||
export default {
|
||||
name: 'DashboardLayout',
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import PanelsBase from './panels_base.vue';
|
|||
|
||||
export default {
|
||||
component: PanelsBase,
|
||||
title: 'vue_shared/components/panels_base',
|
||||
title: 'vue_shared/components/customizable_dashboard/panels_base',
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
|
|
|
|||
|
|
@ -5,6 +5,14 @@ import { isObject } from 'lodash';
|
|||
import { VARIANT_DANGER, VARIANT_WARNING, VARIANT_INFO } from '~/alert';
|
||||
import { PANEL_POPOVER_DELAY } from './constants';
|
||||
|
||||
/**
|
||||
* This component provides a standardized layout and functionality for dashboard panels.
|
||||
*
|
||||
* It extends [`GlDashboardPanel`](https://design.gitlab.com/storybook/?path=/story/dashboards-dashboards-panel--default) by adding support for various states including loading, error states with different alert variants,
|
||||
* and editing mode with configurable actions.
|
||||
*
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'PanelsBase',
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -27,9 +27,8 @@ export default {
|
|||
},
|
||||
props: {
|
||||
staticBreadcrumbs: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({ items: [] }),
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -69,9 +68,7 @@ export default {
|
|||
indexCrumb.href = this.listPath;
|
||||
}
|
||||
|
||||
const staticCrumbs = this.staticBreadcrumbs.items;
|
||||
|
||||
const crumbs = [...staticCrumbs, indexCrumb];
|
||||
const crumbs = [...this.staticBreadcrumbs, indexCrumb];
|
||||
|
||||
if (this.$route.name === ROUTES.new) {
|
||||
crumbs.push({
|
||||
|
|
|
|||
|
|
@ -77,10 +77,7 @@ export const initWorkItemsRoot = ({ workspaceType, withTabs } = {}) => {
|
|||
breadcrumbParams.listPath = issuesListPath;
|
||||
}
|
||||
|
||||
injectVueAppBreadcrumbs(router, WorkItemBreadcrumb, apolloProvider, breadcrumbParams, {
|
||||
// Cf. https://gitlab.com/gitlab-org/gitlab/-/merge_requests/186906
|
||||
singleNavOptIn: true,
|
||||
});
|
||||
injectVueAppBreadcrumbs(router, WorkItemBreadcrumb, apolloProvider, breadcrumbParams);
|
||||
|
||||
apolloProvider.clients.defaultClient.cache.writeQuery({
|
||||
query: activeDiscussionQuery,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ module Groups
|
|||
end
|
||||
|
||||
def show
|
||||
not_found unless namespace_work_items_enabled?
|
||||
not_found unless !!group&.supports_group_work_items?
|
||||
|
||||
@work_item = ::WorkItems::WorkItemsFinder.new(current_user, group_id: group.id)
|
||||
.execute.with_work_item_type.find_by_iid(show_params[:iid])
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module TimeFrameFilter
|
||||
def by_timeframe(items)
|
||||
def by_timeframe(items, with_namespace_cte: false)
|
||||
return items unless params[:start_date] && params[:end_date]
|
||||
|
||||
start_date = params[:start_date].to_date
|
||||
end_date = params[:end_date].to_date
|
||||
|
||||
items.within_timeframe(start_date, end_date)
|
||||
if with_namespace_cte
|
||||
items.within_timeframe(start_date, end_date, with_namespace_cte: true)
|
||||
else
|
||||
items.within_timeframe(start_date, end_date)
|
||||
end
|
||||
rescue ArgumentError
|
||||
items
|
||||
end
|
||||
|
|
|
|||
|
|
@ -311,7 +311,7 @@ class IssuableFinder
|
|||
end
|
||||
|
||||
def by_parent(items)
|
||||
return items.none unless params.projects
|
||||
return items.none unless accessible_projects
|
||||
|
||||
if use_namespace_filtering?
|
||||
filter_by_namespace(items)
|
||||
|
|
@ -326,9 +326,9 @@ class IssuableFinder
|
|||
# When finding issues for multiple projects it's more efficient
|
||||
# to use a JOIN instead of running a sub-query
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/commit/8591cc02be6b12ed60f763a5e0147f2cbbca99e1
|
||||
items.join_project_through_namespace.merge(params.projects.reorder(nil))
|
||||
items.join_project_through_namespace.merge(accessible_projects.reorder(nil))
|
||||
else
|
||||
items.in_namespaces(params.projects.map(&:project_namespace_id)).references_project
|
||||
items.in_namespaces(accessible_projects.map(&:project_namespace_id)).references_project
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -337,13 +337,17 @@ class IssuableFinder
|
|||
# When finding issues for multiple projects it's more efficient
|
||||
# to use a JOIN instead of running a sub-query
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/commit/8591cc02be6b12ed60f763a5e0147f2cbbca99e1
|
||||
items.merge(params.projects.reorder(nil)).join_project
|
||||
items.merge(accessible_projects.reorder(nil)).join_project
|
||||
else
|
||||
items.of_projects(params.projects).references_project
|
||||
items.of_projects(accessible_projects).references_project
|
||||
end
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def accessible_projects
|
||||
params.projects
|
||||
end
|
||||
|
||||
def use_namespace_filtering?
|
||||
::Feature.enabled?(:use_namespace_id_for_issue_and_work_item_finders, current_user, type: :wip) &&
|
||||
[::Issue, ::WorkItem].include?(klass)
|
||||
|
|
@ -351,7 +355,7 @@ class IssuableFinder
|
|||
|
||||
def use_join_strategy_for_project?
|
||||
strong_memoize(:use_join_strategy_for_project) do
|
||||
params.projects.is_a?(ActiveRecord::Relation)
|
||||
accessible_projects.is_a?(ActiveRecord::Relation)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -456,7 +460,7 @@ class IssuableFinder
|
|||
elsif params.filter_by_any_milestone?
|
||||
items.any_milestone
|
||||
elsif params.filter_by_upcoming_milestone?
|
||||
upcoming_ids = Milestone.upcoming_ids(params.projects, params.related_groups,
|
||||
upcoming_ids = Milestone.upcoming_ids(accessible_projects, params.related_groups,
|
||||
legacy_filtering_logic: use_legacy_milestone_filtering?)
|
||||
items.left_joins_milestones.where(milestone_id: upcoming_ids)
|
||||
elsif params.filter_by_started_milestone?
|
||||
|
|
|
|||
|
|
@ -28,10 +28,9 @@ module WorkItems
|
|||
def filter_items(items)
|
||||
items = super(items)
|
||||
|
||||
# We require namespace_level_work_items to be true here, since we need the namespace_ids CTE provided by the
|
||||
# by_parent method for performance reasons see: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181904
|
||||
items = by_timeframe(items) if include_namespace_level_work_items?
|
||||
items = by_widgets(items)
|
||||
items = by_timeframe(items, with_namespace_cte: with_namespace_cte)
|
||||
|
||||
by_work_item_parent_ids(items)
|
||||
end
|
||||
|
||||
|
|
@ -61,95 +60,24 @@ module WorkItems
|
|||
items.with_work_item_parent_ids(work_item_parent_ids)
|
||||
end
|
||||
|
||||
override :use_full_text_search?
|
||||
def use_full_text_search?
|
||||
return false if include_namespace_level_work_items?
|
||||
def accessible_projects
|
||||
return if exclude_projects?
|
||||
return if params.group && !include_descendants? # group-level work items are not supported in CE
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
override :by_confidential
|
||||
def by_confidential(items)
|
||||
return super unless include_namespace_level_work_items?
|
||||
|
||||
Issues::ConfidentialityFilter.new(
|
||||
current_user: current_user,
|
||||
params: original_params,
|
||||
parent: root_ancestor_group,
|
||||
assignee_filter: assignee_filter,
|
||||
related_groups: related_groups
|
||||
).filter(items)
|
||||
end
|
||||
|
||||
override :by_parent
|
||||
def by_parent(items)
|
||||
return super unless include_namespace_level_work_items?
|
||||
|
||||
relations = [group_namespaces, project_namespaces].compact
|
||||
|
||||
return items.none if relations.empty?
|
||||
|
||||
namespaces = if relations.one?
|
||||
relations.first
|
||||
else
|
||||
Namespace.from_union(relations, remove_duplicates: false)
|
||||
end
|
||||
|
||||
items.in_namespaces_with_cte(namespaces)
|
||||
end
|
||||
|
||||
def group_namespaces
|
||||
return if params[:project_id] || params[:projects]
|
||||
|
||||
related_groups_with_access.select(:id)
|
||||
end
|
||||
|
||||
def related_groups_with_access
|
||||
# If the user is not signed in, we just return public groups
|
||||
return related_groups.public_to_user unless current_user
|
||||
|
||||
# If the user is an admin or a member of the root group, they will have read access to all
|
||||
# work items in the subgroups so we can skip the expensive permissions check
|
||||
if Ability.allowed?(current_user, :read_all_resources) || root_ancestor_group.member?(current_user)
|
||||
return related_groups
|
||||
end
|
||||
|
||||
Group.id_in(
|
||||
Group.groups_user_can(related_groups, current_user, :read_work_item, same_root: true)
|
||||
)
|
||||
end
|
||||
|
||||
def related_groups
|
||||
if include_ancestors? && include_descendants?
|
||||
params.group.self_and_hierarchy
|
||||
elsif include_ancestors?
|
||||
params.group.self_and_ancestors
|
||||
elsif include_descendants?
|
||||
params.group.self_and_descendants
|
||||
else
|
||||
Group.id_in(params.group.id)
|
||||
end
|
||||
end
|
||||
strong_memoize_attr :related_groups
|
||||
|
||||
def root_ancestor_group
|
||||
include_ancestors? ? params.group.root_ancestor : params.group
|
||||
end
|
||||
|
||||
def project_namespaces
|
||||
return if !include_descendants? || exclude_projects?
|
||||
|
||||
projects = Project.in_namespace(params.group.self_and_descendant_ids)
|
||||
projects = projects.id_in(params[:projects]) if params[:projects]
|
||||
|
||||
projects
|
||||
projects = Project
|
||||
.public_or_visible_to_user(current_user, ProjectFeature.required_minimum_access_level(klass.base_class))
|
||||
.with_feature_available_for_user(klass.base_class, current_user)
|
||||
.select(:project_namespace_id)
|
||||
end
|
||||
|
||||
def include_namespace_level_work_items?
|
||||
params.group? && params.group.namespace_work_items_enabled?
|
||||
projects = projects.in_namespace(params.group.self_and_descendant_ids) if params.group && include_descendants?
|
||||
projects = projects.id_in(params[:projects]) if params[:projects]
|
||||
projects = projects.id_in(params[:project_id]) if params[:project_id]
|
||||
|
||||
projects
|
||||
end
|
||||
strong_memoize_attr :accessible_projects
|
||||
|
||||
def with_namespace_cte
|
||||
false
|
||||
end
|
||||
|
||||
def include_descendants?
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ module Resolvers
|
|||
def ready?(**args)
|
||||
validate_timeframe_limit!(args[:timeframe]) if args[:timeframe]
|
||||
|
||||
super && resource_parent.namespace_work_items_enabled?
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -19,8 +19,13 @@ module Types
|
|||
field :access_level, ::Types::Ci::RunnerAccessLevelEnum, null: false,
|
||||
description: 'Access level of the runner.'
|
||||
field :active, GraphQL::Types::Boolean, null: false,
|
||||
description: 'Indicates the runner is allowed to receive jobs.',
|
||||
deprecated: { reason: 'Use paused', milestone: '14.8' }
|
||||
description: "Indicates the runner is allowed to receive jobs.",
|
||||
deprecated: {
|
||||
reason: 'Use `paused`. ' \
|
||||
"This field is the inverse of `paused` and has no relationship to the runner's job execution status. " \
|
||||
"For more details, see `jobExecutionStatus`",
|
||||
milestone: '14.8'
|
||||
}
|
||||
field :admin_url, GraphQL::Types::String, null: true,
|
||||
description: 'Admin URL of the runner. Only available for administrators.'
|
||||
field :contacted_at, Types::TimeType, null: true,
|
||||
|
|
|
|||
|
|
@ -353,16 +353,6 @@ module ProjectsHelper
|
|||
end
|
||||
end
|
||||
|
||||
def show_projects?(projects, params)
|
||||
!!(
|
||||
params[:personal] ||
|
||||
params[:name] ||
|
||||
params[:language] ||
|
||||
params[:archived] == 'only' ||
|
||||
any_projects?(projects)
|
||||
)
|
||||
end
|
||||
|
||||
def push_to_create_project_command(user = current_user)
|
||||
repository_url =
|
||||
if Gitlab::CurrentSettings.current_application_settings.enabled_git_access_protocol == 'http'
|
||||
|
|
|
|||
|
|
@ -39,11 +39,7 @@ module Ci
|
|||
}
|
||||
)
|
||||
|
||||
# Skip self-referential accesses as they are always allowed and don't need
|
||||
# to be logged neither added to the allowlist.
|
||||
return if label == 'same-project'
|
||||
|
||||
# We are tracking an attempt of cross-project utilization but we
|
||||
# We are tracking ci job token access to project resources, but we
|
||||
# are not yet persisting this log until a request successfully
|
||||
# completes. We will do that in a middleware. This is because the policy
|
||||
# rule about job token scope may be satisfied but a subsequent rule in
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ module Ci
|
|||
|
||||
validates :source_project, presence: true
|
||||
validates :target_project, presence: true
|
||||
validate :not_self_referential_link
|
||||
validate :source_project_under_link_limit, on: :create
|
||||
|
||||
# When outbound the target project is allowed to be accessed by the source job token.
|
||||
|
|
@ -43,14 +42,6 @@ module Ci
|
|||
|
||||
private
|
||||
|
||||
def not_self_referential_link
|
||||
return unless source_project && target_project
|
||||
|
||||
if source_project == target_project
|
||||
self.errors.add(:target_project, _("can't be the same as the source project"))
|
||||
end
|
||||
end
|
||||
|
||||
def source_project_under_link_limit
|
||||
return unless source_project
|
||||
|
||||
|
|
|
|||
|
|
@ -25,8 +25,6 @@ module Ci
|
|||
end
|
||||
|
||||
def accessible?(accessed_project)
|
||||
return true if self_referential?(accessed_project)
|
||||
|
||||
if inbound_accessible?(accessed_project)
|
||||
# We capture only successful inbound authorizations
|
||||
Ci::JobToken::Authorization.capture(origin_project: current_project, accessed_project: accessed_project)
|
||||
|
|
@ -40,14 +38,12 @@ module Ci
|
|||
end
|
||||
|
||||
def policies_allowed?(accessed_project, policies)
|
||||
return true if self_referential?(accessed_project)
|
||||
|
||||
# We capture policies even if job token policies or allowlists are disabled, or the project is not allowlisted
|
||||
Ci::JobToken::Authorization.capture_job_token_policies(policies) if policies.present?
|
||||
|
||||
return true unless accessed_project.job_token_policies_enabled?
|
||||
return true unless accessed_project.ci_inbound_job_token_scope_enabled? # allowlists are disabled
|
||||
return false unless inbound_accessible?(accessed_project) # the current project is not allowlisted
|
||||
return true unless accessed_project.ci_inbound_job_token_scope_enabled?
|
||||
return false unless accessible?(accessed_project)
|
||||
|
||||
policies_allowed_for_accessed_project?(accessed_project, policies)
|
||||
end
|
||||
|
|
@ -90,7 +86,8 @@ module Ci
|
|||
if accessed_project.ci_inbound_job_token_scope_enabled?
|
||||
::Gitlab::Ci::Pipeline::Metrics.job_token_inbound_access_counter.increment(legacy: false)
|
||||
|
||||
inbound_linked_as_accessible?(accessed_project) ||
|
||||
self_referential?(accessed_project) ||
|
||||
inbound_linked_as_accessible?(accessed_project) ||
|
||||
group_linked_as_accessible?(accessed_project)
|
||||
else
|
||||
::Gitlab::Ci::Pipeline::Metrics.job_token_inbound_access_counter.increment(legacy: true)
|
||||
|
|
@ -102,6 +99,7 @@ module Ci
|
|||
|
||||
def policies_allowed_for_accessed_project?(accessed_project, policies)
|
||||
scope = nearest_scope(accessed_project)
|
||||
return true if scope.nil? && self_referential?(accessed_project)
|
||||
return true if scope.default_permissions?
|
||||
return false if policies.empty?
|
||||
|
||||
|
|
|
|||
|
|
@ -1124,6 +1124,11 @@ class Group < Namespace
|
|||
::Feature.enabled?(:work_item_epics_list, root_ancestor, type: :wip)
|
||||
end
|
||||
|
||||
# overriden in EE
|
||||
def supports_group_work_items?
|
||||
false
|
||||
end
|
||||
|
||||
def create_group_level_work_items_feature_flag_enabled?
|
||||
::Feature.enabled?(:create_group_level_work_items, self, type: :wip)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -35,17 +35,19 @@ class WorkItem < Issue
|
|||
)
|
||||
}
|
||||
|
||||
scope :within_timeframe, ->(start_date, due_date) do
|
||||
scope :within_timeframe, ->(start_date, due_date, with_namespace_cte: false) do
|
||||
date_filtered_issue_ids = ::WorkItems::DatesSource
|
||||
.select('issue_id')
|
||||
.where('start_date IS NOT NULL OR due_date IS NOT NULL')
|
||||
# Require the namespace_ids CTE from by_parent to be present when filtering by timeframe
|
||||
# for performance reasons.
|
||||
# see: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181904
|
||||
.where('namespace_id IN (SELECT id FROM namespace_ids)')
|
||||
.where('start_date IS NULL OR start_date <= ?', due_date)
|
||||
.where('due_date IS NULL OR due_date >= ?', start_date)
|
||||
|
||||
# The namespace_ids CTE from by_parent by timeframe helps with performance when querying across multiple namespaces.
|
||||
# see: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181904
|
||||
if with_namespace_cte
|
||||
date_filtered_issue_ids = date_filtered_issue_ids.where('namespace_id IN (SELECT id FROM namespace_ids)')
|
||||
end
|
||||
|
||||
joins("INNER JOIN (#{date_filtered_issue_ids.to_sql}) AS filtered_dates ON issues.id = filtered_dates.issue_id")
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ module Ci
|
|||
validate_target_exists!(target)
|
||||
validate_permissions!(target)
|
||||
|
||||
link = find_link_using_source_and_target(target)
|
||||
link = link_for(target)
|
||||
|
||||
return error_link_not_found unless link
|
||||
|
||||
|
|
@ -29,6 +29,14 @@ module Ci
|
|||
|
||||
private
|
||||
|
||||
def link_for(target)
|
||||
link = find_link_using_source_and_target(target)
|
||||
return link if link
|
||||
return unless target == project
|
||||
|
||||
build_scope_for_self_target
|
||||
end
|
||||
|
||||
def error_target_not_found
|
||||
ServiceResponse.error(message: 'The target does not exist', reason: :not_found)
|
||||
end
|
||||
|
|
@ -68,6 +76,15 @@ module Ci
|
|||
::Ci::JobToken::ProjectScopeLink.with_access_direction(:inbound).for_source_and_target(project, target)
|
||||
end
|
||||
end
|
||||
|
||||
def build_scope_for_self_target
|
||||
::Ci::JobToken::ProjectScopeLink.new(
|
||||
source_project: project,
|
||||
target_project: project,
|
||||
direction: :inbound,
|
||||
added_by: current_user
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -30,12 +30,6 @@ module ContainerRegistry
|
|||
end
|
||||
|
||||
def protected_for_delete?(project:, current_user:)
|
||||
if Feature.enabled?(:container_registry_immutable_tags, project) &&
|
||||
project.container_registry_protection_tag_rules.immutable.present? &&
|
||||
project.has_container_registry_tags?
|
||||
return true
|
||||
end
|
||||
|
||||
return false if current_user.can_admin_all_resources?
|
||||
|
||||
return false unless project.has_container_registry_protected_tag_rules?(
|
||||
|
|
@ -50,3 +44,5 @@ module ContainerRegistry
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
ContainerRegistry::Protection::Concerns::TagRule.prepend_mod
|
||||
|
|
|
|||
|
|
@ -4582,7 +4582,8 @@
|
|||
:resource_boundary: :memory
|
||||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
:tags:
|
||||
- :import_shared_storage
|
||||
:queue_namespace:
|
||||
- :name: projects_import_export_relation_export
|
||||
:worker_name: Projects::ImportExport::RelationExportWorker
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ module Projects
|
|||
feature_category :importers
|
||||
worker_resource_boundary :memory
|
||||
urgency :low
|
||||
tags :import_shared_storage
|
||||
loggable_arguments 1, 2
|
||||
sidekiq_options retries: 3, dead: false, status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION
|
||||
|
||||
|
|
|
|||
|
|
@ -8,14 +8,6 @@ description: Tracks the assignment and unassignment events for merge requests
|
|||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117379
|
||||
milestone: '15.11'
|
||||
gitlab_schema: gitlab_main_cell
|
||||
desired_sharding_key:
|
||||
project_id:
|
||||
references: projects
|
||||
backfill_via:
|
||||
parent:
|
||||
foreign_key: merge_request_id
|
||||
table: merge_requests
|
||||
sharding_key: target_project_id
|
||||
belongs_to: merge_request
|
||||
desired_sharding_key_migration_job_name: BackfillMergeRequestAssignmentEventsProjectId
|
||||
sharding_key:
|
||||
project_id: projects
|
||||
table_size: small
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddMergeRequestAssignmentEventsProjectIdNotNull < Gitlab::Database::Migration[2.3]
|
||||
milestone '18.1'
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_not_null_constraint :merge_request_assignment_events, :project_id
|
||||
end
|
||||
|
||||
def down
|
||||
remove_not_null_constraint :merge_request_assignment_events, :project_id
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
cd74cc1d7004ba2a75c24c40b01d46fa369f6cb488fd430fbadece1f9caa1854
|
||||
|
|
@ -16944,7 +16944,8 @@ CREATE TABLE merge_request_assignment_events (
|
|||
merge_request_id bigint NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
action smallint DEFAULT 1 NOT NULL,
|
||||
project_id bigint
|
||||
project_id bigint,
|
||||
CONSTRAINT check_31395542a4 CHECK ((project_id IS NOT NULL))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE merge_request_assignment_events_id_seq
|
||||
|
|
|
|||
|
|
@ -23487,7 +23487,7 @@ CI/CD variables for a project.
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="cirunneraccesslevel"></a>`accessLevel` | [`CiRunnerAccessLevel!`](#cirunneraccesslevel) | Access level of the runner. |
|
||||
| <a id="cirunneractive"></a>`active` {{< icon name="warning-solid" >}} | [`Boolean!`](#boolean) | **Deprecated** in GitLab 14.8. Use paused. |
|
||||
| <a id="cirunneractive"></a>`active` {{< icon name="warning-solid" >}} | [`Boolean!`](#boolean) | **Deprecated** in GitLab 14.8. Use `paused`. This field is the inverse of `paused` and has no relationship to the runner's job execution status. For more details, see `jobExecutionStatus`. |
|
||||
| <a id="cirunneradminurl"></a>`adminUrl` | [`String`](#string) | Admin URL of the runner. Only available for administrators. |
|
||||
| <a id="cirunnercontactedat"></a>`contactedAt` | [`Time`](#time) | Timestamp of last contact from the runner. |
|
||||
| <a id="cirunnercreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp of creation of the runner. |
|
||||
|
|
|
|||
|
|
@ -178,6 +178,10 @@ method as the default:
|
|||
1. **Edit** the selected payment method and check the **Make default payment method** checkbox.
|
||||
1. Select **Save Changes**.
|
||||
|
||||
### Delete a default payment method
|
||||
|
||||
You cannot delete your default payment method directly through the Customers Portal. To delete a default payment method, [contact our Billing team](https://customers.gitlab.com/contact_us) for assistance.
|
||||
|
||||
## Pay for an invoice
|
||||
|
||||
You can pay for your invoices in the Customers Portal with a credit card.
|
||||
|
|
|
|||
|
|
@ -74,6 +74,12 @@ module Keeps
|
|||
description = match[:description]
|
||||
|
||||
file = File.expand_path("../#{filename}", __dir__)
|
||||
|
||||
unless File.exist?(file)
|
||||
puts "#{file} does not exist! Skipping"
|
||||
return
|
||||
end
|
||||
|
||||
full_file_content = File.read(file)
|
||||
|
||||
file_lines = full_file_content.lines
|
||||
|
|
|
|||
|
|
@ -111,8 +111,9 @@ module Gitlab
|
|||
|
||||
def valid?
|
||||
return true unless composite?
|
||||
return false unless linked?
|
||||
|
||||
linked?
|
||||
!scoped_user.has_composite_identity?
|
||||
end
|
||||
|
||||
def scoped_user
|
||||
|
|
|
|||
|
|
@ -3381,9 +3381,6 @@ msgstr ""
|
|||
msgid "Account: %{account}"
|
||||
msgstr ""
|
||||
|
||||
msgid "AccountTokens|Personal Access Token"
|
||||
msgstr ""
|
||||
|
||||
msgid "AccountTokens|Service Account"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -3702,6 +3699,9 @@ msgstr ""
|
|||
msgid "Add environment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add exception(s)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add exclusions"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -13254,6 +13254,9 @@ msgstr ""
|
|||
msgid "CiVariable|All environments"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariable|Create environment scope: %{searchTerm}"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariable|Create wildcard"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -19224,6 +19227,9 @@ msgstr ""
|
|||
msgid "Current password"
|
||||
msgstr ""
|
||||
|
||||
msgid "Current project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Current sign-in IP:"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -52119,9 +52125,6 @@ msgstr ""
|
|||
msgid "Roles and permissions"
|
||||
msgstr ""
|
||||
|
||||
msgid "Roles|Groups"
|
||||
msgstr ""
|
||||
|
||||
msgid "Roll up totals may reflect child items you don’t have access to."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -53838,9 +53841,6 @@ msgstr ""
|
|||
msgid "ScanResultPolicy|Add exception"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|Add exception(s)"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|Add new criteria"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -53904,6 +53904,9 @@ msgstr ""
|
|||
msgid "ScanResultPolicy|Customized CI Variables"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|Define branch patterns that can bypass policy requirements using wildcards and regex patterns. Use * for simple wildcards or regex patterns for advanced matching. %{linkStart}Learn more%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|Denied"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -54135,9 +54138,6 @@ msgstr ""
|
|||
msgid "ScanResultPolicy|Severity is:"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|Source branch pattern"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|Specify the packages where this license requires approval before use"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -54213,6 +54213,12 @@ msgstr ""
|
|||
msgid "ScanResultPolicy|denylist (%{licenseCount} %{licenses})"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|input source branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|input target branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|license status"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -72366,9 +72372,6 @@ msgstr ""
|
|||
msgid "can't be specified if a project was already provided"
|
||||
msgstr ""
|
||||
|
||||
msgid "can't be the same as the source project"
|
||||
msgstr ""
|
||||
|
||||
msgid "can't include: %{invalid_storages}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -74493,6 +74496,9 @@ msgstr ""
|
|||
msgid "this document"
|
||||
msgstr ""
|
||||
|
||||
msgid "to"
|
||||
msgstr ""
|
||||
|
||||
msgid "today"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ ENV GITLAB_LICENSE_MODE=test \
|
|||
GDK_KILL_CONFIRM=true \
|
||||
TOOL_VERSION_MANAGER=mise
|
||||
|
||||
# Disable idiomatic version files like .ruby-version by default and only rely on .tool-versions
|
||||
RUN mise settings add idiomatic_version_file_enable_tools "[]"
|
||||
|
||||
# Clone GDK at specific sha and bootstrap packages
|
||||
#
|
||||
ARG GDK_SHA
|
||||
|
|
|
|||
|
|
@ -20,7 +20,13 @@ RSpec.describe 'Dashboard Merge Requests', :js, feature_category: :code_review_w
|
|||
sign_in(current_user)
|
||||
end
|
||||
|
||||
it_behaves_like 'page with product usage data collection banner'
|
||||
context 'when the test is flaky',
|
||||
quarantine: {
|
||||
issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/535181',
|
||||
type: :flaky
|
||||
} do
|
||||
it_behaves_like 'page with product usage data collection banner'
|
||||
end
|
||||
|
||||
describe 'sidebar' do
|
||||
it 'has nav items for assigned MRs and review requests' do
|
||||
|
|
|
|||
|
|
@ -10,24 +10,27 @@ RSpec.describe WorkItems::WorkItemsFinder, feature_category: :team_planning do
|
|||
context 'with group parameter' do
|
||||
include_context '{Issues|WorkItems}Finder#execute context', :work_item
|
||||
|
||||
it_behaves_like 'work items finder group parameter'
|
||||
it_behaves_like 'work items finder group parameter', expect_group_items: false
|
||||
end
|
||||
|
||||
context 'with start and end date filtering' do
|
||||
include_context '{Issues|WorkItems}Finder#execute context', :work_item
|
||||
|
||||
let_it_be(:work_item1) { create(:work_item, :epic, project: project1) }
|
||||
let_it_be(:work_item2) { create(:work_item, :epic, project: project1) }
|
||||
|
||||
let(:scope) { 'all' }
|
||||
let(:params) { { start_date: '2020-08-12', end_date: '2020-08-14', group_id: group.id } }
|
||||
let(:params) { { start_date: '2020-08-12', end_date: '2020-08-14', project_id: project1.id } }
|
||||
|
||||
context 'when namespace level work items are disabled' do
|
||||
before do
|
||||
stub_feature_flags(namespace_level_work_items: false)
|
||||
group.add_developer(user)
|
||||
end
|
||||
before do
|
||||
group.add_developer(user)
|
||||
end
|
||||
|
||||
let_it_be(:work_item1) { create(:work_item, project: project1) }
|
||||
let_it_be(:work_item2) { create(:work_item, project: project1) }
|
||||
it 'does not return work items without a dates source' do
|
||||
expect(items).to be_empty
|
||||
end
|
||||
|
||||
context 'when work item start and due dates are both present' do
|
||||
let_it_be(:date_source1) do
|
||||
create(:work_items_dates_source, work_item: work_item1, start_date: '2020-08-13', due_date: '2020-08-15')
|
||||
end
|
||||
|
|
@ -36,137 +39,111 @@ RSpec.describe WorkItems::WorkItemsFinder, feature_category: :team_planning do
|
|||
create(:work_items_dates_source, work_item: work_item2, start_date: '2020-08-16', due_date: '2020-08-20')
|
||||
end
|
||||
|
||||
it 'does not attempt to filter by timeframe' do
|
||||
expect(items).to include(work_item1, work_item2)
|
||||
it 'returns only work items within timeframe' do
|
||||
expect(items).to contain_exactly(work_item1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when namespace level work items are enabled' do
|
||||
before do
|
||||
stub_feature_flags(namespace_level_work_items: true, work_item_epics: true)
|
||||
group.add_developer(user)
|
||||
context 'when only start date or due date is present' do
|
||||
let_it_be(:date_source_only_start) do
|
||||
create(:work_items_dates_source, work_item: work_item1, start_date: '2020-08-12', due_date: nil)
|
||||
end
|
||||
|
||||
let_it_be(:work_item1) { create(:work_item, :group_level, :epic, namespace: group) }
|
||||
let_it_be(:work_item2) { create(:work_item, :group_level, :epic, namespace: group) }
|
||||
let_it_be(:date_source_only_due) do
|
||||
create(:work_items_dates_source, work_item: work_item2, start_date: nil, due_date: '2020-08-14')
|
||||
end
|
||||
|
||||
it 'does not return work items without a dates source' do
|
||||
it 'returns only work items within timeframe' do
|
||||
expect(items).to contain_exactly(work_item1, work_item2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when using work_item_parent_ids filter' do
|
||||
include_context '{Issues|WorkItems}Finder#execute context', :work_item
|
||||
|
||||
let(:scope) { 'all' }
|
||||
|
||||
context 'when user has access to child item' do
|
||||
let_it_be(:child_item1) { create(:work_item, project: project1) }
|
||||
let_it_be(:parent_item1) { create(:work_item, :epic, project: project1) }
|
||||
|
||||
let(:params) { { work_item_parent_ids: [parent_item1.id] } }
|
||||
|
||||
before do
|
||||
create(:parent_link, work_item_parent: parent_item1, work_item: child_item1)
|
||||
end
|
||||
|
||||
it 'returns corresponding child work items' do
|
||||
expect(items).to contain_exactly(child_item1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by parent item from different project' do
|
||||
let_it_be(:another_project) { create(:project) }
|
||||
let_it_be(:child_item2) { create(:work_item, project: project1) }
|
||||
let_it_be(:parent_item2) { create(:work_item, :epic, project: another_project) }
|
||||
|
||||
let(:params) { { work_item_parent_ids: [parent_item2.id] } }
|
||||
|
||||
before do
|
||||
create(:parent_link, work_item_parent: parent_item2, work_item: child_item2)
|
||||
end
|
||||
|
||||
it 'returns corresponding child work items' do
|
||||
expect(items).to contain_exactly(child_item2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by multiple parent items' do
|
||||
let_it_be(:child_item3) { create(:work_item, project: project1) }
|
||||
let_it_be(:child_item4) { create(:work_item, project: project1) }
|
||||
|
||||
let_it_be(:parent_item3) { create(:work_item, :epic, project: project1) }
|
||||
let_it_be(:parent_item4) { create(:work_item, :epic, project: project1) }
|
||||
|
||||
let(:params) { { work_item_parent_ids: [parent_item3.id, parent_item4.id] } }
|
||||
|
||||
before do
|
||||
create(:parent_link, work_item_parent: parent_item3, work_item: child_item3)
|
||||
create(:parent_link, work_item_parent: parent_item4, work_item: child_item4)
|
||||
end
|
||||
|
||||
it 'returns corresponding child work items' do
|
||||
expect(items).to contain_exactly(child_item3, child_item4)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have access to child items' do
|
||||
let_it_be(:confidential_work_item) { create(:work_item, confidential: true, project: project1) }
|
||||
let_it_be(:parent_item5) { create(:work_item, :epic, confidential: true, project: project1) }
|
||||
|
||||
let(:search_user) { user2 }
|
||||
let(:params) { { work_item_parent_ids: [parent_item5.id] } }
|
||||
|
||||
before do
|
||||
create(:parent_link, work_item_parent: parent_item5, work_item: confidential_work_item)
|
||||
end
|
||||
|
||||
it 'does not return those items' do
|
||||
expect(items).to be_empty
|
||||
end
|
||||
|
||||
context 'when work item start and due dates are both present' do
|
||||
let_it_be(:date_source1) do
|
||||
create(:work_items_dates_source, work_item: work_item1, start_date: '2020-08-13', due_date: '2020-08-15')
|
||||
end
|
||||
|
||||
let_it_be(:date_source2) do
|
||||
create(:work_items_dates_source, work_item: work_item2, start_date: '2020-08-16', due_date: '2020-08-20')
|
||||
end
|
||||
|
||||
it 'returns only work items within timeframe' do
|
||||
expect(items).to contain_exactly(work_item1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when only start date or due date is present' do
|
||||
let_it_be(:date_source_only_start) do
|
||||
create(:work_items_dates_source, work_item: work_item1, start_date: '2020-08-12', due_date: nil)
|
||||
end
|
||||
|
||||
let_it_be(:date_source_only_due) do
|
||||
create(:work_items_dates_source, work_item: work_item2, start_date: nil, due_date: '2020-08-14')
|
||||
end
|
||||
|
||||
it 'returns only work items within timeframe' do
|
||||
expect(items).to contain_exactly(work_item1, work_item2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when using parent_ids filter' do
|
||||
let(:scope) { 'all' }
|
||||
context 'when user does not have access to child and parent items' do
|
||||
let_it_be(:private_project) { create(:project, :private) }
|
||||
let_it_be(:private_work_item) { create(:work_item, project: private_project) }
|
||||
let_it_be(:private_parent_item) { create(:work_item, :epic, project: private_project) }
|
||||
|
||||
context 'when user has access to child item' do
|
||||
let_it_be(:child_item1) { create(:work_item, project: project1) }
|
||||
let_it_be(:parent_item1) { create(:work_item, :epic, project: project1) }
|
||||
let(:search_user) { user2 }
|
||||
let(:params) { { work_item_parent_ids: [private_parent_item.id] } }
|
||||
|
||||
let(:params) { { work_item_parent_ids: [parent_item1.id] } }
|
||||
|
||||
before do
|
||||
create(:parent_link, work_item_parent: parent_item1, work_item: child_item1)
|
||||
end
|
||||
|
||||
it 'returns corresponding child work items' do
|
||||
expect(items).to contain_exactly(child_item1)
|
||||
end
|
||||
before do
|
||||
create(:parent_link, work_item_parent: private_parent_item, work_item: private_work_item)
|
||||
end
|
||||
|
||||
context 'when filtering by parent item from different project' do
|
||||
let_it_be(:another_project) { create(:project) }
|
||||
let_it_be(:child_item2) { create(:work_item, project: project1) }
|
||||
let_it_be(:parent_item2) { create(:work_item, :epic, project: another_project) }
|
||||
|
||||
let(:params) { { work_item_parent_ids: [parent_item2.id] } }
|
||||
|
||||
before do
|
||||
create(:parent_link, work_item_parent: parent_item2, work_item: child_item2)
|
||||
end
|
||||
|
||||
it 'returns corresponding child work items' do
|
||||
expect(items).to contain_exactly(child_item2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by multiple parent items' do
|
||||
let_it_be(:child_item3) { create(:work_item, project: project1) }
|
||||
let_it_be(:child_item4) { create(:work_item, project: project1) }
|
||||
|
||||
let_it_be(:parent_item3) { create(:work_item, :epic, project: project1) }
|
||||
let_it_be(:parent_item4) { create(:work_item, :epic, project: project1) }
|
||||
|
||||
let(:params) { { work_item_parent_ids: [parent_item3.id, parent_item4.id] } }
|
||||
|
||||
before do
|
||||
create(:parent_link, work_item_parent: parent_item3, work_item: child_item3)
|
||||
create(:parent_link, work_item_parent: parent_item4, work_item: child_item4)
|
||||
end
|
||||
|
||||
it 'returns corresponding child work items' do
|
||||
expect(items).to contain_exactly(child_item3, child_item4)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have access to child items' do
|
||||
let_it_be(:confidential_work_item) { create(:work_item, confidential: true, project: project1) }
|
||||
let_it_be(:parent_item5) { create(:work_item, :epic, confidential: true, project: project1) }
|
||||
|
||||
let(:search_user) { user2 }
|
||||
let(:params) { { work_item_parent_ids: [parent_item5.id] } }
|
||||
|
||||
before do
|
||||
create(:parent_link, work_item_parent: parent_item5, work_item: confidential_work_item)
|
||||
end
|
||||
|
||||
it 'does not return those items' do
|
||||
expect(items).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have access to child and parent items' do
|
||||
let_it_be(:private_project) { create(:project, :private) }
|
||||
let_it_be(:private_work_item) { create(:work_item, project: private_project) }
|
||||
let_it_be(:private_parent_item) { create(:work_item, :epic, project: private_project) }
|
||||
|
||||
let(:search_user) { user2 }
|
||||
let(:params) { { work_item_parent_ids: [private_parent_item.id] } }
|
||||
|
||||
before do
|
||||
create(:parent_link, work_item_parent: private_parent_item, work_item: private_work_item)
|
||||
end
|
||||
|
||||
it 'does not return those items' do
|
||||
expect(items).to be_empty
|
||||
end
|
||||
it 'does not return those items' do
|
||||
expect(items).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@ import VueApollo from 'vue-apollo';
|
|||
import possibleTypes from '~/graphql_shared/possible_types.json';
|
||||
import { typePolicies } from '~/lib/graphql';
|
||||
|
||||
/**
|
||||
* Creates a mock Apollo client
|
||||
* @param {[DocumentNode, RequestHandler][]} handlers
|
||||
* @param {import('@apollo/client/core').Resolvers} resolvers
|
||||
* @param {import('@apollo/client/core').InMemoryCacheConfig} cacheOptions
|
||||
*/
|
||||
export function createMockClient(handlers = [], resolvers = {}, cacheOptions = {}) {
|
||||
const cache = new InMemoryCache({
|
||||
possibleTypes,
|
||||
|
|
@ -26,6 +32,12 @@ export function createMockClient(handlers = [], resolvers = {}, cacheOptions = {
|
|||
return mockClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock Apollo provider
|
||||
* @param {[DocumentNode, RequestHandler][]} handlers
|
||||
* @param {import('@apollo/client/core').Resolvers} resolvers
|
||||
* @param {import('@apollo/client/core').InMemoryCacheConfig} cacheOptions
|
||||
*/
|
||||
export default function createMockApollo(handlers, resolvers, cacheOptions) {
|
||||
const mockClient = createMockClient(handlers, resolvers, cacheOptions);
|
||||
return new VueApollo({ defaultClient: mockClient });
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ describe('Ci environments dropdown', () => {
|
|||
const findActiveIconByIndex = (index) => findListboxItemByIndex(index).findComponent(GlIcon);
|
||||
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
|
||||
const findListboxText = () => findListbox().props('toggleText');
|
||||
const findCreateWildcardButton = () => wrapper.findByTestId('create-wildcard-button');
|
||||
const findCreateScopeButton = () => wrapper.findByTestId('create-scope-button');
|
||||
const findDropdownDivider = () => wrapper.findComponent(GlDropdownDivider);
|
||||
const findSearchQueryNote = () => wrapper.findByTestId('search-query-note');
|
||||
|
||||
|
|
@ -40,18 +40,18 @@ describe('Ci environments dropdown', () => {
|
|||
createComponent({ props: { canCreateWildcard: true }, searchTerm: 'stable/*' });
|
||||
});
|
||||
|
||||
it('renders create button during search', () => {
|
||||
expect(findCreateWildcardButton().exists()).toBe(true);
|
||||
it('renders a button to create wildcard', () => {
|
||||
expect(findCreateScopeButton().text()).toBe('Create wildcard: stable/*');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when canCreateWildcard is true and wildcard character is missing from search', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ props: { canCreateWildcard: true }, searchTerm: 'stable/' });
|
||||
createComponent({ props: { canCreateWildcard: true }, searchTerm: 'stable' });
|
||||
});
|
||||
|
||||
it('does not render create button during search', () => {
|
||||
expect(findCreateWildcardButton().exists()).toBe(false);
|
||||
it('renders a button to select environment scope', () => {
|
||||
expect(findCreateScopeButton().text()).toBe('Create environment scope: stable');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ describe('Ci environments dropdown', () => {
|
|||
});
|
||||
|
||||
it('does not render create button during search', () => {
|
||||
expect(findCreateWildcardButton().exists()).toBe(false);
|
||||
expect(findCreateScopeButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -77,7 +77,7 @@ describe('Ci environments dropdown', () => {
|
|||
});
|
||||
|
||||
it('renders create button with search term if environments do not contain search term', () => {
|
||||
const button = findCreateWildcardButton();
|
||||
const button = findCreateScopeButton();
|
||||
expect(button.exists()).toBe(true);
|
||||
expect(button.text()).toBe('Create wildcard: stable/*');
|
||||
});
|
||||
|
|
@ -103,6 +103,10 @@ describe('Ci environments dropdown', () => {
|
|||
expect(findActiveIconByIndex(0).classes('gl-invisible')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render create button during search', () => {
|
||||
expect(findCreateScopeButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('when isEnvironmentRequired is false', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ props: { isEnvironmentRequired: false, environments: envs } });
|
||||
|
|
@ -208,7 +212,7 @@ describe('Ci environments dropdown', () => {
|
|||
});
|
||||
|
||||
it('sets new environment scope as the selected environment scope', async () => {
|
||||
findCreateWildcardButton().trigger('click');
|
||||
findCreateScopeButton().trigger('click');
|
||||
|
||||
await findListbox().vm.$emit('search', searchTerm);
|
||||
|
||||
|
|
@ -216,7 +220,7 @@ describe('Ci environments dropdown', () => {
|
|||
});
|
||||
|
||||
it('includes new environment scope in search if it matches search term', async () => {
|
||||
findCreateWildcardButton().trigger('click');
|
||||
findCreateScopeButton().trigger('click');
|
||||
|
||||
await findListbox().vm.$emit('search', searchTerm);
|
||||
|
||||
|
|
@ -225,7 +229,7 @@ describe('Ci environments dropdown', () => {
|
|||
});
|
||||
|
||||
it('excludes new environment scope in search if it does not match the search term', async () => {
|
||||
findCreateWildcardButton().trigger('click');
|
||||
findCreateScopeButton().trigger('click');
|
||||
|
||||
await findListbox().vm.$emit('search', 'not-new-env');
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ describe('Environment Breadcrumb', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const mountComponent = ($route, props = {}) => {
|
||||
const mountComponent = ($route, props = { staticBreadcrumbs: [] }) => {
|
||||
wrapper = mount(EnvironmentBreadcrumb, {
|
||||
mocks: {
|
||||
$route,
|
||||
|
|
@ -75,9 +75,7 @@ describe('Environment Breadcrumb', () => {
|
|||
describe('when staticBreadcrumbs are provided', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent(routes[0], {
|
||||
staticBreadcrumbs: {
|
||||
items: [{ text: 'static', href: '/static' }],
|
||||
},
|
||||
staticBreadcrumbs: [{ text: 'static', href: '/static' }],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,8 @@ describe('Breadcrumbs utils', () => {
|
|||
const MockComponent = Vue.component('MockComponent', {
|
||||
props: {
|
||||
staticBreadcrumbs: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({ items: [] }),
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
render: (createElement) =>
|
||||
|
|
@ -59,14 +58,6 @@ describe('Breadcrumbs utils', () => {
|
|||
];
|
||||
});
|
||||
|
||||
it('nulls text and href of the last static breadcrumb item', () => {
|
||||
injectVueAppBreadcrumbs(mockRouter, MockComponent);
|
||||
expect(staticBreadcrumbs.items[0].text).toBe('First');
|
||||
expect(staticBreadcrumbs.items[0].href).toBe('/first');
|
||||
expect(staticBreadcrumbs.items[1].text).toBe('');
|
||||
expect(staticBreadcrumbs.items[1].href).toBe('');
|
||||
});
|
||||
|
||||
it('mounts given component at the inject target id', () => {
|
||||
const wrapper = createWrapper(
|
||||
injectVueAppBreadcrumbs(mockRouter, MockComponent, mockApolloProvider),
|
||||
|
|
@ -78,42 +69,24 @@ describe('Breadcrumbs utils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when singleNavOptIn is enabled', () => {
|
||||
it('removes the last item from staticBreadcrumbs and passes it to the component', () => {
|
||||
const breadcrumbsHTML = `
|
||||
<div id="js-vue-page-breadcrumbs-wrapper">
|
||||
<nav id="js-vue-page-breadcrumbs" class="gl-breadcrumbs"></nav>
|
||||
<div id="js-injected-page-breadcrumbs"></div>
|
||||
</div>
|
||||
`;
|
||||
<div id="js-vue-page-breadcrumbs-wrapper">
|
||||
<nav id="js-vue-page-breadcrumbs" class="gl-breadcrumbs"></nav>
|
||||
<div id="js-injected-page-breadcrumbs"></div>
|
||||
</div>
|
||||
`;
|
||||
setHTMLFixture(breadcrumbsHTML);
|
||||
staticBreadcrumbs.items = [
|
||||
{ text: 'First', href: '/first' },
|
||||
{ text: 'Last', href: '/last' },
|
||||
];
|
||||
const wrapper = createWrapper(
|
||||
injectVueAppBreadcrumbs(mockRouter, MockComponent, mockApolloProvider),
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
setHTMLFixture(breadcrumbsHTML);
|
||||
staticBreadcrumbs.items = [
|
||||
{ text: 'First', href: '/first' },
|
||||
{ text: 'Last', href: '/last' },
|
||||
];
|
||||
});
|
||||
|
||||
it('removes the last item from staticBreadcrumbs.items and passes it to the component', () => {
|
||||
const wrapper = createWrapper(
|
||||
injectVueAppBreadcrumbs(
|
||||
mockRouter,
|
||||
MockComponent,
|
||||
mockApolloProvider,
|
||||
{},
|
||||
{ singleNavOptIn: true },
|
||||
),
|
||||
);
|
||||
|
||||
expect(staticBreadcrumbs.items).toHaveLength(1);
|
||||
expect(staticBreadcrumbs.items[0].text).toBe('First');
|
||||
expect(staticBreadcrumbs.items[0].href).toBe('/first');
|
||||
|
||||
const component = wrapper.findComponent(MockComponent);
|
||||
expect(component.props('staticBreadcrumbs')).toEqual({
|
||||
items: [{ text: 'First', href: '/first' }],
|
||||
});
|
||||
});
|
||||
const component = wrapper.findComponent(MockComponent);
|
||||
expect(component.props('staticBreadcrumbs')).toEqual([{ text: 'First', href: '/first' }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ describe('HarborRegistryBreadcrumb', () => {
|
|||
|
||||
const createComponent = ({ route, routes, props = {} }) => {
|
||||
wrapper = shallowMount(HarborRegistryBreadcrumb, {
|
||||
propsData: props,
|
||||
propsData: { staticBreadcrumbs: [], ...props },
|
||||
mocks: {
|
||||
$route: route,
|
||||
$router: { options: { routes } },
|
||||
|
|
@ -64,30 +64,26 @@ describe('HarborRegistryBreadcrumb', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when static breadcrumbs are provided', () => {
|
||||
it('renders static breadcrumbs along with route breadcrumbs', () => {
|
||||
const staticBreadcrumbs = {
|
||||
items: [{ text: 'Static Item', href: '/static' }],
|
||||
};
|
||||
createComponent({
|
||||
route: { name: 'details', meta: detailsRoute.meta },
|
||||
routes: [rootRoute, detailsRoute],
|
||||
props: { staticBreadcrumbs },
|
||||
});
|
||||
const items = findBreadcrumbs().props('items');
|
||||
expect(items[0]).toEqual(staticBreadcrumbs.items[0]);
|
||||
it('renders static breadcrumbs along with route breadcrumbs', () => {
|
||||
const staticBreadcrumbs = [{ text: 'Static Item', href: '/static' }];
|
||||
createComponent({
|
||||
route: { name: 'details', meta: detailsRoute.meta },
|
||||
routes: [rootRoute, detailsRoute],
|
||||
props: { staticBreadcrumbs },
|
||||
});
|
||||
const items = findBreadcrumbs().props('items');
|
||||
expect(items[0]).toEqual(staticBreadcrumbs[0]);
|
||||
});
|
||||
|
||||
it('handles empty static breadcrumbs', () => {
|
||||
createComponent({
|
||||
route: { name: 'details', meta: detailsRoute.meta },
|
||||
routes: [rootRoute, detailsRoute],
|
||||
props: { staticBreadcrumbs: { items: [] } },
|
||||
});
|
||||
expect(findBreadcrumbs().props('items')).toStrictEqual([
|
||||
{ text: 'Root', to: '/' },
|
||||
{ text: 'Details', to: '/details' },
|
||||
]);
|
||||
it('handles empty static breadcrumbs', () => {
|
||||
createComponent({
|
||||
route: { name: 'details', meta: detailsRoute.meta },
|
||||
routes: [rootRoute, detailsRoute],
|
||||
props: { staticBreadcrumbs: [] },
|
||||
});
|
||||
expect(findBreadcrumbs().props('items')).toStrictEqual([
|
||||
{ text: 'Root', to: '/' },
|
||||
{ text: 'Details', to: '/details' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@ describe('Registry Breadcrumb', () => {
|
|||
{ name: 'details', path: '/:id', meta: { nameGenerator, path: '/details' } },
|
||||
];
|
||||
|
||||
const mountComponent = ($route) => {
|
||||
const mountComponent = ($route, props = {}) => {
|
||||
wrapper = shallowMount(component, {
|
||||
propsData: { staticBreadcrumbs: [], ...props },
|
||||
mocks: {
|
||||
$route,
|
||||
$router: {
|
||||
|
|
@ -62,4 +63,11 @@ describe('Registry Breadcrumb', () => {
|
|||
expect(breadcrumbItems[1].href).toBe('/:id');
|
||||
});
|
||||
});
|
||||
|
||||
it('passes static breadcrumbs along with route breadcrumbs', () => {
|
||||
mountComponent(routes[1], { staticBreadcrumbs: [{ text: 'Static', href: '/static' }] });
|
||||
const breadcrumbItems = wrapper.findComponent(GlBreadcrumb).props('items');
|
||||
expect(breadcrumbItems).toHaveLength(3);
|
||||
expect(breadcrumbItems[0]).toEqual({ text: 'Static', href: '/static' });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import {
|
|||
inboundRemoveNamespaceSuccess,
|
||||
inboundUpdateScopeSuccessResponse,
|
||||
mockAuthLogsCountResponse,
|
||||
mockCiJobTokenScopeAllowlistResponse,
|
||||
mockAutopopulateAllowlistResponse,
|
||||
mockRemoveAutopopulatedEntriesResponse,
|
||||
} from './mock_data';
|
||||
|
|
@ -55,6 +56,9 @@ describe('TokenAccess component', () => {
|
|||
let wrapper;
|
||||
|
||||
const authLogCountResponseHandler = jest.fn().mockResolvedValue(mockAuthLogsCountResponse(4));
|
||||
const ciJobTokenScopeAllowlistResponseHandler = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockCiJobTokenScopeAllowlistResponse);
|
||||
const authLogZeroCountResponseHandler = jest.fn().mockResolvedValue(mockAuthLogsCountResponse(0));
|
||||
const autopopulateAllowlistResponseHandler = jest
|
||||
.fn()
|
||||
|
|
@ -265,6 +269,18 @@ describe('TokenAccess component', () => {
|
|||
message: 'There was a problem fetching the job token scope value',
|
||||
});
|
||||
});
|
||||
|
||||
it('adds the current project in alphabetical order', async () => {
|
||||
await createComponent(
|
||||
[[getCiJobTokenScopeAllowlistQuery, ciJobTokenScopeAllowlistResponseHandler]],
|
||||
{ isJobTokenPoliciesEnabled: true },
|
||||
);
|
||||
|
||||
expect(findTokenAccessTable().props('items')[0].fullPath).toBe('abc/123');
|
||||
expect(findTokenAccessTable().props('items')[1].fullPath).toBe('root/my-repo');
|
||||
expect(findTokenAccessTable().props('items')[2].fullPath).toBe('root/your-repo');
|
||||
expect(findTokenAccessTable().props('items')[3].fullPath).toBe('zed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('inbound CI job token scope', () => {
|
||||
|
|
|
|||
|
|
@ -271,6 +271,70 @@ export const mockAuthLogsCountResponse = (count) => ({
|
|||
},
|
||||
});
|
||||
|
||||
export const mockCiJobTokenScopeAllowlistResponse = {
|
||||
data: {
|
||||
project: {
|
||||
id: 'gid://gitlab/Project/26',
|
||||
name: 'my-repo',
|
||||
fullPath: 'root/my-repo',
|
||||
avatarUrl: '',
|
||||
webUrl: 'http://localhost/root/my-repo',
|
||||
ciJobTokenScopeAllowlist: {
|
||||
groupsAllowlist: {
|
||||
nodes: [
|
||||
{
|
||||
defaultPermissions: true,
|
||||
jobTokenPolicies: [],
|
||||
autopopulated: true,
|
||||
target: {
|
||||
id: 'gid://gitlab/Group/4',
|
||||
name: 'zed',
|
||||
fullPath: 'zed',
|
||||
avatarUrl: '',
|
||||
webUrl: 'http://localhost/zed',
|
||||
__typename: 'CiJobTokenAccessibleGroup',
|
||||
},
|
||||
__typename: 'CiJobTokenScopeAllowlistEntry',
|
||||
},
|
||||
],
|
||||
},
|
||||
projectsAllowlist: {
|
||||
nodes: [
|
||||
{
|
||||
defaultPermissions: true,
|
||||
jobTokenPolicies: [],
|
||||
autopopulated: true,
|
||||
target: {
|
||||
id: 'gid://gitlab/Project/23',
|
||||
name: 'your-repo',
|
||||
fullPath: 'root/your-repo',
|
||||
avatarUrl: '',
|
||||
webUrl: 'http://localhost/root/your-repo',
|
||||
__typename: 'CiJobTokenAccessibleProject',
|
||||
},
|
||||
__typename: 'CiJobTokenScopeAllowlistEntry',
|
||||
},
|
||||
{
|
||||
defaultPermissions: true,
|
||||
jobTokenPolicies: [],
|
||||
autopopulated: true,
|
||||
target: {
|
||||
id: 'gid://gitlab/Project/14',
|
||||
name: 'abc123',
|
||||
fullPath: 'abc/123',
|
||||
avatarUrl: '',
|
||||
webUrl: 'http://localhost/abc/123',
|
||||
__typename: 'CiJobTokenAccessibleProject',
|
||||
},
|
||||
__typename: 'CiJobTokenScopeAllowlistEntry',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockAuthLogsResponse = (hasNextPage = false) => ({
|
||||
data: {
|
||||
project: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { GlTable, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlBadge, GlTable, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import TokenAccessTable from '~/token_access/components/token_access_table.vue';
|
||||
import { mockGroups, mockProjects } from './mock_data';
|
||||
|
|
@ -6,14 +6,15 @@ import { mockGroups, mockProjects } from './mock_data';
|
|||
describe('Token access table', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = (props) => {
|
||||
const createComponent = (props, provided) => {
|
||||
wrapper = mountExtended(TokenAccessTable, {
|
||||
provide: { fullPath: 'root/ci-project' },
|
||||
provide: { fullPath: 'root/ci-project2', ...provided },
|
||||
propsData: props,
|
||||
});
|
||||
};
|
||||
|
||||
const findTable = () => wrapper.findComponent(GlTable);
|
||||
const findCurrentProjectBadge = () => wrapper.findComponent(GlBadge);
|
||||
const findEditButton = () => wrapper.findByTestId('token-access-table-edit-button');
|
||||
const findRemoveButton = () => wrapper.findByTestId('token-access-table-remove-button');
|
||||
const findAllTableRows = () => findTable().findAll('tbody tr');
|
||||
|
|
@ -68,13 +69,23 @@ describe('Token access table', () => {
|
|||
expect(wrapper.emitted('editItem')[0][0]).toBe(items[0]);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show the current project badge', () => {
|
||||
expect(findCurrentProjectBadge().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when item is the current project', () => {
|
||||
beforeEach(() => createComponent({ items: [mockProjects.at(-1)] }));
|
||||
beforeEach(() =>
|
||||
createComponent({ items: [mockProjects.at(-1)] }, { fullPath: 'root/ci-project' }),
|
||||
);
|
||||
|
||||
it('does not show edit button', () => {
|
||||
expect(findEditButton().exists()).toBe(false);
|
||||
it('shows the edit button', () => {
|
||||
expect(findEditButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('shows the current project badge', () => {
|
||||
expect(findCurrentProjectBadge().text()).toBe('Current project');
|
||||
});
|
||||
|
||||
it('does not show remove button', () => {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ describe('WorkItemBreadcrumb', () => {
|
|||
mocks: {
|
||||
$route,
|
||||
},
|
||||
propsData: props,
|
||||
propsData: { staticBreadcrumbs: [], ...props },
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -163,7 +163,7 @@ describe('WorkItemBreadcrumb', () => {
|
|||
createComponent({
|
||||
$route: { name: 'workItem', params: { iid: '1' }, path: '/1' },
|
||||
props: {
|
||||
staticBreadcrumbs: { items: [{ text: 'Static', href: '/static' }] },
|
||||
staticBreadcrumbs: [{ text: 'Static', href: '/static' }],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -414,32 +414,6 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#show_projects' do
|
||||
let(:projects) do
|
||||
Project.all
|
||||
end
|
||||
|
||||
it 'returns true when there are projects' do
|
||||
expect(helper.show_projects?(projects, {})).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns true when there are no projects but a name is given' do
|
||||
expect(helper.show_projects?(Project.none, name: 'foo')).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns true when there are no projects but personal is present' do
|
||||
expect(helper.show_projects?(Project.none, personal: 'true')).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns false when there are no projects and there is no name' do
|
||||
expect(helper.show_projects?(Project.none, {})).to eq(false)
|
||||
end
|
||||
|
||||
it 'returns true when there are no projects but archived param is "only"' do
|
||||
expect(helper.show_projects?(Project.none, archived: 'only')).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#push_to_create_project_command' do
|
||||
let(:user) { build_stubbed(:user, username: 'john') }
|
||||
|
||||
|
|
|
|||
|
|
@ -776,7 +776,8 @@ RSpec.describe Gitlab::Auth::AuthFinders, feature_category: :system_access do
|
|||
end
|
||||
|
||||
context 'when scoped user is specified' do
|
||||
let(:scopes) { "user:#{user.id}" }
|
||||
let(:scoped_user) { create(:user) }
|
||||
let(:scopes) { "user:#{scoped_user.id}" }
|
||||
|
||||
context 'when linking composite identitiy succeeds' do
|
||||
it 'returns the oauth token' do
|
||||
|
|
|
|||
|
|
@ -179,6 +179,22 @@ RSpec.describe Gitlab::Auth::Identity, :request_store, feature_category: :system
|
|||
end
|
||||
end
|
||||
|
||||
describe '#valid?' do
|
||||
context 'when a composite identity is linked to another composite identity' do
|
||||
before do
|
||||
allow(primary_user).to receive(:composite_identity_enforced).and_return(true)
|
||||
allow(scoped_user).to receive(:composite_identity_enforced).and_return(true)
|
||||
end
|
||||
|
||||
it 'is not valid' do
|
||||
identity = described_class.fabricate(primary_user)
|
||||
identity.link!(scoped_user)
|
||||
|
||||
expect(identity).not_to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.sidekiq_restore!' do
|
||||
context 'when job has primary and scoped identity stored' do
|
||||
let(:job) { { 'jid' => 123, 'sqci' => [primary_user.id, scoped_user.id] } }
|
||||
|
|
|
|||
|
|
@ -481,6 +481,16 @@ RSpec.describe Ability, feature_category: :system_access do
|
|||
expect(subject).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'when delegated user is a composite identity' do
|
||||
let_it_be(:delegated_user) { user }
|
||||
|
||||
it 'returns false' do
|
||||
group.add_developer(user)
|
||||
|
||||
expect(subject).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -91,9 +91,11 @@ RSpec.describe Ci::JobToken::Authorization, feature_category: :secrets_managemen
|
|||
context 'when origin project is the same as the accessed project' do
|
||||
let(:accessed_project) { origin_project }
|
||||
|
||||
it 'does not capture the authorization' do
|
||||
it 'captures the authorization in the RequestStore' do
|
||||
capture
|
||||
expect(described_class.captured_authorizations).to be_nil
|
||||
expect(described_class.captured_authorizations).to eq(
|
||||
origin_project_id: origin_project.id,
|
||||
accessed_project_id: accessed_project.id)
|
||||
end
|
||||
|
||||
it "triggers an internal event" do
|
||||
|
|
|
|||
|
|
@ -80,11 +80,10 @@ RSpec.describe Ci::JobToken::ProjectScopeLink, feature_category: :continuous_int
|
|||
expect(link.errors[:target_project]).to contain_exactly("can't be blank")
|
||||
end
|
||||
|
||||
it 'must have a target project different than source project', :aggregate_failures do
|
||||
it 'can have a target project that is the source project', :aggregate_failures do
|
||||
link = build(:ci_job_token_project_scope_link, target_project: project, source_project: project)
|
||||
|
||||
expect(link).not_to be_valid
|
||||
expect(link.errors[:target_project]).to contain_exactly("can't be the same as the source project")
|
||||
expect(link).to be_valid
|
||||
end
|
||||
|
||||
describe 'job token policies' do
|
||||
|
|
|
|||
|
|
@ -320,7 +320,7 @@ RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, f
|
|||
|
||||
it { is_expected.to be(true) }
|
||||
|
||||
it_behaves_like 'not capturing job token policies'
|
||||
it_behaves_like 'capturing job token policies'
|
||||
end
|
||||
|
||||
context 'when accessed project has ci_inbound_job_token_scope_enabled and instance-level scope set to false' do
|
||||
|
|
|
|||
|
|
@ -4311,6 +4311,14 @@ RSpec.describe Group, feature_category: :groups_and_projects do
|
|||
it { is_expected.to be false }
|
||||
end
|
||||
|
||||
describe '#supports_group_work_items?' do
|
||||
let(:group) { build(:group) }
|
||||
|
||||
subject { group.supports_group_work_items? }
|
||||
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
|
||||
describe '#continue_indented_text_feature_flag_enabled?' do
|
||||
it_behaves_like 'checks self and root ancestor feature flag' do
|
||||
let(:feature_flag) { :continue_indented_text }
|
||||
|
|
|
|||
|
|
@ -1,237 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require 'request_store'
|
||||
|
||||
RSpec.describe 'getting Work Item counts by state', feature_category: :portfolio_management do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:group) { create(:group, :private) }
|
||||
let_it_be(:milestone) { create(:milestone, group: group) }
|
||||
let_it_be(:label) { create(:group_label, group: group) }
|
||||
let_it_be(:work_item_opened1) do
|
||||
create(:work_item, namespace: group, milestone_id: milestone.id, labels: [label], title: 'Foo')
|
||||
end
|
||||
|
||||
let_it_be(:work_item_opened2) { create(:work_item, :confidential, namespace: group, author: current_user) }
|
||||
let_it_be(:work_item_closed1) do
|
||||
create(:work_item, :closed, :confidential, namespace: group, milestone_id: milestone.id)
|
||||
end
|
||||
|
||||
let_it_be(:work_item_closed2) do
|
||||
create(:work_item, :epic, :closed, namespace: group, assignees: [current_user], labels: [label], description: 'Bar')
|
||||
end
|
||||
|
||||
let(:params) { {} }
|
||||
|
||||
subject(:query_counts) { post_graphql(query, current_user: current_user) }
|
||||
|
||||
context 'with work items count data' do
|
||||
let(:work_item_counts) { graphql_data.dig('group', 'workItemStateCounts') }
|
||||
|
||||
context 'with group permissions' do
|
||||
before_all do
|
||||
group.add_developer(current_user)
|
||||
end
|
||||
|
||||
it_behaves_like 'a working graphql query' do
|
||||
before do
|
||||
query_counts
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns the correct counts for each state' do
|
||||
query_counts
|
||||
|
||||
expect(work_item_counts).to eq(
|
||||
'all' => 4,
|
||||
'opened' => 2,
|
||||
'closed' => 2
|
||||
)
|
||||
end
|
||||
|
||||
context 'when filters are provided' do
|
||||
context 'when filtering by author username' do
|
||||
let(:params) { { 'authorUsername' => current_user.username } }
|
||||
|
||||
it 'returns the correct counts for each state' do
|
||||
query_counts
|
||||
|
||||
expect(work_item_counts).to eq(
|
||||
'all' => 1,
|
||||
'opened' => 1,
|
||||
'closed' => 0
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by assignee usernames' do
|
||||
let(:params) { { 'assigneeUsernames' => [current_user.username] } }
|
||||
|
||||
it 'returns the correct counts for each state' do
|
||||
query_counts
|
||||
|
||||
expect(work_item_counts).to eq(
|
||||
'all' => 1,
|
||||
'opened' => 0,
|
||||
'closed' => 1
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by confidential' do
|
||||
let(:params) { { 'confidential' => true } }
|
||||
|
||||
it 'returns the correct counts for each state' do
|
||||
query_counts
|
||||
|
||||
expect(work_item_counts).to eq(
|
||||
'all' => 2,
|
||||
'opened' => 1,
|
||||
'closed' => 1
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by label name' do
|
||||
let(:params) { { 'labelName' => [label.name] } }
|
||||
|
||||
it 'returns the correct counts for each state' do
|
||||
query_counts
|
||||
|
||||
expect(work_item_counts).to eq(
|
||||
'all' => 2,
|
||||
'opened' => 1,
|
||||
'closed' => 1
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by milestone title' do
|
||||
let(:params) { { 'milestoneTitle' => [milestone.title] } }
|
||||
|
||||
it 'returns the correct counts for each state' do
|
||||
query_counts
|
||||
|
||||
expect(work_item_counts).to eq(
|
||||
'all' => 2,
|
||||
'opened' => 1,
|
||||
'closed' => 1
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by reaction emoji' do
|
||||
before_all do
|
||||
create(:award_emoji, :upvote, user: current_user, awardable: work_item_opened1)
|
||||
create(:award_emoji, :upvote, user: current_user, awardable: work_item_opened2)
|
||||
create(:award_emoji, :downvote, user: current_user, awardable: work_item_closed2)
|
||||
end
|
||||
|
||||
let(:params) { { 'myReactionEmoji' => AwardEmoji::THUMBS_UP } }
|
||||
|
||||
it 'returns the correct counts for each state' do
|
||||
query_counts
|
||||
|
||||
expect(work_item_counts).to eq(
|
||||
'all' => 2,
|
||||
'opened' => 2,
|
||||
'closed' => 0
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by type' do
|
||||
let(:params) { { 'types' => [:ISSUE] } }
|
||||
|
||||
it 'returns the correct counts for each state' do
|
||||
query_counts
|
||||
|
||||
expect(work_item_counts).to eq(
|
||||
'all' => 3,
|
||||
'opened' => 2,
|
||||
'closed' => 1
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when searching by text' do
|
||||
let(:params) { { search: 'Foo' } }
|
||||
|
||||
it 'returns the correct counts for each status' do
|
||||
query_counts
|
||||
|
||||
expect(work_item_counts).to eq(
|
||||
'all' => 1,
|
||||
'opened' => 1,
|
||||
'closed' => 0
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when searching in title' do
|
||||
let(:params) { { search: 'Foo', in: [:TITLE] } }
|
||||
|
||||
it 'returns the correct counts for each status' do
|
||||
query_counts
|
||||
|
||||
expect(work_item_counts).to eq(
|
||||
'all' => 1,
|
||||
'opened' => 1,
|
||||
'closed' => 0
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when searching in description' do
|
||||
let(:params) { { search: 'Bar', in: [:DESCRIPTION] } }
|
||||
|
||||
it 'returns the correct counts for each status' do
|
||||
query_counts
|
||||
|
||||
expect(work_item_counts).to eq(
|
||||
'all' => 1,
|
||||
'opened' => 0,
|
||||
'closed' => 1
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the namespace_level_work_items feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(namespace_level_work_items: false)
|
||||
end
|
||||
|
||||
it 'does not return work item counts' do
|
||||
query_counts
|
||||
|
||||
expect_graphql_errors_to_be_empty
|
||||
expect(work_item_counts).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without group permissions' do
|
||||
it 'does not return work item counts' do
|
||||
query_counts
|
||||
|
||||
expect_graphql_errors_to_be_empty
|
||||
expect(work_item_counts).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def query(args: params)
|
||||
fields = <<~QUERY
|
||||
#{all_graphql_fields_for('WorkItemStateCountsType'.classify)}
|
||||
QUERY
|
||||
|
||||
graphql_query_for(
|
||||
'group',
|
||||
{ 'fullPath' => group.full_path },
|
||||
query_graphql_field('workItemStateCounts', args, fields)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -630,10 +630,14 @@ RSpec.describe API::ProjectJobTokenScope, feature_category: :secrets_management
|
|||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
|
||||
it 'returns bad_request when adding the source project' do
|
||||
it 'returns created and creates a job token scope link for self-project when adding the source project' do
|
||||
post api(post_job_token_scope_allowlist_path, user), params: { target_project_id: project.id }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(json_response).to include(
|
||||
'target_project_id' => project.id,
|
||||
'source_project_id' => project.id
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns not_found when project for param `project_id` does not exist' do
|
||||
|
|
|
|||
|
|
@ -73,8 +73,9 @@ RSpec.describe 'Group Level Work Items', feature_category: :team_planning do
|
|||
end
|
||||
|
||||
describe 'GET /groups/:group/-/work_items/:iid' do
|
||||
let_it_be(:work_item) { create(:work_item, :group_level, namespace: group) }
|
||||
let(:iid) { work_item.iid }
|
||||
let(:current_user) { developer }
|
||||
let_it_be(:work_item) { create(:work_item, :group_level, namespace: group) }
|
||||
let(:work_items_path) do
|
||||
url_for(controller: 'groups/work_items', action: :show, group_id: group.full_path, iid: iid)
|
||||
end
|
||||
|
|
@ -83,73 +84,10 @@ RSpec.describe 'Group Level Work Items', feature_category: :team_planning do
|
|||
sign_in(current_user)
|
||||
end
|
||||
|
||||
context 'when the user can read the group' do
|
||||
let(:current_user) { developer }
|
||||
it 'returns not found' do
|
||||
get work_items_path
|
||||
|
||||
it 'renders show' do
|
||||
get work_items_path
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to render_template(:show)
|
||||
end
|
||||
|
||||
context 'when the new page gets requested' do
|
||||
let(:iid) { 'new' }
|
||||
|
||||
it 'renders show' do
|
||||
get work_items_path
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to render_template(:show)
|
||||
expect(response.body).to have_pushed_frontend_feature_flags(namespaceLevelWorkItems: true)
|
||||
end
|
||||
end
|
||||
|
||||
it 'has correct metadata' do
|
||||
get work_items_path
|
||||
|
||||
expect(response.body).to include("#{work_item.title} (#{work_item.to_reference})")
|
||||
expect(response.body).to include(work_item.work_item_type.name.pluralize)
|
||||
end
|
||||
|
||||
context 'when the namespace_level_work_items feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(namespace_level_work_items: false)
|
||||
end
|
||||
|
||||
it 'returns not found' do
|
||||
get work_items_path
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
|
||||
context 'on new page' do
|
||||
let(:iid) { 'new' }
|
||||
|
||||
it 'returns not found' do
|
||||
get work_items_path
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user cannot read the group' do
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
it 'returns not found' do
|
||||
get work_items_path
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
|
||||
it 'does not include sensitive metadata' do
|
||||
get work_items_path
|
||||
|
||||
expect(response.body).not_to include("#{work_item.title} (#{work_item.to_reference})")
|
||||
expect(response.body).not_to include(work_item.work_item_type.name.pluralize)
|
||||
end
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ RSpec.describe Ci::JobTokenScope::AddProjectService, feature_category: :continuo
|
|||
|
||||
let(:target_project) { project }
|
||||
|
||||
it_behaves_like 'returns error', "Validation failed: Target project can't be the same as the source project"
|
||||
it_behaves_like 'adds project'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -127,6 +127,38 @@ RSpec.describe Ci::JobTokenScope::UpdatePoliciesService, feature_category: :cont
|
|||
expect(project_link.job_token_policies).to eq(%w[read_deployments])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the target project is the current project' do
|
||||
let_it_be(:target) { project }
|
||||
|
||||
context 'when the job token scope does not exist yet' do
|
||||
it 'creates a new job token scope', :aggregate_failures do
|
||||
expect(execute).to be_success
|
||||
|
||||
project_link = execute.payload
|
||||
|
||||
expect(project_link.source_project).to eq(project)
|
||||
expect(project_link.target_project).to eq(project)
|
||||
expect(project_link.default_permissions).to be(false)
|
||||
expect(project_link.job_token_policies).to eq(%w[read_deployments read_packages])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the job token scope already exists' do
|
||||
before do
|
||||
project_scope_link.update!(target_project: project)
|
||||
end
|
||||
|
||||
it 'updates the existing job token scope', :aggregate_failures do
|
||||
expect(execute).to be_success
|
||||
|
||||
project_link = project_scope_link.reload
|
||||
|
||||
expect(project_link.default_permissions).to be(false)
|
||||
expect(project_link.job_token_policies).to eq(%w[read_deployments read_packages])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -111,100 +111,12 @@ RSpec.describe ContainerRegistry::Protection::Concerns::TagRule, feature_categor
|
|||
|
||||
subject(:protected_by_rules) { service.protected_for_delete?(project:, current_user:) }
|
||||
|
||||
shared_examples 'checking for mutable tag rules' do
|
||||
context 'when project has matching mutable tag rules for delete and access level' do
|
||||
before_all do
|
||||
create(:container_registry_protection_tag_rule, tag_name_pattern: 'b',
|
||||
minimum_access_level_for_delete: :owner, project: project)
|
||||
end
|
||||
|
||||
where(:user_role, :protected_from_delete_with_tags) do
|
||||
:guest | true
|
||||
:reporter | true
|
||||
:developer | true
|
||||
:maintainer | true
|
||||
:owner | false
|
||||
:admin | false
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
if user_role == :admin
|
||||
allow(current_user).to receive(:can_admin_all_resources?).and_return(true)
|
||||
else
|
||||
project.send(:"add_#{user_role}", current_user)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the project has container registry tags' do
|
||||
before do
|
||||
allow(project).to receive(:has_container_registry_tags?).and_return(true)
|
||||
end
|
||||
|
||||
it { is_expected.to be(protected_from_delete_with_tags) }
|
||||
end
|
||||
|
||||
context 'when project has no container registry tags' do
|
||||
before do
|
||||
allow(project).to receive(:has_container_registry_tags?).and_return(false)
|
||||
end
|
||||
|
||||
it { is_expected.to be(false) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project has no matching tag rules for delete and access level' do
|
||||
before do
|
||||
allow(project).to receive(:has_container_registry_tags?).and_return(true)
|
||||
end
|
||||
|
||||
it { is_expected.to be(false) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project has immutable tag rules' do
|
||||
before_all do
|
||||
create(:container_registry_protection_tag_rule, :immutable, tag_name_pattern: 'a', project: project)
|
||||
end
|
||||
|
||||
context 'when project has container registry tags' do
|
||||
before do
|
||||
allow(project).to receive(:has_container_registry_tags?).and_return(true)
|
||||
end
|
||||
|
||||
it { is_expected.to be(true) }
|
||||
|
||||
context 'when current_user is an admin', :enable_admin_mode do
|
||||
let(:current_user) { build_stubbed(:admin) }
|
||||
|
||||
it { is_expected.to be(true) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project has no container registry tags' do
|
||||
before do
|
||||
allow(project).to receive(:has_container_registry_tags?).and_return(false)
|
||||
end
|
||||
|
||||
it { is_expected.to be(false) }
|
||||
end
|
||||
|
||||
context 'when immutable tags feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(container_registry_immutable_tags: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'checking for mutable tag rules'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project has no immutable tag rules' do
|
||||
before do
|
||||
allow(project).to receive(:has_container_registry_tags?).and_return(true)
|
||||
end
|
||||
|
||||
it_behaves_like 'checking for mutable tag rules'
|
||||
it_behaves_like 'checking for mutable tag rules' # immutable rules are ignored
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -38,19 +38,17 @@ RSpec.describe Groups::AutocompleteService, feature_category: :groups_and_projec
|
|||
let_it_be(:project) { create(:project, group: group) }
|
||||
let_it_be(:sub_group_project) { create(:project, group: sub_group) }
|
||||
|
||||
let_it_be(:group_issue) { create(:work_item, :group_level, namespace: group) }
|
||||
let_it_be(:project_issue) { create(:issue, project: project) }
|
||||
let_it_be(:sub_group_issue) { create(:work_item, :group_level, namespace: sub_group) }
|
||||
let_it_be(:sub_group_project_issue) { create(:issue, confidential: true, project: sub_group_project) }
|
||||
|
||||
it 'returns issues in group and subgroups' do
|
||||
issues = subject.issues
|
||||
|
||||
expect(issues.map(&:iid)).to contain_exactly(
|
||||
project_issue.iid, sub_group_project_issue.iid, group_issue.iid, sub_group_issue.iid
|
||||
project_issue.iid, sub_group_project_issue.iid
|
||||
)
|
||||
expect(issues.map(&:title)).to contain_exactly(
|
||||
project_issue.title, sub_group_project_issue.title, group_issue.title, sub_group_issue.title
|
||||
project_issue.title, sub_group_project_issue.title
|
||||
)
|
||||
expect(issues.map(&:icon_name).uniq).to contain_exactly('issue-type-issue')
|
||||
end
|
||||
|
|
@ -62,23 +60,10 @@ RSpec.describe Groups::AutocompleteService, feature_category: :groups_and_projec
|
|||
expect(issues.map(&:title)).to contain_exactly(sub_group_project_issue.title)
|
||||
end
|
||||
|
||||
context 'when namespace_level_work_items feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(namespace_level_work_items: false)
|
||||
end
|
||||
|
||||
it 'returns issues in projects only' do
|
||||
issues = subject.issues
|
||||
|
||||
expect(issues.map(&:iid)).to contain_exactly(project_issue.iid, sub_group_project_issue.iid)
|
||||
expect(issues.map(&:title)).to contain_exactly(project_issue.title, sub_group_project_issue.title)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when search param is given' do
|
||||
let_it_be(:issue_8) { create(:issue, project: project, iid: 8) }
|
||||
let_it_be(:issue_80) { create(:work_item, :group_level, namespace: group, iid: 80) }
|
||||
let_it_be(:issue_800) { create(:work_item, :group_level, namespace: sub_group, iid: 800) }
|
||||
let_it_be(:issue_80) { create(:work_item, project: project, iid: 80) }
|
||||
let_it_be(:issue_800) { create(:work_item, project: project, iid: 800) }
|
||||
let_it_be(:issue_8000) { create(:issue, project: sub_group_project, iid: 8000) }
|
||||
let_it_be(:issue_80000) { create(:issue, project: sub_group_project, iid: 80000) }
|
||||
let_it_be(:issue_90000) { create(:issue, project: project, title: 'gitlab issue 8', iid: 90000) }
|
||||
|
|
|
|||
|
|
@ -538,20 +538,8 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
|
|||
.to receive(:remove_legacy_registry_tags).and_return(true)
|
||||
end
|
||||
|
||||
it { is_expected.to be false }
|
||||
|
||||
context 'when the current user is an admin', :enable_admin_mode do
|
||||
let(:user) { build_stubbed(:admin) }
|
||||
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
|
||||
context 'when the feature `container_registry_immutable_tags` is disabled' do
|
||||
before do
|
||||
stub_feature_flags(container_registry_immutable_tags: false)
|
||||
end
|
||||
|
||||
it { is_expected.to be true }
|
||||
it 'ignores the immutable tag protection rules' do
|
||||
is_expected.to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'checking for mutable tag rules' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
context 'when project has matching mutable tag rules for delete and access level' do
|
||||
before_all do
|
||||
create(:container_registry_protection_tag_rule, tag_name_pattern: 'b',
|
||||
minimum_access_level_for_delete: :owner, project: project)
|
||||
end
|
||||
|
||||
where(:user_role, :protected_from_delete_with_tags) do
|
||||
:guest | true
|
||||
:reporter | true
|
||||
:developer | true
|
||||
:maintainer | true
|
||||
:owner | false
|
||||
:admin | false
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
if user_role == :admin
|
||||
allow(current_user).to receive(:can_admin_all_resources?).and_return(true)
|
||||
else
|
||||
project.send(:"add_#{user_role}", current_user)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the project has container registry tags' do
|
||||
before do
|
||||
allow(project).to receive(:has_container_registry_tags?).and_return(true)
|
||||
end
|
||||
|
||||
it { is_expected.to be(protected_from_delete_with_tags) }
|
||||
end
|
||||
|
||||
context 'when project has no container registry tags' do
|
||||
before do
|
||||
allow(project).to receive(:has_container_registry_tags?).and_return(false)
|
||||
end
|
||||
|
||||
it { is_expected.to be(false) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project has no matching tag rules for delete and access level' do
|
||||
before do
|
||||
allow(project).to receive(:has_container_registry_tags?).and_return(true)
|
||||
end
|
||||
|
||||
it { is_expected.to be(false) }
|
||||
end
|
||||
end
|
||||
|
|
@ -24,7 +24,7 @@ RSpec.shared_examples 'variable list env scope' do
|
|||
|
||||
click_button('All (default)')
|
||||
fill_in 'Search', with: 'review/*'
|
||||
find('[data-testid="create-wildcard-button"]').click
|
||||
find('[data-testid="create-scope-button"]').click
|
||||
|
||||
click_button('Add variable')
|
||||
end
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
|
|||
end
|
||||
|
||||
context 'when filtering by group id' do
|
||||
let(:params) { { group_id: subgroup.id } }
|
||||
# WorkItemsFinder only fetches project-level work items when `include_descendants: true`
|
||||
let(:params) { { group_id: subgroup.id, include_descendants: true } }
|
||||
|
||||
it 'returns no items' do
|
||||
expect(items).to be_empty
|
||||
|
|
@ -1321,7 +1322,9 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
|
|||
it_behaves_like 'returns public, does not return hidden or confidential'
|
||||
|
||||
it 'does not filter by confidentiality' do
|
||||
allow(items_model).to receive(:where).and_call_original
|
||||
expect(items_model).not_to receive(:where).with(a_string_matching('confidential'), anything)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
|
@ -1356,6 +1359,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
|
|||
it_behaves_like 'returns public and confidential, does not return hidden'
|
||||
|
||||
it 'does not filter by confidentiality' do
|
||||
allow(items_model).to receive(:where).and_call_original
|
||||
expect(items_model).not_to receive(:where).with(a_string_matching('confidential'), anything)
|
||||
|
||||
subject
|
||||
|
|
@ -1371,6 +1375,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
|
|||
it_behaves_like 'returns public, confidential, and hidden'
|
||||
|
||||
it 'does not filter by confidentiality' do
|
||||
allow(items_model).to receive(:where).and_call_original
|
||||
expect(items_model).not_to receive(:where).with(a_string_matching('confidential'), anything)
|
||||
|
||||
subject
|
||||
|
|
@ -1402,6 +1407,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
|
|||
end
|
||||
|
||||
it 'does not filter by confidentiality' do
|
||||
allow(items_model).to receive(:where).and_call_original
|
||||
expect(items_model).not_to receive(:where).with(a_string_matching('confidential'), anything)
|
||||
|
||||
subject
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'work items finder group parameter' do
|
||||
RSpec.shared_examples 'work items finder group parameter' do |expect_group_items: true|
|
||||
context 'when group parameter is present' do
|
||||
let_it_be(:group_work_item) { create(:work_item, :group_level, namespace: group, author: user) }
|
||||
let_it_be(:group_confidential_work_item) do
|
||||
|
|
@ -21,32 +21,12 @@ RSpec.shared_examples 'work items finder group parameter' do
|
|||
let(:params) { { group_id: group } }
|
||||
let(:scope) { 'all' }
|
||||
|
||||
before do
|
||||
stub_licensed_features(epics: true)
|
||||
end
|
||||
|
||||
context 'when namespace_level_work_items and work_item_epics is disabled' do
|
||||
before do
|
||||
stub_feature_flags(namespace_level_work_items: false, work_item_epics: false)
|
||||
end
|
||||
|
||||
it 'does not return group level work items' do
|
||||
expect(items).to contain_exactly(item1, item5)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when work_item_epics is disabled' do
|
||||
before do
|
||||
stub_feature_flags(work_item_epics: false)
|
||||
end
|
||||
|
||||
it 'returns group level work items' do
|
||||
expect(items).to contain_exactly(group_work_item)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns group level work items' do
|
||||
expect(items).to contain_exactly(group_work_item)
|
||||
if expect_group_items
|
||||
expect(items).to contain_exactly(group_work_item)
|
||||
else
|
||||
expect(items).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has access to confidential items' do
|
||||
|
|
@ -55,7 +35,11 @@ RSpec.shared_examples 'work items finder group parameter' do
|
|||
end
|
||||
|
||||
it 'includes confidential group-level items' do
|
||||
expect(items).to contain_exactly(group_work_item, group_confidential_work_item)
|
||||
if expect_group_items
|
||||
expect(items).to contain_exactly(group_work_item, group_confidential_work_item)
|
||||
else
|
||||
expect(items).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -66,7 +50,11 @@ RSpec.shared_examples 'work items finder group parameter' do
|
|||
|
||||
context 'when user does not have access to all subgroups' do
|
||||
it 'includes work items from subgroups and child projects with access' do
|
||||
expect(items).to contain_exactly(group_work_item, subgroup_work_item, item1, item4, item5)
|
||||
if expect_group_items
|
||||
expect(items).to contain_exactly(group_work_item, subgroup_work_item, item1, item4, item5)
|
||||
else
|
||||
expect(items).to contain_exactly(item1, item4, item5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -76,14 +64,18 @@ RSpec.shared_examples 'work items finder group parameter' do
|
|||
end
|
||||
|
||||
it 'includes work items from subgroups and child projects with access' do
|
||||
expect(items).to contain_exactly(
|
||||
group_work_item,
|
||||
subgroup_work_item,
|
||||
subgroup2_work_item,
|
||||
item1,
|
||||
item4,
|
||||
item5
|
||||
)
|
||||
if expect_group_items
|
||||
expect(items).to contain_exactly(
|
||||
group_work_item,
|
||||
subgroup_work_item,
|
||||
subgroup2_work_item,
|
||||
item1,
|
||||
item4,
|
||||
item5
|
||||
)
|
||||
else
|
||||
expect(items).to contain_exactly(item1, item4, item5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -93,17 +85,21 @@ RSpec.shared_examples 'work items finder group parameter' do
|
|||
end
|
||||
|
||||
it 'includes confidential items from subgroups and child projects' do
|
||||
expect(items).to contain_exactly(
|
||||
group_work_item,
|
||||
group_confidential_work_item,
|
||||
subgroup_work_item,
|
||||
subgroup_confidential_work_item,
|
||||
subgroup2_work_item,
|
||||
subgroup2_confidential_work_item,
|
||||
item1,
|
||||
item4,
|
||||
item5
|
||||
)
|
||||
if expect_group_items
|
||||
expect(items).to contain_exactly(
|
||||
group_work_item,
|
||||
group_confidential_work_item,
|
||||
subgroup_work_item,
|
||||
subgroup_confidential_work_item,
|
||||
subgroup2_work_item,
|
||||
subgroup2_confidential_work_item,
|
||||
item1,
|
||||
item4,
|
||||
item5
|
||||
)
|
||||
else
|
||||
expect(items).to contain_exactly(item1, item4, item5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -113,15 +109,19 @@ RSpec.shared_examples 'work items finder group parameter' do
|
|||
end
|
||||
|
||||
it 'includes confidential items from subgroups and child projects with access' do
|
||||
expect(items).to contain_exactly(
|
||||
group_work_item,
|
||||
subgroup_work_item,
|
||||
subgroup2_work_item,
|
||||
subgroup2_confidential_work_item,
|
||||
item1,
|
||||
item4,
|
||||
item5
|
||||
)
|
||||
if expect_group_items
|
||||
expect(items).to contain_exactly(
|
||||
group_work_item,
|
||||
subgroup_work_item,
|
||||
subgroup2_work_item,
|
||||
subgroup2_confidential_work_item,
|
||||
item1,
|
||||
item4,
|
||||
item5
|
||||
)
|
||||
else
|
||||
expect(items).to contain_exactly(item1, item4, item5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -131,7 +131,11 @@ RSpec.shared_examples 'work items finder group parameter' do
|
|||
end
|
||||
|
||||
it 'does not include work items from projects' do
|
||||
expect(items).to contain_exactly(group_work_item, subgroup_work_item)
|
||||
if expect_group_items
|
||||
expect(items).to contain_exactly(group_work_item, subgroup_work_item)
|
||||
else
|
||||
expect(items).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -140,7 +144,11 @@ RSpec.shared_examples 'work items finder group parameter' do
|
|||
let(:params) { { group_id: subgroup, include_ancestors: true } }
|
||||
|
||||
it 'includes work items from ancestor groups' do
|
||||
expect(items).to contain_exactly(group_work_item, subgroup_work_item)
|
||||
if expect_group_items
|
||||
expect(items).to contain_exactly(group_work_item, subgroup_work_item)
|
||||
else
|
||||
expect(items).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -151,7 +159,11 @@ RSpec.shared_examples 'work items finder group parameter' do
|
|||
let(:params) { { group_id: subgroup, include_descendants: true, include_ancestors: true } }
|
||||
|
||||
it 'includes work items from ancestor groups, subgroups, and child projects' do
|
||||
expect(items).to contain_exactly(group_work_item, subgroup_work_item, sub_subgroup_work_item, item4)
|
||||
if expect_group_items
|
||||
expect(items).to contain_exactly(group_work_item, subgroup_work_item, sub_subgroup_work_item, item4)
|
||||
else
|
||||
expect(items).to contain_exactly(item4)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -57,8 +57,9 @@ RSpec.shared_examples 'logs inbound authorizations via job token' do |success_st
|
|||
|
||||
context 'when pipeline is triggered by the same project job token' do
|
||||
let(:job_token) { create(:ci_build, :running, project: accessed_project, user: user).token }
|
||||
let(:origin_project) { accessed_project }
|
||||
|
||||
it_behaves_like 'does not attempt to capture authorization', success_status
|
||||
it_behaves_like 'successfully logs authorization'
|
||||
end
|
||||
|
||||
context 'when pipeline is triggered by another project job token and project scope is disabled' do
|
||||
|
|
|
|||
Loading…
Reference in New Issue