Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-05-18 21:08:58 +00:00
parent e69b62ceed
commit acc1c1c468
72 changed files with 622 additions and 492 deletions

View File

@ -885,7 +885,6 @@
- <<: *if-merge-request
- <<: *if-default-branch-refs
variables:
ARCH: amd64,arm64
BUILD_GDK_BASE: "true"
.build-images:rules:build-assets-image:

View File

@ -7,6 +7,8 @@ import {
GlTooltip,
GlTooltipDirective,
GlPopover,
GlBadge,
GlPagination,
} from '@gitlab/ui';
import semverLt from 'semver/functions/lt';
import semverInc from 'semver/functions/inc';
@ -15,7 +17,7 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { helpPagePath } from '~/helpers/help_page_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { AGENT_STATUSES, I18N_AGENT_TABLE } from '../constants';
import { MAX_LIST_COUNT, AGENT_STATUSES, I18N_AGENT_TABLE } from '../constants';
import { getAgentConfigPath } from '../clusters_util';
import DeleteAgentButton from './delete_agent_button.vue';
@ -28,6 +30,8 @@ export default {
GlSprintf,
GlTooltip,
GlPopover,
GlBadge,
GlPagination,
TimeAgoTooltip,
DeleteAgentButton,
},
@ -60,6 +64,12 @@ export default {
type: Number,
},
},
data() {
return {
currentPage: 1,
limit: this.maxAgents ?? MAX_LIST_COUNT,
};
},
computed: {
fields() {
const tdClass = 'gl-pt-3! gl-pb-4! gl-vertical-align-middle!';
@ -114,6 +124,16 @@ export default {
serverVersion() {
return this.kasVersion || this.gitlabVersion;
},
showPagination() {
return !this.maxAgents && this.agents.length > this.limit;
},
prevPage() {
return Math.max(this.currentPage - 1, 0);
},
nextPage() {
const nextPage = this.currentPage + 1;
return nextPage > Math.ceil(this.agents.length / this.limit) ? null : nextPage;
},
},
methods: {
getStatusCellId(item) {
@ -184,84 +204,105 @@ export default {
</script>
<template>
<gl-table
:items="agentsList"
:fields="fields"
stacked="md"
class="gl-mb-4!"
data-testid="cluster-agent-list-table"
>
<template #cell(name)="{ item }">
<gl-link :href="item.webPath" data-testid="cluster-agent-name-link">
{{ item.name }}
</gl-link>
</template>
<template #cell(status)="{ item }">
<span
:id="getStatusCellId(item)"
class="gl-md-pr-5"
data-testid="cluster-agent-connection-status"
>
<span :class="$options.AGENT_STATUSES[item.status].class" class="gl-mr-3">
<gl-icon :name="$options.AGENT_STATUSES[item.status].icon" :size="16" /></span
>{{ $options.AGENT_STATUSES[item.status].name }}
</span>
<gl-tooltip v-if="item.status === 'active'" :target="getStatusCellId(item)" placement="right">
<gl-sprintf :message="$options.AGENT_STATUSES[item.status].tooltip.title"
><template #timeAgo>{{ timeFormatted(item.lastContact) }}</template>
</gl-sprintf>
</gl-tooltip>
<gl-popover
v-else
:target="getStatusCellId(item)"
:title="$options.AGENT_STATUSES[item.status].tooltip.title"
placement="right"
container="viewport"
>
<p>
<gl-sprintf :message="$options.AGENT_STATUSES[item.status].tooltip.body"
><template #timeAgo>{{ timeFormatted(item.lastContact) }}</template></gl-sprintf
>
</p>
<p class="gl-mb-0">
<gl-link :href="$options.troubleshootingLink" target="_blank" class="gl-font-sm">
{{ $options.i18n.troubleshootingText }}</gl-link
>
</p>
</gl-popover>
</template>
<template #cell(lastContact)="{ item }">
<span data-testid="cluster-agent-last-contact">
<time-ago-tooltip v-if="item.lastContact" :time="item.lastContact" />
<span v-else>{{ $options.i18n.neverConnectedText }}</span>
</span>
</template>
<template #cell(version)="{ item }">
<span :id="getVersionCellId(item)" data-testid="cluster-agent-version">
{{ getAgentVersionString(item) }}
<gl-icon
v-if="isVersionMismatch(item) || isVersionOutdated(item)"
name="warning"
class="gl-text-orange-500 gl-ml-2"
/>
</span>
<gl-popover
v-if="isVersionMismatch(item) || isVersionOutdated(item)"
:target="getVersionCellId(item)"
:title="getVersionPopoverTitle(item)"
:data-testid="getPopoverTestId(item)"
placement="right"
container="viewport"
>
<div v-if="isVersionMismatch(item) && isVersionOutdated(item)">
<p>{{ $options.i18n.versionMismatchText }}</p>
<div>
<gl-table
:items="agentsList"
:fields="fields"
:per-page="limit"
:current-page="currentPage"
stacked="md"
class="gl-mb-4!"
data-testid="cluster-agent-list-table"
>
<template #cell(name)="{ item }">
<gl-link :href="item.webPath" data-testid="cluster-agent-name-link">{{ item.name }}</gl-link
><gl-badge v-if="item.isShared" class="gl-ml-3">{{
$options.i18n.sharedBadgeText
}}</gl-badge>
</template>
<template #cell(status)="{ item }">
<span
:id="getStatusCellId(item)"
class="gl-md-pr-5"
data-testid="cluster-agent-connection-status"
>
<span :class="$options.AGENT_STATUSES[item.status].class" class="gl-mr-3">
<gl-icon :name="$options.AGENT_STATUSES[item.status].icon" :size="16" /></span
>{{ $options.AGENT_STATUSES[item.status].name }}
</span>
<gl-tooltip
v-if="item.status === 'active'"
:target="getStatusCellId(item)"
placement="right"
>
<gl-sprintf :message="$options.AGENT_STATUSES[item.status].tooltip.title"
><template #timeAgo>{{ timeFormatted(item.lastContact) }}</template>
</gl-sprintf>
</gl-tooltip>
<gl-popover
v-else
:target="getStatusCellId(item)"
:title="$options.AGENT_STATUSES[item.status].tooltip.title"
placement="right"
container="viewport"
>
<p>
<gl-sprintf :message="$options.AGENT_STATUSES[item.status].tooltip.body"
><template #timeAgo>{{ timeFormatted(item.lastContact) }}</template></gl-sprintf
>
</p>
<p class="gl-mb-0">
<gl-link :href="$options.troubleshootingLink" target="_blank" class="gl-font-sm">
{{ $options.i18n.troubleshootingText }}</gl-link
>
</p>
</gl-popover>
</template>
<template #cell(lastContact)="{ item }">
<span data-testid="cluster-agent-last-contact">
<time-ago-tooltip v-if="item.lastContact" :time="item.lastContact" />
<span v-else>{{ $options.i18n.neverConnectedText }}</span>
</span>
</template>
<template #cell(version)="{ item }">
<span :id="getVersionCellId(item)" data-testid="cluster-agent-version">
{{ getAgentVersionString(item) }}
<gl-icon
v-if="isVersionMismatch(item) || isVersionOutdated(item)"
name="warning"
class="gl-text-orange-500 gl-ml-2"
/>
</span>
<gl-popover
v-if="isVersionMismatch(item) || isVersionOutdated(item)"
:target="getVersionCellId(item)"
:title="getVersionPopoverTitle(item)"
:data-testid="getPopoverTestId(item)"
placement="right"
container="viewport"
>
<div v-if="isVersionMismatch(item) && isVersionOutdated(item)">
<p>{{ $options.i18n.versionMismatchText }}</p>
<p class="gl-mb-0">
<gl-sprintf :message="$options.i18n.versionOutdatedText">
<template #version>{{ serverVersion }}</template>
</gl-sprintf>
<gl-link :href="$options.versionUpdateLink" class="gl-font-sm">
{{ $options.i18n.viewDocsText }}</gl-link
>
</p>
</div>
<p v-else-if="isVersionMismatch(item)" class="gl-mb-0">
{{ $options.i18n.versionMismatchText }}
</p>
<p v-else-if="isVersionOutdated(item)" class="gl-mb-0">
<gl-sprintf :message="$options.i18n.versionOutdatedText">
<template #version>{{ serverVersion }}</template>
</gl-sprintf>
@ -269,53 +310,54 @@ export default {
{{ $options.i18n.viewDocsText }}</gl-link
>
</p>
</div>
<p v-else-if="isVersionMismatch(item)" class="gl-mb-0">
{{ $options.i18n.versionMismatchText }}
</p>
</gl-popover>
</template>
<p v-else-if="isVersionOutdated(item)" class="gl-mb-0">
<gl-sprintf :message="$options.i18n.versionOutdatedText">
<template #version>{{ serverVersion }}</template>
</gl-sprintf>
<gl-link :href="$options.versionUpdateLink" class="gl-font-sm">
{{ $options.i18n.viewDocsText }}</gl-link
>
</p>
</gl-popover>
</template>
<template #cell(agentID)="{ item }">
<span data-testid="cluster-agent-id">
{{ getAgentId(item) }}
</span>
</template>
<template #cell(agentID)="{ item }">
<span data-testid="cluster-agent-id">
{{ getAgentId(item) }}
</span>
</template>
<template #cell(configuration)="{ item }">
<span data-testid="cluster-agent-configuration-link">
<gl-link v-if="item.configFolder" :href="item.configFolder.webPath">
{{ getAgentConfigPath(item.name) }}
</gl-link>
<template #cell(configuration)="{ item }">
<span data-testid="cluster-agent-configuration-link">
<gl-link v-if="item.configFolder" :href="item.configFolder.webPath">
{{ getAgentConfigPath(item.name) }}
</gl-link>
<span v-else-if="item.isShared">
{{ $options.i18n.externalConfigText }}
</span>
<span v-else
>{{ $options.i18n.defaultConfigText }}
<gl-link
v-gl-tooltip
:href="$options.configHelpLink"
:title="$options.i18n.defaultConfigTooltip"
:aria-label="$options.i18n.defaultConfigTooltip"
class="gl-vertical-align-middle"
><gl-icon name="question-o" :size="14" /></gl-link
></span>
</span>
</template>
<span v-else
>{{ $options.i18n.defaultConfigText }}
<gl-link
v-gl-tooltip
:href="$options.configHelpLink"
:title="$options.i18n.defaultConfigTooltip"
:aria-label="$options.i18n.defaultConfigTooltip"
class="gl-vertical-align-middle"
><gl-icon name="question-o" :size="14" /></gl-link
></span>
</span>
</template>
<template #cell(options)="{ item }">
<delete-agent-button
:agent="item"
:default-branch-name="defaultBranchName"
:max-agents="maxAgents"
/>
</template>
</gl-table>
<template #cell(options)="{ item }">
<delete-agent-button
v-if="!item.isShared"
:agent="item"
:default-branch-name="defaultBranchName"
/>
</template>
</gl-table>
<gl-pagination
v-if="showPagination"
v-model="currentPage"
:prev-page="prevPage"
:next-page="nextPage"
align="center"
class="gl-mt-5"
/>
</div>
</template>

View File

@ -1,9 +1,9 @@
<script>
import { GlAlert, GlKeysetPagination, GlLoadingIcon, GlBanner } from '@gitlab/ui';
import { GlAlert, GlLoadingIcon, GlBanner } from '@gitlab/ui';
import { s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { MAX_LIST_COUNT, AGENT_FEEDBACK_ISSUE, AGENT_FEEDBACK_KEY } from '../constants';
import { AGENT_FEEDBACK_ISSUE, AGENT_FEEDBACK_KEY } from '../constants';
import getAgentsQuery from '../graphql/queries/get_agents.query.graphql';
import { getAgentLastContact, getAgentStatus } from '../clusters_util';
import AgentEmptyState from './agent_empty_state.vue';
@ -27,7 +27,6 @@ export default {
return {
defaultBranchName: this.defaultBranchName,
projectPath: this.projectPath,
...this.cursor,
};
},
update(data) {
@ -37,13 +36,15 @@ export default {
result() {
this.emitAgentsLoaded();
},
error() {
this.queryErrored = true;
},
},
},
components: {
AgentEmptyState,
AgentTable,
GlAlert,
GlKeysetPagination,
GlLoadingIcon,
GlBanner,
LocalStorageSync,
@ -69,41 +70,41 @@ export default {
},
data() {
return {
cursor: {
first: this.limit ? this.limit : MAX_LIST_COUNT,
last: null,
},
folderList: {},
feedbackBannerDismissed: false,
queryErrored: false,
};
},
computed: {
agentList() {
let list = this.agents?.project?.clusterAgents?.nodes;
const localAgents = this.agents?.project?.clusterAgents?.nodes || [];
const sharedAgents = [
...(this.agents?.project?.ciAccessAuthorizedAgents?.nodes || []),
...(this.agents?.project?.userAccessAuthorizedAgents?.nodes || []),
].map((node) => {
return {
...node.agent,
isShared: true,
};
});
if (list) {
list = list.map((agent) => {
const filteredList = [...localAgents, ...sharedAgents]
.filter((node, index, list) => {
return node && index === list.findIndex((agent) => agent.id === node.id);
})
.map((agent) => {
const configFolder = this.folderList[agent.name];
const lastContact = getAgentLastContact(agent?.tokens?.nodes);
const status = getAgentStatus(lastContact);
return { ...agent, configFolder, lastContact, status };
});
}
})
.sort((a, b) => b.lastUsedAt - a.lastUsedAt);
return list;
},
agentPageInfo() {
return this.agents?.project?.clusterAgents?.pageInfo || {};
return filteredList;
},
isLoading() {
return this.$apollo.queries.agents.loading;
},
showPagination() {
return !this.limit && (this.agentPageInfo.hasPreviousPage || this.agentPageInfo.hasNextPage);
},
treePageInfo() {
return this.agents?.project?.repository?.tree?.trees?.pageInfo || {};
},
feedbackBannerEnabled() {
return this.glFeatures.showGitlabAgentFeedback;
},
@ -112,22 +113,6 @@ export default {
},
},
methods: {
nextPage() {
this.cursor = {
first: MAX_LIST_COUNT,
last: null,
afterAgent: this.agentPageInfo.endCursor,
afterTree: this.treePageInfo.endCursor,
};
},
prevPage() {
this.cursor = {
first: null,
last: MAX_LIST_COUNT,
beforeAgent: this.agentPageInfo.startCursor,
beforeTree: this.treePageInfo.endCursor,
};
},
updateTreeList(data) {
const configFolders = data?.project?.repository?.tree?.trees?.nodes;
@ -138,8 +123,7 @@ export default {
}
},
emitAgentsLoaded() {
const count = this.agents?.project?.clusterAgents?.count;
this.$emit('onAgentsLoad', count);
this.$emit('onAgentsLoad', this.agentList?.length);
},
handleBannerClose() {
this.feedbackBannerDismissed = true;
@ -151,7 +135,7 @@ export default {
<template>
<gl-loading-icon v-if="isLoading" size="lg" />
<section v-else-if="agentList">
<section v-else-if="!queryErrored">
<div v-if="agentList.length">
<local-storage-sync
v-if="feedbackBannerEnabled"
@ -174,12 +158,8 @@ export default {
<agent-table
:agents="agentList"
:default-branch-name="defaultBranchName"
:max-agents="cursor.first"
:max-agents="limit"
/>
<div v-if="showPagination" class="gl-display-flex gl-justify-content-center gl-mt-5">
<gl-keyset-pagination v-bind="agentPageInfo" @prev="prevPage" @next="nextPage" />
</div>
</div>
<agent-empty-state v-else />

View File

@ -39,11 +39,6 @@ export default {
required: false,
type: String,
},
maxAgents: {
default: null,
required: false,
type: Number,
},
},
data() {
return {
@ -64,8 +59,6 @@ export default {
getAgentsQueryVariables() {
return {
defaultBranchName: this.defaultBranchName,
first: this.maxAgents,
last: null,
projectPath: this.projectPath,
};
},

View File

@ -13,10 +13,9 @@ import {
MODAL_TYPE_EMPTY,
MODAL_TYPE_REGISTER,
} from '../constants';
import { addAgentToStore, addAgentConfigToStore } from '../graphql/cache_update';
import { addAgentConfigToStore } from '../graphql/cache_update';
import createAgent from '../graphql/mutations/create_agent.mutation.graphql';
import createAgentToken from '../graphql/mutations/create_agent_token.mutation.graphql';
import getAgentsQuery from '../graphql/queries/get_agents.query.graphql';
import agentConfigurations from '../graphql/queries/agent_configurations.query.graphql';
import AvailableAgentsDropdown from './available_agents_dropdown.vue';
import AgentToken from './agent_token.vue';
@ -148,14 +147,6 @@ export default {
projectPath: this.projectPath,
},
},
update: (store, { data: { createClusterAgent } }) => {
addAgentToStore(
store,
createClusterAgent,
getAgentsQuery,
this.getAgentsQueryVariables,
);
},
})
.then(({ data: { createClusterAgent } }) => {
return createClusterAgent;

View File

@ -1,7 +1,7 @@
import { __, s__, sprintf } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
export const MAX_LIST_COUNT = 25;
export const MAX_LIST_COUNT = 20;
export const INSTALL_AGENT_MODAL_ID = 'install-agent';
export const ACTIVE_CONNECTION_TIME = 480000;
export const NAME_MAX_LENGTH = 50;
@ -86,6 +86,8 @@ export const I18N_AGENT_TABLE = {
viewDocsText: s__('ClusterAgents|How to update an agent?'),
defaultConfigText: s__('ClusterAgents|Default configuration'),
defaultConfigTooltip: s__('ClusterAgents|What is default configuration?'),
sharedBadgeText: s__('ClusterAgents|shared'),
externalConfigText: s__('ClusterAgents|External project'),
};
export const I18N_AGENT_TOKEN = {

View File

@ -2,27 +2,6 @@ import produce from 'immer';
export const hasErrors = ({ errors = [] }) => errors?.length;
export function addAgentToStore(store, createClusterAgent, query, variables) {
if (!hasErrors(createClusterAgent)) {
const { clusterAgent } = createClusterAgent;
const sourceData = store.readQuery({
query,
variables,
});
const data = produce(sourceData, (draftData) => {
draftData.project.clusterAgents.nodes.push(clusterAgent);
draftData.project.clusterAgents.count += 1;
});
store.writeQuery({
query,
variables,
data,
});
}
}
export function addAgentConfigToStore(
store,
clusterAgentTokenCreate,
@ -65,7 +44,12 @@ export function removeAgentFromStore(store, deleteClusterAgent, query, variables
draftData.project.clusterAgents.nodes = draftData.project.clusterAgents.nodes.filter(
({ id }) => id !== deleteClusterAgent.id,
);
draftData.project.clusterAgents.count -= 1;
draftData.project.ciAccessAuthorizedAgents.nodes = draftData.project.ciAccessAuthorizedAgents.nodes.filter(
({ agent }) => agent.id !== deleteClusterAgent.id,
);
draftData.project.userAccessAuthorizedAgents.nodes = draftData.project.userAccessAuthorizedAgents.nodes.filter(
({ agent }) => agent.id !== deleteClusterAgent.id,
);
});
store.writeQuery({

View File

@ -2,6 +2,7 @@ fragment ClusterAgentFragment on ClusterAgent {
id
name
webPath
createdAt
connections {
nodes {
metadata {

View File

@ -1,26 +1,28 @@
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
#import "../fragments/cluster_agent.fragment.graphql"
query getAgents(
$defaultBranchName: String!
$projectPath: ID!
$first: Int
$last: Int
$afterAgent: String
$beforeAgent: String
) {
query getAgents($defaultBranchName: String!, $projectPath: ID!) {
project(fullPath: $projectPath) {
id
clusterAgents(first: $first, last: $last, before: $beforeAgent, after: $afterAgent) {
clusterAgents {
nodes {
...ClusterAgentFragment
}
}
pageInfo {
...PageInfo
ciAccessAuthorizedAgents {
nodes {
agent {
...ClusterAgentFragment
}
}
}
count
userAccessAuthorizedAgents {
nodes {
agent {
...ClusterAgentFragment
}
}
}
repository {

View File

@ -577,6 +577,7 @@ export const saveDiffDiscussion = async ({ state, dispatch }, { note, formData }
const postData = getNoteFormData({
commit: state.commit,
note,
showWhitespace: state.showWhitespace,
...formData,
});

View File

@ -140,6 +140,7 @@ export function getFormData(params) {
linePosition,
positionType,
lineRange,
showWhitespace,
} = params;
const position = JSON.stringify({
@ -156,6 +157,7 @@ export function getFormData(params) {
width: params.width,
height: params.height,
line_range: lineRange,
ignore_whitespace_change: !showWhitespace,
});
const postData = {

View File

@ -164,7 +164,7 @@ export default {
<gl-tabs
content-class="gl-pt-0"
data-testid="security-configuration-container"
data-qa-selector="security_configuration_container"
sync-active-tab-with-query-params
lazy
>
@ -196,9 +196,12 @@ export default {
{{ $options.i18n.description }}
</p>
<p v-if="canViewCiHistory">
<gl-link data-testid="security-view-history-link" :href="gitlabCiHistoryPath">{{
$options.i18n.configurationHistory
}}</gl-link>
<gl-link
data-testid="security-view-history-link"
data-qa-selector="security_configuration_history_link"
:href="gitlabCiHistoryPath"
>{{ $options.i18n.configurationHistory }}</gl-link
>
</p>
</template>

View File

@ -28,7 +28,7 @@ export default {
variant="info"
:primary-button-link="autoDevopsPath"
:primary-button-text="$options.i18n.primaryButtonText"
data-testid="autodevops-container"
data-qa-selector="autodevops_container"
@dismiss="dismissMethod"
>
<gl-sprintf :message="$options.i18n.body">

View File

@ -122,7 +122,7 @@ export default {
v-if="isNotSastIACTemporaryHack"
:class="statusClasses"
data-testid="feature-status"
:data-qa-feature="`${feature.type}_${hasEnabledStatus}_status`"
:data-qa-selector="`${feature.type}_status`"
>
<feature-card-badge
v-if="hasBadge"
@ -164,7 +164,7 @@ export default {
:href="feature.configurationPath"
variant="confirm"
:category="configurationButton.category"
:data-testid="`${feature.type}_enable_button`"
:data-qa-selector="`${feature.type}_enable_button`"
class="gl-mt-5"
>
{{ configurationButton.text }}
@ -176,7 +176,7 @@ export default {
variant="confirm"
:category="manageViaMrButtonCategory"
class="gl-mt-5"
:data-testid="`${feature.type}_mr_button`"
:data-qa-selector="`${feature.type}_mr_button`"
@error="onError"
/>

View File

@ -43,7 +43,6 @@ class DiffNotePosition < ApplicationRecord
def self.position_to_attrs(position)
position_attrs = position.to_h
position_attrs[:diff_content_type] = position_attrs.delete(:position_type)
position_attrs.delete(:line_range)
position_attrs
position_attrs.except(:line_range, :ignore_whitespace_change)
end
end

View File

@ -146,6 +146,12 @@
{ "type": "integer" },
{ "type": "string", "maxLength": 10 }
]
},
"ignore_whitespace_change": {
"oneOf": [
{ "type": "null" },
{ "type": "boolean" }
]
}
}
}

View File

@ -3,12 +3,12 @@
= dropdown_tag(_('Select'),
options: { toggle_class: 'js-allowed-to-merge wide',
dropdown_class: 'dropdown-menu-selectable capitalize-header', dropdown_qa_selector: 'allowed_to_merge_dropdown_content', dropdown_testid: 'allowed-to-merge-dropdown',
data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes', qa_selector: 'select_allowed_to_merge_dropdown' }})
data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes', qa_selector: 'allowed_to_merge_dropdown' }})
- content_for :push_access_levels do
.push_access_levels-container
= dropdown_tag(_('Select'),
options: { toggle_class: "js-allowed-to-push js-multiselect wide",
dropdown_class: 'dropdown-menu-selectable capitalize-header', dropdown_qa_selector: 'allowed_to_push_dropdown_content' , dropdown_testid: 'allowed-to-push-dropdown',
data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes', qa_selector: 'select_allowed_to_push_dropdown' }})
data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes', qa_selector: 'allowed_to_push_dropdown' }})
= render 'protected_branches/shared/create_protected_branch', protected_branch_entity: protected_branch_entity

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/366854
milestone: '15.3'
type: development
group: group::tenant scale
default_enabled: false
default_enabled: true

View File

@ -119,6 +119,41 @@ NOTE:
To protect, update, or unprotect an environment, you must have at least the
Maintainer role.
#### Migrate to multiple approval rules
You can migrate a protected environment from unified approval rules to multiple
approval rules. Unified approval rules allow all entities that can deploy to an
environment to approve deployment jobs. To migrate to multiple approval rules,
create a new approval rule for each entity allowed to deploy to the environment.
To migrate with the UI:
1. On the top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, select **Settings > CI/CD**.
1. Expand **Protected environments**.
1. From the **Environment** list, select your environment.
1. For each entity allowed to deploy to the environment:
1. Select **Add approval rules**.
1. In the modal window, select which entity is allowed to approve the
deployment job.
1. Enter the number of required approvals.
1. Select **Save**.
Each deployment requires the specified number of approvals from each entity.
For example, the `Production` environment below requires five total approvals,
and allows deployments from only the group `Very Important Group` and the user
`Administrator`:
![unified approval rules](img/unified_approval_rules_v16_0.png)
To migrate, create rules for the `Very Important Group` and `Administrator`. To
preserve the number of required approvals, set the number of required approvals
for `Very Important Group` to four and `Administrator` to one. The new rules
require `Administrator` to approve every deployment job in `Production`.
![multiple approval rules](img/multiple_approval_rules_v16_0.png)
### Allow self-approval **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/381418) in GitLab 15.8.

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

View File

@ -12,7 +12,7 @@ or open threads, you can prevent it from being accepted before you
[mark it as ready](#mark-merge-requests-as-ready). Flag it as a draft to disable
the **Merge** button until you remove the **Draft** flag:
![Blocked Merge Button](img/draft_blocked_merge_button_v13_10.png)
![Blocked Merge Button](img/merge_request_draft_blocked_v16_0.png)
## Mark merge requests as drafts
@ -42,10 +42,7 @@ When a merge request is ready to be merged, you can remove the `Draft` flag in s
- **Viewing a merge request**: In the upper-right corner of the merge request, select **Mark as ready**.
Users with at least the Developer role
can also scroll to the bottom of the merge request description and select **Mark as ready**:
![Mark as ready](img/draft_blocked_merge_button_v13_10.png)
can also scroll to the bottom of the merge request description and select **Mark as ready**.
- **Editing an existing merge request**: Remove `[Draft]`, `Draft:` or `(Draft)`
from the beginning of the title, or clear **Mark as draft**
below the **Title** field.
@ -71,7 +68,7 @@ draft merge requests:
1. Select **Yes** to include drafts, or **No** to exclude, and press **Return**
to update the list of merge requests:
![Filter draft merge requests](img/filter_draft_merge_requests_v13_10.png)
![Filter draft merge requests](img/filter_draft_merge_requests_v16_0.png)
## Pipelines for drafts

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -93,6 +93,8 @@ or:
To filter the list of merge requests:
1. On the top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, select **Merge requests**.
1. Above the list of merge requests, select **Search or filter results...**.
1. From the dropdown list, select the attribute you wish to filter by. Some examples:
- [**By environment or deployment date**](#by-environment-or-deployment-date).
@ -132,17 +134,15 @@ Projects using a [fast-forward merge method](methods/index.md#fast-forward-merge
do not return results, as this method does not create a merge commit.
When filtering by an environment, a dropdown list presents all environments that
you can choose from:
you can choose from.
![Filter MRs by their environment](img/filtering_merge_requests_by_environment_v14_6.png)
When filtering by `Deployed-before` or `Deployed-after`:
When filtering by `Deployed-before` or `Deployed-after`, the date refers to when
the deployment to an environment (triggered by the merge commit) completed successfully.
You must enter the deploy date manually. Deploy dates
use the format `YYYY-MM-DD`, and must be quoted if you wish to specify
both a date and time (`"YYYY-MM-DD HH:MM"`):
![Filter MRs by a deploy date](img/filtering_merge_requests_by_date_v14_6.png)
- The date refers to when the deployment to an environment (triggered by the
merge commit) completed successfully.
- You must enter the deploy date manually.
- Deploy dates use the format `YYYY-MM-DD`, and must be wrapped in double quotes (`"`)
if you want to specify both a date and time (`"YYYY-MM-DD HH:MM"`).
## Add changes to a merge request
@ -182,7 +182,7 @@ The merge request is added to the user's assigned merge request list.
GitLab enables multiple assignees for merge requests, if multiple people are
accountable for it:
![multiple assignees for merge requests sidebar](img/multiple_assignees_for_merge_requests_sidebar.png)
![multiple assignees for merge requests sidebar](img/merge_request_assignees_v16_0.png)
To assign multiple assignees to a merge request, use the `/assign @user`
[quick action](../quick_actions.md#issues-merge-requests-and-epics) in a text area, or:

View File

@ -13,7 +13,7 @@ If you review a merge request and it's ready to merge, but the pipeline hasn't
completed yet, you can set it to auto-merge. You don't
have to remember later to merge the work manually:
![Auto-merge a merge request](img/mwps_v15_4.png)
![Auto-merge is ready](img/auto_merge_ready_v16_0.png)
NOTE:
[In GitLab 16.0 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/359057), **Merge when pipeline succeeds** and **Add to merge train when pipeline succeeds** are renamed **Set to auto-merge**.

View File

@ -41,7 +41,7 @@ for environments. If it's the first time the branch is deployed, the link
returns a `404` error until done. During the deployment, the stop button is
disabled. If the pipeline fails to deploy, the deployment information is hidden.
![Merge request pipeline](img/merge_request_pipeline.png)
![Merge request pipeline](img/post_merge_pipeline_v16_0.png)
For more information, [read about pipelines](../../../ci/pipelines/index.md).

View File

@ -38,7 +38,7 @@ module API
}
}
} do |note|
note.position.to_h
note.position.to_h.except(:ignore_whitespace_change)
end
end
end

View File

@ -18,7 +18,7 @@ module API
expose :commit_id, if: ->(note, options) { note.noteable_type == "MergeRequest" && note.is_a?(DiffNote) }
expose :position, if: ->(note, options) { note.is_a?(DiffNote) } do |note|
note.position.to_h
note.position.to_h.except(:ignore_whitespace_change)
end
expose :resolvable?, as: :resolvable

View File

@ -22,33 +22,6 @@ module API
end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
namespace ':id/-/packages/npm' do
params do
requires :package_name, type: String, desc: 'Package name'
end
namespace '-/package/*package_name' do
get 'dist-tags', format: false do
not_found!
end
namespace 'dist-tags/:tag' do
put format: false do
not_found!
end
delete format: false do
not_found!
end
end
end
post '-/npm/v1/security/audits/quick' do
not_found!
end
post '-/npm/v1/security/advisories/bulk' do
not_found!
end
include ::API::Concerns::Packages::NpmEndpoints
end
end

View File

@ -3,7 +3,7 @@
module Gitlab
module Database
class TablesTruncate
GITLAB_SCHEMAS_TO_IGNORE = %i[gitlab_geo].freeze
GITLAB_SCHEMAS_TO_IGNORE = %i[gitlab_geo gitlab_embedding].freeze
def initialize(database_name:, min_batch_size:, logger: nil, until_table: nil, dry_run: false)
@database_name = database_name

View File

@ -9,6 +9,7 @@ module Gitlab
attr_reader :base_sha
attr_reader :start_sha
attr_reader :head_sha
attr_reader :ignore_whitespace_change
def initialize(attrs)
if diff_file = attrs[:diff_file]

View File

@ -14,6 +14,7 @@ module Gitlab
@y = attrs[:y]
@width = attrs[:width]
@height = attrs[:height]
@ignore_whitespace_change = false
super(attrs)
end

View File

@ -12,6 +12,7 @@ module Gitlab
@old_line = attrs[:old_line]
@new_line = attrs[:new_line]
@line_range = attrs[:line_range]
@ignore_whitespace_change = !!attrs[:ignore_whitespace_change]
super(attrs)
end
@ -25,7 +26,8 @@ module Gitlab
end
def to_h
super.merge(old_line: old_line, new_line: new_line, line_range: line_range)
super.merge(old_line: old_line, new_line: new_line, line_range: line_range,
ignore_whitespace_change: ignore_whitespace_change)
end
def line_age

View File

@ -19,7 +19,8 @@ module Gitlab
:x,
:y,
:line_range,
:position_type, to: :formatter
:position_type,
:ignore_whitespace_change, to: :formatter
# A position can belong to a text line or to an image coordinate
# it depends of the position_type argument.
@ -69,11 +70,11 @@ module Gitlab
end
def to_json(opts = nil)
Gitlab::Json.generate(formatter.to_h, opts)
Gitlab::Json.generate(to_h.except(:ignore_whitespace_change), opts)
end
def as_json(opts = nil)
to_h.as_json(opts)
to_h.except(:ignore_whitespace_change).as_json(opts)
end
def type
@ -134,7 +135,7 @@ module Gitlab
end
def diff_options
{ paths: paths, expanded: true, include_stats: false }
{ paths: paths, expanded: true, include_stats: false, ignore_whitespace_change: ignore_whitespace_change }
end
def diff_line(repository)

View File

@ -21,6 +21,7 @@ module Gitlab
return unless old_diff_refs&.complete? && new_diff_refs&.complete?
return unless old_position.diff_refs == old_diff_refs
@ignore_whitespace_change = old_position.ignore_whitespace_change
strategy = old_position.on_text? ? LineStrategy : ImageStrategy
strategy.new(self).trace(old_position)
@ -50,7 +51,7 @@ module Gitlab
def compare(start_sha, head_sha, straight: false)
compare = CompareService.new(project, head_sha).execute(project, start_sha, straight: straight)
compare.diffs(paths: paths, expanded: true)
compare.diffs(paths: paths, expanded: true, ignore_whitespace_change: @ignore_whitespace_change)
end
end
end

View File

@ -62,6 +62,8 @@ module Gitlab
# The line number as of D can be found by using the LineMapper on diff C->D
# and providing the line number as of C.
@ignore_whitespace_change = position.ignore_whitespace_change
if position.added?
trace_added_line(position)
elsif position.removed?
@ -189,7 +191,8 @@ module Gitlab
diff_file: diff_file,
old_line: old_line,
new_line: new_line,
line_range: line_range
line_range: line_range,
ignore_whitespace_change: @ignore_whitespace_change
}.compact
Position.new(**params)

View File

@ -1853,9 +1853,6 @@ msgstr ""
msgid "AI actions"
msgstr ""
msgid "AI generated this test"
msgstr ""
msgid "AI-generated test file"
msgstr ""
@ -10236,6 +10233,9 @@ msgstr ""
msgid "ClusterAgents|Event occurred"
msgstr ""
msgid "ClusterAgents|External project"
msgstr ""
msgid "ClusterAgents|Failed to create a token"
msgstr ""
@ -10406,6 +10406,9 @@ msgstr ""
msgid "ClusterAgents|Your instance doesn't have the %{linkStart}GitLab Agent Server (KAS)%{linkEnd} set up. Ask a GitLab Administrator to install it."
msgstr ""
msgid "ClusterAgents|shared"
msgstr ""
msgid "ClusterAgent|User has insufficient permissions to create a token for this project"
msgstr ""
@ -29311,38 +29314,56 @@ msgstr ""
msgid "NamespaceLimits|You must select a namespace and add a reason for excluding it"
msgstr ""
msgid "NamespaceStorageSize|%{namespace_name} contains %{locked_project_count} locked project"
msgid_plural "NamespaceStorageSize|%{namespace_name} contains %{locked_project_count} locked projects"
msgid "NamespaceStorageSize|%{namespace_name} is now read-only. Your ability to write new data to this namespace is restricted. %{read_only_link_start}Which actions are restricted?%{link_end}"
msgstr ""
msgid "NamespaceStorageSize|For more information about storage limits, see our %{faq_link_start}FAQ%{link_end}."
msgstr ""
msgid "NamespaceStorageSize|If %{namespace_name} exceeds the storage quota, your ability to write new data to this namespace will be restricted. %{read_only_link_start}Which actions become restricted?%{link_end}"
msgstr ""
msgid "NamespaceStorageSize|If a project reaches 100%% of the storage quota (%{free_size_limit}) the project will be in a read-only state, and you won't be able to push to your repository or add large files."
msgstr ""
msgid "NamespaceStorageSize|To prevent your projects from being in a read-only state %{manage_storage_link_start}manage your storage usage%{link_end}, or %{purchase_more_link_start}purchase more storage%{link_end}."
msgstr ""
msgid "NamespaceStorageSize|To prevent your projects from being in a read-only state %{manage_storage_link_start}manage your storage usage%{link_end}, or contact a user with the %{group_member_link_start}owner role for this namespace%{link_end} and ask them to %{purchase_more_link_start}purchase more storage%{link_end}."
msgstr ""
msgid "NamespaceStorageSize|To reduce storage usage, reduce git repository and git LFS storage."
msgstr ""
msgid "NamespaceStorageSize|To remove the read-only state %{manage_storage_link_start}manage your storage usage%{link_end}, or %{purchase_more_link_start}purchase more storage%{link_end}."
msgstr ""
msgid "NamespaceStorageSize|To remove the read-only state %{manage_storage_link_start}manage your storage usage%{link_end}, or contact a user with the %{group_member_link_start}owner role for this namespace%{link_end} and ask them to %{purchase_more_link_start}purchase more storage%{link_end}."
msgstr ""
msgid "NamespaceStorageSize|To remove the read-only state, reduce git repository and git LFS storage, or %{purchase_more_link_start}purchase more storage%{link_end}."
msgstr ""
msgid "NamespaceStorageSize|To remove the read-only state, reduce git repository and git LFS storage, or contact a user with the %{group_member_link_start}owner role for this namespace%{link_end} and ask them to %{purchase_more_link_start}purchase more storage%{link_end}."
msgstr ""
msgid "NamespaceStorageSize|You have consumed all available storage and you can't push or add large files to projects over the free tier limit (%{free_size_limit})."
msgstr ""
msgid "NamespaceStorageSize|You have reached the free storage limit of %{free_size_limit} for %{namespace_name}"
msgstr ""
msgid "NamespaceStorageSize|You have reached the free storage limit of %{free_size_limit} on %{readonly_project_count} project"
msgid_plural "NamespaceStorageSize|You have reached the free storage limit of %{free_size_limit} on %{readonly_project_count} projects"
msgstr[0] ""
msgstr[1] ""
msgid "NamespaceStorageSize|%{namespace_name} is now read-only. Projects under this namespace are locked and actions are restricted. %{actions_restricted_link}"
msgstr ""
msgid "NamespaceStorageSize|If %{namespace_name} exceeds the storage quota, all projects in the namespace will be locked and actions will be restricted. %{actions_restricted_link}"
msgstr ""
msgid "NamespaceStorageSize|If you reach 100%% storage capacity, you will not be able to: %{repository_limits_description}"
msgstr ""
msgid "NamespaceStorageSize|Manage your storage usage or, if you are a namespace Owner, purchase additional storage. %{learn_more_link}."
msgstr ""
msgid "NamespaceStorageSize|Please purchase additional storage to unlock your projects over the free %{free_size_limit} project limit. You can't %{repository_limits_description}"
msgstr ""
msgid "NamespaceStorageSize|You have consumed all of your additional storage, please purchase more to unlock your projects over the free %{free_size_limit} limit. You can't %{repository_limits_description}"
msgstr ""
msgid "NamespaceStorageSize|You have reached the free storage limit of %{free_size_limit} on one or more projects"
msgid "NamespaceStorageSize|You have used %{usage_in_percent} of the storage quota for %{namespace_name}"
msgstr ""
msgid "NamespaceStorageSize|You have used %{usage_in_percent} of the storage quota for %{namespace_name} (%{used_storage} of %{storage_limit})"
msgstr ""
msgid "NamespaceStorageSize|push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines. %{learn_more_link}."
msgstr ""
msgid "NamespaceStorage|%{name_with_link} is now read-only. Projects under this namespace are locked and actions are restricted."
msgstr ""
@ -44752,6 +44773,9 @@ msgid_plural "Test coverage: %d hits"
msgstr[0] ""
msgstr[1] ""
msgid "Test generated by AI"
msgstr ""
msgid "Test settings"
msgstr ""
@ -50629,12 +50653,6 @@ msgstr ""
msgid "Which API requests are affected?"
msgstr ""
msgid "Which actions are restricted?"
msgstr ""
msgid "Which actions become restricted?"
msgstr ""
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""

View File

@ -13,7 +13,9 @@ module QA
@attributes[:pattern] ||= selector
options.each do |option|
@attributes[:pattern] = option if option.is_a?(String) || option.is_a?(Regexp)
if option.is_a?(String) || option.is_a?(Regexp)
@attributes[:pattern] = option
end
end
end
@ -26,7 +28,7 @@ module QA
end
def selector_css
%(#{qa_selector},.#{selector})
%Q([data-qa-selector="#{@name}"]#{additional_selectors},.#{selector})
end
def expression
@ -38,19 +40,14 @@ module QA
end
def matches?(line)
!!(line =~ /["']#{name}['"]|["']#{name.to_s.tr('_', '-')}['"]|#{expression}/)
!!(line =~ /["']#{name}['"]|#{expression}/)
end
private
def qa_selector
%([data-testid="#{@name}"]#{additional_selectors},[data-testid="#{@name.to_s.tr('_',
'-')}"]#{additional_selectors},[data-qa-selector="#{@name}"]#{additional_selectors})
end
def additional_selectors
@attributes.dup.delete_if { |attr| attr == :pattern || attr == :required }.map do |key, value|
%([data-qa-#{key.to_s.tr('_', '-')}="#{value}"])
%Q([data-qa-#{key.to_s.tr('_', '-')}="#{value}"])
end.join
end
end

View File

@ -14,10 +14,8 @@ module QA
end
end
def wait_for_markdown_preview(component, content)
return if has_markdown_preview?(component, content)
raise ElementNotFound, %("Couldn't find #{component} element with content '#{content}')
def preview
click_link('Preview')
end
end
end

View File

@ -9,13 +9,15 @@ module QA
view 'app/assets/javascripts/security_configuration/components/app.vue' do
element :security_configuration_container
element :security_view_history_link
element :security_configuration_history_link
end
view 'app/assets/javascripts/security_configuration/components/feature_card.vue' do
element :feature_status
element :dependency_scanning_status, "`${feature.type}_status`" # rubocop:disable QA/ElementWithPattern
element :sast_status, "`${feature.type}_status`" # rubocop:disable QA/ElementWithPattern
element :sast_enable_button, "`${feature.type}_enable_button`" # rubocop:disable QA/ElementWithPattern
element :dependency_scanning_mr_button, "`${feature.type}_mr_button`" # rubocop:disable QA/ElementWithPattern
element :license_scanning_status, "`${feature.type}_status`" # rubocop:disable QA/ElementWithPattern
end
view 'app/assets/javascripts/security_configuration/components/auto_dev_ops_alert.vue' do
@ -23,15 +25,15 @@ module QA
end
def has_security_configuration_history_link?
has_element?(:security_view_history_link)
has_element?(:security_configuration_history_link)
end
def has_no_security_configuration_history_link?
has_no_element?(:security_view_history_link)
has_no_element?(:security_configuration_history_link)
end
def click_security_configuration_history_link
click_element(:security_view_history_link)
click_element(:security_configuration_history_link)
end
def click_sast_enable_button
@ -42,20 +44,40 @@ module QA
click_element(:dependency_scanning_mr_button)
end
def has_true_sast_status?
has_element?(:feature_status, feature: 'sast_true_status')
def has_sast_status?(status_text)
within_element(:sast_status) do
has_text?(status_text)
end
end
def has_false_sast_status?
has_element?(:feature_status, feature: 'sast_false_status')
def has_no_sast_status?(status_text)
within_element(:sast_status) do
has_no_text?(status_text)
end
end
def has_true_dependency_scanning_status?
has_element?(:feature_status, feature: 'dependency_scanning_true_status')
def has_dependency_scanning_status?(status_text)
within_element(:dependency_scanning_status) do
has_text?(status_text)
end
end
def has_false_dependency_scanning_status?
has_element?(:feature_status, feature: 'dependency_scanning_false_status')
def has_no_dependency_scanning_status?(status_text)
within_element(:dependency_scanning_status) do
has_no_text?(status_text)
end
end
def has_license_compliance_status?(status_text)
within_element(:license_scanning_status) do
has_text?(status_text)
end
end
def has_no_license_compliance_status?(status_text)
within_element(:license_scanning_status) do
has_no_text?(status_text)
end
end
def has_auto_devops_container?

View File

@ -11,9 +11,9 @@ module QA
end
view 'app/views/protected_branches/_create_protected_branch.html.haml' do
element :select_allowed_to_push_dropdown
element :allowed_to_push_dropdown
element :allowed_to_push_dropdown_content
element :select_allowed_to_merge_dropdown
element :allowed_to_merge_dropdown
element :allowed_to_merge_dropdown_content
end
@ -45,7 +45,7 @@ module QA
private
def select_allowed(action, allowed)
click_element :"select_allowed_to_#{action}_dropdown"
click_element :"allowed_to_#{action}_dropdown"
allowed[:roles] = Resource::ProtectedBranch::Roles::NO_ONE unless allowed.key?(:roles)

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
# tagged transient due to feature-flag caching flakiness. Remove tag along with feature flag removal.
module QA
RSpec.describe 'Create', feature_flag: { name: 'source_editor_toolbar', scope: :global } do
RSpec.describe 'Create' do
describe 'Source editor toolbar preview', product_group: :source_code do
let(:project) do
Resource::Project.fabricate_via_api! do |project|
@ -13,14 +13,9 @@ module QA
let(:edited_readme_content) { 'Here is the edited content.' }
before do
Runtime::Feature.enable(:source_editor_toolbar)
Flow::Login.sign_in
end
after do
Runtime::Feature.disable(:source_editor_toolbar)
end
it 'can preview markdown side-by-side while editing',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/367749' do
project.visit!
@ -30,16 +25,11 @@ module QA
Page::File::Show.perform(&:click_edit)
# wait_until required due to feature_caching. Remove along with feature flag removal.
Page::File::Edit.perform do |file|
Support::Waiter.wait_until(sleep_interval: 2, max_duration: 60, reload_page: page,
retry_on_exception: true) do
expect(file).to have_element(:editor_toolbar_button)
end
file.remove_content
file.click_editor_toolbar
file.add_content('# ' + edited_readme_content)
file.wait_for_markdown_preview('h1', edited_readme_content)
file.preview
expect(file.has_markdown_preview?('h1', edited_readme_content)).to be true
file.commit_changes
end

View File

@ -73,7 +73,7 @@ RSpec.describe QA::Page::Element do
subject { described_class.new(:something, /link_to 'something'/) }
it 'has an attribute[pattern] of the pattern' do
expect(subject.attributes[:pattern]).to eq(/link_to 'something'/)
expect(subject.attributes[:pattern]).to eq /link_to 'something'/
end
it 'is not required by default' do
@ -98,7 +98,7 @@ RSpec.describe QA::Page::Element do
subject { described_class.new(:something, /link_to 'something_else_entirely'/, required: true) }
it 'has an attribute[pattern] of the passed pattern' do
expect(subject.attributes[:pattern]).to eq(/link_to 'something_else_entirely'/)
expect(subject.attributes[:pattern]).to eq /link_to 'something_else_entirely'/
end
it 'is required' do
@ -118,10 +118,6 @@ RSpec.describe QA::Page::Element do
expect(subject.selector_css).to include(%q([data-qa-selector="my_element"]))
end
it 'properly translates to a data-testid' do
expect(subject.selector_css).to include(%q([data-testid="my_element"]))
end
context 'additional selectors' do
let(:element) { described_class.new(:my_element, index: 3, another_match: 'something') }
let(:required_element) { described_class.new(:my_element, required: true, index: 3) }

View File

@ -0,0 +1,49 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'User comments on a diff with whitespace changes', :js, feature_category: :code_review_workflow do
include MergeRequestDiffHelpers
let_it_be(:project) { create(:project, :repository) }
let(:merge_request) do
create(:merge_request_with_diffs, source_project: project, target_project: project,
source_branch: 'changes-with-whitespace')
end
let(:user) { create(:user) }
before do
project.add_maintainer(user)
sign_in(user)
visit(diffs_project_merge_request_path(project, merge_request, view: 'parallel'))
end
context 'when hiding whitespace changes' do
before do
find('.js-show-diff-settings').click
find('[data-testid="show-whitespace"]').click
wait_for_requests
end
it 'allows commenting on line combinations that are not present in the real diff' do
# Comment on line combination old: 19, new 20
# This line combination does not exist when whitespace is shown
click_diff_line(
find_by_scrolling('div[data-path="files/ruby/popen.rb"] .left-side a[data-linenumber="19"]').find(:xpath,
'../..'), 'left')
page.within('.js-discussion-note-form') do
fill_in(:note_note, with: 'Comment on diff with whitespace')
click_button('Add comment now')
end
wait_for_requests
page.within('.notes_holder') do
expect(page).to have_content('Comment on diff with whitespace')
end
end
end
end

View File

@ -1,4 +1,4 @@
import { GlLink, GlIcon } from '@gitlab/ui';
import { GlLink, GlIcon, GlBadge, GlTable, GlPagination } from '@gitlab/ui';
import { sprintf } from '~/locale';
import AgentTable from '~/clusters_list/components/agent_table.vue';
import DeleteAgentButton from '~/clusters_list/components/delete_agent_button.vue';
@ -17,6 +17,7 @@ const provideData = {
};
const defaultProps = {
agents: clusterAgents,
maxAgents: null,
};
const DeleteAgentButtonStub = stubComponent(DeleteAgentButton, {
@ -39,7 +40,11 @@ describe('AgentTable', () => {
const findAgentId = (at) => wrapper.findAllByTestId('cluster-agent-id').at(at);
const findConfiguration = (at) =>
wrapper.findAllByTestId('cluster-agent-configuration-link').at(at);
const findDeleteAgentButton = () => wrapper.findAllComponents(DeleteAgentButton);
const findDeleteAgentButtons = () => wrapper.findAllComponents(DeleteAgentButton);
const findTableRow = (at) => wrapper.findComponent(GlTable).find('tbody').findAll('tr').at(at);
const findSharedBadgeByRow = (at) => findTableRow(at).findComponent(GlBadge);
const findDeleteAgentButtonByRow = (at) => findTableRow(at).findComponent(DeleteAgentButton);
const findPagination = () => wrapper.findComponent(GlPagination);
const createWrapper = ({ provide = provideData, propsData = defaultProps } = {}) => {
wrapper = mountExtended(AgentTable, {
@ -64,6 +69,11 @@ describe('AgentTable', () => {
`('displays agent link for $agentName', ({ agentName, link, lineNumber }) => {
expect(findAgentLink(lineNumber).text()).toBe(agentName);
expect(findAgentLink(lineNumber).attributes('href')).toBe(link);
expect(findSharedBadgeByRow(lineNumber).exists()).toBe(false);
});
it('displays "shared" badge if the agent is shared', () => {
expect(findSharedBadgeByRow(9).text()).toBe(I18N_AGENT_TABLE.sharedBadgeText);
});
it.each`
@ -116,8 +126,9 @@ describe('AgentTable', () => {
},
);
it('displays actions menu for each agent', () => {
expect(findDeleteAgentButton()).toHaveLength(clusterAgents.length);
it('displays actions menu for each agent except the shared agents', () => {
expect(findDeleteAgentButtons()).toHaveLength(clusterAgents.length - 1);
expect(findDeleteAgentButtonByRow(9).exists()).toBe(false);
});
});
@ -132,6 +143,7 @@ describe('AgentTable', () => {
${6} | ${'14.8.0'} | ${'15.0.0'} | ${false} | ${true} | ${outdatedTitle}
${7} | ${'14.8.0'} | ${'15.0.0-rc1'} | ${false} | ${true} | ${outdatedTitle}
${8} | ${'14.8.0'} | ${'14.8.10'} | ${false} | ${false} | ${''}
${9} | ${''} | ${'14.8.0'} | ${false} | ${false} | ${''}
`(
'when agent version is "$agentVersion", KAS version is "$kasVersion" and version mismatch is "$versionMismatch"',
({ agentMockIdx, agentVersion, kasVersion, versionMismatch, versionOutdated, title }) => {
@ -181,5 +193,32 @@ describe('AgentTable', () => {
}
},
);
describe('pagination', () => {
it('should not render pagination buttons when there are no additional pages', () => {
createWrapper();
expect(findPagination().exists()).toBe(false);
});
it('should render pagination buttons when there are additional pages', () => {
createWrapper({
propsData: { agents: [...clusterAgents, ...clusterAgents, ...clusterAgents] },
});
expect(findPagination().exists()).toBe(true);
});
it('should not render pagination buttons when maxAgents is passed from the parent component', () => {
createWrapper({
propsData: {
agents: [...clusterAgents, ...clusterAgents, ...clusterAgents],
maxAgents: 6,
},
});
expect(findPagination().exists()).toBe(false);
});
});
});
});

View File

@ -1,4 +1,4 @@
import { GlAlert, GlKeysetPagination, GlLoadingIcon, GlBanner } from '@gitlab/ui';
import { GlAlert, GlLoadingIcon, GlBanner } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue';
@ -19,6 +19,7 @@ Vue.use(VueApollo);
describe('Agents', () => {
let wrapper;
let testDate = new Date();
const defaultProps = {
defaultBranchName: 'default',
@ -31,9 +32,9 @@ describe('Agents', () => {
props = {},
glFeatures = {},
agents = [],
pageInfo = null,
ciAccessAuthorizedAgentsNodes = [],
userAccessAuthorizedAgentsNodes = [],
trees = [],
count = 0,
queryResponse = null,
}) => {
const provide = provideData;
@ -43,12 +44,16 @@ describe('Agents', () => {
id: '1',
clusterAgents: {
nodes: agents,
pageInfo,
connections: { nodes: [] },
tokens: { nodes: [] },
count,
},
repository: { tree: { trees: { nodes: trees, pageInfo } } },
ciAccessAuthorizedAgents: {
nodes: ciAccessAuthorizedAgentsNodes,
},
userAccessAuthorizedAgents: {
nodes: userAccessAuthorizedAgentsNodes,
},
repository: { tree: { trees: { nodes: trees } } },
},
},
};
@ -78,7 +83,6 @@ describe('Agents', () => {
const findAgentTable = () => wrapper.findComponent(AgentTable);
const findEmptyState = () => wrapper.findComponent(AgentEmptyState);
const findPaginationButtons = () => wrapper.findComponent(GlKeysetPagination);
const findAlert = () => wrapper.findComponent(GlAlert);
const findBanner = () => wrapper.findComponent(GlBanner);
@ -87,13 +91,13 @@ describe('Agents', () => {
});
describe('when there is a list of agents', () => {
let testDate = new Date();
const agents = [
{
__typename: 'ClusterAgent',
id: '1',
name: 'agent-1',
webPath: '/agent-1',
createdAt: testDate,
connections: null,
tokens: null,
},
@ -102,6 +106,7 @@ describe('Agents', () => {
id: '2',
name: 'agent-2',
webPath: '/agent-2',
createdAt: testDate,
connections: null,
tokens: {
nodes: [
@ -113,8 +118,26 @@ describe('Agents', () => {
},
},
];
const count = 2;
const ciAccessAuthorizedAgentsNodes = [
{
agent: {
__typename: 'ClusterAgent',
id: '3',
name: 'ci-agent-1',
webPath: 'shared-project/agent-1',
createdAt: testDate,
connections: null,
tokens: null,
},
},
];
const userAccessAuthorizedAgentsNodes = [
{
agent: {
...agents[0],
},
},
];
const trees = [
{
@ -156,10 +179,26 @@ describe('Agents', () => {
],
},
},
{
id: '3',
name: 'ci-agent-1',
configFolder: undefined,
webPath: 'shared-project/agent-1',
status: 'unused',
isShared: true,
lastContact: null,
connections: null,
tokens: null,
},
];
beforeEach(() => {
return createWrapper({ agents, count, trees });
return createWrapper({
agents,
ciAccessAuthorizedAgentsNodes,
userAccessAuthorizedAgentsNodes,
trees,
});
});
it('should render agent table', () => {
@ -172,7 +211,7 @@ describe('Agents', () => {
});
it('should emit agents count to the parent component', () => {
expect(wrapper.emitted().onAgentsLoad).toEqual([[count]]);
expect(wrapper.emitted().onAgentsLoad).toEqual([[expectedAgentsList.length]]);
});
describe.each`
@ -192,7 +231,7 @@ describe('Agents', () => {
localStorage.setItem(AGENT_FEEDBACK_KEY, true);
}
return createWrapper({ glFeatures, agents, count, trees });
return createWrapper({ glFeatures, agents, trees });
});
it(`should ${bannerShown ? 'show' : 'hide'} the feedback banner`, () => {
@ -206,7 +245,7 @@ describe('Agents', () => {
showGitlabAgentFeedback: true,
};
beforeEach(() => {
return createWrapper({ glFeatures, agents, count, trees });
return createWrapper({ glFeatures, agents, trees });
});
it('should render the correct title', () => {
@ -238,51 +277,6 @@ describe('Agents', () => {
expect(findAgentTable().props('agents')).toMatchObject(expectedAgentsList);
});
});
it('should not render pagination buttons when there are no additional pages', () => {
expect(findPaginationButtons().exists()).toBe(false);
});
describe('when the list has additional pages', () => {
const pageInfo = {
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'prev',
endCursor: 'next',
};
beforeEach(() => {
return createWrapper({
agents,
pageInfo: {
...pageInfo,
__typename: 'PageInfo',
},
});
});
it('should render pagination buttons', () => {
expect(findPaginationButtons().exists()).toBe(true);
});
it('should pass pageInfo to the pagination component', () => {
expect(findPaginationButtons().props()).toMatchObject(pageInfo);
});
describe('when limit is passed from the parent component', () => {
beforeEach(() => {
return createWrapper({
props: { limit: 6 },
agents,
pageInfo,
});
});
it('should not render pagination buttons', () => {
expect(findPaginationButtons().exists()).toBe(false);
});
});
});
});
describe('when the agent list is empty', () => {
@ -302,7 +296,10 @@ describe('Agents', () => {
describe('when agents query has errored', () => {
beforeEach(() => {
return createWrapper({ agents: null });
createWrapper({
queryResponse: jest.fn().mockRejectedValue({}),
});
return waitForPromises();
});
it('displays an alert message', () => {

View File

@ -8,7 +8,7 @@ import deleteAgentMutation from '~/clusters_list/graphql/mutations/delete_agent.
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import DeleteAgentButton from '~/clusters_list/components/delete_agent_button.vue';
import { MAX_LIST_COUNT, DELETE_AGENT_BUTTON } from '~/clusters_list/constants';
import { DELETE_AGENT_BUTTON } from '~/clusters_list/constants';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { getAgentResponse, mockDeleteResponse, mockErrorDeleteResponse } from '../mocks/apollo';
@ -16,7 +16,6 @@ Vue.use(VueApollo);
const projectPath = 'path/to/project';
const defaultBranchName = 'default';
const maxAgents = MAX_LIST_COUNT;
const agent = {
id: 'agent-id',
name: 'agent-name',
@ -53,8 +52,6 @@ describe('DeleteAgentButton', () => {
variables: {
projectPath,
defaultBranchName,
first: maxAgents,
last: null,
},
data: getAgentResponse.data,
});
@ -71,7 +68,6 @@ describe('DeleteAgentButton', () => {
};
const propsData = {
defaultBranchName,
maxAgents,
agent,
};

View File

@ -205,4 +205,14 @@ export const clusterAgents = [
],
},
},
{
name: 'ci-agent-1',
id: '3',
webPath: 'shared-project/agent-1',
status: 'inactive',
lastContact: connectedTimeInactive.getTime(),
isShared: true,
connections: null,
tokens: null,
},
];

View File

@ -3,6 +3,7 @@ const agent = {
id: 'agent-id',
name: 'agent-name',
webPath: 'agent-webPath',
createdAt: new Date(),
};
const token = {
id: 'token-id',
@ -14,13 +15,6 @@ const tokens = {
const connections = {
nodes: [],
};
const pageInfo = {
endCursor: '',
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
};
const count = 1;
export const createAgentResponse = {
data: {
@ -73,10 +67,12 @@ export const getAgentResponse = {
project: {
__typename: 'Project',
id: 'project-1',
clusterAgents: { nodes: [{ ...agent, connections, tokens }], pageInfo, count },
clusterAgents: { nodes: [{ ...agent, connections, tokens }] },
ciAccessAuthorizedAgents: { nodes: [] },
userAccessAuthorizedAgents: { nodes: [] },
repository: {
tree: {
trees: { nodes: [{ ...agent, path: null }], pageInfo },
trees: { nodes: [{ ...agent, path: null }] },
},
},
},

View File

@ -140,6 +140,7 @@ describe('DiffsStoreUtils', () => {
old_line: options.noteTargetLine.old_line,
new_line: options.noteTargetLine.new_line,
line_range: options.lineRange,
ignore_whitespace_change: true,
});
const postData = {
@ -198,6 +199,7 @@ describe('DiffsStoreUtils', () => {
position_type: TEXT_DIFF_POSITION_TYPE,
old_line: options.noteTargetLine.old_line,
new_line: options.noteTargetLine.new_line,
ignore_whitespace_change: true,
});
const postData = {

View File

@ -7,12 +7,14 @@ RSpec.describe API::Entities::DraftNote, feature_category: :code_review_workflow
let_it_be(:json) { entity.as_json }
it 'exposes correct attributes' do
position = entity.position.to_h.except(:ignore_whitespace_change)
expect(json["id"]).to eq entity.id
expect(json["author_id"]).to eq entity.author_id
expect(json["merge_request_id"]).to eq entity.merge_request_id
expect(json["resolve_discussion"]).to eq entity.resolve_discussion
expect(json["discussion_id"]).to eq entity.discussion_id
expect(json["note"]).to eq entity.note
expect(json["position"].transform_keys(&:to_sym)).to eq entity.position.to_h
expect(json["position"].transform_keys(&:to_sym)).to eq position
end
end

View File

@ -10,7 +10,8 @@ RSpec.describe Gitlab::Diff::Formatters::TextFormatter do
head_sha: 789,
old_path: 'old_path.txt',
new_path: 'new_path.txt',
line_range: nil
line_range: nil,
ignore_whitespace_change: false
}
end

View File

@ -18,7 +18,7 @@ RSpec.describe Gitlab::Diff::PositionTracer do
let(:project) { double }
let(:old_diff_refs) { diff_refs }
let(:new_diff_refs) { diff_refs }
let(:position) { double(on_text?: on_text?, diff_refs: diff_refs) }
let(:position) { double(on_text?: on_text?, diff_refs: diff_refs, ignore_whitespace_change: false) }
let(:tracer) { double }
context 'position is on text' do

View File

@ -1454,7 +1454,7 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
it "returns the number of commits in the whole repository" do
options = { all: true }
expect(repository.count_commits(options)).to eq(315)
expect(repository.count_commits(options)).to eq(316)
end
end

View File

@ -160,7 +160,8 @@ RSpec.describe Gitlab::GithubImport::Importer::DiffNoteImporter, :aggregate_fail
new_path: file_path,
old_path: file_path,
position_type: 'text',
line_range: nil
line_range: nil,
ignore_whitespace_change: false
})
expect(note.note)
.to eq <<~NOTE

View File

@ -104,7 +104,8 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote, :clean_gitlab_red
old_line: nil,
old_path: 'README.md',
position_type: 'text',
start_sha: 'start'
start_sha: 'start',
ignore_whitespace_change: false
)
end
end
@ -122,7 +123,8 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote, :clean_gitlab_red
new_line: nil,
old_path: 'README.md',
position_type: 'text',
start_sha: 'start'
start_sha: 'start',
ignore_whitespace_change: false
)
end
end

View File

@ -153,46 +153,32 @@ RSpec.describe API::NpmGroupPackages, feature_category: :package_registry do
end
describe 'GET /api/v4/packages/npm/-/package/*package_name/dist-tags' do
let(:url) { api("/groups/#{group.id}/-/packages/npm/-/package/#{package_name}/dist-tags") }
subject { get(url) }
it_behaves_like 'returning response status', :not_found
it_behaves_like 'handling get dist tags requests', scope: :group do
let(:url) { api("/groups/#{group.id}/-/packages/npm/-/package/#{package_name}/dist-tags") }
end
end
describe 'PUT /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do
let(:tag_name) { 'test' }
let(:headers) { build_token_auth_header(personal_access_token.token) }
let(:url) { api("/groups/#{group.id}/-/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
subject { put(url, headers: headers) }
it_behaves_like 'returning response status', :not_found
it_behaves_like 'handling create dist tag requests', scope: :group do
let(:url) { api("/groups/#{group.id}/-/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
end
end
describe 'DELETE /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do
let(:tag_name) { 'test' }
let(:headers) { build_token_auth_header(personal_access_token.token) }
let(:url) { api("/groups/#{group.id}/-/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
subject { delete(url, headers: headers) }
it_behaves_like 'returning response status', :not_found
it_behaves_like 'handling delete dist tag requests', scope: :group do
let(:url) { api("/groups/#{group.id}/-/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
end
end
describe 'POST /api/v4/groups/:id/-/packages/npm/-/npm/v1/security/advisories/bulk' do
let(:url) { api("/groups/#{group.id}/-/packages/npm/-/npm/v1/security/advisories/bulk") }
subject { post(url) }
it_behaves_like 'returning response status', :not_found
it_behaves_like 'handling audit request', path: 'advisories/bulk', scope: :group do
let(:url) { api("/groups/#{group.id}/-/packages/npm/-/npm/v1/security/advisories/bulk") }
end
end
describe 'POST /api/v4/groups/:id/-/packages/npm/-/npm/v1/security/audits/quick' do
let(:url) { api("/groups/#{group.id}/-/packages/npm/-/npm/v1/security/audits/quick") }
subject { post(url) }
it_behaves_like 'returning response status', :not_found
it_behaves_like 'handling audit request', path: 'audits/quick', scope: :group do
let(:url) { api("/groups/#{group.id}/-/packages/npm/-/npm/v1/security/audits/quick") }
end
end
end

View File

@ -93,7 +93,8 @@ module TestEnv
'gitaly-rename-test' => '94bb47c',
'smime-signed-commits' => 'ed775cc',
'Ääh-test-utf-8' => '7975be0',
'ssh-signed-commit' => '7b5160f'
'ssh-signed-commit' => '7b5160f',
'changes-with-whitespace' => 'f2d141fadb33ceaafc95667c1a0a308ad5edc5f9'
}.freeze
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
RSpec.shared_examples 'delegates AI request to Workhorse' do |provider_flag|
RSpec.shared_examples 'behind AI related feature flags' do |provider_flag|
context "when #{provider_flag} is disabled" do
before do
stub_feature_flags(provider_flag => false)
@ -24,7 +24,9 @@ RSpec.shared_examples 'delegates AI request to Workhorse' do |provider_flag|
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
RSpec.shared_examples 'delegates AI request to Workhorse' do
it 'responds with Workhorse send-url headers' do
post api(url, current_user), params: input_params

View File

@ -18,10 +18,12 @@ RSpec.shared_examples 'diff discussions API' do |parent_type, noteable_type, id_
it "returns a discussion by id" do
get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions/#{diff_note.discussion_id}", user)
position = diff_note.position.to_h.except(:ignore_whitespace_change)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(diff_note.discussion_id)
expect(json_response['notes'].first['body']).to eq(diff_note.note)
expect(json_response['notes'].first['position']).to eq(diff_note.position.to_h.stringify_keys)
expect(json_response['notes'].first['position']).to eq(position.stringify_keys)
expect(json_response['notes'].first['line_range']).to eq(nil)
end
end
@ -39,7 +41,7 @@ RSpec.shared_examples 'diff discussions API' do |parent_type, noteable_type, id_
}
}
position = diff_note.position.to_h.merge({ line_range: line_range })
position = diff_note.position.to_h.merge({ line_range: line_range }).except(:ignore_whitespace_change)
post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user),
params: { body: 'hi!', position: position }

View File

@ -449,7 +449,7 @@ RSpec.shared_examples 'handling audit request' do |path:, scope: :project|
project.send("add_#{user_role}", user) if user_role
project.update!(visibility: visibility.to_s)
if scope == :instance
if %i[instance group].include?(scope)
allow_fetch_application_setting(attribute: "npm_package_requests_forwarding", return_value: request_forward)
else
allow_fetch_cascade_application_setting(attribute: "npm_package_requests_forwarding", return_value: request_forward)
@ -459,7 +459,7 @@ RSpec.shared_examples 'handling audit request' do |path:, scope: :project|
example_name = "#{params[:expected_result]} audit request"
status = params[:expected_status]
if scope == :instance && params[:expected_status] != :unauthorized
if %i[instance group].include?(scope) && params[:expected_status] != :unauthorized
if params[:request_forward]
example_name = 'redirect audit request'
status = :temporary_redirect
@ -638,6 +638,8 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project|
status = :not_found
end
status = :not_found if scope == :group && params[:package_name_type] == :non_existing
it_behaves_like example_name, status: status
end
end
@ -854,6 +856,8 @@ RSpec.shared_examples 'handling different package names, visibilities and user r
status = params[:auth].nil? ? :unauthorized : :not_found
end
status = :not_found if scope == :group && params[:package_name_type] == :non_existing && params[:auth].present?
it_behaves_like example_name, status: status
end
end