Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-06-04 12:07:18 +00:00
parent 6a4cea0a8f
commit d58a3afef8
78 changed files with 717 additions and 1015 deletions

View File

@ -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:

View File

@ -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>

View File

@ -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);

View File

@ -109,7 +109,7 @@ export const initPage = async () => {
},
});
injectVueAppBreadcrumbs(router, EnvironmentBreadcrumbs, null, {}, { singleNavOptIn: true });
injectVueAppBreadcrumbs(router, EnvironmentBreadcrumbs);
return new Vue({
el,

View File

@ -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),
},
});
},

View File

@ -92,14 +92,7 @@ export default () => {
});
return {
attachBreadcrumb: () =>
injectVueAppBreadcrumbs(
router,
RegistryBreadcrumb,
apolloProvider,
{},
{ singleNavOptIn: true },
),
attachBreadcrumb: () => injectVueAppBreadcrumbs(router, RegistryBreadcrumb, apolloProvider),
attachMainComponent,
};
};

View File

@ -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];
},
},
};

View File

@ -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,
};
};

View File

@ -51,14 +51,7 @@ export default () => {
});
return {
attachBreadcrumb: () =>
injectVueAppBreadcrumbs(
router,
RegistryBreadcrumb,
apolloProvider,
{},
{ singleNavOptIn: true },
),
attachBreadcrumb: () => injectVueAppBreadcrumbs(router, RegistryBreadcrumb, apolloProvider),
attachMainComponent,
};
};

View File

@ -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,

View File

@ -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 [

View File

@ -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"

View File

@ -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 }) => ({

View File

@ -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 = {

View File

@ -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: {

View File

@ -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 }) => ({

View File

@ -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: {

View File

@ -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({

View File

@ -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,

View File

@ -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])

View File

@ -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

View File

@ -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?

View File

@ -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?

View File

@ -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

View File

@ -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,

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
cd74cc1d7004ba2a75c24c40b01d46fa369f6cb488fd430fbadece1f9caa1854

View File

@ -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

View File

@ -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. |

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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 dont 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 ""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 });

View File

@ -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');

View File

@ -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' }],
});
});

View File

@ -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' }]);
});
});
});

View File

@ -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' },
]);
});
});

View File

@ -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' });
});
});

View File

@ -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', () => {

View File

@ -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: {

View File

@ -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', () => {

View File

@ -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' }],
},
});

View File

@ -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') }

View File

@ -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

View File

@ -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] } }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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